赞
踩
秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统需要进行哪些关注。博文主要介绍有关于秒杀系统的设计和底层实现原理。
通过对需求分析,我们提炼出秒杀活动的三大子流程,即:
有了这个整体把握,我们再针对每一个子流程,分析该场景下需要编排哪些产品功能。
创建秒杀活动:做的事情很简单,需要配置好活动场次,每个场次又需配置参与活动的秒杀商品;这个功能点,主要是面向运营,为了方便运营完成活动配置编辑,附带着还需提供活动查询能力:如查看活动列表、活动详情、发布/禁用活动等mis接口。
查看秒杀活动:主要功能为支持秒杀会场的活动列表、秒杀商品列表、秒杀商品详情等活动页面;这些都是直接面向C端用户的系列读接口,承载流量会很高。
查看秒杀活动:这一块核心述求是要能正确高效完成库存扣减,严格保证不能出现超卖!从功能点上来看,C端买家下单实现秒杀商品库存扣减,如果买家在规定时间未完成支付或拍下后取消订单,需及时释放用户下单锁定的库存,也就是要回库存。
E-R关系图,用于指导如何建立领域模型。从E-R图上我们能看出,几个比较重要的领域模型:如活动、活动商品,系统真正编码落地的时候,紧紧围绕这些领域模型去建模,做到代码和领域模型的表达一致的。
概要设计的目的是为了明确产品功能和系统边界,通过领域驱动的界限上下文图,能清晰地看出完成当前需求需要参与协作的团队,以及团队与团队之间任务划分边界。活动上下文是我们关注的重点,同时也应该看到,我们需要商品团队、交易团队的协作。
通过概要设计划清了系统边界,每部分每个团队应该做什么就容易确定了,Api定义呼之欲出。
- {
- "activityName": "双十一秒杀第一场",
- "startTime": 1540174800000, // 活动开始时间
- "endTime": 1541988000000, // 活动结束时间
- "itemLine": [
- {
- "itemId": 123, // 活动商品id
- "itemType": 7, // 活动商品类型
- "itemTitle": "这是商品标题",
- "subTitle": "这是商品副标题",
- "itemImage": "这是图片链接",
- "salePrice": 66800, // 商品原价(单位:分)
- "activityPrice": 100, // 活动价
- "quota": 3, // 单个用户商品抢购件数限制
- "stock": 100 // 商品活动库存
- }
- ],
- "activityRuleConfigs": [ // 活动规则列表
- {
- "configKey": "city", // 城市规则:在规则列表的城市可看到活动
- "configValue": "17,5,10,2,3,4,11"
- }
- ]
- }
- {
- "traceId": "2910c88d0d4f45d5fe299f0c5829d72c",
- "code": "SERVICE_RUN_SUCCESS",
- "msg": "服务运行成功",
- "status": 10000,
- "success": true
- }
- {
- "traceId": "2910c88a0d4f45d5be290f0c5829d72c",
- "success": true,
- "status": 10000,
- "msg": "OK",
- "code": "SUCCESS",
- "data": [
- {
- "activityId": 1,
- "activityName": "双十一秒杀第一场",
- "startTime": 1541901600000,
- "endTime": 1741951999000,
- "enabled" true
- }
- ]
- }
- {
- "traceId": "889924ef8e6241a7a766107f38c5e0c0",
- "success": true,
- "status": 10000,
- "msg": "OK",
- "code": "SUCCESS",
- "data": {
- "activityId": 1,
- "activityName": "双十一秒杀第一场",
- "startTime": 1541901600000,
- "endTime": 1741951999000,
- "enabled" true
- "items": [
- {
- "itemId": 53725,
- "itemType": 1,
- "itemTitle": "x商品",
- "subTitle": "x商品副标题",
- "itemImage": "http://img.xxxx.com/static/do1_QtSq1m2xM7VL6zEI4sUH",
- "itemPrice": 19800,
- "activityPrice": 4800,
- "quota": 3,
- "stock": 50,
- "sold": 0
- },
- {
- "itemId": 53724,
- "itemType": 1,
- "itemTitle": "y商品",
- "subTitle": "y商品副标题",
- "itemImage": "http://img.xxxx.com/static/MrcNjUeeoOG24zZH7nR.png",
- "itemPrice": 42800,
- "activityPrice": 17000,
- "quota": 3,
- "stock": 50,
- "sold": 0
- }
- ]
- }
- }
- {
- "traceId": "a77edf653da644959d331b7b55607958",
- "success": true,
- "status": 10000,
- "msg": "OK",
- "code": "SUCCESS",
- "data": {
- "itemId": 53724,
- "itemType": 1,
- "itemTitle": "x商品",
- "subTitle": "商品副标题",
- "itemImage": "http://img.xxxx.com/static/do1_QtSq1m2xM7VL6zEI4sUH",
- "itemPrice": 42800,
- "activityPrice": 17000,
- "quota": 3,
- "stock": 50,
- "sold": 0,
- "activity": {
- "activityId": 1,
- "activityName": "双十一秒杀第一场",
- "startTime": 1541901600000,
- "endTime": 1741951999000,
- "enabled" true
- }
- }
- }
- {
- "activityId": 1,
- "buyerId": "buyer_001",
- "itemId": 53724,
- "orderId": "20191111123456789",
- "orderTime": 1541901700000,
- "quantity": 1
- }
- {
- "traceId": "f689852f113e413d9940ce24020e7083",
- "success": true,
- "status": 10000,
- "msg": "OK",
- "code": "SUCCESS",
- "data": true
- }
- {
- "activityId": 1,
- "orderId": "20191111123456789"
- }
- {
- "traceId": "5342243fd424468ab9ad13d03ffcdc62",
- "success": true,
- "status": 10000,
- "msg": "OK",
- "code": "SUCCESS"
- }
秒杀特点:短时间内,大量用户涌入,集中读和写有限的库存。
解决方案:层层拦截,将请求尽量拦截在系统上游,避免将锁冲落到数据库上。
产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求; JS层面,限制用户在x秒之内只能提交一次请求,比如微信摇一摇抢红包。 基本可以拦截80%的请求。
怎么防止程序员写for循环调用,有去重依据么? IP? cookie-id? …想复杂了,这类业务都需要登录,用uid即可。在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但最简单,比如guava本地缓存)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。 对于5s内的无效请求,统一返回错误提示或错误页面。
这个方式拦住了写for循环发HTTP请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不住了。
方案一:写请求放到队列中,每次只透有限的写请求到数据层,如果成功了再放下一批,直到库存不够,队列里的写请求全部返回“已售完”。
方案二:或采用漏斗机制,只放一倍的流量进来,多余的返回“已售完”,把写压力转换成读压力。 读请求,用cache,redis单机可以抗10W QPS,用异步线程定时更新缓存里的库存值。
还有提示“模糊化”,比如火车余票查询,票剩了58张,还是26张,你真的关注么,其实我们只关心有票和无票。
浏览器拦截了80%,站点层拦截了99.9%并做了页面缓存,服务层又做了写请求队列与数据缓存,每次透到数据库层的请求都是可控的。 db基本就没什么压力了,通过自身锁机制来控制,避免出现超卖。
对于一个日常平稳的业务系统,如果直接开通秒杀功能的话,往往会出现很多问题
用户 | 问题分类 | 业务出现的问题 | 设计要求 |
用户 | 体验较差 | 秒杀开始,系统瞬间承受平时数十倍甚至上百倍的流量,直接宕掉 | 高性能 |
用户下单后却付不了款,显示商品已经被其他人买走了 | 一致性 | ||
商家 | 商品超卖 | 100 件商品,却出现 200 人下单成功,成功下单买到商品的人数远远超过活动商品数量的上限 | 一致性 |
资金受损 | 竞争对手通过恶意下单的方式将活动商品全部下单,导致库存清零,商家无法正常售卖 | 高可用 | |
秒杀器猖獗,黄牛通过秒杀器扫货,商家无法达到营销目的 | 高可用 | ||
平台 | 风险不可控 | 系统的其它与秒杀活动不相关的模块变得异常缓慢,业务影响面扩散 | 高可用 |
拖垮网站 | 在线人数创新高,核心链路涉及的上下游服务从前到后都在告警 | 高性能 | |
库存只有一份,所有请求集中读写同一个数据,DB 出现单点 | 高性能 |
秒杀本质是要求一个瞬时高发下的承压系统,这也是其区别于其他业务的核心场景。对日常系统秒杀产生的问题逐一进行拆解分类,秒杀对应到架构设计,其实就是高可用、一致性和高性能的要求。关于秒杀系统的设计思考
大家可能会注意到,秒杀过程中你是不需要刷新整个页面的,只有时间在不停跳动。这是因为一般都会对大流量的秒杀系统做系统的静态化改造,即数据意义上的动静分离。动静分离三步走:
6.1.1.1 数据拆分
动静分离的首要目的是将动态页面改造成适合缓存的静态页面。因此第一步就是分离出动态数据,主要从以下 2 个方面进行:
这里你可以打开电商平台的一个秒杀页面,看看这个页面里都有哪些动静数据。
6.1.1.2 静态缓存
分离出动静态数据之后,第二步就是将静态数据进行合理的缓存,由此衍生出两个问题:
1、怎么缓存;
静态化改造的一个特点是直接缓存整个 HTTP 连接而不是仅仅缓存静态数据,如此一来,Web 代理服务器根据请求 URL,可以直接取出对应的响应体然后直接返回,响应过程无需重组 HTTP 协议,也无需解析 HTTP 请求头。而作为缓存键,URL唯一化是必不可少的,只是对于商品系统,URL 天然是可以基于商品 ID 来进行唯一标识的,比如淘宝的 https://item.taobao.com/item...
2、哪里缓存。
静态数据缓存到哪里呢?可以有三种方式:1、浏览器;2、CDN ;3、服务端。
浏览器当然是第一选择,但用户的浏览器是不可控的,主要体现在如果用户不主动刷新,系统很难主动地把消息推送给用户(注意,当讨论静态数据时,潜台词是 “相对不变”,言外之意是 “可能会变”),如此可能会导致用户端在很长一段时间内看到的信息都是错误的。对于秒杀系统,保证缓存可以在秒级时间内失效是不可或缺的。
服务端主要进行动态逻辑计算及加载,本身并不擅长处理大量连接,每个连接消耗内存较多,同时 Servlet 容器解析 HTTP 较慢,容易侵占逻辑计算资源;另外,静态数据下沉至此也会拉长请求路径。
因此通常将静态数据缓存在 CDN,其本身更擅长处理大并发的静态文件请求,既可以做到主动失效,又离用户尽可能近,同时规避 Java 语言层面的弱点。需要注意的是,上 CDN 有以下几个问题需要解决:
因此,将数据放到全国所有的 CDN 节点是不太现实的,失效问题、命中率问题都会面临比较大的挑战。更为可行的做法是选择若干 CDN 节点进行静态化改造,节点的选取通常需要满足以下几个条件:
基于以上因素,选择 CDN 的二级缓存比较合适,因为二级缓存数量偏少,容量也更大,访问量相对集中,这样就可以较好解决缓存的失效问题以及命中率问题,是当前比较理想的一种 CDN 化方案。部署方式如下图所示:
6.1.1.3 数据整合
分离出动静态数据之后,前端如何组织数据页就是一个新的问题,主要在于动态数据的加载处理,通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
6.1.1.4 动静分离策略
动静分离对于性能的提升,抽象起来只有两点,一是数据要尽量少,以便减少没必要的请求,二是路径要尽量短,以便提高单次请求的效率。具体方法其实就是基于这个大方向进行的。
热点分为热点操作和热点数据,以下分开进行讨论。零点刷新、零点下单、零点添加购物车等都属于热点操作。热点操作是用户的行为,不好改变,但可以做一些限制保护,比如用户频繁刷新页面时进行提示阻断。热点数据的处理三步走,一是热点识别,二是热点隔离,三是热点优化。
6.1.2.1 热点识别
热点数据分为静态热点和动态热点,具体如下:
因此秒杀系统需要实现热点数据的动态发现能力,一个常见的实现思路是:
需要注意的是:
6.1.2.2 热点隔离
热点数据识别出来之后,第一原则就是将热点数据隔离出来,不要让 1% 影响到另外的 99%,可以基于以下几个层次实现热点隔离:
当然,实现隔离还有很多种办法。比如,可以按照用户来区分,为不同的用户分配不同的 Cookie,入口层路由到不同的服务接口中;再比如,域名保持一致,但后端调用不同的服务接口;又或者在数据层给数据打标进行区分等等,这些措施的目的都是把已经识别的热点请求和普通请求区分开来。
6.1.2.3 热点优化
热点数据隔离之后,也就方便对这 1% 的请求做针对性的优化,方式无外乎两种:
6.1.2.4 热点优化总结
数据的热点优化与动静分离是不一样的,热点优化是基于二八原则对数据进行了纵向拆分,以便进行针对性地处理。热点识别和隔离不仅对“秒杀”这个场景有意义,对其他的高性能分布式系统也非常有参考价值。
对于一个软件系统,提高性能可以有很多种手段,如提升硬件水平、调优JVM 性能,这里主要关注代码层面的性能优化——
性能优化需要一个基准值,所以系统还需要做好应用基线,比如性能基线(何时性能突然下降)、成本基线(去年大促用了多少机器)、链路基线(核心流程发生了哪些变化),通过基线持续关注系统性能,促使系统在代码层面持续提升编码质量、业务层面及时下掉不合理调用、架构层面不断优化改进。
秒杀系统中,库存是个关键数据,卖不出去是个问题,超卖更是个问题。秒杀场景下的一致性问题,主要就是库存扣减的准确性问题。
电商场景下的购买过程一般分为两步:下单和付款。“提交订单”即为下单,“支付订单”即为付款。基于此设定,减库存一般有以下几个方式:
6.2.2.1 下单减库存
优势:用户体验最好。下单减库存是最简单的减库存方式,也是控制最精确的一种。下单时可以直接通过数据库事务机制控制商品库存,所以一定不会出现已下单却付不了款的情况。
劣势:可能卖不出去。正常情况下,买家下单后付款概率很高,所以不会有太大问题。但有一种场景例外,就是当卖家参加某个促销活动时,竞争对手通过恶意下单的方式将该商品全部下单,导致库存清零,那么这就不能正常售卖了——要知道,恶意下单的人是不会真正付款的,这正是 “下单减库存” 的不足之处。
6.2.2.2 付款减库存
优势:一定实际售卖。“下单减库存” 可能导致恶意下单,从而影响卖家的商品销售, “付款减库存” 由于需要付出真金白银,可以有效避免。
劣势:用户体验较差。用户下单后,不一定会实际付款,假设有 100 件商品,就可能出现 200 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在大促的热门商品上。如此一来就会导致很多买家下单成功后却付不了款,购物体验自然是比较差的。
6.2.2.3 预扣库存
优势:缓解了以上两种方式的问题。预扣库存实际就是“下单减库存”和 “付款减库存”两种方式的结合,将两次操作进行了前后关联,下单时预扣库存,付款时释放库存。
劣势:并没有彻底解决以上问题。比如针对恶意下单的场景,虽然可以把有效付款时间设置为 10 分钟,但恶意买家完全可以在 10 分钟之后再次下单。
业界最为常见的是预扣库存。无论是外卖点餐还是电商购物,下单后一般都有个 “有效付款时间”,超过该时间订单自动释放,这就是典型的预扣库存方案。但如上所述,预扣库存还需要解决恶意下单的问题,保证商品卖的出去;另一方面,如何避免超卖,也是一个痛点。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
业务手段保证商品卖的出去,技术手段保证商品不会超卖,库存问题从来就不是简单的技术难题,解决问题的视角是多种多样的。
库存是个关键数据,更是个热点数据。对系统来说,热点的实际影响就是 “高读” 和 “高写”,也是秒杀场景下最为核心的一个技术难题。
6.2.4.1 高并发读
秒杀场景解决高并发读问题,关键词是“分层校验”。即在读链路时,只进行不影响性能的检查操作,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等,而不做一致性校验等容易引发瓶颈的检查操作;直到写链路时,才对库存做一致性检查,在数据层保证最终准确性。
因此,在分层校验设定下,系统可以采用分布式缓存甚至LocalCache来抵抗高并发读。即允许读场景下一定的脏数据,这样只会导致少量原本无库存的下单请求被误认为是有库存的,等到真正写数据时再保证最终一致性,由此做到高可用和一致性之间的平衡。
实际上,分层校验的核心思想是:不同层次尽可能过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径。
6.2.4.2 高并发写
高并发写的优化方式,一种是更换DB选型,一种是优化DB性能,以下分别进行讨论。
6.2.4.3 更换DB选型
秒杀商品和普通商品的减库存是有差异的,核心区别在数据量级小、交易时间短,因此能否把秒杀减库存直接放到缓存系统中实现呢,也就是直接在一个带有持久化功能的缓存中进行减库存操作,比如 Redis?
如果减库存逻辑非常单一的话,比如没有复杂的 SKU 库存和总库存这种联动关系的话,个人认为是完全可以的。但如果有比较复杂的减库存逻辑,或者需要使用到事务,那就必须在数据库中完成减库存操作。
6.2.4.4 优化DB性能
库存数据落地到数据库实现其实是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁。但并发越高,等待线程就会越多,TPS 下降,RT 上升,吞吐量会受到严重影响——注意,这里假设数据库已基于上文【性能优化】完成数据隔离,以便于讨论聚焦 。
解决并发锁的问题,有两种办法:
盯过秒杀流量监控的话,会发现它不是一条蜿蜒而起的曲线,而是一条挺拔的直线,这是因为秒杀请求高度集中于某一特定的时间点。这样一来就会造成一个特别高的零点峰值,而对资源的消耗也几乎是瞬时的。所以秒杀系统的可用性保护是不可或缺的。
对于秒杀的目标场景,最终能够抢到商品的人数是固定的,无论 100 人和 10000 人参加结果都是一样的,即有效请求额度是有限的。并发度越高,无效请求也就越多。但秒杀作为一种商业营销手段,活动开始之前是希望有更多的人来刷页面,只是真正开始后,秒杀请求不是越多越好。因此系统可以设计一些规则,人为的延缓秒杀请求,甚至可以过滤掉一些无效请求。
答题
早期秒杀只是简单的点击秒杀按钮,后来才增加了答题。为什么要增加答题呢?主要是通过提升购买的复杂度,达到两个目的:
需要注意的是,答题除了做正确性验证,还需要对提交时间做验证,比如<1s 人为操作的可能性就很小,可以进一步防止机器答题的情况。答题目前已经使用的非常普遍了,本质是通过在入口层削减流量,从而让系统更好地支撑瞬时峰值。
排队
最为常见的削峰方案是使用消息队列,通过把同步的直接调用转换成异步的间接推送缓冲瞬时流量。除了消息队列,类似的排队方案还有很多,例如:
排队方式的弊端也是显而易见的,主要有两点:
排队本质是在业务层将一步操作转变成两步操作,从而起到缓冲的作用,但鉴于此种方式的弊端,最终还是要基于业务量级和秒杀场景做出妥协和平衡。
过滤
过滤的核心结构在于分层,通过在不同层次过滤掉无效请求,达到数据读写的精准触发。常见的过滤主要有以下几层:
1、读限流:对读请求做限流保护,将超出系统承载能力的请求过滤掉
2、读缓存:对读请求做数据缓存,将重复的请求过滤掉
3、写限流:对写请求做限流保护,将超出系统承载能力的请求过滤掉
4、写校验:对写请求做一致性校验,只保留最终的有效数据
过滤的核心目的是通过减少无效请求的数据IO保障有效请求的IO性能。
系统可以通过入口层的答题、业务层的排队、数据层的过滤达到流量削峰的目的,本质是在寻求商业诉求与架构性能之间的平衡。另外,新的削峰手段也层出不穷,以业务切入居多,比如零点大促时同步发放优惠券或发起抽奖活动,将一部分流量分散到其他系统,这样也能起到削峰的作用。
当一个系统面临持续的高峰流量时,其实是很难单靠自身调整来恢复状态的,日常运维没有人能够预估所有情况,意外总是无法避免。尤其在秒杀这一场景下,为了保证系统的高可用,必须设计一个 Plan B 方案来进行兜底。高可用建设,其实是一个系统工程,贯穿在系统建设的整个生命周期。
具体来说,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时,逐一进行分析:
对于日常运维而言,高可用更多是针对运行阶段而言的,此阶段需要额外进行加强建设,主要有以下几种手段:
在系统建设的整个生命周期中,每个环节中都可能犯错,甚至有些环节犯的错,后面是无法弥补的或者成本极高的。所以高可用是一个系统工程,必须放到整个生命周期中进行全面考虑。同时,考虑到服务的增长性,高可用更需要长期规划并进行体系化建设。
高可用其实是在说 “稳定性”,稳定性是一个平时不重要,但出了问题就要命的事情,然而它的落地又是一个问题——平时业务发展良好,稳定性建设就会降级给业务让路。解决这个问题必须在组织上有所保障,比如让业务负责人背上稳定性绩效指标,同时在部门中建立稳定性建设小组,小组成员由每条线的核心力量兼任,绩效由稳定性负责人来打分,这样就可以把体系化的建设任务落实到具体的业务系统中了。
一个极简、高效的秒杀系统-战术实践篇(内附源码)_温柔一cai刀的博客-CSDN博客
一个极简、高效的秒杀系统-战略设计篇_温柔一cai刀的博客-CSDN博客
susanSayJava/面试必考:如何设计秒杀系统?.md at master · dvsusan/susanSayJava · GitHub
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。