加锁不当,高并发下踩坑了!

点击上方“小罗技术笔记”,关注公众号
java

第一时间送达实用干货mysql


原本是不打算写这个文章可是在一个群里面发现又有群友遇到和我同样的问题不知道咋办
spring

知识点

一、并发(勉强) 二、mysql MVCC原理 三、spring 事务机制sql

原由

这个话题是由最近一次对接第三方商城发现的,该商城执行流程很奇特,流程以下:数据库

一、用户购买,三方平台调用本系统积分扣除接口,返回结果给三方。编程

二、三方回调本系统商品兑换接口,是否兑换成功,否单独调用三方失败处理接口(有步骤3回调),并返回现有接口结果给三方(有步骤3回调)。服务器

三、三方回调用本系统商品兑换成功/失败接口(确认三方已经收到消息并处理)微信

ps:步骤2兑换流程 加锁——>查询订单是否存在——>扣积分——>插入订单——>减库存——>赠送金币——>释放锁(因为流程如今不管是否兑换成功都必须保存订单,因此不能在步骤2方法使用事务回滚)并发

这个流程整体看起来很怪,我也是第一次遇到这样的,不过即便以为不合理也得按照人家的来。app

问题

若是仔细看看上面执行流程就会发现步骤2会带来两次连续的回调,这个连续回调也引起了本文的问题。在测试兑换失败场景时我这边要把扣的积分返还给用户,操做伪代码以下:

   
   
   
    
    
             
    
    
ServiceImpl:@Transactionalpublic void dealOrderExchangeNotice(....){        RedisLocklock= null;        try{            lock=newRedisLock(bizId);            if(lock.lock()) {                 //查询订单                 IntegralShoppingOrder shoppingOrder = selectOne(bizId);                   //shoppingOrder.getStatus()==1 表明订单扣积分红功 能够返还积分                    if(shoppingOrder != null&& shoppingOrder.getStatus() == 1) {                        //返还积分                        //更新订单状态为 4(订单失败)                    }        }catch(Exception e) {        }finally{            if(lock!= null) {                lock.unlock();            }        }  }

若是没有出现问题看着上面的代码感受没有啥问题的.....

测试时发现若是库存不够每次都是给用户返还了两次积分(至关于花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,将具体的逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染,所以实际使用中声明式事务用的比较多。

小知识: 一、默认配置下 Spring 只会回滚运行时、未检查异常(继承自 RuntimeException 的异常)或者 Error。

二、@Transactional 注解只能应用到 public 方法才有效。

很明显我这边也是采用声明式事务,Aop自动提交事务是在dealOrderExchangeNotice代码块中的方法执行完毕后才执行事务提交工做

ps:在群里面讨论时有一个群友说事务提交是在finally执行以前,这个观点是错误的

由于这个还在一个群里面被人喷了讨论的话题老旧

从上面两个知识点结合以前看的《Mysql45讲》(须要,公众号回复‘Mysql45讲’),我画了一个执行图很清晰的说明了问题所在(不懂千万不要空想动手画一画可能立刻明白了)

最后把上面的加锁代码转到controller层后重试没有出现多返积分的问题了

   
   
   
    
    
             
    
    

Controller:public void dealOrderExchangeNotice(....){    RedisLock lock= null;    try{        lock=newRedisLock(bizId);        if(lock.lock()) {            S.dealOrderExchangeNotice(....);        }finally{                if(lock!= null) {                    lock.unlock();                }        }  }ServiceImpl:@Transactionalpublic void dealOrderExchangeNotice(....){        try{             //查询订单            IntegralShoppingOrder shoppingOrder = selectOne(bizId);            //shoppingOrder.getStatus()==1 表明订单扣积分红功 能够返还积分            if(shoppingOrder != null&& shoppingOrder.getStatus() == 1) {            //返还积分            //更新订单状态为 4(订单失败)        }catch(Exception e) {        }}

相似像这种写法也是错误的

   
   
   
    
    
             
    
    
@Service@Transactionalpubli cclassOrderServiceImpl implements OrderService{    @Override    public synchronizedint update(Integer id) {          ...          ...          ...        }}

总结

锁不要加载事务中

因为本人文笔水平有限,文中的描述可能有些不清晰,可是经过问题的排查让我体验到理论结合实际代码的快乐,理论可能不是很高深、很难懂,可是有时木有结合实际也会出现意想不到的问题。

长按二维码关注

点个在看再走呗!

本文分享自微信公众号 - 小罗技术笔记(javaCodeNote)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索