redis结合lua实现秒杀

5 篇文章 0 订阅
  1. 简介

我们都知道秒杀是一个高并发,大量请求的场景,如果每次秒杀,我们都直接去操作数据库,校验库存,扣减库存,大量请求的话,数据库肯定扛不住,会出现各种问题。那怎么办?数据库虽然扛不住,但是redis能抗,所以我们可以使用定时任务,提前将秒杀商品的库存同步到redis中,每次秒杀请求,扣减redis中的库存,然后异步修改数据库的库存。到这里,大量请求又会出现一个问题,假如redis中某秒杀库存为1,这时候有两台服务器查询,发现都是1,都执行了redis库存扣减操作,这个时候,redis中的商品库存会变成-1,产生了超卖问题。这个如何解决呢?可以配合lua脚本来解决,众所周知,redis保证lua脚本以原子性的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 redis命令被执行。扣减库存操作写在lua脚本中,即可防止超卖。

完整流程:首先使用定时任务,将数据库秒杀商品库存同步到redis中,秒杀时,使用lua脚本扣减redis中的库存,然后将订单信息打入阻塞队列中,之后异步处理阻塞队列中的订单信息,进行数据库的库存扣减,订单信息生成等等操作。

  1. 流程图

下单流程

异步处理订单流程

  1. 实现

lua脚本

-- 存活时间
local expire = tonumber(ARGV[2])
local value = tonumber(ARGV[1])
local key = KEYS[1]
-- 加锁
local ret = redis.call("SET", key, value, "NX", "EX", expire)
local strret = tostring(ret)
if strret == 'false' then
    return 0
else
    return 1
end
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
--商品id
local goodsId = tostring(ARGV[1])
--用户id
local userId = ARGV[2]
--购买数量
local num = ARGV[3]
--商品库存
local stock = "cache_seckill_goods:"..goodsId
--商品订单列表
local order = "cache_seckill:order:"..goodsId
-- 查询商品库存
local stockNum = redis.call('get',stock)
if tonumber(stockNum) ~= nil and tonumber(stockNum) >= tonumber(num) then
    -- 判断用户是否下过单
    if redis.call('sismember',order,tonumber(userId)) == 0 then
        -- 扣除库存
        redis.call('incrby',stock,-tonumber(num))
        redis.call('sadd',order,tonumber(userId))
        return 1 -- 下单成功
    else
        return 0 -- 下单失败:用户限购
    end
else
    return -1 -- 库存不足
end

Java代码

redis运行lua脚本的工具类

/**
* 运行lua脚本
* @param fileName lua文件名
* @param keyList key
* @param args value
*/
public Object runRedisScript(String fileName,List<String> keyList,Object... args){
    killLuaScript.setLocation(new ClassPathResource("/lua/"+fileName)); // 指定脚本文件路径
    killLuaScript.setResultType(Long.class); // 指定脚本返回值类型
    log.info("准备运行/lua/{}脚本,key为:{},参数为:{}",fileName,keyList.toArray(),args);
    Object result = redisTemplate.opsForValue().getOperations().execute(killLuaScript,keyList,args);
    log.info("运行/lua/{}脚本结束,结果为:{}",fileName,result);
    return result;
}

/**
* 加锁
* @param key
* @param seconds
* @return
*/
public Object lock(String key,Integer seconds){
    List<String> keyList = new ArrayList<>();
    keyList.add(key);
    return runRedisScript("lock.lua",keyList,1,seconds);
}

/**
* 解锁
* @param key
* @return
*/
public Object unlock(String key){
    List<String> keyList = new ArrayList<>();
keyList.add(key);
return runRedisScript("unlock.lua",keyList,1);
}

订单服务类

/**
 * 订单服务类
 * @author gcp
 */
@Service
@Slf4j
public class OrderService extends ServiceImpl<OrderMapper, Order> {

    /**
     * 等待处理订单
     */
    private static BlockingQueue<HashMap<String,Object>> orders = new ArrayBlockingQueue<>(1024);

    /**
     * 处理失败订单
     */
    private static BlockingQueue<HashMap<String,Object>> loseOrders = new ArrayBlockingQueue<>(1024);

    /**
     * 处理完成的订单
     */
    private static final String ORDER_DEAL_KEY = "cache_order_deal:";

    /**
     * 定义线程池
     */
    private static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(2,4, 30,TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),new TestThreadFactory());

    /**
     * 自定义线程工厂
     */
    private static class TestThreadFactory implements ThreadFactory{

        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("线程"+count.addAndGet(1));
            return t;
        }
    }

    @Resource
    RedisCache redisCache;

    @Resource
    GoodsService goodsService;
    @Resource
    OrderGoodsService orderGoodsService;

    /**
     * spring容器初始化的时候执行
     */
    @PostConstruct
    private void orderhandle() {
        //处理等待队列
        POOL_EXECUTOR.submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {

                    while(true) {
                        //队列中取出商品id
                        HashMap<String,Object> take = orders.take();
                        if(!take.isEmpty()) {
                            OrderService orderService = ReflectUtil.getBean(OrderService.class);
                            try {
                                orderService.createOrder(take);
                            } catch (Exception e) {
                                e.printStackTrace();
                                // 处理失败的订单进入失败订单队列
                                loseOrders.put(take);
                            }
                        }
                    }
            }
        });
        //处理失败队列
        POOL_EXECUTOR.submit(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {

                while(true) {
                    //队列中取出商品id
                    HashMap<String,Object> take = loseOrders.take();
                    if(!take.isEmpty()) {
                        log.info("秒杀请求失败,请重新下单:{}",take.get("orderId"));
                    }
                }
            }
        });
    }

    /**
     * 创单逻辑
     * @param take
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(HashMap<String,Object> take){
            log.info("开始处理订单:{}",take.get("orderId"));
            SecKillReqDto secKillReqDto = JSON.parseObject(take.get("orderId").toString(),SecKillReqDto.class);
            Goods goods = goodsService.getById(secKillReqDto.getGoodsId());
            //更新商品库存
            goodsService.update(Wrappers.lambdaUpdate(Goods.class).set(Goods::getStockNum,goods.getStockNum()-secKillReqDto.getNum()).set(Goods::getFreezeNum,goods.getFreezeNum()+secKillReqDto.getNum()).set(Goods::getSaleNum,goods.getSaleNum()+secKillReqDto.getNum()).eq(Goods::getId,secKillReqDto.getGoodsId()));
            //订单信息
            Order order = new Order().setOrderPrice(goods.getPrice()).setCreateTime(LocalDateTime.now()).setUserId(secKillReqDto.getUserId()).setStatus(0).setExpiration_time(LocalDateTime.now().plusMinutes(10));
            this.save(order);
            //订单商品信息
            OrderGoods orderGoods = new OrderGoods().setUserId(secKillReqDto.getUserId())
                    .setGoodsId(secKillReqDto.getGoodsId())
                    .setNum(secKillReqDto.getNum())
                    .setCreateTime(LocalDateTime.now())
                    .setPrice(goods.getPrice().multiply(BigDecimal.valueOf(secKillReqDto.getNum())))
                    .setOrderId(order.getId()).setStatus(0);
            orderGoodsService.save(orderGoods);
            //保存处理完成信息,保存10s
            redisCache.setCacheObject(ORDER_DEAL_KEY+secKillReqDto.getUserId()+":"+secKillReqDto.getGoodsId(),1,10,TimeUnit.SECONDS);
            log.info("处理订单:{}结束",order.getId());
    }

    /**
     * 秒杀
     * @param secKillReqDto 秒杀入参
     */
    public Boolean secKill(SecKillReqDto secKillReqDto) throws InterruptedException {
        List<String> list = new ArrayList<>();
        Object executeRes;
        try {
            //运行lua脚本
            executeRes = redisCache.runRedisScript("secKill.lua",list , secKillReqDto.getGoodsId(),secKillReqDto.getUserId(),secKillReqDto.getNum());
        } catch (Exception e) {
            e.printStackTrace();
            throw new CommonException("秒杀请求失败!!!!");
        }
        if ((Long) executeRes == 1L) {
            // 将用户下单信息保存到阻塞队列
            HashMap<String, Object> hashMap = new HashMap<>();
            hashMap.put("orderId", JSON.toJSONString(secKillReqDto));
            orders.put(hashMap);
            log.info("用户[{}]秒杀请求成功",secKillReqDto.getUserId());
            return true;
        } else if((Long) executeRes == -1L){
            throw new CommonException("库存不足");
        }else{
            throw new CommonException("限制购买");
        }
    }

}
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

☆叙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值