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 |
如何添加索引?
如何扣减库存?
- 读取判断库存, 然后扣减库存.
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), 但是这样有可能总库存一下子就成负数了.
所以如何防止超卖呢?
- 可以在读取和判断的过程中增加上事务.(直接在该行加入写锁)
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;
这样实际上是将所有请求都强制串行处理, 影响性能.
- 使用 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
;
超卖问题解决了, 那其他问题呢?
- 大量请求直接访问 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 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南