分类:架构与高并发 · 作者:管理员 · 发布于 2026-10-03 · 阅读 0 · 标签:分库分表 / ShardingSphere / 读写分离 / 电商架构 / 分布式主键

一、场景痛点:一张订单表撑起整个中台

电商中台跑了两三年后,订单表往往最先撞上单库天花板:

  • 数据量过亿,索引层级变深,写入锁竞争加剧,慢查询开始日常化;
  • 单库连接数打满,应用线程池排队,接口 RT 陡增;
  • 大促洪峰时单库 QPS 到顶,扩从库只能缓解读,写瓶颈无解;
  • 备份与恢复时间随体量线性增长,夜间维护窗口不够用。

这个阶段"加索引、加缓存、加从库"往往已经做过了,下一步就是分库分表。本文以典型电商中台为背景,讲清拆分思路与 ShardingSphere-JDBC 的落地配置。

二、拆分方案:先垂直后水平,分片键是灵魂

1. 垂直拆分

按业务域拆库:订单库、支付库、商品库、用户库各管一摊,库之间不 JOIN,通过应用层组装或消息解耦。垂直拆分解决"不同业务互相拖累"的问题,属于架构层面的一次性动作,改动面大但收益立竿见影。

2. 水平拆分

数据量最大的订单表按分片键散列到多个库多张表。分片键选型是核心决策:

  • user_id(用户维度):最贴合业务,用户查"我的订单"天然带上分片键,命中率最高;
  • order_id(订单维度):适合订单详情、后台运营查询,但用户维度查询会触发全路由;
  • 折中做法:主分片键用 user_id,order_id 用雪花算法生成并在订单表冗余 user_id 列,需要按 order_id 反查时先查 user_order 映射表拿到 user_id。

分片数量建议一次性规划 16 或 32 张表 × 2 至 4 个库。"先 2 个库以后扩容"的教训是:取模分片扩容要迁移数据,非常痛苦。

三、ShardingSphere-JDBC 落地:分片配置

ShardingSphere-JDBC 以轻量 jar 形式嵌入应用,对业务代码几乎零侵入,是中小团队最稳妥的选型。以两个库(order_ds0 / order_ds1)、订单表 t_order 拆 16 张为例(所有资源名均为占位示例):

# sharding.yaml(ShardingSphere 5.5.x)
dataSources:
  order_ds0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://db0.example.com:3306/order_db?serverTimezone=Asia/Shanghai&useSSL=false
    username: app_user
    password: ******
  order_ds1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://db1.example.com:3306/order_db?serverTimezone=Asia/Shanghai&useSSL=false
    username: app_user
    password: ******
rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: order_ds${0..1}.t_order_${0..15}
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: t_order_inline
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake
    defaultDatabaseStrategy:
      standard:
        shardingColumn: user_id
        shardingAlgorithmName: database_inline
    shardingAlgorithms:
      database_inline:
        type: INLINE
        props:
          algorithm-expression: order_ds${user_id % 2}
      t_order_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${user_id % 16}
    keyGenerators:
      snowflake:
        type: SNOWFLAKE
props:
  sql-show: true

要点:

  • 分库分表都用 INLINE 行表达式,${user_id % 2} 表示按 user_id 取模路由;
  • order_id 由 SNOWFLAKE 生成,全局唯一且有序,天然适配分片场景;
  • 启动后打开 sql-show,通过日志里的 Logic SQL / Actual SQL 观察路由是否符合预期。

四、读写分离:主库写、从库读

分片解决容量问题,读写分离解决"读多写少"的并发问题。一个典型的订单域拓扑:主库承载写流量,两个从库扛读流量。注意只有读多写少才值得上,主从延迟敏感的查询要强制走主库:

# readwrite.yaml(与分片规则可合并进同一 rules)
rules:
  - !READWRITE_SPLITTING
    dataSources:
      order_rw:
        writeDataSourceName: order_ds0
        readDataSourceNames:
          - order_ds0_read0
          - order_ds0_read1
        transactionalReadQueryStrategy: PRIMARY
        loadBalancerName: random
    loadBalancers:
      random:
        type: RANDOM

注意 transactionalReadQueryStrategy: PRIMARY:事务内的读强制走主库,避免"先写后读"读到从库旧数据。主从延迟较大时(如大促批量更新),可对关键接口用 Hint 强制主库路由。

五、踩坑记录

1. 跨分片分页与聚合

分片后 LIMIT 100000, 20 这类深分页会被下推到每个分片各自取 100020 条再归并,越翻越慢。解法:

  • 禁止深分页,改成游标分页(WHERE user_id = ? AND order_id < ? ORDER BY order_id DESC LIMIT 20);
  • 后台运营的复杂报表不要直接查分片表,落地到宽表或数仓再查。

2. 分布式事务

分库后"下单扣库存 + 写订单"跨库,本地事务失效。方案按一致性要求选:

  • 最终一致性:本地消息表 + MQ 异步对账(订单创建成功才发消息,消费方做幂等);
  • 强一致:Seata AT 模式,注意 Seata 要单独部署事务协调器,别指望零成本;
  • 原则:交易链路尽量收敛到"一个库内完成",跨库操作越少越好。

3. 广播表

字典、区域、促销规则这类"所有分片都要能 JOIN"的小表,配置成 BROADCAST 广播表,每个分片全量复制一份,避免跨库 JOIN:

  - !BROADCAST
    tables:
      - t_dict

4. 数据迁移

存量数据迁移用"双写 + 校验 + 灰度切读":应用先双写新旧两套,跑数据校验任务比对差异,比对通过后灰度把读流量切到新库,最后只留新库。切忌一次性 cutover。

六、最佳实践清单

  • 能不分就不分:先缓存、先索引、先读写分离,分库分表是最后的扩容手段;
  • 分片键必须出现在 WHERE 中,否则触发全路由,等于把每个分片全扫一遍;
  • 分片数量一次规划到位(建议 16 或 32 张),取模扩容要迁移数据,成本极高;
  • 订单与订单明细同分片键的表配置 bindingTables,JOIN 时避免笛卡尔路由;
  • 把"分片键必传"作为接口契约,缺少分片键的查询直接拒绝而非全路由;
  • 上线前压测关注 Actual SQL 条数:一次逻辑查询变成 N 次物理查询,RT 会成倍放大。

最后提醒一句:分库分表治标,业务上"按用户维度收敛数据访问"才是治本。把用户的订单、购物车、优惠券都按 user_id 组织,中台才能平滑长大。