10. 读写分离与分库分表

数据库读写分离架构设计、分库分表策略、分布式主键生成方案、ShardingSphere 实战与数据迁移最佳实践。

1. 读写分离架构

1.1 为什么需要读写分离

大多数业务场景读多写少(读:写 ≈ 8:1 ~ 50:1)。将读请求分发到多个从库,可线性扩展读性能。

单库架构的问题:

  App ──→ MySQL
            │
            ├── 读请求(80% CPU)
            └── 写请求(20% CPU)
            
  读请求增加 → CPU 100% → 所有请求变慢

读写分离架构:

  写请求 ──→ [Master] ──→ 异步复制 ──→ [Slave 1]
                                   ──→ [Slave 2]
                                   ──→ [Slave 3]
                                   
  读请求 ──→ [负载均衡器] ──→ Slave 1 / Slave 2 / Slave 3

1.2 实现方式

方式原理代表产品侵入性
代理层独立进程解析 SQL 路由MySQL Router、ProxySQL、MaxScale
中间件分库分表中间件内置读写分离ShardingSphere、MyCat
应用层代码中配置多数据源Spring RoutingDataSource
// Spring Boot 应用层读写分离
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource master,
            @Qualifier("slaveDataSource") DataSource slave) {
        DynamicRoutingDataSource routing = new DynamicRoutingDataSource();
        Map<Object, Object> targets = new HashMap<>();
        targets.put("master", master);
        targets.put("slave", slave);
        routing.setTargetDataSources(targets);
        routing.setDefaultTargetDataSource(master);
        return routing;
    }
}

// Service 方法指定数据源
@Transactional(readOnly = true)  // 走 Slave
public List<Order> getOrders() { ... }

@Transactional  // 走 Master
public void createOrder(Order order) { ... }

1.3 复制延迟问题

写 Master ──→ 复制到 Slave(延迟 1ms ~ 数秒)
                    │
                    └──→ 用户立即读 Slave → 读不到刚写入的数据!

解决策略:
1. 关键读走 Master(写入后立即读取的场景)
2. 缓存补偿(写入后先更新缓存,读时先查缓存)
3. 半同步复制(Semi-Sync:至少一个 Slave 确认后再返回)
4. 会话绑定(同一连接内写后读走 Master)

2. 分库分表

2.1 何时需要分库分表

指标建议分片
单表数据量> 500万行(MySQL)/ > 1亿(PostgreSQL)
单库磁盘> 500GB
单库 QPS> 5000(读)/ > 1000(写)
单表索引大小> 内存容量

2.2 分片策略

策略实现优点缺点
哈希分片hash(key) % N数据均匀扩容需迁移数据
范围分片按 ID 或时间区间范围查询友好热点风险
列表分片按枚举值映射灵活可控需人工维护
一致性哈希哈希环 + 虚拟节点扩容迁移少实现复杂
// 按用户 ID 哈希分表
String tableName = "order_" + (userId % 8);  // order_0 ~ order_7

// 按月份分表
String tableName = "log_" + YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
// log_202601, log_202602, ...

2.3 ShardingSphere 实战

# shardingsphere-config.yaml
dataSources:
  ds_0: !!com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://db0:3306/shop
  ds_1: !!com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://db1:3306/shop

shardingRule:
  tables:
    orders:
      actualDataNodes: ds_${0..1}.orders_${0..7}
      tableStrategy:
        inline:
          shardingColumn: user_id
          algorithmExpression: orders_${user_id % 8}
      databaseStrategy:
        inline:
          shardingColumn: user_id
          algorithmExpression: ds_${user_id % 2}
      keyGenerator:
        type: SNOWFLAKE
        column: order_id

3. 分布式主键

方案原理优点缺点
Snowflake41位时间+10位机器+12位序列趋势递增、高性能依赖时钟
Leaf(美团)号段模式稳定、可控需维护号段表
Redis 自增INCR简单单点、网络开销
数据库步长auto_increment_offset简单扩展性差
// Snowflake 实现(简化)
public class SnowflakeIdGenerator {
    private final long workerId;
    private long sequence = 0;
    private long lastTimestamp = -1;
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 0xFFF;  // 12 bit
            if (sequence == 0) timestamp = tilNextMillis();
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        return ((timestamp - EPOCH) << 22) | (workerId << 12) | sequence;
    }
}

4. 数据迁移

双写迁移方案(不停机):

阶段 1:双写
  应用同时写入旧库和新库(新库异步)
  读仍然走旧库

阶段 2:数据校验 + 补平
  对比旧库和新库数据差异
  修复不一致数据

阶段 3:灰度切读
  1% 流量读新库 → 10% → 50% → 100%
  监控错误率和延迟

阶段 4:切写
  写操作切换到新库
  停止旧库写入

阶段 5:下线旧库
  保留一段时间后归档删除

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南