[Golang 修仙之路] 场景题:转账接口设计

33 阅读4分钟

参考了:mp.weixin.qq.com/s/6JQBJ8ZVC…

仅供个人学习使用。

1. 转账场景要考虑哪些问题?

  1. 分布式场景下的事务:转出100,对方资金也要增加100.

    • 「转出100,对方资金未增加」怎么应对
    • 「对方资金到账100,转出方却以为出错,回滚了」怎么应对
  2. 接口的安全性:如果知道了转账接口,直接调用,是不是可以无限增加资金?

  3. 对账:

    • 总金额要对得上
    • 明细也要对得上

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) 转账,发生了什么?

  1. 出金方,提供一个转账方法:
// 账户A 向 账户B 转账,金额是100元(10000分)
transfer(src=1001, dst=2002, amount=10000)
  1. 出金方要判断A的余额够不够,为了防止并发读带来的超支问题,「判断余额」和「加锁」2步操作要保证原子性,单机加锁就行。

  2. 具体来说:A 获取锁,查询 账户表,判读「余额」-「冻结」-「转出金额」是否大于0,是-流程继续,否-流程终止。

  3. 出金方 更新 账户表,A的 余额不变,冻结金额 += 转出金额(100元)。

  4. 出金方 向流水表中插入

  transaction_no='ICBC-000001' // 流水ID
  account_id=1001, // 账户A
  direction=0, // 转出
  amount=10000, // 金额100target_transaction_no=NULL, // 暂时没有对方流水
  status=0 // 已发起
  1. 出金方 向 冻结记录表 中,插入一条冻结记录。

  2. 出金方 调用 入金方的 「入账方法」

  3. 如果「入账方法」返回成功,则万事大吉。

    • 出金方 更新 账户表 冻结金额-= 转出金额(100元)总金额-= 转出金额(100元)
    • 出金方 更新 流水表target_transaction_no = 'DBS-000001'status = 1(成功)
    • 出金方 向 冻结记录表 中插入一条 解冻记录。
  4. 如果 「入账方法」超时,则 重试。

    • 重试问题1: 入账方接口要幂等,否则一直重试导致入账方多次入账。
    • 重试问题2: 如果重试3次后,没收到响应,出金方回滚了。入账方很久以后收到并执行了入账动作。不一致再次发生。
    • 对于问题2,我觉得这就是2将军问题,
      • 要么无限重试,直到成功。
      • 要么重试3次失败之后,转异步重试,记录流水状态为 3(超时),向客户端返回转账处理中。
      • 要么依赖对账机制,即使产生了不一致,也能在一天内补偿回来,达成「最终一致性」。
  5. 如果 「入账方法」返回错误,则回滚。

    • 出金方 更新 账户表 冻结金额+= 转出金额(100元),总金额不变。
    • 出金方 更新 流水表status = 2(失败)
    • 出金方 向 冻结记录表 中插入一条 解冻记录。

4. 入账方

通过记录处理过的入账流水信息来实现幂等。

5. 对账

对账要对2个方面:

  • 总金额。(卡住某个时间点前转出的总金额是否等于对方转入的总金额)
  • 流水。(检查转出的每一笔流水是否在对方的转入记录中都能查到)