HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 交易所技术完整体系

    • 交易所技术完整体系
    • 交易所技术架构总览
    • 交易基础概念
    • 撮合引擎原理
    • 撮合引擎实现-内存撮合
    • 撮合引擎优化 - 延迟与吞吐
    • 撮合引擎高可用
    • 清算系统设计
    • 风控系统设计
    • 资金管理系统
    • 行情系统设计
    • 去中心化交易所(DEX)设计
    • 合约交易系统
    • 数据库设计与优化
    • 缓存与消息队列
    • 用户系统与KYC
    • 交易所API设计
    • 监控与告警系统
    • 安全防护与攻防
    • 高可用架构设计
    • 压力测试与性能优化
    • 项目实战-完整交易所实现

数据库设计与优化

1. 数据库选型

1.1 关系型数据库

MySQL 8.0 - 核心业务数据

用途说明
用户信息账号、KYC、安全设置
订单数据委托订单、成交记录
账户资产余额、流水、锁定
仓位信息合约持仓、保证金

优势:

  • ACID事务保证
  • 成熟的主从复制
  • 丰富的索引类型
  • 完善的生态工具

1.2 NoSQL数据库

Redis - 缓存和实时数据

用途数据结构说明
行情数据StringTicker、最新价
订单簿Sorted Set价格档位排序
K线数据Hash多周期K线
用户会话StringJWT token
限流计数String滑动窗口计数

MongoDB - 半结构化数据

用途说明
操作日志用户行为、系统日志
配置数据交易对配置、费率配置
统计数据日报、月报

InfluxDB - 时序数据

用途说明
K线数据多周期历史K线
监控指标系统性能、业务指标
资金费率历史资金费率

2. 核心表设计

2.1 用户表

CREATE TABLE `users` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `email` VARCHAR(255) NOT NULL COMMENT '邮箱',
    `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
    `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希',

    -- KYC信息
    `kyc_level` TINYINT NOT NULL DEFAULT 0 COMMENT 'KYC等级: 0-未认证, 1-L1, 2-L2, 3-L3',
    `real_name` VARCHAR(100) DEFAULT NULL COMMENT '真实姓名',
    `id_number` VARCHAR(50) DEFAULT NULL COMMENT '身份证号',
    `country` VARCHAR(10) DEFAULT NULL COMMENT '国家代码',

    -- 安全设置
    `totp_secret` VARCHAR(255) DEFAULT NULL COMMENT '2FA密钥',
    `is_2fa_enabled` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用2FA',
    `login_password_updated_at` DATETIME DEFAULT NULL COMMENT '登录密码最后更新时间',
    `trade_password_hash` VARCHAR(255) DEFAULT NULL COMMENT '交易密码哈希',

    -- 状态
    `status` ENUM('active', 'suspended', 'closed') NOT NULL DEFAULT 'active' COMMENT '账户状态',
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_email` (`email`),
    UNIQUE KEY `uk_phone` (`phone`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

2.2 账户余额表

CREATE TABLE `account_balances` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `currency` VARCHAR(20) NOT NULL COMMENT '币种',
    `account_type` ENUM('spot', 'margin', 'futures') NOT NULL DEFAULT 'spot' COMMENT '账户类型',

    -- 余额
    `available` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '可用余额',
    `frozen` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '冻结余额',

    -- 版本号(乐观锁)
    `version` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '版本号',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_currency_type` (`user_id`, `currency`, `account_type`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账户余额表';

乐观锁更新示例:

-- 冻结余额(下单)
UPDATE account_balances
SET available = available - 100,
    frozen = frozen + 100,
    version = version + 1
WHERE user_id = 1001
  AND currency = 'USDT'
  AND account_type = 'spot'
  AND version = 5        -- 乐观锁
  AND available >= 100;  -- 余额检查

-- 检查affected_rows,如果为0则表示更新失败,需要重试

2.3 资金流水表

CREATE TABLE `account_transactions` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `currency` VARCHAR(20) NOT NULL COMMENT '币种',
    `account_type` ENUM('spot', 'margin', 'futures') NOT NULL COMMENT '账户类型',

    -- 交易信息
    `type` ENUM('deposit', 'withdraw', 'trade', 'fee', 'funding', 'transfer', 'airdrop') NOT NULL COMMENT '交易类型',
    `amount` DECIMAL(36, 18) NOT NULL COMMENT '金额(正数为入账,负数为出账)',
    `balance_after` DECIMAL(36, 18) NOT NULL COMMENT '交易后余额',

    -- 关联信息
    `ref_type` VARCHAR(50) DEFAULT NULL COMMENT '关联类型: order, trade, deposit, withdraw',
    `ref_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联ID',

    -- 描述
    `comment` VARCHAR(255) DEFAULT NULL COMMENT '备注',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    KEY `idx_user_currency` (`user_id`, `currency`, `created_at`),
    KEY `idx_ref` (`ref_type`, `ref_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资金流水表';

-- 分区策略:按月分区
ALTER TABLE account_transactions PARTITION BY RANGE (TO_DAYS(created_at)) (
    PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
    PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
    PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
    -- ... 继续添加分区
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

2.4 订单表

CREATE TABLE `orders` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `order_id` VARCHAR(64) NOT NULL COMMENT '订单ID(全局唯一)',
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `symbol` VARCHAR(20) NOT NULL COMMENT '交易对',

    -- 订单信息
    `side` ENUM('buy', 'sell') NOT NULL COMMENT '买卖方向',
    `type` ENUM('limit', 'market', 'stop_limit', 'stop_market') NOT NULL COMMENT '订单类型',
    `price` DECIMAL(36, 18) DEFAULT NULL COMMENT '价格',
    `quantity` DECIMAL(36, 18) NOT NULL COMMENT '数量',
    `stop_price` DECIMAL(36, 18) DEFAULT NULL COMMENT '止损价',

    -- 成交信息
    `filled_quantity` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '已成交数量',
    `filled_amount` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '已成交金额',
    `avg_price` DECIMAL(36, 18) DEFAULT NULL COMMENT '平均成交价',

    -- 手续费
    `fee` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '手续费',
    `fee_currency` VARCHAR(20) DEFAULT NULL COMMENT '手续费币种',

    -- 状态
    `status` ENUM('pending', 'partial', 'filled', 'cancelled', 'rejected') NOT NULL DEFAULT 'pending' COMMENT '订单状态',
    `time_in_force` ENUM('GTC', 'IOC', 'FOK') NOT NULL DEFAULT 'GTC' COMMENT '有效期',

    -- 时间
    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `filled_at` DATETIME DEFAULT NULL COMMENT '完全成交时间',

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_id` (`order_id`),
    KEY `idx_user_symbol_status` (`user_id`, `symbol`, `status`, `created_at`),
    KEY `idx_symbol_status` (`symbol`, `status`, `created_at`),
    KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

-- 分库分表策略(稍后详述)

2.5 成交表

CREATE TABLE `trades` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `trade_id` VARCHAR(64) NOT NULL COMMENT '成交ID',
    `symbol` VARCHAR(20) NOT NULL COMMENT '交易对',

    -- 订单信息
    `buy_order_id` VARCHAR(64) NOT NULL COMMENT '买单ID',
    `sell_order_id` VARCHAR(64) NOT NULL COMMENT '卖单ID',
    `buy_user_id` BIGINT UNSIGNED NOT NULL COMMENT '买方用户ID',
    `sell_user_id` BIGINT UNSIGNED NOT NULL COMMENT '卖方用户ID',

    -- 成交信息
    `price` DECIMAL(36, 18) NOT NULL COMMENT '成交价',
    `quantity` DECIMAL(36, 18) NOT NULL COMMENT '成交量',
    `amount` DECIMAL(36, 18) NOT NULL COMMENT '成交额',

    -- Maker/Taker
    `buyer_is_maker` BOOLEAN NOT NULL COMMENT '买方是否为Maker',

    -- 手续费
    `buy_fee` DECIMAL(36, 18) NOT NULL COMMENT '买方手续费',
    `sell_fee` DECIMAL(36, 18) NOT NULL COMMENT '卖方手续费',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_trade_id` (`trade_id`),
    KEY `idx_symbol_created` (`symbol`, `created_at`),
    KEY `idx_buy_user` (`buy_user_id`, `created_at`),
    KEY `idx_sell_user` (`sell_user_id`, `created_at`),
    KEY `idx_buy_order` (`buy_order_id`),
    KEY `idx_sell_order` (`sell_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成交表';

2.6 合约仓位表

CREATE TABLE `positions` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `symbol` VARCHAR(20) NOT NULL COMMENT '合约',

    -- 仓位信息
    `side` ENUM('long', 'short') NOT NULL COMMENT '多空方向',
    `quantity` DECIMAL(36, 18) NOT NULL COMMENT '持仓量',
    `entry_price` DECIMAL(36, 18) NOT NULL COMMENT '开仓均价',
    `mark_price` DECIMAL(36, 18) NOT NULL COMMENT '标记价格',
    `liquidation_price` DECIMAL(36, 18) NOT NULL COMMENT '强平价格',

    -- 保证金
    `leverage` INT NOT NULL COMMENT '杠杆倍数',
    `margin_mode` ENUM('isolated', 'cross') NOT NULL COMMENT '保证金模式',
    `initial_margin` DECIMAL(36, 18) NOT NULL COMMENT '起始保证金',
    `maintenance_margin` DECIMAL(36, 18) NOT NULL COMMENT '维持保证金',
    `margin` DECIMAL(36, 18) NOT NULL COMMENT '仓位保证金',

    -- 盈亏
    `unrealized_pnl` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '未实现盈亏',
    `realized_pnl` DECIMAL(36, 18) NOT NULL DEFAULT 0 COMMENT '已实现盈亏',

    -- 版本号
    `version` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '版本号',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_symbol` (`user_id`, `symbol`),
    KEY `idx_symbol` (`symbol`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合约仓位表';

2.7 充值提现表

CREATE TABLE `deposits` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `currency` VARCHAR(20) NOT NULL COMMENT '币种',

    -- 金额
    `amount` DECIMAL(36, 18) NOT NULL COMMENT '充值金额',

    -- 区块链信息
    `chain` VARCHAR(20) NOT NULL COMMENT '链: ETH, BSC, TRX',
    `txid` VARCHAR(255) NOT NULL COMMENT '交易哈希',
    `from_address` VARCHAR(255) NOT NULL COMMENT '发送地址',
    `to_address` VARCHAR(255) NOT NULL COMMENT '充值地址',
    `confirmations` INT NOT NULL DEFAULT 0 COMMENT '确认数',
    `required_confirmations` INT NOT NULL COMMENT '所需确认数',

    -- 状态
    `status` ENUM('pending', 'confirming', 'completed', 'failed') NOT NULL DEFAULT 'pending',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `completed_at` DATETIME DEFAULT NULL,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_txid_currency` (`txid`, `currency`),
    KEY `idx_user_status` (`user_id`, `status`, `created_at`),
    KEY `idx_status_created` (`status`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值表';

CREATE TABLE `withdrawals` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `withdrawal_id` VARCHAR(64) NOT NULL COMMENT '提现ID',
    `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    `currency` VARCHAR(20) NOT NULL COMMENT '币种',

    -- 金额
    `amount` DECIMAL(36, 18) NOT NULL COMMENT '提现金额',
    `fee` DECIMAL(36, 18) NOT NULL COMMENT '手续费',
    `actual_amount` DECIMAL(36, 18) NOT NULL COMMENT '实际到账金额',

    -- 区块链信息
    `chain` VARCHAR(20) NOT NULL COMMENT '链',
    `to_address` VARCHAR(255) NOT NULL COMMENT '提现地址',
    `txid` VARCHAR(255) DEFAULT NULL COMMENT '交易哈希',

    -- 审核
    `status` ENUM('pending', 'reviewing', 'approved', 'processing', 'completed', 'rejected', 'failed') NOT NULL DEFAULT 'pending',
    `reviewer_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '审核员ID',
    `review_comment` VARCHAR(500) DEFAULT NULL COMMENT '审核备注',

    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `completed_at` DATETIME DEFAULT NULL,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_withdrawal_id` (`withdrawal_id`),
    KEY `idx_user_status` (`user_id`, `status`, `created_at`),
    KEY `idx_status_created` (`status`, `created_at`),
    KEY `idx_txid` (`txid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提现表';

3. 分库分表

3.1 分库策略

按用户ID分库,将用户数据分散到多个数据库。

Database Sharding:
┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐
│   DB_00    │  │   DB_01    │  │   DB_02    │  │   DB_03    │
│ user_id%4=0│  │ user_id%4=1│  │ user_id%4=2│  │ user_id%4=3│
└────────────┘  └────────────┘  └────────────┘  └────────────┘

路由算法:

type DatabaseRouter struct {
    shardCount int
    databases  []*sql.DB
}

func NewDatabaseRouter(shardCount int) *DatabaseRouter {
    router := &DatabaseRouter{
        shardCount: shardCount,
        databases:  make([]*sql.DB, shardCount),
    }

    // 初始化数据库连接
    for i := 0; i < shardCount; i++ {
        dsn := fmt.Sprintf("user:pass@tcp(db-%02d:3306)/exchange", i)
        db, err := sql.Open("mysql", dsn)
        if err != nil {
            panic(err)
        }
        router.databases[i] = db
    }

    return router
}

func (dr *DatabaseRouter) GetDB(userID int64) *sql.DB {
    shardID := int(userID % int64(dr.shardCount))
    return dr.databases[shardID]
}

// 使用示例
func (dr *DatabaseRouter) GetUserBalance(userID int64, currency string) (*Balance, error) {
    db := dr.GetDB(userID)

    var balance Balance
    err := db.QueryRow(`
        SELECT available, frozen FROM account_balances
        WHERE user_id = ? AND currency = ?
    `, userID, currency).Scan(&balance.Available, &balance.Frozen)

    return &balance, err
}

3.2 分表策略

订单表分表:按 symbol 和时间分表

orders_BTCUSDT_202401
orders_BTCUSDT_202402
orders_ETHUSDT_202401
orders_ETHUSDT_202402

分表路由:

type TableRouter struct{}

func (tr *TableRouter) GetOrderTable(symbol string, createTime time.Time) string {
    // 标准化symbol:BTC/USDT -> BTCUSDT
    symbol = strings.ReplaceAll(symbol, "/", "")
    symbol = strings.ToUpper(symbol)

    // 按月分表
    month := createTime.Format("200601")

    return fmt.Sprintf("orders_%s_%s", symbol, month)
}

// 插入订单
func (tr *TableRouter) InsertOrder(db *sql.DB, order *Order) error {
    tableName := tr.GetOrderTable(order.Symbol, order.CreatedAt)

    query := fmt.Sprintf(`
        INSERT INTO %s (order_id, user_id, symbol, side, type, price, quantity, status, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, tableName)

    _, err := db.Exec(query,
        order.OrderID, order.UserID, order.Symbol, order.Side,
        order.Type, order.Price, order.Quantity, order.Status,
        order.CreatedAt,
    )

    return err
}

// 查询订单(需要指定时间范围)
func (tr *TableRouter) QueryOrders(db *sql.DB, userID int64, symbol string,
    startTime, endTime time.Time) ([]*Order, error) {

    // 生成所有需要查询的表
    tables := tr.getTablesInRange(symbol, startTime, endTime)

    var allOrders []*Order

    for _, table := range tables {
        query := fmt.Sprintf(`
            SELECT order_id, symbol, side, type, price, quantity, status, created_at
            FROM %s
            WHERE user_id = ? AND created_at BETWEEN ? AND ?
            ORDER BY created_at DESC
        `, table)

        rows, err := db.Query(query, userID, startTime, endTime)
        if err != nil {
            // 表可能不存在,跳过
            continue
        }
        defer rows.Close()

        for rows.Next() {
            var order Order
            err := rows.Scan(
                &order.OrderID, &order.Symbol, &order.Side, &order.Type,
                &order.Price, &order.Quantity, &order.Status, &order.CreatedAt,
            )
            if err != nil {
                return nil, err
            }
            allOrders = append(allOrders, &order)
        }
    }

    return allOrders, nil
}

func (tr *TableRouter) getTablesInRange(symbol string, start, end time.Time) []string {
    var tables []string

    current := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC)
    endMonth := time.Date(end.Year(), end.Month(), 1, 0, 0, 0, 0, time.UTC)

    for current.Before(endMonth) || current.Equal(endMonth) {
        tables = append(tables, tr.GetOrderTable(symbol, current))
        current = current.AddDate(0, 1, 0)
    }

    return tables
}

3.3 全局ID生成

分库分表后,需要全局唯一ID。

Snowflake算法:

type Snowflake struct {
    mu sync.Mutex

    // 配置
    epoch      int64 // 起始时间戳(毫秒)
    workerID   int64 // 机器ID(10bit,支持1024台机器)
    datacenter int64 // 数据中心ID(10bit)

    // 状态
    sequence      int64 // 序列号(12bit,每毫秒4096个ID)
    lastTimestamp int64 // 上次生成ID的时间戳
}

func NewSnowflake(workerID, datacenter int64) *Snowflake {
    return &Snowflake{
        epoch:      1640995200000, // 2022-01-01 00:00:00
        workerID:   workerID,
        datacenter: datacenter,
        sequence:   0,
        lastTimestamp: 0,
    }
}

func (sf *Snowflake) NextID() (int64, error) {
    sf.mu.Lock()
    defer sf.mu.Unlock()

    timestamp := time.Now().UnixNano() / 1e6

    if timestamp < sf.lastTimestamp {
        return 0, fmt.Errorf("clock moved backwards")
    }

    if timestamp == sf.lastTimestamp {
        // 同一毫秒内,序列号递增
        sf.sequence = (sf.sequence + 1) & 0xFFF // 12bit mask
        if sf.sequence == 0 {
            // 序列号用完,等待下一毫秒
            for timestamp <= sf.lastTimestamp {
                timestamp = time.Now().UnixNano() / 1e6
            }
        }
    } else {
        // 新的毫秒,序列号重置
        sf.sequence = 0
    }

    sf.lastTimestamp = timestamp

    // 组装ID:
    // | 1bit (未使用) | 41bit (时间戳) | 10bit (数据中心+机器) | 12bit (序列号) |
    id := ((timestamp - sf.epoch) << 22) |
          (sf.datacenter << 17) |
          (sf.workerID << 12) |
          sf.sequence

    return id, nil
}

// 使用示例
func main() {
    sf := NewSnowflake(1, 1)

    for i := 0; i < 10; i++ {
        id, _ := sf.NextID()
        fmt.Printf("ID: %d\n", id)
    }
}

// 输出:
// ID: 1234567890123456789
// ID: 1234567890123456790
// ...

4. 索引优化

4.1 索引设计原则

1. 选择性高的列优先

-- 好的索引:email唯一性高
CREATE INDEX idx_email ON users(email);

-- 不好的索引:status只有几个值
-- CREATE INDEX idx_status ON users(status); -- 不建议

2. 复合索引遵循最左前缀

-- 复合索引
CREATE INDEX idx_user_symbol_status ON orders(user_id, symbol, status, created_at);

-- 可以利用该索引的查询:
--  WHERE user_id = ?
--  WHERE user_id = ? AND symbol = ?
--  WHERE user_id = ? AND symbol = ? AND status = ?
--  WHERE user_id = ? AND symbol = ? AND status = ? AND created_at > ?

-- 无法利用该索引的查询:
-- ✗ WHERE symbol = ?
-- ✗ WHERE status = ?
-- ✗ WHERE created_at > ?

3. 覆盖索引减少回表

-- 查询订单ID和创建时间
SELECT order_id, created_at FROM orders
WHERE user_id = ? AND symbol = ?;

-- 创建覆盖索引(包含查询所需的所有列)
CREATE INDEX idx_user_symbol_cover ON orders(user_id, symbol, order_id, created_at);

4.2 慢查询优化

案例1:查询用户历史订单

-- 慢查询(全表扫描)
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 20;

-- EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 20;

-- 优化:添加复合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);

案例2:统计用户成交量

-- 慢查询(需要扫描大量数据)
SELECT COUNT(*), SUM(amount)
FROM trades
WHERE buy_user_id = 1001 AND created_at >= '2024-01-01';

-- 优化:添加复合索引 + 覆盖索引
CREATE INDEX idx_buy_user_created_amount ON trades(buy_user_id, created_at, amount);

案例3:避免函数索引失效

-- 错误:对索引列使用函数
SELECT * FROM orders
WHERE DATE(created_at) = '2024-01-01';

-- 正确:使用范围查询
SELECT * FROM orders
WHERE created_at >= '2024-01-01 00:00:00'
  AND created_at < '2024-01-02 00:00:00';

5. 查询优化

5.1 避免N+1查询

问题:

// 查询订单(1次查询)
orders := queryOrders(userID)

// 为每个订单查询成交记录(N次查询)
for _, order := range orders {
    trades := queryTradesByOrder(order.OrderID) // N次查询!
    order.Trades = trades
}

优化:

// 一次性查询所有订单
orders := queryOrders(userID)

// 提取所有订单ID
orderIDs := make([]string, len(orders))
for i, order := range orders {
    orderIDs[i] = order.OrderID
}

// 批量查询成交记录(1次查询)
trades := queryTradesByOrderIDs(orderIDs)

// 组装数据
tradeMap := make(map[string][]*Trade)
for _, trade := range trades {
    tradeMap[trade.BuyOrderID] = append(tradeMap[trade.BuyOrderID], trade)
    tradeMap[trade.SellOrderID] = append(tradeMap[trade.SellOrderID], trade)
}

for _, order := range orders {
    order.Trades = tradeMap[order.OrderID]
}

5.2 分页查询优化

深度分页问题:

-- 慢查询:深度分页需要扫描+跳过大量数据
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 100000, 20;  -- 需要扫描100020行!

优化:使用游标分页:

-- 第一页
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 20;

-- 记录最后一条的created_at和id

-- 第二页(使用WHERE代替OFFSET)
SELECT * FROM orders
WHERE user_id = 1001
  AND (created_at < ? OR (created_at = ? AND id < ?))
ORDER BY created_at DESC, id DESC
LIMIT 20;

Go实现:

type OrderCursor struct {
    CreatedAt time.Time
    ID        int64
}

func QueryOrdersWithCursor(db *sql.DB, userID int64, cursor *OrderCursor, limit int) ([]*Order, *OrderCursor, error) {
    var query string
    var args []interface{}

    if cursor == nil {
        // 第一页
        query = `
            SELECT id, order_id, symbol, side, price, quantity, created_at
            FROM orders
            WHERE user_id = ?
            ORDER BY created_at DESC, id DESC
            LIMIT ?
        `
        args = []interface{}{userID, limit}
    } else {
        // 后续页
        query = `
            SELECT id, order_id, symbol, side, price, quantity, created_at
            FROM orders
            WHERE user_id = ?
              AND (created_at < ? OR (created_at = ? AND id < ?))
            ORDER BY created_at DESC, id DESC
            LIMIT ?
        `
        args = []interface{}{userID, cursor.CreatedAt, cursor.CreatedAt, cursor.ID, limit}
    }

    rows, err := db.Query(query, args...)
    if err != nil {
        return nil, nil, err
    }
    defer rows.Close()

    var orders []*Order
    var newCursor *OrderCursor

    for rows.Next() {
        var order Order
        err := rows.Scan(&order.ID, &order.OrderID, &order.Symbol,
            &order.Side, &order.Price, &order.Quantity, &order.CreatedAt)
        if err != nil {
            return nil, nil, err
        }
        orders = append(orders, &order)

        // 更新游标
        newCursor = &OrderCursor{
            CreatedAt: order.CreatedAt,
            ID:        order.ID,
        }
    }

    return orders, newCursor, nil
}

6. 事务处理

6.1 下单事务

func PlaceOrder(db *sql.DB, order *Order) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // 1. 冻结余额(乐观锁)
    result, err := tx.Exec(`
        UPDATE account_balances
        SET available = available - ?,
            frozen = frozen + ?,
            version = version + 1
        WHERE user_id = ?
          AND currency = ?
          AND version = ?
          AND available >= ?
    `, order.Amount, order.Amount, order.UserID, order.Currency,
       order.Version, order.Amount)

    if err != nil {
        return err
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return errors.New("insufficient balance or version conflict")
    }

    // 2. 插入订单
    _, err = tx.Exec(`
        INSERT INTO orders (order_id, user_id, symbol, side, type, price, quantity, status, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, order.OrderID, order.UserID, order.Symbol, order.Side, order.Type,
       order.Price, order.Quantity, "pending", time.Now())

    if err != nil {
        return err
    }

    // 3. 记录流水
    _, err = tx.Exec(`
        INSERT INTO account_transactions (user_id, currency, type, amount, ref_type, ref_id, created_at)
        VALUES (?, ?, 'freeze', ?, 'order', ?, ?)
    `, order.UserID, order.Currency, -order.Amount, order.OrderID, time.Now())

    if err != nil {
        return err
    }

    return tx.Commit()
}

6.2 分布式事务

对于跨库的分布式事务,使用两阶段提交(2PC)或TCC模式。

TCC模式示例(转账):

type TransferService struct {
    fromDB *sql.DB
    toDB   *sql.DB
}

// Try阶段:冻结资金
func (ts *TransferService) TryTransfer(fromUserID, toUserID int64, amount float64) (string, error) {
    transferID := generateTransferID()

    // 1. 冻结转出方资金
    _, err := ts.fromDB.Exec(`
        UPDATE account_balances
        SET available = available - ?,
            frozen = frozen + ?
        WHERE user_id = ? AND currency = 'USDT' AND available >= ?
    `, amount, amount, fromUserID, amount)

    if err != nil {
        return "", err
    }

    // 2. 记录TCC事务
    _, err = ts.fromDB.Exec(`
        INSERT INTO tcc_transactions (transfer_id, from_user, to_user, amount, status)
        VALUES (?, ?, ?, ?, 'trying')
    `, transferID, fromUserID, toUserID, amount)

    return transferID, err
}

// Confirm阶段:确认转账
func (ts *TransferService) ConfirmTransfer(transferID string) error {
    // 1. 获取转账信息
    var fromUserID, toUserID int64
    var amount float64
    err := ts.fromDB.QueryRow(`
        SELECT from_user, to_user, amount FROM tcc_transactions
        WHERE transfer_id = ? AND status = 'trying'
    `, transferID).Scan(&fromUserID, &toUserID, &amount)

    if err != nil {
        return err
    }

    // 2. 转出方:解冻并扣除
    _, err = ts.fromDB.Exec(`
        UPDATE account_balances
        SET frozen = frozen - ?
        WHERE user_id = ? AND currency = 'USDT'
    `, amount, fromUserID)

    if err != nil {
        return err
    }

    // 3. 转入方:增加余额
    _, err = ts.toDB.Exec(`
        UPDATE account_balances
        SET available = available + ?
        WHERE user_id = ? AND currency = 'USDT'
    `, amount, toUserID)

    if err != nil {
        return err
    }

    // 4. 更新TCC状态
    _, err = ts.fromDB.Exec(`
        UPDATE tcc_transactions SET status = 'confirmed' WHERE transfer_id = ?
    `, transferID)

    return err
}

// Cancel阶段:取消转账
func (ts *TransferService) CancelTransfer(transferID string) error {
    // 1. 获取转账信息
    var fromUserID int64
    var amount float64
    err := ts.fromDB.QueryRow(`
        SELECT from_user, amount FROM tcc_transactions
        WHERE transfer_id = ? AND status = 'trying'
    `, transferID).Scan(&fromUserID, &amount)

    if err != nil {
        return err
    }

    // 2. 解冻资金
    _, err = ts.fromDB.Exec(`
        UPDATE account_balances
        SET available = available + ?,
            frozen = frozen - ?
        WHERE user_id = ? AND currency = 'USDT'
    `, amount, amount, fromUserID)

    if err != nil {
        return err
    }

    // 3. 更新TCC状态
    _, err = ts.fromDB.Exec(`
        UPDATE tcc_transactions SET status = 'cancelled' WHERE transfer_id = ?
    `, transferID)

    return err
}

7. 数据归档与清理

7.1 冷热数据分离

策略:

  • 热数据:最近3个月的订单,存储在MySQL
  • 温数据:3-12个月的订单,存储在只读从库
  • 冷数据:12个月以上的订单,归档到对象存储(S3)

归档脚本:

func ArchiveOldOrders(db *sql.DB, archiveDate time.Time) error {
    // 1. 导出旧订单到CSV
    rows, err := db.Query(`
        SELECT * FROM orders
        WHERE created_at < ? AND status IN ('filled', 'cancelled')
    `, archiveDate)

    if err != nil {
        return err
    }
    defer rows.Close()

    csvFile := fmt.Sprintf("orders_%s.csv", archiveDate.Format("20060102"))
    file, err := os.Create(csvFile)
    if err != nil {
        return err
    }
    defer file.Close()

    writer := csv.NewWriter(file)
    defer writer.Flush()

    for rows.Next() {
        // 写入CSV
        // ... 省略代码
    }

    // 2. 上传到S3
    err = uploadToS3(csvFile, "archive/orders/")
    if err != nil {
        return err
    }

    // 3. 删除已归档数据
    _, err = db.Exec(`
        DELETE FROM orders
        WHERE created_at < ? AND status IN ('filled', 'cancelled')
        LIMIT 10000
    `, archiveDate)

    return err
}

7.2 定期清理

-- 删除3个月前的操作日志
DELETE FROM operation_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 3 MONTH)
LIMIT 10000;

-- 删除1年前的成交记录(保留在归档中)
DELETE FROM trades
WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR)
LIMIT 10000;

小结

本章介绍了交易所数据库设计与优化:

  1. 数据库选型:MySQL、Redis、MongoDB、InfluxDB的使用场景
  2. 核心表设计:用户、余额、订单、成交、仓位等表结构
  3. 分库分表:水平分库、分表路由、全局ID生成
  4. 索引优化:复合索引、覆盖索引、慢查询优化
  5. 查询优化:避免N+1查询、游标分页
  6. 事务处理:本地事务、分布式事务(TCC)
  7. 数据归档:冷热分离、定期清理

下一章将讲解缓存与消息队列的设计,包括Redis缓存策略和Kafka消息处理。

Prev
合约交易系统
Next
缓存与消息队列