Sharding-JDBC 分库分表实战:从 800 万订单表的查询优化说起

0 阅读7分钟

一、背景:当订单表扛不住了

在电商平台的订单中心,单表数据量突破 800 万行后,查询开始明显变慢。订单查询接口 P99 从 200ms 涨到了 1200ms,EXPLAIN 一跑,典型的全表扫描。

这不是孤例。当单表数据超过 5000 万行时,即使是简单的 SELECT * FROM table WHERE id = ?,也可能因为 B+ 树深度增加而导致性能急剧下降。订单表按日均 5 万+ 的增长速度,很快会触及这个天花板。

分库分表是必由之路。我选择 Sharding-JDBC,因为它以 jar 包形式集成,无需额外部署,兼容 JDBC 和 MyBatis,对业务代码侵入极低。

二、分片键选型:80% 的故障从这里开始

分片键选错了,后面全是坑。根据生产环境的经验,60% 的分库分表故障都源于分片键设计错误。

选分片键有三条黄金法则,订单表场景下最关键:

第一,高频查询匹配原则。 订单表 80% 的查询是“查我的订单”,条件里一定带 user_id。如果选 order_id 做分片键,按用户查订单时就会全分片扫描。

第二,数据均匀分布原则。 不能用 status(只有几个枚举值)这种低基数字段,否则数据严重倾斜。

第三,不可变原则。 user_id 创建后不会变,status 会从“待支付”变“已完成”,用会变的字段分片,数据会在分片间迁移,灾难。

最终方案:分库键 user_id,分表键 order_id。这样用户维度的查询能精准路由到指定库,而同一用户的订单按订单号分散在不同表,避免单表热点。

三、配置实战:Sharding-JDBC 怎么配

3.1 基础配置(2 库 2 表)

yaml

spring:
  shardingsphere:
    # 数据源列表
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0?serverTimezone=UTC
        username: root
        password: root
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_1?serverTimezone=UTC
        username: root
        password: root

    # 分片规则
    sharding:
      tables:
        t_order:
          # 实际数据节点:ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1
          actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
          
          # 分库策略:按 user_id 取模
          database-strategy:
            inline:
              sharding-column: user_id
              algorithm-expression: ds$->{user_id % 2}
          
          # 分表策略:按 order_id 取模
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: t_order_$->{order_id % 2}
          
          # 分布式主键:雪花算法
          key-generator:
            column: order_id
            type: SNOWFLAKE

关键配置解析:

  • actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}:这是行表达式,最终解析为 ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1 四个物理表。
  • database-strategy.inline:使用行表达式分片策略,Groovy 语法实现简单取模。
  • key-generator.type: SNOWFLAKE:使用雪花算法生成分布式主键,避免分表后自增 ID 重复。

3.2 分布式主键配置详解

分表后自增主键必然重复——两张表各自从 1 开始自增,全局主键冲突。雪花算法通过“时间戳 + 机器 ID + 序列号”保证全局唯一。

yaml

key-generator:
  column: order_id
  type: SNOWFLAKE
  props:
    worker-id: 1  # 机器唯一标识,集群中每台不同
    max-tolerate-time-difference-milliseconds: 10  # 最大容忍时钟回退时间
属性说明默认值
worker-id工作机器唯一标识,集群模式下每台机器不同0
max-tolerate-time-difference-milliseconds最大容忍时钟回退时间(毫秒)10
max-vibration-offset最大抖动上限,范围 [0, 4096)1

注意:如果雪花算法生成的 ID 直接用作分片键,建议配置 max-vibration-offset,否则生成的 ID 取模后可能集中在少数分片。

3.3 绑定表配置(避免笛卡尔积)

订单表和订单项表如果都按 order_id 分片,但没配绑定表关系,关联查询会执行 N×N 次跨分片关联。

yaml

sharding:
  binding-tables:
    - t_order,t_order_item

配置绑定表后,Sharding-JDBC 知道这两张表的分片规则一致,关联查询可以精准路由到对应的分片,而不是广播。

3.4 广播表配置(字典表同步)

有些表(如 t_config、t_dict)需要在每个库中都有一份完整数据,用于关联查询。

yaml

sharding:
  broadcast-tables:
    - t_config

广播表在写入时会同步到所有分片,查询时从任意分片读取即可。

3.5 完整配置模板

yaml

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0?serverTimezone=UTC
        username: root
        password: root
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_1?serverTimezone=UTC
        username: root
        password: root

    sharding:
      # 默认数据源(不分片的表走这里)
      default-data-source-name: ds0
      
      tables:
        t_order:
          actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
          database-strategy:
            inline:
              sharding-column: user_id
              algorithm-expression: ds$->{user_id % 2}
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: t_order_$->{order_id % 2}
          key-generator:
            column: order_id
            type: SNOWFLAKE
            props:
              worker-id: 1
              max-tolerate-time-difference-milliseconds: 10
              
        t_order_item:
          actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..1}
          database-strategy:
            inline:
              sharding-column: user_id
              algorithm-expression: ds$->{user_id % 2}
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: t_order_item_$->{order_id % 2}
          key-generator:
            column: item_id
            type: SNOWFLAKE
      
      # 绑定表:订单与订单项分片规则一致,关联查询不广播
      binding-tables:
        - t_order,t_order_item
      
      # 广播表:每个库都有一份完整数据
      broadcast-tables:
        - t_config
        - t_dict
    
    # 开启 SQL 日志,便于验证路由
    props:
      sql:
        show: true

四、效果验证:SQL 真的路由对了吗

配完不是结束,必须验证。开启 sql-show: true 后,观察实际执行的 SQL:

sql

-- 插入 user_id=1 的订单,order_id=1001
INSERT INTO t_order_1 (order_id, user_id, ...) VALUES (1001, 1, ...) ::: ds1.t_order_1

-- 插入 user_id=2 的订单,order_id=1002  
INSERT INTO t_order_0 (order_id, user_id, ...) VALUES (1002, 2, ...) ::: ds0.t_order_0

user_id=1 路由到 ds1,user_id=2 路由到 ds0,分库规则生效。order_id 的尾数决定落在哪张分表。

查询时同理:WHERE user_id = 1 自动路由到 ds1,WHERE user_id = 2 路由到 ds0,不会全库扫描。EXPLAIN 显示 type=ref,扫描行数从 800 万降到 156 行。

五、避坑指南:生产环境最常踩的 5 个坑

坑一:非分片键查询导致全路由。 如果查询条件里没有 user_id 也没有 order_id,Sharding-JDBC 会广播到所有分片,性能比不分表还差。对策:核心查询必须带分片键。

坑二:绑定表未配置导致笛卡尔积。 订单表和订单项表没配 binding-tables,关联查询执行 N×N 次跨分片关联。对策:分片规则一致的主子表,配置绑定关系。

坑三:雪花算法 ID 作分片键的陷阱。 雪花 ID 取模 2^n 后可能集中在少数分片。对策:配置 max-vibration-offset,或避免直接用雪花 ID 分片。

坑四:读写分离的主从延迟。 主库写入后立即查询,从库可能还没同步。对策:核心业务强制路由主库。

坑五:分页查询的性能陷阱。 LIMIT 10000, 20 在分片场景下需要从每个分片取 10020 条再归并,性能极差。对策:使用分片键 + 游标分页。

六、总结:分库分表的本质是权衡

分库分表不是银弹,它用分布式复杂性换取了性能和扩展性。订单表场景下,选对分片键(user_id + order_id),配好雪花算法主键,验证路由正确性,避开关联查询和扩容的坑,就能让 800 万订单表的查询重回毫秒级。

回到开头的问题:EXPLAIN 显示 type=ALL 的全表扫描,在分库分表后变成了 type=ref 的索引查找,P99 从 1200ms 回到了 180ms。这才是分库分表真正的价值——不是“数据能存更多”,而是“查询依然快”。


配置补充说明:以上配置基于 ShardingSphere 5.x 版本,核心的 actual-data-nodes、database-strategy、table-strategy、key-generator 和 binding-tables 都是官方文档中明确支持的标准配置项。雪花算法的 worker-id 在单机模式下可手动指定,集群模式下系统自动生成以保证不重复。