支付结算异常处理须坚持“操作可逆、状态可控、结果可溯”,通过本地事务保障核心数据一致性,结合可靠消息、saga补偿、幂等设计、db约束及全量操作日志,守住资金与数据安全底线。

在支付结算这类资金强敏感业务中,异常处理不能只关注“捕获不报错”,而要围绕“操作可逆、状态可控、结果可溯”来设计。核心不是消灭异常,而是让异常发生时系统仍能守住资金和数据的底线。
事务边界必须覆盖完整业务动作
比如充值成功后需更新订单状态 + 增加用户余额 + 发送完成消息。这三步若拆成多个独立事务,任一环节失败就会导致状态撕裂。正确做法是:
- 用 @Transactional(rollbackFor = Exception.class) 显式声明所有异常都回滚,避免默认只对 RuntimeException 生效
- 把数据库操作(订单、账户)放在同一本地事务内;消息发送不参与事务,改用“事务+可靠消息”模式——先落库再发 Kafka,并记录消息发送状态
- 禁止在事务方法里调用本类另一个 @Transactional 方法,否则代理失效,事务不生效
区分业务异常与系统异常,响应策略完全不同
“余额不足”和“数据库连接超时”虽都抛异常,但处理逻辑截然不同:
- 业务异常(如 InsufficientBalanceException、InvalidAmountException):不触发回滚,而是返回明确错误码(如 HTTP 400),前端提示用户“余额不够,请充值”,同时记录业务日志供运营核查
- 系统异常(如 SQLException、TimeoutException):必须回滚,且对幂等接口(如按 orderNo 扣款)可自动重试 1–2 次;非幂等操作(如首次发起支付)则直接失败,由人工或定时任务兜底
- 不可恢复异常(如 OutOfMemoryError):立即熔断,记录全量堆栈+traceId,避免线程卡死拖垮整个服务
跨服务场景靠补偿 + 幂等双保险
订单服务调账户服务扣款、再调通知服务发消息,已超出单库事务能力。此时需 SAGA 模式:
- 每步操作自带补偿动作:扣款成功 → 记录“待补偿”状态;库存扣减失败 → 触发退款操作
- 所有接口必须幂等:用唯一业务 ID(如 payment_id)做数据库唯一索引,或 Redis SETNX 校验是否已处理
- 补偿任务异步执行,失败后进重试队列(如 DelayQueue 或 RocketMQ 延迟消息),并提供人工干预入口(如后台“强制补偿”按钮)
数据库层加固,防住最后一道防线
代码再严谨,也挡不住并发写或人为误操作。需在 DB 层加约束:
- 账户表加 version 字段,每次更新带 where version = ?,防止并发覆盖余额
- 支付流水表设 UNIQUE KEY (order_no, channel),杜绝重复回调插入
- 关键字段统一用 BigDecimal 存储金额,比对前 setScale(2, RoundingMode.HALF_UP),避免浮点误差误判不一致
- 所有资金变动操作,强制写 操作日志表(含操作人、原始参数、前后余额、traceId),支持事后对账与审计
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











