前阵子处理过一个极端案例:订单表存了 3000 万条数据,一个简单的 “查询用户近 3 个月订单” 的 SQL 要跑 5 秒多,用户在 APP 上点 “我的订单” 经常超时。更要命的是,每次加索引、做 DDL 都要锁表几小时,业务根本受不了。后来用 ShardingSphere 做了分表,把 3000 万条数据分到 30 个小表,同样的查询降到 0.1 秒,加索引也能在秒级完成。
order_info
分库分表是 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. 数据分片:按规则分配数据到不同表
应用执行时,ShardingSphere 会解析 SQL,提取分片键(如
INSERT INTO order_info (order_id, user_id, ...));根据预设的分片策略(如
user_id)计算出分片索引(如
user_id % 10→1001%10=1→分到
user_id=1001表);自动将 SQL 改写为
order_info_1,并执行到对应的数据库节点。
INSERT INTO order_info_1 (...)
2. 分布式 SQL 改写:让应用像用单表一样用分表
当应用执行时,ShardingSphere 会自动改写为
SELECT * FROM order_info WHERE user_id=1001;当查询跨多个分表(如
SELECT * FROM order_info_1 WHERE user_id=1001且 1001%10=1、1002%10=2),会改写为
user_id IN (1001,1002)查询:
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两个分表时,ShardingSphere 提供两种事务方案:
payment_info
AT 模式:基于本地事务 + undo 日志,性能好,适合大多数场景;TCC 模式:需要业务代码实现 Try/Confirm/Cancel,适合高一致性场景(如支付)。
三、实战环境:给 3000 万行订单表做水平分表
我们以生鲜电商的表为例,将 3000 万行数据按
order_info拆分成 30 个分表(
user_id到
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 万行;查询:耗时 5.2 秒;插入一条数据:耗时 0.8 秒(索引维护耗时);添加字段
SELECT * FROM order_info WHERE user_id=1001:耗时 145 分钟(锁表导致业务中断)。
ALTER TABLE order_info ADD COLUMN remark VARCHAR(255)
四、实战步骤:用 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)
核心配置分片策略:按分片,共 30 个分表:
user_id % 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 分到不同表
调用创建订单,查看控制台输出的 SQL:
POST /orders?userId=1001&amount=99.00
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')
→1001%30=1→写入
user_id=1001表,符合预期。
order_info_1
(2)测试查询:是否能正确从分表读取
调用,控制台输出:
GET /orders/user/1001
plaintext
SELECT * FROM order_info_1 WHERE user_id = 1001
直接查询表,耗时 0.08 秒(原单表查询需 5.2 秒)。
order_info_1
(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()会导致分表 ID 重复(比如
AUTO_INCREMENT和
order_info_1都有 ID=1 的订单),必须用全局唯一 ID。
order_info_2
解决方案:用 ShardingSphere 的雪花算法生成 ID
修改,配置分布式 ID 生成策略:
application.yml
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(集群部署时每个节点不同)
效果:
会自动生成全局唯一的雪花 ID(64 位,包含时间戳、机器 ID、序列号),避免分表 ID 冲突。
order_id
2. 问题 2:跨分片查询性能差(如查 “今日所有订单”)
按分片后,查询 “今日所有订单” 需要扫描 30 个分表,耗时会增加。
user_id
解决方案 1:二级分片(按时间 + 用户 ID 双层分片)
先按 “创建时间” 分表(如到
order_info_202511_0),再按
order_info_202511_29分 30 片,查询 “今日订单” 只需扫描当天的 30 个分表,而非所有历史分表。
user_id
修改分片规则:
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注解
@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)
六、分库分表的 “避坑指南”(实战总结)
分片键选择要 “高频且均匀”:
高频:查询中经常用的字段(如,用户查自己的订单是高频操作);均匀:避免数据倾斜(如按 “地区 ID” 分片,北京用户多,导致某分表数据量过大)。
user_id
分表数量不是越多越好:
3000 万行数据分 30 表(每表 100 万)足够,分 100 表会导致连接数增加、元数据管理复杂;分表数量建议是 2 的幂次方(如 16、32),方便后续扩容。
避免跨分片 JOIN:
分表后操作性能极差(如
JOIN和
order_info都分表,JOIN 需要跨多个表);解决方案:冗余字段(在
user_info中冗余
order_info),避免 JOIN。
user_name
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 同时处理分表和读写分离。
















暂无评论内容