编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

    • README
    • 架构与组件

    • 数据与存储

    • 通信与协议

    • 稳定性与安全

      • 幂等性设计方案
        • 01.一次扣两次款
          • 1.1 重复扣款事故
          • 1.2 故障扩散链路
          • 1.3 反思幂等设计
        • 02.要解决的核心矛盾
          • 2.1 重复请求的来源
          • 2.2 一致性与性能
          • 2.3 判重与时效
          • 2.4 幂等的本质
        • 03.业界主流方案
          • 03.1 主流方案概览
          • 03.2 横向对比矩阵
          • 03.3 适用场景速查
        • 04.设计核心原则
          • 04.1 唯一标识原则
          • 04.2 先判后做原则
          • 04.3 状态机原则
          • 04.4 时效边界原则
        • 05.方案落地实战
          • 05.1 整体架构
          • 05.2 Token 防重方案
          • 05.3 唯一索引方案
          • 05.4 状态机方案
          • 05.5 分布式锁方案
        • 06.关键问题解决
          • 06.1 幂等键设计
          • 06.2 结果回放问题
          • 06.3 跨服务幂等
        • 07.常见陷阱与反例
          • 07.1 先做后判反例
          • 07.2 时间戳幂等反例
          • 07.3 部分幂等反例
        • 08.演进路线
          • 08.1 V1 业务唯一约束
          • 08.2 V2 通用幂等中间件
          • 08.3 V3 全链路幂等
        • 09.总结与决策
          • 09.1 上线检查表
          • 09.2 选型决策树
      • 分布式锁方案设计
      • 监控告警方案设计
      • 终端设备鉴权设计
      • 移动端防抓包实践
    • 端侧专项性

    • 研发的效能

  • 专栏
  • 方案设计思想
  • 稳定性与安全
杨充
2026-05-21
目录

幂等性设计方案

# 17.幂等性设计方案

本篇定位:幂等性是分布式系统的"防重保险丝"——网络抖动、客户端重试、消息重投都可能让同一个操作执行多次。本文从一次"扣两次款"的支付事故讲起,回答三个核心问题——为什么必须做幂等?业界主流方案怎么选?怎么落地一个生产级幂等体系?

# 目录介绍

  • 01.一次扣两次款
    • 1.1 重复扣款事故
    • 1.2 故障扩散链路
    • 1.3 反思幂等设计
  • 02.要解决的核心矛盾
    • 2.1 重复请求的来源
    • 2.2 一致性与性能
    • 2.3 判重与时效
    • 2.4 幂等的本质
  • 03.业界主流方案
    • 03.1 主流方案概览
    • 03.2 横向对比矩阵
    • 03.3 适用场景速查
  • 04.设计核心原则
    • 04.1 唯一标识原则
    • 04.2 先判后做原则
    • 04.3 状态机原则
    • 04.4 时效边界原则
  • 05.方案落地实战
    • 05.1 整体架构
    • 05.2 Token 防重方案
    • 05.3 唯一索引方案
    • 05.4 状态机方案
    • 05.5 分布式锁方案
  • 06.关键问题解决
    • 06.1 幂等键设计
    • 06.2 结果回放问题
    • 06.3 跨服务幂等
  • 07.常见陷阱与反例
    • 07.1 先做后判反例
    • 07.2 时间戳幂等反例
    • 07.3 部分幂等反例
  • 08.演进路线
    • 08.1 V1 业务唯一约束
    • 08.2 V2 通用幂等中间件
    • 08.3 V3 全链路幂等
  • 09.总结与决策
    • 09.1 上线检查表
    • 09.2 选型决策树

# 01.一次扣两次款

# 1.1 重复扣款事故

某电商支付系统某天晚上 22:00 大促,有 3700 个用户被重复扣款——一笔订单 199 元的,账户被扣了 398 元。

sequenceDiagram
    participant U as 用户
    participant App as App
    participant Pay as 支付服务
    participant Bank as 银行
    
    U->>App: 点支付按钮
    App->>Pay: 发起支付 ¥199
    Pay->>Bank: 扣款请求
    Bank-->>Pay: 扣款成功(响应慢 5 秒)
    Note over App: 用户等不及<br/>以为没成功<br/>再点一次
    
    U->>App: 再点支付按钮
    App->>Pay: 又发起支付 ¥199
    Pay->>Bank: 又一次扣款
    Bank-->>Pay: 扣款成功
    
    Note over U: 实际扣了 ¥398
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

故障代价:

  • 客服 3 天处理 3700+ 投诉
  • 退款 + 补偿 ¥73 万
  • App 评分掉到 2.1 星
  • 监管约谈

# 1.2 故障扩散链路

flowchart TD
    A[网络抖动 / 响应慢] --> B[用户重复点击]
    B --> C[多次请求到达支付服务]
    
    C --> D{有幂等保护?}
    D -->|否 ❌| E[每次请求都扣款]
    D -->|是 ✅| F[第二次直接返回首次结果]
    
    E --> G[资金损失]
    
    Cause[根因] --> R1[支付接口没幂等键]
    Cause --> R2[Bank 接口快但响应慢]
    Cause --> R3[App 没做按钮防抖]
    Cause --> R4[支付状态机不严]
    
    style E fill:#ffebee
    style G fill:#ffebee
    style F fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 1.3 反思幂等设计

事后这个团队总结了三个最深刻的教训:

  1. 重试是分布式系统的常态——网络抖动、超时重发、用户重点击都会触发
  2. 幂等不是"可选优化",而是"必备保险"——尤其涉及钱、库存、消息
  3. 幂等要靠"业务唯一标识 + 服务端去重"——不能依赖客户端不重发

这次事故揭示一个本质:分布式系统不存在"恰好一次"的网络通信——只有"至少一次 + 服务端幂等 = 恰好一次"。

# 02.要解决的核心矛盾

# 2.1 重复请求的来源

mindmap
  root((重复请求来源))
    用户层
      多次点击按钮
      浏览器后退/刷新
      表单重复提交
    客户端层
      超时重试
      断网重连补发
      App 后台进程重发
    网络层
      代理重试
      LB 超时重试
      MQ 至少一次
    服务层
      RPC 失败重试
      定时任务重跑
      补偿任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键认知:只要有重试就一定会有重复请求——而分布式系统离不开重试。

# 2.2 一致性与性能

graph LR
    A[强一致幂等<br/>每次都查 DB 加锁] --> B[性能差]
    B --> C[QPS 上不去]
    
    A2[最终一致幂等<br/>缓存层去重] --> B2[性能高]
    B2 --> C2[但有窗口期重复风险]
    
    A3[分层幂等<br/>缓存 + DB 唯一约束] --> B3[兼顾]
    
    style C fill:#fff3e0
    style C2 fill:#fff3e0
    style B3 fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12

# 2.3 判重与时效

幂等记录不能永久保留(成本爆炸),但保留太短又会失效:

业务 保留时长 理由
支付下单 24 小时 重试一般几分钟内
消息消费 7 天 MQ 最长重投周期
接口调用 1 小时 短期重试为主
定时任务 30 天 跨月对账

# 2.4 幂等的本质

幂等 = 同一操作执行 N 次和执行 1 次的结果完全相同

数学上:f(f(x)) = f(x)。

工程上:操作前先判断"这件事我做过没",做过就直接返回首次结果。

# 03.业界主流方案

# 03.1 主流方案概览

方案 核心思路 典型场景
唯一索引 DB 唯一约束阻止重复 创建订单、注册用户
Token 防重 调用前先取 Token,调用时消费 表单提交、支付
状态机 仅在特定状态下才执行 订单状态流转
乐观锁 version 字段控制 库存扣减、计数器
悲观锁/分布式锁 串行化操作 高竞争场景
幂等表 单独存幂等键和结果 通用接口幂等

# 03.2 横向对比矩阵

维度 唯一索引 Token 状态机 乐观锁 分布式锁 幂等表
实现复杂度 ⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐ ⭐⭐
性能 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
可靠性 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
是否返回首次结果 ❌ ✅ ❌ ❌ ❌ ✅
跨服务可用 ❌ ✅ ❌ ❌ ✅ ✅

# 03.3 适用场景速查

flowchart TD
    Start([需要幂等]) --> Q1{操作类型?}
    
    Q1 -->|创建类<br/>下单/注册| UI[唯一索引<br/>+ 业务 ID]
    Q1 -->|更新类<br/>状态流转| SM[状态机<br/>+ 乐观锁]
    Q1 -->|查询类| Skip[天然幂等<br/>无需处理]
    Q1 -->|通用接口<br/>需返回原结果| IT[幂等表<br/>+ Token]
    Q1 -->|高竞争资源<br/>抢购/秒杀| DL[分布式锁]
    
    style UI fill:#e8f5e8
    style SM fill:#fff3e0
    style IT fill:#e3f2fd
    style DL fill:#ffebee
1
2
3
4
5
6
7
8
9
10
11
12
13

# 04.设计核心原则

# 04.1 唯一标识原则

铁律:必须有一个业务级唯一 ID 来标识这次操作。

graph LR
    A[业务唯一标识]
    A --> B1[订单号 orderId]
    A --> B2[请求号 requestId]
    A --> B3[业务键 业务 ID + 操作类型]
    A --> B4[Token clientToken]
    
    Bad[❌ 不能用]
    Bad --> N1[时间戳 同一毫秒并发会撞]
    Bad --> N2[随机数 不同请求可能撞]
    Bad --> N3[自增 ID 服务端生成无法去重]
    
    style A fill:#e8f5e8
    style Bad fill:#ffebee
1
2
3
4
5
6
7
8
9
10
11
12
13
14

幂等键的好特征:

  • 客户端生成(如 UUID)—— 服务端才能识别"这是同一个请求"
  • 业务相关(如 userId+orderId+amount)—— 防止业务上的重复
  • 可读性—— 排查问题时一眼看出是哪笔

# 04.2 先判后做原则

永远先判重,再执行业务:

// ✅ 正确:先判重
fun pay(req: PayRequest): PayResult {
    val key = "pay:${req.requestId}"
    val cached = cache.get(key)
    if (cached != null) return cached  // 重复请求,返回首次结果
    
    val result = doPayment(req)  // 真正业务
    cache.set(key, result, ttl = 24.hours)
    return result
}

// ❌ 错误:先做后判(资金已经动了)
fun pay(req: PayRequest): PayResult {
    val result = doPayment(req)  // 已经扣款
    val key = "pay:${req.requestId}"
    if (cache.exists(key)) {
        rollback(result)  // 回滚已经晚了
        return cache.get(key)
    }
    cache.set(key, result)
    return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 04.3 状态机原则

业务状态只能按规定路径流转:

stateDiagram-v2
    [*] --> 待支付: 创建订单
    待支付 --> 已支付: 支付成功
    待支付 --> 已取消: 取消订单
    已支付 --> 已发货: 发货
    已发货 --> 已完成: 收货
    已支付 --> 退款中: 退款申请
    退款中 --> 已退款: 退款成功
    
    note right of 已支付: 重复"支付成功"<br/>会被状态机拒绝
1
2
3
4
5
6
7
8
9
10

状态机天然幂等:因为只有"待支付 → 已支付"是允许的,第二次想再"已支付 → 已支付"会被拒绝。

-- 用 SQL 实现状态机
UPDATE orders 
SET status = 'PAID', pay_time = NOW()
WHERE order_id = ? 
  AND status = 'WAITING_PAY';  -- 只在特定状态下才执行

-- 影响行数 = 0 → 已经执行过了
-- 影响行数 = 1 → 首次执行成功
1
2
3
4
5
6
7
8

# 04.4 时效边界原则

幂等不是永久的,必须有清晰的时效边界:

业务 时效 过期后怎么办
支付下单 24 小时 同样的 requestId 算新订单
消息消费 7 天 跟 MQ 最长重投周期对齐
Token 防重 5-30 分钟 表单页面停留时间

# 05.方案落地实战

# 05.1 整体架构

graph TB
    Client[客户端] -->|requestId| Gateway[API 网关]
    Gateway -->|提取 requestId| IdmFilter[幂等过滤器]
    
    IdmFilter --> Cache{Redis 查 requestId?}
    Cache -->|命中| ReturnCached[返回首次结果]
    Cache -->|未命中| Lock[获取分布式锁]
    
    Lock --> Biz[业务处理]
    Biz --> DB[(DB 唯一约束兜底)]
    Biz --> SetCache[写 Redis 缓存结果]
    SetCache --> ReleaseLock[释放锁]
    ReleaseLock --> Resp[返回结果]
    
    style IdmFilter fill:#fff3e0
    style Cache fill:#e8f5e8
    style DB fill:#f3e5f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 05.2 Token 防重方案

适用场景:表单提交、支付按钮等"用户主动触发"的操作。

sequenceDiagram
    participant U as 用户
    participant App as App
    participant Server as 服务端
    participant Redis as Redis
    
    Note over U,Redis: 第一步:进入页面时取 Token
    U->>App: 进入下单页
    App->>Server: GET /token
    Server->>Redis: SET token:abc123 → unused, ttl=30min
    Server-->>App: token=abc123
    
    Note over U,Redis: 第二步:提交时带上 Token
    U->>App: 点击提交
    App->>Server: POST /order, token=abc123
    Server->>Redis: GETDEL token:abc123
    
    alt Token 存在
        Redis-->>Server: unused
        Server->>Server: 处理业务
        Server-->>App: 成功
    else Token 不存在或已用
        Redis-->>Server: nil
        Server-->>App: 重复提交
    end
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

核心逻辑:GETDEL 是原子操作——同一 Token 只能成功"消费"一次,第二次必然失败。

# 05.3 唯一索引方案

适用场景:创建类操作(订单、用户、支付单)。

-- 业务唯一键作为唯一索引
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    request_id VARCHAR(64) NOT NULL,
    amount DECIMAL(10,2),
    status VARCHAR(20),
    UNIQUE KEY uk_request_id (request_id)
);

-- 插入:重复 requestId 会抛 DuplicateKey 异常
INSERT INTO orders(user_id, request_id, amount, status)
VALUES (?, ?, ?, 'WAITING_PAY');
1
2
3
4
5
6
7
8
9
10
11
12
13

捕获异常的两种处理:

fun createOrder(req: OrderRequest): Order {
    return try {
        orderDao.insert(req.toEntity())
    } catch (e: DuplicateKeyException) {
        // 重复请求,查首次记录返回
        orderDao.findByRequestId(req.requestId) 
            ?: throw IllegalStateException("数据不一致")
    }
}
1
2
3
4
5
6
7
8
9

为什么唯一索引最可靠:DB 是数据的最后一道防线,缓存可以丢、应用可以重启,但 DB 唯一约束永远生效。

# 05.4 状态机方案

适用场景:状态流转类(订单支付、退款、发货)。

@Transactional
fun payOrder(orderId: String): PayResult {
    val rows = orderDao.updateStatus(
        orderId = orderId,
        fromStatus = "WAITING_PAY",
        toStatus = "PAID"
    )
    
    return when (rows) {
        1 -> {
            // 首次执行成功
            triggerPaidEvents(orderId)
            PayResult.SUCCESS
        }
        0 -> {
            // 状态不对,可能已支付或已取消
            val current = orderDao.findById(orderId)
            when (current.status) {
                "PAID" -> PayResult.ALREADY_PAID  // 幂等返回
                "CANCELLED" -> PayResult.CANCELLED
                else -> PayResult.STATUS_ERROR
            }
        }
        else -> error("unexpected rows: $rows")
    }
}
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

关键:UPDATE 影响行数判断——这是最优雅的状态机幂等。

# 05.5 分布式锁方案

适用场景:高并发竞争资源(秒杀、抢购、防超卖)。

sequenceDiagram
    participant R1 as 请求 1
    participant R2 as 请求 2
    participant Lock as Redis 锁
    participant DB as DB
    
    par 同时到达
        R1->>Lock: SETNX key 1, ex=10
        Lock-->>R1: OK 拿到锁
    and
        R2->>Lock: SETNX key 1, ex=10
        Lock-->>R2: FAIL
        R2->>R2: 直接拒绝或返回缓存
    end
    
    R1->>DB: 执行业务
    R1->>Lock: DEL key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意:分布式锁性能差(每个请求多一次 Redis),只在真正高竞争场景用。

详见下一篇 18.分布式锁方案。

# 06.关键问题解决

# 06.1 幂等键设计

好的幂等键设计示例:

业务场景 幂等键构造
用户下单 userId + orderTraceId + sku
支付 payOrderId + paymentChannel
退款 refundId + originalOrderId
消息消费 consumerGroup + topic + msgId
资金转账 srcAccount + dstAccount + serialNo

避免的设计:

  • ❌ userId + timestamp —— 同毫秒并发撞键
  • ❌ 随机字符串 —— 重发时无法识别
  • ❌ 自增 ID —— 客户端无法生成

# 06.2 结果回放问题

进阶问题:重复请求时,要不要返回完全一致的结果?

flowchart TD
    Q[重复请求来了] --> Q1{要返回首次结果吗?}
    
    Q1 -->|是 严格幂等| Plan1[幂等表存请求 + 响应]
    Q1 -->|否 业务幂等就行| Plan2[只校验"已处理"<br/>返回当前状态]
    
    Plan1 --> R1[需要存储响应内容]
    Plan1 --> R2[空间成本]
    
    Plan2 --> R3[实现简单]
    Plan2 --> R4[但客户端要能识别"已处理"]
    
    style Plan1 fill:#fff3e0
    style Plan2 fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14

幂等表设计:

CREATE TABLE idempotent_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_type VARCHAR(50),           -- 业务类型
    idempotent_key VARCHAR(128),    -- 幂等键
    request_body TEXT,              -- 原始请求(可选)
    response_body TEXT,             -- 首次响应
    status VARCHAR(20),             -- PROCESSING / SUCCESS / FAIL
    created_at DATETIME,
    expired_at DATETIME,
    UNIQUE KEY uk_biz_key (biz_type, idempotent_key)
);
1
2
3
4
5
6
7
8
9
10
11

# 06.3 跨服务幂等

链路:A → B → C,三个服务都要幂等。

graph LR
    Client -->|requestId=abc| A[服务 A]
    A -->|透传 requestId=abc<br/>+生成 subId=a1| B[服务 B]
    B -->|透传 requestId=abc<br/>+生成 subId=b1| C[服务 C]
    
    A & B & C --> Note[每个服务都用 requestId 做自己的幂等]
    
    style A fill:#e3f2fd
    style B fill:#e8f5e8
    style C fill:#fff3e0
1
2
3
4
5
6
7
8
9
10

核心:幂等键沿调用链透传——每个服务都用同一个 requestId 来识别"这是不是同一次外部请求"。

# 07.常见陷阱与反例

# 07.1 先做后判反例

反例:先扣款再判重,重复时回滚。

问题:

  • 重复扣款的瞬间已经发生了资金变化
  • 回滚可能失败(网络抖动)
  • 多次调用银行接口浪费资源

正确:永远先判后做。

# 07.2 时间戳幂等反例

反例:用时间戳做幂等键。

// ❌ 错误
val key = "${userId}:${System.currentTimeMillis()}"
1
2

问题:

  • 同毫秒并发撞键
  • 跨机器时钟不同步
  • 重试时时间戳变了,幂等失效

正确:用客户端生成的 UUID 或业务唯一 ID。

# 07.3 部分幂等反例

反例:接口"主流程"做了幂等,但"副作用"(发短信、写日志、扣积分)没做。

问题:

  • 用户收到 2 条相同短信
  • 积分被扣 2 次
  • 业务上看起来"重复"

正确:所有副作用都要纳入幂等保护——要么全部跳过(已处理),要么打包在事务里。

mindmap
  root((三大反例))
    先做后判
      资金已动
      回滚有风险
      银行调用浪费
    时间戳幂等
      并发撞键
      时钟不同步
      重试失效
    部分幂等
      副作用遗漏
      短信重发
      积分重扣
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 08.演进路线

# 08.1 V1 业务唯一约束

特征:业务起步、单体应用。

做法:

  • 在关键表上加唯一索引
  • 业务代码捕获 DuplicateKey
  • 状态机控制流转

适用阶段:单体 / 小型微服务

# 08.2 V2 通用幂等中间件

特征:微服务规模化、需要标准化。

做法:

  • 抽象幂等切面(注解 + AOP)
  • 统一幂等表 / Redis 幂等键
  • Token 防重组件
  • 失败补偿机制
@Idempotent(key = "#req.requestId", expire = 24, unit = HOURS)
fun pay(req: PayRequest): PayResult { ... }
1
2

适用阶段:中大型微服务

# 08.3 V3 全链路幂等

特征:超大规模 / 多团队协同。

做法:

  • 网关层注入 requestId 并透传
  • 链路追踪整合
  • 全链路幂等监控(重复率统计)
  • 自动化补偿与对账

适用阶段:超大型分布式系统

flowchart LR
    V1[V1 唯一约束<br/>简单业务] --> V2[V2 幂等中间件<br/>微服务标配]
    V2 --> V3[V3 全链路幂等<br/>超大规模]
    
    style V1 fill:#e3f2fd
    style V2 fill:#e8f5e8
    style V3 fill:#fff3e0
1
2
3
4
5
6
7

# 09.总结与决策

# 09.1 上线检查表

新增涉及"写"操作的接口前对照:

  • [ ] 已识别幂等键(业务唯一标识)
  • [ ] 幂等键由客户端生成(重试时不变)
  • [ ] 选定幂等方案(唯一索引 / Token / 状态机 / 锁)
  • [ ] 重复请求时返回明确语义(成功/已处理/拒绝)
  • [ ] 副作用纳入幂等保护(短信、积分、消息)
  • [ ] 幂等键有时效(不会无限期占用)
  • [ ] DB 唯一约束作为最后一道防线
  • [ ] 异常处理覆盖 DuplicateKey / 锁超时
  • [ ] 客户端有按钮防抖(前端兜底)
  • [ ] 监控覆盖(重复率、命中率)
  • [ ] 测试覆盖(并发重发、超时重试场景)

# 09.2 选型决策树

flowchart TD
    Start([我要做幂等]) --> Q1{业务类型?}
    
    Q1 -->|创建/插入| UI{需返回首次结果?}
    UI -->|否| UniqueIdx[唯一索引]
    UI -->|是| IdmTable[幂等表 + 唯一索引]
    
    Q1 -->|更新/状态流转| SM[状态机<br/>UPDATE WHERE status]
    Q1 -->|高并发竞争| DL[分布式锁]
    Q1 -->|表单提交| Token[Token 防重]
    Q1 -->|消息消费| MsgIdm[消息 ID 去重<br/>+ 幂等表]
    
    Q2([场景增强]) --> Pay{涉及钱?}
    Pay -->|是| Combine[多重保险<br/>Token + 唯一索引 + 状态机]
    Pay -->|否| Single[单一方案够用]
    
    style UniqueIdx fill:#e8f5e8
    style SM fill:#fff3e0
    style Token fill:#e3f2fd
    style Combine fill:#ffebee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最后一句话:幂等是分布式系统的"防重保险丝"——开篇 3700 个用户被扣两次款只是因为支付接口少了一个 requestId 字段。

好的幂等设计 = 业务唯一标识、先判后做、时效清晰、副作用全包。

上次更新: 2026/06/07, 10:26:12
网络检测方案设计
分布式锁方案设计

← 网络检测方案设计 分布式锁方案设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式