1 秒杀系统设计

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

1 Scenario (场景)#

限量出售, 售完为止.

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

流程如下:

购买
没库存
有库存
用户
创建订单
库存已无?
秒杀结束
锁定库存成功?
下单失败
支付倒计时
按时支付?
扣减库存
购买成功
释放库存
购买失败

秒杀系统需要解决的问题

  • 瞬时大流量高并发

数据库一般是在单机 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

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

验证码

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

作者:Kohn

出处:https://www.cnblogs.com/geraldkohn/p/17091094.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   kohn  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示