目录
数据库方案
FOR UPDATE
库存大于0判定
库存设置为无符号整形
乐观锁
分布式锁
利用Redis
总结
数据库方案
以下的方案重点在于防止超卖,库存信息不加载到缓存Redis,而是直接同DB交互,实际场景下通常不会如此,但是其中用到的细节还是值得学习的。
FOR UPDATE
该方案是在MySQL层面进行加锁,行锁Or表锁,要根据Where条件来判定。
该方案通过事务+for update
进行保证,伪代码如下所示:
begin transcational
count = SELECT number FROM seckill WHERE seckill_id=? FOR UPDATE
if count > 0
UPDATE seckill SET number=number-1 WHERE seckill_id=?
INSERT order //下订单
commit
说明:
- 同一个事务中 如果查询最后限定了
for update
那么非本次事务中的 其他SQL指令会被阻塞。 - 如果
where
条件为主键
或者索引列
的时候才会锁住行,即行级锁
。否则会锁表,即锁表
。当前的where条件很明确是seckill_id
为主键,所以是行级锁。 - select操作和update操作都是在本次事务中进行的。
库存大于0判定
该方案主要通过在执行update减少库存的时候,加上对库存大于0的判定。
begin transcational
count = UPDATE seckill SET number=number-1 WHERE seckill_id=? AND number>0
if count > 0
INSERT order //下订单
commit
其中最为核心的就是最后一个条件number>0
库存设置为无符号整形
核心就是将库存设置为无符号整形
,就是不允许库存为负数
begin transcational
count = UPDATE seckill SET number=number-1 WHERE seckill_id=?
if count > 0
INSERT order //下订单
commit
这点同where条件有些像。
乐观锁
通过乐观锁来保证商品在每一个只会被消费一次,通过对number
进行乐观锁来进行判定,伪代码如下所示:
BEGIN transcational
n = SELECT goods number
count = UPDATE seckill SET number=number-1 WHERE seckill_id=? AND number = n
if count > 0
INSERT order //下订单
else
Loop //再次循环操作
commit
说明
- 请注意每次进行update前先查询,查询的目的是获取版本号。
- 通过对CAS的思想通过版本号对库存进行更新,如果符合预期那么更新,否则肯定是被其他线程消费过。
分布式锁
通过分布式锁来保证,同一时间只会有一个线程在处理某类商品秒杀业务:查询库存-->判定库存 -->减少库存。
dis-lock goods type //1. 通过分布式锁:锁住商品类型
begin transcational //2. 开启事务
count = SELECT number FROM seckill WHERE seckill_id=?
if count > 0
UPDATE seckill SET number=number-1 WHERE seckill_id=?
INSERT order //下订单
// dis-un-lock goods type 错误释放分布式锁
commit //3. 提交事务
dis-un-lock goods type //4. 释放分布式锁
说明
- 请注意3和4 的顺序,一定要先提交事务后,再释放分布式锁。
为什么了?假设此时的库存为1。第一个线程在第提交事务前释放了锁,假设提交事务需要5个单位的时间。另外一个线程在第一个线程释放锁的瞬间,抢占了锁,然后在3个时间就完成了查询和减少库存并提交事务的操作,此时库存为0。2个单位后第一个线程事务提交才完成,此时库存为-1了。这样导致了超卖。
所以事务的提交一定要释放分布式锁之前。 - 在Spring中事务通过是通过注解
@Transcational
来实现的,如果直接在@Transcational
包裹的方法里面获取锁和释放锁可能会出现超卖,此时需要通过另外一个AOP进行包装,这里涉及到2个知识点。
- 一个方法有多个AOP注解时候,切面的执行顺序怎么确定的问题。该问题是每个切面都可以通过
@order
来定义顺序,越小的越先执行,而@Transcational
的order是最大,所以肯定是在内部执行。 - :多个切面的执行顺序和退出顺序问题,可以参考此文:http://www.hicode.club/2018/03/01/apsect-order/
利用Redis
因为Reis
是单线程的,所以可以通过其特性decr
后进行判定,实际场景也是推荐这么做的。
- 秒杀业务前,将秒杀商品和库存信息缓存到
Redis
中。 - 对
Redis
中缓存的商品数量做decr
减1操作,如果小于0则将商品id加入商品售卖完成缓存
中。避免每次都往Redis请求。
总结
技术是为了解决业务难题而存在和发展的,切记脱离业务来学习技术。
上述方案都是解决秒杀业务的初步原形,大概思路如上所述,其中其实有很多细节,后续抽时间补上。
业务处理上需要结合自己的业务进行扩展,个人推荐:
redis > 乐观锁 > 库存大于0 > 分布式锁 > for update
- 原则上尽可能的避免锁表操作。
- 尽量避免请求直接打到数据库上。