不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

系统设计_秒杀系统和订票系统

系统设计分析

Scenario 场景

秒杀系统场景

2020 年6 月18 日 0 点开始,京东自营限量 100台,以 4000 元的价格,抢购 iPhone 11 64G 版本,先到先得,一人限购一台,售完即止。
微信抢红包
抢春运火车票
抢购小米手机

QPS 分析

平日每秒 1000 人访问该页面。
秒杀时每秒数10万人访问该页面。
QPS 增加 100 倍以上。

商品购买和下单流程

秒杀系统需要解决问题

  • 瞬时大流量高并发:服务器、数据库等能承载的 QPS 有限,如数据库一般是单机 1000 QPS。需要根据业务预估并发量。
  • 有限库存,不能超卖:库存是有限的,需要精准地保证,就是卖掉了 N 个商品。不能超卖,当然也不能少卖了。
  • 黄牛恶意请求:使用脚本模拟用户购买,模拟出十几万个请求去抢购。
  • 固定时间开启:时间到了才能购买,提前一秒都不可以(以商家「京东」「淘宝」的时间为准)。
  • 严格限购:一个用户,只能购买 1 个或 N 个。

需求拆解

商家侧(京东自营、淘宝天猫店家):新建秒杀活动,配置秒杀活动。

用户侧:商品秒杀页面(前端或客户端),购买,下单,付款。

Service 服务

服务结构设计 - 单体架构

  • 前后端耦合,服务压力较大。
  • 各功能模块耦合严重。
  • 系统复杂,一个模块的升级需要导致整个服务都升级。
  • 扩展性差,难以针对某个模块单独扩展。
  • 开发协作困难,各个部门的人都在开发同一个代码仓库。
  • 级联故障,一个模块的故障导致整个服务不可用。
  • 陷入某种单一技术和语言中。
  • 数据库崩溃导致整个服务崩溃。

服务结构设计 - 微服务

  • 各功能模块解耦,保证单一职责。
  • 系统简单,升级某个服务不影响其他服务。
  • 扩展性强。可对某个服务进行单独扩容或缩容。
  • 各个部门协作明晰。
  • 故障隔离。某个服务出现故障不完全影响其他服务。
  • 可对不同的服务选用更合适的技术架构或语言。
  • 数据库独立,互不干扰。

Storage 存储

数据库表设计

商品信息表commodity_info

商品id-id 商品名称-name 商品描述-desc 价格-price
189 iPhone 11 64G xxxx 5999

库存信息表stock_info

库存id-id 商品id-commodity_id 活动id-seckill_id 库存-stock 锁定-lock
1 189 0 100000 0
2 189 28 100 5

秒杀活动表seckill_info

秒杀id-id 秒杀名称-name 商品id-commodity_id 价格-price 数量-number
28 iPhone 11 64G 189 4000 100

订单信息表order_info

订单id-id 商品id-commodity_id 活动id-seckill_id 用户id-user_id 是否付款-paid
1 189 28 Jack 1

数据流

秒杀操作

扣减库存

读取判断库存,然后扣减库存:

  1. 查询库存余量

    1
    2
    SELECT stock FROM `stock_info`
    WHERE commodity_id = 189 AND seckill_id = 28;
  2. 扣减库存

    1
    2
    UPDATE `stock_info` SET stock = stock - 1
    WHERE commodity_id = 189 AND seckill_id = 28;

读取和判断过程中加上事务:

1,事务开始

1
START TRANSACTION;
  1. 查询库存余量,并锁住数据

    1
    2
    SELECT stock FROM `stock_info`
    WHERE commodity_id = 189 AND seckill_id = 28 FOR UPDATE;
  2. 扣减库存

    1
    2
    UPDATE `stock_info` SET stock = stock - 1
    WHERE commodity_id = 189 AND seckill_id = 28;
  3. 事务提交

使用 UPDATE 语句自带的行锁:

  1. 查询库存余量

    1
    2
    SELECT stock FROM `stock_info`
    WHERE commodity_id = 189 AND seckill_id = 28;
  2. 扣减库存

    1
    2
    UPDATE `stock_info` SET stock = stock - 1
    WHERE commodity_id = 189 AND seckill_id = 28 AND stock > 0;

超卖问题解决了,其他问题呢?

  1. 大量请求都访问 MySQL,导致 MySQL 崩溃。
    对于抢购活动来说,可能几十万人抢 100 台 iPhone,实际大部分请求都是无效的,不需要下沉到 MySQL。

库存预热

秒杀的本质,就是对库存的抢夺。
每个秒杀的用户来都去数据库查询库存校验库存,然后扣减库存,导致数据库崩溃。

MySQL 数据库单点能支撑 1000 QPS,但是 Redis 单点能支撑 10万 QPS,可以考虑将库存信息加载到 Redis 中。直接通过 Redis 来判断并扣减库存。

什么时候进行预热 (Warm-up)?

活动开始前:

通过 Redis 扣减库存

语法: GET KEY
作用: 获取 key 存储的值
GET seckill:28:commodity:189:stock

语法: DECR KEY
作用: 将 key 中储存的数字值减一
DECR seckill:28:commodity:189:stock

大部分请求都被 Redis 挡住了,实际下沉到 MySQL 的理论上应该就是能创建的订单了。比如只有 100 台 iPhone,那么到MySQL 的请求量理论上是 100。

这个流程有没有问题?
1.检查 Redis 库存和扣减 Redis 库存是两步操作。
2.有并发问题仍然会导致超卖。

解决方案
哪怕 Redis 侧放行,可以创建订单了,到MySQL 的时候也需要再检查一次。

新的问题
如果并发量超高,Redis 侧实际超卖的量过大,如 100万个请求同时到达,Redis 全部放行。再到 MySQL 去检测,那 Redis 作用等于没有。

通过 Lua 脚本执行原子操作
Lua 脚本功能是 Reids 在 2.6 版本中推出, 通过内嵌对 Lua 环境的支持,Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
Lua 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 Redis 事务性的操作。

如果秒杀数量是1万台,或者10万台呢?
因为 Redis 和 MySQL 处理能力的巨大差异。实际下沉到MySQL 的量还是巨大,MySQL 无法承受。

解决思路
可不可以在通过 Redis 扣库存后,到 MySQL 的请求慢一点?

解决方案
通过消息队列(Message Queue,MQ)进行削峰(Peak Clipping)操作。

通过消息队列异步地创建订单

如果消息队列出现部分投递失败怎么办?
Redis 中的库存量,可以比实际的库存量多一点,比如 1.5 倍或者 2倍。

库存扣减时机

下单时立即减库存。

用户体验最好,控制最精准,只要下单成功,利用数据库锁机制,用户一定能成功付款。
可能被恶意下单。下单后不付款,别人也买不了了。

先下单,不减库存。实际支付成功后减库存。

可以有效避免恶意下单。
对用户体验极差,因为下单时没有减库存,可能造成用户下单成功但无法付款。

下单后锁定库存,支付成功后,减库存。

如何限购

MySQL 数据校验

Redis 数据校验

付款和减库存的数据一致性

分布式事务

保证多个存在于不同数据库的数据操作,要么同时成功,要么同时失败。主要用于强一致性的保证。

三阶段提交,有超时机制

Sacle 拓展

是否有遗漏什么功能?

可能十万人抢购 100 台 iPhone,大部分请求是无效的。
Redis 能力高过 MySQL,但能力还是有限。
Redis 库存扣减完毕后,是否后面的请求可以直接拒绝了?

防止刷爆商品页面

CDN 的全称是 Content Delivery Network,即内容分发网络。
CDN 是依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

前端限流:点击一次后,按钮短时间内置灰;部分请求直接跳转到「繁忙页」。

未开始抢购时,禁用抢购按钮。

如何计算倒计时?

  1. 打开页面获取活动开始时间,然后前端页面开始倒计时
  2. 打开页面获取距离活动开始的时间差,然后前端页面开始倒计时
  3. 前端轮询 (Poll) 服务器的时间,并获取距离活动开始的时间差

秒杀服务器挂掉,怎么办?

尽量不要影响其他服务,尤其是非秒杀商品的正常购买。

服务雪崩 (Avalanche)

多个微服务之间调用的时候,假设 微服务A 调用 微服务B 和 微服务C,微服务B 和微服务C 又调用其他的微服务,这就是所谓的”扇出 (Fan-out)”,如扇出的链路上某个微服务的调用响应式过长或者不可用,对 微服务A 的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”。
服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费着”的不可用并将这种不可用逐渐放大的过程。

服务熔断 (Fuse or Circuit-breaker)

熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,快速返回”错误”的响应信息。当检测到该节点微服务响应正常后恢复调用链路。

防止恶意刷请求或者爬虫请求

验证码机制Verification Code Mechanism

抢购->填写验证码->进入抢购服务

限流机制Ratelimit Mechanism

黑名单机制Blacklist Mechanism

1.黑名单 IP 地址
2.黑名单用户ID

秒杀系统 vs 订票系统

在业务上,他们有哪些差异?
100 台 iPhone 没有区别
但是 100 张同一车次的火车票,有座位的区别(暂时忽略一等座二等座等)

QA

  1. microservice⾥,db基本上不都是⼀个吗?都host在⼀个database的service⾥?
    基本原则是每个微服务都有⾃⼰单独的数据库,⽽且只有微服务本⾝可以访问这个数据库。微服务之间的数据共享可以通过服务调⽤,或者主、从表的⽅式实现。

  2. 为啥不能做成串⾏的单⼀职责系统?毕竟秒杀过不了的话,也就没必要提供商品信息/库存或者订单服务是吧?
    可以做成单⼀串⾏,使⽤微服务的设计思想,然后再⽤分布式的部署⽅式。

  3. 怎么理解事务提交的同时失败?万⼀两个⼈都通过服务器提交,同时失败就说俩⼈都没秒成?
    ⼀个事务中可能有许多的操作,⽐如修改库存信息,修改订单信息,修改⽀付信息等等,假设修改库存信息和修改订单信息都成功了,但是最有修改⽀付信息失败了,那么之前成功的操作都要回滚。

  4. 对于库存预热Redis,具体是如何更新对应object呢?在query时候,是同时更新SQL和Redis吗?
    先更新redis,再异步更新mysql

  5. 假如这时候苹果还有两个,那为⼩明锁定库存的时候,另⼀个⼈还可以买其中另⼀个吗?锁定库存是锁定所有,还是锁住其中⼀个?
    如果⼩明买了⼀个,那就只会锁定⼀个,另⼀个还是可以购买。

  6. 在数据库⾥怎么实现timer计时锁定?
    下⾯⼀种通过数据库来实现,我们加上⼀个定时任务表,字段有执⾏时间,version字段,每个定时任务对应表中的⼀条记录,通过update…whereversion=and update_date=做CAS操作。

  7. 所以Redis在这⼉只是当做cache来⽤吗?
    是的

  8. ⽣产者如果⽣产速度⽐消费者快,消息队列是不是会⽆限延⻓?
    是的

  9. 在Redis⾥的库存是1.5-2倍的话也就是有⼈会在最后付钱的时候db库存不够⽽失败是吧?
    不会,Redis这边只是做初步校验,通过Redis后还需要在数据库中进⾏判断,如果库存还有余,才能创建订单。Redis的作⽤只是拦截⼤部分的请求,只有少部分请求才会落到数据库。

  10. 前端校准时间怎么平衡poll服务器时请求的roundtrip时间。不然离服务器更近的⽤⼾就会⽐更远的⽤⼾有优势?
    前端是可以获取服务端到客⼾端响应时间的。

  11. 前端poll的时间是哪台服务器的时间?难道有⼀个clockservice?
    对,后端会有⼀个专⻔做时间校准的服务器。

  12. 为什么Redis上的并发不会使得redis上的库存出现超卖?
    因为redis是单线程的,串⾏执⾏,再加上lua脚本事务来解决这个问题

  13. 如果这个系统需要多个服务那在哪个S聊服务之间的通讯?
    系统设计中呢不同的s聊代表了不同的服务,具体是要要看场景业务服务还是存储服务等需求。

  14. querypersecond
    ⼀般4S分析法中Scale服务涉及到性能优化,所以⼀般QPS涉及到查询效率问题会⽤Scale服务。

  15. ⾯试中这些需要解决的问题是⾯试官提出的吗?
    ⼤部分的浅显的问题是⾯试官提问的,但是呢你在进⾏设计思考的时候如果可以将⾯试官注意不到的问题考虑在内的话,然后对问题进⾏解答,那么⾯试官⼀定会很欣赏你。

  16. ⼀个模块⼀个服务器流量分流?
    模块和服务器不⼀定要⼀对⼀,因为可以⼀对多,多对⼀。

  17. ⾯试的时候需要把流程图和这些优缺点对⽐都要体现出来吗?
    ⾯试的时候设计的优缺点你都考虑在内的话当然对你⾯试更加有利呀

  18. 什么情况下会⽤单体架构呢?
    简单的项⽬,单体可以满⾜就⽤单体

  19. 如果某⼀个服务变的很复杂,然后⾃⼰变成了⼀个⼤单体怎么办?
    增加这个服务的机器数量

  20. 就是⽤Redis做缓存吧
    也可以将数据持久化存储到磁盘上的

  21. nosql就是cache系统吗?Redis和memcache怎么选呢?
    nosql是数据库,不是缓存,只是基于内存的nosql很快,可以⽤做缓存。redis更强⼤,memcached更快

  22. nosql都是内存存储吗,那断电了怎么办
    不是,redis是内存的,但是可以持久化,mencached是内存的,但是不能持久化,hbase是nosql,硬盘的。具体可以⾕歌搜索“nosql持久化”

  23. redis扣减库存,如何保证和mysql中的库存保持⼀致,数据同步的时机和频率如何控制?redis宕机或数据丢失,如何保证库存的正确性?
    现在数据库操作有读写⼀致的技术,所以我们的database数据变化了就可以使⽤读写⼀致来能保证redis数据⼀致性

  24. 库存扣减以后,如何更新Redis的集群来反映更新后的库存呢?
    使⽤redis实现扣减库存,由于是分布式环境下所以还需要⼀个分布式锁来控制只能有⼀个服务去初始化库存,需要提供⼀个回调函数,在初始化库存的时候去调⽤这个函数获取初始化库存,库存扣减完之后可以进⾏⼀个异步的更改数据库数据,保证⼀致性。

  25. “锁定”是指SELECTFORUPDATE⾏锁吗
    看数据库和存储引擎,不是所有的数据库都⽀持⾏锁

  26. 消息队列的最⼤容量是多少?可配置的吗?
    最⼤容量是10000,可以配置的

  27. 如果使⽤了Lua脚本,假设Redis中只有100个库存,理论上只会有100个请求会通过库存扣减,下沉到数据库,为什么还需要消息队列呢?
    消息队列可以解耦合,如果有些任务产⽣异常那么消息队列可以阻⽌我们的任务因为异常阻塞

  28. 秒杀系统⼤概多少的latency是可以接受的?
    我们在后台可以开启n个队列处理程序,不断的消费消息队列中的任务,然后校验库存接着下单等操作,现在由于我们是有限的队列处理线程在执⾏,所以最终落到数据库上的并发请求也是有限的。⽤⼾请求是可以在消息队列中短暂堆积的,当库存为零了,消息队列堆积的请求也就可以全部释放了。

  29. ⽤⼾等待下单成功是同步操作吗?如果加了消息队列后,会不会由于消息过多,导致⽤⼾等待响应时间过⻓?
    消息队列解耦合呀,不同的任务会异步执⾏,不会等待时间过⻓产⽣阻塞问题。

  30. 消息队列,装不下了,就不接收了。
    可以⽤分布式消息队列

  31. Redis真好⽤啊
    众多语⾔都⽀持Redis,因为Redis交换数据快,所以在服务器中常⽤来存储⼀些需要频繁调取的数据,这样可以⼤⼤节省系统直接读取磁盘来获得数据的I/O开销,更重要的是可以极⼤提升速度。

  32. 锁定库存是啥意思。。如果限制10个,有11个⼈抢,然⽽第⼀个⼈到最后也没付款,那在付款时间内15mins第11个⼈可以抢单吗(这个时候库存被锁定了)
    下单后锁定库存,超时会释放库存

  33. 这⾥本质上是想通过redis来隔离,避免直接访问数据库么
    提⾼查询性能减少查询数据库效率

  34. 锁定库存,就是⼀个客⼾把产品预订了。
    是的,预定⼀个产品以后会锁定库存。

  35. cdn节点间同步的时间延迟多久
    CDN节点的缓存内容不是实时更新的,只有当缓存内容到期后才能回源拉取最新的内容并更新节点缓存。您可以通过设置缓存过期时间规则或者提交刷新请求来实现缓存内容的更新。

  36. 有张slide,消息队列投递失败,然后⽼师给的答案是增⼤Redis容量到1.5倍。没看懂什么意思。可以解释下吗?
    redis(key:value数据库,缓存,消息队列),redis不仅仅是缓存数据,也是消息队列的产物,所以如果消息队列投递失败,我们可以⽤增⼤redis容量来解决问题。

  37. mysql库存加回去了,redis也要加回库存,刚才好像没有体现
    redis缓存库存数据中,数据是和mysql数据库中保持⼀致的,如果库存变化了,那么redis当然是要变化的。

补充

Redis简介

一种主要将数据存储于内存中的非关系型的键值对数据库 (NoSQL 的一种) ,但也可以将数据持久化(Data Persistence) 到磁盘中。
支持多种数据非关系型的数据结构。

  • 1.字符串/数字 (STRING)
  • 2.哈希表 (HASH)
  • 3.链表 (LIST)
  • 4.集合 (SET)
  • 5.有序集合 (ZSET)

单线程的数据库。通过IO多路复用实现并发。
支持数据的主备容灾 (Disaster Tolerance) 存储。
所有单个指令操作都是原子的,即要么完全执行成功,要么完全执行失败。多个指令也可以通过 Lua 脚本事务操作实现原子性。
因为都在内存中操作,性能极高,单机一般可支撑 10万数量级的 QPS。
可用作数据缓存 (Cache)、数据持久存储和消息队列 (Message Queue)。

消息队列简介

一类基于生产者/消费者模型的组件。
用于实现两个不同的系统之间的解耦和异步操作。
生产者可以高速地向消息队列中投递(生产)消息。
消费者可以按照自己的节奏去消费生产者投递的消息。
消息队列一般带有重试的能力。可以持续投递,直到消费者消费成功。

------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!