目录
超发问题分析
针对上篇文章中,用户抢到红包后,红包总量应-1,当多个用户同时抢红包,此时多个线程同时读得库存为n,相应的逻辑执行后,最后将均执update T_RED_PACKET set stock = stock - 1 where id = #{id} ,很明显这是错误的。
1.悲观锁(for update)
悲观锁是一种利用数据库内部机制提供的锁的方法,对更新的数据加锁, 在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不再对数据进行更新了。
1.1 项目使用悲观锁操作
- 线程1查询红包数时使用排他锁
<!--查询红包具体信息--> <select id="getRedPacket" parameterType="long" resultType="cn.whc.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>
- 然后进行后续操作(redPacketDao.decreaseRedPacket和userRedPacketDao.grapRedPacket)更新红包数量,最后提交事务
- 线程2在查询红包数时,如果线程1还未释放排他锁,线程2将等待
- 线程3同线程2,以此类推
1.2 共享锁(S锁) 和 排他锁(X锁)
共享锁和排他锁是悲观锁不同的实现,都属于悲观锁的范畴
数据库的增删改操作默认都会加排他锁,但查询不会加任何锁
共享锁: 指的是对于多个不同的事务,对同一资源共享同一个锁。对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源,但无法修改。
排他锁: 指对于多个不同的事务,对同一资源只能有一把锁。对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。
1.3 悲观锁消除超发问题
在RedPacket.xml中查询红包具体信息后面添加for update语句,为查询增加行级锁,这样就不会出现超发现象引发数据不一致性问题了
<!--查询红包具体信息-->
<select id="getRedPacketForUpdate" parameterType="long" resultType="cn.whc.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>
测试结果:
性能问题:
目前只对数据库加了一个锁,显示结果还不太明显,但是如果加得锁比较多的时候,数据库的性能会持续下降,原因如下:
- 对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,这个时候,CPU就会将这些得不到资源的线程挂起,挂起的线程也会消耗CPU的资源,尤其是在高并发的请求中
2.乐观锁
乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现。
乐观锁使用的是CAS原理。
2.1 CAS原理
在CAS原理中,对于多个线程共同的资源,先保存一个旧值,比如进入线程后,查询当前存量为100个红包,那么先把旧值保存为100,然后经过一定的逻辑处理。当需要扣减红包的时候,先比较数据库当前的值和旧值是否一致,如果一致则进行扣减红包的操作,否则就认为它已经被其他线程修改过,不再进行操作
CAS可能会存在ABA问题,只要在数据中加入一个版本号的属性,就能解决ABA问题
2.2 乐观锁实现抢红包业务
规避CAS原理产生的ABA问题,需要先在红包表(T_RED_PACKET)加入一个新的列版本号(version),我们已经在一开始创建表的时候就建立了
<!--通过版本号扣减抢红包,每更新一次,版本增1,其次增加对版本号的判断-->
<update id="decreaseRedPacketForVersion">