--- title: "01-分库分表最佳实践:从基础原理到高阶架构设计" created: 2026-01-09 tags: - 博客 --- # 分库分表最佳实践:从基础原理到高阶架构设计 > “分库分表的本质是为了解决单机数据库的物理极限。当单表数据超过 **500万行** 或 **连接数/IO** 打满时 ,传统的 B+ 树索引性能会急剧下降。这时候,必须通过拆分来用空间换时间。 > > “拆分策略通常分两步走: > > - 先**垂直拆分**:按业务模块(如用户库、订单库)把数据物理隔离,解决业务耦合和连接数问题 。 > - 再**水平拆分**:当单表数据量依然过大时,把一张大表拆成多个子表(分片)。基础策略有**范围分片**(扩容容易但有热点)和**哈希分片**(数据均匀但扩容难) 。” > > “传统分片有两大痛点:**扩容难**和**多维查询难**。我们的最佳实践是用两套高阶方案来解决 : > > - **解决扩容难用‘虚拟槽’**:引入一个逻辑层,数据映射到槽,槽再映射到库。扩容时只需要移动槽位,**迁移数据量小且无需修改代码** 。 > - **解决查询难用‘基因法’**:在生成订单 ID(如雪花算法)时,把用户 ID 的最后几位(基因)嵌入进去。这样**订单 ID 和用户 ID 的路由规则就一致了**,彻底避免了跨库查询的全表扫描 。” ## **引言:为什么需要分库分表?** ### **单库单表的性能瓶颈** 在单体应用或业务初期,我们通常使用单一数据库和单表存储数据。然而,随着业务增长,数据量激增,单表性能瓶颈逐渐显现: - **数据量瓶颈**:MySQL单表数据量超过 500 万行或几十 GB时,B+树索引深度增加,查询性能显著下降。 - **连接数瓶颈**:单数据库连接数有限,高并发场景下连接池耗尽。 - **磁盘I/O瓶颈**:单磁盘读写能力有限,无法支撑海量数据访问。 - **维护困难**:单表过大时,DDL操作(如加索引)可能锁表数小时。 ![[7-Blog/后端与微服务/assets/单表性能瓶颈-ca6a3c4d.jpg]] ### **性能衰减量化分析** ![[7-Blog/后端与微服务/assets/性能衰减量化分析-79fb74da.jpg]] **关键指标参考值:** | **数据量** | **索引深度** | **单次查询耗时** | **写入TPS** | | --- | --- | --- | --- | | 100万 | 3层 | 1-5ms | 5000+ | | 500万 | 3-4层 | 5-20ms | 3000 | | 2000万 | 4层 | 20-100ms | 1000 | | 1亿 | 4-5层 | 100-500ms | 300 | ### **数据库架构演进路径** ![[7-Blog/后端与微服务/assets/数据库架构演进路径-b8a82237.jpg]] ## **分库分表的基本策略** ### **垂直拆分(Vertical Sharding)** **定义**:按业务模块拆分数据库,每个服务独立使用一个数据库。 ![[7-Blog/后端与微服务/assets/垂直拆分-7db58eaa.jpg]] #### **SQL示例** ```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)** **定义**:将单表数据按某种规则拆分到多个表或库中,每个分片包含部分数据。 ![[7-Blog/后端与微服务/assets/水平拆分-779db2b7.jpg]] #### **水平分片策略详解** ##### **策略一:范围分片(Range Sharding)** **做法**:按 ID 或时间范围划分数据。 ![[7-Blog/后端与微服务/assets/范围分片-8312f717.jpg]] **代码实现:** ```java /** * 范围分片算法 */ 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 routeByRange(Long startId, Long endId) { List 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 个库/表。 ![[7-Blog/后端与微服务/assets/哈希分片-376b2a49.jpg]] **代码实现:** ```java /** * 哈希分片算法 */ 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); } } ``` **扩容灾难问题:** ![[7-Blog/后端与微服务/assets/扩容灾难问题-f741b84f.jpg]] **优缺点:** | **优点** | **缺点** | | --- | --- | | 数据分布均匀,负载均衡 | **扩容灾难**:分片数变化导致大量数据迁移 | | 写入压力均衡 | 范围查询需全分片扫描 | | 适合高并发点查场景 | **多维查询困难**:按其他字段查询无法定位分片 | | 无热点问题 | 分片数必须提前规划好 | ### **分片策略对比总结** ![[7-Blog/后端与微服务/assets/分片策略对比表-4867ef7f.jpg]] ## **高阶架构:解决传统分片的痛点** ### **痛点与解决方案概览** ![[7-Blog/后端与微服务/assets/解决传统分片的痛点-456bd66d.jpg]] ### **虚拟槽分片(Virtual Slot Sharding):解决扩容难题** #### **核心思想:两层映射** 将数据与物理库解耦,通过逻辑槽位映射到物理库。 ![[7-Blog/后端与微服务/assets/虚拟槽分片-d521be09.jpg]] #### **槽位映射示例** ![[7-Blog/后端与微服务/assets/槽位映射示例-bedc3b89.jpg]] #### **代码实现** ```java /** * 虚拟槽分片算法 */ public class VirtualSlotAlgorithm implements StandardShardingAlgorithm { private static final int VIRTUAL_SLOT_COUNT = 1024; // 虚拟槽数量(固定) // 槽位到物理库的映射(从配置中心动态加载) private volatile Map 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 availableTargetNames, PreciseShardingValue 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 parseSlotMapping(String config) { Map 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张表 ``` #### **扩容流程详解** ![[7-Blog/后端与微服务/assets/虚拟槽扩容流程-4de9a2a0.jpg]] #### **优势** - **无需修改代码**:分片算法永远是 `ID % 1024`,业务逻辑不变。 - **迁移数据量可控**:只需迁移部分槽位的数据,而非全量数据。 - **平滑扩容**:支持在线扩容,无需停机。 ### **基因法(Gene Method):解决多维查询难题** #### **问题场景** ```sql -- 按订单ID查询(高效 ✓ 知道分片位置) SELECT * FROM orders WHERE order_id = 1234567890; -- 按用户ID查询(低效 ✗ 需要全库扫描) SELECT * FROM orders WHERE user_id = 456; -- 不知道用户的订单在哪个分片!需要查询所有分片! ``` #### **核心思想:** 在分布式ID中嵌入用户基因,使得 `order_id` 和 `user_id` 在分片规则上保持一致。 ![[7-Blog/后端与微服务/assets/基因法-b8c0f366.jpg]] #### **数学证明:** ![[7-Blog/后端与微服务/assets/基因法数学证明-3f39de8b.jpg]] #### **代码实现:** ```java /** * 带基因的分布式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; } } ``` #### **基因法分片算法:** ```java /** * 基因法分片算法 */ public class GeneShardingAlgorithm implements StandardShardingAlgorithm { 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 availableTargetNames, PreciseShardingValue shardingValue) { Long value = shardingValue.getValue(); // 提取基因位进行分片 int gene = (int) (value & GENE_MASK); int shardIndex = gene % shardCount; return String.format("order_%d", shardIndex); } @Override public Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) { // 范围查询需要扫描所有分片 return availableTargetNames; } } ``` #### **多基因场景:** ```java /** * 多基因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); } } ``` ## **完整的最佳实践架构** ### **架构分层设计** ![[7-Blog/后端与微服务/assets/分库分表架构分层设计-1571f965.jpg]] ### **关键组件设计** #### **ID生成服务** ```java /** * 分布式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 batchGenerateOrderId(long userId, int count) { long gene = userId & GENE_MASK; List ids = new ArrayList<>(count); for (int i = 0; i < count; i++) { long baseId = snowflake.nextId(); ids.add((baseId & ~GENE_MASK) | gene); } return ids; } } ``` #### **数据迁移服务** ```java /** * 数据迁移服务 * 支持虚拟槽扩容、数据重平衡等操作 */ @Service @Slf4j public class DataMigrationService { @Autowired private JdbcTemplate sourceJdbcTemplate; @Autowired private JdbcTemplate targetJdbcTemplate; @Autowired private RedisTemplate 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> 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动态配置** ```yaml # 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 ``` #### **配置热更新监听** ```java /** * 配置热更新监听器 * 监听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 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"); } } } ``` ### **监控与治理** #### **分片健康度监控** ```java /** * 分片健康度监控服务 */ @Component @Slf4j public class ShardingHealthMonitor { @Autowired private List 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 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 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 sampleUserIds = getSampleUserIds(100); int failedCount = 0; for (Long userId : sampleUserIds) { Set 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); } } } } ``` ## **实战注意事项** ### **分片键选择原则** ![[7-Blog/后端与微服务/assets/分片键选择原则-390e6520.jpg]] ### **常见业务场景分片键推荐** | **业务表** | **推荐分片键** | **理由** | | --- | --- | --- | | 用户表 | user\_id | 用户查询频繁,天然均匀分布 | | 订单表 | order\_id (含user基因) | 支持按订单ID和用户ID两种查询 | | 订单明细表 | order\_id | 与订单表绑定,保证同一订单数据在同一分片 | | 商品表 | product\_id (含seller基因) | 支持按商品ID和商家ID查询 | | 交易流水 | trade\_id (含user基因) | 支持按流水ID和用户ID查询 | | 日志表 | log\_id + 时间分区 | 按时间范围归档,按ID查询 | ### **避免的坑** #### **坑1:业务代码硬编码分片逻辑** ```java // ❌ 错误示例:在业务代码中硬编码分片逻辑 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** ```sql -- ❌ 错误:跨分片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:全分片扫描** ```sql -- ❌ 错误:没有分片键条件,需要扫描所有分片 SELECT * FROM orders WHERE status = 'PAID'; -- 查询所有已支付订单,需要查询所有分片! -- ✅ 正确:带上分片键或基因列 SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID'; -- 通过user_id定位分片 -- 对于必须全表查询的场景,考虑: -- 1. 添加汇总表/宽表 -- 2. 使用ES等搜索引擎 -- 3. 使用数据仓库 ``` #### **坑4:分布式事务** ```java // ❌ 错误:跨分片事务不会自动回滚 @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); // 消费者处理失败会重试,保证最终一致 } ``` ### **查询优化建议** ```sql -- 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) | 只需迁移少量数据 | | **基因法设计** | 多维查询无需全库扫描 | 用数学优雅解决业务问题 | | **配置驱动** | 业务代码零修改 | 通过配置中心动态调整 | | **完整工具链** | 包含迁移、监控、校验 | 运维自动化 | ### **设计原则** ![[7-Blog/后端与微服务/assets/分库分表设计原则-84e063ee.jpg]] ### **决策流程图** ![[7-Blog/后端与微服务/assets/决策流程图-fa56515d.jpg]] ### **Quick Reference Card** ![[7-Blog/后端与微服务/assets/Quick_Reference_Card-c48c41a2.jpg]]