MySQL分库分表实战:ShardingSphere分片策略与扩容方案

单表数据量超过千万行、单库写入QPS逼近硬件上限,就需要分库分表ShardingSphere-JDBC是客户端形态的分库分表中间件,以jar包嵌入应用,无独立代理层,性能损耗低,适合大多数Java技术栈。分片键选择和扩容方案是落地时的两大难点,选错分片键,后续所有优化都是在还债。

分片键选择:查询模式决定数据分布

分片键的选择原则:绝大多数高频查询条件里都包含这个字段,且写入分布均匀。以订单库为例,C端查询基本都带user_id,运营后台查询带order_time和商家ID,两边的查询模式冲突。常见的取舍是以user_id做分片键,保证单用户订单落在同一库,单表查询不带分片键的走广播或异构索引表。

-- 数据分布规划
-- db0: t_order_0, t_order_1
-- db1: t_order_2, t_order_3

-- 分片键 user_id % 4 确定表,再路由到库
-- user_id=1001 的订单永远落在 t_order_1
SELECT * FROM t_order WHERE user_id = 1001;  -- 单表路由

反例是用自增order_id做分片键:写入确实均匀,但C端查询都带user_id不带order_id,每次查询都广播到所有分片再聚合,分库分表把单表查询压力放大成了全集群扫描。

ShardingSphere-JDBC配置:分片规则落地

Spring Boot项目引入shardingsphere-jdbc依赖,配置分片规则。实际分片算法推荐用INLINE表达式,简单场景够用且性能最好。

# application.yml 核心配置
rules:
- !SHARDING
  tables:
    t_order:
      actual-data-nodes: ds$->{0..1}.t_order_$->{0..3}
      database-strategy:
        standard:
          sharding-column: user_id
          sharding-algorithm-name: db-inline
      table-strategy:
        standard:
          sharding-column: user_id
          sharding-algorithm-name: order-table-inline
  sharding-algorithms:
    db-inline:
      type: INLINE
      props:
        algorithm-expression: ds$->{user_id % 2}
    order-table-inline:
      type: INLINE
      props:
        algorithm-expression: t_order_$->{user_id % 4}

actual-data-nodes定义真实节点,4张表分布在2个库。查询SQL里带user_id时精确路由到单库单表,不带分片键时全路由,控制台SQL日志里能看到路由结果,上线前重点检查全路由SQL的占比。

全局ID生成:避免分片后主键冲突

分表后数据库自增主键不再全局唯一,需要应用层ID方案。常用三种:雪花算法、号段模式、UUID。雪花算法生成有序64位ID,含时间戳和机器位,需要解决时钟回拨问题,适合大多数场景。

// 雪花算法时间戳位设置,容忍时钟回拨2秒
public class SnowflakeIdGen {
    private final long epoch = 1735689600000L; // 2025-01-01
    private final long workerId;

    public synchronized long nextId() {
        long now = System.currentTimeMillis();
        if (now < lastTimestamp) {
            // 回拨在2ms内等待,超过则抛异常告警
            if (lastTimestamp - now < 2) {
                try { wait(lastTimestamp - now); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            } else {
                throw new IllegalStateException("时钟回拨过大");
            }
            now = System.currentTimeMillis();
        }
        // 时间戳左移22位拼机器位与序列号,此处省略
        return compose(now, workerId, sequence);
    }
}

机器位分配用配置中心统一管理,或者用Redis自增序列临时领取,避免多实例拿到相同workerId生成重复ID。

平滑扩容方案:双写迁移与数据回填

2库4表扩到4库8表,直接重写分片路由会导致老数据位置错乱。平滑扩容走双写迁移:老库继续服务,新库结构建好;写请求双写新老两个集群;存量数据按新规则回填;读逐步切到新集群;观察无误后停老库写。

扩容期间的读写策略:
1. 双写开启,写老库为主,写新库为异步(失败只记日志)
2. 存量数据回填:按分片键扫描老库,按新规则写入新库
3. 回填完成后做数据校验:
   SELECT COUNT(*), SUM(CRC32(CONCAT(user_id, order_id)))
   FROM t_order WHERE create_time < '切换时间点';
   -- 新老两侧结果必须一致
4. 读流量灰度切换到新库:5% -> 50% -> 100%
5. 写老库改为仅日志,观察一周后下线老库

回填务必按分片键并行,单线程回填千万级数据要跑数小时。校验环节用聚合签名对比而不是逐行比对,两张千万级表逐行diff性能不可接受。

跨分片查询治理:异构索引与ES分流

运营后台的复杂条件查询、多维统计、模糊搜索,这类需求不该打在分片集群上。成熟做法是Binlog同步到Elasticsearch,后台查询走ES,分片集群只服务核心交易链路。ES里文档结构面向查询设计,分片集群面向交易设计,两边各司其职。同步链路用Canal或Flink CDC,延迟通常在秒级,后台查询对实时性的要求基本都能满足。把这个边界划清楚,分库分表方案才不会陷入为长尾查询不断加广播的泥潭。

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

(0)
小编小编
上一篇 3天前
下一篇 3天前

相关推荐

MySQL分库分表实战:ShardingSphere分片策略与路由配置

MySQL 分库分表实战:ShardingSphere 分片策略与路由配置

数据库运维中,单库单表到一定规模后性能会遇到瓶颈。MySQL 分库分表是应对海量数据的常规手段,ShardingSphere 作为成熟的数据库中间件,通过 JDBC 驱动形式无缝嵌入应用,支持分片、读写分离、数据加密。本文从分片键选择、分片算法、读写分离到扩容方案,给出生产可落地的配置与代码示例。

# application.yml - ShardingSphere 分库分表配置
spring:
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_0
        username: root
        password: secret
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_1
        username: root
        password: secret
    rules:
      sharding:
        tables:
          t_order:
            actualDataNodes: ds$->{0..1}.t_order_$->{0..1}
            tableStrategy:
              standard:
                shardingColumn: order_id
                shardingAlgorithmName: order_hash_mod
            keyGenerateStrategy:
              column: id
              keyGeneratorName: snowflake
        shardingAlgorithms:
          order_hash_mod:
            type: HASH_MOD
            props:
              sharding-count: 4

分片键设计与哈希取模分片算法

分片键必须选查询频率最高的字段。订单表按 order_id 或 user_id 分片,日志表按时间字段分片,避免跨片查询。HASH_MOD 对分片键取模,数据分布均匀,但要提前规划分片数,后续扩片需要迁移数据。Range 分片(按时间区间)适合日志与流水,数据按时间顺序落不同分片,冷热分离方便。ShardingSphere 用 sharding-count 声明分片总数,数据按哈希结果路由到 ds0/ds1 与对应表。

读写分离配置与强制主库路由

分库分表之后,读写分离是数据库运维必配项。在主从架构中,ShardingSphere 将写请求路由到主库,读请求根据负载策略分发到从库。配置 load-balancers 指定从库负载算法,如 ROUND_ROBIN。注意:对于强一致性读(如下单后立即查询订单),用 HintManager 强制走主库,否则主从延迟会导致刚写的数据查不到。启用 readSensitive 参数控制事务内读请求是否走从库,事务内默认强制主库。

HintManager hintManager = HintManager.getInstance();
hintManager.setPrimaryDataSourceOnly();   // 强制主库
try {
    return orderMapper.selectById(orderId);
} finally {
    hintManager.close();
}

分库分表后的数据迁移与扩容方案

分片数不足时数据迁移是工程难点。推荐用双写方案:新库旧库同时写入,历史数据用 DataX 或 Canal 全量+增量同步,数据比对通过一致性校验工具核对,全部一致后切流量。扩容前先规划目标分片数,把分片数设为 2 的幂次(如 4→8→16),HASH_MOD 按幂次扩展只用迁移一半数据,比取模到新模数省力。迁移期间数据库运维要监控延迟与主从差距,扩容窗口选业务低峰。

分库分表性能监控与常见问题排查

部署后要监控路由正确性、各分片负载与慢查询。ShardingSphere 暴露了路由日志(shardingsphere.routing)与 SQL 执行日志,打开 debug 可查看每条 SQL 的路由结果;在物理库上做慢查询监控,看是否因分片不均导致局部热点。常见问题:未按分片键查询导致全库扫描,生成 SQL 出现错误,跨分片聚合性能差。应对办法:查询全部带分片键、用分布式事务中间件处理跨库事务、对汇总需求做预聚合表。分库分表是最后手段,能用缓存和索引解决的先用简单方案。

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

(0)
小编小编
上一篇 4天前
下一篇 4天前

相关推荐

MySQL分库分表实战:ShardingSphere分片策略与跨库查询方案

单表数据量超过千万行后,查询性能会显著下降,B+树索引层级增加导致IO放大,写操作的锁竞争也会加剧。分库分表是处理海量数据的标准方案,但引入了跨库JOIN、分布式事务、全局ID等新问题。本文以Apache ShardingSphere为中间件,记录从分片策略设计到跨库查询优化的完整实战过程。

分片策略评估与数据量估算

分库分表前需评估数据增长趋势和访问模式。以电商订单系统为例,假设日均订单量50万,单表保留2年数据约3.65亿行,远超单表性能拐点。

# 数据量估算
daily_orders = 500_000
retention_days = 730  # 2年
total_rows = daily_orders * retention_days  # 3.65亿行

# 分片方案
# 2个数据库实例 x 8个表 = 16个分片
# 每个分片约2280万行,在MySQL单项性能拐点以内
num_databases = 2
tables_per_db = 8
total_shards = num_databases * tables_per_db  # 16
rows_per_shard = total_rows / total_shards  # 约2280万

# 分片键选择
# 订单表:user_id取模 -> 同一用户的订单落在同一分片
# 查询场景:用户维度查询最高频,按user_id分片最合理

ShardingSphere分片规则配置

使用ShardingSphere-JDBC作为分片中间件,通过YAML配置分片规则。以订单表和订单明细表为例,按user_id分片。

# application-sharding.yml
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://192.168.30.21:3306/order_db_0?useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: order_app
    password: Order2026!
    connectionTimeout: 30000
    idleTimeout: 60000
    maxLifetime: 1800000
    maximumPoolSize: 50

  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://192.168.30.22:3306/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: order_app
    password: Order2026!
    connectionTimeout: 30000
    idleTimeout: 60000
    maxLifetime: 1800000
    maximumPoolSize: 50

rules:
  - !SHARDING
    tables:
      # 订单主表:2库 x 8表 = 16分片
      t_order:
        actualDataNodes: ds_${0..1}.t_order_${0..7}
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: db_mod
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: table_mod

      # 订单明细表:与订单表同分片键
      t_order_item:
        actualDataNodes: ds_${0..1}.t_order_item_${0..7}
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: db_mod
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: table_mod

      # 订单明细绑定关系(保证与主表同库同表)
    bindingTables:
      - t_order, t_order_item

    # 广播表(字典表,所有分片都有完整副本)
    broadcastTables:
      - t_product_category
      - t_payment_method

    shardingAlgorithms:
      # 数据库分片:user_id % 2
      db_mod:
        type: MOD
        props:
          sharding-count: 2
      # 表分片:user_id % 8
      table_mod:
        type: MOD
        props:
          sharding-count: 8

    # 分布式ID生成策略
    keyGenerators:
      snowflake:
        type: SNOWFLAKE
        props:
          worker-id: 1

Spring Boot集成配置:

# pom.xml
<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>shardingsphere-jdbc</artifactId>
  <version>5.5.1</version>
</dependency>

# application.yml
spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:application-sharding.yml
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10

全局唯一ID方案:Snowflake优化配置

分库分表后MySQL自增ID不再保证全局唯一。Snowflake算法通过时间戳+机器ID+序列号生成64位ID,需确保worker-id在集群内不重复。

// 自定义Snowflake ID生成器(Java实现)
public class SnowflakeIdGenerator {

    // 起始时间戳:2024-01-01 00:00:00 UTC
    private static final long EPOCH = 1704067200000L;

    // 各部分位数
    private static final long WORKER_ID_BITS = 10L;   // 机器ID 10位
    private static final long SEQUENCE_BITS = 12L;     // 序列号 12位

    // 最大值
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);  // 1023
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);    // 4095

    // 位移
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    private final long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(
                "workerId超出范围: 0-" + MAX_WORKER_ID);
        }
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        // 时钟回拨检测
        if (currentTimestamp < lastTimestamp) {
            throw new IllegalStateException(
                "时钟回拨 " + (lastTimestamp - currentTimestamp) + "ms");
        }

        if (currentTimestamp == lastTimestamp) {
            // 同一毫秒内序列号递增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                // 序列号耗尽,等待下一毫秒
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
            | (workerId << WORKER_ID_SHIFT)
            | sequence;
    }

    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

// workerId自动分配(基于ZooKeeper)
public class ZookeeperWorkerIdAssigner {

    private final CuratorFramework zkClient;
    private static final String PATH = "/snowflake/workers";

    public long assignWorkerId() throws Exception {
        // 创建临时顺序节点
        String nodePath = zkClient.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(PATH + "/worker-");

        // 从节点路径提取序号作为workerId
        String[] parts = nodePath.split("-");
        long workerId = Long.parseLong(parts[parts.length - 1]);

        if (workerId > 1023) {
            throw new IllegalStateException("workerId超过1023上限");
        }

        return workerId;
    }
}

跨库查询与绑定表优化

分库分表后最大的痛点是跨库JOIN。ShardingSphere通过绑定表(binding table)保证拥有相同分片键和分片算法的表数据落在同一分片,使JOIN操作在单分片内完成。

-- 绑定表JOIN:t_order和t_order_item按相同user_id分片
-- ShardingSphere会将JOIN下推到单分片执行,无需跨库
SELECT o.order_id, o.user_id, o.total_amount, oi.product_name, oi.quantity
FROM t_order o
JOIN t_order_item oi ON o.order_id = oi.order_id
WHERE o.user_id = 100086;
-- 路由到: ds_0.t_order_6 JOIN ds_0.t_order_item_6(同一分片)

-- 问题场景:不带分片键的查询会全分片扫描
SELECT * FROM t_order WHERE order_id = 'ORD202608050001';
-- ShardingSphere广播到所有16个分片,性能很差

-- 解决方案1:建立order_id -> user_id的路由表
SELECT user_id FROM t_order_id_mapping WHERE order_id = 'ORD202608050001';
-- 先查出user_id,再带分片键查询
SELECT * FROM t_order WHERE user_id = ? AND order_id = 'ORD202608050001';

-- 解决方案2:使用广播表JOIN
-- t_payment_method是广播表(每个分片都有完整数据)
SELECT o.*, pm.method_name
FROM t_order o
JOIN t_payment_method pm ON o.payment_method_id = pm.id
WHERE o.user_id = 100086;
-- 广播表JOIN在单分片内完成,无需跨库

分页查询的跨库合并是另一个性能瓶颈。ShardingSphere采用流式归并排序处理分页,但深度分页(大offset)性能急剧下降:

-- 分页查询:第一页(各分片取前20条,归并后取20条)
SELECT * FROM t_order WHERE user_id = 100086 ORDER BY created_at DESC LIMIT 0, 20;
-- 16个分片各取20条,共320条,归并排序后取前20条

-- 深度分页:第100页(LIMIT 1980, 20)
SELECT * FROM t_order WHERE user_id = 100086 ORDER BY created_at DESC LIMIT 1980, 20;
-- 16个分片各取2000条,共32000条,归并排序后丢弃前1980条取20条
-- 性能极差,IO和内存开销大

-- 优化方案:游标分页(基于上一页最后一条记录的created_at)
SELECT * FROM t_order 
WHERE user_id = 100086 AND created_at < '2026-08-05 10:30:00'
ORDER BY created_at DESC 
LIMIT 20;
-- 各分片仅返回20条,无需大offset扫描

分布式事务与数据一致性方案

分库分表后跨库事务无法依赖MySQL本地事务。ShardingSphere提供XA和BASE两种分布式事务方案。

// XA事务:强一致性,适用于资金交易类场景
// 配置XA事务管理器
@Configuration
public class XATransactionConfig {
    @Bean
    public XATransactionManagerDataSource xaTransactionManagerDataSource(
            DataSource dataSource) {
        return new XATransactionManagerDataSource(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(
            XATransactionManagerDataSource xaDataSource) {
        return new DataSourceTransactionManager(xaDataSource);
    }
}

// XA事务使用(@Transactional注解自动启用)
@Service
public class OrderTransactionService {

    @Transactional
    public void createOrderWithItems(OrderDTO orderDTO) {
        // 这两个操作可能落在不同分片(不同数据库实例)
        // XA保证两个操作原子性提交或回滚
        orderMapper.insert(orderDTO);
        for (OrderItemDTO item : orderDTO.getItems()) {
            orderItemMapper.insert(item);
        }
        // 扣减库存(可能在不同分片)
        inventoryMapper.deduct(item.getProductId(), item.getQuantity());
    }
}

// BASE事务:最终一致性,性能更高,适用于非核心场景
// 使用Seata AT模式
@GlobalTransactional(timeoutMills = 60000)
public void createOrderWithItemsBASE(OrderDTO orderDTO) {
    orderMapper.insert(orderDTO);
    orderItemMapper.insertBatch(orderDTO.getItems());
    // 远程调用库存服务
    inventoryFeignClient.deduct(orderDTO);
    // 异常时自动回滚所有分支事务
}

XA事务的性能开销约为本地事务的3-5倍,因为需要两阶段提交和全局锁。仅在强一致性要求的场景使用,订单创建等高频操作建议采用BASE事务+补偿机制。

数据迁移与扩容方案

分片数量变化(扩容)需要数据重新分布,是分库分表后的重大运维挑战。常见方案是双倍扩容——从N个分片扩到2N个分片,利用一致性分片算法减少数据迁移量。

// 扩容流程示例:2库4表 -> 4库8表
// 步骤1:建立新分片的数据源配置
// 步骤2:数据同步(全量+增量)
// 步骤3:双写新分片(写入时同时写新旧分片)
// 步骤4:读取切换到新分片
// 步骤5:清除旧分片数据

# 迁移脚本(全量同步示例)
python3 migrate_data.py \
    --source "jdbc:mysql://192.168.30.21:3306/order_db_0" \
    --target "jdbc:mysql://192.168.30.21:3306/order_db_0,192.168.30.23:3306/order_db_2" \
    --table t_order \
    --sharding-key user_id \
    --batch-size 5000 \
    --parallel 4

分库分表是数据量增长到一定规模后的必选项,ShardingSphere作为Java生态主流分片中间件,对应用层透明且功能完善。核心设计原则:分片键选择需匹配最高频查询场景、尽量使用绑定表减少跨库JOIN、深度分页用游标替代offset、分布式事务按一致性要求分级选择XA或BASE。运维层面需建立完善的数据迁移流程和分片监控指标,提前规划扩容窗口期。

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

(0)
小编小编
上一篇 2026年8月5日
下一篇 2026年8月5日

相关推荐

MySQL分库分表实战:ShardingSphere分片策略配置与跨库查询优化

单表数据量超过千万级后,MySQL查询性能会显著下降,即使SQL查询优化和索引调整也难以根本解决。分库分表是处理海量数据的标准方案。Apache ShardingSphere作为轻量级Java框架,通过改写SQL实现透明化分片,应用层无需感知底层分片逻辑。本文演示ShardingSphere-JDBC的完整配置流程和跨库查询优化策略。

分库分表方案选型:水平分片与垂直分片

垂直分表按字段拆分,将热点字段和冷字段分离到不同表。垂直分库按业务模块拆分到不同数据库实例。水平分表在同一库内按行拆分到多个表。水平分库将数据分散到多个数据库实例,是处理亿级数据的核心手段。

水平分片的关键是分片键的选择。分片键决定数据如何分布,直接影响查询路由效率。以订单系统为例,按user_id分片可以让同一用户的订单落在同一库表,用户维度的查询只需访问一个分片。但如果按订单ID查询且不携带user_id,则需广播到所有分片。

ShardingSphere-JDBC环境搭建

ShardingSphere-JDBC以JDBC扩展形式存在,无需额外部署代理层,性能损耗极小。Spring Boot项目集成步骤:

<!-- Maven依赖 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc</artifactId>
    <version>5.5.0</version>
</dependency>

<!-- 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://192.168.1.10:3306/order_db_0?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
        username: root
        password: YourPassword
        maximum-pool-size: 20
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
        username: root
        password: YourPassword
        maximum-pool-size: 20

分片策略与算法配置

分片策略包含分库策略和分表策略。以订单表为例:2个库 x 4张表 = 8个分片,按user_id取模路由。

spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds$->{0..1}.t_order_$->{0..3}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-mod
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake
          
          t_order_item:
            actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..3}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: table-mod
        
        sharding-algorithms:
          db-mod:
            type: MOD
            props:
              sharding-count: 2
          table-mod:
            type: MOD
            props:
              sharding-count: 4
        
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1

绑定表与广播表配置

订单表和订单明细表是典型的绑定表关系,两者按相同维度分片可以保证关联查询在同一库内完成,避免跨库JOIN。广播表(如字典表、配置表)在所有分片中完整存储,便于JOIN查询。

spring:
  shardingsphere:
    rules:
      sharding:
        binding-tables:
          - t_order,t_order_item
        
        broadcast-tables:
          - t_dict_order_status
          - t_dict_payment_type

分片后的跨库查询问题与优化

分片后最大的挑战是不包含分片键的查询。ShardingSphere会将其路由到所有分片执行,合并结果返回。这种广播查询性能差,需要针对性优化。

以按order_id查询为例,order_id不是分片键,ShardingSphere需扫描所有8个分片:

-- 应用层SQL(透明分片,开发者无需感知)
SELECT * FROM t_order WHERE order_id = 123456789;

-- ShardingSphere实际执行的SQL(路由到所有分片)
-- ds0.t_order_0: SELECT * FROM t_order_0 WHERE order_id = 123456789
-- ds0.t_order_1: SELECT * FROM t_order_1 WHERE order_id = 123456789
-- ...共8条SQL

解决方案是在order_id中编码分片信息。Snowflake ID天然包含worker ID,可将其与分片映射关联。或建立二级索引表,存储order_id到user_id的映射,先查索引表获取分片键再精准路由。

// 二级索引方案:先查索引表定位分片
public Order findByOrderId(Long orderId) {
    Long userId = jdbcTemplate.queryForObject(
        "SELECT user_id FROM t_order_index WHERE order_id = ?",
        Long.class, orderId
    );
    
    return jdbcTemplate.queryForObject(
        "SELECT * FROM t_order WHERE order_id = ? AND user_id = ?",
        orderRowMapper, orderId, userId
    );
}

// 方案2: Hint强制路由
public List<Order> findByOrderIds(List<Long> orderIds) {
    List<Order> results = new ArrayList<>();
    
    for (Long orderId : orderIds) {
        long workerId = (orderId >> 12) & 0x3FF;
        int shardIndex = (int)(workerId % 8);
        
        HintManager hintManager = HintManager.getInstance();
        hintManager.addDatabaseShardingValue("t_order", shardIndex / 4);
        hintManager.addTableShardingValue("t_order", shardIndex % 4);
        
        try {
            Order order = jdbcTemplate.queryForObject(
                "SELECT * FROM t_order WHERE order_id = ?",
                orderRowMapper, orderId
            );
            if (order != null) results.add(order);
        } finally {
            hintManager.close();
        }
    }
    return results;
}

分页查询的跨库归并问题

分片的分页查询是性能重灾区。LIMIT 100000, 10在分片场景下,每个分片都要取100010条数据再归并排序,ShardingSphere取前N条返回,内存和网络开销极大。

深度分页优化方案:使用游标分页替代offset分页,每次记住上一页最后一条记录的排序值,下一页从该值之后查询。

-- 反面案例:深度偏移分页(分片下性能极差)
SELECT * FROM t_order ORDER BY create_time DESC LIMIT 100000, 10;

-- 优化方案:游标分页
-- 第一页
SELECT * FROM t_order 
WHERE user_id = ? 
ORDER BY create_time DESC 
LIMIT 10;

-- 第二页
SELECT * FROM t_order 
WHERE user_id = ? AND create_time < '2026-08-01 12:00:00'
ORDER BY create_time DESC 
LIMIT 10;

-- 如果必须用offset,限制最大页数
-- 超过100页提示用户缩小筛选范围

分布式事务处理方案

分库后跨库事务无法使用MySQL本地事务。ShardingSphere支持XA和BASE两种分布式事务模式。

// XA事务(强一致性,性能较低)
@Transactional
public void createOrder(OrderDTO dto) {
    XATransactionManager.begin();
    try {
        orderMapper.insert(dto.toOrder());
        orderItemMapper.insertBatch(dto.toOrderItems());
        stockMapper.deduct(dto.getProductId(), dto.getQuantity());
        XATransactionManager.commit();
    } catch (Exception e) {
        XATransactionManager.rollback();
        throw e;
    }
}

// BASE事务(最终一致性,性能好,推荐)
// 使用Seata AT模式
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    orderMapper.insert(dto.toOrder());
    orderItemMapper.insertBatch(dto.toOrderItems());
    stockMapper.deduct(dto.getProductId(), dto.getQuantity());
}

分库分表是数据增长到一定规模后的必然选择,ShardingSphere提供了较完善的分片中间件能力。实施前需做好容量规划,确定分片数量时考虑未来3-5年的数据增长。分片键选择是成败关键,应根据核心查询场景确定。数据迁移实战中建议使用双写方案:新老架构并行写入,逐步切读流量,验证无误后下线旧架构。

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

(0)
小编小编
上一篇 2026年8月5日
下一篇 2026年8月5日

相关推荐