秒杀系统
秒杀系统
简历描述
高并发秒杀系统 - Golang
- 设计并实现基于 Go、Kafka、Etcd
的高并发秒杀系统,涵盖抢购下单、库存扣减、异步落库、排行榜等核心功能,单节点
QPS 3000,支持 K8s 3 节点集群部署。
- 使用 Etcd 实现强一致分布式锁,结合 WAL 持久化记录锁状态(UUID +
LeaseID),实现锁的自动续租与服务重启恢复机制,避免锁丢失与惊群问题。
- 引入令牌桶限流与 Kafka 异步下单机制,将秒杀高峰削峰至 1000 QPS,结合
Redis 缓存库存预热策略,在3节点环境下,1000 QPS 持续 30
秒压测,总请求中约 20% 出现 429 限流,总成功率达 70%。
- 实现 Redis ZSet 异步排行榜同步机制,使用 Kafka + Goroutine
将高并发更新操作异步解耦,排行榜更新时间控制在 100ms 以内。
- 部署 Prometheus 和 Grafana,监控库存命中率、限流 QPS、Kafka 消费延迟等关键指标,保障系统性能与稳定。
问题 第一行
设计并实现基于 Go、Kafka、Etcd 的高并发秒杀系统,涵盖抢购下单、库存扣减、异步落库、排行榜等核心功能,单节点 QPS 3000,支持 K8s 3 节点集群部署。
1. 秒杀系统整体架构是怎样的?各组件之间如何通信?
架构总览:
- 整体分为:网关层(限流)→ 秒杀服务(校验、锁、库存)→ 消息队列(Kafka)→ 异步落库服务 → 排行榜同步服务;
- 各模块通过 HTTP/gRPC + Kafka 实现异步解耦;
- 读操作走 Redis 缓存,写操作通过 Kafka 异步落库,最终一致性。
通信流程简述:
- 用户发起秒杀请求,经网关限流(令牌桶)后到达
seckill-service
; seckill-service
检查库存缓存 → 加 Etcd 锁 → 原子扣减 Redis 库存;- 请求成功后写入 Kafka(下单 Topic);
order-consumer
消费消息并写入 MySQL;- 同时 Kafka 另一路消息用于排行榜异步更新(ZSet);
- 所有模块接入 Prometheus,暴露
/metrics
端点用于监控。
2. 令牌桶算法是什么,解决什么问题,具体数值设置为多少
原理简述:
- 令牌桶是流控算法之一,系统按固定速率生成令牌,请求必须获取令牌才被处理,超限请求被拒绝(返回 429);
- 相比漏桶能容忍短期突发流量(令牌可以累积);
在本项目中:
- 每个商品设置 1000 QPS
的令牌速率(
rate = 1000/s
),桶容量为 1000; - 使用 Redis + Lua 实现共享限流器,支持多副本共享状态;
- 限流控制入口在网关或服务中间件(Gin 中间件);
解决的问题:
- 避免瞬时流量压垮下游系统(Redis、MySQL);
- 提升服务稳定性,使后端服务处理能力与流量强度解耦。
3. 单节点 QPS 3000 是如何测试的?测试工具和环境如何配置?
- 使用k6进行压力测试,配置了 1000 个虚拟用户并发请求,最大尝试每秒 3000 次请求,持续 30 秒。测试环境为单节点部署,docker-compose 启动 Golang 服务,加上健康检查,Kafka 和 Etcd 也在同一节点上运行。
- 单节点,i5-12600KF, 32GB DDR4 6000MHz;接口为本地网关直连(无 CDN、无反向代理),实际上线运行时为防止下游写压力,设置削峰限流至 1000 QPS。
4. Kafka 在系统中的作用具体是什么?为什么不用 RabbitMQ?
核心解耦组件:
- 接收异步下单消息;
- 分发排行榜更新事件;
- 实现削峰填谷,防止高并发直接写数据库;(削峰:在流量高峰期,通过限流、排队、异步处理等方式,减少瞬时请求量,防止系统过载;填谷:在流量低谷期,利用系统空闲资源处理积压的请求,避免资源浪费。)
选择Kafka的原因:
- 吞吐高,消息积压容忍度高
- 持久化存储默认使用WAL,高可用
- 分区有序适合订单类场景,RabbitMQ无强顺序保证
- Go有sarama库支持
5. Etcd 主要做什么?为什么不选 Redis 实现锁?
etcd的作用:
- 提供 CP(强一致)分布式锁服务;
- 使用
Put + Lease + Compare
保证锁唯一性与可恢复性; - 锁内容包含 UUID、商品 ID、租约 ID,配合 WAL 落盘支持自动恢复;
- 避免惊群与锁漂移问题。
惊群问题
问题描述
当多个客户端同时竞争同一把锁时,如果锁释放,所有等待的客户端会同时被唤醒,并疯狂重试抢锁,导致:
- 大量无效请求:只有一个客户端能成功,其余请求被拒绝,造成资源浪费。
- 网络和 CPU 压力激增:高并发场景下可能压垮服务端。
etcd 的解决方案
etcd 通过
Lease
(租约) +Watch
机制 避免惊群:
- 客户端抢锁失败后,不会轮询查询,而是通过
Watch
监听锁的释放事件。- 锁释放时,etcd 只会通知一个等待的客户端(按 FIFO 或公平调度),其他客户端继续等待。
对比 Redis 的缺陷: Redis 的锁(如 Redlock)在释放时,所有客户端会立即重试抢锁,容易引发惊群效应。
点 | Etcd | Redis |
---|---|---|
一致性 | CP 保证强一致 | AP,有主从延迟 |
锁续约 | 有 Lease 机制 | 需手动设置过期 |
健康检查 | 原生绑定客户端 | 需额外处理心跳 |
自动恢复 | Lease+WAL 可恢复 | 崩溃后锁状态丢失 |
6. 3 节点 K8s 部署是多台物理机还是单机模拟?调度策略怎么做的?
- 多台物理机,每台机器运行一个 K8s 节点,使用 kubeadm 部署;
- 调度策略:使用 K8s 的默认调度器,Pod 分布在 3 个节点上,保证高可用;
- Pod 副本数:每个服务(如
seckill-service
、order-consumer
)设置为 3 副本,确保负载均衡和容错; - 服务发现:使用 K8s 内置的 DNS 服务,Pod 通过服务名访问;
- 健康检查:配置 Liveness 和 Readiness 探针,确保 Pod 健康状态,自动重启不健康的 Pod。
- 资源限制:每个 Pod 设置 CPU 和内存限制,防止单个 Pod 占用过多资源影响其他服务。
7. 下单、扣库存、落库、排行榜之间是串行的还是异步解耦的?
- 下单:用户请求到达
seckill-service
,先进行限流和库存检查; - 扣库存:通过 Etcd 分布式锁保证同一商品的并发请求串行处理,避免超卖;
- 落库:下单成功后,将订单信息写入 Kafka,异步消费落库;
- 排行榜:使用 Kafka
异步更新排行榜,
order-consumer
消费订单消息后更新 Redis ZSet。 - 异步解耦:下单、扣库存、落库、排行榜更新之间通过 Kafka 实现异步解耦,避免直接依赖,提高系统吞吐量。
8. 高并发下如何避免超卖和重复下单?
- 超卖:使用 Etcd 分布式锁,确保同一商品的并发请求串行处理,锁粒度为商品 ID;
- 重复下单:每个订单生成唯一 ID(如
UUID),在下单前检查 Redis 缓存中是否已存在该订单 ID;
- 如果存在,直接返回已下单响应;
- 如果不存在,继续处理下单逻辑;
- 幂等性:下单接口设计为幂等,确保同一请求多次发送不会导致重复扣减库存或重复下单;
- Redis 缓存:使用 Redis 缓存订单状态,快速判断是否已下单,避免重复请求。
- 数据库唯一索引:在 MySQL 中对订单 ID 设置唯一索引,防止重复插入。
- 乐观锁:在扣减库存时使用乐观锁(如
UPDATE ... WHERE stock = stock - 1
),确保库存正确。 - 事务处理:在 MySQL 中使用事务,确保下单、扣减库存、写入订单等操作要么全部成功,要么全部回滚,避免数据不一致。
- 限流:使用令牌桶限流,控制每秒请求数,防止瞬时高并发导致超卖。
9. 如何做多副本部署时的服务注册与发现?
- 服务注册:使用 K8s 的内置服务发现机制,Pod 启动时自动注册到 K8s DNS;
- 服务发现:其他服务通过 K8s 服务名访问,如
seckill-service
,K8s DNS 会自动解析到对应的 Pod IP; - 负载均衡:K8s 内置的负载均衡器会将请求分发到不同的 Pod 副本上,保证流量均匀分布;
- 健康检查:配置 Liveness 和 Readiness 探针,确保只有健康的 Pod 接受请求,自动剔除不健康的 Pod;
- 配置管理:使用 ConfigMap 和 Secret 管理配置和敏感信息,Pod 启动时自动加载;
- 滚动更新:使用 K8s 的滚动更新策略,确保服务升级时不中断服务,逐个替换 Pod 副本,保持高可用。
问题 第二行
使用 Etcd 实现强一致分布式锁,结合 WAL 持久化记录锁状态(UUID + LeaseID),实现锁的自动续租与服务重启恢复机制,避免锁丢失与惊群问题。
为什么选用 Etcd 做分布式锁,而不是 Redis?
- Etcd 提供 CP(强一致性)模型,适合需要强一致性的分布式锁场景;
- Redis 是 AP(可用性优先),在网络分区或主从延迟时可能导致锁状态不一致;
- Etcd 的
Lease
机制支持自动续租,避免锁过期导致的丢失; - Etcd 的
Compare-And-Swap
操作可以确保锁的唯一性和原子性; - Etcd 的
Watch
机制可以监听锁状态变化,避免惊群问题。
你是如何使用 LeaseID 做自动续租的?是否会续租失败?
- 使用
Grant
方法创建租约,指定租约 TTL(如 60 秒); - 在获取锁时,将租约 ID 绑定到锁的 key 上;
- 启动一个协程定期调用
KeepAliveOnce
方法续租,确保锁在 TTL 内不会过期; - 如果续租失败(如 Etcd 集群不可用),会记录日志并尝试重新获取锁;
- 如果服务重启,Etcd 会自动恢复锁状态,只要租约未过期,锁仍然有效。
1 | // Etcd 锁续租示例 |
什么是 WAL(Write-Ahead Log)?你怎么实现落盘和恢复的?
- WAL(Write-Ahead Log):写前日志,是一种持久化机制,确保数据在内存操作前先写入日志,保证数据一致性和可靠性;
- 在 Etcd 中,WAL 用于记录所有写操作(如锁的获取和释放),即使服务崩溃也能恢复锁状态;
- 实现步骤:
- 在获取锁时,将锁状态(如 UUID、LeaseID)写入 WAL;
- 使用 Etcd 的
Put
操作将锁状态存储在 Etcd 中; - 服务重启时,读取 WAL 日志,恢复锁状态;
- 如果租约未过期,继续使用原有锁状态,否则重新获取锁。
- WAL 日志通常存储在 Etcd 的数据目录中,Etcd 会自动管理日志的回收和清理。
- Etcd 的 WAL 机制保证了即使在网络分区或节点故障时,锁状态也不会丢失。
如果服务意外崩溃,如何保证锁的状态不会丢失?
- Etcd 使用 WAL(写前日志) 持久化所有写操作,锁的状态(UUID、LeaseID 等)都会先写入 WAL 确保数据安全。
- 锁绑定了一个租约(Lease),租约是有 TTL 的,Etcd 集群管理租约的生命周期。
- 当服务崩溃后重启,Etcd 会从 WAL 日志恢复锁状态,只要租约未过期,锁仍然有效。
- 租约到期,Etcd 自动释放锁,避免死锁。
- 通过租约机制避免服务异常挂掉后锁永久持有,保证锁不会丢失也不会被无限期占用。
什么是惊群问题?Etcd 如何避免?
- 惊群问题 指多个客户端同时等待同一把锁释放,锁释放时所有客户端同时被唤醒,疯狂尝试抢锁,造成大量无效请求和资源浪费。
- Etcd 通过以下机制避免惊群:
- 使用
Watch
机制,等待抢锁失败的客户端订阅锁状态变化事件; - 只有一个客户端会收到锁释放通知,依次抢锁,避免所有客户端同时争抢;
- 通过锁请求排队保证公平调度,减少抢锁冲突和重试压力。
- 使用
- 相比 Redis Redlock 释放锁后所有客户端同时重试,Etcd 的 Watch 机制有效缓解了惊群。
你们有没有考虑锁饥饿、死锁的情况?
锁饥饿:
- Etcd 的锁实现基于公平队列,按照请求先后顺序分配锁,避免某些客户端长期得不到锁。
- 租约 TTL 机制限制持锁时间,避免持锁者阻塞过久导致其他请求饥饿。
死锁:
- 由于 Etcd 使用租约机制,锁会自动过期释放,防止死锁。
- 应用层设计中避免锁重入和长时间持锁,保证锁粒度合理。
- 在业务逻辑中通过合理拆分锁、避免交叉依赖,降低死锁风险。
Lease 到期前锁失效怎么办?是否会误删其他节点的锁?
- 租约到期导致锁失效通常是因为:
- 持锁服务崩溃或网络隔离,续租请求无法发送到 Etcd。
- 租约 TTL 到期,Etcd 自动释放锁。
- Etcd 租约绑定机制保证锁与租约一一对应:
- 只有拥有对应租约的客户端才能续租或释放该锁。
- 租约过期后,锁自动释放,不会误删其他客户端持有的锁。
- 续租失败后客户端应重新尝试获取锁,防止资源竞争异常。
- 通过 LeaseID 绑定锁,避免误删其他节点锁的情况发生。
问题 第三行
引入令牌桶限流与 Kafka 异步下单机制,将秒杀高峰削峰至 1000 QPS,结合 Redis 缓存库存预热策略,在 3 节点环境下,1000 QPS 持续 30 秒压测,总请求中约 20% 出现 429 限流,总成功率达 70%。
为什么用令牌桶限流而不是漏桶?限流粒度是 IP 级还是全局?
- 令牌桶优点:令牌桶允许短时间内的突发流量积累(桶内令牌可以暂存),而漏桶算法是固定速率漏水,不支持突发流量。秒杀场景常有突发流量,令牌桶更适合控制瞬时峰值。
- 限流粒度:本系统采用全局限流(即对整体请求流量进行限制),也可以基于商品 ID 做细粒度限流。
- IP 级限流可以防止单个 IP 过度请求,但对秒杀场景影响有限,因此主要用全局和商品维度限流。
你是如何设置令牌生成速率和桶容量的?
- 令牌生成速率设置为秒杀商品的最大处理能力,当前配置为
rate=1000/s
,即每秒产生1000个令牌。 - 桶容量设置为 1000,允许短期突发请求快速通过令牌消耗。
- 通过压测和线上监控反馈,动态调节令牌速率以适应系统承载。
Kafka 下单异步流程怎么保证消息不丢?用了哪些配置?
- 设置 Producer 的
acks=all
,确保所有副本写入确认。 - 启用 Producer 重试机制
retries
,防止临时失败丢消息。 - 使用幂等生产者
enable.idempotence=true
,避免重试导致重复消息。 - Consumer 端采用手动提交 offset,确认消费成功后再提交。
- 监控 Consumer 消费延迟和失败重试机制,避免消息积压和丢失。
Redis 缓存库存是怎么预热的?预热策略是什么?怎么回源?
- 秒杀开始前,提前将商品库存数量写入 Redis,避免热点查询数据库。
- 预热时使用批量写入,减少 Redis 连接和网络消耗。
- 库存扣减先在 Redis 原子执行,库存不足时回源数据库或直接拒绝。
- 异步定期同步 Redis 库存和数据库库存,保证数据一致性。
为什么 429 限流率这么高?是否考虑更细化限流策略?
- 秒杀是高并发场景,超过系统处理能力时必须限流,20%限流保证系统稳定运行。
- 后续考虑对不同用户、IP、商品做分层限流,降低整体拒绝率。
- 通过更精细的限流粒度,提升用户体验和成功率。
成功率 70% 是针对所有请求吗?剩下 30% 是失败还是限流?
- 成功率指所有到达系统的请求中,真正完成下单并写入数据库的比例。
- 剩余约 30% 主要为限流拒绝(429)和库存不足导致失败。
- 失败请求中大部分为限流,少部分因库存耗尽。
压测用的是什么工具?并发模型是怎样的?
- 使用 k6 作为压测工具,模拟 1000 个虚拟用户持续请求。
- 并发模型为固定并发,持续 30 秒,逐步增加请求压力至 3000 QPS。
- 压测环境为单节点部署,包含 Golang 服务、Kafka、Etcd。
如果 Kafka 消费延迟了,会导致库存错误吗?如何避免重复消费?
- Kafka 消费延迟不会直接导致库存错误,库存扣减在 Redis 端实时处理,库存是强实时控制点。
- Kafka 异步落库确保最终订单数据一致,延迟只影响落库时效。
- 为避免重复消费,使用 Consumer 手动提交 offset 并结合幂等订单 ID,确保同一订单只写一次数据库。
如何保证 Kafka 消息幂等?是否使用了唯一 ID 或幂等键?
- 生产端启用幂等生产者
enable.idempotence=true
。 - 消费端基于订单唯一 ID 做幂等写库,防止重复插入。
- 使用事务(事务性 Producer 和 Consumer)保证消息处理的原子性。
问题 第四行
实现 Redis ZSet 异步排行榜同步机制,使用 Kafka + Goroutine 将高并发更新操作异步解耦,排行榜更新时间控制在 100ms 以内。
排行榜的业务目的是什么?展示什么指标?
- 排行榜展示秒杀成功用户的抢购速度、购买数量等指标。
- 支持实时展示热门用户、热门商品及抢购热度。
- 作为用户激励和产品营销的核心功能。
为什么使用 Redis ZSet?ZSet 是怎么构造 key 和 score 的?
- Redis ZSet(有序集合)支持根据分值排序,快速获取排名区间。
- Key 设计为
seckill:leaderboard:<product_id>
,方便按商品区分。 - Score 设计为抢购时间戳、抢购数量或综合得分,保证有序性。
异步同步具体是怎么做的?Kafka 消息如何转成 ZSet 操作?
- 秒杀服务将抢购成功事件写入 Kafka 排行榜 Topic。
- 排行榜服务消费消息,解析订单信息,计算排名分数。
- 通过 Redis
ZADD
命令异步更新 ZSet。 - 使用 Goroutine 并发消费和批量写入,提升吞吐。
Kafka 消费慢了怎么办?是否有批量处理优化?
- 通过批量消费消息合并 Redis 命令,减少网络 IO。
- 增加消费者实例数量,提升消费能力。
- 监控消费延迟,报警及时扩容。
排行榜更新频率高怎么做并发控制?
- 采用本地缓存和节流机制,控制更新频率在 100ms 内。
- 合并同一用户的多次更新,减少重复写操作。
- 通过 Redis Pipeline 批量执行,提高写入效率。
为什么控制在 100ms 以内?这个数据怎么测出来的?
- 100ms 以内基本保证用户看到的是近实时排名,用户体验良好。
- 通过压测和线上监控,结合延迟指标和用户反馈确定。
Redis 高并发写入是否考虑分片或持久化压力?
- 使用 Redis 集群分片,避免单点瓶颈。
- 结合异步持久化和内存淘汰策略,控制内存和磁盘压力。
- 定期备份数据,防止数据丢失。
问题 第五行
部署 Prometheus 和 Grafana,监控库存命中率、限流 QPS、Kafka 消费延迟等关键指标,保障系统性能与稳定。
用的哪些指标做系统监控?Prometheus 是如何采集这些指标的?
- 监控指标包括:
- 秒杀请求总数、成功数、失败数
- Redis 库存命中率和缓存命中率
- 限流 QPS(请求被拒绝次数)
- Kafka 消费延迟、消费速率
- Etcd 锁的状态和续租成功率
- 服务实例的 CPU、内存、网络流量
- Prometheus 通过服务暴露的
/metrics
HTTP 端点抓取数据(基于 Go 的 Prometheus client 库)。
限流 QPS 如何暴露为指标?是通过 middleware 中间件采集的吗?
- 是的,在 Gin 或 HTTP 框架中添加限流中间件,拦截请求计数。
- 拦截到限流请求时,统计对应指标(如
rate_limit_rejected_total
)。 - Prometheus 定期拉取指标数据,Grafana 进行展示。
Kafka 消费延迟是怎么算的?是消费时间减生产时间吗?
- 是的,消费延迟 = 当前消费时间戳 - 消息生产时间戳。
- 消息中携带生产时间,消费时计算差值作为延迟指标。
库存命中率是如何定义和记录的?从 Redis 采集吗?
- 库存命中率 = Redis 缓存命中次数 / (缓存命中 + 缓存未命中次数)
- Redis 客户端统计命中与未命中次数,或者使用 Redis 内置的命令统计。
- 统计数据通过代码暴露给 Prometheus。
有设置告警吗?什么阈值下触发告警?
- 设置如下告警阈值(可调):
- 限流率超过 30% 持续 5 分钟,触发告警。
- Kafka 消费延迟超过 1 秒。
- Redis 库存命中率低于 80%。
- 服务 CPU 利用率超过 80%。
- 告警通过邮件、钉钉或 Slack 通知。
Grafana 展示的关键仪表盘有哪些?
- 秒杀请求与响应统计面板。
- 限流及失败率趋势图。
- Kafka 消费速率和延迟监控。
- Redis 缓存命中率与库存变化。
- Etcd 锁续租状态。
- 服务资源使用率(CPU、内存)。
是否有做分布式链路追踪(如 Jaeger)?对定位性能瓶颈是否有帮助?
- 有接入 Jaeger 做分布式链路追踪。
- 通过 Trace 跟踪请求从入口到数据库的全链路调用,定位慢调用、瓶颈。
- 能快速发现服务依赖、网络延迟、数据库慢查询等问题。
- 对排查性能问题和系统调优非常有帮助。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 The Coding Odyssey | Chronicles of a Software Developer!