我的逻辑是
- 查询优惠券
- 判断库存是否充足(领取数量<总数量)
- 如果充足,更新优惠券领取数量
这里是先查询、再判断、再更新的方案,而这三个步骤都是独立的,不具备原子性。在单线程swagger测试的情况下是不会触发的
但是多线程并发运行会存在下面这种情况:
当线程1尚未更新时线程2就来查询,此时查到的库存数据是旧的,但是线程2又不知道就会认为库存充足,就会导致并发安全问题产生。主要原因:
- 多线程并行运行
- 多行代码操作共享资源,但不具备原子性
要解决这种并发问题,大家基本都知道怎么做:加锁,锁一般可以分为:悲观锁、乐观锁
悲观锁:一种独占和排他的锁机制,保守地认为数据会被其他事务修改,所以在整个数据处理过程中将数据处于锁定状态。
乐观锁:一种较为乐观的**并发**控制方法,假设多用户并发的不会产生安全问题,因此无需独占和锁定资源。但在更新数据前,会先检查是否有其他线程修改了该数据,如果有,则认为可能有风险,会放弃修改操作
可见,悲观锁、乐观锁是对并发安全问题的处理态度不同:
- 悲观锁认为安全问题一定会发生,所以直接独占资源。结果就是多个线程会串行执行被保护的代码。
- 优点:安全性非常高
- 缺点:性能较差
- 乐观锁则认为安全问题不一定发生,所以不独占资源。结果就是允许多线程并行执行。但如果真的发生并发修改怎么办??乐观锁采用CAS(Compare And Set)思想,在更新数据前先判断数据与我之前查询到的是否一致,不一致则证明有其它线程也在更新。为了避免出现安全问题,放弃本次更新或者重新尝试一次。
乐观锁举例:
比如我们现在total_num
为10,issue_num
为9,也就是说还剩下1个库存了。现在有两个线程来执行修改操作。
- 线程1、线程2都查询数据,发现
total_num
为10,issue_num
为9 - 线程1、线程2都判断库存是否充足,
if(issue_num < total_num)
,发现都成立了。 - 线程1和线程2都开始执行数据库写操作,更新
issue_num
。但是由于数据库的事务互斥,肯定有先有后。我们假设线程1先执行。按照乐观锁机制,在更新时要做数据检查(CAS),判断数据是否变化。因此SQL是这样:UPDATE coupon SET issue_num = issue_num + 1 WHERE id = 1 AND issue_num = 9
- 注意SQL语句结尾的
AND issue_num = 9
, 这里的9就是之前查询的结果,这里就是校验是否变化,假如issue_num
发生变化,此处不一致,肯定SQL就执行失败。当然线程1是第一个执行的,issue_num
没有变化,所以这里会成功。因此issue_num
的值+1
,变为10
- 紧接着,线程2执行,因为线程2查询的时候issue_num是9,所以线程2执行相同SQL:
UPDATE coupon SET issue_num = issue_num + 1 WHERE id = 1 AND issue_num = 9
- 但线程1已经将
issue_num
的值更新为10,线程2的这条SQL执行时where条件不成立,执行失败,乐观锁生效了。
以上就是乐观锁的工作原理,可以发现乐观锁:
- 优点:性能好、安全性也好
- 缺点:并发较高时,可能出现更新成功率较低的问题(并行的N个线程只会有1个成功)
不过,针对更新成功率低的问题,在优惠券库存这个业务中,有一个乐观锁的改进方案:
我们无需判断issue_num是否与原来一致,只要判断issue_num是否小于total_num即可。这样,只要issue_num小于total_num,不管有多少线程来执行,都会成功。
综上,我们最终的执行SQL是这样的:
UPDATE coupon SET issue_num = issue_num + 1 WHERE id = 1 AND issue_num < total_num