在我的数藏项目中,库存扣减的方法中(本文特指数据库的扣减,不包含 Redis 层),我们全程是没有加锁的,甚至乐观锁都没加。
没加悲观锁是因为秒杀场景不适合用悲观锁(为什么秒杀不能用悲观锁或者分布式锁来实现,详见我的面试宝典),那么为啥乐观锁都没加呢?这个是我们的库存预扣减的 SQL:
图片
大家可以先忽略 SQL 中`/*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */`部分内容,这是利用了阿里云 DMS 的 Inventory Hint 的机制抗高并发的实现。具体可以到我的项目课程中学习。
如上的 SQL 中,我们只做了数量的扣减,以及数量的上限的控制。但是没有通过 lock_version 做乐观锁的版本控制。
虽然我们用了MybatisPlus。也用到了他的@Version 注解,但是我们自己写的 SQL 的这种方式,MyBatisPlus 也不会主动帮我们加乐观锁的控制:
图片
那就是说我们这里没有做加锁,为什么呢?
如果你知道乐观锁和悲观锁的区别,那么就会知道,乐观锁因为比较乐观,所以一般是先做业务逻辑操作,比如参数处理,内存中进行模型组装调整,然后再去更新数据库。悲观锁因为比较悲观,所以会先尝试加锁,然后再去做业务逻辑操作。
也就是说,乐观锁是先干活,后加锁。悲观锁是先加锁,再干活。乐观锁适合用在并发冲突不高的场景中。
而我们的这个库存扣减这里,其实并发冲突还挺高的,如果这里用了乐观锁,那么就会有大量的失败。
举个例子,100个线程同时来查询库存,得到的 lock_version = 1,然后同时去更新库存,都要求当前的 lock_version = 1,这就会导致99个线程都失败,那么就会导致整体失败,如果事务中有其他操作,就要回滚。
库存扣减的时候,我们的目的肯定是让多个线程都扣减成功,并且不扣错就行了。
如果你的 SQL 是这样的:
UPDATE collection
SET saleable_inventory = #{saleableInventory}
WHERE id = #{id}
也就是说这个变更后的saleableInventory是你自己算好的,给到 SQL 去执行的,那么就必须要加锁来避免并发的时候计算出错,但是我们通过在 SQL 中扣减不会有任何并发导致出错的问题。
而如果这里不加乐观锁,只通过库存扣减,以及库存不能小于0来做控制,那么只需要他们各自抢锁然后按顺序扣减就行了,反正每次库存都是在前面的结果上依次扣减的:
SET saleable_inventory = saleable_inventory - #{quantity}
所以,这里我们只需要保证不会超卖就行了,剩下的并发问题 update 过程中的锁会帮我们解决的。
本文的技术方案来自于我出的数藏项目实战课,里面还有很多类似的干货内容。