Redis+RocketMQ实现并发条件下库存的扣减/增加(秒杀库存控制)

前言

前面我的博客介绍了有关分布式锁,分布式事务相关的问题以及解决方案,但是还是不能解决并发下单,扣减的问题,并发的时候由于数据库的隔离级别/乐观锁/悲观锁…总是会出现一些问题。最近集成了一套方案解决此类型问题,并可以适用于一般情况的秒杀方案。欢迎拍砖…

情景分析

前提条件:
商品 P 库存数量为 100
用户A,用户B 同时分别想买P,并且A,B分别买一个
数据库有版本号控制乐观锁
期望结果:
用户A,B都买到商品P,并且商品P的库存变为98

分析:
1.以前碰到这类问题的时候,我们说,可以添加分布式锁(也就是悲观锁),将商品P的id锁住,然后A,B提交订
单的时候分别预扣商品P的库存就好了(如方案一)。
2.是的,1分析的有道理,我们的核心思路确实要将并行控制为串行,但是事与愿违。我们仔细想一想:
  如果,A,B想买的不仅仅P一个商品,而是P,Q两个商品呢?我们该怎么锁?一次性将P,Q都锁住?显然不是
  很现实,并且实际上提交订单的时候都是从购物车下单,一个购物车里包括多个商品去下单也是很常见的,
  并且还有如下逻辑大家仔细思考:
  
  用户A下单->用户A读到库存为100
  用户A预扣库存,判断剩余库存是否>=0,100-1=99>0,则A预扣
  用户A的下单流程....
  此时A的事务没有提交,B下单了:
  用户B下单->用户B读到库存为100(这里读到100,是因为A还有其他逻辑在执行,还未提交事务,B读未提交了!)
  用户B预扣库存,判断剩余库存是否>=0,100-1=99>0,则B预扣
  用户B的下单流程....   
  
  最后不论A/B谁先提交事务,后面提交事务的就不会扣减库存成功。因为版本号不一致(就算没有乐观锁,
  修改的结果也会错,而且错的更离谱)。最终的结局就是库存是99
  
3.解决方案
  目前控制库存的方案有很多种,我这边介绍通过redis预减库存,通过mq发送消息同步的扣减数据库库存的
  方案。

方案一


@Transactional
public void createOrder(...){
    //1.校验,扣减库存
    check(Item item);
    
    //2.创建订单
    
    //3.创建支付单

}

@RedisLock("pId")
public void check(Item item){
}

解决方案伪代码


  //当然,我们管理平台新建商品时需要初始化到redis库存里,
  //这里暂时就不介绍了
  
  
  //下单部分
  @Transactional
    public void createOrder(...){
        //1.校验,扣减库存
        check(Item item);
        
        //2.创建订单
        
        //3.创建支付单
        
        //4.redis扣减库存
    
    }
    
    //支付回调部分
    @Transactional
    public void wxCall(...){
        //1.校验订单状态
        
        //2.修改订单,支付单状态
        
        //3.mq发送全局顺序消息 扣减库存

    }
    
    
    //取消支付部分
    @Transactional
    public void cancelOrder(...){
        //1.校验订单状态
        
        //2.修改订单,支付单状态
        
        //3.redis回退库存

    }
    
    
    //退货/退款部分
       @Transactional
    public void returnOrder(...){
        //1.校验订单状态
        
        //2.修改订单,支付单状态
        
        //3.redis回退库存
        
        //4.mq发送全局顺序消息

    }  

代码部分

  • 实现思路

    • 我们使用redis的lua脚本来实现扣减库存
    • 由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存
    • 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存
  • 初始化库存回调函数(IStockCallback )

/**
 * 获取库存回调
 * create by liuliang
 * on 2019-11-13  10:45
 */
public interface IStockCallback {
    /**
     * 获取库存
     * @return
     */
    String getStock();
}

  • 分布式锁控制初始化库存

/**
 *
 * Redis分布式锁
 * 使用 SET resource-name anystring NX EX max-lock-time 实现
 * <p>
 * 该方案在 Redis 官方 SET 命令页有详细介绍。
 * http://doc.redisfans.com/string/set.html
 * <p>
 * 在介绍该分布式锁设计之前,我们先来看一下在从 Redis 2.6.12 开始 SET 提供的新特性,
 * 命令 SET key value [EX seconds] [PX milliseconds] [NX|XX],其中:
 * <p>
 * EX seconds — 以秒为单位设置 key 的过期时间;
 * PX milliseconds — 以毫秒为单位设置 key 的过期时间;
 * NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
 * XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。
 * <p>
 * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
 * <p>
 * 客户端执行以上的命令:
 * <p>
 * 如果服务器返回 OK ,那么这个客户端获得锁。
 * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
 *
 *
 * create by liuliang
 * on 2019-11-13  10:49
 */

public class RedisStockLock {

    private static Logger logger = LoggerFactory.getLogger(RedisStockLock.class);

    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
     */
    public static final String NX = "NX";

    /**
     * seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
     */
    public static final String EX = "EX";

    /**
     * 调用set后的返回值
     */
    public static final String OK = "OK";

    /**
     * 默认请求锁的超时时间(ms 毫秒)
     */
    private static final long TIME_OUT = 100;

    /**
     * 默认锁的有效时间(s)
     */
    public static
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值