存储

数据库 Schema 设计

完整的数据库表结构设计,涵盖 PostgreSQL 订单/成交/账户表、TimescaleDB K 线存储、Redis 热数据缓存的详细 Schema 与索引策略。

1. ER 关系图

2. 订单表 (orders)

CREATE TABLE orders (
    -- 主键 & 标识
    order_id          BIGINT PRIMARY KEY,           -- Snowflake ID
    client_order_id   VARCHAR(64) NOT NULL,
    user_id           BIGINT NOT NULL,
    account_id        BIGINT NOT NULL,

    -- 交易对
    symbol            VARCHAR(20) NOT NULL,          -- e.g. 'BTCUSDT'
    base_asset        VARCHAR(10) NOT NULL,          -- e.g. 'BTC'
    quote_asset       VARCHAR(10) NOT NULL,          -- e.g. 'USDT'

    -- 订单参数
    side              VARCHAR(4) NOT NULL,           -- 'BUY' / 'SELL'
    order_type        VARCHAR(16) NOT NULL,          -- 'LIMIT','MARKET','STOP_LIMIT','STOP_MARKET'
    time_in_force     VARCHAR(4) DEFAULT 'GTC',      -- 'GTC','IOC','FOK','GTT'
    price             DECIMAL(32,16) DEFAULT 0,      -- 限价价格
    stop_price        DECIMAL(32,16) DEFAULT 0,      -- 止损触发价
    quantity          DECIMAL(32,16) NOT NULL,        -- 委托数量
    quote_order_qty   DECIMAL(32,16) DEFAULT 0,      -- 按金额下单

    -- 标志位
    post_only         BOOLEAN DEFAULT FALSE,
    reduce_only       BOOLEAN DEFAULT FALSE,
    is_liquidation    BOOLEAN DEFAULT FALSE,

    -- 成交状态
    status            VARCHAR(20) NOT NULL DEFAULT 'PENDING_NEW',
    executed_qty      DECIMAL(32,16) DEFAULT 0,      -- 累计成交量
    cumulative_quote  DECIMAL(32,16) DEFAULT 0,      -- 累计成交额
    avg_price         DECIMAL(32,16) DEFAULT 0,      -- 平均成交价
    remaining_qty     DECIMAL(32,16) DEFAULT 0,      -- 剩余量

    -- 手续费
    total_fee         DECIMAL(32,16) DEFAULT 0,
    fee_asset         VARCHAR(10),

    -- 杠杆
    leverage          SMALLINT DEFAULT 1,
    margin_type       VARCHAR(10) DEFAULT 'CROSS',   -- 'CROSS' / 'ISOLATED'
    frozen_margin     DECIMAL(32,16) DEFAULT 0,

    -- 时间
    created_at        TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    expire_at         TIMESTAMP WITH TIME ZONE,

    -- 版本控制
    version           INT DEFAULT 1,

    -- 唯一约束
    CONSTRAINT uq_client_order UNIQUE (user_id, client_order_id)
);

-- 状态检查
ALTER TABLE orders ADD CONSTRAINT chk_status
    CHECK (status IN (
        'PENDING_NEW','NEW','PARTIALLY_FILLED','FILLED',
        'PENDING_CANCEL','CANCELED','REJECTED','EXPIRED'
));
ALTER TABLE orders ADD CONSTRAINT chk_side
    CHECK (side IN ('BUY','SELL'));

3. 成交表 (trades)

CREATE TABLE trades (
    trade_id          BIGINT PRIMARY KEY,            -- 全局唯一成交 ID
    symbol            VARCHAR(20) NOT NULL,
    price             DECIMAL(32,16) NOT NULL,       -- 成交价格
    quantity          DECIMAL(32,16) NOT NULL,       -- 成交数量
    quote_qty         DECIMAL(32,16) NOT NULL,       -- 成交额

    -- 买卖双方
    buy_order_id      BIGINT NOT NULL REFERENCES orders(order_id),
    buy_user_id       BIGINT NOT NULL,
    sell_order_id     BIGINT NOT NULL REFERENCES orders(order_id),
    sell_user_id      BIGINT NOT NULL,

    -- 角色
    buyer_is_maker    BOOLEAN NOT NULL,

    -- 手续费
    buy_fee           DECIMAL(32,16) DEFAULT 0,
    buy_fee_asset     VARCHAR(10),
    sell_fee          DECIMAL(32,16) DEFAULT 0,
    sell_fee_asset    VARCHAR(10),

    -- 时间
    trade_time        TIMESTAMP WITH TIME ZONE NOT NULL,
    sequence_id       BIGINT NOT NULL,                -- 撮合序列号

    -- 索引
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

4. 账户与余额表

-- 账户表
CREATE TABLE accounts (
    id                BIGINT PRIMARY KEY,
    user_id           BIGINT NOT NULL,
    account_type      VARCHAR(10) NOT NULL DEFAULT 'SPOT',
                      -- 'SPOT' / 'MARGIN' / 'FUTURES'
    status            VARCHAR(10) DEFAULT 'ACTIVE',
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    CONSTRAINT uq_user_type UNIQUE (user_id, account_type)
);

-- 余额表(每个账户每种资产一行)
CREATE TABLE balances (
    id                BIGINT PRIMARY KEY,
    account_id        BIGINT NOT NULL REFERENCES accounts(id),
    asset             VARCHAR(10) NOT NULL,           -- 'BTC','USDT','ETH'
    available         DECIMAL(32,16) DEFAULT 0,       -- 可用余额
    frozen            DECIMAL(32,16) DEFAULT 0,       -- 冻结金额
    total             DECIMAL(32,16) GENERATED ALWAYS AS (available + frozen) STORED,
    version           INT DEFAULT 1,                  -- 乐观锁
    updated_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    CONSTRAINT uq_account_asset UNIQUE (account_id, asset),
    CONSTRAINT chk_available CHECK (available >= 0),
    CONSTRAINT chk_frozen CHECK (frozen >= 0)
);

余额操作原子性

-- 冻结余额(下单时)
UPDATE balances
SET available = available - :amount,
    frozen    = frozen + :amount,
    version   = version + 1,
    updated_at = NOW()
WHERE account_id = :accountId
  AND asset = :asset
  AND available >= :amount
  AND version = :expectedVersion;
-- affected_rows == 0 → 余额不足或版本冲突

-- 成交结算(释放冻结 + 增加对手资产)
BEGIN;
  -- 买方: 释放 USDT 冻结,增加 BTC
  UPDATE balances SET frozen = frozen - :quoteAmt, version = version + 1
    WHERE account_id = :buyerAccountId AND asset = 'USDT';
  UPDATE balances SET available = available + :baseQty, version = version + 1
    WHERE account_id = :buyerAccountId AND asset = 'BTC';
  -- 卖方: 释放 BTC 冻结,增加 USDT
  UPDATE balances SET frozen = frozen - :baseQty, version = version + 1
    WHERE account_id = :sellerAccountId AND asset = 'BTC';
  UPDATE balances SET available = available + :quoteAmt, version = version + 1
    WHERE account_id = :sellerAccountId AND asset = 'USDT';
COMMIT;

余额操作模拟器

点击按钮模拟下单冻结、成交结算、撤单解冻的资金流转过程:

资金冻结/解冻/结算模拟 — 点击按钮模拟操作
资产可用冻结总额
USDT50000.00000.000050000.0000
BTC2.00000.00002.0000
ETH10.00000.000010.0000
USDT50000.0000
🟢 可用 50000.00🔴 冻结 0.00
BTC2.0000
🟢 可用 2.00🔴 冻结 0.00
ETH10.0000
🟢 可用 10.00🔴 冻结 0.00
系统就绪

5. 冻结流水表

CREATE TABLE frozen_logs (
    id                BIGSERIAL PRIMARY KEY,
    account_id        BIGINT NOT NULL,
    asset             VARCHAR(10) NOT NULL,
    order_id          BIGINT NOT NULL,                -- 关联订单
    action            VARCHAR(10) NOT NULL,            -- 'FREEZE','UNFREEZE'
    amount            DECIMAL(32,16) NOT NULL,
    reason            VARCHAR(32),                     -- 'ORDER_NEW','TRADE_SETTLE','CANCEL'
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    CONSTRAINT uq_freeze UNIQUE (order_id, action, reason)
                      -- 幂等:同一订单同一操作不重复
);

冻结流水表记录每次资金冻结/解冻的完整审计轨迹。UNIQUE(order_id, action, reason) 保证同一订单的冻结操作幂等,防止重复冻结或解冻。

6. 手续费表

CREATE TABLE fee_records (
    id                BIGSERIAL PRIMARY KEY,
    trade_id          BIGINT NOT NULL REFERENCES trades(trade_id),
    user_id           BIGINT NOT NULL,
    order_id          BIGINT NOT NULL,
    symbol            VARCHAR(20) NOT NULL,

    -- 费率
    fee_role          VARCHAR(5) NOT NULL,            -- 'MAKER' / 'TAKER'
    base_rate         DECIMAL(10,8) NOT NULL,          -- 基础费率
    discount_rate     DECIMAL(10,8) DEFAULT 0,         -- 折扣后费率
    discount_source   VARCHAR(20),                     -- 'VIP','PLATFORM_TOKEN','COUPON'

    -- 费用
    fee_amount        DECIMAL(32,16) NOT NULL,
    fee_asset         VARCHAR(10) NOT NULL,
    original_amount   DECIMAL(32,16) NOT NULL,         -- 折扣前费用

    -- 时间
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- VIP 费率表
CREATE TABLE fee_tiers (
    id                SERIAL PRIMARY KEY,
    vip_level         INT NOT NULL,
    maker_rate        DECIMAL(10,8) NOT NULL,
    taker_rate        DECIMAL(10,8) NOT NULL,
    effective_from    TIMESTAMP WITH TIME ZONE NOT NULL,
    effective_to      TIMESTAMP WITH TIME ZONE,
    CONSTRAINT uq_vip_period UNIQUE (vip_level, effective_from)
);

7. 持仓表 (positions)

CREATE TABLE positions (
    id                BIGSERIAL PRIMARY KEY,
    user_id           BIGINT NOT NULL,
    account_id        BIGINT NOT NULL,
    symbol            VARCHAR(20) NOT NULL,

    -- 仓位信息
    side              VARCHAR(5) NOT NULL,             -- 'LONG' / 'SHORT'
    quantity          DECIMAL(32,16) DEFAULT 0,        -- 持仓数量
    entry_price       DECIMAL(32,16) DEFAULT 0,        -- 平均入场价
    mark_price        DECIMAL(32,16) DEFAULT 0,        -- 最新标记价格

    -- 保证金
    margin            DECIMAL(32,16) DEFAULT 0,        -- 当前保证金
    margin_type       VARCHAR(10) DEFAULT 'CROSS',
    leverage          SMALLINT DEFAULT 1,

    -- 盈亏
    unrealized_pnl    DECIMAL(32,16) DEFAULT 0,        -- 未实现盈亏
    realized_pnl      DECIMAL(32,16) DEFAULT 0,        -- 已实现盈亏
    liquidation_price DECIMAL(32,16) DEFAULT 0,        -- 强平价格

    -- 时间
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    version           INT DEFAULT 1,

    CONSTRAINT uq_user_symbol UNIQUE (user_id, symbol, side)
);

-- 持仓历史(每次变更记录)
CREATE TABLE position_history (
    id                BIGSERIAL PRIMARY KEY,
    position_id       BIGINT NOT NULL,
    trade_id          BIGINT,
    action            VARCHAR(10) NOT NULL,            -- 'OPEN','ADD','REDUCE','CLOSE','LIQUIDATE'
    quantity_change   DECIMAL(32,16) NOT NULL,
    price             DECIMAL(32,16) NOT NULL,
    realized_pnl      DECIMAL(32,16) DEFAULT 0,
    created_at        TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

仓位更新逻辑

-- 加仓: 更新平均入场价
new_entry_price = (old_qty * old_entry + add_qty * add_price) / (old_qty + add_qty)

-- 减仓/平仓: 计算已实现盈亏
realized_pnl = (exit_price - entry_price) * close_qty  -- LONG
realized_pnl = (entry_price - exit_price) * close_qty  -- SHORT

-- 未实现盈亏: 根据 mark_price 实时计算
unrealized_pnl = (mark_price - entry_price) * qty      -- LONG
unrealized_pnl = (entry_price - mark_price) * qty      -- SHORT

8. K 线存储 (OHLCV)

-- TimescaleDB hypertable
CREATE TABLE klines (
    symbol            VARCHAR(20) NOT NULL,
    interval          VARCHAR(4) NOT NULL,             -- '1m','5m','15m','1h','4h','1d','1w'
    open_time         TIMESTAMP WITH TIME ZONE NOT NULL,
    close_time        TIMESTAMP WITH TIME ZONE NOT NULL,

    -- OHLCV
    open              DECIMAL(32,16) NOT NULL,
    high              DECIMAL(32,16) NOT NULL,
    low               DECIMAL(32,16) NOT NULL,
    close             DECIMAL(32,16) NOT NULL,
    volume            DECIMAL(32,16) NOT NULL,         -- 基础资产成交量
    quote_volume      DECIMAL(32,16) NOT NULL,         -- 计价资产成交额
    trade_count       INT DEFAULT 0,                   -- 成交笔数

    PRIMARY KEY (symbol, interval, open_time)
);

-- 转为 TimescaleDB hypertable(按 open_time 分块)
SELECT create_hypertable('klines', 'open_time',
    chunk_time_interval => INTERVAL '7 days'
);

-- 压缩策略:7 天以上的数据自动压缩
ALTER TABLE klines SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'symbol,interval',
    timescaledb.compress_orderby = 'open_time DESC'
);
SELECT add_compression_policy('klines', INTERVAL '7 days');

-- 保留策略:只保留 2 年数据,更老的归档到冷存储
SELECT add_retention_policy('klines', INTERVAL '2 years');

K 线聚合流程

以 1m K 线为基础,每分钟窗口关闭时写入 TimescaleDB 并触发向上合成。5m 由 5 根 1m 合成,15m 由 3 根 5m 合成,以此类推。缺少成交的窗口插入空蜡烛(open=close=上一根 close, high=low=close, volume=0)。

9. 索引策略

-- === orders 表索引 ===
CREATE INDEX idx_orders_user_time ON orders (user_id, created_at DESC);
CREATE INDEX idx_orders_symbol_status ON orders (symbol, status);
CREATE INDEX idx_orders_status ON orders (status) WHERE status IN ('NEW','PARTIALLY_FILLED');
                      -- 部分索引:只索引活跃订单

-- === trades 表索引 ===
CREATE INDEX idx_trades_symbol_time ON trades (symbol, trade_time DESC);
CREATE INDEX idx_trades_buy_user ON trades (buy_user_id, trade_time DESC);
CREATE INDEX idx_trades_sell_user ON trades (sell_user_id, trade_time DESC);
CREATE INDEX idx_trades_order ON trades (buy_order_id);
CREATE INDEX idx_trades_order2 ON trades (sell_order_id);

-- === balances 表索引 ===
CREATE UNIQUE INDEX idx_balances_account_asset ON balances (account_id, asset);

-- === positions 表索引 ===
CREATE INDEX idx_positions_user ON positions (user_id);
CREATE INDEX idx_positions_liquidation ON positions (liquidation_price)
    WHERE quantity > 0;  -- 只索引有持仓的记录

-- === klines 表索引(TimescaleDB 自动创建 hypertable 索引)===
CREATE INDEX idx_klines_symbol_interval ON klines (symbol, interval, open_time DESC);

部分索引

只索引活跃订单(NEW/PARTIALLY_FILLED),避免已结束订单占用索引空间。

覆盖索引

热查询字段放入索引,避免回表。如用户历史订单查询覆盖 (user_id, created_at, status)。

写入优化

高频写入表(trades)减少索引数量,用异步物化视图替代实时聚合索引。

定期 REINDEX

定时对高更新表做 REINDEX CONCURRENTLY,减少索引膨胀。

10. 分区与归档

-- 按月分区订单表
CREATE TABLE orders (
    -- ... 同上 ...
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2026_01 PARTITION OF orders
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- ... 自动化脚本定期创建未来月份分区

-- 成交表同样按月分区
CREATE TABLE trades (
    -- ... 同上 ...
) PARTITION BY RANGE (trade_time);

数据生命周期模拟器

点击时间推进按钮,观察订单数据如何在热/温/冷三层存储间自动迁移:

数据生命周期 — 点击时间推进观察数据迁移
当前模拟日期
2025-01-11
Day 10
🔴
热数据 (Hot)
PostgreSQL Primary
< 7 days
4
#L54R8T已撤销
ETH/USDTSELL
2025-01-10 (1d ago)
#BHXVMV部分成交
BTC/USDTBUY
2025-01-09 (2d ago)
#WUHIBH已撤销
DOGE/USDTSELL
2025-01-07 (4d ago)
#MG8GDT部分成交
BNB/USDTBUY
2025-01-07 (4d ago)
查询延迟: < 1ms
🟡
温数据 (Warm)
PostgreSQL Archive
7 - 90 days
3
#PJUW03已撤销
ETH/USDTSELL
2025-01-04 (7d ago)
#SSRZ9Z已成交
ETH/USDTSELL
2025-01-03 (8d ago)
#EB18A6部分成交
XRP/USDTSELL
2025-01-03 (8d ago)
查询延迟: ~10ms
🔵
冷数据 (Cold)
Object Storage (S3/Parquet)
> 90 days
0
暂无记录
查询延迟: ~100ms
统计面板
热数据记录数 (Hot)4
温数据记录数 (Warm)3
冷数据记录数 (Cold)0
总记录数7
总查询延迟估算< 1ms (按最热层)

11. Redis 数据结构

# === 订单簿快照 ===
# Sorted Set: 价格作为 score
ZADD  ob:BTCUSDT:asks  42000  "orderId1:qty1"
ZADD  ob:BTCUSDT:asks  42050  "orderId2:qty2"
ZADD  ob:BTCUSDT:bids  41950  "orderId3:qty3"

# === 最新成交价 ===
SET   ticker:BTCUSDT:last  "42000.5"
HSET  ticker:BTCUSDT  price "42000.5"  vol24h "1234.5"  high24h "43000"  low24h "41000"

# === 用户活跃订单缓存 ===
HSET  user:42:orders  "ORD-123"  "{json}"
# TTL: 订单完成后删除

# === 幂等去重 ===
SET   dedup:42:web-123  "ORD-123"  EX 86400
# clientOrderId 去重,TTL 24h

# === K 线实时缓存 ===
HSET  kline:BTCUSDT:1m:current  open "42000"  high "42100"  low "41950"  close "42050"  vol "123.5"

# === 撮合引擎快照 offset ===
SET   engine:BTCUSDT:offset  "1050"
SET   engine:BTCUSDT:snapshot_time  "1700000000000"

# === 限流计数器 ===
# 滑动窗口限流
ZADD  ratelimit:42:orders  {timestamp}  {requestId}
ZREMRANGEBYSCORE  ratelimit:42:orders  0  {timestamp - window}
ZCARD  ratelimit:42:orders  -- 当前窗口请求数

订单簿快照

Sorted Set 按价格排序,撮合引擎定期全量快照。重启时加载快照 + Kafka 重放恢复。

Ticker 缓存

Hash 存储最新价、24h 量价等统计数据。前端直接读取,减少数据库查询。

幂等键

String + TTL 实现请求去重。24 小时后自动过期,不影响历史查询。

限流

Sorted Set 滑动窗口。score=时间戳,定期清理窗口外数据,ZCARD 获取当前计数。

12. 数据迁移与版本管理

Flyway/Liquibase

使用数据库迁移工具管理 Schema 版本。每次变更生成编号迁移文件,CI/CD 自动执行。

在线 DDL

使用 pg_repack 或 CREATE INDEX CONCURRENTLY 进行在线 Schema 变更,不锁表。

回滚策略

每个迁移文件包含 UP/DOWN 脚本。生产环境优先使用可逆变更,不可逆变更需人工确认。

数据校验

迁移后运行校验脚本检查数据一致性:余额总和、订单状态分布、索引完整性。