驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
秒杀业务中不超卖的实现方案汇总
/  

秒杀业务中不超卖的实现方案汇总

开篇

这段时间看了些秒杀业务处理方案,学到了一些东西,可能是比较粗浅的,但是万丈高楼平地起,所以还是做一个记录。

本文内容包括如下几点:

  1. 认知中最优的方案。
  2. 其他方案以及关联的知识点

学而时习之,不亦乐乎!

数据库方案

以下的方案重点在于防止超卖,库存信息不加载到缓存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

说明:

  1. 同一个事务中 如果查询最后限定了 for update 那么非本次事务中的 其他SQL指令会被阻塞。
  2. 如果where条件为主键或者索引列的时候才会锁住行,即行级锁。否则会锁表,即锁表。当前的where条件很明确是seckill_id为主键,所以是行级锁。
  3. 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

说明

  1. 请注意每次进行update前先查询,查询的目的是获取版本号。
  2. 通过对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. 释放分布式锁

说明

  1. 请注意3和4 的顺序,一定要先提交事务后,再释放分布式锁。

    为什么了?假设此时的库存为1。第一个线程在第提交事务前释放了锁,假设提交事务需要5个单位的时间。另外一个线程在第一个线程释放锁的瞬间,抢占了锁,然后在3个时间就完成了查询和减少库存并提交事务的操作,此时库存为0。2个单位后第一个线程事务提交才完成,此时库存为-1了。这样导致了超卖。

    所以事务的提交一定要释放分布式锁之前。

  2. 在Spring中事务通过是通过注解@Transcational来实现的,如果直接在@Transcational包裹的方法里面获取锁和释放锁可能会出现超卖,此时需要通过另外一个AOP进行包装,这里涉及到2个知识点。

    • 一个方法有多个AOP注解时候,切面的执行顺序怎么确定的问题。该问题是每个切面都可以通过@order来定义顺序,越小的越先执行,而@Transcational的order是最大,所以肯定是在内部执行。
    • :多个切面的执行顺序和退出顺序问题,可以参考此文:http://www.hicode.club/2018/03/01/apsect-order/

利用Redis

因为Reis是单线程的,所以可以通过其特性decr后进行判定,实际场景也是推荐这么做的。

  1. 秒杀业务前,将秒杀商品和库存信息缓存到Redis中。
  2. Redis中缓存的商品数量做decr减1操作,如果小于0则将商品id加入商品售卖完成缓存中。避免每次都往Redis请求。

总结

技术是为了解决业务难题而存在和发展的,切记脱离业务来学习技术。

上述方案都是解决秒杀业务的初步原形,大概思路如上所述,其中其实有很多细节,后续抽时间补上。

业务处理上需要结合自己的业务进行扩展,个人推荐:

redis > 乐观锁 > 库存大于0 > 分布式锁 > for update
  1. 原则上尽可能的避免锁表操作。
  2. 尽量避免请求直接打到数据库上。
骐骥一跃,不能十步。驽马十驾,功在不舍。