引子
上一篇《复式记账工程化》把账务模型落到了
accounts、journal_entries、postings
三张表。到这里,设计图纸画得再漂亮,也挡不住一个现实问题:这三张表放哪儿?
“放数据库”三个字在账务场景里远不是一行
CREATE TABLE
能解决的。一个成熟支付机构,日分录(postings)量级在亿到十亿级,单行写入必须带事务,读写延迟在
P99 十毫秒以内,还要满足
RPO=0(零数据丢失)、RTO<1min(故障一分钟切换)、审计数据保留七到十年、任何一行都不能被
UPDATE。你要扛住这些约束,数据库选型、分片策略、索引、归档、并发控制几乎都要重新推一遍。
这篇文章的读者画像是:
- 支付/交易/银行核心系统的架构师与 DBA;
- 正在把单库 MySQL/PostgreSQL 的账务库往分布式迁的工程团队;
- 想了解 OceanBase、TiDB、TigerBeetle 这些”金融级数据库”实际差异的技术决策者。
我们不会挨个讲 SQL 语法,重点在为什么选、怎么切、怎么扛热点、怎么归档、怎么对审计交差。文末会落到一份选型清单和一张可以直接抄的部署架构图。
一、金融账务的 SLA 框架
账务数据库的设计上限由业务 SLA(Service Level Agreement)决定。离开 SLA 谈”用什么数据库最好”是空中楼阁。
1.1 RPO 与 RTO
- RPO(Recovery Point Objective):故障发生时最多允许丢多少数据。账务场景基本要求 RPO=0,即任何一笔已确认的分录都不能丢。达成 RPO=0 的前提是同步复制(synchronous replication)或共识协议(Paxos/Raft)多数派确认。
- RTO(Recovery Objective):从故障发生到业务恢复的时间。支付、卡组织侧通常要求 RTO < 1 min,交易所撮合侧更严格(秒级)。
这两个指标直接排除了”异步主从 MySQL”、“跨可用区异步同步的 PostgreSQL 主从”这类方案在关键账务路径上的独立部署可能。要么叠加同步复制,要么直接上共识协议。
1.2 一致性与隔离级别
账务强一致来源于复式记账的约束:借贷必须同事务成功或同事务失败,余额视图必须反映最新分录。工程语境下至少需要:
- 事务原子性:一次 journal entry 的所有 postings 要么都写入,要么都回滚;
- 线性一致读:余额查询必须读到最新提交;这在分布式场景下不是默认行为,MySQL 主从异步复制的从库读会”走到过去”;
- 隔离级别:至少
READ COMMITTED,对扣款类幂等更新场景需要REPEATABLE READ或显式SELECT FOR UPDATE。Oracle/PostgreSQL 的SERIALIZABLE在热点账户上代价太高,一般不做默认级别。
1.3 审计、归档与合规
- WORM(Write Once Read Many):分录一旦落库就不能改,任何”更正”必须通过红冲分录(reversing entry)体现,数据库层面通过去 UPDATE/DELETE 权限、触发器保护、审计日志三层保障;
- 归档保留:中国《会计档案管理办法》要求会计凭证、账簿保留 30 年,银行业对明细账和交易流水要求 15 年以上;欧盟 PSD2、美国 SOX 对审计数据保留 7 年;
- 可追溯:任何一笔分录要能追到它背后的业务事件(订单、交易、风控决策),反向也要能从事件还原账务。
把以上组合起来,账务数据库实际要满足的”组合 SLA”是:
| 维度 | 账务数据库目标 |
|---|---|
| 写入吞吐 | 单集群 10k–100k TPS(分录行) |
| 读延迟 | P99 < 10 ms |
| 可用性 | 99.99%(年停机 < 52 分钟) |
| RPO | 0 |
| RTO | < 1 min |
| 保留年限 | 热数据 1–2 年、温 3–7 年、冷 10–30 年 |
| 一致性 | 线性一致、事务原子 |
| 审计 | append-only、不可篡改 |
1.4 不同业务形态的差异
不是所有账务都长一个样:
- 支付账务:高并发写、强事务、热点账户突出(商户、渠道、手续费池);
- 证券账务:中频写但要求与行情、撮合、清算在同一个”账户日”内严格对齐;
- 银行核心:写频低于支付侧但单笔金额大,容错要求极严;
- 交易所内账:极高并发、可接受短时异步持久化(但事务必须最终落地),撮合内存账 + 持久化分离。
后面选型要对照不同形态来看。
二、主流选型对比
把能选的都摆出来,再按成本、规模、案例筛。
2.1 PostgreSQL
优势
- 类型系统完备:
numeric精确十进制、uuid、jsonb、range类型,对金额建模友好(见《钱的建模》一篇对numeric(38, 18)的讨论); - 约束强:
CHECK、EXCLUDE、domain、generated columns 足以在 DDL 层编码余额守恒、币种一致等规则; - MVCC 成熟,
SERIALIZABLE实现为 SSI(Serializable Snapshot Isolation),正确性高; - 分区(partitioning)、逻辑复制(logical replication)、外部表(FDW)都是一等公民;
- 生态:TimescaleDB、Citus、CloudNativePG、PostGIS 等扩展丰富。
劣势
- 写扩展性受限于单机:主库写 TPS 通常在 10k–30k 量级;再往上要分库(Citus / 自研 shard router);
- 高可用靠外挂(Patroni、repmgr、CloudNativePG + etcd),切换逻辑需要自己保障;
- 缺乏原生的多主同步写,跨机房强一致需要外部共识。
适用规模:中小规模账务(日分录 < 1 亿)、B 端 SaaS、证券/基金/信托核心。典型用法是主 + 同步复制从 + 异地异步备 + 分区归档。
案例:Stripe 早期账务层核心表建在 PostgreSQL 上;Revolut 2020 年代初期全栈 PostgreSQL;国内不少持牌支付机构的备付金账务使用 PostgreSQL + Patroni 集群。
2.2 MySQL InnoDB
优势
- 运维生态最成熟,DBA 人才池最大;
- InnoDB 的行锁 + 聚簇索引对点查和小范围扫描友好;
- 分库分表中间件成熟(ShardingSphere、MyCat、DRDS、Vitess);
- 半同步复制(semi-sync)在多数派 ACK 后返回,可以近似 RPO=0。
劣势
- 数值类型历史包袱(
FLOAT/DOUBLE的误用)导致金额建模需要严格规范; - 事务嵌套、在线 DDL 能力逐步改善但仍弱于 PostgreSQL;
- 单机写扩展到瓶颈后,分库分表是必经之路,而分库分表的跨分片事务需要额外基础设施(XA、TCC、SAGA)。
适用规模:互联网支付、电商账务中等到大规模。阿里/蚂蚁、京东、美团、拼多多、字节跳动的账务体系都有长期在 MySQL 上深耕的历史。
典型演进路径:单机 MySQL → 一主多从 →
垂直拆分(按业务域)→ 水平分库分表(按
account_id / user_id hash)→
分布式数据库(OceanBase / TiDB)。
2.3 OceanBase
蚂蚁集团自研的分布式关系数据库,原生 Paxos 三副本、MySQL 协议兼容(也兼容部分 Oracle 语法),典型”金融核心场景”产品。
优势
- Paxos 多数派同步:三副本部署天然 RPO=0;
- 多租户:同一集群内不同租户资源隔离,账务租户、报表租户、风控租户各自为政;
- 分布式事务:支持跨分区(partition)两阶段提交,对账务的跨账户事务友好;
- HTAP:OLAP 列存(OceanBase Labs 的 OB 4.x 引入行列混存)对账单汇总、对账报表场景友好;
- MySQL 协议兼容:迁移改动相对小;
- 公开案例:蚂蚁集团 2014 年前后开始把交易 / 账务从 MySQL 分库分表迁到 OceanBase,2017 年双十一已承载核心链路;网商银行、中国工商银行核心、西安银行核心、建行部分场景公开案例可查。
劣势
- 商业产品,部署运维门槛高于开源 MySQL;
- 社区版(4.x 之后开源)生态仍在建设,第三方工具适配落后于 MySQL/PG;
- 部分 Oracle 兼容特性仅在企业版。
适用规模:日分录 10 亿 + 的超大规模账务、银行核心、证券账户体系。
2.4 TiDB
PingCAP 开源的分布式数据库,TiKV + TiDB Server + PD 三组件架构,Raft 多副本。
优势
- 完全开源,社区活跃;
- MySQL 协议兼容,迁移路径清晰;
- TiFlash 列存提供 HTAP 能力,账务 + 报表同一份数据;
- 自动分片(Region):开发者基本不用手工管理分片,PD 自动负载均衡;
- 开源案例:中国人寿、微众银行部分场景、平安银行、PayPay(日本)、SmartNews、Pinterest(部分场景)。
劣势
- 强一致来自 Raft 多数派,跨机房部署的延迟会直接进入写入路径;
- 单行热点场景(超热账户)需要配合业务侧打散;
- 乐观事务模式下大事务容易冲突回滚,账务场景应使用悲观事务(TiDB 默认自 5.0 起为悲观);
- 索引类型较 PostgreSQL 少(无 partial index,GIN/GiST 不可用)。
适用规模:日分录数亿到十亿级、有 HTAP 诉求、开源栈偏好。
2.5 CockroachDB
美国 Cockroach Labs 开源产品(自 2022 年起部分核心代码转为 CockroachDB Community License,非完全 OSI 开源),主打全球部署。
优势
- 原生 SQL + 跨地域多活,Raft + 范围分区(range partition);
- 兼容 PostgreSQL 协议;
- 地域亲和(follower reads、region-aware routing):适合”用户所在地读本地”场景。
劣势
- 单机性能较 MySQL/PG 单机弱;
- 国内部署案例少,生态支持有限;
- 许可证变化后部分商业使用需要付费。
适用规模:跨境支付、全球化 SaaS 账务、需要多地数据主权合规的场景。国际案例如 Bose、DoorDash、Form3(开放银行支付平台)。
2.6 Oracle
优势
- RAC + Data Guard + GoldenGate 的高可用与复制方案在传统银行被验证 20 年;
- PL/SQL 在存储过程、物化视图、分区、压缩、闪回上几乎是工业最强;
- ASM、AWR、优化器、诊断工具链极成熟。
劣势
- 许可证成本高(按核收费 + 选件包);
- Exadata、Engineered Systems 强绑定硬件;
- 在互联网支付级 TPS 上需要 RAC 多节点 + 大内存,规模越大性价比越低;
- 国内”去 O”趋势明显(工、建、招、平安、各地农信社),存量多于增量。
适用规模:传统银行核心、保险、清算机构。典型案例:中国人民银行第二代支付系统(CNAPS2)历史上依赖 Oracle + DB2,逐步向国产数据库迁移中。
2.7 专用账务数据库:TigerBeetle
一个非常有意思的路线:为复式记账专门设计的数据库,用 Zig 写,单表结构(accounts + transfers)、强一致(VSR/Viewstamped Replication)、单机百万 TPS 设计目标。
优势
- 数据模型与复式记账 1:1,开发者不用自己在通用数据库上搭
postings表; - 内置余额守恒检查、债务 / 债权双向计算;
- 固定模式让编译器能做极致优化;
- 有限状态机 + WAL,崩溃恢复行为清晰。
劣势
- 仅适用账务场景,报表、BI、审计检索还要走另一套存储;
- 年轻(2020 年后),生产案例仍在积累;
- 固定 schema 意味着自定义字段要走
user_data泛化字段,语义要自己约定。
适用规模:新建高性能账务、加密资产交易所内账、游戏内经济系统。
2.8 自研 Ledger:Stripe、Square、大厂内部账务
互联网金融公司普遍在通用数据库之上包一层”Ledger Service”:
- Stripe Ledger:在 PostgreSQL(早期)与自研分布式存储(后期)之上建 double-entry 服务;对外 API 即”create transaction / query balance”;
- Square/Block:MySQL 分库分表 + 自研 Subledger 服务;
- 蚂蚁集团”全域账务(OmniLedger / GTS)“:OceanBase + 分布式事务服务(GTS/DTX),上层业务不直接操作分录表,而是调用账务服务;
- Uber Ledger:长期 MySQL 分库 + Cassandra 归档,近年向 Docstore / 自研迁移。
这里的核心启发:数据库不是账务的全部,账务服务层负责”复式约束、幂等、热点拆解、审计落库”,数据库负责”存、查、事务、复制”。
2.9 一张选型对比表
| 数据库 | 开源 | 协议 | 副本协议 | 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 | 少量新系统 |
三、分片维度的选择
只要数据库在单机或单集群规模达到瓶颈,就必须分片。分片维度直接决定了事务代价、热点分布、索引成本、运维复杂度。
3.1 按用户(user_id / account_owner_id)分片
适用:面向 C 端的钱包、银行核心、证券经纪业务。
优点
- 同一用户的账户、分录、订单都落在同一分片,单用户操作是本地事务;
- 绝大多数查询(“我的账单”、“我的余额”)天然命中单片。
缺点
- 跨用户转账是跨分片事务(A 用户分片 → B 用户分片),需要 2PC / TCC / SAGA;
- 商户、平台、渠道这类”共享账户”天然不属于任何 C 端用户,需要额外”系统账户分片”;
- 用户活跃度差异大,热点用户(大 V、头部商户)会造成分片负载不均。
3.2 按科目(account_code)分片
适用:总账(General Ledger)、财务报表系统。
优点
- 报表查询按科目天然命中单片;
- 科目数量有限,分片规模可预测。
缺点
- 单一科目(如”手续费收入”、“用户钱包总额”)数据量巨大,分片不均;
- 一笔业务的借贷两侧通常涉及不同科目(用户应付 → 手续费收入),写入即跨分片;
- 不适合交易明细账。
3.3 按时间分片
适用:流水表、事件日志、审计日志。
优点
- 热数据集中在最近时间段,老数据自然沉到冷分片;
- 按日/月创建分区,归档、清理、压缩都方便;
- 配合 PostgreSQL 原生 partitioning 或 TiDB 的分区表表达力强。
缺点
- 写入集中在”当前分区”,当前分区压力大(典型的”尾写热点”);
- 跨时间查询需要分区裁剪优化,否则全量扫。
3.4 按机构(institution_id / merchant_id)分片
适用:B 端 SaaS、PaaS 支付平台,一个租户就是一个机构。
优点
- 多租户隔离清晰;
- 单租户事务本地化;
- 计费、配额、限流按分片粒度容易实现。
缺点
- 大租户 vs 小租户差异可能巨大(头部商户的流水是小商户的千倍);
- 跨机构业务(清算、分账、代发)天然跨分片。
3.5 组合维度与”分片键即路由”
实战中极少单维度,典型做法:
- 一级分片键:
user_id(或merchant_id),决定路由到哪个逻辑分片; - 二级分区:时间(按月),决定在该分片内的存储位置;
- 全局表:币种、科目表、机构表等小表,在所有分片复制(broadcast table)。
以蚂蚁/支付宝早期的经验为例(公开资料可查):账户表按用户 ID hash 分 N 片(N 从 100 到 上千再到 OceanBase 之后不再可见),每片再按月分区。商户 / 渠道 / 平台账户单独放在”系统分片”里,和普通用户 C2C 路径走不同库。
3.6 跨分片事务的代价
真实数字不夸张地说:跨分片事务是单分片事务成本的 5–10 倍。原因:
- 多段 RPC / 两阶段提交协议额外网络;
- 分布式锁持有时间变长,并发度下降;
- 故障模式增多(协调者故障、参与者故障),需要补偿;
- 监控、审计、幂等语义变复杂。
应对策略:
- 减少跨分片事务数量:能单分片就单分片。用户间转账走”入总账 + 异步反向分录”的 SAGA 模式,而非强一致 2PC(详见下一篇《幂等、事务与一致性》);
- 批处理合并:夜间对账、清算、结算这类批量跨分片操作走”批量 + 对账补偿”;
- 分布式事务服务:OceanBase、TiDB、GTS(蚂蚁)原生提供,用好它们就行,不要自己写 2PC。
3.7 分片演进的工程原则
- 分片键一旦确定,极难更换。上线前务必压测头部与长尾的数据倾斜;
- 分片数量取 2 的幂或质数:2 的幂便于一致性哈希扩容;
- 预留分片:上线时分片数 > 当前需要,比如未来 3 年的 2 倍容量;
- 路由层独立:ShardingSphere、Vitess 这类在应用和数据库间插一层,而不是把路由逻辑写在应用里;
- 分片映射元数据本身要高可用:路由信息放在 etcd / ZK / PD 里,不能丢。
四、热点账户:金融系统的”头号 BUG 制造机”
4.1 什么是热点账户
一笔 C2C 转账可能涉及:
- 付款人钱包账户(几千万个,分布均匀);
- 收款人钱包账户(同上);
- 平台手续费账户(只有一个,每笔都写);
- 备付金归集账户(每个渠道 1 个,每笔都写);
- 损益科目、暂存科目等(少数几个,高频写)。
前两者是分布账户,后三者是集中账户 = 热点账户。一台机器哪怕 SSD、哪怕 InnoDB,行级锁争抢也会卡死在那几行上。
公开案例:Uber 2019 年 CNCF 演讲披露早期 MySQL 上 “platform fee account” 单行 QPS 瞬时过万,成为整个支付链路瓶颈;蚂蚁的”贷款平台总账”、支付宝的”手续费总账”都是同类问题的历史体现。
4.2 Shadow Account(影子账户)/ 分桶
思路:把一个逻辑账户拆成 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';工程注意
- 桶数量要预估:过多浪费索引,过少仍然热点;典型 64 / 128 / 256 起步;
- 聚合查询有延迟代价,准实时余额场景要配合缓存;
- 对账要改:原本
SELECT balance FROM accounts WHERE account_id=...改成聚合; - 桶的归属要稳定(同一个 merchant 永远路由到同一个桶),否则对账历史接不上;
- 热点账户不止”写”,读也可能热(审计查询全平台手续费)。
4.3 批量合并(Batching)
思路:业务写入不直接打到热点账户行,先写入到”待合并队列”(一张流水表或 Kafka),定时(秒级)批处理合并一笔大额分录落到热点账户。
业务事件 → 写入 fee_pending 表(按 merchant 一行一笔,不争抢)
→ 每 N 秒或每 M 条 flush:
INSERT 一笔合并分录到 journal
UPDATE FEE-POOL 账户余额 += SUM(pending)
DELETE fee_pending
适用:手续费池、渠道归集、平台总账这类”可容忍秒级延迟”的账户。
代价:
- 余额准实时性下降,报表侧要清楚这一点;
- 合并窗口内系统故障需要保证
fee_pending不丢(WAL、持久化 MQ); - 合并事务本身变大,要做好分批。
4.4 事件溯源(Event Sourcing)
思路:账户余额不是”现值”,而是”从创建到现在所有事件的累加”。写入就是 append 一条事件(几乎无锁),余额是一个视图(可物化)。
events: (account_id, seq, delta, ts, event_id)
PK: (account_id, seq)
写:INSERT, 按 account_id 自然分散
读余额:SUM(delta) WHERE account_id=...;或物化视图 / snapshot + 增量
优点:
- 无行更新,纯 append,写入吞吐高;
- 天然审计:每个事件保留原因;
- 与 Kafka / Pulsar 等事件系统天然契合。
缺点:
- 余额计算需要快照 + 增量,冷账户首次查询慢;
- 业务侧的”余额校验 / 负余额禁止”变成读后写,需要乐观控制;
- 热点账户依然是(所有写都集中到 FEE-POOL),需要结合分桶。
TigerBeetle 内部模型近似于这个思路。Stripe Ledger 的 postings 表也有强烈 append-only 色彩。
4.5 异步双写与对账兜底
对超热账户,有些系统走”同步写分布账户 + 异步写热点账户”:
- 付款人扣款同步;
- 手续费池写 Kafka,后台消费器消费并合并写入热点账户;
- 日终对账核对”从分布账户流出的手续费总额” = “热点账户增量”。
这是工程折中,必须配对账。否则”异步丢了几条”无人发现。
五、索引设计
5.1 账务核心索引模式
以上篇的 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 索引避免排序;- INCLUDE 覆盖索引(PostgreSQL 11+、SQL Server、MySQL 8.0 generated columns 替代):让热查询在索引内完成,不回表;
- entry_id 索引:用于”一笔业务的所有借贷”聚合,对账必用;
- 业务回溯索引:风控、客服从业务 ID 反查分录。
5.2 Partial Index / Filtered Index
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 的优势:索引变小、更新代价低、大账户的冷数据不污染索引。
5.3 时间分区 + 本地索引
当表达到十亿级,分区变成必须。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);要点:
- 每个分区建各自本地索引,分区裁剪能直接跳过旧分区;
- 分区边界要和业务”会计期间”对齐(月 / 日),以便归档以分区为单位脱离主表;
- 分区过多(上千)本身是元数据压力,PostgreSQL 13+ 的 partition-wise join / prune 能好很多。
5.4 MySQL / TiDB / OceanBase 的差异
- MySQL InnoDB:主键即聚簇索引,建议把
(account_id, effective_time, posting_id)作为主键,排好顺序提升局部性; - TiDB:Region 自动切分,主键建议
AUTO_RANDOM或 composite,避免单调递增写入造成 Region 热点; - OceanBase:分区策略(hash/range/list/key)丰富,按
account_idhash 分区能天然打散; - PostgreSQL:非聚簇索引,CLUSTER 命令可以按索引物理重排,但在高写入下很少用;用 BRIN 索引可以给时间列加低成本索引。
5.5 索引反模式
- 宽列无差别索引:
CREATE INDEX idx ON postings(account_id, entry_id, direction, amount, ...),看上去什么都能走,实际上维护成本高、选择性低; - 包装字段索引:
WHERE DATE(effective_time) = '2026-04-22'会使索引失效,改为区间WHERE effective_time >= '...' AND < '...'; - 大表加索引不在线:PG 要
CONCURRENTLY,MySQL 要ALGORITHM=INPLACE, LOCK=NONE,TiDB/OB 可以在线; - JSON 字段索引:表达式索引或 GIN 可以,但成本要提前压测。
六、冷热分层与归档
6.1 三层数据分层模型
┌─────────────────┐
│ 热层(Hot) │ 最近 1–2 年 OLTP 主库(OB/TiDB/PG)
│ 全索引、事务 │
├─────────────────┤
│ 温层(Warm) │ 2–7 年 列存副本 + 只读副本(TiFlash/OB 列存/PG 归档分区)
│ 报表、对账、审计│
├─────────────────┤
│ 冷层(Cold) │ 7–30 年 对象存储 + Iceberg + Parquet(S3/OSS)
│ WORM、审计取证 │
└─────────────────┘
6.2 热层:Online
- 表分区粒度:月 / 周;
- 索引齐全,支持 P99 < 10ms 查询;
- 写入、事务、幂等校验都在这一层;
- 单表数据量控制:建议单分区 1 亿行以内,单表 10 亿以内。
6.3 温层:Near-line
- 数据迁出 OLTP 主节点,留在温存储(列存副本、OLAP 库);
- 对账、报表、风控历史回溯走这一层;
- 典型技术组合:
- TiDB 主 + TiFlash 列存副本;
- OceanBase 3.x 的 OLAP 节点 / 4.x 的行列混存;
- PostgreSQL 主 → 逻辑复制到 Greenplum / ClickHouse / StarRocks / Doris;
- MySQL → 同步到 Doris / StarRocks / Hudi。
- 温层查询接口和热层解耦:业务层有”历史查询”专属 API。
6.4 冷层:Archive
- 目标:长期保留、成本极低、不可篡改;
- 技术栈:S3 / OSS / COS 对象存储 + Apache Iceberg(或 Delta Lake / Hudi)+ Parquet;
- 写入路径:温层 → ETL → Parquet(按
account_id分桶、按effective_time分区)→ Iceberg 注册; - 查询:Trino / Spark / Doris / StarRocks 等引擎直接挂 Iceberg catalog;
- 合规保证:
- S3 / OSS 的 Object Lock / Retention 配置 WORM(Compliance 模式),在保留期内连 root 账号都无法删除;
- 配合 AWS GovCloud、阿里云金融云合规版。
6.5 归档迁移流程
flowchart LR
A[OLTP 主库] -->|CDC/逻辑复制| B[温层列存]
A -->|分区 DETACH| C[归档区]
B -->|ETL 按月| D[Iceberg + Parquet<br/>S3 Object Lock]
D -->|Trino/Spark| E[审计/取证/报表]
B -->|只读副本| E
关键点:
- 归档迁移是按分区粒度做的,不要按行做(代价差 1000 倍);
- 迁移完成后,冷层要有校验:行数、SUM(amount)、MAX(effective_time) 三项对齐;
- 冷层数据任何变更必须通过”红冲 + 新分录”体现,从不修改历史 Parquet 文件;
- 审计团队对冷层的访问走独立权限和只读通道。
6.6 PostgreSQL 归档的实战细节
-- 归档时先 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 到冷层。
七、并发控制
7.1 MVCC 基础
PostgreSQL、MySQL InnoDB、Oracle、OceanBase、TiDB 都实现了 MVCC:读不阻塞写、写不阻塞读。账务读查询(余额、明细)几乎都在快照上跑,不持锁。
要点:
- 快照点选择:事务开始时(REPEATABLE READ)或每个语句开始时(READ COMMITTED);
- 长事务会拖慢 VACUUM / GC:PostgreSQL 的
pg_stat_activity里xact_start老的会让 dead tuple 无法回收; - 快照读的一致性:同一事务内多次读同一余额,不会”闪变”。
7.2 扣款路径:悲观 vs 乐观
悲观(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。
7.3 死锁避免
跨账户转账经典死锁:事务 A 锁账户 X 再锁 Y,事务 B 锁 Y 再锁 X。
防御:
- 账户 ID 排序加锁:永远先锁
min(from, to)再锁max(from, to); - 统一事务模板:账务服务层做这件事,业务方不接触
FOR UPDATE; - 超时短:MySQL
innodb_lock_wait_timeout设 3–5s; - 监控死锁:MySQL
SHOW ENGINE INNODB STATUS、PGpg_stat_activity+pg_locks。
7.4 隔离级别陷阱
- READ COMMITTED:同一事务内两次读同一余额可能不同;业务要注意;
- REPEATABLE READ(MySQL 默认):写冲突以 gap lock 体现,可能阻塞插入;
- SERIALIZABLE:PG 的 SSI 正确性最高但有回退成本,热点场景不推荐全局用;
- Snapshot:Oracle 的
SERIALIZABLE实际是快照隔离,小心 write skew。
7.5 行级锁热点
即便有悲观锁,同一行的高并发更新是逃不掉的。应对就是前面第四节的热点方案——分桶、批量合并、事件溯源。
7.6 分布式事务
- 2PC:OceanBase / TiDB / CRDB 原生支持,延迟代价在 10ms 级;
- TCC(Try-Confirm-Cancel):业务层补偿,适合跨库跨服务;
- SAGA:长事务链式补偿,适合跨域流程;
- XA:MySQL 支持但运维代价高,生产很少用。
下一篇《幂等、事务与一致性》会专门展开。
八、压测与容量规划
真实数字不要乱编,但几个工业常识区间可以参考:
- 单机 PostgreSQL(SSD、64c、256G):TPC-C 12k–25k TPMC;金融账务负载(小事务、索引命中)10k–30k TPS;
- 单机 MySQL InnoDB:同配置,TPC-C 15k–30k TPMC;
- OceanBase:2019 年蚂蚁 TPC-C 官方结果 707 万 tpmC(经过审计);实际金融生产集群单租户十万级 TPS;
- TiDB:单集群可以扩到数十万 TPS,公开报告参考 PingCAP TiDB Benchmark;
- CockroachDB:公开 TPC-C 百万 warehouses 级;
- TigerBeetle:设计目标单实例 1M TPS transfers,实测数据参考其官方 benchmark。
重要:TPC-C / TPC-E 是合成负载,和真实账务差异不小。上生产前必须用自己的 业务流量回放 做压测:
- 从生产采样最近 7 天流水;
- 按账户 ID 重放(保证热点账户比例正确);
- 包含读写比(账务侧典型 7:3,一次写配几次流水读);
- 跨分片比例不能丢:典型 5%–15% 的事务跨分片;
- 灰度切流:10% → 30% → 100%,每步有回退开关。
容量规划的一个经验公式:
峰值 TPS ≈ 日交易量 / 86400 * 峰谷比(3–10)
热点账户 QPS ≈ 峰值 TPS * 热点账户参与率(0.5–1.0)
单库上限 ≈ 单机 TPS * 0.6(保留 40% 冗余)
分片数 ≥ 峰值 TPS / 单库上限
九、数据完整性:让数据库帮你兜底
9.1 Check 约束
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);- MySQL 8.0 起
CHECK才真正生效; - TiDB、OceanBase 支持 CHECK;
- OceanBase 的”余额不允许为负”场景还可以配合”授信额度”字段。
9.2 余额守恒约束
复式记账的最强约束:每一笔 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 提交前守恒。
9.3 外键与事件日志
支付事件 → 分录的映射通过
business_type + business_id
字段建立软外键(真正的外键跨表跨库难)。每一笔分录都能回到业务事件是审计的基石。
9.4 生成列与物化视图
balance列可以用生成列 + 窗口函数做派生(极少用,代价高);- 账户余额普遍走物化:
- 单独的
account_balances表,事务内同步更新; - 定时
reconcile:
SELECT SUM(direction * amount) FROM postings WHERE account_id = ?对账验证; - 如果不等立即报警。
- 单独的
十、审计、WORM 与哈希链
十.1 Append-Only 是底线
账务表必须禁止 UPDATE / DELETE(余额表例外,但余额是物化,可以重算):
- 应用层:DAO 层严禁写 UPDATE/DELETE SQL;
- 数据库层:对
postings不授权 UPDATE/DELETE; - 只允许 INSERT + 红冲:任何更正通过新插入一笔
reversal_of指向原分录的反向 entry 体现。
十.2 数据库内审计日志
- PostgreSQL:
pgAudit扩展; - MySQL:
audit_log插件(企业版)或Percona Audit Plugin; - Oracle:Unified Audit;
- OceanBase:内置审计,可开启 SQL 级审计到独立表;
- TiDB:企业版审计 / 开源侧靠 general log + 处理。
审计日志要独立存储(不要和业务同库),保留与账务一致的年限。
十.3 哈希链 / Merkle 日志
对高合规要求(证券、跨境、数字资产)系统,除了 append-only 之外,还要能对外证明”这段历史没被偷改过”:
hash_i = H(posting_i || hash_{i-1})
每日终结时把 hash_N
写入独立存储(公证、区块链、OSS
WORM)。任何历史数据被改过,重算哈希会对不上。
工程实现:
- 分片内部各自哈希链;
- 日终聚合生成”账务 Merkle root”;
- 可选:上链到联盟链(长安链、蚂蚁链、Hyperledger Fabric)做第三方存证。
10.4 BTG / Before-The-Gate
“事件进账务之前的门”。任何要写账务的请求必须先过 BTG 记录:
- 请求元数据(来源、业务 ID、签名、版本)落到
ledger_requests; - 账务服务内部幂等键 =
ledger_requests.id; - 账务处理失败 / 超时 / 回滚都可以从 BTG 重放。
BTG 本身也是 append-only + 哈希链,保护”账务请求”这一层。
十一、真实案例演进
11.1 蚂蚁集团:从 MySQL 分库分表到 OceanBase
公开资料(蚂蚁技术白皮书、阿里云栖大会演讲)显示的演进:
- 2004–2010:Oracle 为主,支付宝核心交易、账务在 Oracle RAC;
- 2010–2013:推行”去 IOE”,账务逐步迁 MySQL + 分库分表(自研 TDDL / DRDS),单元化架构落地;
- 2014 起:OceanBase 承接交易账务,2017 年双十一完全支撑核心交易;
- 2019:TPC-C 707 万 tpmC(官方审计);
- 2020 后:OceanBase 4.x 走向 HTAP,开始承接分析型场景。
关键工程经验:
- 账务服务化:业务不直接操作数据库,走”账务中心”服务;
- 单元化:同一用户的账户、订单、分录在同一单元;跨单元弹性切换;
- 分布式事务 GTS:上层统一调用,底层 2PC + 时钟;
- 三地五中心:RPO=0、RTO 分钟级;
- 对账是工程的一半:任何两条链路之间日终对账是硬要求。
11.2 PayPal:Couchbase + Oracle
PayPal 公开分享过账务架构:
- Oracle 承担主账本:长期稳定、运维成熟;
- Couchbase 承担高并发缓存与只读副本:手续费池、热点账户的读流量分摊;
- Kafka 作为事件总线:账务变更事件分发到风控、报表、对账。
启发:不是所有场景都要”一个数据库解决所有”。热点读可以出库,主账本保守稳妥。
11.3 Stripe:自建 Ledger
Stripe 工程博客(“Designing robust and predictable APIs with idempotency”、“Online migrations at scale”)披露:
- 早期 PostgreSQL 做账本;
- 自建 Ledger 服务封装 double-entry 语义;
- 演进出分布式 Ledger,支持全球多法域、多币种;
- 核心原则:账务 API 幂等、append-only、每条记录可追到源事件。
11.4 Square / Block:MySQL 分库 + 自研 Subledger
公开演讲(QCon、SREcon)描述:
- MySQL 分库分表做主账;
- Subledger 层做业务账 → 总账映射;
- Reconciliation Service 日终对账、差错处理;
- 逐步把部分业务迁到分布式数据库(2023 后引入 Vitess)。
11.5 传统银行:Oracle / DB2 + 分布式改造
中国四大行、股份行在过去十年普遍走”核心系统分布式改造”路线:
- 工行:ICBC-M 核心 + OceanBase / GaussDB;
- 建行、中行:部分核心走 TDSQL / GaussDB;
- 招行:Oracle 长期稳定 + 部分 TiDB 试点;
- 交行、中信:OceanBase 金融级落地。
监管侧信通院《分布式数据库发展报告》、银保监会《金融机构信息科技外包风险管理办法》为背景推动力。
十二、部署架构图
一张标准的”两地三中心 + 异地异步 + 冷归档”账务集群:
十三、工程坑点
- 分片键拍脑袋:上线才发现 80% 流量集中在 20% 分片;
- 跨分片事务滥用:业务随手写跨库 join,性能塌方;
- 自增主键:分布式下热点 tail write;改 Snowflake / AUTO_RANDOM;
- CHAR vs VARCHAR vs NUMERIC 不统一:金额精度丢失灾难;
- 长事务:批处理跑了 30 分钟阻塞 VACUUM;
- 索引失效:
WHERE DATE(ts)、WHERE ts::DATE、函数包裹; - 只建主库索引:从库回放慢导致复制延迟;
- 在线加索引不
CONCURRENTLY/ 不ALGORITHM=INPLACE:锁表几分钟业务全挂; FOR UPDATE NOWAIT没兜底:重试风暴;- 余额物化表不对账:长期小差错滚成大事故;
- DDL 变更没评审:删字段、改类型直接上生产;
- 备份恢复没演练:真出事才发现备份没校验;
- 审计日志和业务同库:一起坏一起丢;
- 冷归档没有校验:迁移程序 bug,几百万行静悄悄丢了;
- 依赖云厂商单一区域:跨 region 切换演练缺失。
十四、选型与落地清单
14.1 选型建议表
| 场景 | 日分录规模 | 推荐 |
|---|---|---|
| 初创支付 / 单一国家 | < 1 亿 | PostgreSQL + 分区 + 同步副本 |
| 高速增长互联网金融 | 1–10 亿 | TiDB 或 MySQL 分库分表 |
| 大型支付 / 银行核心 | 10 亿+ | OceanBase / 国产金融级数据库 |
| 全球化跨境支付 | 跨境 | CockroachDB / 多集群 + 异步 |
| 加密 / 交易所内账 | 高并发 | TigerBeetle + 关系型主账本 |
| 存量传统银行 | 稳定 | Oracle + 分布式改造 |
14.2 落地 Checklist
十五、本文小结
账务数据库选型和设计的决策链条是这样的:
- SLA 先行:RPO=0、RTO<1min、保留年限决定副本协议与存储分层;
- 模型锁定:复式记账的 append-only 语义是底线;
- 数据库挑选:看规模、看生态、看案例;PostgreSQL 进、MySQL 分、OB/TiDB 扛大规模、TigerBeetle 特种;
- 分片维度:以
user_id/merchant_id为主,时间为辅,尽量让事务本地化; - 热点账户:分桶 + 合并 + 事件溯源,几乎一定要做;
- 索引:
(account_id, time DESC)+ 覆盖索引 + partial + 分区本地; - 并发:分布账户用悲观锁,热点账户用分桶 + 乐观 CAS;
- 分层:热 OLTP + 温列存 + 冷 Iceberg WORM;
- 审计:append-only + DB audit + 哈希链 + 第三方存证;
- 演练:切换、恢复、归档、红冲每季度跑一次。
下一篇我们进入幂等、事务与一致性,把”一笔请求进到账务服务”的全链路幂等、SAGA、TCC、对账补偿讲透。
参考资料
- OceanBase 官方文档与技术白皮书:https://www.oceanbase.com/docs
- TiDB 官方文档:https://docs.pingcap.com/tidb/stable
- PostgreSQL 官方文档 · Partitioning:https://www.postgresql.org/docs/current/ddl-partitioning.html
- CockroachDB 官方文档:https://www.cockroachlabs.com/docs/
- TigerBeetle 设计文档:https://docs.tigerbeetle.com
- Stripe Engineering Blog:“Online migrations at scale”、“Idempotency and correctness at Stripe”:https://stripe.com/blog/engineering
- Apache Iceberg 官方:https://iceberg.apache.org/
- TPC-C / TPC-E Benchmark:http://www.tpc.org/
- 信通院《分布式数据库发展研究报告》(历年):http://www.caict.ac.cn/
- 中国人民银行《支付系统发展报告》:http://www.pbc.gov.cn/
- Uber Engineering:“Building Reliable Reprocessing and Dead Letter Queues with Apache Kafka”、Ledger 架构分享:https://www.uber.com/blog/
- PayPal Engineering:https://medium.com/paypal-tech
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【金融科技工程】复式记账工程化:科目、分录、余额、对账
把 500 年历史的复式记账翻译成工程师可以落地的数据模型、SQL 表结构与余额计算策略,覆盖充值、下单、退款、分润、红包、多币种与冲销的真实场景,并对比 TigerBeetle、beancount、Ledger CLI、Square LedgerDB、Stripe Ledger 等开源与工业实现。
【金融科技工程】金融级可靠性:两地三中心、单元化、RPO/RTO、灰度
金融系统的可用性不是 SLA 表里的一个数字,而是人民银行、银保监、GB/T 20988 六级灾备、SOX、FFIEC 这些监管框架共同压出来的工程形态。本文从 RTO/RPO 的定义出发,走过单机→主备→同城双活→两地三中心→三地五中心→单元化(LDC/Set)的架构演进,拆解异地多活的数据同步、冲突处理与流量调度,配套混沌工程、全链路压测、应急预案与复盘文化,最后用 Go 写一个可运行的多活流量切换骨架。结合光大 8·16 乌龙指、2021 AWS us-east-1 故障、2020 工行手机银行故障、蚂蚁春节红包等公开案例展开。
【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图
金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。
【金融科技工程】钱的建模:金额精度、币种、会计单位、多语言金额
在代码里正确地表示"一笔钱"远比看起来难。本文系统梳理金额的数值建模(浮点、定点、Decimal、最小单位)、币种标准(ISO 4217)、本地化显示、汇率换算与数据库存储,并给出 Go、Python、Java、Rust 的工程化示例。