Database Architecture
ODTS-45: 数据库架构——15 个数据库背后的故事
目标读者:想知道 OTC 衍生品系统的所有数据存放在哪、数据如何流动、以及这为何重要的 BA/PM
数据来源:各项目的
application.yml、Flyway 迁移脚本、数据源配置、连接池设置一句话结论:这套系统经历了从”一个 Oracle 搞定一切”到”PostgreSQL 为主、Oracle 留守、达梦国产化、Redis/Kafka 辅助”的演进。背后是多数据源共存、CDC 数据同步、手动迁移向 Flyway 过渡的现实。
引言
衍生品系统本质上是一个数据加工厂:交易从对手方来,经过定价、风控、结算、报告四个环节,每个环节产生新的数据。理解了数据在哪、怎么走,就理解了系统的一半。
数据库全景
当前系统共涉及 6 种数据库引擎、15+ 个数据库实例:
┌────────────────────────────────────────────────────────────────┐
│ CICC OTC 数据库全景 │
│ │
│ Oracle PostgreSQL │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ODTSDB │ │ trans (OFA) │── 达梦国产化 │
│ │ (EDS 主库) │ │ finance │ │
│ │ npprod │ │ trade_dev │ │
│ │ (大宗商品) │ │ sentinel_dev │ │
│ └──────────────┘ │ tianyuan │ │
│ └──────────────┘ │
│ │
│ MySQL Redis │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ otcopt │ │ 会话缓存 │ │
│ │ linear-products│ │ OTC 订单流 │ │
│ └──────────────┘ │ 速率限制 │ │
│ └──────────────┘ │
└────────────────────────────────────────────────────────────────┘
按业务领域
| 业务领域 | 数据库 | 引擎 | 代表项目 |
|---|---|---|---|
| 衍生品运营(核心) | ODTSDB | Oracle | eds-web-app, odyssey-report-processing-service |
| 交易对手与合约 | ODTSDB | Oracle | eds-web-app, edsWeb |
| 大宗商品 | npprod | Oracle | CATS/commodity |
| 监管申报(主) | trans | PostgreSQL | ofareg/data-api |
| 监管申报(国产化) | 达梦 DM8 | 达梦 | ofareg/data-api |
| 行情数据 | otcopt | MySQL | ofareg/data-api |
| 线性产品 | linear-products | MySQL | odts-linear-web-1 |
| 股票投研 | finance | PostgreSQL | agquant-finance-backend |
| 交易记录 | trade_dev | PostgreSQL | agquant-trade-backend |
| 估值哨兵 | sentinel_dev | PostgreSQL | agquant-sentinel-backend |
| 仓位 Lens | positionlens_dev | PostgreSQL (PostGIS) | agquant-lens-backend |
| 用户画像 | profile_dev | PostgreSQL | agquant-profile-backend |
| 农场管理 | tianyuan | PostgreSQL | agquant-tianyuan-backend |
为什么需要 6 种数据库?
这背后的原因勾勒出一条清晰的演进路径:
- Oracle 是起点:2015 年系统基础层启动时(与
tradedesign协议中枢同期,早于主后端eds-web-app的 2016 年底),全公司标准就是 Oracle。ODTSDB 和 npprod 是那个时代的产物 - PostgreSQL 是迁移目标:从 OFA 监管申报(2019-2020)开始,新系统开始用 PostgreSQL。为什么?成本(Oracle 许可证贵)、生态(PostgreSQL 对开发者更友好)、性能(PG 对复杂查询的支持)
- 达梦是合规要求:国产化替代。达梦在 SQL 语法上与 Oracle 兼容,但仍需要独立的迁移路径
- MySQL 是”遗留依赖”:otcopt 和 linear-products 的 MySQL 不是主动选择——它们是历史上就存在的,因为成本原因没有迁移
- Redis 不是数据库:它是缓存和消息通道,但承担了订单流水等关键功能
- PostGIS 只是估值计算的工具:估值日需要对资产位置做地理空间分析
关键数据流
数据流 1:一笔交易的生命周期
交易员在 UI 下单
│
▼
通过 WebSocket 送入 IMS 网关
│
▼
写入 Oracle ODTSDB
├── EDS_CONTRACT → 合约主数据
├── EDS_TRADEACCOUNT → 交易账户
├── EDS_COUNTERPARTY → 对手方信息
└── EDS_INSTRUMENT → 标的物
│
├──→ ActiveMQ → hedging-as(定价引擎)
│
├──→ Kafka CDC (odts-cdc-EDS_CONTRACT) → 下游监听者
│
├──→ EOD 批处理 → odts-report-processing-service → 生成报告
│
└──→ OFA 监管申报 → data-api 从 Oracle 读取 → 写入 PostgreSQL trans
数据流 2:行情数据
Tushare / 金十数据 API
│
▼
agquant 的 data-pipeline(Python ETL)
│ ── 断点续传(_pipeline_checkpoints 表记录进度)
│ ── daily incremental load
▼
Finance PostgreSQL
├── stock_daily → 日线行情
├── fin_income → 利润表
├── fin_balance → 资产负债表
├── fin_cashflow → 现金流量表
├── news_flash → 新闻快讯
└── anns_d → 上市公司公告
│
▼
Finance REST API → 供 Trade/Sentinel/Profile 服务消费
数据流 3:CDC 数据同步
值得一提的架构决策——Oracle 的变更数据捕获(CDC):
Oracle ODTSDB 表的 DML 变更
│
▼
Debezium / 类似 CDC 工具
│
▼
Kafka Topic
├── odts-cdc-EDS_TRADEACCOUNT → 交易账户变更
├── odts-cdc-EDS_COUNTERPARTY → 对手方变更
├── odts-cdc-EDS_CONTRACT → 合约变更
└── odts-cdc-EDS_CASHMOVEMENT → 资金变动
│
▼
下游消费者(报告服务、缓存更新、监管申报)
为什么不是直接读 Oracle?
CDC 比直接 JDBC 查询有三个优势:
- 低延迟:数据变更在毫秒级被下游感知
- 低耦合:消费者不直接依赖 Oracle 的可用性
- 增量处理:无需全量扫描,只处理变更行
连接模式
IMS 时代的数据库连接(2015-2020)
旧版系统的数据库连接走的是 IMS 通信层:
前端 UI
│
▼ Protobuf
IMS Gateway (accessapp)
│
▼ IMS 协议
Oracle ODTSDB (192.168.163.195:1521/oracle)
数据库配置写在本地文件:
# eds-web-app/config/comm.properties
pool.database.url=jdbc:oracle:thin:@192.168.163.195:1521/oracle
pool.initCapacity=80
pool.maxCapacity=150
微服务时代的数据库连接(2022-现在)
新服务使用 Spring Boot 数据源配置,通过 Apollo 配置中心管理:
# odyssey-report-processing-service
spring.datasource:
primary:
jdbc-url: jdbc:oracle:thin:@pb-oracle-host:1521/pbdb
username: ${PB_DB_USER}
password: ${PB_DB_PASS}
eds:
jdbc-url: jdbc:oracle:thin:@eds-oracle-host:1521/edsdb
或通过环境变量:
# research/trade-backend application.yml
spring.datasource:
url: ${TRADE_DB_URL:jdbc:postgresql://localhost:5432/trade_dev}
username: ${TRADE_DB_USER:postgres}
password: ${TRADE_DB_PASS:postgres}
多数据源模式
odyssey-report-processing-service 同时连接 3 个 Oracle 数据源:
@Configuration
public class MultiDataSourceConfig {
@Bean @Primary
public DataSource dataSource() { /* 主数据源 */ }
@Bean("dataSourcePB")
public DataSource dataSourcePB() { /* PB 数据源 */ }
@Bean("dataSourceEDS")
public DataSource dataSourceEDS() { /* EDS 数据源 */ }
}
Schema 迁移
四代迁移方案
| 时代 | 方案 | 代表项目 | 风险 |
|---|---|---|---|
| 2015-2017 | 手动 PL/SQL 脚本 | edswd-app (eds-web-app/db/) | 无版本控制、无回滚 |
| 2017-2019 | Flyway V1(早期) | ofareg | 有版本管理,但未强制 |
| 2022-2024 | Flyway V2(标准) | research 全部后端 | 成熟,与 CI/CD 集成 |
| 2024-至今 | Flyway + 国产化 | ofareg (PG + 达梦双通道) | 跨数据库兼容性管理 |
最成熟的迁移模式(ofareg)
ofareg 的 Flyway 迁移目录结构展示了一个能支持多数据库的成熟方案:
ofareg/doc/sql/flyway/
├── common/
│ ├── V4.10.3.0.0__init.sql
│ ├── V4.11.7.0_1__modify.sql
│ └── R__init_data.sql ← 可重复迁移
├── pg/
│ └── V4.10.3.0.0__init_postgres.sql ← PostgreSQL 专用 DDL
└── dm/
└── V4.10.3.0.0__init_dameng.sql ← 达梦专用 DDL
common 目录中的迁移脚本在 PG 和 DM 上都执行,而 pg/ 和 dm/ 目录包含特定数据库的 DDL。Flyway 配置为:
flyway:
enabled: true
clean-disabled: true # 生产安全(不能 DROP)
baseline-on-migrate: true # 允许从非空数据库启动
table: flyway_schema_history # 跟踪表
locations:
- classpath:sql/flyway/common # 先执行通用部分
- classpath:sql/flyway/${db} # 再执行数据库特定部分
自定义迁移(odts-linear-web)
线性应用的迁移是一个纯 SQL 文件排序列:
app/migration/sqls/
├── 20231101.sql ← 创建用户表(REPLACE INTO)
├── 20231115.sql ← 用户权限表
└── 20231201.sql ← 角色表
没有 Flyway 的版本表——迁移脚本是幂等的(使用 REPLACE INTO 而不是 INSERT),所以即使重复执行也不会出错。
连接池对比
| 项目 | 连接池 | 最大连接数 | 特点 |
|---|---|---|---|
| eds-web-app(旧) | 自定义 pool.* | 150 | IMS 内置连接池,未使用连接池库 |
| ofareg(OFA) | Druid | 100 | 监控、慢 SQL 日志 |
| agquant 后端 | HikariCP | 5-10 | 轻量、快速、Spring Boot 默认 |
| data-pipeline(Python) | SQLAlchemy | 5 | 单进程、单数据库 |
连接池大小背后有业务逻辑:估值和风控服务用的连接数少(5-10),因为它们是 CPU-bound 的数学计算,不需要大量并发数据库操作。OFA 监管申报需要更多(100),因为它需要批量读取大量历史数据生成报表。
Redis 的三个角色
Redis 在整个系统中身兼三职:
1. 会话/令牌缓存
# odts1/config/redis-config.properties
redis.host=10.102.60.78:26379;10.102.60.79:26379;10.102.60.80:26379
redis.password=Yg5XSuC4QAZ25d
redis.database=0 # 会话数据
用于管理用户登录状态和 SSO 令牌。
2. 业务缓存
# IMS Redis
redis.database=1 # SBL 订单
redis.database=2 # DMA 订单
redis.database=3 # OTC 订单
不同数据库编号隔离不同类型的业务缓存——为什么不分多个 Redis 实例?成本原因。
3. 消息通道
OTC 订单/成交的实时推送依赖 Redis Streams(在某些场景替代 Kafka)。这是架构中的”轻量级消息方案”——在不需要 Kafka 那样的大规模吞吐和持久化保证时,用 Redis 更简单。
国产化(达梦数据库)
OFA 监管申报系统是系统中最早开始国产化改造的。达梦数据库(DM8)在语法上兼容 Oracle,但仍有一些差异:
| 差异点 | PostgreSQL | 达梦 DM8 |
|---|---|---|
| 序列 | SERIAL / IDENTITY | IDENTITY(Oracle 风格) |
| 分页 | LIMIT/OFFSET | SELECT TOP n / ROWNUM |
| 字符串连接 | string1 || string2 | 同 Oracle(||) |
| 数据类型 | TEXT, VARCHAR | VARCHAR2, CLOB |
| 日期函数 | NOW() | SYSDATE |
这就是为什么 ofareg 需要 common + pg/dm 双目录的迁移结构。
业务代价:数据库架构如何变成运营风险
硬编码 IP 的单点
IMS 时代(eds-web-app/config/comm.properties)把 Oracle 的 IP 直接写死:192.168.163.195:1521/oracle。这意味着:
风险:
→ 数据库 IP 不能变(一变就要改代码、重新打包、重启)
→ 没有连接故障转移(Oracle 挂了 = 整个交易台停摆)
→ 迁移到 PostgreSQL / 达梦时,每个写死 IP 的地方都是一处改动点
业务影响(估算):
→ 一次 Oracle 主库故障,如果持续 30 分钟,
交易台无法簿记新交易、EOD 跑不出来
→ 等同于 34 文档描述的 "eds-web-app 宕机" 场景:
交易员"盲交易",补录增加运营风险
连接池上限 150 的瓶颈
pool.maxCapacity=150 是 IMS 时代的设定。EOD 批处理(估值、风险、保证金几十个作业并发跑)在雪球爆发期(2022 年 2000+ 雪球)会瞬间打满连接池:
现象:
→ 150 个连接被 EOD 作业占满
→ 交易台实时查询拿不到连接 → 界面转圈
→ 或 EOD 作业互相阻塞 → 批处理超时(见 11 / 34 的雪球危机)
PM/BA 启示:
连接池不是一个"技术参数",它直接决定了
"交易量翻 3 倍时系统会不会垮"。
2016 年设的 150,到 2022 年才显得不够——
中间没有人回过头去重新评估这个 6 年前的数字。
6 种数据库的隐藏成本
45 文档列出了 Oracle / PostgreSQL / 达梦 / MySQL / Redis / PostGIS 六种。对 BA 而言,每一种都是:
→ 一份许可证成本(Oracle 最贵)
→ 一套运维知识(DBA 要懂 6 种)
→ 一条数据同步链路(跨库 CDC,见"数据流 3")
→ 一类合规证据(国产化要求达梦,要单独迁移 + 验证)
隐含的人日:
光是"把 Oracle 上的某个模块迁到达梦"这一件事,
在 45 文档的"国产化"一节里就涉及语法兼容、迁移路径、回归测试,
一个中等模块约 2-4 周。全公司 45+ 项目若都走一遍,
这是以"人年"计的工程。
跨系统数据目录
Oracle ODTSDB 核心表:
odts1/eds-web-app/config/db/EDS/V1.4/ → EDS 数据库初始化脚本
Oracle npprod 大宗商品:
odts1/CATS/commodity/config/ → 数据源配置
PostgreSQL trans(OFA 申报):
odyssey/ofareg/data-api/doc/sql/flyway/ → Flyway 迁移(pg + dm + common)
MySQL otcopt(行情):
odyssey/ofareg/data-api/ → otcopt 数据源配置
MySQL linear-products:
odyssey/odts-linear-web-1/app/migration/sqls/ → 自定义 SQL 迁移
PostgreSQL finance(行情 + 财务):
research/data-pipeline/schemas.py → 表结构定义(Python)
research/data-pipeline/postgres.py → ETL 加载逻辑
research/data-pipeline/migrations/ → 手工迁移脚本
PostgreSQL trade/sentinel/lens/profile:
research/*/order-quant/src/main/resources/db/migration/ → Flyway 迁移
PostgreSQL tianyuan:
research/tianyuan/order-quant/src/main/resources/db/migration/ → 52 个 Flyway 迁移
Redis 配置:
odts1/config/redis-config.properties → 旧版 Redis 哨兵配置
odyssey/*/config/ → 新服务 Redis 配置(Apollo 管理)
Kafka CDC 配置:
odts1/config/*.properties → CDC 主题配置