支付业务退款流程,数据库事务处理余额回滚,日志留存凭证
支付退款时,余额从“已扣减”回到“可用”,不是写一条新记录就结束。
真正安全的方式是:把扣减、回滚、流水写入放在同一个数据库事务里,同时把退款前后的关键信息记入日志留存凭证。
这样既保证金额不会因程序中断而丢失,也让后续对账有据可查。
本文按照支付业务退款流程,从准备、事务实现、日志留存到避坑验证,一步步说明。
开始设计退款流程前,先准备好这三项
在写代码前,确认你的余额表和流水表结构。
典型的余额表至少包含 user_id、balance、version 字段;
流水表至少包含 id、order_no、refund_no、change_amount、before_balance、after_balance、create_time。
确认数据库隔离级别。
建议使用 MySQL InnoDB 的 REPEATABLE READ 或 READ COMMITTED,同时配合行锁避免并发覆盖。
确认退款业务有唯一幂等键。
比如“原订单号 + 退款单号”,防止同一个退款请求被重复执行。
余额回滚的事务操作步骤
这里以 MySQL 为例,展示一个标准的退款事务模板:
BEGIN;
-- 锁定用户余额行,防止并发修改
SELECT balance FROM user_balance WHERE user_id = 1001 FOR UPDATE;
-- 执行余额回滚
UPDATE user_balance
SET balance = balance - 100.00
WHERE user_id = 1001 AND balance >= 100.00;
-- 写入余额变动流水
INSERT INTO balance_log (
user_id, refund_no, order_no, change_amount,
before_balance, after_balance, create_time
) VALUES (
1001, 'R2025001', 'O2025001', -100.00,
(SELECT balance + 100.00 FROM user_balance WHERE user_id = 1001),
(SELECT balance FROM user_balance WHERE user_id = 1001),
NOW()
);
COMMIT;
注意 before_balance 与 after_balance 应按更新后的实际值记录。
更稳妥的做法是先查询锁定的余额,在应用层计算前后值写入流水,避免子查询产生歧义。
执行 UPDATE 时一定要带上 balance >= 回滚金额 的条件,如果影响行数为 0,立即 ROLLBACK,拒绝后续操作。
日志留存凭证不能只写“退款成功”
日志留存凭证要支持事后审计。
建议至少记录以下字段:
- 请求来源:app_id、接口名、客户端 IP;
- 业务标识:原订单号、退款单号、支付渠道流水号;
- 金额信息:退款金额、退款前后余额、币种;
- 操作上下文:操作人/系统账号、操作时间、请求体摘要;
- 结果状态:事务提交或回滚、失败原因、重试次数。
把关键日志写入独立日志表或消息队列,不要只保存在应用日志文件里。
文件日志容易滚动丢失,数据库日志表更便于对账查询。
最容易踩的四个坑
第一个坑:事务里放了外部支付接口调用。
退款时要先更新本地余额,再把“退款结果通知”作为异步任务单独处理,千万不要在事务里等待第三方接口响应,否则会长时间占用行锁。
第二个坑:忽略幂等控制。
没有唯一键时,网络超时重试会导致同一退款单被处理两次。
给流水表增加 refund_no 唯一索引,插入重复时捕获异常并返回已处理。
第三个坑:只记录最终余额。
如果日志没有保留 change_amount 和 before_balance,对账时无法判断回滚是否准确。
第四个坑:UPDATE 不带余额校验。
如果用户余额已经小于退款金额,直接 UPDATE 会把余额变成负数,应该用条件更新和影响行数判断。
退款完成后如何验证数据一致性
验证分三步。
第一步,用下面 SQL 检查余额与流水是否匹配:
SELECT
u.user_id,
u.balance,
(SELECT SUM(change_amount) FROM balance_log l WHERE l.user_id = u.user_id) AS total_change
FROM user_balance u
WHERE u.user_id = 1001;
第二步,模拟故障。
在 COMMIT 之前手动 KILL 连接或重启数据库,观察数据是否回滚到退款前状态,流水表中不应出现半条记录。
第三步,做日常对账。
每天定时核对“支付成功订单金额总和”与“退款流水金额总和”的差额是否等于当前待处理退款金额,一旦不一致立刻告警。
支付退款流程中的余额回滚是否可靠,关键在于数据库事务边界是否清晰、日志留存凭证是否完整。
建议把本文的 SQL 模板改造成自己的函数或存储过程,并在测试环境完成并发、断点、重复请求三类验证后再上线。
若遇到数据不一致,优先检查事务是否提交、幂等键是否生效和日志表是否完整。