先简单描述一下状况,有订单相关业务,一共有2个service类,一个操作订单简称order(Service),一个操作库存的,简称inventory(Service),在order中有A方法,是在数据库中新增订单数据的,在inventory中,有B方法,是扣除库存用的,在方法A中,会调用B,这2个service的class都加了 @Transactional 注解,情况大概是这样。
class orderService{
public void A(){
//保存订单信息
saveOrder();
//扣除库存
B();
//其他业务代码
}
}
在B方法,扣除库存当中,会先查询该商品的库存(大概意思,实际业务中会比较多的校验),然后做对应得库存扣减,但是呢,这个B方法,是个通用(公共)方法,在仓储模块中,也会调用到这个方法,所以我非(zi)常(zuo)得(cong)意(ming)的在 B 方法上加了 synchronized 关键字,企图给这个方法加个同步锁,好在并发的时候不要扣错库存。
class inventoryService{
//扣除订单库存方法
public synchronized void B(String productId){
//获取库存对象
InventoryItem ii = findProductInventory(productId);
//扣除库存数量
ii.setQuantity(ii.getQuantity - 100);
//保存库存
updateById(ii);
}
}
但终究还是太年轻,在运行了几个月后,最近出现了一个BUG,mysql出现了锁等待超时的报错,这一串神秘代码相信大家不会陌生的
Lock wait timeout exceeded; try restarting transaction
查了一下,阿里云的默认锁等待时间是50s,所以肯定是哪里的业务相互卡死了,并且还没有 DeadLock的级别,查了一阵子,定位到了 A、B这两个方法来了,错误的场景是对发布的接口中,同时收到了多条第三方传递过来的数据,简单描述为第三方同时发了2个请求调用了A方法下单,并且这个2个订单中都有多个明细,且有相同的商品,最终在 B 方法中的 updateById(ii); 这一行抛出异常。
同事陪我找到很久,后来终于发现问题,简单来说,应该是代码中的同步锁加错位置了,下面就简单画一下,当时发生了什么
订单1:商品P1 + 商品P2
订单2:商品P1 + 商品P2
时间 | 订单1 | 订单2 | 描述 |
---|---|---|---|
13:00:00.000 | 进入controller | 进入 controller | - |
13:00:00.005 | 进入方法A | 准备循环调用B扣减P1、P2的库存 | |
13:00:00.008 | 进入方法A | 准备循环调用B扣减P1、P2的库存 | |
13:00:00.010 | 进入方法B操作P1 | 等待 | 拿到B的同步锁 |
13:00:00.010 | 获取P1商品库存为100 | 等待 | - |
13:00:00.011 | 扣减P1库存 | 等待 | 调用updateById更新P1库存 获取了mysql对应得行级锁 |
13:00:00.015 | B方法操作P1执行完 | 等待 | 释放B方法的同步锁 注意,此时订单1的事务还没有提交 |
13:00:00.015 | 操作P1剩余业务 | 进入方法B操作P1 | 订单2拿到了B方法的同步锁 |
13:00:00.015 | 试图进入方法B操作P2 | 获取P1的商品库存为100 | 因为订单1的事务还没提交,订单2拿到P1的库存还是100,而订单2的线程已经在第二次循环操作P2了 |
13:00:00.018 | 等待 | 扣减P1库存 | 订单1更新P1的库存在整个订单流程中还未结束,事务未提交,所以,订单2的updateById在等待mysql的行级锁的释放 |
13:00:00.018 | 你好了吗? | 你好了吗? | 因为订单2的updateById还在等待,所以B方法的同步锁并没有释放,所以,订单1的B方法进不来 |
13:00:50.018 | - | - | 50秒过后,大家都GG |
问题大概就是这样,怎么修复还没想好,从整个业务结构上需要修改一下,大致是因为线程1的事务还没有提交,但是有个数据更新在这个事务中,而另外线程2需要更新数据主键和线程1还未提交的一致,在等这个事务提交,却又通过同步锁卡住了主线程1,不让他提交,最后两败俱伤。
在此记录一下这个问题,以后不要再犯。