【双十一】我教女票做秒杀

555 阅读8分钟

大家好,我是牛牛。

双十一又要到了,牛牛有点慌,以前一个人的时候,一分钱都不花,现在有了女票,不仅得剁手,还得帮忙抢各种秒杀商品。

今年,牛牛真的不想再去抢秒杀了,为什么呢?

太难了,成千上万的人就盯着秒杀放出来的那点商品。牛牛凭着单身十几年的手速也抢不过啊。

牛牛苦思妙想,终于想出一条完(zuo)美(si)妙计:给女朋友讲讲程序员是如何做一个秒杀系统的。

对头,就是要用知识的海洋淹没她。如果她不愿意听,或者听不懂,那么今年就不参加双十一了。

至于拒绝理由嘛。。。那就是【你都不认真听我说话,你一定是不爱我了】;如果不幸她听懂了,也不碍事,至少让她知道了我们程序员兄弟多么牛(jian)逼(xin)。

于是,牛牛找到了女朋友阿酱🐰。

🐮:呐,你知道我工作上也经常做秒杀系统吗?今天我就给你讲讲秒杀是怎么做的,如果你听懂了,今年我就帮你抢秒杀!

🐰:可是要是我听不懂怎么办啊?

🐮:我的宝贝怎么可能听不懂,要是听不懂一定是我讲得不够好!

🐰:那。。。我试试吧

问题抛出

首先,秒杀有哪些要考虑的地方呢?

第一点,海量请求,服务要能扛住。

秒杀活动一开始,瞬间会有海量流量涌入,热门的商品甚至会有几百万人来抢。这个规模的流量砸下来,服务可能就挂了,活动也就GG了,收获的只有骂声。

怎么让服务能打能抗,是需要考虑的问题。

第二点,不能超卖。

因为秒杀有时候就是赔本赚吆喝,价格可能比成本价还低。而这时候要是比原计划的数量卖多了,那到底发不发货呢?

发货会超预算亏损,要是超卖数量过多,说不定厂子都要倒闭了;不发货会被投诉,影响商家声誉。

不管怎样,都是硬伤,只能找程序员赔钱了。

第三点,尽量避免少卖。

少卖会比超卖好一些,商家不存在经济上的损失。但要是被眼尖的消费者发现的话,也是免不了一场麻烦的。所以我们还是要尽可能避免这种情况。

第四点,保证触达到用户而不是黄牛。

黄牛可能是开脚本,一次发很多请求过来,抢到之后再转卖。但我们做活动,希望的就是回馈客户,进而吸引用户,而不是去让黄牛赚外快。因此,我们要尽量挡住黄牛的魔爪。

🐰:不听了,不听了,脑壳痛。

🐮:那今年不用剁手啦~

🐰:???你继续,我能行!

🐮:问题我说完了,下面才是重点,来说说解决方案。

🐰:我好像已经开始听不懂了。。。

对症下药

硬抗高并发

在高并发的情况下,MySQL就显得有些力不从心了。

一方面是MySQL本身要支持事务的ACID,单机性能不高。

另一方面,MySQL是个单机数据库,本身是不能水平扩展的,如果要搞分库分表,费时费力。

这时候就可以借助MySQL的好伙伴Redis的能力。

Redis小哥可是单机支撑每秒几万的写入,并且可以做成集群,提高扩展能力的。

我们可以先将库存名额预加载到Redis,然后在Redis中进行扣减,扣减成功的再通过消息队列,传递到MySQL做真正的订单生成。

为什么要通过消息队列呢?

主要有两点好处,一个是这种投递的方式,可以让抢和购解耦。另一个是可以很方便地限频,不至于让MySQL过度承压。

我们说回Redis,如果请求量超过6W每秒,就要考虑使用多个Redis来分流。预计有100W请求量,我们就可以临时调度20个Redis实例来支持,一个5W/s,留点Buffer。

这种模式倒是不需要使用Redis Cluster那种一致性Hash的做法,直接前面接个Nginx,做负载均衡就可以了。

拒绝超卖

解决了高并发的问题,我们再来看看怎么防止超卖。

既然我们将库存名额加载到了Redis,那就需要精确计数。

我们抢购场景最核心的,有两个步骤:

第一步,判断库存名额是否充足;

第二步,减少库存名额,扣减成功就是抢到。

这里有一个问题要考虑,如果第一步判断的时候还有库存,但是由于是并发操作,实际调用的时候,可能已经没有库存了,这样就会造成超卖。

所以第一步和第二步都是需要原子操作的。

但是Redis没有直接提供这种场景原子化的操作。

遇事不要慌,仔细想一想,Redis是不是还有个特性,专门整合原子操作,对,就是它——Lua。

Redis➕Lua,可以说是专门为解决原子问题而生,在Lua脚本中调用Redis的多个命令,这些命令整体上会作为原子操作来进行。

尽量避免少卖

少卖什么情况会出现呢?

库存减少了,但用户订单没生成。

什么情况会这样呢?

在Redis操作成功,但是向Kafka发送消息失败,这种情况就会白白消耗Redis中的库存。

作为一个专业的程序员,只要知道问题是什么、怎么发生的,问题就解决了一半。说白了,我们只需要保证Redis库存+Kafka消耗的最终一致性

但是一致性问题,一直是分布式场景的恶龙,要对付并不容易。

第一种,也最简单的方式,在投递Kafka失败的情况下,增加渐进式重试;

第二种,更安全一点,就是在第一种的基础上,将这条消息记录在磁盘上,慢慢重试;

第三种,写磁盘之前就可能失败,可以考虑走WAL路线,但是这样做下去说不定就做成MySQL的undo log,redo log这种WAL技术了,会相当复杂,没有必要。

针对少卖这种极端场景可接受的问题,一般选择第二种方式即可,毕竟是异常情况的小概率事件,真出问题了大不了人工介入。

打击黄牛

黄牛的恶劣影响,很多时候是被低估了。

不仅仅是侵害了正常用户的权益,同时由于黄牛善于使用脚本,很容易造成大量的恶意请求,让本就不富裕的服务器资源,雪上加霜。

通常来说,为了打击黄牛,最常见的方式是限购,一个用户最多只能抢到N份,这样可以大大保障正常用户的权益。

具体怎么做呢,为了性能,我们还是将限制逻辑加入到Redis中,所以我们的Lua脚本中,第一步查询库存,第二步扣减库存,需要优化为第一步查询库存,第二步查询用户已购买个数,第三步扣减库存,第四步记录用户购买数

这里需要注意的是,如果使用Redis集群,那么Redis的一致性Hash Key,需要根据用户来分Key,不然用户数据会查询不到。

有了限购,我们可以保证货品不会被黄牛占据太多,那么还剩一个问题,黄牛大多是通过代码来抢购,点击速度比人点击快得多,这样就导致了竞争不公平。

作为追求极致的coder,我们希望还能更进一步,做到竞争公平。

怎么解决呢?某个用户请求接口次数过于频繁,一般说明是用脚本在跑,可以只针对该用户做限制。

针对IP做限制也是常见做的做法,但这样容易误杀,主要考虑到使用同一个网络的用户,可能都是一个出口IP。限制IP,会导致正常用户也受到影响。

更好用的方案是加上一个验证码验证。验证码符合91原则,90%的时间,都用在验证码输入上,所以使用脚本点击的影响会降到很低。

当然,我们要明白没有银弹,这种方式缺点在于降低了用户的体验感。 故事尾声 🐮:这样一来,我们的秒杀场景就基本OK啦!

🐰:😣😣😣

🐮:怎么样,听懂了吗?

🐰:嗯嗯!(心虚的用力点头)我们能去秒杀了吗?

🐮:那我来检验一下?

🐰:我难道不是你的宝贝了吗?😰😰😰

🐮无奈地叹气:说吧,你这次又想抢什么东西?

回到现实

理想很美好,然而现实是。。。

牛牛心里苦,牛牛不敢说😭😭😭