转账是支付系统里最“简单也最复杂”的场景之一。简单在于流程明确,复杂在于并发、幂等、事务和分库分表。
转账 vs 汇款
- Transfer:一般账户间转账,强调资金在账户间的流动
- Remittance:更多用于跨境汇款
本质上,充值、支付、退款、提现、红包,都可以抽象为“转账”。
存储设计
用户余额
最简单的余额表:
CREATE TABLE `wallet_tab_v1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`balance` bigint(20) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
真实业务里,余额通常要区分来源。比如“转账收入提现要收费,银行卡入金免费提现”。
CREATE TABLE `wallet_tab_v2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`wallet_type` tinyint(3) NOT NULL DEFAULT '0',
`balance` bigint(20) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id_wallet_type` (`user_id_wallet_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
转账订单
订单用于查询、对账与风控:
CREATE TABLE `order_tab_v1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(64) NOT NULL,
`from_user_id` bigint(20) NOT NULL,
`to_user_id` bigint(20) NOT NULL,
`amount` bigint(20) NOT NULL,
`status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
幂等性
幂等意味着:重复请求不会重复扣款。
常见方案:
- 客户端生成
nonce - 服务端保存
nonce并建立唯一索引
CREATE TABLE `order_tab_v2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(64) NOT NULL,
`from_user_id` bigint(20) NOT NULL,
`to_user_id` bigint(20) NOT NULL,
`amount` bigint(20) NOT NULL,
`status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`nonce` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `order_id` (`order_id`),
UNIQUE KEY `nonce` (`nonce`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
状态机设计
简单状态机示例:
- 0:未完成
- 1:处理中
- 2:失败
- 3:已完成
状态流转必须单向可追踪,避免回滚穿越。
原子性
转账至少包含四步:
- 扣减转出方余额
- 增加接收方余额
- 更新订单状态
- 写入流水
示例:
START TRANSACTION;
SELECT balance FROM wallet_tab WHERE user_id = A FOR UPDATE;
UPDATE wallet_tab SET balance = balance - 100 WHERE user_id = A;
SELECT balance FROM wallet_tab WHERE user_id = B FOR UPDATE;
UPDATE wallet_tab SET balance = balance + 100 WHERE user_id = B;
UPDATE order_tab SET status = 3 WHERE order_id = 'order_id';
COMMIT;
并发与死锁
A 转 B 与 B 转 A 同时发生,容易死锁。
解决思路:按 user_id 排序,固定加锁顺序。
分库分表挑战
分库分表后会遇到两个难题:
- 订单查询变复杂
- 跨库事务无法用单库事务解决
订单拆分
把一笔转账拆成两笔订单:
- 转出订单(A)
- 转入订单(B)
表结构示例:
CREATE TABLE `order_tab_v3` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(64) NOT NULL,
`order_type` tinyint(4) NOT NULL DEFAULT '0',
`user_id` bigint(20) NOT NULL,
`linked_user_id` bigint(20) NOT NULL,
`amount` bigint(20) NOT NULL,
`status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`nonce` varchar(64) NOT NULL,
`linked_order_id` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `order_id` (`order_id`),
UNIQUE KEY `nonce` (`nonce`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
处理流程拆分
基本流程:
- 创建 A 的转出订单
- A 库事务:扣 A 余额,更新订单
- B 库事务:加 B 余额,写转入订单
- 更新订单关联信息
失败时通过消息队列重试或退款补偿。
红包场景
红包是 1-N 转账,压力更大。
常见优化:
- 预分配金额并缓存
- 领取失败则回滚金额
- 过期未领退款
小结
转账不是“扣钱 + 加钱”这么简单。它涉及幂等、事务、死锁、分库分表和补偿。做好这些细节,才能支撑稳定的转账体验。