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)
小编小编
上一篇 19小时前
下一篇 19小时前

相关推荐

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)
小编小编
上一篇 21小时前
下一篇 21小时前

相关推荐