作者:浩宇天尚
链接:https://www.jianshu.com/p/dad900926de6
秒杀系统-情报背景
相信大家都接触过新浪微博、淘宝、京东等等这些访问量较为巨大的平台以及网站,针对于“高流量”、“高并发”来讲,更是我们【技术开发者】都要面临的的一个很难的“包袱”难题。哎,看来如果要在这行混下去,即使你可能没有接触高并发场景,也要自己创造“高并发”进行迎难而上,因为只有这样子我们才可以更进一步啊!
秒杀系统-情报介绍
对于今天我们要介绍的内容就属于高并发的一个最极端的场景之一:“秒杀”,这个名词一般会在“大促”的时候出现,当然也会在某些平台活动上出现,那么肯定会有小伙伴会说,秒杀系统要注意哪些问题啊!为啥会比较难呢,难在哪里啊!
秒杀系统- 特点分析
瞬时剧增:在某一个时刻开始进入流量(很少会有热身以及缓慢增长机制),秒杀时大量用户会在同一时间,抢购同一商品,网站瞬时流量激增。
僧多粥少:商品的库存是有限的,秒杀请求下的订单数量会远远大于库存数量,只有少部分用户能够秒杀成功。
资源锁定:秒杀业务流程比较简单,一般就是下订单减库存。库存就是用户争夺的“资源”,实际被消费的“资源”不能超过计划要售出的“资源”,也就是不能被“超卖”。
秒杀系统- 难度分析
它的难度就在于要完成一个“60-100分”的秒杀系统,那么它必须要要至少兼顾以下这三个方面,才算合格,这三个“恶魔”分别叫“服务可用性”、“数据一致性”和“快速响应性”,有点“苛刻”!
在我们现在的场景下,很难再去考虑一个非分布式系统的架构了。(分布式架构)相信大家都知道CAP理论吧!没事不知道也没关系,可见内容:
CAP理论又称CAP定理,它说的是在一个分布式系统中,服务(数据)层面的一致性(Consistency)、服务自身的可用性(Availability)、网络不同节点分区容错性(Partition tolerance)。
A和C相信大家从字面上都可以理解了,这里要声明一下比较陌生的P:它代表如果要保证不同的节点即使在网络出现问题的时候仍能够访问到数据,那么最直接的办法就是冗余赋值节点,否则一切都是空谈,所以作为一个分布式系统而言,无法忽略P,我们可以理解它就是A和C的基础。
CAP体系总结
只保证AC就是一个单体应用,根本不是分布式。意义当然有,在分布式出现之前都是这么搭系统。倘若这个系统的节点之一挂了,不会发生脑裂而是整个系统直接宕掉。
进一步说如果网络中存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。
为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。
以上三者成为了“矛盾论”,而CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
回到我们的主体:秒杀三要素,它们三个可不完全等同于CAP三要素,甚至比它们的要求更高,甚至是基于前三者的一个更高层次的水平要求。
服务的可用性(Availability)
服务可用性,是在于高并发流量的冲击下,仍然可以保持服务的可用性并且还要保证一直可以输出对外界的服务能力,不会造成宕机以及资源损坏,即使在内存和网络甚至硬件资源有限的情况下,也不会被击垮“死亡”。
比如就像你养鱼,你玩命的给鱼放饲料,而超过了鱼能够承受的量,它受不了了活活被噎死或者撑死了,这鱼就像你的系统一样,一定要保证鱼的健康啊!
数据的一致性(Consistency)
都知道,我们开发的程序以及现在多数的服务器,比如数据库,他们在处理数据的时候,很有可能会存在多个线程同时在修改同一行数据或者同一块内存,在Java角度而言本身也会存在不一致的问题,而在程序和中间件的角度而言,也是一样,会出现同一时刻在数据修改顺序的乱序化,以及数据的紊乱,造成数据的重复操作,造成与我们预期的设想不同。
除非你可以实现串行化,一条一条处理,不让它们同一时刻就行修改或者操作数据,这个是最本质且最安全的办法,但是也是最影响性能的办法。(悲观锁、同步队列)。
此外还有一种办法就是,时时刻刻在原子层级,也就是最接近底层的计算机修改数据的时候,或者在所有节点之间建立一个应用层级的中间汇总干路点(redis或者database的主干点),上面加入写屏障和读屏障,在修改之前,在进行一次校验判断,如果数据与预期不同,就不进行修改。这就是著名的乐观锁!
服务快速响应性(Quick Response)
一般来讲这个属于用户体验,一个较为合格的秒杀系统,是不应该让用户漫长的等待而是最好尽可能快速反馈结果。
要做成快速响应,就不需要是异步返回,直接快速响应。
此外还需要尽快帮助用户计算数据,直接返回。
总结一下
(异步返回+同步处理)总结一下就是异步中套用者同步进行计算,既可以保证快速响应,又可以保证数据的一致性。
(异步返回+乐观锁处理)总结一下就是异步中套用者乐观锁进行计算,既可以保证快速响应,又可以保证数据的一致性。
情报分析结束后,我们要重头戏!进行技术分析了。
秒杀系统-架构设计
我们将秒杀架构进行一下划分,大体分为三个层级进行分析:由外到内进行分析,分别是:应用层、服务层、数据访问层。
![0da55042b5c66382b4690d2f8ffe6e45.png](https://i-blog.csdnimg.cn/blog_migrate/ab142e9ee9a7f31ba3def34281fd2782.png)
秒杀架构设计点
应用层架构设计
![311f5c3f64cda086847aee927c885493.png](https://i-blog.csdnimg.cn/blog_migrate/89f490ac8edd098b2eea18f0b5185ae9.png)
动静分离+CDN技术
# 动静分离分析
场景分析:在秒杀活动开启之前,用户一般都会尝试不断的刷新浏览器页面(俗称F5)以保证不会错过秒杀活动的商品。
按照常用的网站应用架构:
我们假设,如果这些无用的请求,频繁的冲击我们的后台服务器,比如说经过:Web服务器(LVS、Nginx等)->应用服务器(tomcat或者Jetty等)、连接数据库(MySQL),者无疑会对后端服务以及服务器造成非常大的压力。
解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,减少/隔绝无用的请求经过后端服务。
![613addcab94c19b0260e1d9dd1c5de51.png](https://i-blog.csdnimg.cn/blog_migrate/dccfd7720fae5240a206f7c64fe3594d.png)
# CDN技术分析
# 突然增加的网络及服务器带宽
网站的静态页面数据大小100K,那么需要的网络和服务器带宽是2G(100K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
即使将动态业务转换为静态化页面,但是秒杀活动会非常剧烈的增加的网络带宽的消耗,同时并不会减轻前端网站服务器的压力,所以如果可以的话,需要再进一步将秒杀商品页面缓存在CDN,而不在是单纯的我们的前端Nginx服务器层面,所以需要和CDN服务商临时租借新增的出口带宽。
![ec9baf27d42bfda7b3542907c1204f91.png](https://i-blog.csdnimg.cn/blog_migrate/4038edc896ebbc8f20e25d0ad79d1c4f.png)
# 防止缓存干扰页面刷新为秒杀页面
# 通过javascript文件进行传递随机号+状态位!
在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含秒杀开始标志为否;
![]()
当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。
这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存。
这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。
总结一下:前端秒杀页面使用专门的页面,这些页面包括静态的 HTML 和动态的 JS,他们都需要在 CDN 上缓存。
根据UID限制频率热度
为了控制公平性原则,由于黄牛或者一些黑客达人,会采用”高科技“,比如说,采用爬虫脚本操作,疯狂的去刷新页面。为了防止一些人的破坏以及公平分散,所以采用同一个标准去控制UID(用户ID)去访问频率信息,当超过每个人所需要达到的频率阈值,就要进行限制互动窗口内能够访问刷新的数据量!
例如:可以用Redis给每个用户做访问统计,根据用户的ID和商品的标识双方面进行对用户对某一个商品的访问频率控制,超过访问频率后,就会将他的请求暂时性熔断。
![]()
反向代理+负载均衡
秒杀系统必然是一个集群系统,在硬件不提升的情况下利用nginx做负载均衡也是不错的选择。
负载均衡(Load Balance)是集群技术(Cluster)的一种应用,可以将工作任务分摊到多个处理单元,从而提高并发处理能力,有利于提升中大型网站的性能。
需要使用服务集群和水平扩展,让“高峰”请求分流到不同的服务器进行处理。![]()
# http重定向协议实现负载均衡
根据用户的http请求的DNAT计算出一个真实的web服务器地址,并将该web服务器地址写入http重定向响应中返回给浏览器,由浏览器重新进行访问。该方式比较简单,但性能较差。
一般来讲经常用的SpringCloud-Gateway或者Neflix的Zuul等就属于该类型。
# 协议层:DNS域名解析负载均衡
DNS服务器上配置多个域名对应IP的记录。该方式直接将负载均衡的工作交给了DNS,为网站管理维护省掉了很多麻烦,访问速度快,有效改善性能。
一般来讲经常用的DNS服务器或者国内的DNS服务器等就属于该类型。
# 协议层:反向代理负载均衡
反向代理服务器在提供负载均衡功能的同时,管理着一组web服务器,根据负载均衡算法将请求的浏览器访问转发到不同的web服务器处理,处理结果经过反向服务器返回给浏览器。
该方式部署简单,web服务器地址不能直接暴露在外,不需要使用外部IP地址,而反向代理服务作为沟通桥梁就需要配置双网卡、外部内部两套IP地址。
一般来讲经常用的Nginx或者HaProxy等就属于该类型。
# 网络层IP负载均衡
网络层通过修改目标地址进行负载均衡,该方式在响应请求时速度较反向服务器负载均衡要快,但是,当请求数据较大(大型视频或文件)时,速度反应就会变慢。
一般来讲经常用的Nginx或者HaProxy等就属于该类型。
# 数据链路层负载均衡
数据链路层修改Mac地址进行负载均衡,负载均衡服务器的IP和它所管理的web 服务群的虚拟IP一致。它不需要负载均衡服务器进行地址的转换,但是对负载均衡服务器的网卡带宽要求较高。
一般来讲经常用的LVS等就属于该类型。
# F5 和 A10 负载均衡器
F5的全称是F5-BIG-IP-GTM,硬件负载均衡设备,其并发能力达到。该方式能够实现多链路的负载均衡和冗余,可以接入多条ISP链路,在链路之间实现负载均衡和高可用。
服务层架构设计
![aee0ee18cb037fa03d3f03a0249b064f.png](https://i-blog.csdnimg.cn/blog_migrate/a7dd27070237f567a4bee8475a316356.png)
缓存技术分析
硬盘持久化的IO操作将耗费大量资源。所以决定采用基于内存操作的redis,redis的密集型io。
分批放行+排队处理
即使我们扩展再多的应用,使用再多的应用服务器,部署再多的负载均衡器,都会遇到支撑不住海量请求的时候。
所以,在这一层我们要考虑的是如何做好限流,当超过系统承受范围的时候,需要果断阻止请求的涌入。
# 排队处理
排队处理机制,正如,我们日常买东西排队一样的道理,这样子就不会处理不过来,并且也可以保证数据执行的正确性!
它直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,有一些将多线程处理方式变成单线程处理机制,会大大影响数据的效率和性能!
![]()
# Java的三个常用的并发队列
ArrayBlockingQueue是初始容量固定的阻塞队列,可以用来作为数据库成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。
ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。
LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。
在请求预处理阶段,由于系统入队需求要远大于出队需求,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现,甚至可以采用Disruptor异步处理框架机制。
# 分批放行
在同步排队的基础上,我们可以在加入一个分批放行执行处理机制。
顾名思义的就是,为了提高性能,我们可以考虑达到预定阈值以后,在进行相关的执行后端服务,这样子可以提高一定的性能以及减少后端请求的次数和压力,如下图所示:
![]()
还会利用缓存和队列技术减轻应用处理的压力,通过异步请求的方式做到最终一致性。
限流
漏桶算法
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
![]()
设定漏桶流出速度及漏桶的总容量,在请求到达时判断当前漏桶容量是否已满,不满则可将请求存入桶中,否则抛弃请求。
采用一个线程以设定的速率取出请求进行处理。
# 算法弊端
由于其只能以特定速率处理请求,则如何确定该速率就是核心问题,如果速率设置太小则会浪费性能资源,设置太大则会造成资源不足。
速度执行敏感度不高!无论输入速率如何波动,均不会体现在服务端,即使资源有空余,对于突发请求也无法及时处理,故对有突发请求处理需求时,不宜选择该方法。
![]()
令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
# 实现原理
设定令牌桶中添加令牌的速率,并且设置桶中最大可存储的令牌,当请求到达时,向桶中请求令牌(根据应用需求,可能为1个或多个),若令牌数量满足要求,则删除对应数量的令牌并通过当前请求,若桶中令牌数不足则触发限流规则。
![]()
为解决固定窗口计数带来的周期切换处流量突发问题,可以使用滑动窗口计数。滑动窗口计算本质上也是固定窗口计数,区别在于将计数周期进行细化。
滑动窗口
滑动窗口计数法与固定窗口计数法相比较,除了计数周期T及周期内最大访问(调用)数N两个参数,增加一个参数M,用于设置周期T内的滑动窗口数。
![]()
数据访问层
由于要承受高并发,mysql在高并发情况下的性能下降尤其严重。
![]()
数据更新点(库存扣除)
悲观锁更新数据
可以从“悲观锁”的方向
![]()
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。
虽然它解决了线程安全的问题,但是“高并发”场景下,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。
同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。
行锁、页锁、表锁、同步锁、分布式锁、分布式队列、意向所等。
乐观锁更新数据
讨论一下“乐观锁”的思路了。
![336eea622711cad230c26797833154d3.png](https://i-blog.csdnimg.cn/blog_migrate/42539d58702922c2e3165bb47779edd2.png)
乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新/通过状态化进行更新操作机制。
实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。
这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是在冲突较小的时候,这是一个比较好的解决方案。
缓存乐观锁、数据库乐观锁。(判断更新行数是否>0),CAS机制
姊妹篇【「绝密档案」“爆料”完整秒杀架构的设计到技术关键点的“八卦资料”】
再此会进行扩展技术介绍,以下内容:
热点分离
热点识别技术
热点隔离技术
热点优化技术
事务完整性
接口幂等性
分布式事务系统
事务处理去重法
事务处理幂等性
数据高可用
读写分离
分库分表
数据库集群
优化数据库
DB层面的单行记录做并发排队机制
服务降级分析
降低冲击法(延时处理)
延时队列机制(单点法)
延时队列机制(分布式)