Redis实现秒杀系统详解
秒杀
是什么
秒杀通常指因为某种活动瞬时产生巨大流量的场景
比如双十一0点抢10000个苹果折扣手机,这种活动通常会吸引几十万甚至数百万人参与,而且大家都盯着0点,等0点一到就是海量的请求。

问题
1. 海量请求,服务要能扛住
秒杀活动一开始,瞬间会有海量流量涌入,热门的商品甚至会有几百万人来抢。
这个规模的流量砸下来,服务可能就挂了,活动也就GG了,收获的只有骂声。
2. 不能超卖
因为秒杀有时候就是赔本赚吆喝,价格可能比成本价还低。
而这时候要是比原计划的数量卖多了,那到底发不发货呢? 发货会超预算亏损,要是超卖数量过多,说不定厂子都要倒闭了;
不发货会被投诉,影响商家声誉。 不管怎样,都是硬伤,只能找程序员赔钱了。
3. 避免少卖
少卖会比超卖好一些,商家不存在经济上的损失。但要是被眼尖的消费者发现的话,也是免不了一场麻烦的。所以我们还是要尽可能避免这种情况。
4. 保证触达的用户不是黄牛
黄牛可能是开脚本,一次发很多请求过来,抢到之后再转卖。但我们做活动,希望的就是回馈客户,进而吸引用户,而不是去让黄牛赚外快。因此,我们要尽量挡住黄牛的魔爪。
黄牛的恶劣影响,很多时候是被低估了。
不仅仅是侵害了正常用户的权益,同时由于黄牛善于使用脚本,很容易造成大量的恶意请求,让本就不富裕的服务器资源,雪上加霜。
限购
通常来说,为了打击黄牛,最常见的方式是限购,一个用户最多只能抢到N份,这样可以大大保障正常用户的权益。
具体怎么做呢,为了性能,还是将限制逻辑加入到Redis中,所以我们的Lua脚本中
第一步查询库存,第二步扣减库存
需要优化为
第一步查询库存,第二步查询用户已购买个数,第三步扣减库存,第四步记录用户购买数。

竞争公平:没有银弹
作为追求极致的coder,我们希望还能更进一步,做到竞争公平。
怎么解决呢?
某个用户请求接口次数过于频繁,一般说明是用脚本在跑,可以只针对该用户做限制。
针对IP做限制也是常见做的做法,但这样容易误杀,主要考虑到使用同一个网络的用户,可能都是一个出口IP。限制IP,会导致正常用户也受到影响。
更好用的方案是加上一个验证码验证
验证码符合91原则,90%的时间,都用在验证码输入上,所以使用脚本点击的影响会降到很低。
怎么高并发
主要思路:削峰、限流、异步、补偿
异步
可以通过消息队列实现,将抢和购解耦,还可以方便限频,不至于让 mysql 压力过大
抢 使用 reids 做处理,因为 reids 处理简单的扣减请求非常快,reids 单机支持每秒几万的写入,还可以做成集群,提高扩展能力
简单流程
先将库存名额预加载到 redis- 在 redis 中进行扣减
- 扣减成功的再通过消息队列传递到 mysql 做正在的订单生成

预计有 100w 请求量
可以选择临时调度 20 个 redis 实例来支持,一个 5w/s,留点 buffer
也不用用 cluster 模式,用 nginx 负载均衡就可

拒绝超卖
将库存名额加载到了Redis,那就需要精确计数。
我们抢购场景最核心的,有两个步骤:
• 第一步,判断库存名额是否充足;
• 第二步,减少库存名额,扣减成功就是抢到。

有一个问题要考虑
如果第一步判断的时候还有库存,但是由于是并发操作,实际调用的时候,可能已经没有库存了,这样就会造成超卖。
所以第一步和第二步都是需要原子操作的。
Redis➕Lua,可以说是专门为解决原子问题而生,在Lua脚本中调用Redis的多个命令,这些命令整体上会作为原子操作来进行。
redis➕Lua
有了这套机制之后,我们看下访问Redis扣减库存时各种异常情况
- 正常业务错误,比如库存用完,这种情况符合预期,直接返回给用户即可。
- 访问Redis错误,这种情况返回给用户,让其重试即可
- 访问Redis超时,这种情况下,其实可能库存已经扣减成功,此时不用再重试,避免产生更多的无效扣减,虽然多了一次扣减,但是总数是不变的,只会少卖不会多卖。
避免少卖
什么情况会这样呢?有几种可能:
- 上面提到的,减少库存操作超时,但实际是成功的,因为超时并不会进入生成订单流程;
- 在Redis操作成功,但是向Kafka发送消息失败,这种情况也会白白消耗Redis中的库存。

问题:证Redis库存+Kafka消耗的最终一致性
说白了,我们只需要保证Redis库存+Kafka消耗的最终一致性。
但是一致性问题,一直是分布式场景的恶龙
• 第一种,也最简单的方式,在投递Kafka失败的情况下,增加渐进式重试;
• 第二种,更安全一点,就是在第一种的基础上,将这条消息记录在磁盘上,慢慢重试;
• 第三种,写磁盘之前就可能失败,可以考虑走WAL路线,但是这样做下去说不定就做成MySQL的 undo log,redo log这种WAL技术了,会相当复杂,没有必要。
Redis角色
Redis扮演扣减库存的角色,这个主要源自Redis比关系型存储高很多的处理性能。
实际上,除了扣件库存,Redis有时候也可以扮演队列的角色,请求过来先记录在Redis,虽然不如传统消息队列可靠,但胜在轻量。