Java开发面试基础,“并发
测试时发现每次都是给用户返还了两次积分(相当于花100送200了,这哪了得…),刚开始看上面的代码看了好久没有发现问题,加上log后查询服务器日志发现失败订单几乎在同一时间会收到两条回调信息,
(勉强算作一个高并发吧),两个请求都拿到了锁且shoppingOrder的getStatus()都是一样的,感觉到问题了出现重复读了…

解决过程
两个请求都拿到了锁证明第一个回调请求已经执行完毕了,按道理应该将订单状态更新成4了第二个请求查询到的也应该是4,但是还是出现同样的值说明第二个请求查询时第一个没有提交事务。
这样明确出两个排查方向 重复读(mysql MVCC原理)、事务提交(spring 事务机制)。
mysql MVCC原理
mysql默认事务隔离级别是 RR(Repeatable Read,可重复读),事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事物对同一张表,同一时刻看到的数据可能是不一样的。
由此可以确定第二个请求执行查询时第一个请求事务没有提交,两者的事务版本号是一样的所以查询的值是一样的,因此问题不在数据库了!
小知识:
第一个SELECT执行的时候,当前事务取到了系统版本号n(并不是begin的时候就生成版本号,而是执行事务内第一个语句时生成),系统版本号自增为n+1。此后,其他事务的更新操作能取到的系统版本号最小为n+1,所以当前事务再次SELECT将看不见它们的更新。
spring 事务机制
Spring 事务管理分为编程式和声明式两种。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体的逻辑与事务处理解耦。
声明式事务管理使业务代码逻辑不受污染,因此实际使用中声明式事务用的比较多。
小知识:
1、默认配置下 Spring 只会回滚运行时、未检查异常(继承自 RuntimeException 的异常)或者 Error。
2、@Transactional 注解只能应用到 public 方法才有效。
很明显我这边也是采用声明式事务,Aop自动提交事务是在dealOrderExchangeNotice代码块中的方法执行完毕后才执行事务提交工作
ps:在群里面讨论时有一个群友说事务提交是在finally执行之前,这个观点是错误的
因为这个还在一个群里面被人喷了讨论的话题老旧


我画了一个执行图很清晰的说明了问题所在(不懂千万不要空想动手画一画可能马上明白了)

最后把上面的加锁代码转到controller层后重试没有出现多返积分的问题了
Controller:
public void dealOrderExchangeNotice(....){
RedisLock lock = null;
try{
lock=new RedisLock(bizId);
if (lock.lock()) {
S.dealOrderExchangeNotice(....);
}finally {
if (lock != null) {
lock.unlock();
}
}
}
ServiceImpl:
@Transactional
public void dealOrderExchangeNotice(....){
lock = null;
try{
//查询订单
IntegralShoppingOrder shoppingOrder = selectOne(bizId);
//shoppingOrder.getStatus()==1 代表订单扣积分成功 可以返还积分
if (shoppingOrder != null && shoppingOrder.getStatus() == 1) {
//返还积分
//更新订单状态为 4(订单失败)
}catch (Exception e) {
}
}
类似像这种写法也是错误的
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Override
public synchronized int update(Integer id) {
...
...
...
}
}
最后
如果觉得本文对你有帮助的话,不妨给我点个赞,关注一下吧!

[外链图片转存中…(img-uyObKzJA-1628071542495)]
