秒杀通用解决方案
秒杀的实质
秒杀的实质,是围绕库存管理展开的并发读写
如果架构设计里面包含商品系统,包含库存,秒杀就要解决库存热点行高并发读写问题。
秒杀的底线是:不能超卖。qty库存 ≥ qty卖出 && qty库存 - qty卖出 ≈ 0。
秒杀能够容忍的一些思路:渐进趋于一致,允许漏卖。
秒杀架构的特性
高性能:秒杀架构要承载的访问流量比平时高出许多倍,涉及大量的并发读和并发写,因此支持高并发访问非常关键。
一致性:秒杀活动中有限数量的商品在同一时刻被很多倍的请求同时扣减库存,在大并发更新的过程中要保证数据准确,不能发生超卖的问题(超卖,本来应该卖完下架的商品,在前台展示依然有库存,依然不停的被卖出),即库存是多少,理应卖出多少(qty库存 ≥ qty卖出 && qty库存 - qty卖出 ≈ 0)。
高可用:秒杀架构虽经多次打磨优化,但现实中总难免出现一些考虑不到的情况,要保证系统的高可用,还要设计一个兜底预案,以便在最坏的情况发生时仍能从容应对。
秒杀技术难点
- 在有限的资源下,秒杀链路承载合理的最大流量。
- 大并发下扣减库存准确,“一致性”中说到的“超卖”往往发生在该环节,减库存一般有下面几个方案:
- 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样基本不会出现超卖的情况。但是,有些人下完单可能并不会付款。
- 付款减库存,即买家下单后,并不立即减库存,而是等到用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为商品可能已经被其他人买走了。
- 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如10分钟),超过这个时间,库存将会自动释放,释放后其他买家可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款购买成功。
秒杀典型时序
- 查询库存
- 创建订单
- 扣减库存(2 和 3 应该在一个事务里)
- 更新订单
- 付款
秒杀的 3 大挑战
超卖
2012 年的双11 产生了大量的超卖。事后复盘,大家发现系统中出现了未知的热点。
热点问题
缓存雪崩、缓存击穿,进而导致服务雪崩。
吞吐瓶颈
在高并发的情况下,整体吞吐水平会低于正常水平。
准则
热点发现
- 根据业务场景,提前摸清热点数据。
- 智能识别、动态散列,能够根据基线,自动识别热点,并扩容对应分片。
做好隔离
链路、机器、系统都要做好隔离、缓存和限流。不止考虑业务系统层面的限流和熔断,如果有必要,秒杀的资源要单独做好隔离。
动静分离
把用户请求的数据分为静态和动态数据。
基于时间分片削峰
- 答题
- 排队,同步变异步,从紧耦合变松耦合
- 读缓存(舍 c 得 a)
最终实现漏斗状的写,实现有效写入的数量足够小。
数据库的相关配置
说到数据库层秒杀优化,我们到底在做什么:加速事务执行的每个关键环节,降低事务耗时。
- 库存分片,分而治之,提升热点行变更TPS,需要业务配合改造。
- 降低行锁(row lock)持有时间,持锁时间长的SQL放在最后,提升并发。
- 降低事务耗时,关闭deadlock detect、binlog group commit,和双1,加速事务。
- 减少线程间切换,使用线程池Threadpool,提升并发效率,正在测试中。
现实案例分析
现实案例分析 1
热点商品的 sku,同步select for update 的锁定,在秒杀多个商品时冲突很严重,一种解决方案是使用 lua 的原子脚本来实现分布式锁,再异步锁定 sku 行,然后要加上对热点商品的限流,在前端撞限流来减轻 db 的压力,提前让漏斗变小。
乐观锁不适合高并发场景,因为重试很容易让系统过载。
悲观锁适合高并发场景-扣减库存的关键还是要排队,要看业务流程中是否允许异步化,允许就用 mq 排队,不允许就用悲观锁排队。
现实案例分析 2
用 nowait 或者 skip locked 来减少锁等待,避免阻塞和死锁。
每个 sql 的执行时间是 1.25ms,这一行被更新的 tps 就是 800。但实际上单行事务非常少,所以大事务晚释放锁会让行的并发性降低。
方法:
- 合并 update,把热点 key 的更新语句在内存里 merge 一下。
- 组提交:把双一写关掉,让 fsync 慢一点。
- 删除多余的索引:需要考虑数据库的自治系统的建设。
秒杀是一种系统工程,需要业务理解数据库,关键还是在业务系统中做优化。
阿里和美团的解决方案
阿里使用 alisql 的内部排队和组提交事务的功能,大秒系统可以达到 8 万的 tps。其中排队指的是让请求的事务按照求锁顺序【使用信号量排队】获取锁,而不是将事务异步化,这可以让 MySQL 内核的死锁检测时间复杂度从 O(N*N)下降到 O(N)。
美团的解决有:架设一个 Redis 集群,通过 lua 脚本,实现一个在 redis 里的事务,扣减库存在 Redis 里实现,然后解析 aof 文件异步地真扣减库存,同时加上一个 mq(先写 redis 再写 mq),防止 Redis 丢失数据。mq 和 redis 对写操作做了建模,写操作带有版本,这样可以防止分布式环境下消息乱序和错漏重的问题。我猜这个版本可以是 seqNum,也可以是 行 version。这个方案被称作双通道冗余,在保证高可用的前提下逐步区域一致。aof 文件是一个纯异步复制的模式。这里使用的架构方案有点在本地做服务编排和 saga 的感觉。
阿里大秒系统的规范
热点隔离
秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让1%的请求影响到另外的99%,隔离出来后也更方便对这1%的请求做针对性优化。针对秒杀我们做了多个层次的隔离:
- 业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就是已知热点,当真正开始时我们可以提前做好预热。
- 系统隔离。系统隔离更多是运行时的隔离,可以通过分组部署的方式和另外99%分开。秒杀还申请了单独的域名,目的也是让请求落到不同的集群中。
- 数据隔离。秒杀所调用的数据大部分都是热数据,比如会启用单独cache集群或MySQL数据库来放热点数据,目前也是不想0.01%的数据影响另外99.99%。
当然实现隔离很有多办法,如可以按照用户来区分,给不同用户分配不同cookie,在接入层路由到不同服务接口中;还有在接入层可以对URL的不同Path来设置限流策略等。服务层通过调用不同的服务接口;数据层可以给数据打上特殊的标来区分。目的都是把已经识别出来的热点和普通请求区分开来。
动静分离
前面介绍在系统层面上的原则是要做隔离,接下去就是要把热点数据进行动静分离,这也是解决大流量系统的一个重要原则。如何给系统做动静分离的静态化改造我以前写过一篇《高访问量系统的静态化架构设计》详细介绍了淘宝商品系统的静态化设计思路,感兴趣的可以在《程序员》杂志上找一下。我们的大秒系统是从商品详情系统发展而来,所以本身已经实现了动静分离,如图1。
- 把整个页面Cache在用户浏览器
- 如果强制刷新整个页面,也会请求到CDN
- 实际有效请求只是“刷新抢宝”按钮
这样把90%的静态数据缓存在用户端或者CDN上,当真正秒杀时用户只需要点击特殊的按钮“刷新抢宝”即可,而不需要刷新整个页面,这样只向服务端请求很少的有效数据,而不需要重复请求大量静态数据。秒杀的动态数据和普通的详情页面的动态数据相比更少,性能也比普通的详情提升3倍以上。所以“刷新抢宝”这种设计思路很好地解决了不刷新页面就能请求到服务端最新的动态数据。
基于时间分片削峰
熟悉淘宝秒杀的都知道,第一版的秒杀系统本身并没有答题功能,后面才增加了秒杀答题,当然秒杀答题一个很重要的目的是为了防止秒杀器,2011年秒杀非常火的时候,秒杀器也比较猖獗,而没有达到全民参与和营销的目的,所以增加的答题来限制秒杀器。增加答题后,下单的时间基本控制在2s后,秒杀器的下单比例也下降到5%以下。新的答题页面如图2。
对大流量系统的数据做分层校验也是最重要的设计原则,所谓分层校验就是对大量的请求做成“漏斗”式设计,如图3所示:在不同层次尽可能把无效的请求过滤,“漏斗”的最末端才是有效的请求,要达到这个效果必须对数据做分层的校验,下面是一些原则:
- 先做数据的动静分离
- 将90%的数据缓存在客户端浏览器(难以实现)
- 将动态请求的读数据Cache在Web端
- 对读数据不做强一致性校验
- 对写数据进行基于时间的合理分片
- 对写请求做限流保护(易被忽略)
- 对写数据进行强一致性校验
实时热点发现
其实秒杀系统本质是还是一个数据读的热点问题,而且是最简单一种,因为在文提到通过业务隔离,我们已能提前识别出这些热点数据,我们可以提前做一些保护,提前识别的热点数据处理起来还相对简单,比如分析历史成交记录发现哪些商品比较热门,分析用户的购物车记录也可以发现那些商品可能会比较好卖,这些都是可以提前分析出来的热点。比较困难的是那种我们提前发现不了突然成为热点的商品成为热点,这种就要通过实时热点数据分析了,目前我们设计可以在3s内发现交易链路上的实时热点数据,然后根据实时发现的热点数据每个系统做实时保护。
具体实现如下:
- 构建一个异步的可以收集交易链路上各个中间件产品如Tengine、Tair缓存、HSF等本身的统计的热点key(Tengine和Tair缓存等中间件产品本身已经有热点统计模块)。
- 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(详情、购物车、交易、优惠、库存、物流)访问的时间差,把上游已经发现的热点能够透传给下游系统,提前做好保护。比如大促高峰期详情系统是最早知道的,在统计接入层上Tengine模块统计的热点URL。
- 将上游的系统收集到热点数据发送到热点服务台上,然后下游系统如交易系统就会知道哪些商品被频繁调用,然后做热点保护。
阿里双十一的经验
AliSQl
库存热点补丁、SQL限流保护、线程池。
2012年的双11结束后,我们就开始着手解决库存数据库的性能提升。库存扣减场景是一个典型的热点问题,即多个用户去争抢扣减同一个商品的库存(对数据库来说,一个商品的库存就是数据库内的一行记录),数据库内对同一行的更新由行锁来控制并发。我们发现当单线程(排队)去更新一行记录时,性能非常高,但是当非常多的线程去并发更新一行记录时,整个数据库的性能会跌到惨不忍睹,趋近于零。
当时数据库内核团队做了两个不同的技术实现:一个是排队方案,另一个并发控制方案。两者各有优劣,解决的思路都是要把无序的争抢变为有序的排队,从而提升热点库存扣减的性能问题。两个技术方案通过不断的完善和PK,最终都做到了成熟稳定,满足业务的性能要求,最终为了万无一失,我们把两个方案都集成到了AliSQL(阿里巴巴的MySQL分支)中,并且可以通过开关控制。最终,我们通过一整年的努力,在2013年的双11解决了库存热点的问题,这是第一次库存的性能提升。在这之后的2016年双11,我们又做了一次重大的优化,把库存扣减性能在2013年的基础上又提升了十倍,称为第二次库存性能优化。
使用 zero copy api 来加快云上的 io 速度。
存储计算分离的成功离不开一位幕后英雄:高性能和低延迟网络,2017年双11我们使用了25G的TCP网络,为了进一步降低延迟,2018年双11我们大规模使用了RDMA技术,大幅度降低了网络延迟,这么大规模的RDMA应用在整个业界都是独一无二的。为了降低IO延迟,我们在文件系统这个环节也做了一个大杀器-DBFS,通过用户态技术,旁路kernel,实现I/O路径的Zero
copy。通过这些技术的应用,达到了接近于本存储地的延时和吞吐。腾讯的经验
参考:[《如何设计一个高可用、高并发秒杀系统》][3]
先分客户端、接入层、逻辑层、存储层。