转账是支付系统里最“简单也最复杂”的场景之一。简单在于流程明确,复杂在于并发、幂等、事务和分库分表。

转账 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;

处理流程拆分

基本流程:

  1. 创建 A 的转出订单
  2. A 库事务:扣 A 余额,更新订单
  3. B 库事务:加 B 余额,写转入订单
  4. 更新订单关联信息

失败时通过消息队列重试或退款补偿。

红包场景

红包是 1-N 转账,压力更大。

常见优化:

  • 预分配金额并缓存
  • 领取失败则回滚金额
  • 过期未领退款

小结

转账不是“扣钱 + 加钱”这么简单。它涉及幂等、事务、死锁、分库分表和补偿。做好这些细节,才能支撑稳定的转账体验。