返回金融系统设计
记账引擎 · 主设计文档

金融级复式记账引擎 — 架构设计文档

02-accounting-engine/design-note.md

金融级复式记账引擎 — 架构设计文档

文档版本: v1.0
作者: MomoFinance
日期: 2026-04-13
状态: Draft → Review
关联ADR: ADR-001 记账模型选择 / ADR-002 热点账户方案 / ADR-003 日终批处理架构


一、需求分析

1.1 面试场景还原

面试官: "请设计一个金融级复式记账引擎,支持多币种、多账户类型,日终结算,日均千万笔记账请求。"

1.2 需求澄清——10个关键问题

在动手设计之前,必须先通过需求澄清来收敛范围。以下是我会向面试官确认的10个问题:

#问题预设回答对设计的影响
1记账模式是复式还是单式?复式(Double-Entry)必须保证每笔凭证"借贷必相等",数据模型需要 Journal + Posting 两层
2需要支持哪些账户类型?资产/负债/所有者权益/收入/费用 五大类科目表需要层级设计,借贷方向规则不同
3多币种的范围?10+币种,需要本位币折算每笔分录需记录原币+本位币金额+汇率,汇兑损益自动处理
4日均交易量级别?千万笔/天,峰值约1万TPS必须考虑热点账户、分库分表、异步批量写入
5是否需要实时余额?是,但可接受最终一致性(秒级延迟)余额可用"缓存+异步刷新",不必每笔同步更新
6日终处理窗口?不超过2小时需要并行日终处理,按账户分片
7是否需要冲正/红冲?冲正不能物理删除,必须生成反向分录
8审计合规要求?数据保留7年+,不可篡改Append-only设计,Hash链,归档策略
9是否多法人/多账簿?初期单法人,后续扩展多法人预留 legal_entity_id 字段,科目表按法人隔离
10与上游系统的交互模式?同步接口+异步消息支付系统同步调用记账,记账完成后异步通知报表系统

1.3 功能需求

模块功能优先级
科目管理科目树CRUD、启用/停用、层级管理P0
开户创建账户、关联科目、设定币种P0
记账创建凭证、生成分录、过账、余额更新P0
冲正红冲(生成反向分录)、部分冲正P0
余额查询实时余额、可用余额、冻结余额、历史余额P0
日终结算结息计算、损益结转、余额快照、试算平衡P0
报表试算平衡表、科目余额表、明细账、总账P1
审计追溯操作日志、凭证链路、数据变更追踪P0

1.4 非功能需求

维度要求量化指标
一致性借贷必须严格平衡,余额不允许出错0容错
不可篡改已过账凭证不可修改,只能冲正Append-only
吞吐量支撑千万级日交易峰值 ≥ 10,000 TPS
时延单笔记账响应P99 < 50ms
日终窗口全量日终处理< 2小时
可用性核心记账服务99.99%(年停机 < 52min)
数据保留审计合规≥ 7年
可审计每笔操作可追溯完整操作链路

1.5 会计准则约束

记账引擎不是纯技术系统,必须遵循会计准则:

  1. 复式记账法(Double-Entry Bookkeeping): 每笔经济业务至少涉及两个账户,一借一贷
  2. 有借必有贷: 每笔凭证必须同时包含借方和贷方分录
  3. 借贷必相等: 同一凭证下 SUM(借方金额) = SUM(贷方金额)
  4. 会计恒等式: 资产 = 负债 + 所有者权益(任意时刻成立)
  5. 权责发生制: 收入和费用按实际发生时间入账
  6. 历史成本原则: 初始入账按交易时的实际金额

二、高层架构设计

2.1 C4 Context — 系统上下文

记账引擎是金融系统的"账本心脏",与以下外部系统交互:

外部系统交互方式数据流向说明
支付系统同步API支付→记账支付完成后触发记账,记账结果同步返回
信贷系统同步API信贷→记账放款/还款/结息触发记账
风控系统事件订阅记账→风控异常交易通知风控
财务报表系统异步消息+批量记账→报表日终推送余额快照和汇总数据
监管报送系统批量文件记账→监管按监管要求定时生成报送文件
审计系统查询API审计→记账审计人员查询凭证链路和操作日志

详见 diagrams/c4-context.mmd

2.2 C4 Container — 容器视图

记账引擎内部拆分为以下容器:

Container职责技术选型说明
API Gateway统一入口、鉴权、限流Nginx + Kong保护后端服务
Account Service账户生命周期管理Java/Spring Boot开户、冻结、销户
Chart of Accounts Service科目表管理Java/Spring Boot科目树CRUD,低频操作
Ledger Core核心记账引擎Java/Spring Boot凭证创建→分录→过账→余额更新
Journal Service凭证和分录存储Java/Spring Boot分录写入、查询
Balance Service余额计算与查询Java/Spring Boot + Redis实时余额、余额快照
Batch Engine日终批处理Spring Batch结息、损益结转、报表生成
Report Service报表生成与查询Java/Spring Boot试算平衡表、科目余额表
Audit Service审计日志记录与查询Java/Spring Boot + ES全操作链路追溯
Primary DB核心数据存储PostgreSQL (主)凭证、分录、余额
Cache热数据缓存Redis Cluster余额缓存、幂等键
Message Queue异步消息Apache Kafka记账事件、日终任务分发

详见 diagrams/c4-container.mmd

2.3 核心记账流程

调用方 → API Gateway → Ledger Core
  ├── 1. 幂等检查(Redis:idempotency_key 是否已存在)
  ├── 2. 科目校验(Chart of Accounts:科目是否有效、方向是否正确)
  ├── 3. 分录生成(根据记账模板生成借贷分录)
  ├── 4. 试算平衡(校验 SUM(DR) = SUM(CR))
  ├── 5. 过账(写入 journal_entry + posting 表,单库本地事务)
  ├── 6. 余额更新(同步更新 Redis 缓存,异步落库 balance_snapshot)
  ├── 7. 审计日志(异步写入 Kafka → Audit Service → ES)
  └── 8. 返回结果

详见 diagrams/sequence.mmd

关键设计点

  • 步骤5的"过账"使用单库本地事务,journal_entry 和 posting 在同一个数据库实例中,保证原子性
  • 步骤6的余额更新使用 Redis INCRBY 原子操作,保证并发安全
  • 步骤7的审计日志通过 Kafka 异步写入,不影响主流程性能

三、数据模型设计

3.1 核心表结构

chart_of_accounts(科目表)

字段类型说明
idBIGINT PK主键
codeVARCHAR(16)科目编码,如 1001.01.001
nameVARCHAR(128)科目名称
account_typeENUMASSET / LIABILITY / EQUITY / REVENUE / EXPENSE
levelINT层级 1-4
parent_codeVARCHAR(16)上级科目编码
normal_balanceENUMDEBIT / CREDIT(自然方向)
currencyVARCHAR(3)币种,NULL 表示不限
statusENUMACTIVE / DISABLED
created_atTIMESTAMP创建时间
updated_atTIMESTAMP更新时间

account(账户)

字段类型说明
idBIGINT PK主键
account_noVARCHAR(32) UK账户号(业务唯一键)
account_nameVARCHAR(128)账户名称
chart_codeVARCHAR(16) FK所属科目编码
account_typeENUMASSET / LIABILITY / EQUITY / REVENUE / EXPENSE
currencyVARCHAR(3)账户币种
statusENUMACTIVE / FROZEN / CLOSED
legal_entity_idVARCHAR(16)法人实体ID(预留多法人)
opened_atTIMESTAMP开户时间
closed_atTIMESTAMP销户时间
metadataJSONB扩展属性

journal_entry(记账凭证)

字段类型说明
idBIGINT PK主键
entry_noVARCHAR(32) UK凭证编号(业务唯一键)
idempotency_keyVARCHAR(64) UK幂等键
business_typeVARCHAR(32)业务类型(PAYMENT / LOAN / INTEREST / REVERSAL)
descriptionVARCHAR(256)摘要
statusENUMPENDING / VALIDATING / POSTED / REVERSED / FAILED
accounting_dateDATE会计日期(可能与交易日期不同)
transaction_timeTIMESTAMP交易时间
posted_atTIMESTAMP过账时间
operatorVARCHAR(64)操作人
source_systemVARCHAR(32)来源系统
source_refVARCHAR(64)来源单号
reversal_ofBIGINT FK被冲正的凭证ID(冲正时填写)
hashVARCHAR(64)凭证Hash(用于防篡改)
prev_hashVARCHAR(64)前一笔凭证Hash(Hash链)

posting(分录明细)

字段类型说明
idBIGINT PK主键
journal_entry_idBIGINT FK所属凭证ID
account_idBIGINT FK账户ID
directionENUMDEBIT / CREDIT
amountDECIMAL(20,4)原币金额
currencyVARCHAR(3)原币币种
base_amountDECIMAL(20,4)本位币金额
base_currencyVARCHAR(3)本位币(如 CNY)
exchange_rateDECIMAL(16,8)汇率
posting_seqINT分录序号

核心约束: 对同一个 journal_entry_id 下的所有 posting,必须满足:

SUM(CASE WHEN direction='DEBIT' THEN base_amount ELSE 0 END) 
= SUM(CASE WHEN direction='CREDIT' THEN base_amount ELSE 0 END)

balance_snapshot(余额快照)

字段类型说明
idBIGINT PK主键
account_idBIGINT FK账户ID
balance_typeENUMAVAILABLE / FROZEN / IN_TRANSIT / TOTAL
amountDECIMAL(20,4)余额金额
currencyVARCHAR(3)币种
snapshot_dateDATE快照日期
snapshot_timeTIMESTAMP快照时间
last_posting_idBIGINT最后一笔过账的posting ID

audit_log(审计日志)

字段类型说明
idBIGINT PK主键
event_typeVARCHAR(32)事件类型(ACCOUNT_OPEN / POSTING / REVERSAL / BALANCE_QUERY)
entity_typeVARCHAR(32)实体类型(JOURNAL / ACCOUNT / BALANCE)
entity_idVARCHAR(64)实体ID
actionVARCHAR(16)CREATE / UPDATE / QUERY
before_stateJSONB变更前状态
after_stateJSONB变更后状态
operatorVARCHAR(64)操作人
ip_addressVARCHAR(45)操作IP
timestampTIMESTAMP操作时间
trace_idVARCHAR(64)链路追踪ID

3.2 分库分表策略

分片键分片策略说明
journal_entryaccounting_date按月分表journal_entry_202604
postingjournal_entry_id (同库同表)随 journal_entry 分片保证凭证和分录在同一分片
balance_snapshotaccount_idHash取模(16片)按账户均匀分布
audit_logtimestamp按月分表冷数据定期归档到对象存储

关键原则:journal_entry 和 posting 必须在同一个数据库分片中,确保本地事务的原子性。


四、核心组件详细设计

4.1 科目体系管理

科目编码规则(四级)

大类(1位) - 中类(2位) - 小类(2位) - 明细(3位)
示例:
  1          资产
  1.01       流动资产
  1.01.01    银行存款
  1.01.01.001  工商银行人民币账户

  2          负债
  2.01       流动负债
  2.01.01    应付账款

  4          收入
  4.01       利息收入

科目自然方向

类型自然方向增加记减少记
资产借方(DEBIT)借方贷方
费用借方(DEBIT)借方贷方
负债贷方(CREDIT)贷方借方
权益贷方(CREDIT)贷方借方
收入贷方(CREDIT)贷方借方

4.2 复式记账核心

记账模板机制

为了减少上游系统的复杂度,Ledger Core 内置记账模板。上游只需传入业务类型和金额,引擎自动生成分录。

业务类型: PAYMENT_IN(收款入账)
模板:
  DR 1.01.01.XXX(银行存款)    金额
  CR 2.01.02.XXX(客户备付金)   金额

业务类型: LOAN_DISBURSEMENT(放款)
模板:
  DR 1.02.01.XXX(贷款资产)    金额
  CR 1.01.01.XXX(银行存款)    金额

业务类型: INTEREST_ACCRUAL(计提利息)
模板:
  DR 1.02.02.XXX(应收利息)    金额
  CR 4.01.01.XXX(利息收入)    金额

过账流程

  1. 接收记账请求(含 idempotency_key)
  2. 幂等检查:Redis SETNX idempotency_key
  3. 解析业务类型,匹配记账模板
  4. 生成凭证(journal_entry)和分录(posting[])
  5. 试算平衡校验:SUM(DR) == SUM(CR)
  6. 开启数据库事务:INSERT journal_entry + INSERT posting[]
  7. 更新凭证状态为 POSTED
  8. 更新 Redis 中的实时余额
  9. 发送 Kafka 消息通知下游
  10. 返回凭证编号

4.3 余额计算引擎

三类余额

类型计算公式用途
总余额所有已过账分录的净值账务查询、报表
可用余额总余额 - 冻结金额 - 在途金额业务判断(是否可支付)
冻结余额被风控/业务冻结的金额风控hold

实时余额 vs 快照余额

  • 实时余额:存储在 Redis 中,每次过账通过 INCRBY/DECRBY 原子更新
  • 快照余额:每日日终持久化到 balance_snapshot 表,用于历史查询和对账
  • 恢复机制:Redis 故障后,从最近快照 + 增量 posting 重算

4.4 热点账户解决方案

详见 ADR-002

问题:某些账户(如备付金户、过渡户)被所有交易共同操作,单一行锁成为瓶颈。

方案:缓冲记账(Buffer Posting) + 影子账户

缓冲记账

  1. 热点账户的分录不直接写入 posting 表
  2. 先写入 posting_buffer 缓冲表(无锁竞争)
  3. 定时任务(每5秒)将缓冲表中的分录按账户汇总,生成一笔汇总分录写入 posting 表
  4. 余额通过 Redis 实时更新(不受数据库锁影响)

影子账户

  1. 将热点账户拆分为 N 个影子子账户(如 10 个)
  2. 写入时 Hash 路由到不同子账户
  3. 查询余额时 SUM 所有子账户
  4. 日终合并到主账户

4.5 日终批处理

详见 ADR-003

日终任务链

Step 1: 切日(设定会计日期,停止当日新交易)
Step 2: 结息计算(按账户并行计算应计利息,生成计息凭证)
Step 3: 汇兑损益(多币种账户按日终汇率重估,生成损益凭证)
Step 4: 损益结转(将收入/费用类科目余额结转至本年利润)
Step 5: 试算平衡(全量校验 SUM(DR) = SUM(CR))
Step 6: 余额快照(持久化当日所有账户余额)
Step 7: 报表生成(科目余额表、试算平衡表)
Step 8: 数据归档(将 N 天前的明细数据归档到冷存储)

并行策略(MapReduce方式):

  • 按 account_id Hash 分为 M 个分片
  • 每个分片由独立 Worker 处理
  • 步骤 2/3/6 可按分片并行
  • 步骤 4/5/7 需要汇总结果,使用 Reduce 阶段

检查点与失败恢复

  • 每个 Step 完成后写入检查点(checkpoint 表)
  • 失败后从最后一个成功的检查点恢复重跑
  • 每个 Step 设计为幂等的,重复执行不会产生重复数据

4.6 多币种处理

原则:原币记账 + 本位币折算

例:收到 1000 USD,汇率 7.25
  DR 银行存款-USD    1000 USD (base: 7250 CNY, rate: 7.25)
  CR 客户备付金-USD  1000 USD (base: 7250 CNY, rate: 7.25)

汇率管理

  • 汇率表(exchange_rate)存储每日各币种对本位币汇率
  • 记账时使用交易时刻的汇率
  • 日终重估使用日终汇率

汇兑损益处理

  • 日终重估:比较原入账汇率和日终汇率,差额记入"汇兑损益"科目
  • 实现损益:实际兑换时,差额记入"已实现汇兑损益"
  • 未实现损益:持有外币资产的浮动盈亏,记入"未实现汇兑损益"

五、深度问题

5.1 性能优化

热点账户(已在4.4详述):缓冲记账 + 影子账户

批量记账优化

  • 支持 Batch API,一次提交多笔凭证
  • 内部使用 COPY 批量写入代替单条 INSERT
  • Pipeline Redis 操作,减少网络往返

读写分离

  • 写操作走主库
  • 余额查询、报表查询走只读副本
  • Redis 作为实时余额的第一层缓存

5.2 一致性保证

记账内部一致性

  • 凭证和分录在同一数据库分片,使用本地事务保证原子性
  • 试算平衡检查在事务提交前执行,不平衡则回滚

跨系统一致性

  • 采用"本地事务 + 消息队列"模式(Transactional Outbox)
  • 支付系统记账:支付服务写入本地事务表 → 发送记账请求 → 记账引擎过账 → 返回结果 → 支付服务更新状态
  • 如果记账失败,支付系统触发补偿(冲正/重试)
  • 引擎保证幂等性,相同 idempotency_key 不会重复记账

5.3 不可篡改设计

Append-Only

  • 所有表设计为只插入,不允许 UPDATE 或 DELETE
  • 已过账凭证状态只能变为 REVERSED(通过生成新的冲正凭证)
  • 数据库层面:使用 PostgreSQL Rule/Trigger 禁止 UPDATE/DELETE

Hash链

  • 每笔凭证计算 Hash = SHA256(entry_no + amount + prev_hash)
  • 前一笔凭证的 Hash 存入 prev_hash 字段
  • 形成类似区块链的链式结构,任何篡改会导致 Hash 链断裂
  • 定期校验 Hash 链完整性

数据归档

  • 超过 1 年的明细数据归档到对象存储(如 S3/OSS)
  • 归档数据保留 7 年以上
  • 归档前生成 Merkle Tree Root,存入归档索引表

5.4 日终处理优化

时间窗口优化

  • 增量计算:只处理当日有变动的账户(通过 Kafka 记录当日活跃账户列表)
  • 并行分片:按 account_id 分为 16-64 个分片并行处理
  • 预计算:结息公式在记账时预计算存入中间表,日终只做汇总

失败恢复

  • 每个 Step 结束写检查点,记录 Step 编号 + 处理进度 + 中间结果
  • 失败后自动从检查点恢复
  • 关键 Step 设计幂等性:使用 INSERT ... ON CONFLICT DO NOTHING

5.5 审计合规

完整操作链路

  • 每个操作生成 trace_id,贯穿从上游请求到最终过账的全链路
  • audit_log 记录操作前后状态(before_state / after_state)
  • 支持按 entity_id / operator / time_range 多维查询

数据保留

  • 热数据(1年内):主库 PostgreSQL
  • 温数据(1-3年):只读副本 / 分析库
  • 冷数据(3-7年+):对象存储,按需加载

监管报送

  • 日终生成监管报送文件(固定格式)
  • 支持按监管要求的时间粒度聚合数据
  • 保留完整的报送记录和回执

5.6 架构演进路径

阶段1(MVP): 单库、单法人、单币种
  → 验证核心记账逻辑正确性

阶段2: 多币种支持
  → 增加汇率管理、汇兑损益处理

阶段3: 分库分表
  → journal/posting 按月分表,balance 按账户分片

阶段4: 多法人
  → 科目表按法人隔离,支持合并报表

阶段5: 集团合并报表
  → 多法人余额汇总,内部交易抵消

六、架构决策摘要

ADR决策理由
ADR-001采用 Journal/Posting + Balance 模型符合复式记账原理,读写性能平衡
ADR-002缓冲记账 + 影子账户解决热点账户并发,不牺牲一致性
ADR-003MapReduce 并行日终处理可控复杂度,满足2小时窗口要求
-PostgreSQL 作为主存储ACID 事务强,JSONB 灵活,分区表成熟
-Redis 作为余额缓存原子操作保证并发安全,亚毫秒延迟
-Kafka 作为消息队列高吞吐、持久化、支持事件溯源
-Append-Only + Hash链满足审计不可篡改要求

七、面试口述版

2分钟版本

"这是一个金融级复式记账引擎。核心设计思路是三层分离:上层是API和业务适配层,中间是Ledger Core记账核心,底层是数据存储和缓存。

数据模型上采用经典的Journal-Posting-Balance三表结构:Journal是凭证头,Posting是分录明细,Balance是余额快照。每笔凭证必须满足借贷相等。

性能方面有三个关键设计:第一是热点账户用缓冲记账解决——先写缓冲表再汇总,避免单行锁竞争;第二是余额用Redis原子操作实时更新,不依赖数据库锁;第三是日终处理用MapReduce按账户分片并行处理,控制在2小时内。

一致性方面,凭证和分录用本地事务保证原子性,跨系统用Transactional Outbox模式。全链路设计为Append-Only加Hash链保证不可篡改。"

5分钟版本

在2分钟版本基础上补充:

"科目体系采用四级编码,区分资产/负债/权益/收入/费用五大类,每类有自然借贷方向。引擎内置记账模板,上游系统只需传业务类型和金额,引擎自动生成分录。

多币种处理采用原币记账加本位币折算,每条分录同时记录原币金额和按当时汇率折算的本位币金额。日终对外币账户做汇率重估,差额记入汇兑损益科目。

冲正采用红冲方式,不物理删除,而是生成一笔等额反向凭证。每笔凭证通过Hash链与前一笔关联,形成不可篡改的链式结构。

日终处理分8个Step执行:切日→结息→汇兑损益→损益结转→试算平衡→余额快照→报表→归档。每个Step有检查点,失败可从断点恢复。所有Step设计为幂等。

分库分表策略:journal和posting按会计月分表,balance按account_id Hash分片。关键约束是凭证和分录必须在同一分片保证本地事务。"

15分钟版本

在5分钟版本基础上展开每个组件的实现细节、异常处理、监控告警、容量规划,以及演进路径的每个阶段详细说明。具体参考本文档的第四、五章节。


八、自评与反思

设计优势

  1. 严格遵循会计准则:复式记账、借贷平衡、Append-Only,符合金融合规要求
  2. 性能与一致性平衡:缓冲记账解决热点问题,Redis保证实时性,本地事务保证原子性
  3. 可演进架构:从单库单法人到多法人合并报表,每个阶段边界清晰
  4. 运维友好:检查点恢复、幂等设计、Hash链校验、完整监控

设计取舍

  1. 没有使用Event Sourcing:虽然ES天然不可篡改,但实现复杂度高,余额查询需要重放事件,不适合高频余额查询场景
  2. 缓冲记账引入最终一致性:热点账户的余额在缓冲窗口内可能有秒级延迟,但通过Redis实时余额弥补
  3. 分库分表增加运维成本:但千万级TPS必须分片,这是必要的代价

可改进方向

  1. 引入 CQRS 模式分离读写模型,进一步优化查询性能
  2. 考虑 TiDB 等分布式数据库替代手动分库分表
  3. 引入 AI 异常检测,自动识别异常记账模式
  4. 添加实时风控集成,记账前检查账户风险状态