Hello,大家好,我是 猿 java。a。
说起秒杀,大家肯定不陌生,阿里的的双11,京东的618,12306 抢火车票,抖音的直播带货等等,“秒杀”场景在互联网的世界似乎处处可见。今天我们就来聊聊如何设计一个秒杀系统。
1. 什么是秒杀
所谓秒杀,就是在同一个时刻有大量的客户端请求争抢同一个商品并完成交易的过程,瞬时会产生大量的并发读和并发写。
秒杀系统本质上就是一个满足高并发、高性能和高可用的分布式系统。下面给出一张下单交互概要图:
2. 秒杀系统的特点
高性能
秒杀涉及大量的并发读和并发写,因此秒系统必须能支持高并发访问,而且 RT(响应时间)需要在一定的范围内,通常是小于200ms
一致性
秒杀系统中通常会使用缓存,如何保证缓存和数据库中库存数据的一致性,保证商品库存的准确性
高可用
秒杀系统会在瞬间收到大量的读写操作,如何保证服务能稳定的运行,设计系统时是否考虑到系统容灾问题,保证服务的高可用
可扩展性
当服务达到瓶颈时,如何能实现服务器快速扩容
3. 如何设计秒杀系统
秒杀系统,是一个要求上下游服务必须紧密配合的工程,任何一个环节掉链子可能就会导致秒杀失败,从技术角度上,整个秒杀流程包含前端和后端 2 个核心部分,因此我们就从这 2个核心部分来讲解秒杀系统是如何设计的。
3.1 前端秒杀设计
前端是秒杀的入口,用户首先就是在前端界面进行商品浏览,然后加购想要的商品进行下单付款操作。因此,对于秒杀系统的设计,前端主要需要考虑以下几点:
服务高可用
前端是直接面向用户,所以,前端服务一定要保证高可用,要不然前端页面崩了,秒杀的入口都没有了,谈何秒杀。
页面静态化
前端数据源动静分离,静态的数据可以放到 CDN,前端从 CDN 获取,动态的数据放到服务器。静态数据,比如商品的详情信息,图片等;动态数据,商品的数量,价格等。比如:可以通过 Url 地址作为 key 来存储静态数据
控制对服务器请求的频率
控制对服务器请求频率能在一定程度上缓解服务器的压力,限频的方式有很多, 比如 秒杀按钮点击后置灰一定的时长后才能再次点击,前端 答题正确后才向服务器发起请求,前端将请求加入队列进行排队,当有多个秒杀活动时,可以分时段进行,这些方式都是无损的。
控制对服务器请求参数的大小
因为秒杀期间,瞬时会有大量的请求涌向服务器,所以前端和服务器的数据交互要尽量的少,减少网络传输以及编解码的开销
限流,降级
当下游服务器达到瓶颈时,可以采用前端限流方式,降低对服务器的 TPS 和 QPS。但是当客户端比较分散时,限流阈值的设置是一个比较大的挑战:阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;设的太大,则起不到限制的作用。
3.2 后端秒杀设计
后端主要是接收和响应前端的请求,几乎所有的业务逻辑都是在后端来实现,因此后端是整个秒杀环节最具有挑战性,主要需要考虑以下几点:
服务高可用
后端是处理请求的核心服务,所以必须做好高可用部署,容灾设计(异地多活)
降级,限流,拒绝服务
降级,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。所以降级一般需要前后端配合执行,可以通过开关系统来实现。比如:当 QPS 达到一个阈值时,可设置开关,将原来分页查 50 条数据,变成查 10 条,减少一次交互的数据量。
限流,就是当系统容量达到瓶颈时,通过限制一部分流量来保护系统,限流可以是接口级别,服务器级别,iP 级别等等,此处的限流是有损操作,限流的阈值一般可以根据压测结果来设置
直接拒绝服务,如果限流还不能解决问题,那就直接拒绝服务以求自保,这也是最差的一种兜底情况。
独立部署秒杀服务
秒杀系统和普通的售卖有一定的差异点,秒杀一般是持续时间短,并发量高,所以为了不影响正常的售卖,可以单独部署一套秒杀服务,在物理级别进行隔离,也适合服务端灵活伸缩容以及做一些特殊的个性化处理。有条件的团队可以实施。
流量削峰
当服务流量过大时,可以将请求存入 MQ 消息中间件进行削峰处理,客户端可以采用轮询的方式向服务器获取结果(服务器会收到很多结果查询的请求),或者服务主动 push 结果给客户端(服务需要保留很多和客户端的长链接),2 种方式各有优劣,一般生产上轮询查询结果用的比较多。
热点数据探测
很多时候,一个商品不属于秒杀,但是很多用户购买,可能会成为热点数据,请求量不亚于秒杀,所以网关需要有热点数据探测的功能,实现的方式有很多,比如:统计客户端的请求数
增加缓存
秒杀一般遵从读多写少的 28 法则,所以可以在服务端增加缓存应对高并发读。缓存可以设置 2 层,第一层是本地缓存,可以使用 Google guava 的缓存框架,失效时间一般可以秒级别,本地缓存是属于 jvm 级别的,每次失效后可以从 redis 缓存中加载,redis 缓存要特别注意缓存失效,缓存击穿,缓存雪崩的问题。
缓存击穿:缓存中不存在,数据库存在,这样就会导致请求直接到达数据库,当请求量比较大时,可能直接把数据库打垮。解决方法:
可以考虑缓存永远不过期
同步返回 null,异步加锁查询数据库,更新缓存
缓存穿透:请求的数据在缓存和数据库中都不存在,解决办法:
业务层进行合法校验,拦截大部分不合法的请求
使用布隆过滤器,针对一个或者多个维度,把可能存在的数据值 hash 到 bitmap 中,bitmap 中不存在则该数据一定不存在,bitmap 中存该数据可能存在
对空的结果进行缓存,设置得较短过期时间,当有数据库变更时,必须同时刷新缓存,否则会导致不一致的问题存在
缓存雪崩:指缓存在同一时刻失效,请求都到数据库上,解决的办法:
可以考虑缓存永远不过期
失效时间尽量随机,避免同时过期
多级缓存,数据缓存到 A 和 B,A 设置过期时间,B 不设置过期时间,如果 A 为空的时候去读 B,同时异步去更新缓存,需要同时更新两个缓存
4. 如何保证不超卖
秒杀一般都是优惠售卖,所以库存不超卖是前提,一般来说,防止超卖需要前后端配合。
4.1 库存扣减方式
下单减库存
买家下单后,扣减商品总库存。下单减库存是最简单也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,一定不会出现超卖的情况。出现的问题:恶意刷单,某些人下单后占用库存不付款。
付款减库存
买家付款之后,扣减商品总库存。这种方式产生的问题是,库存超卖。
预扣库存
买家下单后,预扣库存,在一定的时间内未付款,库存将会自动释放。在买家付款前,需再次校验库存是否保留,如果没有保留,则再次尝试预扣;如果库存不足则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。这种方式在生产上用的比较多。
4.2 服务端库存处理
将库存操作的逻辑放到 lua 脚本中,通过 redis 的单线程特性,保证 Lua 脚本执行不会被打断,从而保证库存操作的原子性
5. 总结
百种业务百种架构,不同的领域,不同的场景,不同的流量,可能系统会有差异,但是万变不离其宗,秒杀系统看似简单,其实包含了很多架构的思想,从前端到后端到运维,怎么全局把控,对于各个服务怎么去做高可用,高性能,可扩展保证。如何设计缓存,如何保证缓存和数据库的数据一致性,当服务达到瓶颈时,如何做服务降级,限流。从运维角度,如何智能监控服务器的各项指标,当服务器出现异常时如何智能报警,智能干预或人工干预。下面给出一张秒杀系统核心流程图:
秒杀系统设计时,一般我们遵从几个原则:
前后端交互的数据尽量少
前端尽量控制对后端的无效请求
服务之间的依赖尽量少
请求路径尽量短
服务或者中间件不要有单点,要有容灾
6. 鸣谢
如果你觉得本文章对你有帮助,感谢点赞,在读,或者转发给更多的好友,我们将为你呈现更多的干货, 欢迎关注公众号:猿 java
猿java精彩文章推荐: