返回架构笔记
Arch Day 51

Arch Day 51: 幂等性与分布式一致性 — "不多扣、不少扣、不重扣"的工程实现

支付系统的"不多扣、不少扣、不重扣"三原则是金融系统正确性的生命线。在分布式环境下(网络超时、服务重启、并发冲突),要实现这三原则需要幂等性设计(Idempotency)保证"不重扣",分布式事务(Distributed Transaction)保证"不多扣不少扣",以及补偿机制(Compensation)在异常场景下恢复正确状态。

2026-05-20
第二阶段 - 金融域深度
幂等性分布式事务SagaTCC最终一致性Exactly-Once掉单处理

日期: 2026-05-20 (Day 51) 阶段: 第二阶段 - 金融域深度 标签: #幂等性 #分布式事务 #Saga #TCC #最终一致性 #Exactly-Once #掉单处理


核心概念

一句话定义

支付系统的**"不多扣、不少扣、不重扣"三原则是金融系统正确性的生命线。在分布式环境下(网络超时、服务重启、并发冲突),要实现这三原则需要幂等性设计(Idempotency)**保证"不重扣",**分布式事务(Distributed Transaction)保证"不多扣不少扣",以及补偿机制(Compensation)**在异常场景下恢复正确状态。

为什么资深架构师仍需关注

维度关注理由
支付的生命线扣错一笔钱的严重性远超系统宕机一小时
分布式必然性现代支付系统必然是分布式的,一致性问题无法回避
方案选择复杂2PC/TCC/Saga/本地消息表/事务消息——每种方案都有适用场景和坑
面试高频分布式事务是架构师面试的必考题,支付场景是最好的回答素材
工程难度高理论简单实现难——超时、网络分区、并发冲突的组合场景是工程噩梦

常见误区与反模式

误区真相
"用数据库事务就能保证一致性"跨服务/跨数据库的操作无法用单一数据库事务保证
"2PC可以解决所有分布式事务"2PC有阻塞问题和单点故障问题,在支付场景中很少使用
"幂等就是加个唯一索引"幂等要处理并发、过期、重入、补偿——远比唯一索引复杂
"最终一致性=数据可以不一致"最终一致性要求数据最终达到一致,中间不一致的窗口要有补偿机制
"用MQ就能保证最终一致"MQ保证消息投递,但不保证业务处理的幂等性和正确性

知识点详解

知识点1:支付"三不"原则的场景分析

"不多扣" — 多扣了用户的钱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
典型场景:
├── 分布式事务部分成功: 扣款成功但创建订单失败
├── 并发操作: 同一笔订单被两个线程同时处理
└── 定时任务重复执行: 结算任务重复运行导致多次打款

防护手段:
├── 事务原子性: 扣款和业务操作在同一事务边界内
├── 分布式锁: 防止并发操作
└── 任务幂等: 定时任务设计为可重复执行

"不少扣" — 用户得了服务但没付钱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
典型场景:
├── 先发货后扣款: 发货成功但扣款失败
├── 通道回调丢失: 通道已扣款但平台不知道
└── 退款多退: 退款金额超过原交易金额

防护手段:
├── 先扣后发: 先确认扣款成功再提供服务
├── 掉单查询: 定时查询通道确认交易状态
└── 退款校验: 退款金额不能超过可退金额

"不重扣" — 同一笔交易扣了两次钱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
典型场景:
├── 用户重复点击: 支付按钮被快速点击两次
├── 网络重试: 超时后自动重试,但原请求已成功
├── MQ重复消费: 消息被重复投递
└── 定时任务重跑: 异常恢复后重新执行

防护手段:
├── 前端防重: 按钮置灰 + 防抖
├── 幂等键: 后端通过唯一标识去重
├── 消息去重: MQ消费幂等
└── 任务锁: 分布式锁保证单次执行

知识点2:幂等性设计完整方案

幂等性设计的三层架构
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Layer 1: 幂等键(Idempotency Key)设计
┌─────────────────────────────────────────┐
│ 幂等键生成策略:                           │
│ ├── 方案A: 客户端生成UUID               │
│ │   └── 优: 客户端控制; 劣: 可能不唯一   │
│ ├── 方案B: 业务键组合                    │
│ │   └── merchantId + outOrderNo         │
│ │   └── 优: 业务含义明确; 劣: 需要规范   │
│ └── 方案C: 服务端预分配                  │
│     └── 先申请ID再使用                   │
│     └── 优: 全局唯一; 劣: 多一次调用     │
│                                         │
│ 推荐: 支付场景用方案B                     │
│ 理由: 商户订单号天然是幂等键              │
└─────────────────────────────────────────┘

Layer 2: 幂等检查流程
┌─────────────────────────────────────────┐
│ 请求到达 → 计算幂等键                     │
│     │                                    │
│     ▼                                    │
│ Redis SETNX(key, "PROCESSING", TTL=24h)  │
│     │                                    │
│     ├── 设置成功(首次请求)                 │
│     │   → 执行业务逻辑                   │
│     │   → 完成后更新: SET(key, result)    │
│     │                                    │
│     ├── 设置失败(重复请求)                 │
│     │   → GET(key) 获取当前状态            │
│     │   ├── "PROCESSING" → 返回"处理中"   │
│     │   └── result → 返回缓存结果          │
│     │                                    │
│     └── Redis不可用(降级)                  │
│         → 走数据库唯一索引兜底              │
└─────────────────────────────────────────┘

Layer 3: 数据库层幂等保障
┌─────────────────────────────────────────┐
│ 支付订单表:                              │
│ CREATE TABLE payment_order (             │
│   id BIGINT PRIMARY KEY,                 │
│   merchant_id VARCHAR(32) NOT NULL,      │
│   out_order_no VARCHAR(64) NOT NULL,     │
│   amount BIGINT NOT NULL,                │
│   status VARCHAR(16) NOT NULL,           │
│   ...                                    │
│   UNIQUE INDEX uk_merchant_order         │
│     (merchant_id, out_order_no)          │
│ );                                       │
│                                         │
│ 即使Redis故障,数据库唯一索引也能防重复    │
└─────────────────────────────────────────┘

TypeScript实现示例

// 支付幂等性完整实现
class PaymentIdempotencyService {
  constructor(
    private redis: RedisClient,
    private db: DatabaseClient,
  ) {}

  /**
   * 幂等执行支付
   * 保证同一个(merchantId + outOrderNo)只会被执行一次
   */
  async executeWithIdempotency(
    request: PaymentRequest,
    handler: (req: PaymentRequest) => Promise<PaymentResult>
  ): Promise<PaymentResult> {
    const idempotencyKey = `pay:${request.merchantId}:${request.outOrderNo}`;

    // Step 1: Redis原子检查
    const acquired = await this.redis.set(
      idempotencyKey,
      JSON.stringify({ status: 'PROCESSING', startTime: Date.now() }),
      'EX', 86400,   // 24小时过期
      'NX'            // 只在key不存在时设置
    );

    if (!acquired) {
      // 重复请求 - 返回已有结果
      return this.handleDuplicateRequest(idempotencyKey);
    }

    try {
      // Step 2: 执行业务逻辑
      const result = await handler(request);

      // Step 3: 缓存结果
      await this.redis.set(
        idempotencyKey,
        JSON.stringify({ status: 'COMPLETED', result }),
        'EX', 86400
      );

      return result;
    } catch (error) {
      // Step 4: 执行失败,清除幂等键(允许重试)
      // 注意:只有"可重试"的错误才清除,"业务失败"要保留
      if (this.isRetryableError(error)) {
        await this.redis.del(idempotencyKey);
      } else {
        await this.redis.set(
          idempotencyKey,
          JSON.stringify({ status: 'FAILED', error: error.message }),
          'EX', 86400
        );
      }
      throw error;
    }
  }

  private async handleDuplicateRequest(key: string): Promise<PaymentResult> {
    const cached = await this.redis.get(key);
    if (!cached) {
      // Redis中key存在但值被清除 → 降级到数据库查询
      return this.queryFromDatabase(key);
    }

    const data = JSON.parse(cached);
    switch (data.status) {
      case 'PROCESSING':
        // 还在处理中,返回"处理中"状态
        throw new PaymentInProgressError('Payment is being processed');
      case 'COMPLETED':
        return data.result;  // 返回缓存的成功结果
      case 'FAILED':
        throw new PaymentFailedError(data.error);
      default:
        throw new Error(`Unknown idempotency status: ${data.status}`);
    }
  }

  private isRetryableError(error: Error): boolean {
    // 网络超时、服务不可用等可重试
    // 余额不足、参数错误等不可重试
    return error instanceof TimeoutError ||
           error instanceof ServiceUnavailableError;
  }
}

知识点3:分布式事务方案对比

分布式事务五种方案
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方案1: 2PC (Two-Phase Commit)
┌─────────────────────────────────────────┐
│ Phase 1 (准备): 协调者问所有参与者"能提交吗?"│
│   所有参与者: "我准备好了" ✓                │
│                                         │
│ Phase 2 (提交): 协调者说"全部提交"          │
│   所有参与者: 提交 ✓                       │
│                                         │
│ 问题:                                    │
│ ├── 阻塞: Phase 1到Phase 2之间资源被锁定   │
│ ├── 单点: 协调者宕机全部阻塞               │
│ └── 数据不一致: Phase 2部分失败→不一致      │
│                                         │
│ 支付适用度: ★★☆ (性能差,很少在支付中使用)  │
└─────────────────────────────────────────┘

方案2: TCC (Try-Confirm-Cancel)
┌─────────────────────────────────────────┐
│ Try: 预留资源(如冻结余额100元)            │
│ Confirm: 确认执行(冻结→扣除)             │
│ Cancel: 取消释放(冻结→解冻)              │
│                                         │
│ 流程:                                    │
│ 扣款Try(冻结100) → 下单Try(预创建)         │
│       ↓ 全部Try成功                       │
│ 扣款Confirm(冻结→扣除) + 下单Confirm(确认)  │
│       ↓ 任一Try失败                       │
│ 扣款Cancel(解冻) + 下单Cancel(取消)         │
│                                         │
│ 优点: 无阻塞,性能好                      │
│ 缺点: 业务侵入大,每个操作要写3个方法       │
│ 支付适用度: ★★★★ (适合资金操作)            │
└─────────────────────────────────────────┘

方案3: Saga (编排式/协同式)
┌─────────────────────────────────────────┐
│ 核心思想: 长事务拆成多个本地事务            │
│          每个本地事务有对应的补偿事务        │
│                                         │
│ 正向流程:                                │
│ T1(扣款) → T2(创建订单) → T3(通知商户)     │
│                                         │
│ 补偿流程(T2失败时):                       │
│ C1(退款) ← 触发补偿                       │
│                                         │
│ 编排式(Orchestration): 有中央协调器控制     │
│ 协同式(Choreography): 事件驱动,各自监听    │
│                                         │
│ 优点: 无锁,高性能,自然适合异步场景        │
│ 缺点: 补偿逻辑复杂,中间状态可见           │
│ 支付适用度: ★★★★★ (最常用)               │
└─────────────────────────────────────────┘

方案4: 本地消息表 (Transactional Outbox)
┌─────────────────────────────────────────┐
│ 核心思想: 业务操作和消息发送在同一本地事务中 │
│                                         │
│ BEGIN TX                                │
│   UPDATE account SET balance = balance - 100│
│   INSERT INTO outbox_msg (topic, body, status)│
│     VALUES ('payment.created', '{...}', 'PENDING')│
│ COMMIT TX                               │
│                                         │
│ 后台线程: 轮询outbox_msg表 → 发送MQ → 标记已发│
│                                         │
│ 优点: 简单可靠,保证本地操作和消息的原子性   │
│ 缺点: 需要轮询/CDC,有延迟               │
│ 支付适用度: ★★★★☆ (适合跨服务通信)        │
└─────────────────────────────────────────┘

方案5: 事务消息 (RocketMQ/Kafka Transaction)
┌─────────────────────────────────────────┐
│ 核心: MQ原生支持事务消息                   │
│                                         │
│ ① 发送半消息(Half Message) → MQ           │
│ ② 执行本地事务                            │
│ ③ 根据本地事务结果: Commit/Rollback消息     │
│ ④ MQ回查: 如果长时间没有Commit/Rollback     │
│    → MQ主动查询本地事务状态                 │
│                                         │
│ 优点: MQ保证消息可靠投递                   │
│ 缺点: 依赖特定MQ(RocketMQ)               │
│ 支付适用度: ★★★★☆ (如果用RocketMQ)       │
└─────────────────────────────────────────┘

知识点4:Saga模式在支付中的应用

支付Saga编排示例
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

场景: 用户购买商品 (扣款 → 创建订单 → 扣减库存 → 通知)

正向流程:
┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
│ 扣款    │ → │ 创建   │ → │ 扣减   │ → │ 通知   │
│ T1     │   │ 订单T2 │   │ 库存T3 │   │ T4     │
└────────┘   └────────┘   └────────┘   └────────┘

补偿流程 (T3失败时):
┌────────┐   ┌────────┐
│ 退款    │ ← │ 取消   │ ← T3失败触发
│ C1     │   │ 订单C2 │
└────────┘   └────────┘
(T4没有执行,无需补偿)

Saga编排器实现:
┌─────────────────────────────────────────┐
│        Saga Orchestrator                 │
│                                         │
│  SagaDefinition:                        │
│  ├── Step 1: DebitAccount               │
│  │   ├── action: debitService.debit()   │
│  │   └── compensate: debitService.refund()│
│  │                                      │
│  ├── Step 2: CreateOrder                │
│  │   ├── action: orderService.create()  │
│  │   └── compensate: orderService.cancel()│
│  │                                      │
│  ├── Step 3: DeductInventory            │
│  │   ├── action: inventoryService.deduct()│
│  │   └── compensate: inventoryService.restore()│
│  │                                      │
│  └── Step 4: NotifyMerchant            │
│      ├── action: notifyService.send()   │
│      └── compensate: null (不需要补偿)   │
│                                         │
│  SagaState:                             │
│  ├── sagaId: "saga_abc123"              │
│  ├── currentStep: 2                     │
│  ├── status: RUNNING / COMPENSATING / COMPLETED│
│  ├── stepResults: [                     │
│  │   { step: 1, status: SUCCESS },      │
│  │   { step: 2, status: RUNNING }       │
│  │ ]                                    │
│  └── 持久化到数据库(Saga日志)             │
└─────────────────────────────────────────┘

关键设计点:
├── Saga日志持久化: 服务重启后可从断点恢复
├── 补偿操作幂等: 补偿可能被重复调用
├── 超时处理: 某步骤超时→触发补偿
├── 并行优化: 无依赖的步骤可并行执行
└── 监控告警: 补偿失败需要人工介入

知识点5:Exactly-Once语义在支付链路的实现

Exactly-Once在支付中的实现
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

理论: 分布式系统中真正的Exactly-Once是不可能的
实践: 通过 At-Least-Once + 幂等性 = "效果等价的Exactly-Once"

支付链路的Exactly-Once保障:

环节1: API请求层
├── 幂等键: 保证重复请求不重复处理
└── 状态机: 保证状态只能单向流转

环节2: 服务间通信
├── MQ消费幂等: 消费者维护已处理消息ID表
├── 重复投递: MQ保证At-Least-Once + 消费端去重
└── 代码模式:
    async function handleMessage(msg: Message) {
      // 幂等检查
      const processed = await db.query(
        'SELECT 1 FROM processed_msgs WHERE msg_id = ?',
        [msg.id]
      );
      if (processed) return; // 已处理,跳过

      await db.transaction(async (tx) => {
        // 业务处理
        await processPayment(tx, msg.payload);
        // 记录已处理(在同一事务中)
        await tx.insert('processed_msgs', { msg_id: msg.id });
      });
    }

环节3: 通道调用层
├── 请求幂等: 携带唯一交易号,通道侧去重
├── 结果确认: 超时后主动查询,不是盲目重试
└── 状态同步: 以通道最终状态为准

环节4: 回调通知层
├── 回调幂等: 接收方处理回调时检查是否已处理
├── 重复通知: 通道可能重复发回调,需要去重
└── 乱序处理: 回调可能乱序到达,用状态机过滤

知识点6:掉单处理方案

掉单场景与处理策略
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

掉单定义: 支付处于不确定状态——不知道是成功还是失败

场景1: 网络超时
  平台 ──(请求)──→ 通道
  平台 ←──(超时,没收到响应)

  实际情况可能是:
  A. 通道收到请求并处理成功(但响应丢失)
  B. 通道收到请求但处理失败
  C. 通道根本没收到请求

场景2: 服务重启
  平台发送请求后、收到响应前,平台服务重启
  内存中的请求上下文丢失

场景3: 通道异步
  通道返回"受理成功"(不是最终结果)
  最终结果通过异步回调通知
  但回调可能延迟或丢失

掉单处理三板斧:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

板斧1: 主动查询 (Query)
┌─────────────────────────────────────────┐
│ 超时后不是盲目重试,而是先查询通道         │
│                                         │
│ 策略: 阶梯式查询                         │
│ ├── 30秒后: 第1次查询                    │
│ ├── 1分钟后: 第2次查询                   │
│ ├── 5分钟后: 第3次查询                   │
│ ├── 30分钟后: 第4次查询                  │
│ └── 4小时后: 最后一次查询 → 仍不确定则告警 │
│                                         │
│ 查询结果处理:                             │
│ ├── 通道返回"成功" → 更新为成功           │
│ ├── 通道返回"失败" → 更新为失败           │
│ └── 通道返回"不存在" → 可安全重试/关单     │
└─────────────────────────────────────────┘

板斧2: 对账补偿 (Reconciliation)
┌─────────────────────────────────────────┐
│ T+1对账时发现平台"处理中"的订单           │
│                                         │
│ 情况A: 通道对账文件中有 → 更新为成功       │
│ 情况B: 通道对账文件中无 → 更新为失败       │
│                                         │
│ 这是掉单的最后兜底机制                    │
└─────────────────────────────────────────┘

板斧3: 人工介入 (Manual)
┌─────────────────────────────────────────┐
│ 以上两种都无法确认时:                     │
│ ├── 告警通知运维                          │
│ ├── 联系通道方人工确认                    │
│ └── 根据确认结果手动调账                  │
│                                         │
│ 原则: 宁可少扣(不确定就先不扣)             │
│       用户感知上: 不确定就当失败处理        │
│       事后: 通过对账发现→补扣              │
└─────────────────────────────────────────┘

支付订单状态机(含掉单处理):
  CREATED → PROCESSING → SUCCESS
                       → FAILED
                       → UNKNOWN (掉单状态)

  UNKNOWN → SUCCESS (查询/对账确认成功)
  UNKNOWN → FAILED  (查询/对账确认失败)
  UNKNOWN → CLOSED  (超过最大等待时间)

对比分析

分布式事务方案对比(支付视角)

维度2PCTCCSaga本地消息表事务消息
一致性强一致强一致最终一致最终一致最终一致
性能低(阻塞)
业务侵入高(3个方法)中(补偿方法)
实现复杂度
适用场景数据库间资金操作跨服务编排异步通知异步通知
支付推荐度★★★★★★★★★★★★★★★★★★★

幂等方案对比

方案实现复杂度性能可靠性适用场景
Redis SETNX高(ms)中(Redis故障)首选方案
DB唯一索引高(持久化)兜底方案
Redis+DB双层推荐组合
状态机状态流转控制
Token机制前端防重

架构设计实操

设计目标

用TypeScript实现完整的支付幂等方案,包含幂等键、状态机和补偿机制。

支付状态机实现

// 支付状态机 - 核心设计
enum PaymentStatus {
  CREATED = 'CREATED',         // 已创建
  PROCESSING = 'PROCESSING',   // 处理中
  SUCCESS = 'SUCCESS',         // 成功
  FAILED = 'FAILED',           // 失败
  UNKNOWN = 'UNKNOWN',         // 未知(掉单)
  CLOSED = 'CLOSED',           // 已关闭
  REFUNDING = 'REFUNDING',     // 退款中
  REFUNDED = 'REFUNDED',       // 已退款
}

// 合法的状态转移
const VALID_TRANSITIONS: Record<PaymentStatus, PaymentStatus[]> = {
  [PaymentStatus.CREATED]:    [PaymentStatus.PROCESSING, PaymentStatus.CLOSED],
  [PaymentStatus.PROCESSING]: [PaymentStatus.SUCCESS, PaymentStatus.FAILED, PaymentStatus.UNKNOWN],
  [PaymentStatus.UNKNOWN]:    [PaymentStatus.SUCCESS, PaymentStatus.FAILED, PaymentStatus.CLOSED],
  [PaymentStatus.SUCCESS]:    [PaymentStatus.REFUNDING],
  [PaymentStatus.REFUNDING]:  [PaymentStatus.REFUNDED, PaymentStatus.SUCCESS], // 退款失败回到成功
  [PaymentStatus.FAILED]:     [],  // 终态
  [PaymentStatus.CLOSED]:     [],  // 终态
  [PaymentStatus.REFUNDED]:   [],  // 终态
};

function canTransition(from: PaymentStatus, to: PaymentStatus): boolean {
  return VALID_TRANSITIONS[from]?.includes(to) ?? false;
}

// 状态转移(原子操作)
async function transitStatus(
  db: DatabaseClient,
  paymentId: string,
  expectedFrom: PaymentStatus,
  to: PaymentStatus
): Promise<boolean> {
  // 使用乐观锁:UPDATE ... WHERE status = expectedFrom
  const result = await db.query(
    `UPDATE payment_order
     SET status = ?, updated_at = NOW()
     WHERE payment_id = ? AND status = ?`,
    [to, paymentId, expectedFrom]
  );
  return result.affectedRows === 1;
}

ADR-051:支付一致性采用"Saga+本地消息表"组合方案

项目内容
决策跨服务操作使用Saga模式编排;服务间异步通知使用本地消息表保证可靠性
状态已采纳
上下文支付链路涉及扣款、下单、通道调用、通知等跨服务操作,需要保证最终一致
决策理由Saga适合复杂业务流程编排(有明确的补偿逻辑);本地消息表适合事件通知(简单可靠)。两者组合覆盖了所有场景
不选2PC的原因性能差,跨服务不适用
不选TCC的原因业务侵入太大,每个操作要写3个方法,支付场景中Saga更自然
权衡Saga的补偿逻辑增加了代码量,中间状态对用户可见(需要前端处理)

AI增强实践

AI在一致性保障中的应用

AI增强的一致性系统
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. 智能掉单处理
   ├── 基于历史数据预测掉单的实际状态
   ├── 特征: 通道类型、金额区间、超时时长、错误码
   ├── 预测: 该笔掉单"实际成功"的概率是多少
   └── 决策: 高概率成功→等待;高概率失败→直接关单

2. 异常模式检测
   ├── 实时监控幂等键碰撞率
   ├── 检测异常重试模式(如同一用户短时间大量重试)
   ├── 识别可能的重放攻击
   └── 自动调整幂等键过期时间

3. Saga补偿优化
   ├── 分析历史补偿成功率
   ├── 预测补偿操作的最佳重试时机
   ├── 自动识别"不需要补偿"的场景(如对方也失败了)
   └── 补偿异常自动升级

4. 对账差异AI分析
   ├── 自动关联掉单和对账差异
   ├── 推荐差异处理方案
   └── 学习历史处理经验

与Web3/DeFi的关联

传统支付一致性 vs 区块链一致性

维度传统支付区块链
事务模型多步骤异步最终一致原子交易(要么全成功要么全失败)
幂等性需要自己实现(幂等键)Nonce天然保证(同nonce只能上链一次)
掉单常见(网络超时/异步)也有(交易pending未上链/重组)
补偿Saga补偿/退款无法补偿(交易不可逆,需要正向操作)
确认通道返回结果区块确认数(1/6/12确认)
状态查询调通道API查询查链上交易状态(getTransactionReceipt)

DeFi中的"Exactly-Once"

DeFi的交易唯一性保障:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

EVM的Nonce机制:
├── 每个账户维护一个递增的nonce
├── 交易(address, nonce)全局唯一
├── 同nonce的交易只能有一个上链
├── 天然的幂等性保证
└── 但带来了新问题: nonce管理(并发发送多笔交易时)

DeFi中的"掉单":
├── 交易发出但长时间pending → Gas太低
├── 交易确认后被重组(Reorg) → 需要等足够确认数
├── L2交易确认但L1未最终性 → 跨层最终性问题
└── 处理方式: 加速(提高Gas)/取消(同nonce发空交易)/等待

启示: 即使有原子性保障的区块链,仍然需要状态管理和异常处理

今日思考

深度问题1:支付系统中最难处理的异常场景是什么?

"超时且通道实际成功"。你调了通道的扣款接口,网络超时了,你不知道通道那边成功没有。此时不能重试(可能双扣),不能当失败(可能漏扣),只能查询——但查询也可能超时。这就是为什么需要"掉单"状态和多轮查询策略。最终兜底是T+1对账。

深度问题2:为什么支付系统更倾向最终一致而非强一致?

强一致(如2PC)要求所有参与方在同一时间点达成一致,这需要"锁"——而锁在分布式高并发场景下意味着性能瓶颈和阻塞风险。支付系统选择最终一致,是因为"用户等几秒看到确定结果"比"系统阻塞导致超时"更可接受。但最终一致不等于"可以不一致"——它要求有完善的补偿和对账机制来保证最终达到一致。

深度问题3:幂等键应该由谁生成?

由**调用方(客户端/商户)**生成。原因:只有调用方知道"这是同一个请求的重试还是新请求"。如果由服务端生成,客户端重试时无法携带同一个幂等键,就无法实现幂等。在支付场景中,幂等键通常是商户订单号(outOrderNo)——商户自己保证唯一性。


面试题准备

题目1:如何保证支付"不重扣"?

30秒回答

通过三层幂等保障:第一层Redis SETNX做快速去重(毫秒级),第二层数据库唯一索引做持久化防重,第三层通道侧透传唯一交易号由通道去重。同时用状态机保证支付状态只能单向流转,防止重复执行。

2分钟回答

(详细展开三层幂等+状态机+并发处理,参见知识点2。)

追问准备

  • 追问1:分布式事务在支付中怎么选?→ 首选Saga(跨服务编排)+ 本地消息表(异步通知),不用2PC(性能差)
  • 追问2:掉单如何处理?→ 三板斧:主动查询(阶梯式)→ 对账补偿(T+1)→ 人工介入

学习资源

资源类型推荐理由
Stripe幂等性设计文档文档业界最佳的幂等API设计实践
《微服务架构设计模式》第4章书籍Saga模式的权威阐述(Chris Richardson)
蚂蚁金服分布式事务框架Seata开源AT/TCC/Saga/XA多模式支持
Martin Kleppmann: 分布式系统课程一致性和事务的理论基础

明日预告

Day 52: 实时支付系统 — 全球实时支付网络正在重塑支付基础设施。核心话题:FPS/UPI/PIX/SEPA Instant/FedNow对比、中国实时支付体系(网联/银联/数字人民币DCEP)、实时支付的架构挑战(7x24/秒级确认/最终性)、ISO 20022在实时支付中的应用。