導讀:
最近發現某個項目的用戶流水和帳款金額出現了併發問題, 而後使用樂觀鎖解決了這個問題, 可是由於有跑批任務 在同一時刻 同一用戶的帳款 會增長多條流水因而就出現:redis
的異常, 雖然 帳款的問題是解決了 (單筆沒問題後面的批處理仍是會把失敗的流水繼續推過來) 但仍是不是一個比較好且簡單的解, 開動腦筋,在不改動主要業務邏輯的狀況下如何對其打補丁呢?
最簡單的方式就是將並行改成串行 而後就在數據庫查詢修改的方法外面加了synchronized 關鍵字, 但加上以後並行查詢的數據仍是老數據 synchronized 竟然失效了, 不該該啊, 而後想到了應該是事務未提交致使的,可是方法上有事務註解啊, 接着想到了 事務是經過動態代理實現的顯然動態代理的方法並無synchronized關鍵字修飾.
這是一個例子:spring
@Service class WalletApplicationSerive{ @Transactional(rollbackFor = Exception.class) public synchronized void pay(accountId, amount, outerCode){ // 數據庫查詢以後修改 } }
當咱們使用事務的時候 其背後的實現是動態代理結合IOC容器獲取出來的WalletApplicationSerive 的實例已經被Spring 換成了(spring 實現的更復雜, 爲了方便理解這裏以靜態代理爲例 ,這只是一個簡單的示例)數據庫
class WalletApplicationSeriveProxy{ private WalletApplicationSerivce tagert; public void pay(accountId, amount, outerCode){ tx.begin() try{ tagert.pay(accountId, amount, outerCode) }catch(Exception e){ tx.rollback() throw e; } tx.commit() } }
動態代理:併發
// 目標對象 Object target ; Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 帶有@Transcational註解的方法都會被攔截 tx.begin(); try{ method.invoke(target, args); }catch(Exception e){ tx.rollback(); throw e; } tx.commit(); return null; } });
一切都變得簡單了 這也就是synchronized與ReentrantLock在spring 事務中失效了的緣由.
要如何解決呢? 很簡單 在 代理類外面增長上事務.ide
@Service class WalletApplicationSerive{ @Autowired InnerWalletApplicationSerive inner; public synchronized void pay(accountId, amount, outerCode){ inner.pay(accountId, amount, outerCode) } @Service static class InnerWalletApplicationSerive{ @Transactional(rollbackFor = Exception.class) public void pay(accountId, amount, outerCode){ // 數據庫查詢以後修改 } } }
問題解決, 但這裏鎖的粒度太粗了, 能夠在對鎖進行更細的粒度改造:ui
@Service class WalletApplicationSerive{ @Autowired InnerWalletApplicationSerive inner; public void pay(accountId, amount, outerCode){ synchronized(WalletLockManger.getLock(accountId)){ inner.pay(accountId, amount, outerCode) } } @Service static class InnerWalletApplicationSerive{ @Transactional(rollbackFor = Exception.class) public void pay(accountId, amount, outerCode){ // 數據庫查詢以後修改 } } } class WalletLockManger { private static final Map<String, String> lockMap = new ConcurrentHashMap<>(); public static String getLock(String accountId) { return lockMap.computeIfAbsent(accountId, Function.identity()); } }
synchronized(WalletLockManger.getLock(accountId)) 這個裏有很大的改造空間, 後面若是 要部署多個實例的時候 能夠將這裏換成redis的鎖.代理