转账中途失败时仍保持余额守恒
目标
把扣减、增加和转账记录以原子方式绑在一起,并重现并发的余额竞争。
为什么重要
转出账户已经扣了钱,还没来得及给转入账户加上,进程就崩溃了。重试时因为没有转账 id,又扣了一次钱。反过来,如果先提交转账记录,重试就会认为已经完成,入账便永远丢失。即使金额计算用整数做得很精确,只要事务边界不对,结果也是错的。
步骤
- 在
/root/work/idem-ledger-transfer-lab/service.py中,init_db(path) 以幂等方式创建 accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL) 和 transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab
-
在
/root/work/idem-ledger-transfer-lab/service.py中,add_account(path, account_id, amount) 只允许(不含 bool 的)大于等于 0 的 int,并用 INSERT 创建账户。已存在的账户以 sqlite3.IntegrityError 拒绝,并保留其余额。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,balance(path, account_id) 返回账户余额,账户不存在时抛出 KeyError。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,validate_transfer(source,target,amount) 在 source!=target 且 amount 是(不含 bool 的)正 int 时返回 None,否则抛出 ValueError。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,transfer(path,tx_id,source,target,amount,fault=lambda:None) 在 id 和内容都相同时返回 False;id 冲突、账户不存在、余额不足则抛出 ValueError。新的转账以原子方式依次执行扣减 → fault() → 入账 → 插入 transfers,并返回 True。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,history(path) 把 transfers 按 id 升序,以 (id,source,target,amount) 元组列表的形式返回。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,total_balance(path) 是 accounts.balance 的合计。没有账户时为 0。 -
在
/root/work/idem-ledger-transfer-lab/service.py中,compete(path,source,target,amount) 用两个不同的 id(race-a 和 race-b),在两个线程中执行金额相同的 transfer。返回的列表中,只把 ValueError 转换为 False。如果余额只够一次转账,成功的就只有一笔。
参考
- 无需联网和安装软件包,在现有的 lab-dev 环境中进行。
- 每个步骤都在 45 秒的评分预算内运行。不要加入真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 检查。不要直接返回预期值的常量,而要实现契约。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:它并不是能取代真实金融账本在会计、审计和法律方面要求的系统。这里不涉及币种、小数单位、手续费和多币种,金额是以最小单位表示的正整数。调用外部支付系统并不包含在这个 DB 事务之内,因此需要另行设计。
创建账本表
在 /root/work/idem-ledger-transfer-lab/service.py 中,init_db(path) 以幂等方式创建 accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL) 和 transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab
把唯一性放在 DB 里,以避免同一个转账 id 被提交两次。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/01-contract.sh 确认。
账户只创建一次
在 /root/work/idem-ledger-transfer-lab/service.py 中,add_account(path, account_id, amount) 只允许(不含 bool 的)大于等于 0 的 int,并用 INSERT 创建账户。已存在的账户以 sqlite3.IntegrityError 拒绝,并保留其余额。
避免初始化时的 UPSERT 把实际余额重置回初始值。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/02-contract.sh 确认。
区分不存在的账户与余额为 0
在 /root/work/idem-ledger-transfer-lab/service.py 中,balance(path, account_id) 返回账户余额,账户不存在时抛出 KeyError。
如果把“不存在”换成 0,转账就可能对一个错误的账户继续执行。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/03-contract.sh 确认。
校验转账输入
在 /root/work/idem-ledger-transfer-lab/service.py 中,validate_transfer(source,target,amount) 在 source!=target 且 amount 是(不含 bool 的)正 int 时返回 None,否则抛出 ValueError。
提前拒绝同一账户之间的转账和负数转账。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/04-contract.sh 确认。
把三次写入绑进一个事务
在 /root/work/idem-ledger-transfer-lab/service.py 中,transfer(path,tx_id,source,target,amount,fault=lambda:None) 在 id 和内容都相同时返回 False;id 冲突、账户不存在、余额不足则抛出 ValueError。新的转账以原子方式依次执行扣减 → fault() → 入账 → 插入 transfers,并返回 True。
扣减之后强制抛出异常,检查余额和转账记录是否都恢复如初。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/05-contract.sh 确认。
以确定的顺序读取转账历史
在 /root/work/idem-ledger-transfer-lab/service.py 中,history(path) 把 transfers 按 id 升序,以 (id,source,target,amount) 元组列表的形式返回。
明确写出 ORDER BY,避免把输入顺序与查询顺序混为一谈。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/06-contract.sh 确认。
确认守恒量
在 /root/work/idem-ledger-transfer-lab/service.py 中,total_balance(path) 是 accounts.balance 的合计。没有账户时为 0。
单个请求的返回值,与总金额是否守恒,要分别验证。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/07-contract.sh 确认。
并发转账不会超出余额
在 /root/work/idem-ledger-transfer-lab/service.py 中,compete(path,source,target,amount) 用两个不同的 id(race-a 和 race-b),在两个线程中执行金额相同的 transfer。返回的列表中,只把 ValueError 转换为 False。如果余额只够一次转账,成功的就只有一笔。
如果在事务之外预先检查余额,两个请求就可能看到同一份余额而都成功。
保存后用 bash /opt/lab/checks/idem-ledger-transfer-lab/08-contract.sh 确认。