秒杀系统

概述

秒杀系统是指大量用户同时抢购数量有限的商品,瞬时造成服务端的尖峰压力, 服务端需要保证高并发处理,同时又要保证高可用性、商品不会超发、快速扩容缩容、系统容错、降级熔断等,融合了各种服务端技术。

我们以电商的购买流程举例,电商购买分为放入购物车、下单、支付三个步骤。购物车服务是用来管理用户将要购买的商品,单独一个服务。下单服务是服务端创建订单并减库存的服务,如果库存减为0,则不能再卖出商品,减库存需要做到幂等和防超发。支付系统是对订单进行最终的支付, 如果过期没有支付需要再返回库存。总体上来说订单ID在整个流程中是唯一的,试用订单ID来保证幂等性。如果以微信红包系统来举例的话,经历微信包红包、发红包、抢红包、拆红包的过程。抢红包类似电商的减库存,用户抢到红包但是不知道多少钱,抢到红包的人是说有资格拿到钱(拆红包时有可能失败)。拆红包类似最终支付到账的流程,后台随机给用户分配抢到的金额并写入到数据库中

秒杀系统中,一个重要的概念的幂等性,因为分布式系统中存在各种重试(事务补偿),在各个层面都要做的幂等性,才能有效的防止重复购买和数据一致性。

下面我们按照客户端、服务端、缓存、数据库来说明下各个阶段需要做的事情。

客户端

客户端是请求的发起方,一般来说客户端有一个H5页面或者客户端页面。在秒杀时刻,会有大量用户同时访问服务端,用户也需要首先下载H5页面或者一些静态信息。

静态信息存储
  • 在CDN中存储静态H5页面,快速返回给客户端。
  • 客户端活动开始前访问服务端时候,缓存页面到本地。
防止重复购买
  • 用户只能单点登录,防止多地登录,重复购买。
  • 客户端访问服务端创建订单后,缓存订单号到本地。后续访问服务端时候带上订单号,防止重复下单。
  • 用户抢购过后将抢购按钮置灰,防止用户重复抢购。
  • 添加验证码、游戏、问答等,防止用户快速的重复刷新。增加用户下单的时间,降低服务端性能压力。

服务端

nginx层的日志通过flume写入到Hadoop + Hbase中,风控平台,供以后分析用或者对账用。

限制购买数量

服务端需要记录用户目前已经购买了多少次,防止用户超过购买次数。用户下单后创建订单,如果减库存成功则记录在订单管理系统中,这里可以用来统计用户下单成功的次数。为了提升高并发的能力,考虑以下措施。另外这一步在一些特殊情况下(主备不同步,机器挂掉)可能会存在漏掉一些非法用户,需要后面下单写入数据库的时候也进行判断。

客户端缓存

客户端缓存用户目前在本机上已经购买了多少数量的商品,如果已经超过限定,则置按钮为灰,不能再抢购。而且可以限定用户抢购的频率,每抢购一次停留5s再抢购。但是限制不了用户绕过客户端直接发起http请求到服务端。

Redis缓存

客户端下单时,服务端首先在Redis中存储用户+商品-》购买次数的kv对,如果这期间有同时有该用户的其他请求,发现Redis中已经存在该记录,则暂时返回失败,等待重试。同时一旦有请求进来,对redis进行异步操作加载数据库中用户的订单记录计算用户目前真正的购买次数(前面的抢购中有可能有失败的),异步操作可以根据redis中数据的时间超过5秒更新一次。

  • 一个用户只能购买一个:redis中缓存用户+商品的key-》上次加载时间,操作redis时候使用原子加操作
  • 一个用户限定购买多个:redis中缓存用户+商品-》购买次数,上次加载时间,操作redis时候使用原子加操作。
本地缓存

在某些极端情况下,Redis的性能可能不足以支撑抢购性能。一个优化的策略是说,将用户目前的购买次数存在本地,同样同样适用异步更新的策略。

  • 本地机器只存储非常热门的多次购买的用户次数
  • 用户通过一种hash方式使得相同用户的请求到同一台机器。(待考虑,因为商品ID可能也希望到同一台机器上处理)。但是如果这一台机器挂掉,本地存储的用户购买数量就会丢失,换到新机器后需要去redis中查询存储在本地再判断是否减库存。
数据一致性
  • Redis采用集群格式,一主多副,防止主机挂掉。
  • 如果Redis挂掉,采用熔断方式。并且降级,回复大部分的请求失败,少部分请求走数据库逻辑。
  • 如果跨过这一层,需要后续减库存时候判断是否该用户已经购买了足够多的商品,判断下单是否失败。
  • Redis扩容缩容交由Redis集群保证。

限制购买频率

除了限制用户的购买数量,另一方面可以从用户的购买频率上进行限制。首先客户端和服务端都要限制一个用户一次最多购买多少件商品,一个配置就可以搞定。然后从服务端采用异步的方式计算热点用户或者热点IP的购买频率,计算出黑名单用户,并上传到服务端,对黑名单用户直接返回失败。

  • 限制商品购买频率。如果该抢购商品在一段时间内抢购频率太快,则随机给一些用户返回失败。
  • 限制用户购买频率:防止单个用户购买频率太快,防止是非法用户。

缓存库存

用户下单的时候需要判断库里是否还有库存,如果没有库存就直接返回给用户失败,防止超卖的发生。但是在并发量高的场景,会同时有请求操作数据库,如果不加锁的话会有数据不一致的情况,加锁的话会对系统性能有影响。所以,一种办法是在数据库层增加缓存层,将大部分超过库存的请求挡到外面。

随机返回失败

当大批量请求进来时候,可能已经超过本机的承载能力,则随机将一些请求置为抢购失败,减轻服务端的压力。这就是降级和熔断的功能。

redis缓存

将商品库存缓存在redis中,一旦过来订单,则对redis做原子减操作,如果库存已经减为0,则直接返回失败。否则,将本次的订单写入到消息队列中,后续异步更新到数据库。如果订单写入消息队列失败,同样返回失败。如果写入成功,返回正在处理,等待数据库更新成功后,才真正的返回成功。
通过redis的请求并不一定最后一定处理成功,可能会有一些重复订单(机器挂掉后出现)、或者缓存和数据库库存的不一致导致的漏掉,所以最后的结果要以数据库为准。

防止重复提交

首先服务端需要判断订单是否之前处理过。所以每当用户下单时候,需要把该订单和其当前状态写入到数据库或者Redis中,key是订单号。用来判断是否是重复订单,如果是重复订单,则返回当前订单的状态(正在处理中,抢购成功,抢购失败)。类似抢火车票时候,一直处于排队状态。

本地预分配

使用redis缓存需要经过一次网络调用,如果可以做到将库存缓存到本地,则会大大提升后端性能。所以,可以考虑首先将抢购商品的库存预分配到每台机器上(每天机器上有部分库存),需要保证同一个用户的请求会被分配到同一台机器。这样,当用户请求进来时,直接在本地给用户分配商品,清减库存(这里需要尽量减少锁的操作,保证性能,比如可以考虑分组、数组等减少锁的粒度)。如果失败,则直接返回失败。如果成功,则写入到消息队列,返回抢购成功同时更新订单状态(这里和Redis的不一样,redis方案需要最终写入到数据库中才返回成功)。如果写消息队列失败,则仍然返回失败。但是该用户的这份库存需要释放出来。

防止重复提交

redis方案中使用redis存储订单状态,那么是否可以在本地缓存订单状态呢?保证同一个订单的请求会进入同一台机器,使用订单ID的后两位来路由到某台机器上,同时在更新本地状态后,异步写入到redis中订单状态。考虑机器宕机的情况下,订单请求切换到另外一台机器怎么办,这时候需要去redis中查询一下,判断该订单是否之前处理过。偶尔一些重复请求(一些非法用户或者更换了机器的)可能会进入后面的逻辑,由数据库层面拦截。

机器宕机
  • 如果出现机器宕机导致没来得及更新Redis中订单的状态,用户再来请求时候的直接返回订单目前的状态。直到后续数据库完成操作后,更新redis中订单状态。
  • 机器宕机导致来不及发送到消息队列中,客户端访问超时设置状态为系统正在处理。用户再来请求时候的直接返回订单目前的状态(正在处理)。后续判定该数据过期或者确认失败后,设置为失败状态。
机器扩容

另一个问题是在运行过程中发现压力太大,需要新增一台机器。这时候库存分配策略是怎么样的?如果直接使用hash规则会发生改变,导致大批用户迁移机器,影响性能。一个好的hash策略是参考微信红包的策略,将订单ID的最后两位分配到每台机器上。比如现在有10台机器,目前订单ID的后两位生成时候都是00-09。如果增加了一台机器,生成00-10结尾的订单ID。这样由订单ID生成规则来做机器的路由。

库存分配

机器扩容的另外一个问题是新机器需要新的库存。考虑将整体库存分成多个20w的小库存放到数据库中。每台机器每隔一段时间来拿空闲的库存,并设置该库存的owner是该机器IP。新机器扩容时候,同样来这里拿。如果一台机器挂掉,那么属于它的小库存将不再更新,等到一段时间确定处理完所有的消息队列中的该机器请求后,则设置为无owner,允许其他机器获取。

数据库

数据库中保存最终的商品库存,需要保证数据安全性和数据一致性。从消息队列中顺序读取消息并减库存,因为是顺序操作不涉及到超卖的问题。同时更新redis中订单的状态。为了保证幂等性,需要在同一个事务中同步写入数据库中该订单消耗了多少库存(流水),防止后续同一个订单又来减库存。数据库使用raft强一致性协议保持主从同步。其他的方法:

  • 使用悲观锁
  • 使用消息队列
  • 使用乐观锁,version来实现
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • #0 系列目录# 秒杀系统架构 秒杀系统架构分析与实战 #1 秒杀业务分析# 正常电子商务流程 (1)查询商品;(...
    传奇内服号阅读 797评论 0 11
  • #0 系列目录# 秒杀系统架构 秒杀系统架构分析与实战 #1 秒杀业务分析# 正常电子商务流程 (1)查询商品;(...
    Java架构师Carl阅读 1,601评论 0 8
  • 来源:陶邦仁 链接:http://my.oschina.net/xianggao/blog/524943 0 系列...
    meng_philip123阅读 3,548评论 0 65
  • 1、感谢招行工作人员帮我办理2个账户注销,新办理一张银行卡,谢谢你们的劳动付出,谢谢你们,谢谢!祝福你心想事成,幸...
    海灵1阅读 204评论 0 1
  • 我知道 你想表达的意思 我知道 你内心矛盾的想法 我也知道 你每次面临选择的痛苦 可是 没人每次都会猜透你 没人每...
    仟小羽阅读 106评论 0 4