1 秒杀系统设计

实操-秒杀系统与订票系统设计

1 Scenario (场景)

限量出售, 售完为止.

QPS 分析: 平均每秒 1000 人访问, 秒杀时每秒 10w 人访问, QPS 100 倍增加.

流程如下:

graph LR A((用户)) --> |"购买"| B(创建订单) B --> K{"库存已无?"} K --> |"没库存"| L(秒杀结束) K --> |"有库存"| C{锁定库存成功?} C --> |"否"| D(下单失败) C --> |"是"| E(支付倒计时) E --> F{按时支付?} F --> |"是"| G(扣减库存) G --> H(购买成功) F --> |"否"| I(释放库存) I --> J(购买失败)

秒杀系统需要解决的问题

  • 瞬时大流量高并发

数据库一般是在单机 1000 QPS 左右.

  • 有限库存, 不能超卖, 也不能少卖.

  • 黄牛恶意请求, 使用脚本模拟用户购买.

  • 固定时间开始, 提前一秒都不可以.

  • 严格限购, 一个用户只能购买一个或者 N 个.

2 Service (服务)

单体架构 OR 微服务架构

单体架构的缺点: 耦合严重, 扩展性差, 一个故障导致服务不可用.

微服务架构的优点: 解耦合, 扩展性强, 故障隔离

3 Storage (存储)

数据库表的设计:

  • 商品信息表(commodity_info)
id 名称 价格
189 IPhone 14 13999
  • 秒杀活动表(seckill_info)
id 秒杀名称 commodity_id 数量
28 618 IPhone 11 64G 秒杀 189 100
  • 库存信息表(stock_info)
id commodity_id seckill_id 库存 stock 锁定 lock
1 189 0 (表示没有活动, 普通售卖) 500 0
2 189 28 95 5
  • 订单信息表(order_info)
id commodity_id seckill_id user_id paid 是否付款
1 189 28 Jack 1

如何添加索引?

如何扣减库存?

  1. 读取判断库存, 然后扣减库存.

Select stock from stock_info where commodity_id = 189 and seckill_id = 28;

update stock_info set stock = stock - 1 where commodity_id = 189 and seckill_id = 28;

在并发场景中, 执行完第一个 sql 的后可能有几百个线程都拿到库存了, 发现还有充足的库存, 所以他们都将库存减去了1 (执行第二个 sql), 但是这样有可能总库存一下子就成负数了.

所以如何防止超卖呢?

  1. 可以在读取和判断的过程中增加上事务.(直接在该行加入写锁)

Start transaction;

Select stock from stock_info where commodity_id = 189 and seckill_id = 28 for update ;

update stock_info stock = stock - 1 where commodity_id = 189 and seckill_id = 28;

Commit;

这样实际上是将所有请求都强制串行处理, 影响性能.

  1. 使用 Update 语句自带的行锁

Select stock from stock_info where commodity_id = 189 and seckill_id = 28;

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 单点能支撑 10w QPS, 可以考虑将库存信息加载到 Redis 中, 直接通过 Redis 来判断并扣除库存.多个指令可以通过 Lua 脚本事务操作来实现原子性.

什么时候进行数据库预热?

在秒杀开始之前: Set seckill:28:commodity:189:stock 100

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

这个流程中的问题

检查 Redis 库存和扣减 Redis 库存是两部操作, 在并发场景下仍然会超卖. 所以哪怕 Redis 侧放行, 可以创建订单了, 到 MySQL 的时候也需要检查一次. (double check)

由此引入的新问题

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

如何解决

通过 Lua 脚本来执行原子操作. Lua 脚本类似 Redis 事务, 有一定的原子性, 不会被其他命令插队. 解决了 CAS (check-and-set) 的需求.

如果参加秒杀的商品数量是 10w 台呢

通过消息队列, 将 Redis 发送到 MySQL 的请求进行削峰处理. 如果 Redis 执行 Lua脚本扣减库存成功, 那么就将这个消息投递到消息队列中, 然后将成功的信息返回给用户.

库存扣减时机

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

如何限购

MySQL 数据校验, 查询订单表. 这种方式 MySQL 不能支持大规模的 QPS, 所以必须将数据校验放在 Redis 上去做. 使用 Redis 的 Set 类型, 将下单过的用户id放入 set 中: SADD key value1 value2 ... 在用户下单前检查 set 中是否已经有了用户id: SISMEMBER key value.

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

要使用分布式事务来保证多个事务的一致性. 保证在多个事务中, 所有事务要么全部成功, 要么全部失败. 分布式事务使用三阶段提交的方式:

  • 询问是否可以提交(?)

  • 事务执行但是不提交, 将 undo/redo 日志写入到事务日志中. 这里如果一个事务执行不成功或者执行超时了, 那么就会强制回滚所有事务.

  • 执行事务提交或者回滚事务.

4 Scale 扩展(如何优化系统, 加分项)

数据库/缓存

可能 10w 人抢购 100 台 IPhone, 大部分请求都是无效的, 所以如果库存已经都没了, 那么就直接拒绝请求.

防止刷爆商品界面

前端资源静态化, 使用 CDN 来做. CDN 是 Content Delivery Network 内容分发网络, 依靠部署在各地的服务器, 通过中心平台负载均衡, 内容分发, 调度等功能, 使用户就近获取所需的内容, 降低网络堵塞, 提高用户访问响应速度和命中率.

限流器: 每秒只接受固定请求, 其他请求跳转到繁忙页上去.

前端限流: 点击一次之后, 按钮置灰.

秒杀服务器挂掉怎么办?

服务雪崩: 因为服务提供者的不可用导致服务的消费者的不可用, 并且将这种不可用沿着调用链路主键放大的过程.

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

技术选型: Netflix Hystrix / Alibaba Sentinel

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

验证码

限流机制: 如何设计限流器 (这是一个面试总会问到的题)

posted @ 2023-02-04 10:57  kohn  阅读(61)  评论(0)    收藏  举报