秒杀系统

简历描述

高并发秒杀系统 - 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 异步落库,最终一致性。

通信流程简述:

  1. 用户发起秒杀请求,经网关限流(令牌桶)后到达 seckill-service
  2. seckill-service 检查库存缓存 → 加 Etcd 锁 → 原子扣减 Redis 库存;
  3. 请求成功后写入 Kafka(下单 Topic);
  4. order-consumer 消费消息并写入 MySQL;
  5. 同时 Kafka 另一路消息用于排行榜异步更新(ZSet);
  6. 所有模块接入 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 机制 避免惊群:

  1. 客户端抢锁失败后,不会轮询查询,而是通过 Watch 监听锁的释放事件。
  2. 锁释放时,etcd 只会通知一个等待的客户端(按 FIFO 或公平调度),其他客户端继续等待。

对比 Redis 的缺陷: Redis 的锁(如 Redlock)在释放时,所有客户端会立即重试抢锁,容易引发惊群效应。

Etcd Redis
一致性 CP 保证强一致 AP,有主从延迟
锁续约 有 Lease 机制 需手动设置过期
健康检查 原生绑定客户端 需额外处理心跳
自动恢复 Lease+WAL 可恢复 崩溃后锁状态丢失

6. 3 节点 K8s 部署是多台物理机还是单机模拟?调度策略怎么做的?

  • 多台物理机,每台机器运行一个 K8s 节点,使用 kubeadm 部署;
  • 调度策略:使用 K8s 的默认调度器,Pod 分布在 3 个节点上,保证高可用;
  • Pod 副本数:每个服务(如 seckill-serviceorder-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Etcd 锁续租示例
leaseResp, err := cli.Grant(ctx, leaseTTL) // 创建租约
if err != nil {
return err
}
_, err = cli.Put(ctx, lockKey, lockValue, clientv3.WithLease(leaseResp.ID)) // 设置锁
if err != nil {
return err
}
// 启动续租协程
go func() {
ticker := time.NewTicker(renewInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_, err := cli.KeepAliveOnce(ctx, leaseResp.ID) // 续租
if err != nil {
log.Printf("续租失败: %v", err)
return
}
case <-ctx.Done():
return
}
}
}()

什么是 WAL(Write-Ahead Log)?你怎么实现落盘和恢复的?

  • WAL(Write-Ahead Log):写前日志,是一种持久化机制,确保数据在内存操作前先写入日志,保证数据一致性和可靠性;
  • 在 Etcd 中,WAL 用于记录所有写操作(如锁的获取和释放),即使服务崩溃也能恢复锁状态;
  • 实现步骤
    1. 在获取锁时,将锁状态(如 UUID、LeaseID)写入 WAL;
    2. 使用 Etcd 的 Put 操作将锁状态存储在 Etcd 中;
    3. 服务重启时,读取 WAL 日志,恢复锁状态;
    4. 如果租约未过期,继续使用原有锁状态,否则重新获取锁。
    5. WAL 日志通常存储在 Etcd 的数据目录中,Etcd 会自动管理日志的回收和清理。
    6. 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 跟踪请求从入口到数据库的全链路调用,定位慢调用、瓶颈。
  • 能快速发现服务依赖、网络延迟、数据库慢查询等问题。
  • 对排查性能问题和系统调优非常有帮助。