秒杀系统设计与实现:从限流到最终一致性的完整方案

Jie

秒杀系统设计与实现:从限流到最终一致性的完整方案

本文基于 Go 语言实现,介绍一个高并发秒杀系统的核心设计思路,涵盖网关限流、用户频控、Redis + Lua 预扣库存、MQ 异步落库,以及 Redis 与 MySQL 之间数据一致性的权衡方案。


目录


一、整体架构概览

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
27
28
29
30
31
32
33
34
35
36
37
┌──────────────────────────────────────┐
│ 用户请求 │
└─────────────────┬────────────────────┘

┌──────────────────────────────────────┐
│ API Gateway (限流) │
│ 令牌桶 / 滑动窗口 限制 QPS │
└─────────────────┬────────────────────┘

┌──────────────────────────────────────┐
│ 业务层 (用户频控) │
│ 同一用户 N 秒内只允许一次请求 │
└─────────────────┬────────────────────┘

┌──────────────────────────────────────┐
│ Redis + Lua 预扣减库存 │
│ 基准库存 - 待扣流水 - 本次购买 > 0 │
└────────┬─────────────────┬───────────┘
│ │
预扣成功 库存不足
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 发送 MQ 消息 │ │ 返回"已售罄" │
└──────┬───────┘ └──────────────┘


┌──────────────────────────────────────┐
│ 消费者:WHERE 条件 UPDATE MySQL │
│ UPDATE stock SET cnt=cnt-N │
│ WHERE item_id=? AND cnt>=N │
└─────────────────┬────────────────────┘

┌──────────────────────────────────────┐
│ 成功后:Redis DECR 待扣流水 & 基准 │
│ 失败回滚:Redis DECR 待扣流水计数 │
└──────────────────────────────────────┘

核心思想:

  • Redis 承担 99% 的流量过滤,挡在 MySQL 前面
  • MySQLWHERE 条件 UPDATE 是超售的最后防线
  • 允许 Redis 与 MySQL 之间短暂的数据不一致,通过异步同步兜底
  • 不做加锁强一致——在秒杀场景下,CAP 中选择 AP,容忍短暂 C 不一致

二、前置网关限流

在请求到达业务层之前,API Gateway 做第一层流量整形。防止瞬时流量直接把下游冲垮。

方案:令牌桶限流

使用 Go 的 golang.org/x/time/rate 或自实现令牌桶。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package gateway

import (
"net/http"
"sync"
"golang.org/x/time/rate"
)

// BucketLimiter 基于令牌桶的限流器
type BucketLimiter struct {
mu sync.Mutex
limiters map[string]*rate.Limiter // key: 接口路径
}

func NewBucketLimiter() *BucketLimiter {
return &BucketLimiter{
limiters: make(map[string]*rate.Limiter),
}
}

// GetLimiter 获取或创建限流器
// r 为每秒放入令牌数,b 为桶容量(允许的突发流量)
func (bl *BucketLimiter) GetLimiter(path string, r, b int) *rate.Limiter {
bl.mu.Lock()
defer bl.mu.Unlock()

if lim, ok := bl.limiters[path]; ok {
return lim
}
lim := rate.NewLimiter(rate.Limit(r), b)
bl.limiters[path] = lim
return lim
}

// Middleware 限流中间件
func (bl *BucketLimiter) Middleware(rps, burst int) func(http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Limit(rps), burst)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, `{"code":429,"msg":"请求过于频繁,请稍后再试"}`,
http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}

思维导图

1
2
3
4
5
6
7
8
9
10
网关限流
├── 令牌桶 (Token Bucket)
│ ├── 匀速放令牌(比如 10000/s)
│ ├── 桶容量 = 突发容忍量
│ └── Allow() 取令牌 → 取不到直接 429
├── 滑动窗口
│ ├── 精确控制任意时间窗口内请求数
│ └── Redis ZSET 实现分布式滑动窗口
└── 漏桶 (Leaky Bucket)
└── 强制匀速出队(适合排队场景)

三、单用户购买限制

防止同一个用户用脚本疯狂刷请求。使用 Redis 记录每个用户最近一次请求时间,N 秒内只允许一次。

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
27
28
29
30
package service

import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)

const (
userFreqKeyPrefix = "seckill:user_freq:%d:%s" // seckill:user_freq:活动ID:用户ID
userFreqWindow = 5 * time.Second // 5s 内只能请求一次
)

// CheckUserFreq 检查并设置用户请求频率
// 返回 true 表示允许,false 表示被限制
func CheckUserFreq(ctx context.Context, rdb *redis.Client,
activityID int64, userID string) (bool, error) {

key := fmt.Sprintf(userFreqKeyPrefix, activityID, userID)

// SET NX + EXPIRE 原子操作
// 如果 key 不存在,设置并返回 true(允许)
// 如果 key 存在,返回 false(被限制)
ok, err := rdb.SetNX(ctx, key, 1, userFreqWindow).Result()
if err != nil {
return false, fmt.Errorf("redis setnx error: %w", err)
}
return ok, nil
}

流程图

1
2
3
4
5
6
7
8
9
请求到达


┌─────────────────────┐ 已存在(被限)
│ SETNX user_freq_key ├────────────────▶ 返回 "请勿重复提交"
└──────┬──────────────┘
│ 不存在(设置成功)

继续下一步 → Redis 预扣减库存

四、Redis + Lua 库存预扣减

这是秒杀系统最核心的环节。通过 Lua 脚本保证 Redis 操作的原子性,实现库存的预扣减。

数据结构

Redis Key 类型 说明
seckill:stock:{activityID} String (int) 基准库存,初始化时从 MySQL 同步
seckill:pending:{activityID} String (int) 待扣减订单流水计数(已预扣但还未落库确认)

预扣减逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可用库存 = 基准库存 - 待扣减订单流水数

预扣减条件: 可用库存 - 本次购买数量 >= 0
↓ 满足
预扣减成功 → 待扣减流水数 += 本次购买数量

发送 MQ 消息,异步落库 MySQL

┌───────────────────────┐
│ MySQL UPDATE 成功? │
├───────────────────────┤
│ 成功 → DECR 待扣流水 │
│ DECR 基准库存 │ ← 两条原子执行
├───────────────────────┤
│ 失败 → DECR 待扣流水 │ ← 回滚:只减流水,不动基准
└───────────────────────┘

Lua 脚本实现

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
-- seckill_deduct.lua
-- KEYS[1] = 基准库存 key seckill:stock:{activityID}
-- KEYS[2] = 待扣流水 key seckill:pending:{activityID}
-- ARGV[1] = 本次购买数量
-- ARGV[2] = 订单 ID (用于幂等,可选)

local stockKey = KEYS[1]
local pendingKey = KEYS[2]
local quantity = tonumber(ARGV[1])

-- 获取基准库存和待扣流水
local stock = tonumber(redis.call('GET', stockKey)) or 0
local pending = tonumber(redis.call('GET', pendingKey)) or 0

-- 可用库存 = 基准库存 - 待扣减流水数
local available = stock - pending

-- 判断是否够扣
if available >= quantity then
-- 预扣成功:增加待扣流水计数
redis.call('INCRBY', pendingKey, quantity)
return 1 -- 预扣成功
else
return 0 -- 库存不足
end

Go 调用代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package service

import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)

const seckillDeductScript = `
local stockKey = KEYS[1]
local pendingKey = KEYS[2]
local quantity = tonumber(ARGV[1])

local stock = tonumber(redis.call('GET', stockKey)) or 0
local pending = tonumber(redis.call('GET', pendingKey)) or 0
local available = stock - pending

if available >= quantity then
redis.call('INCRBY', pendingKey, quantity)
return 1
else
return 0
end
`

const (
stockKeyFmt = "seckill:stock:%d"
pendingKeyFmt = "seckill:pending:%d"
)

func DeductStockLua(ctx context.Context, rdb *redis.Client,
activityID int64, quantity int) (bool, error) {

stockKey := fmt.Sprintf(stockKeyFmt, activityID)
pendingKey := fmt.Sprintf(pendingKeyFmt, activityID)

result, err := rdb.Eval(ctx, seckillDeductScript,
[]string{stockKey, pendingKey}, quantity).Int()
if err != nil {
return false, fmt.Errorf("lua eval error: %w", err)
}
return result == 1, nil
}

确认与回滚 Lua 脚本

1
2
3
4
5
6
7
8
9
-- seckill_confirm.lua
-- 落库成功时调用:DECR 待扣流水 + DECR 基准库存
-- KEYS[1] = stockKey
-- KEYS[2] = pendingKey
-- ARGV[1] = quantity

redis.call('DECRBY', KEYS[1], tonumber(ARGV[1])) -- 基准库存 --
redis.call('DECRBY', KEYS[2], tonumber(ARGV[1])) -- 待扣流水 --
return 1
1
2
3
4
5
6
7
8
-- seckill_rollback.lua
-- 落库失败时调用:只 DECR 待扣流水,不恢复基准库存
-- (因为基准库存实际上并没有被扣掉,MySQL 那边也一样)
-- KEYS[1] = pendingKey
-- ARGV[1] = quantity

redis.call('DECRBY', KEYS[1], tonumber(ARGV[1])) -- 仅减待扣流水
return 1

为什么失败回滚只减 pending 而不恢复 stock?

因为 stock(基准库存)代表的是 MySQL 数据库层面的真实初始库存。预扣时只动了 pending,stock 没有变。
失败时回滚 pending 即可,stock 保持不变,后续请求看到的可用 = stock - pending 自然就恢复了。

完整预扣流程思维导图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Redis + Lua 预扣减流程
├── 第 1 步:Lua 预扣减
│ ├── 读取 stock(基准库存)
│ ├── 读取 pending(待扣流水)
│ ├── available = stock - pending
│ ├── available >= quantity?
│ │ ├── YES → INCRBY pending → return 1(成功)
│ │ └── NO → return 0(库存不足)
├── 第 2 步:预扣成功后
│ ├── 生成订单 ID
│ ├── 组装订单数据
│ └── 发送到 MQ
├── 第 3 步:MQ 消费
│ ├── MySQL WHERE 条件 UPDATE
│ │ ├── affected_rows > 0 → 第 4 步 成功路径
│ │ └── affected_rows = 0 → 第 4 步 失败路径
├── 第 4 步-a:成功路径
│ ├── Lua: DECRBY stock N
│ ├── Lua: DECRBY pending N
│ └── 通知用户下单成功
└── 第 4 步-b:失败/超时路径
├── MySQL 实际未扣库存
├── Lua: DECRBY pending N(回滚预扣)
└── 通知用户下单失败 / 自动重试

五、MQ 异步落库 MySQL

预扣成功后不直接写 MySQL,而是丢到消息队列,由消费者异步处理。这样:

  • 业务层响应极快(只是写了一条 Redis + 发了一条 MQ)
  • 数据库压力可控(消费者可以控制消费速率)

MySQL 防超售的 WHERE 条件 UPDATE

1
2
3
4
5
-- 核心 SQL:一条带条件的 UPDATE 天然防止超售
-- 只有当库存 >= 扣减数量时,才会真正更新
UPDATE inventory
SET stock = stock - ?
WHERE item_id = ? AND stock >= ?;

affected_rows = 0 说明库存不够,这条 UPDATE 没有实际执行——这是 MySQL 层面的最后一道防线。

Go 消费者实现

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package consumer

import (
"context"
"database/sql"
"fmt"
"log"

"github.com/redis/go-redis/v9"
)

// MQ 消息体
type OrderMsg struct {
OrderID string `json:"order_id"`
ActivityID int64 `json:"activity_id"`
UserID string `json:"user_id"`
ItemID int64 `json:"item_id"`
Quantity int `json:"quantity"`
}

// ConfirmScript - 成功:DECR stock, DECR pending
const confirmScript = `
redis.call('DECRBY', KEYS[1], tonumber(ARGV[1]))
redis.call('DECRBY', KEYS[2], tonumber(ARGV[1]))
return 1
`

// RollbackScript - 失败:仅 DECR pending
const rollbackScript = `
redis.call('DECRBY', KEYS[1], tonumber(ARGV[1]))
return 1
`

func ConsumeOrder(ctx context.Context, db *sql.DB, rdb *redis.Client, msg OrderMsg) error {
stockKey := fmt.Sprintf("seckill:stock:%d", msg.ActivityID)
pendingKey := fmt.Sprintf("seckill:pending:%d", msg.ActivityID)

// 1. 带 WHERE 条件的 UPDATE——防超售最后一道防线
result, err := db.ExecContext(ctx,
`UPDATE inventory SET stock = stock - ? WHERE item_id = ? AND stock >= ?`,
msg.Quantity, msg.ItemID, msg.Quantity,
)
if err != nil {
return fmt.Errorf("mysql update error: %w", err)
}

affected, _ := result.RowsAffected()
if affected == 0 {
// 库存不足(理论上不应该走到这,但是防御性编程)
// 回滚 Redis 预扣:只减 pending
if err := rdb.Eval(ctx, rollbackScript,
[]string{pendingKey}, msg.Quantity).Err(); err != nil {
log.Printf("[ERROR] redis rollback error: order=%s, err=%v", msg.OrderID, err)
}

// 这里可以标记订单失败,或发 MQ 延迟重试
log.Printf("[WARN] 库存扣减失败,已回滚预扣: order=%s, item=%d, qty=%d",
msg.OrderID, msg.ItemID, msg.Quantity)
return nil // 消费确认,不重试(若需重试则返回 error)
}

// 2. MySQL 扣减成功 → 确认 Redis
if err := rdb.Eval(ctx, confirmScript,
[]string{stockKey, pendingKey}, msg.Quantity).Err(); err != nil {
log.Printf("[ERROR] redis confirm error: order=%s, err=%v", msg.OrderID, err)
}

// 3. 更新订单状态为"已支付/已完成"
if _, err := db.ExecContext(ctx,
`UPDATE orders SET status = 'PAID' WHERE order_id = ?`,
msg.OrderID); err != nil {
log.Printf("[ERROR] update order status error: order=%s, err=%v", msg.OrderID, err)
}

return nil
}

为什么用 MQ 而不是直接写 MySQL?

1
2
3
4
5
6
7
8
9
同步方案:
请求 → Redis预扣 → 写MySQL → 返回

这里会成为瓶颈,每个请求等几十ms

异步方案:
请求 → Redis预扣 → 发MQ(1ms) → 返回

MQ消费者 → 批量写MySQL → 用户收通知

六、数据一致性方案

核心观点:秒杀场景下,引入 Redis 本身就意味着会出现短暂数据不一致。只要 MySQL 的 WHERE 条件 UPDATE 能保证不超售,短暂的不一致是可以接受的。

追求绝对的实时一致 = 放弃 Redis,回到纯 MySQL 。所以我采取的策略是 “最终一致性 + 多种兜底方案组合”

方案 A:Binlog 订阅同步(推荐,毫秒级误差)

通过 Canal / Maxwell / go-mysql 等工具订阅 MySQL binlog,实时同步到 Redis。

1
2
3
4
5
MySQL → binlog → Canal → 解析变更 → 更新 Redis

UPDATE inventory SET stock=95 WHERE item_id=1

SET seckill:stock:1 95

Go 伪代码实现(基于 go-mysql)

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package sync

import (
"context"
"fmt"
"github.com/go-mysql-org/go-mysql/canal"
"github.com/go-mysql-org/go-mysql/schema"
"github.com/redis/go-redis/v9"
)

type BinlogSyncer struct {
canal *canal.Canal
rdb *redis.Client
}

func (s *BinlogSyncer) Start(ctx context.Context) error {
s.canal.SetEventHandler(&eventHandler{rdb: s.rdb})
return s.canal.RunFrom(0) // 从当前 binlog 位置开始
}

type eventHandler struct {
canal.DummyEventHandler // 只覆写需要的方法
rdb *redis.Client
}

func (h *eventHandler) OnRow(e *canal.RowsEvent) error {
// 只处理 inventory 表
if e.Table.Name != "inventory" {
return nil
}

// UPDATE 操作:e.Action == canal.UpdateAction
if e.Action == canal.UpdateAction {
for i := 0; i < len(e.Rows); i += 2 {
// e.Rows[i] = 旧值
// e.Rows[i+1] = 新值
before := e.Rows[i] // []interface{}
after := e.Rows[i+1]

itemID := after[0] // 假设第 0 列是 item_id
stock := after[1] // 假设第 1 列是 stock

// 同步到 Redis
key := fmt.Sprintf("seckill:stock:%v", itemID)
h.rdb.Set(context.Background(), key, stock, 0)
}
}
return nil
}

优点: 正常情况毫秒级延迟,几乎实时
缺点: 依赖 MySQL binlog 基础设施,运维复杂度稍高

方案 B:后台强一致同步(慎用,加锁性能差)

当发现大量不一致,不一致超过阀值,可考虑使用此方法强同步一次。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package sync

import (
"context"
"database/sql"
"fmt"
"log"

"github.com/redis/go-redis/v9"
)

// ForceSync 强一致同步:MySQL SELECT FOR UPDATE → Redis SET
// 会锁住 MySQL 行,影响上游 UPDATE 性能,仅救急使用
func ForceSync(ctx context.Context, db *sql.DB, rdb *redis.Client,
itemIDs []int64) error {

tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx error: %w", err)
}
defer tx.Rollback() // 结束时没 commit 就回滚

for _, itemID := range itemIDs {
// SELECT ... FOR UPDATE — 行级锁,阻塞其他 UPDATE
var stock int
err := tx.QueryRowContext(ctx,
`SELECT stock FROM inventory WHERE item_id = ? FOR UPDATE`,
itemID).Scan(&stock)
if err != nil {
return fmt.Errorf("query for update error: %w", err)
}

// 同步 Redis
key := fmt.Sprintf("seckill:stock:%d", itemID)
if err := rdb.Set(ctx, key, stock, 0).Err(); err != nil {
return fmt.Errorf("redis set error: key=%s, err=%w", key, err)
}
}

return tx.Commit() // 提交释放锁
}

优点: 绝对一致,同步完的那一刻 Redis == MySQL
缺点: FOR UPDATE 锁住整行,秒杀中的 UPDATE 语句会被阻塞 导致 吞吐下降
使用场景: 发现大量不一致需要紧急修复时手动触发,非常用方案,紧急修复时用

方案 C:定时对比同步(有误差)

后台定时任务,每隔一段时间(比如 30s)扫描一次,发现不一致时以 MySQL 为准修正 Redis。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package sync

import (
"context"
"database/sql"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)

// ScheduledSync 定时对比同步
// 周期性地对比 MySQL 和 Redis,以 MySQL 为准
func ScheduledSync(ctx context.Context, db *sql.DB, rdb *redis.Client,
itemIDs []int64, interval time.Duration) {

ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
for _, itemID := range itemIDs {
syncItem(ctx, db, rdb, itemID)
}
case <-ctx.Done():
return
}
}
}

func syncItem(ctx context.Context, db *sql.DB, rdb *redis.Client, itemID int64) {
// 1. 查 MySQL
var mysqlStock int
if err := db.QueryRowContext(ctx,
`SELECT stock FROM inventory WHERE item_id = ?`, itemID,
).Scan(&mysqlStock); err != nil {
log.Printf("[ERROR] syncItem query error: item=%d, err=%v", itemID, err)
return
}

// 2. 查 Redis
key := fmt.Sprintf("seckill:stock:%d", itemID)
redisStock, err := rdb.Get(ctx, key).Int()
if err != nil && err != redis.Nil {
log.Printf("[ERROR] syncItem redis get error: key=%s, err=%v", key, err)
return
}

// 3. 对比
if mysqlStock != redisStock {
// 以 MySQL 为准
if err := rdb.Set(ctx, key, mysqlStock, 0).Err(); err != nil {
log.Printf("[ERROR] syncItem redis set stock error: key=%s, err=%v", key, err)
return
}
// 同时重置 pending(因为基准库存变了,pending 应该归零)
// 注意:正在消费中的 MQ 消息可能导致短暂反复,这是可接受的
pendingKey := fmt.Sprintf("seckill:pending:%d", itemID)
if err := rdb.Set(ctx, pendingKey, 0, 0).Err(); err != nil {
log.Printf("[ERROR] syncItem redis set pending error: key=%s, err=%v", pendingKey, err)
}
}
}

优点: 实现简单,资源消耗低
缺点: 存在一个周期内的误差(比如 30s),用户可能看到”明明显示有货但买不到”
适用: 作为兜底 + 辅助方案

三方案组合策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌────────────────────────────┐
│ Binlog 实时订阅(主力) │
│ 毫秒级正常同步 │
└────────────┬───────────────┘
│ 异常/延迟

┌────────────────────────────┐
│ 定时对比同步(兜底) │
│ 30s 周期,修正小量误差 │
└────────────┬───────────────┘
│ 大量不一致

┌────────────────────────────┐
│ 强一致同步(救急) │
│ FOR UPDATE,手动触发 │
└────────────────────────────┘

为什么不追求绝对实时一致?

1
2
秒杀场景下,短暂的不一致换来的性能提升是值得的。MySQL 的 WHERE 条件 UPDATE 保证了不超售这个底线。


七、总结与权衡

架构职责分布

组件 职责 容忍度
API Gateway 限流,拒绝超过系统承载能力的请求 宁可误杀,不能漏过
用户频控 防止同一用户刷请求 宽松但有
Redis + Lua 预扣减库存,99% 请求在此被拦截(已售罄的) 允许短暂不准
MQ 削峰填谷,异步化 允许堆积(可扩容消费者)
MySQL 真实库存落库,防止超售的最后防线 必须准确
Binlog 同步 毫秒级异步矫正 Redis 主力方案
定时对比 兜底矫正 辅助方案
强一致同步 紧急修复 慎用

关键要点

  1. MySQL WHERE stock >= N UPDATE 是不超售的底线。 只要这条 SQL 跑了,就不会多卖。

  2. Redis 只是”加速层”,不是”真实层”。 它的数据可以滞后,可以偏离,只要最终被拉回到和 MySQL 一致就行。

  3. 预扣减的回滚逻辑: 成功确认 → 减 stock + 减 pending;失败回滚 → 只减 pending。

  4. 三层同步兜底: Binlog 毫秒级 → 定时任务秒级 → 强一致手动触发。


完整代码示例可参考:[GitHub 仓库链接(TODO)]
如有疑问或建议,欢迎在评论区交流讨论。

  • 标题: 秒杀系统设计与实现:从限流到最终一致性的完整方案
  • 作者: Jie
  • 创建于 : 2026-06-02 00:00:00
  • 更新于 : 2026-06-03 13:00:53
  • 链接: https://your-domain.com/flash-sale-system/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论