如何设计银行存取款和转账业务的具体实现,除了加锁还有什么?

1 个回答

  • 莫千绝
    无论什么关系,我看不到我想要的态度。我就不会给你想要的细节。
    银行存取款与转账重点不止是“加锁”,而是**用账本模型 + 幂等 + 约束 + 事务编排**来保证强一致与可审计。 # 1. 核心模型(推荐) * `account(id, owner_id, currency, status, version, created_at, …)` * `ledger_entry(id, account_id, transfer_id, type{DEBIT|CREDIT}, amount DECIMAL(18,2), balance_after, occurred_at)` 记**流水**,而不是直接改余额;余额可做缓存/物化列,但**以流水为准**。 * `transfer(idempotency_key, from_acct, to_acct, amount, state{PENDING|POSTED|CANCELLED}, created_at, …)` * `hold(id, account_id, amount, state{ACTIVE|RELEASED|CONSUMED}, expire_at, …)` 预授权/冻结(如支付场景先占额,后清分)。 * `outbox(id, topic, payload, state, created_at)` / `inbox(idempotency_key, …)` 做可靠消息与幂等消费。 > 核心不变量: > > 1. 每笔转账→至少两条`ledger_entry`(借贷平衡) > 2. 任一时刻**同一转账的借贷和为0**(资产=负债) > 3. 任一账户余额=∑该账户所有`ledger_entry`的有符号和(或以此为准校验物化余额) # 2. 不只是加锁:完整一致性手段 1. **账本式记账(双分录)**:杜绝“直接改余额”的并发漏洞;一切通过流水追加实现。 2. **幂等**:所有对外请求携带`idempotency_key`,以(`idempotency_key`,`biz_type`)建唯一约束,重复请求直接返回首个结果。 3. **乐观并发控制**:`account.version`或`transfer.state`列做版本号;更新时`where version = ?`,失败则重试(限次)。 4. **强约束**: * `CHECK(amount > 0)` * `UNIQUE(idempotency_key)` * `FOREIGN KEY`保障转账与分录一致。 5. **事务边界**:**同库同分片**用数据库本地事务一气呵成;跨库/跨服务用**TCC/Saga**。 6. **冻结-消费模型(Hold)**:大宗/高并发场景先`hold`占额,再异步`post`实际记账;过期自动释放,避免长期占位。 7. **可靠消息(Outbox)**:在本地事务里写`ledger_entry`同时写`outbox`,由后台轮询投递到MQ,消费者端用`inbox`做幂等。 8. **读写分离安全**:支付/转账结果查询走主库(或加延迟感知),避免读到旧数据导致误操作。 9. **审计与对账**: * 日切批处理:核对**账户期初 + 当日分录合计 = 期末** * 与外部清结算/支付通道按`transfer_id`对账+差错处理。 10. **额度/风控**:日累计限额、单笔限额、黑白名单、异常频次拦截。 # 3. 三个核心流程(SQL/伪代码) ## 3.1 存款 Deposit(单账户、单分录或对侧为“银行清算户”) ```sql BEGIN; -- 幂等防重 INSERT INTO transfer(idempotency_key, from_acct, to_acct, amount, state) VALUES (:k, NULL, :acct, :amt, 'PENDING') ON CONFLICT (idempotency_key) DO NOTHING; -- 记贷方:客户账户增加(或用CREDIT/DEBIT按你定义) INSERT INTO ledger_entry(id, account_id, transfer_id, type, amount, balance_after, occurred_at) SELECT gen_random_uuid(), :acct, t.id, 'CREDIT', :amt, a.cached_balance + :amt, now() FROM transfer t JOIN account a ON a.id=:acct WHERE t.idempotency_key=:k; -- 物化余额(可选):乐观锁 UPDATE account SET cached_balance = cached_balance + :amt, version = version + 1 WHERE id=:acct AND version=:old_version; UPDATE transfer SET state='POSTED' WHERE idempotency_key=:k; COMMIT; ``` 要点:幂等、单事务、可无锁(靠版本号),流水为准。 ## 3.2 取款 Withdraw(含余额校验) ```sql BEGIN; -- 锁/校验:可用 SELECT ... FOR UPDATE 或乐观版本 SELECT cached_balance, version FROM account WHERE id=:acct FOR UPDATE; IF cached_balance < :amt THEN ROLLBACK; -- 余额不足 INSERT INTO transfer(idempotency_key, from_acct, to_acct, amount, state) VALUES (:k, :acct, NULL, :amt, 'PENDING') ON CONFLICT (idempotency_key) DO NOTHING; -- 记借方:客户账户减少 INSERT INTO ledger_entry(...) VALUES (uuid(), :acct, (SELECT id FROM transfer WHERE idempotency_key=:k), 'DEBIT', :amt, cached_balance - :amt, now()); UPDATE account SET cached_balance=cached_balance-:amt, version=version+1 WHERE id=:acct AND version=:old_version; UPDATE transfer SET state='POSTED' WHERE idempotency_key=:k; COMMIT; ``` ## 3.3 转账 Transfer(同分片本地事务) ```sql BEGIN; -- 幂等 INSERT INTO transfer(idempotency_key, from_acct, to_acct, amount, state) VALUES (:k, :from, :to, :amt, 'PENDING') ON CONFLICT (idempotency_key) DO NOTHING; -- 读并校验付款方余额(行锁或版本) SELECT cached_balance, version FROM account WHERE id=:from FOR UPDATE; IF cached_balance < :amt THEN ROLLBACK; -- 记两条分录(借贷平衡) INSERT INTO ledger_entry(...) VALUES (uuid(), :from, t.id, 'DEBIT', :amt, from_balance-:amt, now()); INSERT INTO ledger_entry(...) VALUES (uuid(), :to, t.id, 'CREDIT', :amt, to_balance+ :amt, now()); -- 更新两账户余额(乐观或悲观都可) UPDATE account SET cached_balance=cached_balance-:amt, version=version+1 WHERE id=:from AND version=:from_ver; UPDATE account SET cached_balance=cached_balance+:amt, version=version+1 WHERE id=:to AND version=:to_ver; UPDATE transfer SET state='POSTED' WHERE idempotency_key=:k; -- outbox(可选):同时写一条“转账已入账”消息,后台异步投递 INSERT INTO outbox(...) VALUES (...); COMMIT; ``` ### 跨库/跨服务(不同分片)怎么办? 用 **TCC 或 Saga**: * **TCC**: Try:在`from`做`hold`冻结,在`to`做“可用额度+保留”。 Confirm:落两侧`ledger_entry`并更新余额; Cancel:释放`hold`/保留。 * **Saga(本地事务+补偿)**: 步骤A扣款→B加款;任一步失败触发补偿(反向分录);配合**幂等 + 去重 + 有界重试**。 # 4. 除并发外的关键工程细节 * **金额类型**:`DECIMAL(18,2)`(或分为单位的`BIGINT`防精度丢失)。 * **顺序与幂等**:对同一`account_id`的事件可按`transfer_id`排序;消费者用`inbox(idempotency_key)`去重。 * **隔离级别**:业务关键读写走`READ COMMITTED`或`REPEATABLE READ`;涉及扣减强一致可用`SELECT … FOR UPDATE`或**乐观版本**。 * **重放/对账**:任何时候可通过`ledger_entry`重放计算余额,发现与`cached_balance`不一致→告警&纠偏。 * **超时释放**:`hold.expire_at`定时任务扫过期冻结→释放,避免长期占名额。 * **权限与审计**:管理员操作双人复核;所有更改写审计日志(不可篡改存储、WORM)。 * **故障可恢复**:所有状态机都应支持**可重入**与**幂等**(多次执行结果一致)。 # 5. 一张表看懂“除了加锁还有什么” | 目标 | 手段 | | ------- | ----------------------- | | 防重/重试安全 | 幂等键 + 唯一约束 + Inbox | | 强一致扣减 | 账本分录 + 本地事务/行锁或版本号 | | 跨库一致性 | TCC/Saga + Outbox | | 占额不亏 | Hold 冻结 + 过期释放 | | 可审计可追溯 | 只追加流水、不可变,期初期末可核 | | 高并发性能 | 读缓存余额,写走流水;热账户可分片或队列串行化 | | 对账/容灾 | 日切对账、账实差异报警、重放修复 |
    发布于 2025-08-11 14:33 •IP属地 广东
    举报
    评论
    收藏