写点什么

秒杀系统—架构设计和方案简介

  • 2025-05-29
    福建
  • 本文字数:15331 字

    阅读完需:约 50 分钟

1.秒杀系统的方案设计要点

 

(1)秒杀促销活动的数据处理


一.秒杀促销活动的业务流程

二.秒杀促销活动的 Redis 数据结构

 

(2)秒杀促销活动的页面处理


一.秒杀活动页面静态化处理

二.页面静态化以及 CDN 缓存处理

三.静态页面的缓存以及文件服务器存储

四.秒杀接口 url 的隐藏处理

五.后端与前端秒杀倒计时的时钟同步

六.秒杀开始时的验证码处理

七.秒杀活动页面的限流处理

 

(3)秒杀场景下的负载均衡架构


一.防黑客 DDoS 攻击的高防 IP 处理

二.秒杀场景下的 SLB 负载均衡架构

三.基于 SLB 的秒杀场景和普通场景的分流隔离

四.基于 Nginx 的秒杀请求分发

五.基于 Nginx 的秒杀请求限流

六.Nginx 的内核参数调优

 

(4)秒杀场景下的高并发抢购处理


一.秒杀抢购请求的处理链路

二.Tomcat 的内核参数调优

三.秒杀抢购接口的限流处理

四.秒杀抢购请求的去重处理

五.秒杀接口的防刷防作弊处理

六.商品库存的 Redis 分布式缓存

七.商品库存的 Lua 脚本扣减逻辑

八.商品库存的超卖问题处理

九.秒杀接口的多线程并发优化

十.秒杀接口的 Disruptor 内存队列异步化

 

(5)秒杀场景下的异步下单处理


一.秒杀场景下的 RocketMQ 集群架构

二.秒杀场景下的消息队列

三.秒杀场景下的 Redis + RocketMQ 一致性回滚

四.秒杀场景下的数据库架构

五.秒杀场景下的分库分表技术

六.高并发场景下的数据库压测

七.高并发场景下的数据库连接池参数调优

八.高并发场景下的数据库内核参数调优

九.秒杀下单服务的核心业务逻辑

 

(6)秒杀成功后的业务逻辑处理


一.秒杀成功后的异步通知

二.秒杀成功后的订单查询

三.秒杀成功后的订单支付及后续逻辑

四.秒杀成功后长期不支付的处理

 

(7)秒杀系统的高可用设计


一.秒杀系统全链路的中间件高可用

二.秒杀系统全链路的高可用降级

三.Redis 缓存崩溃后的秒杀系统自动恢复

四.RocketMQ 集群崩溃后的临时本地存储降级

五.数据库集群崩溃后的临时本地存储降级

六.秒杀服务崩溃后的防服务雪崩降级

七.秒杀系统的全链路漏斗式流量限制

八.秒杀系统的双机房多活部署

九.秒杀系统部分机房故障时的降级

 

(8)秒杀系统的压测 + 故障演练以及实时大盘


一.秒杀系统的全链路压测及针对性优化

二.秒杀系统的全链路故障演练及高可用验证

三.秒杀系统基于大数据技术的实时数据大盘

 

2.秒杀系统的数据 + 页面 + 接口的处理方案


(1)秒杀活动的描述


每天会有多个场次的秒杀活动,不同场次的秒杀活动有不同的秒杀商品。比如 10:00 正在抢购、12:00 即将开始、14:00 即将开始、16:00 即将开始等。

 

(2)秒杀活动管理系统的功能


新建一个秒杀场次,秒杀场次会有开始时间和结束时间。比如一个秒杀场次持续 3 天,每天都是 12 点时开始抢购。可以给一个秒杀场次加入一些秒杀商品,并设置对应的折扣、价格和数量等。

 

(3)秒杀活动数据进行数据库 + 缓存双写


首先,秒杀活动的数据会存放在数据库里。然后,秒杀活动系统会对外提供一个接口,通过这个接口可以查询获取配置的秒杀场次和秒杀商品。一旦配置好秒杀活动的数据,之后一般不会随便变动。

 

所以当用户在 APP 大量查询查秒杀活动数据时,就没必要频繁查数据库了。因此可采取数据库 + 缓存双写的模式,秒杀活动的数据也存储一份到缓存中,并在查询时直接查缓存中的数据。

 

(4)秒杀活动数据存储在 Redis 的 List 和 Hash 中


可以通过 Redis 的 List 和 Hash 数据结构来存储秒杀活动数据。比如可以通过一个 key 为"seckill::#{日期}::rounds"的 List 数据结构,存储当日秒杀场次,List 里可以存放秒杀场次的 ID 主键值。这样根据"seckill::#{日期}::rounds",就能获取当日秒杀场次的主键 ID。

 

比如可以通过一个 key 为"seckill::round::#{秒杀场次 ID}::info"的 Hash 数据结构,存储秒杀场次的基本信息,Hash 里可存放秒杀场次开始和结束时间等。接着根据秒杀场次的主键 ID,就可以获取每个秒杀场次的具体数据。

 

比如可以通过 key 为"seckill::round::#{秒杀场次 ID}::products"的 List 数据结构,存储秒杀场次对应的所有秒杀商品信息,这样根据秒杀场次 ID 就能获取其对应的秒杀商品的 ID 集合。

 

比如可以通过 key 为"seckill::#{秒杀场次 ID}::#{商品 ID}::info"的 Hash 数据结构,存储秒杀商品的具体数据,比如秒杀价格和秒杀数量等。这样根据秒杀商品的 ID,就能获取秒杀商品的具体标题和描述等。

 

(5)秒杀活动页面数据的动静分离


一.在对普通商品的详情页进行架构设计时


一般采用伪静态化设计,也就是动态渲染,实际上并没有静态化。比如处理页面请求时,先在 Nginx 层基于 Lua 从多级缓存里加载页面数据,然后基于模板技术动态渲染成一个静态页面,最后再返回静态页面。如果采用纯静态化设计,即把每个详情页渲染成静态页面。那么每天都需要对全量几亿甚至几十亿商品的详情页进行一次静态渲染,其中涉及的性能损耗、时间开销、存储空间,在成本上都是无法接受的。

 

二.在对秒杀商品的活动页进行架构设计时


秒杀商品活动页面是不太适合进行动态渲染的。因为每天需要参与秒杀活动的商品数会很少,对应的商品页面数也很少,而这种秒杀商品页面的访问频率却非常高,很多用户都会集中某个时间进行访问。所以秒杀商品活动页面最好采用纯静态化设计,进行静态渲染,秒杀活动页面的数据需要实现动静分离。



三.秒杀活动商品详情页的静态化处理


所谓动静分离,就是秒杀商品活动页已经通过 HTML 页面渲染系统把内容填充好了,如商品标题、价格、描述等,大部分页面信息都是静态的,而页面的个人信息、登录状态、个性化推荐等内容则是动态的。



(6)基于 CDN 来缓存秒杀活动静态页面


CDN 是多台服务器 + 智能 DNS 的结合体。CDN 服务就是把静态页面缓存到不同地区的多台缓存服务器上,然后根据用户线路所在的地区,通过 CDN 服务商的智能 DNS,自动选择一个最近的缓存服务器让用户访问,以此提高访问速度。这种方案对静态页面的效果非常好。

 

由于秒杀商品活动页面(静态页面)一般分为两个部分:一部分是把数据都嵌入 HTML 以后的静态页面,一部分是 HTML 页面引用的 js、css 和图片,所以可以将这些静态资源都推送到 CDN 里去,从而提高秒杀商品活动页面的访问速度。

 

(7)CDN 静态数据缓存的失效与命中


如果不小心修改了秒杀商品的页面,那么就需要让 CDN 缓存快速失效。因为修改页面后直接推送页面到 CDN 服务器上,可能需要时间比较长。所以最好还是修改完页面后,直接通知 CDN 服务器让缓存失效。之后再重新推送修改后的秒杀商品页面过去,这个过程可通过 RocketMQ 进行异步处理。

 

不同的 CDN 分散在不同的地区,必然存在不同的用户访问量。所以有些 CDN 节点的命中率高,有些 CDN 节点的命中率低。让 CDN 缓存快速失效时,可以优先选择命中率高的节点。

 

注意:CDN 服务器的数量不要太多,因为太多了也会影响通知各节点快速失效的效率,而且 CDN 节点尽量都选择在距离系统大部分用户比较近的地方。这样万一 CDN 节点要失效缓存,也可以让失效的速度快一些,成本低一些。

 

(8)基于定时授时的前后端时钟同步处理


用户进入秒杀商品活动页面后,就要等待秒杀活动开始。此时一般需要前端定时访问后端的一个授时服务接口,进行时钟同步。



(9)秒杀抢购接口地址的动态隐藏处理


不应该直接在前端页面里提前暴露秒杀抢购的接口,以防恶意访问。而应该把秒杀抢购的接口做成动态 URL,直到秒杀开始前 1 分钟,才让秒杀前端页面发送请求到后台,去获取动态的秒杀抢购接口 URL,而且访问该 URL 时要带上一个随机字符串的 md5 加密值才能允许访问。

 

3.秒杀系统的负载均衡方案底层相关


(1)LVS(Linux Virtual Server)介绍


LVS 集群的负载均衡器会接收服务的所有客户端请求,然后根据调度算法决定哪个集群节点应该处理和响应请求,负载均衡器(简称 LB)有时也被称为 LVS Director。

 

一.LVS 虚拟服务器的体系结构


一组服务器通过高速的局域网或地理分布的广域网相互连接,在它们的前端有一个负载均衡调度器(Load Balancer)。

 

二.LVS 工作原理


负载均衡调度器能无缝地将网络请求调度到真实服务器上,从而使得服务器集群的结构对客户是透明的。用户访问 LVS 集群提供的服务就像访问一台高性能、高可用的服务器,服务程序不受 LVS 集群的影响,无需作任何修改。系统的伸缩性通过在服务集群中透明地加入和删除一个节点来达到,通过检测节点或服务进程故障和正确地重置系统达到高可用性。注意:LVS 的负载均衡调度技术是在 Linux 内核中实现的。



(2)异地多机房多活 LVS 集群部署介绍


异地多机房多活的 LVS 集群部署:假设有一个国际化站点在不同的国家都部署了一个机房,这些机房都存储了同样的数据,互相间会进行数据交换和同步。然后该国际化站点会通过 LVS 集群部署来共享一个虚拟 IP 地址,这样用户在访问站点时,就会解析这个共享的虚拟 IP 地址,把请求路由到用户国内的机房。

 

(3)基于 NAT 模式实现的 LVS 请求转发原理


一.NAT 模式的原理简介


Virtual Server via Network Address Translation (VS/NAT)。通过 NAT 网络地址转换,负载均衡调度器 LB 会重写请求报文的目标地址。然后根据预设的调度算法,将请求分派给后端的真实服务器,真实服务器处理请求返回响应报文时必须要通过负载均衡调度器 LB。经过负载均衡调度器 LB 时,响应报文的源地址也会被重写。响应报文重写完源地址后再返回给客户端,从而完成整个负载调度过程。

 

NAT 模式类似公路上的收费站,来去都要经过 LB 负载均衡器。通过修改目标地址端口或源地址端口,来完成请求和响应的转发。

 

二.NAT 模式的工作流程


客户端通过虚拟 IP 地址访问服务时,请求报文先到达负载均衡调度器 LB。此时负载均衡调度器会根据调度算法从一组真实服务器中选出一台服务器,然后将请求报文的目标地址(VIP)改写成选定服务器的地址(RIP)。请求报文的目标端口改写成选定服务器的相应端口(RS 提供的服务端口),最后将修改后的报文发送给选出的服务器 RS(Real Server)。

 

同时,负载均衡调度器 LB 会在一个 Hash 表中记录这个连接。这样当这个连接的下一个报文到达时,就可以从连接的 Hash 表中,获取原来选定的服务器地址端口,并改写地址将报文传给该服务器(RS)。

 

当来自真实服务器 RS 的响应报文返回负载均衡调度器 LB 时,LB 会将响应报文的源地址端口改为 VIP 和相应端口,然后再返回给客户端。

 

三.NAT 模式的补充说明


一般 LVS 服务器会对外提供一个 VIP,就是虚拟服务器的 IP 地址。所有客户端都会访问这一个虚拟 IP 地址,也就是 LVS 服务器的地址。接着 LVS 服务器收到请求后,会基于 NAT 技术进行地址改写。

 

比如浏览器要对某服务发送请求,此时需要将该请求发送给 LVS 服务器。首先会基于 TCP/IP 协议,和 LVS 服务器在进行 TCP 三次握手建立 TCP 连接。然后在 TCP 连接基础上,将 HTTP 请求报文发送到 LVS 上。接着 LVS 再把这个 HTTP 请求报文转发给某服务的 Tomcat 服务器。最后 Tomcat 获取到一个完整的 HTTP 请求报文后,就可以处理请求了。

 

注意:LVS 是工作在四层网络协议上的负载均衡技术,转发的是报文。Nginx 是工作在七层网络协议上的负载均衡技术,转发的是请求。第四层的网络协议是 TCP/IP 协议,第七层的网络协议是 HTTP 协议。



(4)基于 IP 隧道模式的 LVS 请求与响应分离原理


一.IP 隧道模式的原理简介


采用 NAT 技术时,由于请求和响应的报文都经过调度器 LB 进行地址重写。当客户请求越来越多时,负载均衡调度器 LB 的处理能力将成为瓶颈。为解决这个问题,LB 会把请求报文通过 IP 隧道转发至真实服务器,而真实服务器将响应处理后直接返回给客户端用户,这样调度器就只需要处理请求的入站报文。

 

由于一般而言,服务的响应数据比请求报文大很多,所以采用 IP 隧道模式后,集群系统的最大吞吐量可以提高 10 倍。

 

二.IP 隧道模式的工作流程


IP 隧道模式的连接调度和管理与 NAT 模式一样,只是报文转发方法不同。LB 会先根据各个服务器的负载情况、连接数多少,动态选择一台服务器。然后将原请求的报文封装在另一个 IP 报文中,接着再将封装后的 IP 报文转发给选出的真实服务器。真实服务器收到报文后,先将其解封获得原来目标地址为 VIP 地址的报文。真实服务器发现 VIP 地址被配置在本地的 IP 隧道设备上(此处要人为配置),所以就处理这个请求,然后根据路由表将响应报文直接返回给客户端。

 

注意,根据 TCP/IP 协议栈处理:由于请求报文的目标地址为 VIP,响应报文的源地址肯定也为 VIP,所以响应报文不需要做任何修改,可以直接返回给客户。客户认为得到正常的服务,而不会知道究竟是哪一台服务器处理的。



(5)LVS 的多种负载均衡算法


一.轮询调度算法(Round-Robin)该算法会将请求依次分配不同的RS节点,也就是在RS节点中均摊请求。这种算法简单,但是只适合于RS节点处理性能相差不大的情况。
二.加权轮询调度算法(Weighted Round-Robin)该算法会依据不同RS节点的权值来分配请求。权值较高的RS将优先获得请求,相同权值的RS得到相同数目的请求数。
三.目的地址哈希调度算法(Destination Hashing)以目的地址为关键字查找一个静态Hash表来获得需要的RS。
四.源地址哈希调度算法(Source Hashing)以源地址为关键字查找一个静态Hash表来获得需要的RS。
五.加权最小连接数调度算法(Weighted Least-Connection)假设各台RS的权值依次为Wi(i=1..n),当前的TCP连接数依次为Ti(i=1..n),依次选取Ti/Wi为最小的RS作为下一个分配的RS。
六.最小连接数调度算法(Least-Connection)IPVS表存储了所有的活跃的TCP连接,把新的连接请求发送到当前连接数最小的RS。
七.基于地址的最小连接数调度算法将来自同一目的地址的请求分配给同一台RS节点。
八.基于地址带重复最小连接数调度算法对于某一目的地址,对应有一个RS子集。对此地址请求,为它分配子集中连接数最小RS。
复制代码


(6)LVS 的 Linux 内核级实现原理


一.LOCAL_IN 链和 IP_FORWARD 链的 IPVS 模块处理报文改写和转发


LVS 实际上是在 Linux 内核里修改了 TCP/IP 协议栈,这样可以对收到的请求直接在 Linux 内核层面进行地址改写和转发。由于 LVS 运行在内核层面,这让它的性能和吞吐量都极高。LVS 有个 IPVS 模块挂载在了内核的 LOCAL_IN 链和 IP_FORWARD 链。

 

当一个请求报文到达时,如果目标地址是 VIP,就会转交给 LOCAL_IN 链,然后请求报文就会被挂载在 LOCAL_IN 链上的 IPVS 模块处理。IPVS 模块会根据负载均衡算法选择一个 RS,对报文进行改写和转发,接着会在 Hash 表中记录这个连接和转发的后端服务器地址。这样报文再到达时,就可根据 Hash 表里的连接对应的服务器地址来转发。

 

当一个响应报文返回时(NAT 模式下),就会交给 IP_FORWARD 链,然后响应报文就会被挂载在 IP_FORWARD 链上的 IPVS 模块处理。也就是改写响应报文的地址,返回给客户端。

 

由于在 LVS 中 Hash 表的一个连接数据只要 128 字节,所以一台 LVS 服务器可以轻松调度几百万个连接。

 

二.Hash 表的超时连接通过时间轮和分段锁来移除


早期 LVS 会对 Hash 表里的连接设置一个定时器,连接超时就会回收该连接。但如果有几百万个连接,那么可能会导致一个很严重的问题。那就是有几百万个定时器,这么多定时器一起运行会导致很大的 CPU 负载。后来采用了 Kafka 的时间轮机制来改进。

 

所以如果要在内存里对数万甚至数十万的任务进行超时监控,最好不要每个任务都设置一个定时器,因为会对 CPU 和内存消耗极大。

 

Kafka 的时间轮机制大概就是:不同的时间轮会有不同的时间周期,可以把不同的超时时间的连接放在不同的时间轮格子里,让一个或者多个指针不停的旋转,每隔一秒让指针转一下,这样就可以让不同的时间格里的连接超时失效。

 

其次,更新 Hash 表时使用分段锁。也就是把 Hash 表拆成很多个小分段,不同的分段一把锁。这样可以降低锁的粒度,减少高并发过程中的锁的频繁冲突,跟 ConcurrentHashMap 是一个原理。

 

(7)基于七层网络协议的负载均衡技术如何运作


LVS 是运行在四层网络协议上的负载均衡技术,即对 TCP 报文进行转发。对 LVS 来说,没有 HTTP 这样的概念,它只关注最底层的网络报文。

 

如果要实现运行在七层网络协议上的负载均衡技术,即对 HTTP 请求转发,那么最大的问题在于:存在大量的内核空间和用户空间的切换。

 

首先,需要经过多次报文交互后建立好一个 TCP 连接。然后,获取通过 TCP 连接发送过来的完整 HTTP 协议请求。接着,从内核空间切换到用户空间,将 HTTP 协议请求交给用户空间运行的一个负载均衡技术去处理,也就是根据请求里的一些内容来将请求转发给真实的后端服务器。最后,从用户空间切换到内核空间,跟真实的后端服务器建立 TCP 连接,把 HTTP 协议请求发送过去。

 

之后,真实后端服务器返回响应时,又会从内核空间切换到用户空间,把响应转交给用户空间的负载均衡技术来处理。处理完后,从用户空间切换到内核空间,将响应通过内核发送给客户端。

 

一般涉及到用户空间的系统,单机每秒可抗几千甚至几万请求,但是并发和吞吐远远低于不涉及用户空间的系统。比如 LVS 单机每秒可抗几万到几十万甚至百万的请求,因为 LVS 直接由 Linux 内核进行处理,无须切换内核空间到用户空间。但运行在七层网络协议上的负载均衡,可以根据 HTTP 请求进行路由转发。

 

(8)结合四层协议的 LVS + 七层协议的 Nginx 来使用


四层协议的 LVS 和七层协议的 Nginx 通常会结合在一起来使用。因为在七层协议上进行负载均衡的性能远不如 LVS,而仅仅在四层协议上进行负载均衡的 LVS 又不能进行一些高阶的转发,也就是没有办法根据 HTTP 请求的内容去进行一些高阶的功能和转发。而基于七层协议的 Nginx 则可根据 HTTP 请求的内容进行很多高阶处理,比如可以在 Nginx 里嵌入 lua 脚本、在 Nginx 本地处理请求、读取缓存等。

 

所以通常的做法是:在负载均衡最外侧,部署一个基于四层协议的 LVS 作为核心的负载均衡的设备。通过对 LVS 进行大量的调优和优化,可以轻松做到单机百万级的并发量。然后 LVS 会将请求转发到基于七层协议的 Nginx,让 Nginx 根据 HTTP 请求的内容进行进一步转发。



(9)KeepAlived + LVS 高可用及 Nginx 集群高可用


Keepalived 一开始就是专为 LVS 设计的,专门用来监控 LVS 集群系统中各个服务节点的状态,但是后来又加入了 VRRP 的功能。因此 Keepalived 除了配合 LVS 服务外,也可作为其他服务的高可用软件,比如 Nginx、Haproxy、httpd、MySql。

 

Keepalive 服务在 LVS 中的两大用途:

一.healthcheck(健康检查)

二.fallover(失败接管)

 

(10)同步异步以及阻塞非阻塞的区别


同步和异步是指通信模式,常用来描述 RPC 网络通信,比如同步 RPC 或异步 RPC。同步就是调用方一直等待响应,异步就是调用方发出请求后直接返回,有结果返回时再回调调用方。

 

阻塞和非阻塞是指 IO 调用模式,常用来描述基于 Socket 的 IO 操作。比如阻塞 IO 和非阻塞 IO,阻塞 IO 就是线程挂起来等待 IO 结果,非阻塞 IO 就是线程不会挂起来等待一个 IO 操作的结果。

 

一.同步阻塞


同步指的是客户端发出请求后,一直同步等待响应。服务端收到请求后,要执行 IO 操作,如磁盘 IO、网络 IO、数据库 IO 等。此时服务端针对所有的 IO 操作,都会阻塞等待 IO 结果。无论是网络 IO 调用其他服务,还是磁盘 IO 读写本地文件。

 

二.同步非阻塞


同步指的是客户端发出请求后,一直同步等待响应。服务端收到请求后,发现 IO 操作没法直接完成,直接去处理其他 IO。但此时不返回响应给客户端,等非阻塞 IO 完成了,再把结果返回给客户端。

 

三.异步阻塞


客户端发出请求后不等待响应,服务端收到请求后阻塞式 IO,IO 完成后再通知客户端。一般很少使用异步阻塞模式。

 

四.异步非阻塞


客户端发出请求后不等待响应,服务端收到请求后非阻塞 IO,IO 完成后再通知客户端。IO 不能马上完成就去处理其他 IO,等 IO 完成后再通知客户端,Nginx 就是使用了异步非阻塞模型。

 

(11)抗下高并发的服务器架构模式


一.多进程模式


有一个主进程,每当收到一个请求就交给一个子进程来进行处理。首会预生成一些子进程,然后处理完请求后不回收子进程,而是通过池化进行管理。Apache 服务器就是这种多进程模式,但现在不太流行了,Nginx 也是多进程模式。如果采用高配置的物理机,那么就可以开辟很多进程,大量的进程就可以高性能高并发地处理大量高并发请求。

 

二.单进程多线程模式


Tomcat 是轻量级的 Servlet 容器服务器,采用了单进程多线程模式。Tomcat 会基于 NIO,使用有限的线程资源,抗下大量的高并发。

 

(12)Nginx 的三大核心架构


异步非阻塞架构 + 多进程架构 + 模块化架构

 

Nginx 启动后会有一个主进程和多个子进程,采用的是异步非阻塞模式。主进程负责建立、绑定和关闭 Socket 网络连接,子进程负责处理客户端请求。

 

一个子进程收到请求后,客户端可以直接去处理其他事情(异步模式)。如果子进程发现 IO 操作不能马上处理,那么就会去处理别的请求。这些 IO 操作比如是读取本地磁盘的 html 或请求后端的 Tomcat 服务器,等 IO 操作执行完毕后,Linux 内核会通知子进程,子进程再通知客户端。

 

此外,Nginx 还会把自己内部的大量功能做成了很多模块,这种模块化架构设计非常容易开发者进行自定义扩展。

 

(13)为什么 Nginx 之后还要接入一个网关


为了避免频繁上下线微服务系统时,出现频繁修改 Nginx 配置的情况。

 

(14)使用独立二级域名隔离秒杀系统与电商系统


假设域名 mall.demo.com 指向了一个电商网站,用户都是通过访问该域名来进行浏览商品、生成订单、支付订单等操作。

 

如果用户也通过该域名来访问秒杀系统、进行秒杀抢购,甚至将秒杀系统和电商系统部署在一起,都在一批机器上,那么系统或机器的瞬时流量将可能超高,影响网站正常请求。

 

所以最好将电商系统和秒杀系统的二级域名部署到不同的 LVS+Nginx。电商系统:mall.demo.com --> 秒杀系统:seckill.demo.com。

 

此外,秒杀商品的详情页所需的静态资源文件最好也部署到 CDN 上去。尤其是商品详情页里可能包含大量的高清图片、甚至视频,尽量避免秒杀时的大流量撑满 LVS + Nginx 服务器的网络带宽,影响并发。

 

4.秒杀系统的限流机制和超卖问题处理


(1)如何使用云厂商的 DDoS 高防产品防止黑客攻击


如果秒杀活动刚开始,黑客就对秒杀系统发起 DDoS 攻击,那么就很容易将 LVS -> Nginx -> Tomcat 服务器资源消耗完,从而影响正常用户的秒杀请求。

 

为了避免 DDoS 攻击,可以使用云厂商的 DDoS 高防商业产品。首先将独立的二级域名解析到 DDoS 高防产品去,然后再将 DDoS 高防产品的源站地址解析到负载均衡服务器上。这样所有请求都会先经过 DDoS 高防产品,其中 DDoS 攻击请求会被过滤掉,合法请求才会到负载均衡服务器上。

 

(2)基于云厂商的验证码产品拦截黄牛和黑客请求


验证码产品会基于大数据和 AI 验证请求是否合法。如果请求合法,则判定验证码滑动通过,将请求发送到秒杀系统后端。

 

(3)开发一套秒杀系统的反作弊机制


比如一个用户在一个抢购场次内,不允许对同一个商品重复抢购,只允许一个用户对同一个商品发起指定次数的抢购。比如分析历史请求日志,看看哪些 IP 和用户喜欢每个秒杀场次都参加。反作弊机制,往往需要对用户日常行为进行分析,来判断请求是否合理。

 

(4)基于限流算法对秒杀系统进行整体限流


在 Nginx 环节,可以基于 Lua 脚本实现秒杀系统的整体性限流。即 Nginx 会允许多少请求可以访问秒杀系统,而限流算法可以选择令牌桶算法和漏桶算法。

 

(5)如何基于 Nginx + Lua 实现一套业务限流机制


一.整体限流机制


首先使用 DDoS 高防产品防 DDoS 攻击,然后通过验证码拦截非法请求,接着开发作弊系统防止作弊刷单,最后先通过 Nginx 进行整体限流、再通过 Redis 进行业务限流。

 

二.业务限流机制


首先在 Nginx 层进行整体限流,然后再通过 Redis 进行业务限流。所谓业务限流,对于秒杀系统而言,就是限制每个商品的购买数量。所以一般来说,业务限流会基于 Redis + Lua 来实现。

 

三.限流机制应用举例


假设当前有几十万的瞬时并发请求,其中的 10 万是来自真实用户的秒杀抢购请求、几十万是 DDoS 攻击请求、几千是黄牛的非法请求、几百是某些用户的作弊请求。

 

那么经过整体限流机制后:几十万的 DDoS 攻击请求会被过滤掉、几千的非法请求会被验证码拦截掉、几百的用户作弊请求会被作弊系统禁止掉,最后进入 Nginx 的请求有 10 万。

 

那么经过 Nginx 层的业务限流机制后,这 10 万请求又可能会过滤掉比如 5 万,最终有 5 万的请求会进行业务限流。如果商品限购数量为 2 万,那么最终又会过滤掉 3 万请求,而放行其中的 2 万请求。这放行的 2 万请求可抢购到商品,8 万请求被整体限流和业务限流过滤了。对于基于 Tomcat 部署的秒杀系统而言,最后收到的最多就是大约 2 万请求。

 

(6)秒杀抢购是否可以基于数据库来实现


利用 MySQL 数据库的行锁来实现抢购效果的隐患:比如 update stock set 库存 = 库存 - 1。虽然数据库的行锁可保证多线程并发更新一行数据时,是串行执行。但如果秒杀的库存数量有 1 万,然后并发涌入 1 万请求,则容易击垮数据库。

 

一般都会使用 Redis 或其他 NoSQL 来存放秒杀商品的库存数量。在秒杀开始前,把秒杀商品的库存提前加载到 Redis 缓存里。进行秒杀时,就扣减 Redis 里的库存,直到 Redis 里的库存扣减完毕。并在监控平台实时展示秒杀商品的:可销售库存、锁定库存、已销售库存。

 

(7)秒杀商品库存写入 Redis 的 Hash 数据结构


可以使用 Redis 的 Hash 结构来存储秒杀商品的库存信息,在 Hash 结构中存储可销售库存、锁定库存、已销售库存,其中会使用 Redis 的 hincrby 命令和 hdecrby 命令来增加库存和扣减库存。


seckill::product::123 = {    sale_stock_amount: 100,//可销售库存    locked_stock_amount: 0,//锁定库存    saled_stock_amount: 0//已销售库存}
复制代码


(8)基于 Redis 缓存实现的秒杀抢购细节


秒杀开始前,需要提前将商品库存写入到 Hash 中的可销售库存里。秒杀开始时,会读取 Hash 中的可销售库存,判断是否可以购买。如果可以购买就扣减可销售库存,并增加锁定库存。之后执行异步下单流程:即等用户支付完订单后,扣减锁定库存,并增加已销售库存。

 

(9)秒杀过程中库存超卖问题的解决方案


秒杀抢购过程中可能出现的库存超卖问题的解决方案如下:(其实这些方案也可以用来解决分布式环境下更新数据的顺序性问题)


方案一:使用分布式锁(悲观锁)


比如首先通过 Redisson 框架去 Redis 中获取一个名为"seckill::product::123::lock"的锁,在某个线程获得锁的期间不会有其他线程去查询和更新库存数据。此时该线程可以放心地查询和扣减库存,之后再释放锁。

 

但这种加分布式锁的方案并不适合高并发场景。Redisson 分布式锁的实现是通过 Lua 脚本 + Watch Dog 来实现锁等待的,每个获取不到锁的线程都会不停轮询然后尝试加锁,中间都有一个等待的过程。在等待的过程中,这些线程就可能会产生很多网络通信开销,影响性能。

 

获取锁的线程释放锁之后,其他线程还都处于锁等待的过程中。此时过了几十 ms 才会有下一个线程获取锁,所以分布式锁的整体并发能力不是太好。

 

方案二:请求放入 FIFO 内存队列


比如秒杀抢购系统将所有进来的抢购请求全部放入同一个内存队列中排队,按照先进先出的顺序从内存队列中出队,然后再去 Redis 里执行扣减逻辑。

 

这种方案需要注意:把对某个商品的抢购请求都路由到一台机器上,进入同一个内存队列,从而保证对这个商品的抢购都是有序的。

 

内存队列的方案,可以严格保证抢购请求的处理顺序。但是万一系统重启或宕机,就会导致内存队列里的数据丢失。以及如果抢购请求特别多,可能会导致内存队列占用大量内存频繁 GC。

 

方案三:使用乐观锁


乐观锁方案要求每个库存数据都绑定一个版本号,每次更新库存时先用乐观锁判断,库存是否发生过改变,如果没发生过改变才可以更新。

 

可以使用 Redis 的 Watch 机制 + pipeline 来实现这种乐观锁。首先 Watch 一些数据是否有变化,然后通过 pipeline 一次性提交多个操作。如果数据有变化,那么 pipeline 就会提交失败。如果数据没变化,那么 pipeline 就会提交成功。提交的 pipeline 的最后一个操作,需要获取库存的最新值。如果发现库存是负数,那么就表示秒杀失败。注意:pipeline 虽然可以保证多个操作命令顺序执行,但却不是原子性的,因为不能保证事务(同时成功和同时失败)。

 

乐观锁方案可能会很耗费 CPU。比如某线程更新库存时,因为库存版本已被其他线程频繁修改而一直失败。此时该线程需要重新查询最新的版本,重新发起更新。所以乐观锁方案可能会导致很多秒杀抢购请求都处于轮询状态。

 

方案四:封装 Lua 脚本(建议的方案)


一个秒杀抢购请求对应一段 Lua 脚本,直接将 Lua 脚本提交到 Redis 中执行。Redis 可以保证 Lua 脚本按顺序执行,且每个 Lua 脚本的执行都是原子的。从而避免了超卖问题,也大大降低了对性能影响和受系统风险的影响。

 

具体来说就是:把查询库存数量是否可以抢购 + 更新库存字段 + 判断是否抢购成功等,这些秒杀抢购过程中涉及到的核心逻辑都封装在一个 Lua 脚本里,然后提交这个 Lua 脚本到 Redis 中去执行。这样即便每秒上万请求,也就是提交上万个 Lua 脚本到 Redis 内存里执行。

 

此外,这种 Lua 脚本方案还可以支持进行库存分片优化。因为毕竟 Redis 是单线程的,通过实现 Redis 库存分片可以提升并发性能。假设 Redis 集群部署了三个 Master,那么每个商品库存可分成三个分片,从而实现对一台 Redis 机器的压力均匀分散到三台 Redis 机器上。

 

方案五:库存放入 Redis 队列


提前把商品库存写入到 Redis 的一个队列中,抢购时再从队列中弹出库存。比如秒杀商品库存为 10,那么就把 10 个库存数据写入 Redis 的一个队列。用户发起秒杀抢购请求时,再不停地从队列里出队。如果从队列中获取不到数据了,则说明抢购结束了。

 

但是这种方案,不适合库存数量特别多的情况,否则可能会出现占用大量内存的问题,比如库存数量有 10000 等。

 

(10)秒杀成功后的异步下单与支付


秒杀成功后,就会立刻返回响应给客户端,并进行异步下单和支付。此时客户端的秒杀页面会显示:"秒杀抢购进行中,请耐心等待"。之后客户端会每隔 1s 发送一个请求到后台查询,秒杀订单是否已生成。

 

(11)秒杀系统是否还需要处理限流、防刷和防重


其实不需要了,秒杀系统只需要关注秒杀抢购的逻辑即可,因为限流、防刷、防重已在 LVS + Nginx + Lua 中处理了。

 

5.秒杀系统的异步下单和高可用方案


(1)使用 MQ 来进行秒杀下单削峰


MQ 的三大作用是:削峰、异步提升性能、解耦。由于秒杀时的抢购请求可能会远远超出订单系统的日常下单请求,比如日常的下单请求数高峰是每秒几百 QPS,而秒杀时的下单请求数则是每秒几千。所以面对这样的请求高峰,为了保护订单系统,最好使用 MQ 进行下单削峰。

 

(2)秒杀异步下单的流量控制处理


秒杀的异步下单可以使用线程池进行流控去请求订单系统。比如部署 2 台服务器,每台机器开启几十个线程消费 MQ 的抢购成功消息。

 

(3)下单成功后的订单支付逻辑与未支付逻辑


用户抢购秒杀商品成功后,会在客户端界面里进行等待。此时客户端可能会不断轮询订单列表,或者用户也可能会刷新订单列表。当客户端发现订单已创建好了,那么用户就可以对订单进行支付。

 

当订单系统成功创建一个订单时,会发送一条延迟消费的消息到 MQ 里,比如发送一条延迟 30 分钟才消费的消息到 MQ 中。这样当订单系统在 30 分钟后消费这个消息时,会检查订单支付状态。如果发现订单还没支付就会直接关闭订单,并且释放锁定的库存。

 

使用 MQ 这类消息中间件时,需要注意三个问题:消息丢失问题、消息重复问题、消息积压问题。

 

(4)MQ 消息零丢失的处理


一.发送消息到 MQ 的零丢失


方案一:同步发送消息 + 反复多次重试

方案二:事务消息机制

 

两者都有保证消息发送零丢失的效果,但是经过分析,事务消息方案整体会更好一些。

 

二.MQ 收到消息之后的零丢失


开启同步刷盘策略 + 主从架构同步机制

 

只要让一个 Broker 收到消息后同步写入磁盘,同时同步复制给其他 Broker,然后再返回响应给生产者表示写入成功,此时就可保证 MQ 不会丢消息。

 

三.消费消息的零丢失


RocketMQ 的消费者天然就可以保证:处理完消息后,才会提交消息的 offset 到 Broker 去,所以只要注意避免采用多线程异步处理消息的方式即可。

 

(5)MQ 消息重复的处理


一般来说,往 MQ 里重复发送同样的消息是可以接受的。因为即使 MQ 里有多条重复消息,也不会直接影响系统的核心数据。所以关键要保证:消费者从 MQ 里获取消息进行处理时,消息不能重复。要保证消息消费的幂等性,优先推荐的是业务判断法,而非状态判断法。

 

一.业务判断法


直接根据数据库存储中的记录判断消息是否处理过,如果已经处理过了,那么就别再次处理了。

 

二.状态判断法


基于 Redis 的消息发送状态来判断,在极端情况下没法完全保证幂等性。

 

(6)MQ 消息积压的处理


方案一:让订单直接在 Redis 中创建


为了避免 MQ 消息积压,导致出现订单迟迟没创建成功的问题,可能会想到先让订单在 Redis 中创建这么一种解决方案。虽然该方案可以让用户马上基于 Redis 来查询订单,并进行支付。但是整个订单的管理链路需要与 Redis 耦合在一起了,改造起来并不优雅。比如可能需要考虑:订单列表的分页展示要基于 MySQL + Redis 来实现。

 

方案二:释放锁定库存的降级处理


消费 MQ 消息时,先检查消息的消费时间和写入时间之差是否已超某阈值。比如消费消息的时间减去消息写入 MQ 的时间已超过 2 分钟,而一般情况下消息从写入 MQ 开始到完成消费只需 20s 左右,那么此时就可认为 MQ 出现了比较严重的消息积压问题,就可以启动针对消息积压而释放锁定库存的降级方案。

 

具体而言就是:对于消费消息的服务端,调用 Redis 的接口释放锁定的库存。对于用户前端,可设定超 2 分钟还没获取订单创建成功的通知则跳转提示。提示用户:秒杀抢购失败,需要重新进行秒杀下单。

 

由于释放秒杀库存是非常快的,所以积压的 MQ 消息可以非常快处理完。当然释放库存的过程中,可以消费积压的 MQ 消息继续正常创建订单。其实这个方案采取的思路就是丢弃 MQ 消息的思路。

 

(7)秒杀系统的全链路高可用架构设计


LVS高可用:LVS双机器部署 + KeepalivedNginx高可用:多机器冗余部署 + LVS会做负载均衡秒杀抢购服务:多机器冗余部署 + Nginx会做负载均衡Redis Cluster:Master-Slave主从架构 + 自动故障切换RocketMQ:集群部署 + 多副本冗余秒杀下单服务:多机器冗余部署秒杀库存服务:多机器冗余部署
复制代码


(8)Redis 集群崩溃时秒杀系统的自动恢复处理


按照上述的高可用架构方案,其实任何一个环节里的任何一台机器宕机,都不会影响系统正常的运行,但就是怕有的环节直接全面崩溃。

 

比如整个 Redis 集群都崩溃了,那么就没法进行秒杀抢购了。如果此时向所有请求都返回抢购失败,那么用户体验也不好,因为这样可能会导致这次秒杀活动彻底失败。

 

此时比较好的方案是:首先将用户秒杀抢购的日志顺序写入本地磁盘,进入 OS Cache。然后返回秒杀状态通知用户,目前商品正在抢购,让用户耐心等待。用户收到通知后,可能会停留在页面等待,可能会进入秒杀商品详情页。其中秒杀状态可以设计为如下:抢购失败、抢购进行中、抢购成功、完成创建订单、完成支付订单等。

 

由于大部分流量在 LVS + Nginx 层拦截了,只有少部分流量进入秒杀系统,所以写入到本地磁盘的用户秒杀抢购日志,也不会太多。

 

当紧急修复或重启 Redis 集群后,就可以让秒杀系统的后台线程自动探查 Redis 是否恢复。如果恢复了,那么就顺序读取本地磁盘的秒杀流水日志,通过流量重放重新去 Redis 执行抢购流程即可。

 

(9)Redis 主节点崩溃时没及时同步导致超卖问题


如果基于 Redis 来做秒杀抢购,那么一般会使用 Redis Cluster。而 Redis Cluster 又是主从同步架构 + 异步复制的,如果 Master 节点宕机,没来得及把库存扣减操作同步到 Slave 节点。当 Slave 节点切换成 Master 节点后,就可能会导致库存超卖问题。

 

这个 Redis 异步复制导致的库存超卖问题比较难解决。根据 CAP 理论,保证了 AP,就必然不能保证 C,所以会出现超卖。为了保证不出现超卖,就要舍弃 A,保证 CP。除非改造 Redis 源码,把异步复制做成同步复制。任何一个秒杀抢购请求在 Master 执行完毕后,库存数据必须从 Master 节点复制到 Slave 节点后,才能认为数据操作成功。但如果改造成同步复制,同步复制又会大大降低 Redis 的高性能。

 

方案一:取消 Redis 主从同步


可以用 Codis 对几台机器上的 Redis 做分布式数据存储和路由,取消 Slave。如果秒杀系统的服务器发现其连接的 Redis 宕机,就将该秒杀系统负责的用户抢购操作转化为流水日志写入本地磁盘文件。等到连接的 Redis 机器恢复后,再重放流水日志去执行抢购流程。这种方案不仅可以解决库存超卖问题,还能实现 Redis 崩溃时的高可用。

 

注意:虽然 Codis 取消了 Slave,但为了让 Redis 的数据不丢失,也可以在 Codis 机器上缓存写命令。

 

方案二:订单系统在创建秒杀订单时进行检查


当订单系统在消费抢购成功的 MQ 消息,准备创建秒杀订单时,检查可销售库存、锁定库存、已销售库存,是否出现异常。如果出现异常则不允许创建订单,然后通知用户创建秒杀订单失败。

 

方案三:自研中间件


借鉴 Redis 的高性能机制、RocketMQ 的高可用机制,开发一个类似的中间件,专门用来存储和扣减秒杀商品的库存。

 

秒杀过程中可能出现库存超卖问题的解决方案:

一.使用分布式锁(悲观锁)

二.请求放入 FIFO 内存队列

三.使用乐观锁

四.封装 Lua 脚本

五.库存放入 Redis 队列

 

(10)MQ 集群崩溃时写内存刷磁盘的处理


MQ 集群崩溃,只不过是无法创建秒杀订单而已,抢购还是能正常执行的。所以当 MQ 集群崩溃时,完全没必要让秒杀系统等待 MQ 恢复。此时依然可以直接返回是否抢购成功给用户,并将抢购成功消息写入内存。然后再将内存中的抢购成功消息刷入本地磁盘文件。接着后台开启一个线程,慢慢读取磁盘数据并直接调用创建订单接口。从而绕过了从 MQ 消费抢购成功的消息,以较低的速度创建抢购订单。

 

(11)订单系统异常时的 MQ 重试队列与死信队列


如果订单系统出现异常,对 MQ 里的抢购成功消息没能消费成功,此时可以基于 MQ 的重试队列进行重试。如果重试多次都不行,则需要进入 MQ 的死信队列。MQ 会有专门处理死信的线程,比如每隔 1 小时再进行反复重试。

 

如果订单系统出现异常,其实也就意味着 MQ 消息会出现积压,此时的解决方案还可以是:若发现 MQ 消息积压超过某个时间,则发送另一条快速失败的消息到 MQ,让非订单系统消费该快速失败的消息,直接修改 Redis 去释放库存。

 

(12)秒杀抢购成功时的异构存储


用户抢购秒杀商品成功后,要修改 Redis 的库存,也要发送消息到 MQ,这时就需要解决异构存储下的一致性问题了。否则可能出现 Redis 的库存修改成功,但是消息没有发送到 MQ。

 

由于 Redis 是支持事务机制的,所以用户秒杀抢购成功时,可以开启一个 Redis 事务。在 Redis 事务里,先更新 Redis 的数据,然后发送消息到 MQ。如果发送消息到 MQ 成功了,那么才提交 Redis 事务,最好返回抢购成功的信息给用户。

 

如果发送消息给 MQ 失败了,此时可以执行降级方案,也就是写 MQ 消息到内存并刷入本地磁盘。只要降级方案执行成功,那么这个 Redis 事务也算成功。

 

如果开启 Redis 事务成功,发送消息到 MQ 成功,但提交 Redis 事务失败。那么此时可使用 MQ 的事务机制,通过 Redis 事务 + MQ 事务保证一致性。


文章转载自:东阳马生架构

原文链接:https://www.cnblogs.com/mjunz/p/18898587

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
秒杀系统—架构设计和方案简介_架构_量贩潮汐·WholesaleTide_InfoQ写作社区