背景:线上运营在后台导入发券给用户的excel后,服务端触发异步、并行发券操作
问题代码:
@Transactional(rollbackFor = Exception.class)
public boolean sendCouponToMoreUser(Date date, CouponToMoreUserMsg couponMsg) {
//1.查询优惠券的基础信息(select 优惠券表)
//2.更新优惠券基础信息中的剩余数量 (update 优惠券表)
//3.发券 (insert 用户领取优惠券表)
}
第一步,执行select时,需要给当前优惠券行加一个“Shared Lock”共享锁,共享锁是允许多个session同时进入的
第二步,执行update时,需要给当前优惠券行加一个“Exclusive Lock”独占锁
当两个session并发时,两个session执行select时都获得了“Shared Lock”,而接下来执行update时都在获取“Exclusive Lock”的同时等待对方释放“Shared Lock”,从而导致了死锁。
Mysql监测到死锁时,会回滚掉undo日志量最小的那个session的事务(持有最少行级排它锁的事务回滚)
tips:共享锁“Shared Lock”,是读取操作创建的锁。所有请求可以并发读取数据,即给该优惠券row行上加了多把共享锁,此时任何事务都不能对该优惠券进行修改(就是获取该优惠券的排他锁),直到已释放所有的共享锁。因为MySQL默认事务隔离级别是可重复读(REPEATABLE READ),事务A读取到的优惠券信息,在事务A还未提交事务时,事务B不能把这个优惠券信息给改了!
解决方案:
1.尽量避免在同一个Transaction中先select再update(锁定同一条row),除非保证当前接口是串行调用的
2.MySQL单节点时,可以使用select ... for update加悲观锁。但是集群下,select...for update锁住的是每个节点的row,之后进行update无法保证死锁!
3.确实需要这么做的情况下,在select之前先做一个不产生任何效果的update,直接抢到“Exclusive Lock”,然后再执行后续操作
注意:
UPDATE t_coupon
SET
surplus_count = surplus_count - 1,
draw_count = draw_count + 1,
update_time = now()
WHERE
coupon_id = 31552;
这种update语句在事务中,高并发调用时。也会造成死锁!
因为surplus_count = surplus_count - 1, draw_count = draw_count + 1,update_time = now(),要先读出surplus_count、draw_count,now()
这种写法会先去拿该优惠券row的“Shared Lock”共享锁再去拿“Exclusive Lock”独占锁!!!