1. 分库分表技术概述
数据库分库分表是解决海量数据存储和高并发访问的核心技术方案。当单表数据量突破千万级,或者数据库QPS达到5000以上时,传统的单库单表架构就会遇到明显的性能瓶颈。我在电商和金融行业的实际项目中,多次遇到订单表、交易流水表等核心业务表数据爆炸式增长的情况,通过分库分表技术成功将单表数据量从1亿+降低到百万级,查询性能提升10倍以上。
ShardingSphere作为Apache顶级开源项目,提供了完整的分库分表解决方案。它包含Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar三个产品,其中Sharding-JDBC是最轻量级的Java客户端方案,也是我们实际项目中最常用的选择。与MyCat等中间件方案相比,Sharding-JDBC采用无代理架构,性能损耗更低(实测额外开销<5%),且支持所有MySQL原生功能。
2. 核心架构设计
2.1 分片策略选型
在实际项目中,我们需要根据业务特点选择合适的分片策略。以下是三种典型场景的解决方案:
- 用户订单数据:采用userId作为分片键,按范围分片(0-1000万在库1,1000-2000万在库2)。这种方案能保证同一用户的所有订单都在同一个库中,避免跨库事务问题。我们通过
inline策略配置:
yaml复制spring:
shardingsphere:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_$->{user_id % 16}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id / 10000000}
- 交易流水数据:使用雪花算法生成分布式ID作为主键,按哈希分片。这种全局唯一ID避免了主键冲突,且数据分布均匀。我们配合
complex策略实现多字段分片:
java复制public class OrderShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> shardingValue) {
// 实现根据orderId和createTime的分片逻辑
}
}
- 地理空间数据:按地区编码分库+时间分表。例如将全国分为华北、华东等大区,每个大区一个库,每个库内按月分表。这种方案在LBS服务中很常见。
2.2 分布式ID方案对比
分库分表必须解决分布式ID生成问题,这是实际项目中最容易踩坑的地方。我们对几种方案做了压测对比:
| 方案 | 吞吐量(QPS) | 局部有序性 | 依赖外部存储 | 推荐场景 |
|---|---|---|---|---|
| 数据库自增ID | 1,200 | 是 | 是 | 小型系统 |
| Redis原子计数器 | 8,500 | 是 | 是 | 中型系统 |
| 雪花算法(Snowflake) | 120,000 | 部分有序 | 否 | 大型分布式系统 |
| UUID | 150,000 | 否 | 否 | 非顺序需求场景 |
提示:雪花算法需要特别注意时钟回拨问题。我们的解决方案是使用美团Leaf方案,在服务启动时检测时钟异常,并实现本地时钟缓存。
3. 详细实现步骤
3.1 环境准备与依赖配置
首先在Spring Boot项目中引入Sharding-JDBC依赖(以5.1.1版本为例):
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<version>5.1.1</version>
</dependency>
然后配置数据源和分片规则。这里给出一个生产级配置示例:
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://db-host1:3306/order_db?useSSL=false
username: user
password: pass
hikari:
maximum-pool-size: 20
ds1:
# 类似ds0配置...
sharding:
default-database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 2}
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 16}
key-generator:
column: order_id
type: SNOWFLAKE
props:
sql.show: true
3.2 分片算法实现
对于复杂分片场景,需要自定义分片算法。以下是按日期范围分表的实现示例:
java复制public class DateRangeShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyyMM");
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
LocalDate date = shardingValue.getValue().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDate();
String suffix = date.format(FORMATTER);
return availableTargetNames.stream()
.filter(name -> name.endsWith(suffix))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未找到对应分表"));
}
}
在配置中引用这个算法:
yaml复制table-strategy:
standard:
sharding-column: create_time
precise-algorithm-class-name: com.example.DateRangeShardingAlgorithm
3.3 分布式事务处理
分库分表后最大的挑战是分布式事务。我们采用Seata的AT模式解决这个问题:
- 首先引入Seata依赖:
xml复制<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
- 配置Seata服务器信息:
yaml复制seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
grouplist:
default: 127.0.0.1:8091
- 在需要事务的方法上添加注解:
java复制@GlobalTransactional
public void createOrder(OrderDTO order) {
// 跨库操作
orderMapper.insert(order);
inventoryService.reduceStock(order.getProductId());
paymentService.createPayment(order);
}
4. 性能优化实践
4.1 读写分离配置
分库分表通常配合读写分离使用。ShardingSphere的读写分离配置非常简洁:
yaml复制spring:
shardingsphere:
masterslave:
name: ms_ds
master-data-source-name: ds_master
slave-data-source-names: ds_slave0, ds_slave1
load-balance-algorithm-type: round_robin
我们实测发现,合理的读写分离配置可以将数据库负载降低40%。但要注意:
- 主从同步延迟可能导致"写完立即查"场景数据不一致
- 事务中的查询强制走主库:
HintManager.getInstance().setMasterRouteOnly()
4.2 柔性事务实践
对于不需要强一致性的场景,我们采用本地消息表实现最终一致性:
- 创建消息表记录事务状态
- 业务与消息在同一个本地事务中提交
- 定时任务扫描并重试失败的消息
sql复制CREATE TABLE transaction_message (
id BIGINT PRIMARY KEY,
biz_id VARCHAR(64) NOT NULL,
biz_type VARCHAR(32) NOT NULL,
payload JSON NOT NULL,
status TINYINT NOT NULL COMMENT '0-待处理,1-处理成功,2-处理失败',
retry_count INT DEFAULT 0,
next_retry_time DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
5. 常见问题与解决方案
5.1 跨库JOIN问题
我们遇到的最典型问题是分片后无法直接JOIN不同分片的表。解决方案包括:
- 冗余字段:在订单表中冗余存储商品名称等关键信息
- 多次查询+内存计算:先查订单ID,再批量查商品信息
- 使用广播表:将小数据量的字典表同步到所有库
广播表配置示例:
yaml复制spring:
shardingsphere:
sharding:
broadcast-tables: t_region, t_category
5.2 分页查询优化
分页查询在大数据量时性能极差。我们的优化方案:
- 先获取分片键再查询:
SELECT id FROM t_order WHERE user_id=123 LIMIT 100 - 使用ES等搜索引擎实现复杂查询
- 禁止大偏移量分页:
LIMIT 10000,20改为WHERE id>last_id LIMIT 20
5.3 扩容方案
当现有分片不够时需要扩容。我们采用以下步骤保证平滑迁移:
- 双写新旧分片
- 使用数据迁移工具同步历史数据
- 验证数据一致性
- 切换读流量
- 最终切换写流量
具体可以通过ShardingSphere的scaling模块实现:
bash复制curl -X POST \
http://localhost:8888/scaling/job/start \
-H 'Content-Type: application/json' \
-d '{
"ruleConfiguration": {
"source": {
"schemaName": "ds_0",
"tableName": "t_order"
},
"target": {
"schemaName": "ds_2",
"tableName": "t_order"
}
}
}'
6. 监控与运维
生产环境必须建立完善的监控体系:
-
Prometheus监控指标:
- 分片查询次数
- 慢查询统计
- 连接池状态
-
日志分析:
- 开启SQL日志分析执行计划
- 监控分布式事务失败率
-
治理中心配置:
yaml复制spring:
shardingsphere:
governance:
name-registry:
type: ZooKeeper
server-lists: localhost:2181
distributed-lock:
type: ZooKeeper
我在实际运维中发现,80%的性能问题都源于不合理的分片键选择。建议在开发阶段就通过EXPLAIN分析SQL路由情况,避免全库扫描。