秒杀超卖问题方案

前言

关于秒杀的设计,网上的讨论很多,良莠不齐,但大多会有这几个共识。

  • 将流量挡在前端,可以用nginx+redis+lua限流
  • 库存提前预热到redis当中,在redis中减库存
  • 减库存之后,发送消息到队列,后续动作消费队列,减轻对数据库的压力
  • 为解决超卖问题,扣库存的操作用redis分布式锁,升级版就是将单个redis库存分成多个,相当于分段锁,提高并发能力
  • 秒杀之后,商品下架或者再等待一下次秒杀,这中间不可用。

还有对于超卖的问题,有的文章建议

  • 在sql加上判断防止数据边为负数
  • 数据库加唯一索引防止用户重复购买
  • redis预减库存减少数据库访问 内存标记减少redis访问 请求先入队列缓冲,异步下单,增强用户体验

个人汇总了一下,感觉都比较繁琐,如果前端能限制大部分的流量,那进入后台的流量很小,这时候可以不用分布式锁,完全用redis减库存就可以了,大体来说有2种思路。第一种是先redis直接自减,当扣成负数的时候,不进行后续的数据库操作,直接返回错误,但是redis会扣成负数。第二种是用redis的lua脚本,先判断是否是大于0,若是大于0则自减,否则直接返回,这种不会扣成负数。

1. 每次都自减

先redis直接自减,当扣成负数的时候,不进行后续的数据库操作,直接返回错误,但是redis会扣成负数。缺点是如果前端显示库存数,可以做一些逻辑判断,若是小于0,直接返回0。还有就是如果要增加库存,不能直接累加,要先恢复到0。

        String key = "redis:test:stock";
        
        redisTemplate.opsForValue().set(key, 10);
        
        Thread[] threads = new Thread[100];
        for (Thread thread : threads) {
            thread = new Thread(() -> {
                long remaind = 0;
                if ((remaind = redisTemplate.opsForValue().increment(key, -1).intValue()) > -1) {
                    System.out.println(Thread.currentThread().getName() + ":" + "扣除成功" + "剩余:" + remaind);
                    //处理后续操作
                }else {
                    System.out.println(Thread.currentThread().getName() + ":" + "扣除失败" + "剩余:" + remaind);
                    //直接返回
                }
            });
            thread.start();
        }

2. 先判断再自减

用redis的lua脚本,先判断是否是大于0,若是大于0则自减,否则直接返回,这种不会扣成负数。

local num=redis.call('get',KEYS[1])
if tonumber(num)>0 then
   return redis.call('decr',KEYS[1])
else
   return -1
end

当返回是负数的时候,说明库存已经扣完了。

        String key = "redis:test:stock";
        
        redisTemplate.opsForValue().set(key, 10);
        
        // 这里必须是Long
        RedisScript<Long> script = RedisScript.of("local num=redis.call('get',KEYS[1])\r\n" + 
            "if tonumber(num)>0 then\r\n" + 
            "   return redis.call('decr',KEYS[1])\r\n" + 
            "else\r\n" + 
            "   return -1\r\n" + 
            "end", Long.class);
        
        Thread[] threads = new Thread[100];
        for (Thread thread : threads) {
            thread = new Thread(() -> {
                long remaind = 0;
                if ((remaind=redisTemplate.execute(script, Arrays.asList(key), new Object[] {})) > -1) {
                    System.out.println(Thread.currentThread().getName() + ":" + "扣除成功" + "剩余:" + remaind);
                    //处理后续逻辑
                }else {
                    System.out.println(Thread.currentThread().getName() + ":" + "扣除失败" + "剩余:" + remaind);
                    //直接返回
                }
            });
            thread.start();
        }

最后总结

直接用redis扣库存,必须要在前端挡掉大部分流量,这中间如果redis挂掉,就灾难了,因为没有穿透到数据库获取缓存的机制,所以保障好redis的稳定是非常有必要的,主从,哨兵等等。

发布了7 篇原创文章 · 获赞 13 · 访问量 3726

猜你喜欢

转载自blog.csdn.net/xiaolong7713/article/details/105103058