Arch Day 51: 幂等性与分布式一致性 — "不多扣、不少扣、不重扣"的工程实现
支付系统的"不多扣、不少扣、不重扣"三原则是金融系统正确性的生命线。在分布式环境下(网络超时、服务重启、并发冲突),要实现这三原则需要幂等性设计(Idempotency)保证"不重扣",分布式事务(Distributed Transaction)保证"不多扣不少扣",以及补偿机制(Compensation)在异常场景下恢复正确状态。
日期: 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 (超过最大等待时间)
对比分析
分布式事务方案对比(支付视角)
| 维度 | 2PC | TCC | Saga | 本地消息表 | 事务消息 |
|---|---|---|---|---|---|
| 一致性 | 强一致 | 强一致 | 最终一致 | 最终一致 | 最终一致 |
| 性能 | 低(阻塞) | 高 | 高 | 中 | 中 |
| 业务侵入 | 低 | 高(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在实时支付中的应用。