分库分表最佳实践:从基础原理到高阶架构设计
“分库分表的本质是为了解决单机数据库的物理极限。当单表数据超过 500万行 或 连接数/IO 打满时 ,传统的 B+ 树索引性能会急剧下降。这时候,必须通过拆分来用空间换时间。
“拆分策略通常分两步走:
- 先垂直拆分:按业务模块(如用户库、订单库)把数据物理隔离,解决业务耦合和连接数问题 。
- 再水平拆分:当单表数据量依然过大时,把一张大表拆成多个子表(分片)。基础策略有范围分片(扩容容易但有热点)和哈希分片(数据均匀但扩容难) 。”
“传统分片有两大痛点:扩容难和多维查询难。我们的最佳实践是用两套高阶方案来解决 :
- 解决扩容难用‘虚拟槽’:引入一个逻辑层,数据映射到槽,槽再映射到库。扩容时只需要移动槽位,迁移数据量小且无需修改代码 。
- 解决查询难用‘基因法’:在生成订单 ID(如雪花算法)时,把用户 ID 的最后几位(基因)嵌入进去。这样订单 ID 和用户 ID 的路由规则就一致了,彻底避免了跨库查询的全表扫描 。”
引言:为什么需要分库分表?
单库单表的性能瓶颈
在单体应用或业务初期,我们通常使用单一数据库和单表存储数据。然而,随着业务增长,数据量激增,单表性能瓶颈逐渐显现:
- 数据量瓶颈:MySQL单表数据量超过 500 万行或几十 GB时,B+树索引深度增加,查询性能显著下降。
- 连接数瓶颈:单数据库连接数有限,高并发场景下连接池耗尽。
- 磁盘I/O瓶颈:单磁盘读写能力有限,无法支撑海量数据访问。
- 维护困难:单表过大时,DDL操作(如加索引)可能锁表数小时。
性能衰减量化分析
关键指标参考值:
| 数据量 | 索引深度 | 单次查询耗时 | 写入TPS |
|---|---|---|---|
| 100万 | 3层 | 1-5ms | 5000+ |
| 500万 | 3-4层 | 5-20ms | 3000 |
| 2000万 | 4层 | 20-100ms | 1000 |
| 1亿 | 4-5层 | 100-500ms | 300 |
数据库架构演进路径
分库分表的基本策略
垂直拆分(Vertical Sharding)
垂直分库
定义:按业务模块拆分数据库,每个服务独立使用一个数据库。
SQL示例
-- 拆分前:所有表在一个库
CREATE DATABASE app_db;
USE app_db;
CREATE TABLE user (...);
CREATE TABLE orders (...);
CREATE TABLE product (...);
CREATE TABLE payment (...);
-- 拆分后:每个业务独立数据库
CREATE DATABASE user_db;
USE user_db;
CREATE TABLE user (...);
CREATE TABLE user_address (...);
CREATE DATABASE order_db;
USE order_db;
CREATE TABLE orders (...);
CREATE TABLE order_item (...);
CREATE DATABASE product_db;
USE product_db;
CREATE TABLE product (...);
CREATE TABLE inventory (...);
优缺点对比
| 优点 | 缺点 |
|---|---|
| 业务解耦,不同业务使用不同数据库 | 无法解决单表数据量大的问题 |
| 单表数据量相对可控 | 跨库事务复杂(需要分布式事务) |
| 减少单库连接数压力 | 跨库JOIN需要在应用层处理 |
| 便于按业务特点独立优化 | 数据库运维成本增加 |
| 故障隔离,一个库挂不影响其他 | 需要处理数据一致性问题 |
垂直分表
垂直分表适用于字段非常多的表,对于很多的查询来说,其实不需要一次将所有的字段全都查询出来,这样很浪费性能,影响效率,那么就将经常查询的字段单独拆分出一个表,将另外的字段单独拆分成另一个表,拆分后的表通过某个字段关联起来,这样既可以减少表的容量大小,又可以提升查询效率
水平拆分(Horizontal Sharding)
垂直拆分其实还是根据业务进行模块话拆分的,当单表的容量越来越大的时候,还是不能解决单表的读写、存储的性能瓶颈,这是就需要水平拆分了
水平分库
水平分库是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,每个数据库的库和表结构都是相同的,只有表中的数据不同。可以实现水平扩展,有效缓解单库的性能瓶颈
定义:将单表数据按某种规则拆分到多个表或库中,每个分片包含部分数据。
水平分片策略详解
策略一:范围分片(Range Sharding)
做法:按 ID 或时间范围划分数据。
代码实现:
/**
* 范围分片算法
*/
public class RangeShardingAlgorithm {
private static final long RANGE_SIZE = 10_000_000L; // 每个分片1000万条
/**
* 按ID范围路由
*/
public String routeById(Long id) {
int shardIndex = (int) (id / RANGE_SIZE);
return String.format("order_%d", shardIndex);
}
/**
* 按时间范围路由
*/
public String routeByTime(LocalDateTime createTime) {
String suffix = createTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
return String.format("order_%s", suffix);
}
/**
* 范围查询路由 - 返回需要查询的所有分片
*/
public List<String> routeByRange(Long startId, Long endId) {
List<String> shards = new ArrayList<>();
int startShard = (int) (startId / RANGE_SIZE);
int endShard = (int) (endId / RANGE_SIZE);
for (int i = startShard; i <= endShard; i++) {
shards.add(String.format("order_%d", i));
}
return shards;
}
}
优缺点:
| 优点 | 缺点 |
|---|---|
| 扩容简单,直接增加新分片 | 热点问题:最新数据集中在最后一个分片 |
| 范围查询高效(如查最近订单) | 数据倾斜:不同范围数据量可能差异大 |
| 数据归档方便(可直接删除旧分片) | 迁移困难:范围调整需大量数据迁移 |
| 实现简单,逻辑清晰 | 写入压力不均衡 |
策略二:哈希分片(Hash Sharding)
做法:通过哈希函数(如 ID % N)将数据均匀分布到 N 个库/表。
代码实现:
/**
* 哈希分片算法
*/
public class HashShardingAlgorithm {
private final int dbCount; // 数据库数量
private final int tableCount; // 每个库的表数量
public HashShardingAlgorithm(int dbCount, int tableCount) {
this.dbCount = dbCount;
this.tableCount = tableCount;
}
/**
* 传统取模分片
* @param id 分片键
* @return 格式: db_X.order_Y
*/
public String route(Long id) {
// 计算数据库索引
int dbIndex = (int) (id % dbCount);
// 计算表索引(使用二次哈希避免数据集中)
int tableIndex = (int) ((id / dbCount) % tableCount);
return String.format("db_%d.order_%d", dbIndex, tableIndex);
}
/**
* 一致性哈希分片(更优的实现)
*/
public String routeWithConsistentHash(Long id) {
// 计算哈希值(使用MurmurHash等高质量哈希函数)
int hash = MurmurHash.hash(id.toString());
// 映射到分片
int totalShards = dbCount * tableCount;
int shardIndex = Math.abs(hash % totalShards);
int dbIndex = shardIndex / tableCount;
int tableIndex = shardIndex % tableCount;
return String.format("db_%d.order_%d", dbIndex, tableIndex);
}
}
扩容灾难问题:
优缺点:
| 优点 | 缺点 |
|---|---|
| 数据分布均匀,负载均衡 | 扩容灾难:分片数变化导致大量数据迁移 |
| 写入压力均衡 | 范围查询需全分片扫描 |
| 适合高并发点查场景 | 多维查询困难:按其他字段查询无法定位分片 |
| 无热点问题 | 分片数必须提前规划好 |
分片策略对比总结
水平分表
水平分表是在同一个数据库内,对大表进行水平拆分,分割成多个表结构相同的表
高阶架构:解决传统分片的痛点
痛点与解决方案概览
虚拟槽分片(Virtual Slot Sharding):解决扩容难题
核心思想:两层映射
将数据与物理库解耦,通过逻辑槽位映射到物理库。
槽位映射示例
代码实现
/**
* 虚拟槽分片算法
*/
public class VirtualSlotAlgorithm implements StandardShardingAlgorithm<Long> {
private static final int VIRTUAL_SLOT_COUNT = 1024; // 虚拟槽数量(固定)
// 槽位到物理库的映射(从配置中心动态加载)
private volatile Map<Integer, String> slotToDbMapping;
/**
* 第一层映射:数据到虚拟槽(永远不变)
*/
private int calculateVirtualSlot(Long shardingValue) {
return (int) (shardingValue % VIRTUAL_SLOT_COUNT);
}
/**
* 第二层映射:虚拟槽到物理库(可动态配置)
*/
private String routeToPhysicalDB(int virtualSlot) {
return slotToDbMapping.get(virtualSlot);
}
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
// 1. 计算虚拟槽位
int slot = calculateVirtualSlot(shardingValue.getValue());
// 2. 根据配置映射到物理库
String targetDb = routeToPhysicalDB(slot);
log.debug("Routing: value={}, slot={}, db={}",
shardingValue.getValue(), slot, targetDb);
return targetDb;
}
/**
* 动态更新映射配置(配置中心回调)
*/
@NacosConfigListener(dataId = "sharding-config")
public void onConfigUpdate(String newConfig) {
this.slotToDbMapping = parseSlotMapping(newConfig);
log.info("Slot mapping updated: {}", slotToDbMapping);
}
/**
* 解析槽位映射配置
*/
private Map<Integer, String> parseSlotMapping(String config) {
Map<Integer, String> mapping = new HashMap<>();
// 配置格式: {"0-255": "db_0", "256-511": "db_2", ...}
JsonNode root = objectMapper.readTree(config);
root.fields().forEachRemaining(entry -> {
String range = entry.getKey();
String db = entry.getValue().asText();
String[] parts = range.split("-");
int start = Integer.parseInt(parts[0]);
int end = Integer.parseInt(parts[1]);
for (int i = start; i <= end; i++) {
mapping.put(i, db);
}
});
return mapping;
}
}
ShardingSphere配置示例
# application-sharding.yml
spring:
shardingsphere:
datasource:
names: db_0, db_1, db_2, db_3
db_0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0
username: root
password: ${DB_PASSWORD}
db_1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1
username: root
password: ${DB_PASSWORD}
# ... db_2, db_3 配置类似
rules:
sharding:
tables:
d_order:
actual-data-nodes: db_${0..3}.d_order_${0..15}
database-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: virtual-slot-db
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: virtual-slot-table
sharding-algorithms:
# 虚拟槽算法 - 库路由
virtual-slot-db:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: com.damai.sharding.VirtualSlotAlgorithm
# 虚拟槽算法 - 表路由
virtual-slot-table:
type: HASH_MOD
props:
sharding-count: 16 # 每个库16张表
扩容流程详解
优势
- 无需修改代码:分片算法永远是
ID % 1024,业务逻辑不变。 - 迁移数据量可控:只需迁移部分槽位的数据,而非全量数据。
- 平滑扩容:支持在线扩容,无需停机。
基因法(Gene Method):解决多维查询难题
问题场景
-- 按订单ID查询(高效 ✓ 知道分片位置)
SELECT * FROM orders WHERE order_id = 1234567890;
-- 按用户ID查询(低效 ✗ 需要全库扫描)
SELECT * FROM orders WHERE user_id = 456;
-- 不知道用户的订单在哪个分片!需要查询所有分片!
核心思想:
在分布式ID中嵌入用户基因,使得 order_id 和 user_id 在分片规则上保持一致。
数学证明:
代码实现:
/**
* 带基因的分布式ID生成器
*/
@Component
public class GeneIdGenerator {
// 基因位数(决定最大分片数:2^GENE_BITS)
private static final int GENE_BITS = 4;
// 基因掩码:00001111
private static final long GENE_MASK = (1L << GENE_BITS) - 1;
@Autowired
private Snowflake snowflake; // 雪花算法生成器
/**
* 生成带用户基因的订单ID
*
* @param userId 用户ID
* @return 带基因的订单ID
*/
public long generateOrderId(long userId) {
// 1. 生成基础雪花ID
long baseId = snowflake.nextId();
// 2. 提取用户基因(最后4位)
long userGene = userId & GENE_MASK;
// 3. 将基因注入订单ID
// 清除雪花ID的最后4位,然后或上用户基因
long orderId = (baseId & ~GENE_MASK) | userGene;
return orderId;
}
/**
* 从订单ID提取用户基因
* 用于验证或调试
*/
public long extractGene(long orderId) {
return orderId & GENE_MASK;
}
/**
* 验证订单ID和用户ID的基因是否匹配
*/
public boolean validateGene(long orderId, long userId) {
long orderGene = orderId & GENE_MASK;
long userGene = userId & GENE_MASK;
return orderGene == userGene;
}
/**
* 生成带商家基因的商品ID(多基因场景)
*/
public long generateProductId(long sellerId) {
long baseId = snowflake.nextId();
long sellerGene = sellerId & GENE_MASK;
return (baseId & ~GENE_MASK) | sellerGene;
}
}
基因法分片算法:
/**
* 基因法分片算法
*/
public class GeneShardingAlgorithm implements StandardShardingAlgorithm<Long> {
private static final int GENE_BITS = 4;
private static final long GENE_MASK = (1L << GENE_BITS) - 1;
private final int shardCount; // 分片数量,必须是2的幂次
public GeneShardingAlgorithm(int shardCount) {
// 验证分片数是2的幂次
if ((shardCount & (shardCount - 1)) != 0) {
throw new IllegalArgumentException("Shard count must be power of 2");
}
// 验证分片数不超过基因容量
if (shardCount > (1 << GENE_BITS)) {
throw new IllegalArgumentException("Shard count exceeds gene capacity");
}
this.shardCount = shardCount;
}
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long value = shardingValue.getValue();
// 提取基因位进行分片
int gene = (int) (value & GENE_MASK);
int shardIndex = gene % shardCount;
return String.format("order_%d", shardIndex);
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// 范围查询需要扫描所有分片
return availableTargetNames;
}
}
多基因场景:
/**
* 多基因ID生成器
* 支持将多个维度的基因嵌入ID
*/
public class MultiGeneIdGenerator {
// 双基因:用户基因(4位) + 商家基因(4位) = 8位
private static final int USER_GENE_BITS = 4;
private static final int SELLER_GENE_BITS = 4;
private static final long USER_GENE_MASK = 0xF; // 最后4位
private static final long SELLER_GENE_MASK = 0xF0; // 倒数5-8位
/**
* 生成带双基因的订单ID
* 支持按用户ID或商家ID查询时都能定位分片
*/
public long generateOrderId(long userId, long sellerId) {
long baseId = snowflake.nextId();
// 提取基因
long userGene = userId & 0xF;
long sellerGene = (sellerId & 0xF) << 4;
// 注入双基因
long orderId = (baseId & ~0xFF) | sellerGene | userGene;
return orderId;
}
/**
* 按用户维度分片
*/
public int shardByUser(long orderId) {
return (int) (orderId & USER_GENE_MASK);
}
/**
* 按商家维度分片
*/
public int shardBySeller(long orderId) {
return (int) ((orderId & SELLER_GENE_MASK) >> 4);
}
}
完整的最佳实践架构
架构分层设计
关键组件设计
ID生成服务
/**
* 分布式ID生成服务
* 统一管理各业务线的ID生成策略
*/
@Service
public class DistributedIdService {
@Autowired
private Snowflake snowflake;
private static final int GENE_BITS = 4;
private static final long GENE_MASK = 0xF;
/**
* 生成带基因的订单ID
* 确保同一用户的订单路由到同一分片
*/
public long generateOrderId(long userId) {
long baseId = snowflake.nextId();
long gene = userId & GENE_MASK;
return (baseId & ~GENE_MASK) | gene;
}
/**
* 从订单ID提取用户基因
* 用于反向验证或调试
*/
public long extractUserGene(long orderId) {
return orderId & GENE_MASK;
}
/**
* 生成不带基因的普通ID
* 用于不需要关联查询的业务表
*/
public long generatePlainId() {
return snowflake.nextId();
}
/**
* 生成带商家基因的商品ID
*/
public long generateProductId(long sellerId) {
long baseId = snowflake.nextId();
long gene = sellerId & GENE_MASK;
return (baseId & ~GENE_MASK) | gene;
}
/**
* 批量生成ID(提升性能)
*/
public List<Long> batchGenerateOrderId(long userId, int count) {
long gene = userId & GENE_MASK;
List<Long> ids = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
long baseId = snowflake.nextId();
ids.add((baseId & ~GENE_MASK) | gene);
}
return ids;
}
}
数据迁移服务
/**
* 数据迁移服务
* 支持虚拟槽扩容、数据重平衡等操作
*/
@Service
@Slf4j
public class DataMigrationService {
@Autowired
private JdbcTemplate sourceJdbcTemplate;
@Autowired
private JdbcTemplate targetJdbcTemplate;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 虚拟槽扩容迁移
* 将指定槽位范围的数据从源库迁移到目标库
*/
@Transactional
public MigrationResult migrateVirtualSlots(MigrationConfig config) {
log.info("Starting migration: slots {} from {} to {}",
config.getSlotRange(), config.getSourceDb(), config.getTargetDb());
MigrationResult result = new MigrationResult();
try {
// 1. 开启双写模式
enableDoubleWrite(config);
result.setPhase("DOUBLE_WRITE_ENABLED");
// 2. 迁移历史数据
long migratedCount = migrateHistoryData(config);
result.setMigratedCount(migratedCount);
result.setPhase("HISTORY_MIGRATED");
// 3. 增量数据同步
syncIncrementalData(config);
result.setPhase("INCREMENTAL_SYNCED");
// 4. 数据一致性校验
ValidationResult validation = validateDataConsistency(config);
if (!validation.isSuccess()) {
throw new MigrationException("Data validation failed: " + validation.getMessage());
}
result.setPhase("VALIDATED");
// 5. 切换读流量到新库
switchReadTraffic(config);
result.setPhase("READ_SWITCHED");
// 6. 关闭双写,完成迁移
disableDoubleWrite(config);
result.setPhase("COMPLETED");
result.setSuccess(true);
} catch (Exception e) {
log.error("Migration failed", e);
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
// 回滚操作
rollback(config);
}
return result;
}
/**
* 开启双写模式
* 对待迁移槽位的数据同时写入新旧库
*/
private void enableDoubleWrite(MigrationConfig config) {
// 更新配置中心,开启双写
String configKey = "sharding.double-write." + config.getSlotRange();
redisTemplate.opsForValue().set(configKey, "true");
log.info("Double write enabled for slots: {}", config.getSlotRange());
}
/**
* 迁移历史数据
*/
private long migrateHistoryData(MigrationConfig config) {
long totalMigrated = 0;
long lastId = 0;
int batchSize = config.getBatchSize();
while (true) {
// 分批查询源库数据
String sql = String.format(
"SELECT * FROM %s WHERE id > ? AND (id %% 1024) BETWEEN ? AND ? ORDER BY id LIMIT ?",
config.getTableName()
);
List<Map<String, Object>> batch = sourceJdbcTemplate.queryForList(
sql, lastId, config.getStartSlot(), config.getEndSlot(), batchSize
);
if (batch.isEmpty()) {
break;
}
// 批量插入目标库
batchInsert(config.getTargetDb(), config.getTableName(), batch);
totalMigrated += batch.size();
lastId = (Long) batch.get(batch.size() - 1).get("id");
log.info("Migrated {} records, total: {}", batch.size(), totalMigrated);
}
return totalMigrated;
}
/**
* 增量数据同步
* 使用Binlog或CDC方式同步迁移期间产生的新数据
*/
private void syncIncrementalData(MigrationConfig config) {
// 通过Canal或Debezium监听Binlog,同步增量数据
// 这里简化处理,实际应使用专门的CDC工具
log.info("Syncing incremental data...");
}
/**
* 数据一致性校验
*/
private ValidationResult validateDataConsistency(MigrationConfig config) {
ValidationResult result = new ValidationResult();
// 1. 校验记录数
String countSql = String.format(
"SELECT COUNT(*) FROM %s WHERE (id %% 1024) BETWEEN ? AND ?",
config.getTableName()
);
Long sourceCount = sourceJdbcTemplate.queryForObject(
countSql, Long.class, config.getStartSlot(), config.getEndSlot()
);
Long targetCount = targetJdbcTemplate.queryForObject(
countSql, Long.class, config.getStartSlot(), config.getEndSlot()
);
if (!sourceCount.equals(targetCount)) {
result.setSuccess(false);
result.setMessage(String.format("Count mismatch: source=%d, target=%d",
sourceCount, targetCount));
return result;
}
// 2. 抽样校验数据内容
// ... 随机抽取部分记录进行内容对比
result.setSuccess(true);
return result;
}
/**
* 切换读流量
*/
private void switchReadTraffic(MigrationConfig config) {
// 更新配置中心的槽位映射
String mappingConfig = buildNewSlotMapping(config);
nacosConfigService.publishConfig("sharding-slot-mapping", mappingConfig);
log.info("Read traffic switched to new db");
}
/**
* 关闭双写
*/
private void disableDoubleWrite(MigrationConfig config) {
String configKey = "sharding.double-write." + config.getSlotRange();
redisTemplate.delete(configKey);
log.info("Double write disabled");
}
/**
* 回滚迁移
*/
private void rollback(MigrationConfig config) {
log.warn("Rolling back migration...");
disableDoubleWrite(config);
// 清理目标库已迁移的数据
// 恢复原有配置
}
}
配置管理
Nacos动态配置
# sharding-config.yaml - 存储在Nacos配置中心
sharding:
# 虚拟槽配置
virtual-slots: 1024
# 槽位到物理库映射(可动态修改)
slot-mapping:
- range: "0-255"
database: db_0
tables: 16
- range: "256-511"
database: db_2
tables: 16
- range: "512-767"
database: db_1
tables: 16
- range: "768-1023"
database: db_3
tables: 16
# 基因配置
gene-config:
order:
gene-column: user_id
gene-bits: 4
gene-position: last # 基因在ID中的位置
product:
gene-column: seller_id
gene-bits: 4
gene-position: last
# 读写分离配置
read-write-splitting:
enabled: true
load-balance-algorithm: round-robin
primary: master
replicas:
- slave_0
- slave_1
# 分片算法配置
algorithms:
order-sharding:
type: VIRTUAL_SLOT_WITH_GENE
props:
virtual-slots: 1024
gene-bits: 4
配置热更新监听
/**
* 配置热更新监听器
* 监听Nacos配置变化,动态更新分片规则
*/
@Component
@Slf4j
public class ShardingConfigListener {
@Autowired
private VirtualSlotAlgorithm virtualSlotAlgorithm;
@Autowired
private ShardingSphereDataSource dataSource;
/**
* 监听槽位映射配置变化
*/
@NacosConfigListener(dataId = "sharding-slot-mapping", groupId = "SHARDING")
public void onSlotMappingChange(String newConfig) {
log.info("Slot mapping config changed: {}", newConfig);
try {
// 解析新配置
SlotMappingConfig config = parseConfig(newConfig);
// 验证配置有效性
validateConfig(config);
// 热更新槽位映射
virtualSlotAlgorithm.updateSlotMapping(config.getSlotMapping());
log.info("Slot mapping updated successfully");
} catch (Exception e) {
log.error("Failed to update slot mapping", e);
// 发送告警
alertService.sendAlert("Slot mapping update failed: " + e.getMessage());
}
}
/**
* 监听数据源配置变化
*/
@NacosConfigListener(dataId = "sharding-datasource", groupId = "SHARDING")
public void onDataSourceChange(String newConfig) {
log.info("DataSource config changed");
// 动态添加/移除数据源
// ShardingSphere支持运行时数据源变更
}
private void validateConfig(SlotMappingConfig config) {
// 验证槽位覆盖完整性(0-1023必须全部覆盖)
Set<Integer> coveredSlots = new HashSet<>();
for (SlotRange range : config.getSlotMapping()) {
for (int i = range.getStart(); i <= range.getEnd(); i++) {
if (coveredSlots.contains(i)) {
throw new ConfigValidationException("Duplicate slot: " + i);
}
coveredSlots.add(i);
}
}
if (coveredSlots.size() != 1024) {
throw new ConfigValidationException("Slot coverage incomplete");
}
}
}
监控与治理
分片健康度监控
/**
* 分片健康度监控服务
*/
@Component
@Slf4j
public class ShardingHealthMonitor {
@Autowired
private List<DataSource> dataSources;
@Autowired
private MeterRegistry meterRegistry;
/**
* 定时检查分片健康状态
*/
@Scheduled(fixedDelay = 60000)
public void monitorShardingHealth() {
// 1. 检查数据分布均匀性
checkDataDistribution();
// 2. 检查热点分片
checkHotSpots();
// 3. 检查基因法有效性
checkGeneEffectiveness();
// 4. 检查各分片连接健康
checkConnectionHealth();
// 5. 检查慢查询
checkSlowQueries();
}
/**
* 检查数据分布均匀性
*/
private void checkDataDistribution() {
Map<String, Long> dataCountByDb = new HashMap<>();
for (DataSource ds : dataSources) {
String dbName = getDbName(ds);
long count = queryDataCount(ds);
dataCountByDb.put(dbName, count);
// 上报Prometheus指标
meterRegistry.gauge("sharding.data.count",
Tags.of("db", dbName), count);
}
// 计算数据分布方差
double variance = calculateVariance(dataCountByDb.values());
double mean = dataCountByDb.values().stream()
.mapToLong(Long::longValue).average().orElse(0);
double cv = Math.sqrt(variance) / mean; // 变异系数
meterRegistry.gauge("sharding.distribution.cv", cv);
if (cv > 0.2) { // 变异系数超过20%
log.warn("Data distribution uneven, CV: {}", cv);
alertService.sendAlert("数据分布不均匀,变异系数: " + cv);
}
}
/**
* 检查热点分片
*/
private void checkHotSpots() {
Map<String, Double> qpsByDb = new HashMap<>();
for (DataSource ds : dataSources) {
String dbName = getDbName(ds);
double qps = queryCurrentQps(ds);
qpsByDb.put(dbName, qps);
meterRegistry.gauge("sharding.qps",
Tags.of("db", dbName), qps);
}
// 找出QPS最高的分片
double maxQps = Collections.max(qpsByDb.values());
double avgQps = qpsByDb.values().stream()
.mapToDouble(Double::doubleValue).average().orElse(0);
if (maxQps > avgQps * 2) { // 最大QPS超过平均值2倍
String hotDb = qpsByDb.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey).orElse("unknown");
log.warn("Hot shard detected: {}, QPS: {}", hotDb, maxQps);
alertService.sendAlert("热点分片: " + hotDb + ", QPS: " + maxQps);
}
}
/**
* 检查基因法有效性
*/
private void checkGeneEffectiveness() {
// 随机抽取用户,检查其订单是否都在同一分片
List<Long> sampleUserIds = getSampleUserIds(100);
int failedCount = 0;
for (Long userId : sampleUserIds) {
Set<String> shards = findUserOrderShards(userId);
if (shards.size() > 1) {
failedCount++;
log.warn("Gene method failed for user {}: orders in shards {}",
userId, shards);
}
}
double failRate = (double) failedCount / sampleUserIds.size();
meterRegistry.gauge("sharding.gene.fail.rate", failRate);
if (failRate > 0.01) { // 失败率超过1%
alertService.sendAlert("基因法异常,失败率: " + failRate);
}
}
/**
* 检查连接健康状态
*/
private void checkConnectionHealth() {
for (DataSource ds : dataSources) {
String dbName = getDbName(ds);
try {
Connection conn = ds.getConnection();
boolean valid = conn.isValid(5);
conn.close();
meterRegistry.gauge("sharding.connection.health",
Tags.of("db", dbName), valid ? 1 : 0);
if (!valid) {
alertService.sendAlert("数据库连接异常: " + dbName);
}
} catch (SQLException e) {
log.error("Connection check failed for {}", dbName, e);
meterRegistry.gauge("sharding.connection.health",
Tags.of("db", dbName), 0);
}
}
}
}
实战注意事项
分片键选择原则
常见业务场景分片键推荐
| 业务表 | 推荐分片键 | 理由 |
|---|---|---|
| 用户表 | user_id | 用户查询频繁,天然均匀分布 |
| 订单表 | order_id (含user基因) | 支持按订单ID和用户ID两种查询 |
| 订单明细表 | order_id | 与订单表绑定,保证同一订单数据在同一分片 |
| 商品表 | product_id (含seller基因) | 支持按商品ID和商家ID查询 |
| 交易流水 | trade_id (含user基因) | 支持按流水ID和用户ID查询 |
| 日志表 | log_id + 时间分区 | 按时间范围归档,按ID查询 |
避免的坑
坑1:业务代码硬编码分片逻辑
// ❌ 错误示例:在业务代码中硬编码分片逻辑
public Order getOrder(Long orderId) {
int shard = (int) (orderId % 4); // 硬编码分片数!
String tableName = "order_" + shard;
String sql = "SELECT * FROM " + tableName + " WHERE id = ?";
// 如果分片数变化,所有代码都要改!
return jdbcTemplate.queryForObject(sql, Order.class, orderId);
}
// ✅ 正确做法:交给中间件处理
public Order getOrder(Long orderId) {
// 直接查询逻辑表,中间件负责路由
return orderMapper.selectById(orderId);
}
坑2:跨分片JOIN
-- ❌ 错误:跨分片JOIN,性能极差
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_id = 123;
-- 如果orders和users在不同分片策略,需要跨库JOIN
-- ✅ 正确:使用基因法保证同分片,或拆分为多次查询
-- 方案1:基因法保证同分片
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = 456 AND o.order_id = 123;
-- 通过user_id基因保证都在同一分片
-- 方案2:应用层聚合
SELECT * FROM orders WHERE order_id = 123; -- 查订单
SELECT * FROM users WHERE id = 456; -- 查用户
-- 应用层合并结果
坑3:全分片扫描
-- ❌ 错误:没有分片键条件,需要扫描所有分片
SELECT * FROM orders WHERE status = 'PAID';
-- 查询所有已支付订单,需要查询所有分片!
-- ✅ 正确:带上分片键或基因列
SELECT * FROM orders
WHERE user_id = 123 AND status = 'PAID';
-- 通过user_id定位分片
-- 对于必须全表查询的场景,考虑:
-- 1. 添加汇总表/宽表
-- 2. 使用ES等搜索引擎
-- 3. 使用数据仓库
坑4:分布式事务
// ❌ 错误:跨分片事务不会自动回滚
@Transactional
public void createOrderWithPayment(Order order, Payment payment) {
orderMapper.insert(order); // 分片A
paymentMapper.insert(payment); // 分片B
// 如果paymentMapper失败,orderMapper不会回滚!
}
// ✅ 正确:使用分布式事务框架
@GlobalTransactional // Seata全局事务
public void createOrderWithPayment(Order order, Payment payment) {
orderMapper.insert(order);
paymentMapper.insert(payment);
// Seata保证全局回滚
}
// 或者使用最终一致性方案
public void createOrderWithPayment(Order order, Payment payment) {
// 1. 本地事务创建订单
orderMapper.insert(order);
// 2. 发送MQ消息,异步创建支付记录
mqTemplate.send("payment-topic", payment);
// 消费者处理失败会重试,保证最终一致
}
查询优化建议
-- 1. 使用绑定表查询(避免笛卡尔积)
-- 订单表和订单明细表使用相同分片键,配置为绑定表
SELECT o.order_id, oi.product_name
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.order_id = 123;
-- ShardingSphere知道它们在同一分片,不会产生笛卡尔积
-- 2. 利用分片键范围查询
SELECT * FROM orders
WHERE user_id = 123
AND create_time BETWEEN '2024-01-01' AND '2024-01-31';
-- user_id定位分片,create_time走分片内索引
-- 3. 批量查询优化
-- 按分片键分组后批量查询,减少跨分片请求
SELECT * FROM orders WHERE order_id IN (1,5,9); -- 同一分片
SELECT * FROM orders WHERE order_id IN (2,6,10); -- 同一分片
-- 而非 IN (1,2,5,6,9,10) 导致多次跨分片
-- 4. COUNT优化
-- ❌ 避免
SELECT COUNT(*) FROM orders; -- 需要查询所有分片再聚合
-- ✅ 推荐
-- 维护计数表或使用缓存
中间件选择
| 中间件 | 特点 | 适用场景 |
|---|---|---|
| ShardingSphere | 功能全面,支持虚拟槽、基因法 | 大型分布式系统 |
| MyCat | 轻量级,易上手 | 中小型项目 |
| Vitess | Google开源,云原生 | 云环境、K8s |
| TiDB | NewSQL,自动分片 | 需要强一致性 |
总结:
技术价值
| 技术点 | 价值 | 复杂度降低 |
|---|---|---|
| 虚拟槽技术 | 扩容复杂度从O(n)降到O(1) | 只需迁移少量数据 |
| 基因法设计 | 多维查询无需全库扫描 | 用数学优雅解决业务问题 |
| 配置驱动 | 业务代码零修改 | 通过配置中心动态调整 |
| 完整工具链 | 包含迁移、监控、校验 | 运维自动化 |
设计原则
决策流程图
Quick Reference Card
企业级项目导航:⬅️ 01-为什么掌握分片原理就能很容易实现分库分表? | 02-分库分表最佳实践:从基础原理到高阶架构设计 | ➡️ 01-用户服务分库分表设计思维全景
💬 评论