1 悲观锁
<!-- 查询红包具体信息 -->
<select id="getRedPacketForUpdate" parameterType="int"
resultType="test814RedPacket.pojo.RedPacket">
select id,user_id as userId,amount,send_date as sendDate,total,unit_amount as
unitAmount,stock,version,note
from T_RED_PACKET where id=#{id} for update
</select>
for update
在sql后面加 for update ,
注意,在 SQL 中加入的 for update 语句,意味着将持有对数据库记录的行更新锁(因 为这里使用主键查询,所以只会对行加锁。如果使用的是非主键查询,要考虑是否对全表 加锁的问题,加锁后可能引发其他查询的阻塞〉,那就意味着在高并发的场景下 条事 务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待,这样 就不会出现超发现象引发的数据一致
发现一个好玩的问题:
在UserRedPacketServiceimpl 中,如果你注释掉
就是不要事务,还是会超发
加上之后才不会。有趣
2乐观锁
说吧了就是判断下之前的stock值有没变化,有变化则说明数据改变,不操作。但是可能存在bab问题,所以加个版本,每次数据改变,版本+1,在对数据修改之前,判断版本,没有变化才操作数据。
<!-- 通过版本号扣减抢红包 ,每更新一次,版本加1,其次增加对版本的判断 注意 的代 减红包的时 增加了对版本号的判断,其次每次扣减都会对
版本号 ,这样保证每次更新在版本号上有记录 从而避免 ABA 问题。对于查询也不使
for update 语旬 避免锁 发生 这样就没有线程阻塞的 题了-->
<update id="decreaseRedPacketForVersion" >
update T_RED_PACKET set stock = stock -1,version=version+1 where id =#{id} and version = #{version}
</update>
这里是主要改动,其余都是改动调用,看下结果:
我设置了2500左右的人抢2000个红包,看到没,剩下不少.....
所以再优化,重入机制
也就是 旦因为版本原因 没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的 SQL 执行,所以目前流 行的重入会加入两种限制, 1种是按时间戳的重入,也就是在 定时间戳内不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。另外 1种是按次数,比如限定 3次,程序尝试超过3 次抢红包后,就判定请求失效,这样有助 于提高用户抢红包的成功率,下面讨论如何重入。
加个时间而已,你也可以把时间换成次数
for (int i = 0; i < 3; i++) {
//获取红包信息,
RedPacket redPacket = redPacketMapper.getRedPacket(redPacketId);
//当前红包数大于0
if(redPacket.getStock()>0){
//再次传入线程保存的 version 旧值给 SQL 判断,是否有其他线程修改过数据
int update = redPacketMapper.decreaseRedPacketForVersion(redPacketId,redPacket.getVersion());
//如果没有数据更新,说明其他线程已经更新过数据,本次抢红包失败
if(update==0){
return FAILED;
}
3 redis
对于使用 redis 实现抢红包 首先需要知道的是redis的功能不如数据库强大,事务也 不完整,因此要保证数据的正确性,数据的正确性可以通过严格的验证得以保证。而 Lua 语言是原子性的,且功能更为强大 所以优先选择使用 Lua 语言来实现抢红包