前提
昨天发生了一件奇怪的事情,我们校车订票系统出现一笔订单,没有正常扣费的现象,查看日志也没发现异常错误,其他数据也是正常的,这导致我们很疑惑了。这时一位经验老道的程序员马上发现的关键点,出来订单的接口同时被调用了,A线程更新钱包,B线程也同时更新钱包,导致A线程更新的钱包数据丢失了。
synchronized 锁强行让线程数据同步
A线程,B线程同时获取钱包,导致线程之间的钱包的可见性为零,那么只要在更新钱包的代码块在一把锁就可以了,当A线程获取锁的时候,B线程只能等待A线程释放锁,才能执行完美解决的这个问题了。 伪代码
public void synchronized updateWallet(Long userId){
getWallet(userId);//获取钱包
do something;//处理扣费逻辑,优惠逻辑等
updateWalletByUserId(userId);//更新钱包
}
分布式锁 -- redis
前面的synchronized锁只能解决一个应用的线程同步问题,但是我们部署了两应用来处理并发问题的,所以我们要用redis锁。
事务
线程同步的问题都解决,还有一个比较重要的问题没有解决,之前粗心的同事忘记在处理订单的时候添加事务了,当我们的订单更新接口出现异常的情况就糟糕了,钱包更新了,但是订单的信息没有更新,那么就要一条条手动去解决这种问题了。粗心的同事马上在更新订单的service接口添加了@Transactional。
问题再现
昨天出现的问题,在老程序员的指导下我们很快的完成修复,并发布了修正版本,晚上美美的睡觉了,但是在今天昨天出现的问题又出现了!按照常理来说应该是没有问题的,线程同步的问题已经测试过没问题了,难道是添加了@Transactional的问题?我们马上做一个并发测试。 伪代码
@Test
public void testTransaction() {
CountDownLatch latch = new CountDownLatch(1);
JSONObject orderInfo0 = JSONObject.parseObject{\"userId\":1,}");
JSONObject orderInfo1 = JSONObject.parseObject{\"userId\":1,}");
new Thread(()->{
try {
//等待latch为0时,执行下面的代码
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
orderService.updateOrder(orderInfo0);
}).start();
new Thread(()->{
try {
//等待latch为0时,执行下面的代码
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
orderService.updateOrder(orderInfo1);
}).start();
//latch 减为0, updateOrder开始执行
latch.countDown();
try {
Thread.sleep(100000);//等待所有线程执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
}
日志出现 output
--- Thread-1 wallet 20元
--- Thread-2 wallet 20元
在没加事务的时候是正常的,现在添加了事务就不正常?
我们mysql才用默认的隔离级别,REPEATABLE READ(可重复读)
A、B同时开始事务,A插入数据并提交事务,B开始查询,但是B查询的数据没有变化,
等待B提交事务后重新查询,才能看到A插入的数据
在logback中开启事务日志<logger name="org.springframework.transaction" level="debugger"/>
重新跑下测试方法,当线程同时进入了orderUpdate方法的时候A、B线程开启了事务,显示事务为PROPAGATION_REQUIRED,该等级为是Spring默认的事务传播机制,就是如果有外层事务的话,当前事务加到外层事务里面,一起提交一起回滚,所以updateWallet钱包添加到外层的事务当中,就会出现线程A更新了钱包并提交事务,但是线程B还是读到就是的数据。
spring 事务传播性
- PROPAGATION_REQUIRED @Transactional(propagation=Propagation.REQUIRED) PROPAGATION_REQUIRED是Spring默认的事务传播机制,就是如果有外层事务的话,当前事务加到外层事务里面。
- PROPAGATION_REQUIRES_NEW @Transactional(propagation=Propagation.REQUIRES_NEW) PROPAGATION_REQUIRES_NEW这个传播机制是每次开启一个新的事务,同时把外层的事务挂起,当前事务执行完毕后,再恢复上层事务的执行;
- PROPAGATION_SUPPORTS @Transactional(propagation=Propagation.SUPPORTS) PROPAGATION_SUPPORTS 该传播机制是如果外层有事务则加入到该事务中,如果不存在,也不会创建新事务,直接使用非事务方式执行;
- PROPAGATION_NOT_SUPPORTED @Transactional(propagation=Propagation.NOT_SUPPORTED) PROPAGATION_NOT_SUPPORTED,该传播机制不支持事务,如果外层有事务的话,挂起外层事务,执行完毕后恢复;
- PROPAGATION_NEVER @Transactional(propagation=Propagation.NEVER) PROPAGATION_NEVER该传播机制不支持事务,如果外层存在事务的话,直接抛出异常IllegalTransactionStateException("Existing transaction found for transaction marked with propagation 'never'");
- PROPAGATION_MANDATORY @Transactional(propagation=Propagation.MANDATORY) PROPAGATION_MANDATORY,该传播机制是只能在已存在事务的方法中调用,如果在没有事务的方法中调用的话,抛出异常IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
- PROPAGATION_NESTED @Transactional(propagation=Propagation.NESTED) PROPAGATION_NESTED该传播机制特点是可以保存状态保存点,当事务回滚后,会回滚到某一个保存点上,从而避免所有嵌套事务都回滚.
所以只要在updateWallet增加事务@Transactional(propagation=Propagation.REQUIRES_NEW) 就解决目前的问题了
总结
在大多数系统设计的时候数据都是采用默认的隔离级别REPEATABLE READ(可重复读),在这种情况先我们要清晰的了解到spring事务传播性,才更好判断出现可重复读的情况,合理更改事务的传播级别构建我们健全的代码。