导读:
最近发现某个项目的用户流水和账款金额出现了并发问题, 然后使用乐观锁解决了这个问题, 但是因为有跑批任务 在同一时刻 同一用户的账款 会增加多条流水于是就出现:

  • StaleObjectStateException,
  • ObjectOptimisticLockingFailureException,
  • CannotAcquireLockException

的异常, 虽然 账款的问题是解决了 (单笔没问题后面的批处理还是会把失败的流水继续推过来) 但还是不是一个比较好且简单的解, 开动脑筋,在不改动主要业务逻辑的情况下如何对其打补丁呢?
最简单的方式就是将并行改为串行 然后就在数据库查询修改的方法外面加了synchronized 关键字, 但加上之后并行查询的数据还是老数据 synchronized 居然失效了, 不应该啊, 然后想到了应该是事务未提交导致的,但是方法上有事务注解啊, 接着想到了 事务是通过动态代理实现的显然动态代理的方法并没有synchronized关键字修饰.
这是一个例子:

@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 事务中失效了的原因.
要如何解决呢? 很简单 在 代理类外面增加上事务.

@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){
           // 数据库查询之后修改
        }
    }
}

问题解决, 但这里锁的粒度太粗了, 可以在对锁进行更细的粒度改造:

@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的锁.


yangrd
1.3k 声望225 粉丝

代码改变世界,知行合一。