分库分表是解决数据库性能瓶颈的经典方案。当单表数据量突破千万级,查询性能会明显下降,这时就需要考虑水平拆分数据。Spring Boot作为Java生态中最流行的应用框架,与分库分表技术结合能有效解决高并发场景下的数据库扩展性问题。
我在电商和金融行业多个千万级用户项目中实践过这套方案。从最初的MyCAT中间件到现在的ShardingSphere,踩过不少坑也积累了一些实战经验。本文将带你从零开始,完整实现一个Spring Boot分库分表项目,包括路由策略、分布式事务、扩容迁移等核心难点。
先看几个关键指标:
如果满足以上任意两条,就该考虑分库分表了。以订单表为例,当平台日订单量突破10万,按这个增长速度,半年后单表就会超过2000万行数据。
常见分片策略对比:
| 策略类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 取模分片 | 均匀分布 | 扩容困难 | 用户ID等离散值 |
| 范围分片 | 易于扩容 | 可能热点 | 时间序列数据 |
| 哈希分片 | 分布均匀 | 不支持范围查询 | 随机分布需求 |
| 目录分片 | 灵活配置 | 维护成本高 | 复杂业务规则 |
推荐组合方案:用户维度用取模分库,时间维度用范围分表。例如将用户表按user_id%8分成8个库,每个库再按月分12张表。
使用ShardingSphere-JDBC最新版:
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.3.2</version>
</dependency>
配置示例(application.yml):
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0: # 主库配置
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db0
username: root
password: 123456
ds1: # 从库配置
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db1
username: root
password: 123456
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
standard:
sharding-column: order_id
precise-algorithm-class-name: com.example.OrderShardingAlgorithm
实现精确分片算法接口:
java复制public class OrderShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
long orderId = shardingValue.getValue();
// 分库逻辑:订单ID后两位模2
String dbSuffix = String.valueOf((orderId % 100) % 2);
// 分表逻辑:订单ID模16
String tableSuffix = String.valueOf(orderId % 16);
for (String each : availableTargetNames) {
if (each.endsWith(dbSuffix + ".t_order_" + tableSuffix)) {
return each;
}
}
throw new IllegalArgumentException();
}
}
推荐使用美团Leaf方案:
java复制// ID生成器配置
@Bean
public IdGenerator idGenerator() {
return new LeafIdGenerator(zkAddress, 8080);
}
// 使用示例
@Autowired
private IdGenerator idGenerator;
public void createOrder() {
long orderId = idGenerator.nextId();
// ...
}
使用Seata实现AT模式:
java复制@GlobalTransactional
public void placeOrder(OrderDTO order) {
// 1. 扣减库存
stockService.reduce(order.getSkuId(), order.getCount());
// 2. 创建订单
orderMapper.insert(order);
// 3. 增加积分
pointsService.add(order.getUserId(), order.getAmount());
}
配置关键参数:
properties复制# Seata配置
seata.tx-service-group=my_test_tx_group
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.disable-global-transaction=false
yaml复制spring:
shardingsphere:
rules:
replica-query:
data-sources:
pr_ds:
primary-data-source-name: ds0
replica-data-source-names: ds1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
使用ShardingSphere-Scaling:
bash复制bin/start.sh \
--config=conf/config.yaml \
--rule=conf/rule.yaml \
--datasource=conf/datasource.yaml
迁移过程注意事项:
Prometheus监控配置:
yaml复制metrics:
enabled: true
prometheus:
host: 0.0.0.0
port: 9090
关键监控项:
分片键选择:不要用UUID作为分片键,会导致跨库查询性能极差。实测varchar类型分片键比整型慢3-5倍。
JOIN优化:避免跨库JOIN,可以通过冗余字段或内存计算解决。曾经有个联表查询导致全库扫描,直接拖垮整个集群。
分布式ID:自增ID会导致热点问题,建议用Snowflake或Leaf方案。我们曾因自增ID导致首库负载是其他库的10倍。
事务超时:Seata默认全局事务超时时间是60秒,对于长事务需要调整:
properties复制seata.tx.timeout=300
yaml复制hikari:
maximum-pool-size: 20 # 每个数据源
connection-timeout: 30000
索引设计:每个分片表都需要单独建立索引,特别是分片键必须建索引。曾遇到没建分片键索引导致全表扫描。
批量操作:使用Batch批量插入,比单条插入快10倍以上:
java复制@Transactional
public void batchInsert(List<Order> orders) {
SqlSessionFactory sqlSessionFactory = ...;
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
OrderMapper mapper = session.getMapper(OrderMapper.class);
orders.forEach(mapper::insert);
session.commit();
}
}
java复制LoadingCache<Long, User> userCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Long, User>() {
@Override
public User load(Long userId) {
return userMapper.selectById(userId);
}
});
当现有分片不够用时,需要在线扩容。推荐使用一致性哈希算法减少数据迁移量:
扩容期间双写配置示例:
java复制public void insertOrder(Order order) {
// 写入旧分片
oldShardMapper.insert(order);
// 写入新分片
if (shouldMigrate(order.getOrderId())) {
newShardMapper.insert(order);
}
}
扩容后数据校验脚本要点:
sql复制-- 校验数据总量
SELECT COUNT(*) FROM (
SELECT * FROM db0.t_order_0 UNION ALL
SELECT * FROM db0.t_order_1 UNION ALL
...
SELECT * FROM db7.t_order_15
) AS all_orders;
经过多个项目实践,我总结出以下黄金准则:
典型错误案例:某金融项目使用手机号作为分片键,结果发现前几位相同的号码会集中到同一个分片,导致严重的数据倾斜。后来改用用户ID哈希才解决问题。
对于Spring Boot项目,推荐以下配置检查清单: