Java红包领取BUG
最近遇到一个很有意思的bug ,业务逻辑很简单, 但涉及到技术面其实很深,拿出来记录一下.
本来要实现的功能是一个订单只发放一个红包,这个致命的错误会导致一个订单同时发放多个红包.
源码:
1 |
|
逻辑分析
原本错误逻辑
- 使用**getOrderForupdate( )**方法对订单在数据库层面加锁 , 防止其他事务修改改记录 ,出现并发等问题.
- 使用**!existRedPacket(orderId , redid:”20241111”)**判断是否发放了特定的红包,没有则插入一条红包记录
排除思路
当时第一时间认为是先线程冲突问题
1 | #A线程执行到这一步,对资源加锁,随后判断是否订单中存在红包 |
很奇怪分析完发现事务隔离看起来是正常的.
那会不会是主从延迟, 导致线程B没有发现存在红包 , 但走的是主库不存在延迟的情况.
问题关键
那到底是哪里出现问题了呢,认真检查代码会发现一个致命的问题
1 | order o = getOrder(orderId); |
这一步处理避免无订单的时候,产生间缝隙的方法时候存在问题 .
事务的隔离级别是默认可重复读
1 | @transactional(rollbackFor=Exception.class) |
发现问题了 ,A线程还未插入红包时候,B线程执行完第一个查询时候 , 是没有红包的 ,采用事务的隔离级别是默认可重复读,在第二个线程执行完第一个查询时MYSQL会通过mvvc机制,创建一个数据库快照, 这个快照中是没有红包的,那第二个查询时是没有红包的,即使数据库中实际上是有红包的 .
修改方法
把读后读改成提交读或者第一次查询不走事务
1 | // 显式设置事务隔离级别为 READ_COMMITTED(提交读) |
大功告成!头发又掉了一点.
总结
SelectForUpdate方法会在查询时给当前的行加行级锁,避免别的事务进来能够更新/删除本行
@Transactional开启事务,是为了让这个行锁在整个方法结束后(即事务提交后)才会被释放,否则selectForUpdate行锁不在事务中,也就相当于没了效果
这里会出问题,是因为两个事物同时开启时,由于默认的“可重复读”隔离级别,第二个事物读的是事务开启前的快照(这个快照会在事务进行第一个select语句时通过mvcc机制被创建)即使第一个事务提交后,第二个事物还在查询一开始的数据,查询不到
处理方法:
1.数据库隔离方式改成读已提交。这样会出现“不可重复读”的现象,即一个事物可以读到另一个事物已提交的东西。这样,第一个线程结束后提交并释放了锁,第二个线程的getOrderForUpdate方法就能查到结果,进而不发放红包
2.第一次查询不要在事务中进行。这样,高并发场景下确保第二次查询时能够查到结果至于为什么要有两个查询语句:
多一条查询语句(不加锁的查询语句,即视频中的第一条查询语句),相当于在数据存在时才会加锁,可以避免每次都加锁,避免了不必要锁竞争,提高并发量