大纲
1.秒杀系统的方案设计要点
2.秒杀系统的数据 + 页面 + 接口的处理方案
3.秒杀系统的负载均衡方案底层相关
4.秒杀系统的限流机制和超卖问题处理
5.秒杀系统的异步下单和高可用方案
1.秒杀系统的方案设计要点
(1)秒杀促销活动的数据处理
(2)秒杀促销活动的页面处理
(3)秒杀场景下的负载均衡架构
(4)秒杀场景下的高并发抢购处理
(5)秒杀场景下的异步下单处理
(6)秒杀成功后的业务逻辑处理
(7)秒杀系统的高可用设计
(8)秒杀系统的压测 + 故障演练以及实时大盘
(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)秒杀活动的描述
(2)秒杀活动管理系统的功能
(3)秒杀活动数据进行数据库 + 缓存双写
(4)秒杀活动数据存储在Redis的List和Hash中
(5)秒杀活动页面数据的动静分离
(6)基于CDN来缓存秒杀活动静态页面
(7)CDN静态数据缓存的失效与命中
(8)基于定时授时的前后端时钟同步处理
(9)秒杀抢购接口地址的动态隐藏处理
(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)介绍
(2)异地多机房多活LVS集群部署介绍
(3)基于NAT技术实现的LVS请求转发原理
(4)基于IP隧道模式的LVS请求与响应分离原理
(5)LVS的多种负载均衡算法
(6)LVS的Linux内核级实现原理
(7)基于七层网络协议的负载均衡技术如何运作
(8)结合四层协议的LVS + 七层协议的Nginx来使用
(9)KeepAlived + LVS高可用及Nginx集群高可用
(10)同步异步以及阻塞非阻塞的区别
(11)抗下高并发的服务器架构模式
(12)Nginx的三大核心架构
(13)为什么Nginx之后还要接入一个网关
(14)使用独立二级域名隔离秒杀系统与电商系统
(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模式的原理简介
二.NAT模式的工作流程
三.NAT模式的补充说明
一.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隧道模式的原理简介
二.IP隧道模式的工作流程
一.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高防产品防止黑客攻击
(2)基于云厂商的验证码产品拦截黄牛和黑客请求
(3)开发一套秒杀系统的反作弊机制
(4)基于限流算法对秒杀系统进行整体限流
(5)如何基于Nginx + Lua实现一套业务限流机制
(6)秒杀抢购是否可以基于数据库来实现
(7)秒杀商品库存写入Redis的Hash数据结构
(8)基于Redis缓存实现的秒杀抢购细节
(9)秒杀过程中库存超卖类型问题的解决方案
方案一:使用分布式锁(悲观锁)
方案二:请求放入FIFO内存队列
方案三:使用乐观锁
方案四:封装Lua脚本
方案五:库存放入Redis队列
(10)秒杀成功后的异步下单与支付
(11)秒杀系统是否还需要处理限流、防刷和防重
(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)秒杀过程中库存超卖问题的解决方案
秒杀抢购过程中可能出现的库存超卖问题的解决方案如下:(其实这些方案也可以用来解决分布式环境下更新数据的顺序性问题)
方案一:使用分布式锁(悲观锁)
方案二:请求放入FIFO内存队列
方案三:使用乐观锁
方案四:封装Lua脚本
方案五:库存放入Redis队列
方案一:使用分布式锁(悲观锁)
比如首先通过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来进行秒杀下单削峰
(2)秒杀异步下单的流量控制处理
(3)下单成功后的订单支付逻辑与未支付逻辑
(4)MQ消息零丢失的处理
(5)MQ消息重复的处理
(6)MQ消息积压的处理
(7)秒杀系统的全链路高可用架构设计
(8)Redis集群崩溃时秒杀系统的自动恢复处理
(9)Redis主节点崩溃时没及时同步导致超卖问题
(10)MQ集群崩溃时写内存刷磁盘的处理
(11)订单系统异常时的MQ重试队列与死信队列
(12)秒杀抢购成功时的异构存储
(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双机器部署 + Keepalived
Nginx高可用:多机器冗余部署 + 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事务保证一致性。