一. 简单秒杀
1.秒杀开始之前,先将商品预设的秒杀库存数量加载到Redis中;
2.后台服务1接收到秒杀请求时,先判断Redis库存是狗足够,如果足够执行第三步,如果库存不足时,直接返回秒杀失败;
3.减掉Redis预设库存并将请求放入异步队列中,返回正在排队中;
4.后台服务2用于消费队列,获取队列信息后首先判断真实库存是否足够,如果库存不足,直接返回秒杀失败,如果库存足够就生成订单信息并通知用户秒杀成功。
在这个demo,我们设置30个Redis秒杀数量。模拟100个请求进行抢购。
后台服务1:效果抢购效果如下
后台服务2:消费队列效果如下
这就完了吗?站住!滑走你就草率了。
这样的写法会出现超卖!为什么呢???
真的出现了超卖的情况!为什么呢???想想就知道了,so easy,假设:
当A查到redis库存为1的时候,做减库存之前,B也查到了库存,也是1,C也查到了库存,也是1;
这个时候A和B和C都进入了redis的自减命令Decr,会执行3次Decr,就变成了-2(也就是说队列的生产者多生产了2条数据进入了队列,消费端必然会多消费这2条数据)
那么怎么解决超卖的问题呢?
-----方法1:如果是单个客户端进行抢购,对线程加锁即可,如果有多个客户端进行抢购,就需要分布式锁。(尽量能不用就不用,坑较多。)
-----方法2:怎么办,我就是不想用分布式锁,行不行?当然可以,只要利用redis单线程原子性,就可以实现
注意:因为redis它在处理命令或者指令的时候是单线程的 (单线程原子性,不需要锁,同时避免上下文切换出现的性能消耗,速度不一定比多线程慢)。也就是说我们发送给redis服务的指令,不管在任何时候,在同一时刻只会执行一条指令,合理利用自减命令Decr是不会出现“超卖”的。
二. 总结
本文使用String和List的完成的秒杀小Demo,目标只是为了简单入门理解。真实开发需要做的封装和优化,Redis适合做缓存和分布式锁,但是Redis做消息队列如果出现系统宕机的情况容易信息丢失并且没有自己的ack机制,可靠性不够高,适用于对信息准确性要求不高的场景。
然后,还有,在高并发场景下,假如在队列消费端,需要真正的减去库存,这个时候数据库必然不是Redis,那么如果消费端也搭建了集群,必然会出现同时查库存减库存的操作(非原子性的库存校验导致在并发场景下,库存校验的结果不准确。这是超卖的根本原因),“超卖”问题依然困扰我们,这个时候,就要考虑使用分布式锁了
但是话说回来,针对秒杀场景,进入队列的数据并不多,大部分情况下是没有必要搭建集群进行消费,刚好好避开了分布式锁。除非有必要,否者建议尽量不要使用分布式锁。