MySQL分库分表实战:ShardingSphere垂直拆分到水平扩展全流程

MySQL分库分表什么时候该动手

单表数据量突破千万行之后,查询延迟肉眼可见地上升——即使索引完整,范围查询和排序操作的响应时间也会从毫秒级退化到秒级。分库分表是解决这个问题的常规武器,但过早分片和错误拆分策略带来的运维代价远大于性能收益。本文从拆分时机判断、垂直拆分到水平扩展,给出一套完整的分库分表实施路径。

分库分表的触发条件:别被数据量吓到

不是数据量大就要分片。以下指标出现2个以上才需要认真考虑:

  • 单表行数超过3000万,且持续增长
  • 慢查询日志中单表查询占比超过60%
  • 单表数据文件超过50GB,备份耗时超过业务窗口
  • 写入QPS超过单机MySQL的写上限(约5000-8000 QPS,取决于硬件)
# 评估单表健康状态
SELECT 
  table_name,
  table_rows,
  ROUND(data_length / 1024 / 1024, 2) AS data_mb,
  ROUND(index_length / 1024 / 1024, 2) AS index_mb,
  ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb
FROM information_schema.tables 
WHERE table_schema = 'order_db'
ORDER BY total_mb DESC;

# 慢查询统计
SELECT 
  LEFT(query_text, 80) AS query_preview,
  exec_count,
  avg_timer_ms / 1000 AS avg_ms,
  rows_examined / exec_count AS avg_rows_scanned
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name = 'order_db'
  AND avg_timer_ms / 1000 > 100
ORDER BY avg_timer_ms DESC LIMIT 20;

垂直拆分:先做业务边界划分

垂直拆分的本质是把一个大库按业务域拆成多个小库。这一步通常不需要中间件。

# 原始订单库表结构
order_db:
  ├── users           (用户表)
  ├── orders          (订单表)
  ├── order_items     (订单明细)
  ├── payments        (支付记录)
  ├── products        (商品表)
  ├── inventory       (库存表)
  └── user_logs       (用户行为日志)

# 垂直拆分后
user_db:    users, user_logs
order_db:   orders, order_items, payments
product_db: products, inventory

垂直拆分的核心原则:高内聚、低耦合。同一个事务中操作的表应该放在同一个库。如果拆分后发现跨库事务大量出现,说明拆分边界选错了。

水平分片:ShardingSphere实战配置

垂直拆分后如果单表仍然过大,进入水平分片阶段。ShardingSphere是目前最成熟的Java分片中间件。

# Spring Boot + ShardingSphere 5.5.0 配置
# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.1.10:3306/order_db_0?useSSL=false
        username: root
        password: Order@2026
        hikari:
          maximum-pool-size: 20
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://10.0.1.11:3306/order_db_1?useSSL=false
        username: root
        password: Order@2026
        hikari:
          maximum-pool-size: 20

    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: ds${0..1}.orders_${0..15}
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: orders-table-inline
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: orders-db-mod
        sharding-algorithms:
          orders-table-inline:
            type: INLINE
            props:
              algorithm-expression: orders_${user_id % 16}
          orders-db-mod:
            type: MOD
            props:
              sharding-count: 2
        key-generate-strategies:
          orders:
            key-generate-algorithm-name: snowflake
        key-generate-algorithms:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1

配置要点解析:

  • actual-data-nodes:定义数据分布,2个库×16张表=32个分片
  • 分库策略:user_id % 2决定数据落入ds0还是ds1
  • 分表策略:user_id % 16决定落入库内的哪张分表
  • 主键生成:雪花算法保证分片环境下ID全局唯一

分片键选择:决定扩展上限的关键决策

分片键选错了,后续改造成本极高。选择原则:

  • 高基数字段:user_id优于status(状态值只有几种,无法均匀分布)
  • 查询高频字段:分片键必须是80%以上查询的WHERE条件
  • 避免跨分片查询:如果按user_id分片但大量按order_id查询,每个请求都会广播到所有分片
// 分片键路由验证
// user_id = 12345
// 分库: 12345 % 2 = 1 → ds1
// 分表: 12345 % 16 = 9 → orders_9
// 最终路由: ds1.orders_9

// 非分片键查询(广播路由)
SELECT * FROM orders WHERE order_id = 'ORD20260729001'
// 会被路由到所有32个分片执行,性能灾难

// 解决方案:建立order_id到user_id的映射表
// 或使用基因法:将user_id的余数信息编码到order_id中

基因法实现:order_id由雪花ID+分片基因组成,从order_id可以直接推算出分片位置,无需映射表。

// 基因法ID生成
public class GeneShardingKeyGenerator {
    // 分片数=16,基因占4bit
    private static final int GENE_BITS = 4;
    private static final int GENE_MASK = (1 << GENE_BITS) - 1;

    public static long generateId(long snowflakeId, long userId) {
        long gene = userId & GENE_MASK; // 取userId低4bit作为基因
        return (snowflakeId << GENE_BITS) | gene; // 拼接到ID末尾
    }

    public static int extractShardIndex(long orderId) {
        return (int) (orderId & GENE_MASK); // 从ID中提取分片位置
    }
}

数据迁移:从单表到分片的平滑切换

已有线上数据的分片迁移是最危险的环节。推荐双写方案:

# 迁移步骤
# 1. 开启双写:新数据同时写入旧表和分片表
# 2. 历史数据迁移:使用DataX分批同步旧表到分片表
# 3. 数据校验:对比旧表和分片表数据一致性
# 4. 读流量切换:逐步将查询切换到分片表
# 5. 停止双写:确认稳定后关闭旧表写入

# DataX迁移任务配置(核心片段)
{
  "job": {
    "content": [{
      "reader": {
        "name": "mysqlreader",
        "parameter": {
          "connection": [{
            "jdbcUrl": "jdbc:mysql://old-db:3306/order_db",
            "query": ["SELECT * FROM orders WHERE id BETWEEN ${start} AND ${end}"]
          }],
          "fetchSize": 5000
        }
      },
      "writer": {
        "name": "mysqlwriter",
        "parameter": {
          "writeMode": "insert",
          "batchSize": 2000
        }
      }
    }],
    "setting": {
      "speed": {
        "channel": 4,
        "bytes": 1048576
      }
    }
  }
}

迁移全程保持旧表可读写,任何异常随时回滚到旧表。校验通过后再切换读流量,逐步放量。完整的分片迁移窗口一般需要2-4周。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-fen-ku-fen-biao-shi-zhan-shardingsphere-chui-zhi-chai/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐