MySQL 分库分表:用 ShardingSphere 解决 “单表千万级数据” 查询慢问题(附分布式事务方案)

前阵子处理过一个极端案例:订单表
order_info
存了 3000 万条数据,一个简单的 “查询用户近 3 个月订单” 的 SQL 要跑 5 秒多,用户在 APP 上点 “我的订单” 经常超时。更要命的是,每次加索引、做 DDL 都要锁表几小时,业务根本受不了。后来用 ShardingSphere 做了分表,把 3000 万条数据分到 30 个小表,同样的查询降到 0.1 秒,加索引也能在秒级完成。

分库分表是 MySQL 应对 “大数据量” 的终极方案 —— 当单表数据量超过 1000 万行,索引会失效、IO 会飙升,此时必须通过 “拆分” 把大表变成小表,让每个表的数据量控制在 100 万 – 500 万行以内。但很多人只知道 “要拆分”,却不懂 “水平拆还是垂直拆”“按什么字段拆”“分布式事务怎么处理”,结果拆完后查询更慢,还出现数据不一致。

一、先搞懂:为什么单表数据量不能太大?(用 “查字典” 类比)

单表数据量就像 “字典的厚度”—— 一本 100 页的字典(小表),你能很快找到 “MySQL” 这个词;但如果是一本 10000 页的字典(大表),即使有目录(索引),翻找起来也会很慢。

1. 单表数据量过大的 3 个致命问题

索引失效:MySQL 索引用 B + 树实现,单表 3000 万行时,B + 树高度会从 3 层变成 5 层(每层对应一次磁盘 IO),查询需要多 2 次 IO,速度慢 2-3 倍;写入卡顿:写入时需要维护索引,3000 万行的表插入一条数据,索引树调整耗时是 100 万行表的 10 倍以上;DDL 灾难:给 3000 万行的表加字段,MySQL 会锁表并重写整个表,耗时几小时,期间业务完全不可用。

2. 分库分表的 2 种核心方式(企业级选型指南)

分库分表分 “水平拆分” 和 “垂直拆分”,适用场景完全不同,选错了会适得其反:

拆分方式 原理 优点 缺点 适用场景
水平拆分 按行拆分,把大表分成多个结构相同的小表(如订单表按用户 ID 拆成 10 个表) 解决单表数据量大的问题,扩展性强 跨表查询复杂(如查所有用户的订单) 单表数据超 1000 万行,且查询有明显分片键(如用户 ID)
垂直拆分 按列拆分,把大表分成多个结构不同的小表(如订单表拆成 “订单基本信息表” 和 “订单详情表”) 降低表宽度,提升 IO 效率 事务一致性难保证(同一订单数据在多个表) 表字段太多(如 100 + 字段),或冷热数据明显(如订单状态和订单日志)

企业级结论:90% 的性能问题是 “水平方向数据量太大” 导致的,优先考虑水平拆分。比如电商订单表、用户表、商品表,几乎都是水平分表的典型场景。

二、ShardingSphere 核心原理:它是怎么 “拆分” 数据的?

ShardingSphere 就像 “图书馆的管理员”—— 你要找一本《MySQL 优化》(对应一条数据),管理员会根据 “分类规则”(分片策略)告诉你它在 “第 3 排第 5 个书架”(对应某个分表)。它的核心工作流程分 3 步:

1. 数据分片:按规则分配数据到不同表

应用执行
INSERT INTO order_info (order_id, user_id, ...)
时,ShardingSphere 会解析 SQL,提取分片键(如
user_id
);根据预设的分片策略(如
user_id % 10
)计算出分片索引(如
user_id=1001
→1001%10=1→分到
order_info_1
表);自动将 SQL 改写为
INSERT INTO order_info_1 (...)
,并执行到对应的数据库节点。

2. 分布式 SQL 改写:让应用像用单表一样用分表

当应用执行
SELECT * FROM order_info WHERE user_id=1001
时,ShardingSphere 会自动改写为
SELECT * FROM order_info_1 WHERE user_id=1001
;当查询跨多个分表(如
user_id IN (1001,1002)
且 1001%10=1、1002%10=2),会改写为
UNION ALL
查询:

sql



SELECT * FROM order_info_1 WHERE user_id=1001
UNION ALL
SELECT * FROM order_info_2 WHERE user_id=1002

应用完全感知不到分表的存在,就像在操作一个单表。

3. 分布式事务:保证跨表操作的数据一致性

当一个订单需要同时写入
order_info

payment_info
两个分表时,ShardingSphere 提供两种事务方案:

AT 模式:基于本地事务 + undo 日志,性能好,适合大多数场景;TCC 模式:需要业务代码实现 Try/Confirm/Cancel,适合高一致性场景(如支付)。

三、实战环境:给 3000 万行订单表做水平分表

我们以生鲜电商的
order_info
表为例,将 3000 万行数据按
user_id
拆分成 30 个分表(
order_info_0

order_info_29
),架构如下:

应用:Java Spring Boot(模拟订单系统);ShardingSphere-JDBC:嵌入应用,负责分表逻辑(无需单独部署中间件);MySQL:单库(先分表,后续可扩展为分库),存储 30 个分表。

1. 原表痛点分析(为什么必须分表)


order_info
表结构及问题:

sql



CREATE TABLE `order_info` (
  `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
  `order_status` tinyint(4) NOT NULL COMMENT '订单状态',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`order_id`),
  KEY `idx_user_id` (`user_id`)  -- 用户ID索引,查询常用
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

性能测试

数据量:3000 万行;查询
SELECT * FROM order_info WHERE user_id=1001
:耗时 5.2 秒;插入一条数据:耗时 0.8 秒(索引维护耗时);添加字段
ALTER TABLE order_info ADD COLUMN remark VARCHAR(255)
:耗时 145 分钟(锁表导致业务中断)。

四、实战步骤:用 ShardingSphere-JDBC 实现水平分表

1. 步骤 1:准备分表(创建 30 个分表)

先在 MySQL 中创建 30 个分表(结构和原表一致,表名按
order_info_0

order_info_29
命名):

sql



-- 创建分表的存储过程
DELIMITER $$
CREATE PROCEDURE create_order_shards()
BEGIN
  DECLARE i INT DEFAULT 0;
  WHILE i < 30 DO
    SET @sql = CONCAT(
      'CREATE TABLE `order_info_', i, '` (',
      '  `order_id` bigint(20) NOT NULL COMMENT "订单ID",',
      '  `user_id` bigint(20) NOT NULL COMMENT "用户ID",',
      '  `order_amount` decimal(10,2) NOT NULL COMMENT "订单金额",',
      '  `order_status` tinyint(4) NOT NULL COMMENT "订单状态",',
      '  `create_time` datetime NOT NULL COMMENT "创建时间",',
      '  PRIMARY KEY (`order_id`),',
      '  KEY `idx_user_id` (`user_id`)',
      ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT="订单分表', i, '"'
    );
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
    SET i = i + 1;
  END WHILE;
END$$
DELIMITER ;
 
-- 执行存储过程,创建30个分表
CALL create_order_shards();

2. 步骤 2:搭建 Spring Boot 应用,集成 ShardingSphere-JDBC

(1)添加 Maven 依赖(pom.xml)

xml



<dependencies>
  <!-- Spring Boot核心 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>
  
  <!-- ShardingSphere-JDBC -->
  <dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
  </dependency>
  
  <!-- MySQL驱动 -->
  <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
  </dependency>
  
  <!--  lombok简化代码 -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>
(2)配置分表规则(application.yml)

核心配置分片策略:按
user_id % 30
分片,共 30 个分表:

yaml



spring:
  shardingsphere:
    datasource:
      names: order-db  # 数据源名称(单库,后续可扩展多库)
      order-db:  # 数据源配置
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.1.100:3306/order_db?useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
 
    rules:
      sharding:
        tables:
          order_info:  # 逻辑表名(应用中使用的表名)
            actual-data-nodes: order-db.order_info_${0..29}  # 实际分表:order_info_0到order_info_29
            database-strategy:  # 分库策略(单库场景可省略)
              none:
            table-strategy:  # 分表策略
              standard:
                sharding-column: user_id  # 分片键:按user_id分片
                sharding-algorithm-name: order_inline  # 分片算法名称
 
        sharding-algorithms:
          order_inline:  # 分片算法配置
            type: INLINE  #  inline表达式算法
            props:
              algorithm-expression: order_info_${user_id % 30}  # 分片规则:user_id模30
 
    props:
      sql-show: true  # 打印改写后的SQL,方便调试

3. 步骤 3:开发业务代码(模拟订单 CRUD)

(1)创建实体类(Order.java)

java

运行



@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private Long orderId;
    private Long userId;
    private BigDecimal orderAmount;
    private Integer orderStatus;
    private LocalDateTime createTime;
}
(2)创建 Mapper 接口(OrderMapper.java)

java

运行



@Mapper
public interface OrderMapper {
    // 新增订单
    @Insert("INSERT INTO order_info (order_id, user_id, order_amount, order_status, create_time) " +
            "VALUES (#{orderId}, #{userId}, #{orderAmount}, #{orderStatus}, #{createTime})")
    int insert(Order order);
 
    // 根据user_id查询订单
    @Select("SELECT * FROM order_info WHERE user_id = #{userId}")
    List<Order> selectByUserId(Long userId);
 
    // 根据order_id查询订单(需跨表,ShardingSphere会自动处理)
    @Select("SELECT * FROM order_info WHERE order_id = #{orderId}")
    Order selectByOrderId(Long orderId);
}
(3)创建测试类(OrderController.java)

java

运行



@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
    private OrderMapper orderMapper;
 
    // 生成全局唯一订单ID(后续讲分布式ID方案)
    private Long generateOrderId() {
        return System.currentTimeMillis();
    }
 
    // 创建订单
    @PostMapping
    public String createOrder(@RequestParam Long userId, @RequestParam BigDecimal amount) {
        Order order = new Order();
        order.setOrderId(generateOrderId());
        order.setUserId(userId);
        order.setOrderAmount(amount);
        order.setOrderStatus(1); // 待支付
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);
        return "订单创建成功,ID:" + order.getOrderId();
    }
 
    // 查询用户订单
    @GetMapping("/user/{userId}")
    public List<Order> getOrdersByUserId(@PathVariable Long userId) {
        return orderMapper.selectByUserId(userId);
    }
 
    // 查询单个订单
    @GetMapping("/{orderId}")
    public Order getOrderById(@PathVariable Long orderId) {
        return orderMapper.selectByOrderId(orderId);
    }
}

4. 步骤 4:验证分表效果

(1)测试写入:数据是否按 user_id 分到不同表

调用
POST /orders?userId=1001&amount=99.00
创建订单,查看控制台输出的 SQL:

plaintext



INSERT INTO order_info_1 (order_id, user_id, order_amount, order_status, create_time) 
VALUES (1730345678901, 1001, 99.00, 1, '2025-11-01T10:00:00')


user_id=1001
→1001%30=1→写入
order_info_1
表,符合预期。

(2)测试查询:是否能正确从分表读取

调用
GET /orders/user/1001
,控制台输出:

plaintext


SELECT * FROM order_info_1 WHERE user_id = 1001

直接查询
order_info_1
表,耗时 0.08 秒(原单表查询需 5.2 秒)。

(3)测试跨表查询:按 order_id 查询(不知道在哪个分表)

调用
GET /orders/1730345678901
,控制台输出:

plaintext



SELECT * FROM order_info_0 WHERE order_id = 1730345678901 UNION ALL
SELECT * FROM order_info_1 WHERE order_id = 1730345678901 UNION ALL
...(省略中间分表)
SELECT * FROM order_info_29 WHERE order_id = 1730345678901

ShardingSphere 会自动扫描所有分表,找到匹配的订单(实际会优化,只扫描可能的分表)。

五、企业级进阶:解决分表后的 3 大核心问题

分表后会引入新的挑战,这 3 个问题不解决,生产环境会出大问题:

1. 问题 1:分布式 ID 生成(避免分表 ID 冲突)

原表用自增 ID(
AUTO_INCREMENT
)会导致分表 ID 重复(比如
order_info_1

order_info_2
都有 ID=1 的订单),必须用全局唯一 ID。

解决方案:用 ShardingSphere 的雪花算法生成 ID

修改
application.yml
,配置分布式 ID 生成策略:

yaml



spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          order_info:
            # 新增:分布式ID配置
            key-generate-strategy:
              column: order_id  # 要生成ID的字段
              key-generator-name: snowflake  # 用雪花算法
 
        # 新增:雪花算法配置
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1  # 工作节点ID(集群部署时每个节点不同)
效果:


order_id
会自动生成全局唯一的雪花 ID(64 位,包含时间戳、机器 ID、序列号),避免分表 ID 冲突。

2. 问题 2:跨分片查询性能差(如查 “今日所有订单”)


user_id
分片后,查询 “今日所有订单” 需要扫描 30 个分表,耗时会增加。

解决方案 1:二级分片(按时间 + 用户 ID 双层分片)

先按 “创建时间” 分表(如
order_info_202511_0

order_info_202511_29
),再按
user_id
分 30 片,查询 “今日订单” 只需扫描当天的 30 个分表,而非所有历史分表。

修改分片规则:

yaml



table-strategy:
  complex:
    sharding-columns: create_time, user_id  # 复合分片键
    sharding-algorithm-name: order_complex  # 复合算法
解决方案 2:读写分离 + 分表结合

查询 “今日所有订单” 这类统计性请求,路由到从库的汇总表(通过定时任务从分表同步数据),避免影响主库。

3. 问题 3:分布式事务(跨分表操作的数据一致性)

当一个业务需要同时操作
order_info

payment_info
两个分表(如下单后创建支付记录),单表事务无法保证一致性。

解决方案:用 ShardingSphere 的 AT 模式分布式事务
(1)添加事务依赖

xml



<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>shardingsphere-transaction-xa-core</artifactId>
  <version>5.3.2</version>
</dependency>
(2)在业务方法上添加
@Transactional
注解

java

运行



@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private PaymentMapper paymentMapper;
 
    // 开启分布式事务
    @Transactional
    public void createOrderWithPayment(Order order, Payment payment) {
        orderMapper.insert(order);       // 写入order_info分表
        paymentMapper.insert(payment);   // 写入payment_info分表
        // 若此处抛出异常,两个插入操作会同时回滚
    }
}
(3)配置事务类型

yaml



spring:
  shardingsphere:
    rules:
      transaction:
        default-type: AT  # 默认为本地事务,改为AT分布式事务
        provider-type: XA
效果:


paymentMapper.insert(payment)
失败时,
orderMapper.insert(order)
会自动回滚,保证两个分表的数据一致性。

六、分库分表的 “避坑指南”(实战总结)

分片键选择要 “高频且均匀”

高频:查询中经常用的字段(如
user_id
,用户查自己的订单是高频操作);均匀:避免数据倾斜(如按 “地区 ID” 分片,北京用户多,导致某分表数据量过大)。

分表数量不是越多越好

3000 万行数据分 30 表(每表 100 万)足够,分 100 表会导致连接数增加、元数据管理复杂;分表数量建议是 2 的幂次方(如 16、32),方便后续扩容。

避免跨分片 JOIN

分表后
JOIN
操作性能极差(如
order_info

user_info
都分表,JOIN 需要跨多个表);解决方案:冗余字段(在
order_info
中冗余
user_name
),避免 JOIN。

DDL 操作要 “分批执行”

给 30 个分表加字段,不要同时执行,分 3 批(每批 10 表),避免 MySQL 连接耗尽;用工具:pt-online-schema-change 在分表上执行 DDL,避免锁表。

监控分表数据分布

定期查询各分表的数据量,发现倾斜及时调整分片策略:

sql



SELECT 
  table_name, 
  TABLE_ROWS 
FROM 
  information_schema.TABLES 
WHERE 
  table_name LIKE 'order_info_%' 
ORDER BY TABLE_ROWS DESC;

七、分库分表的 “演进路线”

分库分表不是一步到位的,建议按这个路线演进:

单库单表:业务初期(数据量 < 100 万),快速上线;单库分表:数据量达 1000 万,先用 ShardingSphere-JDBC 做单库分表(如本文方案);分库分表:单库性能瓶颈(QPS>1 万),拆成多库(如 3 个库,每库 10 个分表);分库分表 + 读写分离:读压力大时,每个分库配从库,用 ShardingSphere 同时处理分表和读写分离。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
AboutJessie的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容