提要
首先注意,本文探讨的不是分布式事务,请读者注意区分!
在我们的日常开发种,分布式锁和spring事务是常见的两种控制数据一致性的方式。
分布式锁和spring事务各自的作用就不做阐述了,不是本文重点,本文重点阐述一下锁释放和事务提交的问题。
不一致的场景
下面这个场景是抽象出来的一个很简单的场景
我们业务种经常会遇到,比如线程A和线程B同时调用test0
假如A先获取到锁,在test1中更新了x字段
在B获取到锁的时候,拿到的x的值是符合预期的么?
@SneakyThrows
@Override
public void test0() {CommonServiceImpl self = (CommonServiceImpl) AopContext.currentProxy();log.info("test0----------------------");self.test1();self.test2();
}@SneakyThrows
@Override
public void test1() {try {// 这里就是普通的一个加锁和释放锁的方法,在两个方法内分别打印个加锁和释放锁的日志LockUtil.acquireLock("test1");log.info("test1----------------------更新数据库里的某个字段x");Thread.sleep(5000);} finally {LockUtil.releaseLock("test1");}
}@SneakyThrows
@Override
public void test2() {log.info("test2----------------------做一些其他无关紧要的查询");Thread.sleep(5000);
}
显然是不符合预期的,B线程拿到的x大概率是A更新前的,如果这里是一些库存之类的场景,很可能就出现超卖的现象。
至于为什么,我们可以看下运行的打印结果
原因就在于锁的释放是在事务提交之前(当然这里的前提是这三个方法在一个事务中),B在拿到锁去查询x的值的时候,A虽然已经更新完了x的值并且释放了锁,但是事务还没来得及提交,所以B拿到的仍然是旧的值,此时去更新就会出问题。
那么什么情况下不会出问题呢?
- B线程获取到锁,去查询x的值的时候,A线程中test2的方法刚好执行完提交了事务,这个时候查询的值就是修改后正确的值(显然我们开发不能碰运气)
- 这几个方法都没有事务,B只要获取到锁,那么拿到的就是最新的数据(这种场景是存在的,而且我们开发中也提倡没必要用事务的场景就不需要用,需要的场景尽可能用小事务,当然这是开发之前就考虑的问题了)
- 某些数据库的事务隔离机制可能不存在这种场景(略有耳闻,但我确实没见过)
- 锁加在事务之外的时候,这样也可以保证锁的释放在事务提交之后(但是有时候避免不了事务传播和嵌套)
- 还有些场景,比如上面的代码中,哪怕test0只调用了test1(),也有可能出现上述场景,因为上面的锁本质上还是加在事务里面的,虽然它锁的内容包含了整个方法,但是遇到一些极端情况,比如数据库性能不太好的时候,就会出现线程B拿到锁的时候A的事务还没提交。
所以,我们还是需要考虑,如果真存在这种场景,我们该怎么处理?
通过事务同步管理器手动控制
大家可以看下代码有什么区别
@SneakyThrows
@Override
public void test0() {CommonServiceImpl self = (CommonServiceImpl) AopContext.currentProxy();log.info("test0----------------------");self.test1();self.test2();
}@SneakyThrows
@Override
public void test1() {try {LockUtil.acquireLock("test1");log.info("test1----------------------更新数据库里的某个字段x");Thread.sleep(5000);} finally {LockUtil.unlockAfterTransaction("test1");}
}@SneakyThrows
@Override
public void test2() {log.info("test2----------------------做一些其他无关紧要的查询");Thread.sleep(5000);
}
先来上运行结果
很显然,这里的释放锁过程跑到了test2执行完成之后,也就是事务提交之后。
这样就保证了B拿到锁的时候,A的事务是已提交状态
这里主要就是把
LockUtil.releaseLock(“test1”)
换成了
LockUtil.unlockAfterTransaction(“test1”)
public static void unlockAfterTransaction(String lockName) {//事物完成后释放锁TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {super.afterCompletion(status);releaseLock(lockName);}});}
就是把之前释放锁的方法包一层,通过事务同步管理器加个判断
事务同步管理器的其他用处
除了确保锁释放和事务提交的时机之外,事务同步管理器还可以有其他作用,比如你在一个方法中调用异步方法,当前事务提交成功之后调用和立刻调用就,结果可能完全不同,下面是一个简单的示例:
立刻调用
public void fireAsynchronous(final String type, final Object... parameters) {this.asyncEventListenerExecutor.execute(new Runnable() {public void run() {EventManager.this.fire(type, parameters);}});}
事务成功提交之后再调用
public void fireSyncOnTransactionSuccess(final String type, final Object... parameters) {TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {public void afterCommit() {EventManager.this.fire(type, parameters);}});}