下载工作台
Go 数据库实战

缓存协作与 Redis 旁路

试读上半部分 · 解锁后可读全文

第 15 章 · 读写分离与分库分表入门

本章目标:理解 主从复制读写分离 架构;掌握 垂直分库水平分表 概念与路由;在 Go/GORM 层认识 读写路由AbstractRoutingDataSource 概念);对照 architecture ch10 数据架构决策;为 shop-db 毕业项目标注扩展边界;认识 ProxySQL 与应用双 DSN 两种落地方式。

学时建议:4~5 小时(含 1 小时 architecture ch10 对照阅读)

前置:完成 ch14 EXPLAIN 优化;建议先读 architecture ch10 分库分表与读写分离。


15.1 何时需要扩展读能力

单库 MySQL 在 shop-db 教学规模(万级商品、十万级订单)通常足够。出现以下信号才考虑读写分离或分片:

信号阈值(经验)优先动作
单表行数>3000~5000 万水平分表
单库容量>500GB~1TB垂直分库 / 分片
写 TPS>2000~3000分片或异步化
读 P99索引优化后仍 >100ms读写分离
连接数> max_connections×70%读库分担

决策顺序(与 architecture ch10 §10.1 一致):

垂直分库 → 读写分离 → 冷数据归档 → 水平分片

本章以 概念 + shop-db 文档 为主,不要求本地搭完整 1 主 2 从集群;Docker Compose 提供 读写双 DSN 模拟 即可。


15.2 主从复制概念

15.2.1 拓扑

                    ┌─────────────┐
         写 ───────►│   Master    │
                    │  (Primary)  │
                    └──────┬──────┘
                           │ binlog 复制
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Slave 1 │  │ Slave 2 │  │ Slave 3 │
        └─────────┘  └─────────┘  └─────────┘
              ▲            ▲
              └──── 读 ────┘
角色职责
Master唯一写;产生 binlog
SlaveIO 线程拉 binlog → relay log → SQL 线程重放
应用写走 Master;读走 Slave(只读)

15.2.2 复制延迟

写 TPS典型延迟业务风险
2000.3s 内
8002~5s下单后列表可能缺单
2000+10s+必须写后读主

监控指标:Seconds_Behind_Master;>5s 触发告警(architecture ch10 §10.5)。

15.2.3 与 shop-db 的关系

shop-db 毕业项目默认 单库单表;文档中注明:

生产扩展路径见 architecture ch10;读写分离不改变 slug / is_published / price 分 契约。

15.3 读写分离规则

操作路由说明
INSERT / UPDATE / DELETEMaster所有写
SELECT 列表、统计Slave可接受秒级延迟
事务内 SELECTMaster同事务一致读
写后立即读(下单详情)Master 或 Redis避免「丢单」错觉
DDL / golang-migrate 迁移Master只连写库
用户下单 POST /orders
    │
    ├─► INSERT orders ──► Master
    │
    └─► GET /orders/{id}  ──► Master(写后读)
         GET /orders?page=1 ──► Slave(列表可延迟)

对照 architecture ch10 §10.4:ProxySQL match_pattern 写走 hostgroup 10、读走 20。

15.3.1 商品域读路径

API路由理由
GET /products/{slug}Slave + 缓存可延迟
POST /products 上架Master
改 price(分)后立刻 GETMaster防延迟读到旧价
运营报表 database/sqlSlave重查询

price 分 在复制链路上仍为 BIGINT,无精度问题;延迟表现为 读到旧价格整数


15.4 垂直分库

业务域 拆库,与微服务边界对齐(architecture ch10 §10.2):

shop_user     ── users, addresses
shop_catalog  ── products, categories
shop_order    ── orders, order_items
shop_payment  ── payments, refunds
优点缺点
故障隔离跨库 JOIN 不可用
团队自治分布式事务复杂度上升
备份粒度细需应用层聚合

15.4.1 跨库场景正解(architecture ch10)

场景错误正解
订单页显示商品名跨库 JOIN下单时 冗余 product_name 快照
用户订单列表跨库查 usersorders 存 user_slug
报表 GMV跨库聚合T+1 数仓宽表

shop-dborder_items.product_slugproduct_nameprice(分)正是为拆库后 零 JOIN 报表 设计(ch07/ch17)。

15.4.2 Go 多数据源配置概念

# 虚构域名,勿写生产
shop:
  datasource:
    user:
      jdbc-url: mysql DSNmysql://db-user.example.com:3306/shop_user
    catalog:
      jdbc-url: mysql DSNmysql://db-catalog.example.com:3306/shop_catalog
    order:
      jdbc-url: mysql DSNmysql://db-order.example.com:3306/shop_order

每个库独立 golang-migrate 迁移目录:db/migration/user/catalog/order/


15.5 水平分表与路由

architecture ch10 §10.3 拓扑:4 库 × 16 表 = 64 物理表

db_index    = user_id % 4   → shop_order_0..3
table_index = user_id % 16  → t_order_00..15

路由例题

user_id计算落点
1008610086%4=2, %16=6shop_order_2.t_order_06
88888801%4=1, %16=1shop_order_1.t_order_01

分片键选择:订单表用 user_id(或 user_slug 哈希),因「我的订单列表」占查询 95%。

15.5.1 Go 路由伪代码

public record OrderShard(int dbIndex, int tableIndex, String tableName) {}

public OrderShard route(long userId) {
    int db = (int) (userId % 4);
    int tbl = (int) (userId % 16);
    String name = "t_order_%02d".formatted(tbl);
    return new OrderShard(db, tbl, name);
}

public String orderInsertSql(long userId) {
    OrderShard s = route(userId);
    return "INSERT INTO shop_order_" + s.dbIndex() + "." + s.tableName() + " (...) VALUES (...)";
}

shop-db 毕业项目:逻辑表名仍为 orders;分表仅在 docs/SCALE.md 描述。

15.5.2 全局 ID

方案说明
雪花算法趋势递增、无中心
DB 号段批量取 id
UUID无序,索引局部性差

订单号 order_no 与主键 id 可分离:order_no 对外展示,id 仅内部。


15.6 应用层读写路由(Spring 概念)

gin-web ch17architecture ch10 均提及 AbstractRoutingDataSource

public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
            ? "read" : "write";
    }
}
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource routingDataSource(
            @Qualifier("masterDs") DataSource master,
            @Qualifier("slaveDs") DataSource slave) {
        ReadWriteRoutingDataSource ds = new ReadWriteRoutingDataSource();
        Map<Object, Object> map = new HashMap<>();
        map.put("write", master);
        map.put("read", slave);
        ds.setTargetDataSources(map);
        ds.setDefaultTargetDataSource(master);
        return ds;
    }
}
注解路由
@Transactional(readOnly = true)read → Slave
@Transactionalwrite → Master

shop-db 纯 Go 阶段:可用两个 sqlx 连接池 DataSource,报表脚本显式连 slave DSN


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。