2026-04-22 · architecture / fintech
【金融科技工程】复式记账工程化:科目、分录、余额、对账
把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。

























上一篇《复式记账工程化》把账务模型落到了
accounts、journal_entries、postings
三张表。到这里,设计图纸画得再漂亮,也挡不住一个现实问题:这三张表放哪儿?
“放数据库”三个字在账务场景里远不是一行
CREATE TABLE
能解决的。一个成熟支付机构,日分录(postings)量级在亿到十亿级,单行写入必须带事务,读写延迟在
P99 十毫秒以内,还要满足
RPO=0(零数据丢失)、RTO<1min(故障一分钟切换)、审计数据保留七到十年、任何一行都不能被
UPDATE。你要扛住这些约束,数据库选型、分片策略、索引、归档、并发控制几乎都要重新推一遍。
这篇文章的读者画像是:
我们不会挨个讲 SQL 语法,重点在为什么选、怎么切、怎么扛热点、怎么归档、怎么对审计交差。文末会落到一份选型清单和一张可以直接抄的部署架构图。
账务数据库的设计上限由业务 SLA(Service Level Agreement)决定。离开 SLA 谈”用什么数据库最好”是空中楼阁。
这两个指标直接排除了”异步主从 MySQL”、“跨可用区异步同步的 PostgreSQL 主从”这类方案在关键账务路径上的独立部署可能。要么叠加同步复制,要么直接上共识协议。
账务强一致来源于复式记账的约束:借贷必须同事务成功或同事务失败,余额视图必须反映最新分录。工程语境下至少需要:
READ COMMITTED,对扣款类幂等更新场景需要
REPEATABLE READ 或显式
SELECT FOR UPDATE。Oracle/PostgreSQL 的
SERIALIZABLE
在热点账户上代价太高,一般不做默认级别。把以上组合起来,账务数据库实际要满足的”组合 SLA”是:
| 维度 | 账务数据库目标 |
|---|---|
| 写入吞吐 | 单集群 10k–100k TPS(分录行) |
| 读延迟 | P99 < 10 ms |
| 可用性 | 99.99%(年停机 < 52 分钟) |
| RPO | 0 |
| RTO | < 1 min |
| 保留年限 | 热数据 1–2 年、温 3–7 年、冷 10–30 年 |
| 一致性 | 线性一致、事务原子 |
| 审计 | append-only、不可篡改 |
不是所有账务都长一个样:
后面选型要对照不同形态来看。
把能选的都摆出来,再按成本、规模、案例筛。
优势
numeric
精确十进制、uuid、jsonb、range
类型,对金额建模友好(见《钱的建模》一篇对
numeric(38, 18) 的讨论);CHECK、EXCLUDE、domain、generated
columns 足以在 DDL 层编码余额守恒、币种一致等规则;SERIALIZABLE 实现为
SSI(Serializable Snapshot Isolation),正确性高;劣势
适用规模:中小规模账务(日分录 < 1 亿)、B 端 SaaS、证券/基金/信托核心。典型用法是主 + 同步复制从 + 异地异步备 + 分区归档。
案例:Stripe 早期账务层核心表建在 PostgreSQL 上;Revolut 2020 年代初期全栈 PostgreSQL;国内不少持牌支付机构的备付金账务使用 PostgreSQL + Patroni 集群。
优势
劣势
FLOAT/DOUBLE
的误用)导致金额建模需要严格规范;适用规模:互联网支付、电商账务中等到大规模。阿里/蚂蚁、京东、美团、拼多多、字节跳动的账务体系都有长期在 MySQL 上深耕的历史。
典型演进路径:单机 MySQL → 一主多从 →
垂直拆分(按业务域)→ 水平分库分表(按
account_id / user_id hash)→
分布式数据库(OceanBase / TiDB)。
蚂蚁集团自研的分布式关系数据库,原生 Paxos 三副本、MySQL 协议兼容(也兼容部分 Oracle 语法),典型”金融核心场景”产品。
优势
劣势
适用规模:日分录 10 亿 + 的超大规模账务、银行核心、证券账户体系。
PingCAP 开源的分布式数据库,TiKV + TiDB Server + PD 三组件架构,Raft 多副本。
优势
劣势
适用规模:日分录数亿到十亿级、有 HTAP 诉求、开源栈偏好。
美国 Cockroach Labs 开源产品(自 2022 年起部分核心代码转为 CockroachDB Community License,非完全 OSI 开源),主打全球部署。
优势
劣势
适用规模:跨境支付、全球化 SaaS 账务、需要多地数据主权合规的场景。国际案例如 Bose、DoorDash、Form3(开放银行支付平台)。
优势
劣势
适用规模:传统银行核心、保险、清算机构。典型案例:中国人民银行第二代支付系统(CNAPS2)历史上依赖 Oracle + DB2,逐步向国产数据库迁移中。
一个非常有意思的路线:为复式记账专门设计的数据库,用 Zig 写,单表结构(accounts + transfers)、强一致(VSR/Viewstamped Replication)、单机百万 TPS 设计目标。
优势
postings 表;劣势
user_data
泛化字段,语义要自己约定。适用规模:新建高性能账务、加密资产交易所内账、游戏内经济系统。
互联网金融公司普遍在通用数据库之上包一层”Ledger Service”:
这里的核心启发:数据库不是账务的全部,账务服务层负责”复式约束、幂等、热点拆解、审计落库”,数据库负责”存、查、事务、复制”。
| 数据库 | 开源 | 协议 | 副本协议 | RPO=0 | 典型规模(日分录) | 代表案例 |
|---|---|---|---|---|---|---|
| PostgreSQL | 是 | PG | 逻辑/物理复制 | 依赖同步复制配置 | < 1 亿 | Stripe 早期、Revolut |
| MySQL + 分库分表 | 是 | MySQL | semi-sync | 近似 | 10 亿 + | 阿里早期、京东、Square |
| OceanBase | 社区版开源 | MySQL/Oracle | Paxos | 是 | 100 亿 + | 蚂蚁、网商、工行 |
| TiDB | 是 | MySQL | Raft | 是 | 10 亿 + | 微众、中国人寿、PayPay |
| CockroachDB | BSL | PG | Raft | 是 | 1–10 亿 | DoorDash、Form3 |
| Oracle | 否 | Oracle | Data Guard | 是 | 10 亿 + | 传统银行核心 |
| TigerBeetle | 是 | 自定义 | VSR | 是 | 设计百万 TPS | 少量新系统 |
只要数据库在单机或单集群规模达到瓶颈,就必须分片。分片维度直接决定了事务代价、热点分布、索引成本、运维复杂度。
适用:面向 C 端的钱包、银行核心、证券经纪业务。
优点
缺点
适用:总账(General Ledger)、财务报表系统。
优点
缺点
适用:流水表、事件日志、审计日志。
优点
缺点
适用:B 端 SaaS、PaaS 支付平台,一个租户就是一个机构。
优点
缺点
实战中极少单维度,典型做法:
user_id(或
merchant_id),决定路由到哪个逻辑分片;以蚂蚁/支付宝早期的经验为例(公开资料可查):账户表按用户 ID hash 分 N 片(N 从 100 到 上千再到 OceanBase 之后不再可见),每片再按月分区。商户 / 渠道 / 平台账户单独放在”系统分片”里,和普通用户 C2C 路径走不同库。
真实数字不夸张地说:跨分片事务是单分片事务成本的 5–10 倍。原因:
应对策略:
一笔 C2C 转账可能涉及:
前两者是分布账户,后三者是集中账户 = 热点账户。一台机器哪怕 SSD、哪怕 InnoDB,行级锁争抢也会卡死在那几行上。
公开案例:Uber 2019 年 CNCF 演讲披露早期 MySQL 上 “platform fee account” 单行 QPS 瞬时过万,成为整个支付链路瓶颈;蚂蚁的”贷款平台总账”、支付宝的”手续费总账”都是同类问题的历史体现。
思路:把一个逻辑账户拆成 N 个物理桶(subaccount / shadow),写入时随机或哈希打散到某个桶,余额查询时对 N 个桶求和。
-- 原始(热点)
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'FEE-POOL';
-- 分桶后
-- 写入:对 merchant_id hash 到桶
UPDATE accounts_buckets
SET balance = balance + 100
WHERE account_id = 'FEE-POOL' AND bucket = hash(merchant_id) % 64;
-- 读取:聚合
SELECT SUM(balance) FROM accounts_buckets WHERE account_id = 'FEE-POOL';工程注意
SELECT balance FROM accounts WHERE account_id=...
改成聚合;思路:业务写入不直接打到热点账户行,先写入到”待合并队列”(一张流水表或 Kafka),定时(秒级)批处理合并一笔大额分录落到热点账户。
业务事件 → 写入 fee_pending 表(按 merchant 一行一笔,不争抢)
→ 每 N 秒或每 M 条 flush:
INSERT 一笔合并分录到 journal
UPDATE FEE-POOL 账户余额 += SUM(pending)
DELETE fee_pending
适用:手续费池、渠道归集、平台总账这类”可容忍秒级延迟”的账户。
代价:
fee_pending
不丢(WAL、持久化 MQ);思路:账户余额不是”现值”,而是”从创建到现在所有事件的累加”。写入就是 append 一条事件(几乎无锁),余额是一个视图(可物化)。
events: (account_id, seq, delta, ts, event_id)
PK: (account_id, seq)
写:INSERT, 按 account_id 自然分散
读余额:SUM(delta) WHERE account_id=...;或物化视图 / snapshot + 增量
优点:
缺点:
TigerBeetle 内部模型近似于这个思路。Stripe Ledger 的 postings 表也有强烈 append-only 色彩。
对超热账户,有些系统走”同步写分布账户 + 异步写热点账户”:
这是工程折中,必须配对账。否则”异步丢了几条”无人发现。
以上篇的 postings 表为例:
CREATE TABLE postings (
posting_id BIGINT PRIMARY KEY,
entry_id BIGINT NOT NULL,
account_id BIGINT NOT NULL,
direction SMALLINT NOT NULL, -- 1 借 / -1 贷
amount NUMERIC(38, 4) NOT NULL CHECK (amount > 0),
currency CHAR(3) NOT NULL,
effective_time TIMESTAMPTZ NOT NULL,
created_time TIMESTAMPTZ NOT NULL DEFAULT now(),
business_type SMALLINT NOT NULL,
business_id BIGINT NOT NULL
);最关键索引:
-- 账户流水查询(最高频)
CREATE INDEX idx_postings_account_time
ON postings (account_id, effective_time DESC)
INCLUDE (direction, amount, currency, entry_id, business_id);
-- entry 聚合(对账)
CREATE INDEX idx_postings_entry ON postings (entry_id);
-- 业务回溯
CREATE INDEX idx_postings_business ON postings (business_type, business_id);几个设计要点:
(account_id, effective_time DESC):账户明细倒序是最频繁的查询模式(“我最近
20 笔”),DESC 索引避免排序;PostgreSQL、SQL Server 支持 partial index,MySQL 目前只能通过生成列近似。典型场景:
-- 只对未入账的、待处理的分录建索引(占比小)
CREATE INDEX idx_postings_pending ON postings (created_time)
WHERE status = 'PENDING';
-- 大额分录专用索引(风控监控)
CREATE INDEX idx_postings_large ON postings (effective_time, account_id)
WHERE amount >= 1000000;Partial index 的优势:索引变小、更新代价低、大账户的冷数据不污染索引。
当表达到十亿级,分区变成必须。PostgreSQL 声明式分区:
CREATE TABLE postings (...) PARTITION BY RANGE (effective_time);
CREATE TABLE postings_2026_04 PARTITION OF postings
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE INDEX ON postings_2026_04 (account_id, effective_time DESC);要点:
(account_id, effective_time, posting_id)
作为主键,排好顺序提升局部性;AUTO_RANDOM 或 composite,避免单调递增写入造成
Region 热点;account_id hash 分区能天然打散;CREATE INDEX idx ON postings(account_id, entry_id, direction, amount, ...),看上去什么都能走,实际上维护成本高、选择性低;WHERE DATE(effective_time) = '2026-04-22'
会使索引失效,改为区间
WHERE effective_time >= '...' AND < '...';CONCURRENTLY,MySQL 要
ALGORITHM=INPLACE, LOCK=NONE,TiDB/OB
可以在线;┌─────────────────┐
│ 热层(Hot) │ 最近 1–2 年 OLTP 主库(OB/TiDB/PG)
│ 全索引、事务 │
├─────────────────┤
│ 温层(Warm) │ 2–7 年 列存副本 + 只读副本(TiFlash/OB 列存/PG 归档分区)
│ 报表、对账、审计│
├─────────────────┤
│ 冷层(Cold) │ 7–30 年 对象存储 + Iceberg + Parquet(S3/OSS)
│ WORM、审计取证 │
└─────────────────┘
account_id 分桶、按 effective_time
分区)→ Iceberg 注册;flowchart LR
A[OLTP 主库] -->|CDC/逻辑复制| B[温层列存]
A -->|分区 DETACH| C[归档区]
B -->|ETL 按月| D[Iceberg + Parquet<br/>S3 Object Lock]
D -->|Trino/Spark| E[审计/取证/报表]
B -->|只读副本| E
关键点:
-- 归档时先 detach 分区
ALTER TABLE postings DETACH PARTITION postings_2020_01 CONCURRENTLY;
-- 导出为 Parquet(COPY + 外部工具 / pg_parquet)
COPY postings_2020_01 TO PROGRAM 'aws s3 cp - s3://archive/postings/year=2020/month=01/data.parquet'
WITH (FORMAT parquet);
-- 校验无误后 DROP
DROP TABLE postings_2020_01;实务上建议用工具:
pg_partman 自动管理时间分区;pg_parquet / pg_duckdb
直接读写 Parquet;Debezium + Iceberg Sink 做实时
CDC 到冷层。PostgreSQL、MySQL InnoDB、Oracle、OceanBase、TiDB 都实现了 MVCC:读不阻塞写、写不阻塞读。账务读查询(余额、明细)几乎都在快照上跑,不持锁。
要点:
pg_stat_activity 里 xact_start
老的会让 dead tuple 无法回收;悲观(SELECT FOR UPDATE)
BEGIN;
SELECT balance FROM accounts WHERE account_id = ? FOR UPDATE;
-- 业务校验 balance >= amount
UPDATE accounts SET balance = balance - ? WHERE account_id = ?;
INSERT INTO postings (...) VALUES (...);
COMMIT;优点:语义直观,并发正确;缺点:热点账户锁等待严重。
乐观(版本号 / CAS)
UPDATE accounts
SET balance = balance - ?, version = version + 1
WHERE account_id = ? AND version = ? AND balance >= ?;
-- affected_rows = 0 → 重试或失败优点:无显式锁,并发高;缺点:冲突率高时全是重试浪费。
选择:分布账户(用户钱包)用悲观简单可靠;热点账户用分桶 + 乐观 CAS。
跨账户转账经典死锁:事务 A 锁账户 X 再锁 Y,事务 B 锁 Y 再锁 X。
防御:
min(from, to) 再锁
max(from, to);FOR UPDATE;innodb_lock_wait_timeout 设 3–5s;SHOW ENGINE INNODB STATUS、PG
pg_stat_activity + pg_locks。SERIALIZABLE 实际是快照隔离,小心 write
skew。即便有悲观锁,同一行的高并发更新是逃不掉的。应对就是前面第四节的热点方案——分桶、批量合并、事件溯源。
下一篇《幂等、事务与一致性》会专门展开。
真实数字不要乱编,但几个工业常识区间可以参考:
重要:TPC-C / TPC-E 是合成负载,和真实账务差异不小。上生产前必须用自己的 业务流量回放 做压测:
容量规划的一个经验公式:
峰值 TPS ≈ 日交易量 / 86400 * 峰谷比(3–10)
热点账户 QPS ≈ 峰值 TPS * 热点账户参与率(0.5–1.0)
单库上限 ≈ 单机 TPS * 0.6(保留 40% 冗余)
分片数 ≥ 峰值 TPS / 单库上限
ALTER TABLE postings
ADD CONSTRAINT chk_amount_positive CHECK (amount > 0),
ADD CONSTRAINT chk_direction CHECK (direction IN (-1, 1)),
ADD CONSTRAINT chk_currency_iso CHECK (currency ~ '^[A-Z]{3}$');
ALTER TABLE accounts
ADD CONSTRAINT chk_balance_nonneg
CHECK (balance >= 0 OR allow_negative = TRUE);CHECK 才真正生效;复式记账的最强约束:每一笔 entry,借方合计 = 贷方合计。可以用触发器或延迟约束:
-- PostgreSQL 延迟约束示例
CREATE OR REPLACE FUNCTION check_entry_balanced(eid BIGINT) RETURNS VOID AS $$
DECLARE net NUMERIC;
BEGIN
SELECT COALESCE(SUM(direction * amount), 0) INTO net
FROM postings WHERE entry_id = eid;
IF net <> 0 THEN
RAISE EXCEPTION 'Entry % not balanced: %', eid, net;
END IF;
END;
$$ LANGUAGE plpgsql;通过事务级触发器(或在服务层强制)保证每个 entry 提交前守恒。
支付事件 → 分录的映射通过
business_type + business_id
字段建立软外键(真正的外键跨表跨库难)。每一笔分录都能回到业务事件是审计的基石。
balance 列可以用生成列 +
窗口函数做派生(极少用,代价高);account_balances
表,事务内同步更新;SELECT SUM(direction * amount) FROM postings WHERE account_id = ?
对账验证;账务表必须禁止 UPDATE / DELETE(余额表例外,但余额是物化,可以重算):
postings 不授权
UPDATE/DELETE;reversal_of 指向原分录的反向 entry 体现。pgAudit
扩展;audit_log
插件(企业版)或 Percona Audit Plugin;审计日志要独立存储(不要和业务同库),保留与账务一致的年限。
对高合规要求(证券、跨境、数字资产)系统,除了 append-only 之外,还要能对外证明”这段历史没被偷改过”:
hash_i = H(posting_i || hash_{i-1})
每日终结时把 hash_N
写入独立存储(公证、区块链、OSS
WORM)。任何历史数据被改过,重算哈希会对不上。
工程实现:
“事件进账务之前的门”。任何要写账务的请求必须先过 BTG 记录:
ledger_requests;ledger_requests.id;BTG 本身也是 append-only + 哈希链,保护”账务请求”这一层。
公开资料(蚂蚁技术白皮书、阿里云栖大会演讲)显示的演进:
关键工程经验:
PayPal 公开分享过账务架构:
启发:不是所有场景都要”一个数据库解决所有”。热点读可以出库,主账本保守稳妥。
Stripe 工程博客(“Designing robust and predictable APIs with idempotency”、“Online migrations at scale”)披露:
公开演讲(QCon、SREcon)描述:
中国四大行、股份行在过去十年普遍走”核心系统分布式改造”路线:
监管侧信通院《分布式数据库发展报告》、银保监会《金融机构信息科技外包风险管理办法》为背景推动力。
一张标准的”两地三中心 + 异地异步 + 冷归档”账务集群:
WHERE DATE(ts)
、WHERE ts::DATE、函数包裹;CONCURRENTLY / 不
ALGORITHM=INPLACE:锁表几分钟业务全挂;FOR UPDATE NOWAIT
没兜底:重试风暴;| 场景 | 日分录规模 | 推荐 |
|---|---|---|
| 初创支付 / 单一国家 | < 1 亿 | PostgreSQL + 分区 + 同步副本 |
| 高速增长互联网金融 | 1–10 亿 | TiDB 或 MySQL 分库分表 |
| 大型支付 / 银行核心 | 10 亿+ | OceanBase / 国产金融级数据库 |
| 全球化跨境支付 | 跨境 | CockroachDB / 多集群 + 异步 |
| 加密 / 交易所内账 | 高并发 | TigerBeetle + 关系型主账本 |
| 存量传统银行 | 稳定 | Oracle + 分布式改造 |
账务数据库选型和设计的决策链条是这样的:
user_id /
merchant_id
为主,时间为辅,尽量让事务本地化;(account_id, time DESC)
+ 覆盖索引 + partial + 分区本地;下一篇我们进入幂等、事务与一致性,把”一笔请求进到账务服务”的全链路幂等、SAGA、TCC、对账补偿讲透。
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-04-22 · architecture / fintech
把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。
2026-04-22 · architecture / fintech
金融系统的可用性不是 SLA 表里的一个数字,而是人民银行、银保监、GB/T 20988 六级灾备、SOX、FFIEC 这些监管框架共同压出来的工程形态。本文从 RTO/RPO 的定义出发,走过单机→主备→同城双活→两地三中心→三地五中心→单元化(LDC/Set)的架构演进,拆解异地多活的数据同步、冲突处理与流量调度,配套混沌工程、全链路压测、应急预案与复盘文化,最后用 Go 写一个可运行的多活流量切换骨架。结合光大 8·16 乌龙指、2021 AWS us-east-1 故障、2020 工行手机银行故障、蚂蚁春节红包等公开案例展开。
2026-04-22 · architecture / fintech
金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。
2026-04-22 · architecture / fintech
在代码里正确地表示"一笔钱"远比看起来难。本文系统梳理金额的数值建模(浮点、定点、Decimal、最小单位)、币种标准(ISO 4217)、本地化显示、汇率换算与数据库存储,并给出 Go、Python、Java、Rust 的工程化示例。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。