参考了:mp.weixin.qq.com/s/6JQBJ8ZVC…
仅供个人学习使用。
1. 转账场景要考虑哪些问题?
-
分布式场景下的事务:转出100,对方资金也要增加100.
- 「转出100,对方资金未增加」怎么应对
- 「对方资金到账100,转出方却以为出错,回滚了」怎么应对
-
接口的安全性:如果知道了转账接口,直接调用,是不是可以无限增加资金?
-
对账:
- 总金额要对得上
- 明细也要对得上
2. 数据库表设计
账户表
CREATE TABLE account (
account_id int PRIMARY KEY AUTO_INCREMET comment '账户ID',
balance int comment '余额,单位是分,1元=100分',
frozen int comment '冻结资金,单位是分,1元=100分',
update_time DATE_TIME comment '最后一次更新时间'
);
流水表
CREATE TABLE transfer_transactions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
transaction_no VARCHAR(32) NOT NULL COMMENT '全局唯一流水号,用于业务追踪',
account_id INT NOT NULL COMMENT '账户ID,关联账户表',
direction int NOT NULL COMMENT '资金方向:转出-0;转入-1',
amount int NOT NULL COMMENT '交易金额,单位是分',
target_transaction_no VARCHAR(32) DEFAULT NULL COMMENT '对方系统的流水号,用于双向核对',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '交易创建时间',
updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
status int NOT NULL DEFAULT 0 COMMENT '交易状态: 0-已发起;1-成功;2-失败;3-超时'
);
冻结记录表
CREATE TABLE fund_freeze_records (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
transaction_no VARCHAR(32) NOT NULL COMMENT '关联的转账操作流水号,全局唯一',
account_id INT NOT NULL COMMENT '账户ID,关联账户表',
amount int NOT NULL COMMENT '交易金额,单位是分',
operation_type int NOT NULL COMMENT '操作类型:冻结-1;解冻-0',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作创建时间',
);
3. 转账流程
账户A(id=1001) 向 账户B(id=2002) 转账,发生了什么?
- 出金方,提供一个转账方法:
// 账户A 向 账户B 转账,金额是100元(10000分)
transfer(src=1001, dst=2002, amount=10000)
-
出金方要判断A的余额够不够,为了防止并发读带来的超支问题,「判断余额」和「加锁」2步操作要保证原子性,单机加锁就行。
-
具体来说:A 获取锁,查询 账户表,判读「余额」-「冻结」-「转出金额」是否大于0,是-流程继续,否-流程终止。
-
出金方 更新 账户表,A的 余额不变,冻结金额 += 转出金额(100元)。
-
出金方 向流水表中插入
transaction_no='ICBC-000001' // 流水ID
account_id=1001, // 账户A
direction=0, // 转出
amount=10000, // 金额100元
target_transaction_no=NULL, // 暂时没有对方流水
status=0 // 已发起
-
出金方 向 冻结记录表 中,插入一条冻结记录。
-
出金方 调用 入金方的 「入账方法」
-
如果「入账方法」返回成功,则万事大吉。
- 出金方 更新 账户表
冻结金额-= 转出金额(100元),总金额-= 转出金额(100元)。 - 出金方 更新 流水表
target_transaction_no = 'DBS-000001',status = 1(成功) - 出金方 向 冻结记录表 中插入一条 解冻记录。
- 出金方 更新 账户表
-
如果 「入账方法」超时,则 重试。
- 重试问题1: 入账方接口要幂等,否则一直重试导致入账方多次入账。
- 重试问题2: 如果重试3次后,没收到响应,出金方回滚了。入账方很久以后收到并执行了入账动作。不一致再次发生。
- 对于问题2,我觉得这就是2将军问题,
- 要么无限重试,直到成功。
- 要么重试3次失败之后,转异步重试,记录流水状态为 3(超时),向客户端返回转账处理中。
- 要么依赖对账机制,即使产生了不一致,也能在一天内补偿回来,达成「最终一致性」。
-
如果 「入账方法」返回错误,则回滚。
- 出金方 更新 账户表
冻结金额+= 转出金额(100元),总金额不变。 - 出金方 更新 流水表
status = 2(失败) - 出金方 向 冻结记录表 中插入一条 解冻记录。
- 出金方 更新 账户表
4. 入账方
通过记录处理过的入账流水信息来实现幂等。
5. 对账
对账要对2个方面:
- 总金额。(卡住某个时间点前转出的总金额是否等于对方转入的总金额)
- 流水。(检查转出的每一笔流水是否在对方的转入记录中都能查到)