在支付项目中,如何保证交易的一致性?
1 个回答
做自己,而不是解释自己。张无忌# 1. 我们要保证哪些“一致” 1. **账户资金不丢不多**:借贷必相等(双重记账)。 2. **订单—支付单—账务**状态一致、可追溯。 3. **内部事实**与**外部支付通道**(微信/支付宝/网关)一致。 4. **并发/重试/回调乱序**下仍能保持上面三条。 # 2. 单体内的强一致(同库事务内完成) 核心:**关系型数据库事务 + 行级锁/版本号 + 不可变流水**。 * **账本(ledger)只追加,不做覆盖;余额通过流水聚合或维护一张快照表**。 * **幂等键**覆盖所有关键写:订单、支付、退款、回调、对账入账。 示例表: ```sql -- 业务幂等去重 CREATE TABLE idempotent_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_key VARCHAR(128) UNIQUE, -- 例如: PAY:orderNo:xxx or REFUND:payNo:xxx created_at DATETIME NOT NULL ); -- 支付单 CREATE TABLE payment ( pay_no VARCHAR(64) PRIMARY KEY, order_no VARCHAR(64) NOT NULL UNIQUE, -- 限制一单一付(或加attempt_no复用) amount DECIMAL(18,2) NOT NULL, state TINYINT NOT NULL, -- 0待发起 1处理中 2成功 3失败 4已退款... version INT NOT NULL DEFAULT 0, -- 乐观锁 channel VARCHAR(32), channel_txn_id VARCHAR(64), created_at DATETIME, updated_at DATETIME ); -- 账务流水(双重记账: 借=贷) CREATE TABLE ledger_entry ( entry_id BIGINT PRIMARY KEY AUTO_INCREMENT, ledger_no VARCHAR(64) NOT NULL, -- 同一笔业务的分录编号 account_id VARCHAR(64) NOT NULL, dc CHAR(1) NOT NULL, -- 'D'借 / 'C'贷 amount DECIMAL(18,2) NOT NULL, biz_type VARCHAR(32), biz_id VARCHAR(64), created_at DATETIME NOT NULL ); CREATE UNIQUE INDEX uk_ledger_no_account ON ledger_entry(ledger_no, account_id, dc); ``` **余额更新两种做法**(二选一): * A. 仅凭流水算余额(查询聚合/物化视图),最安全; * B. 维护 `account.balance` + 流水(更新余额时用**乐观锁**或\*\*`UPDATE ... WHERE version=?`\*\*,失败重试)。 # 3. 跨服务的一致:最终一致性三板斧 在支付系统里,**2PC基本不可用**(外部通道不参与两阶段)。实践里常用三种: ## 3.1 本地消息表 / Outbox(强烈推荐) **在同一数据库事务内**同时写业务数据和一条“待发布消息”,由后台 **Message Relay** 异步投到 MQ,消费者做幂等入账/更新。 表与流程: ```sql CREATE TABLE outbox ( id BIGINT PRIMARY KEY AUTO_INCREMENT, topic VARCHAR(64) NOT NULL, msg_key VARCHAR(128) UNIQUE, -- 幂等键 payload JSON NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0=NEW 1=SENT created_at DATETIME, sent_at DATETIME ); ``` * 下单→创建 `payment(state=处理中)` 与 `outbox(PAY_CREATED)` **同事务提交**; * Relay 扫描 NEW → 发 MQ → 标记 SENT(失败重投,保证**至少一次**); * **消费者**用自己的 `idempotent_log` 去重,保证**恰好一次语义**(业务层实现)。 ## 3.2 事务消息(如 RocketMQ 事务消息) * 先发半消息 → 本地事务提交/回滚 → Broker 回查补发。 * 效果等价于 outbox,但侵入更低;仍需消费者侧幂等。 ## 3.3 SAGA/TCC(资金强一致流程一般走“冻结/解冻”) * **TCC**:try 冻结余额、confirm 扣减、cancel 解冻(适合自控账户体系)。 * **SAGA**:每步都设计补偿动作(如下单失败→取消、入账失败→冲正分录)。 # 4. 与外部通道的一致(回调、异步、乱序、重放) **原则**:通道回调 **可能重复/乱序/晚到**,必须**幂等 + 只前进的状态机**。 支付状态机(只允许前向迁移): ``` INIT -> PROCESSING -> SUCCESS \-> FAIL SUCCESS -> REFUNDING -> REFUNDED ``` 实现要点: * `UPDATE payment SET state=?, version=version+1 WHERE pay_no=? AND state IN (允许的前置状态) AND version=?` * **回调处理**: 1. 验签(签名/时间戳/重放窗口) 2. 幂等键:`CALLBACK:channel:channel_txn_id` 3. 尝试推进状态机;若已是终态则直接返回 200(幂等) 4. 产出 outbox 事件(如 `PAY_SUCCESS`)→ 下游账务入账 # 5. 并发与防双扣 * **同一订单只允许一个活跃支付**:`UNIQUE(order_no)` 或 `(order_no, attempt_no)` 搭配**业务侧路由**。 * **用户连点 & 重试**:前端下发一次性 **idempotencyKey**,服务端 `UNIQUE(idem_key)`。 * **账户扣减**:乐观锁(版本号)或悲观锁(行锁)都可以;金额操作推荐**分录+校验借贷平衡**,而不是直接 `balance = balance - x`。 # 6. 失败恢复与对账/冲正 * **T+0/T+1 渠道对账**:比对 `our_payment` vs `channel_statement` 差异(缺单、金额不符、状态不一致)。 * 差异处理: * **我们成功/渠道失败** → 自动**冲正**或人工介入; * **渠道成功/我们未记账** → 走**账务补记 + 状态修复**(有幂等)。 * **死信队列/重试策略**:MQ 消费失败进 DLQ,提供可观测的“待人工”列表。 # 7. 优雅停机 & 任务可恢复(避免“执行一半被掐断”) * 关键流程写**阶段型进度**(如 `settle_job.step`),**SIGTERM** 时 flush 并可续跑。 * 所有外发请求**要么成功要么可重试**;回调/账务等**都幂等**,保证“多次等于一次”。 # 8. 关键路径伪代码 **发起支付(含 Outbox)** ```java @Transactional public void createPayment(String orderNo, BigDecimal amt, String idemKey){ idempotentGuard("PAY_CREATE:" + idemKey); // INSERT idempotent_log insertPayment(orderNo, amt, PROCESSING); // UNIQUE(order_no) insertOutbox("PAY_CREATED", orderNo, json(...)); } ``` **处理渠道回调** ```java public Response onCallback(Notify n){ verifySignature(n); // 验签 + 防重放 if (!idempotentGuard("CALLBACK:"+n.txnId)) return ok(); // 幂等 Payment p = findByPayNo(n.payNo); // 仅允许从 PROCESSING -> SUCCESS/FAIL boolean ok = updatePaymentStateCAS(p, nextState(n), p.version); if (ok) insertOutbox("PAY_"+nextState(n), p.payNo, json(...)); return ok(); } ``` **消费者入账(幂等)** ```java public void onPaySuccess(Event e){ if (!idempotentGuard("CONSUME:"+e.key)) return; // 幂等 // 双重记账:用户账户 贷 amount,平台中间户 借 amount createBalancedEntries(e.ledgerNo, e.userId, e.amount); // 借贷平衡校验 } ``` # 9. 面试速答版(30 秒) * **单库**:事务 + 乐观锁/行锁 + 不可变流水(双重记账) + 幂等键。 * **跨服务**:Outbox/事务消息 + 消费者幂等(最终一致)。 * **外部通道**:签名校验 + 回调幂等 + 只前进状态机。 * **兜底**:T+0/T+1 对账 + 冲正/补记;全链路可观测、可重试。 * **并发**:唯一约束/幂等 token + 版本校验;扣款不直接改余额,**用分录确保借贷平衡**。发布于 2025-08-07 11:35 •IP属地 吉林举报评论收藏
公众号进入网站体验更佳

