秒杀系统的系统架构
本节分多个维度介绍crazy-springcloud开发脚手架的架构,包括分层架构、限流架构、分布式锁架构、削峰的架构。
秒杀的分层架构
从分层的角度来说,秒杀系统架构可以分成3层,大致如下:
(1)客户端:负责内容提速和交互控制。
(2)接入层:负责认证、负载均衡、限流。
(3)业务层:负责保障秒杀数据的一致性。
1.客户端负责内容提速和交互控制
客户端需要完成秒杀商品的静态化展示。无论是在桌面浏览器还是在移动端App展示秒杀商品,秒杀商品的图片和文字元素都需要尽可能静态化,尽量减少动态元素。这样就可以通过CDN来提速和抗峰值。
另外,在客户端这一层的用户交互上需要具备一定的控制用户行为和禁止重复秒杀的能力。比如,当用户提交秒杀请求之后,可以将秒杀按钮置灰,禁止重复提交。
2.接入层负责认证、负载均衡、限流
秒杀系统的特点是并发量极大,但实际的优惠商品有限,秒杀成功的请求数量很少,如果不在接入层进行拦截,大量请求就会造成数据库连接耗尽、服务端线程耗尽,导致整体雪崩。因此,必须在接入层进行用户认证、负载均衡、接口限流。
对于总流量较小的系统,可以在内部网关(如Zuul)完成用户认证、负载均衡、接口限流的功能,具体的分层架构如图10-2所示。
图10-2 在内部网关(如Zuul)完成认证、负载均衡、接口限流
对于总流量较大的系统会有一层甚至多层外部网关,因此限流的职责会从内部网关剥离到外部网关,内部网关(如Zuul)仍然具备权限认证、负载均衡的能力,具体的分层架构如图10-3所示。
图10-3 外部网关与内部网关相结合完成权限认证、负载均衡、接口限 流
3.业务层负责保障数据一致性
秒杀的业务逻辑主要是下订单和减库存,都是数据库操作。大家都知道,数据库层只能承担“能力范围内”的访问请求,既是非常脆弱的一层,又是需要进行事务保护的一层。在业务层还需要防止超出库存的秒杀(超卖和少卖),为了安全起见,可以使用分布式锁对秒杀的数据库操作进行保护。
秒杀的限流架构
前面提到,秒杀系统中的秒杀商品总是有限的。除此之外,服务节点的处理能力、数据库的处理能力也是有限的,因此需要根据系统的负载能力进行秒杀限流。
总体来说,在接入层可以进行两个级别的限流策略:应用级别的限流和接口级别的限流。
什么是应用级别的限流策略呢?对于整个应用系统来说,一定会有一个QPS的极限值,如果超了极限值,整个应用就会不响应或响应得非常慢。因此,需要在整个应用的维度做好应用级别的限流配置。应用级别的限流应该配置在最顶层的反向代理,具体如图10-4所示。
图10-4 应用级别的限流
应用级别的流量限制可以通过Nginx的limit_req_zone和limit_req两个指令完成。假定要配置Nginx虚拟主机的限流规则为单IP限制为每秒1次请求,整个应用限制为每秒10次请求,那么具体的配置如下:
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s; limit_req_zone $server_name zone=perserver:1m rate=10r/s; server { ... limit_req zone=perip burst=5; limit_req zone=perserver burst=10; }
什么是接口级别的限流策略呢?单个接口可能会有突发访问情况,可能会由于突发访问量太大造成系统崩溃,典型的就是本章所介绍的秒杀类接口。接口级别的限流就是配置单个接口的请求速率,是细粒度的限流。
接口级别的限流也可以通过Nginx的limit_req_zone和limit_req两个指令配合完成,对获取秒杀令牌的接口,同时进行用户Id和商品Id限流的配置如下:
limit_req_zone $arg_goodId zone=pergood:10m rate=100r/s; limit_req_zone $arg_userId zone=peruser:1m rate=1r/s; server { #lua:获取秒杀token令牌 location = /seckill-provider/api/seckill/redis/token/v2 { limit_req zone=peruser burst=5; limit_req zone=pergood burst=10; #获取秒杀token lua脚本 content_by_lua_file luaScript/module/seckill/getToken.lua; } }
以上定义了两个限流规则:pergood和peruser:pergood规则根据请求参数的goodId值进行限流,同一个goodId值的限速为每秒100次请求;peruser规则根据请求参数的userId值进行限流,同一个userId值的限速为每秒1次请求。
但是,Nginx的限流指令只能在同一块内存区域有效,而在生产场景中秒杀的外部网关往往是采用多节点部署的,所以这就需要用到分布式限流组件。高性能的分布式限流组件可以使用Redis+Lua来开发,京东的抢购就是使用Redis+Lua完成限流的,并且无论是Nginx外部网关还是Zuul内部网关,都可以使用Redis+Lua限流组件。
理论上,接入层的限流有多个维度:
(1)用户维度的限流:在某一时间段内只允许用户提交一次请求,比如可以采取客户端IP或者用户ID作为限流的key。
(2)商品维度的限流:对于同一个抢购商品,在某个时间段内只允许一定数量的请求进入,可以采取秒杀商品ID作为限流的key。
无论是哪个维度的限流,只要掌握其中的一个,其他维度的限流在技术实现上都是差不多的。本书的秒杀练习使用的是接口级别的限流策略,在获取秒杀令牌的REST接口时,针对每个秒杀商品的ID配置限流策略,限制每个商品ID每秒内允许通过的请求次数。
如果大家对进行用户维度的限流感兴趣,可以自行修改配置进行尝试。
秒杀的分布式锁架构
前面提到了超卖或少卖问题:比如10万次请求同时发起秒杀请求,正常需要进行10万次库存扣减,但是由于某种原因,往往会造成多减库存或者少减库存,这就会出现超卖或少卖问题。
解决超卖或者少卖问题有效的办法之一就是利用分布式锁将对同一个商品的并行数据库操作予以串行化。秒杀场景的分布式锁应该具备如下条件:
(1)一个方法在同一时间只能被一个机器的一个线程执行。
(2)高可用地获取锁与释放锁。
(3)高性能地获取锁与释放锁。
(4)具备可重入特性。
(5)具备锁失效机制,防止死锁。
(6)具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
常用的分布式锁有两种:ZooKeeper分布式锁和Redis分布式锁。如果使用ZooKeeper分布式锁来保护秒杀的数据库操作,那么它的架构图大致如图10-5所示。
图10-5 使用ZooKeeper分布式锁来保护秒杀的数据库操作
实际上,除了提供分布式锁外,ZooKeeper还能提供高可靠的分布式计数器、高可靠的分布式ID生成器的基础能力。ZooKeeper分布式计数器、分布式锁、分布式ID生成器等基础知识也是大家必须系统地学习和掌握的知识,但是不属于在本书介绍的内容,如果对这一块不了解,可翻阅本书姊妹篇《Netty、Redis、ZooKeeper高并发实战》。
ZooKeeper分布式锁虽然高可靠,但是性能不高,不能满足秒杀场景分布式锁的第3个条件(高性能地获取锁与释放锁),所以在秒杀的场景建议使用Redis分布式锁来保护秒杀的数据库操作。
秒杀的削峰架构
通过接入网关的限流能够拦截无效的刷单请求和超出预期的那部分请求,但是,当秒杀的订单量很大时,比如有100万商品需要参与秒杀,这时后端服务层和数据库的并发请求压力至少为100万。这种请求下,需要使用消息队列进行削峰。
削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。通过消息队列可以大大地缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在入口承接瞬时的流量洪峰,在出口平滑地将消息推送出去。消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。使用消息队列对秒杀进行削峰的架构如图10-6所示。
图10-6 使用消息队列对秒杀进行削峰
对于秒杀消息的入队可以直接在内部网关完成。内部网关在完成用户的权限验证、秒杀令牌的有效性验证之后,将秒杀消息发往消息队列即可。秒杀服务通过消息队列的订阅完成秒杀消息的消费。常用消息队列系统:Kafka、RocketMQ、ActiveMQ、RabbitMQ、ZeroMQ、MetaMQ等。本书的内容主要聚焦在Spring Cloud和Nginx,对消息队列这里不做过多的介绍,使用消息队列进行削峰的秒杀实现版本可参见后续的疯狂创客圈社群博客。