单表数据量超过千万行后,查询性能会显著下降,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/