分库分表实战指南
大约 16 分钟约 4792 字
分库分表实战指南
简介
分库分表(Database Sharding)是解决单库单表数据量过大、写入瓶颈、查询性能下降的核心架构方案。当 MySQL 单表数据超过 500 万行、单库数据超过 100GB、或 QPS 超过 5000 时,通常需要考虑分库分表。
分库分表的核心思想是将数据按照某种规则分散到多个数据库或表中,使得每个库/表的数据量和压力都在可控范围内。根据切分维度,分为垂直拆分(按业务拆分)和水平拆分(按数据行拆分)。根据存储方式,分为分库(不同物理数据库实例)和分表(同一实例的不同表)。
分库分表不是银弹,引入它会带来分布式事务、跨库查询、数据迁移、运维复杂度等一系列挑战。因此决策前需要充分评估:读写分离、缓存优化、索引优化、分区表等方案是否能解决问题。只有当这些方案无法满足需求时,才考虑分库分表。
特点
垂直拆分
-- 垂直拆分:按业务模块将不同表拆分到不同数据库
-- 原始 monolith_db 包含所有表
-- 拆分后:
-- 用户库 (user_db)
CREATE DATABASE user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE user_db;
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
phone VARCHAR(20),
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_phone (phone),
INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB;
CREATE TABLE user_profiles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
nickname VARCHAR(50),
avatar_url VARCHAR(255),
birthday DATE,
gender TINYINT,
bio TEXT,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
-- 订单库 (order_db)
CREATE DATABASE order_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE order_db;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
payment_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_order_no (order_no),
INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(200),
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL,
INDEX idx_order_id (order_id)
) ENGINE=InnoDB;
-- 商品库 (product_db)
CREATE DATABASE product_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE product_db;
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
category_id BIGINT,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
status TINYINT DEFAULT 1,
INDEX idx_category (category_id),
INDEX idx_status (status)
) ENGINE=InnoDB;水平拆分策略
Hash 分片
-- Hash 分片:根据分片键的哈希值确定数据归属
-- 假设按 user_id 分 4 个库,每个库 4 张表
-- 分库规则: user_id % 4 -> 库编号 (0-3)
-- 分表规则: (user_id / 4) % 4 -> 表编号 (0-3)
-- 库 0: ds_0
CREATE DATABASE ds_0 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ds_0;
CREATE TABLE orders_0 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_1 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_2 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_3 (LIKE order_db.orders 原始结构);
-- 库 1: ds_1
CREATE DATABASE ds_1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ds_1;
CREATE TABLE orders_0 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_1 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_2 (LIKE order_db.orders 原始结构);
CREATE TABLE orders_3 (LIKE order_db.orders 原始结构);
-- 库 2、库 3 类似...
-- Hash 分片路由计算示例:
-- user_id = 100
-- 分库: 100 % 4 = 0 -> ds_0
-- 分表: (100 / 4) % 4 = 25 % 4 = 1 -> orders_1
-- 最终: ds_0.orders_1
-- user_id = 255
-- 分库: 255 % 4 = 3 -> ds_3
-- 分表: (255 / 4) % 4 = 63 % 4 = 3 -> orders_3
-- 最终: ds_3.orders_3范围分片
-- 范围分片:按数据范围划分到不同分片
-- 按时间范围分片,适合日志、订单等时序数据
-- 2024年 Q1 订单
CREATE TABLE orders_2024q1 (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB;
-- 2024年 Q2 订单
CREATE TABLE orders_2024q2 (
-- 结构相同
) ENGINE=InnoDB;
-- 范围分片路由规则
-- created_at >= '2024-01-01' AND created_at < '2024-04-01' -> orders_2024q1
-- created_at >= '2024-04-01' AND created_at < '2024-07-01' -> orders_2024q2
-- 范围分片的优点:
-- 1. 范围查询高效(落在单个分片内)
-- 2. 扩容方便(添加新分片不影响旧数据)
-- 3. 数据归档简单(直接删除旧分片)
-- 范围分片的缺点:
-- 1. 热点问题(最新数据集中在同一个分片)
-- 2. 数据分布可能不均匀一致性哈希分片
-- 一致性哈希:解决传统哈希扩容时大量数据迁移的问题
-- 一致性哈希环示例(简化版)
-- 假设有 4 个节点在哈希环上均匀分布
-- 节点分布:
-- Node A: hash range [0, 2^32/4)
-- Node B: hash range [2^32/4, 2^32/2)
-- Node C: hash range [2^32/2, 3*2^32/4)
-- Node D: hash range [3*2^32/4, 2^32)
-- 当添加新节点 E 时:
-- 只需要从 Node D 迁移约 1/5 的数据到 E
-- 其他节点不受影响(传统 hash 取模需要迁移 80% 的数据)
-- 一致性哈希 SQL 查询路由(伪代码)
/*
function route(user_id):
hash = consistent_hash(user_id) // 范围 [0, 2^32)
node = find_clockwise_node(hash) // 顺时针找到第一个节点
return node
*/ShardingSphere 集成
ShardingSphere-JDBC 配置
# application-sharding.yml — ShardingSphere 分片配置
mode:
type: Standalone
repository:
type: JDBC
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/ds_0?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 5
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3307/ds_1?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 5
rules:
- !SHARDING
tables:
# 订单表:按 user_id 分库分表
orders:
actualDataNodes: ds_${0..1}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_table_inline
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_db_inline
keyGenerateStrategy:
column: id
keyGeneratorName: snowflake
# 订单明细表:与订单表同分片
order_items:
actualDataNodes: ds_${0..1}.order_items_${0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_table_inline
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: orders_db_inline
# 分片算法定义
shardingAlgorithms:
orders_db_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
orders_table_inline:
type: INLINE
props:
algorithm-expression: orders_${(user_id / 2).intValue() % 4}
# 主键生成策略
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1.NET 集成 ShardingSphere
// .NET 通过 ShardingCore 或自定义中间件实现分库分表
// 1. 安装 NuGet 包
// dotnet add package ShardingCore
// dotnet add package Microsoft.EntityFrameworkCore
using Microsoft.EntityFrameworkCore;
using ShardingCore.Core.VirtualDatabase.VirtualDataSources;
using ShardingCore.Core.VirtualRoutes;
using ShardingCore.Core.VirtualRoutes.TableRoutes.RouteTails.Abstractions;
using ShardingCore.Sharding;
// 2. 定义实体
public class Order
{
public long Id { get; set; }
public string OrderNo { get; set; }
public long UserId { get; set; }
public decimal TotalAmount { get; set; }
public int Status { get; set; }
public DateTime CreatedAt { get; set; }
}
// 3. 定义 DbContext
public class AppDbContext : AbstractShardingDbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.OrderNo).HasMaxLength(32).IsRequired();
entity.Property(e => e.TotalAmount).HasColumnType("decimal(12,2)");
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.CreatedAt);
});
base.OnModelCreating(modelBuilder);
}
}
// 4. 自定义分片路由
public class OrderVirtualTableRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<Order>
{
// 按 UserId 取模分表,分为 4 个表
public OrderVirtualTableRoute() : base(2, 4) { }
public override string ShardingKeyToTail(object shardingKey)
{
var userId = Convert.ToInt64(shardingKey);
var tail = userId % 4;
return tail.ToString().PadLeft(2, '0');
}
public override Func<string, bool> GetRouteToFilter(string tail)
{
return table => table.EndsWith($"_{tail}");
}
}
// 5. Startup 配置
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddShardingDbContext<AppDbContext>()
.AddEntityConfig(o =>
{
o.ThrowIfQueryRouteNotMatch = false;
o.CreateShardingTableOnStart = true;
})
.AddConfig(o =>
{
o.ConfigId = "c1";
o.AddDefaultDataSource("ds_0", "Server=localhost;Database=ds_0;...");
o.AddExtraDataSource("ds_1",
new DataSourceConfig("ds_1", "Server=localhost;Database=ds_1;..."));
})
.EnsureConfig();
}
}分布式主键
-- 分布式主键方案对比
-- 方案 1: Snowflake 雪花算法(推荐)
-- 结构: 1位符号 + 41位时间戳 + 10位机器ID + 12位序列号
-- 优点: 趋势递增、不依赖数据库、高性能
-- 缺点: 依赖时钟,时钟回拨会出问题
-- 方案 2: UUID
-- 优点: 简单、全局唯一
-- 缺点: 无序(导致 B+ 树频繁页分裂)、长度太长(36字符)
-- 方案 3: 数据库号段模式
-- 一次性取一批 ID,用完再取
CREATE TABLE id_segment (
biz_tag VARCHAR(64) PRIMARY KEY COMMENT '业务标识',
max_id BIGINT NOT NULL DEFAULT 0 COMMENT '当前最大ID',
step INT NOT NULL DEFAULT 1000 COMMENT '步长',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
INSERT INTO id_segment (biz_tag, max_id, step) VALUES ('order', 0, 1000);
-- 获取一批 ID
UPDATE id_segment
SET max_id = max_id + step, version = version + 1
WHERE biz_tag = 'order' AND version = 0;
SELECT max_id FROM id_segment WHERE biz_tag = 'order';
-- 应用侧拿到 max_id 后,在 [max_id - step + 1, max_id] 范围内分配 ID# Python 实现雪花算法
import time
import threading
class SnowflakeGenerator:
"""雪花算法 ID 生成器
ID 结构 (64位):
| 1位符号 | 41位时间戳 | 10位机器ID | 12位序列号 |
|---------|-----------|-----------|-----------|
| 0 | 毫秒级 | 0-1023 | 0-4095 |
41位时间戳可用约69年
10位机器ID支持1024个节点
12位序列号每毫秒可生成4096个ID
"""
EPOCH = 1704067200000 # 2024-01-01 00:00:00 的毫秒时间戳
def __init__(self, worker_id: int = 1, datacenter_id: int = 0):
self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock()
if worker_id > 1023:
raise ValueError("worker_id 不能超过 1023")
def _current_millis(self) -> int:
return int(time.time() * 1000)
def _wait_next_millis(self, last: int) -> int:
timestamp = self._current_millis()
while timestamp <= last:
timestamp = self._current_millis()
return timestamp
def next_id(self) -> int:
with self.lock:
timestamp = self._current_millis()
if timestamp < self.last_timestamp:
raise RuntimeError(
f"时钟回拨: {self.last_timestamp - timestamp}ms"
)
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF # 4095
if self.sequence == 0:
timestamp = self._wait_next_millis(self.last_timestamp)
else:
self.sequence = 0
self.last_timestamp = timestamp
return (
((timestamp - self.EPOCH) << 22)
| (self.datacenter_id << 12)
| (self.worker_id << 12)
| self.sequence
)
# 使用示例
generator = SnowflakeGenerator(worker_id=1)
for _ in range(5):
sid = generator.next_id()
print(f"Snowflake ID: {sid}")跨分片查询
-- 跨分片查询是分库分表最大的挑战之一
-- 场景 1: 分页查询(全局排序)
-- 问题:每个分片返回自己的 TOP N,需要合并后重新排序
-- ShardingSphere 自动处理:
-- 这条 SQL 被改写为多个分片查询
SELECT * FROM orders
WHERE status = 1
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
-- 实际执行(假设 2库 x 4表 = 8 个分片):
-- ds_0.orders_0: SELECT ... ORDER BY created_at DESC LIMIT 10
-- ds_0.orders_1: SELECT ... ORDER BY created_at DESC LIMIT 10
-- ...
-- ds_1.orders_3: SELECT ... ORDER BY created_at DESC LIMIT 10
-- 然后在中间件层合并排序取 TOP 10
-- 场景 2: 跨分片 JOIN
-- 问题:不同分片的表无法直接 JOIN
-- 解决方案 A:绑定表(相同分片键的表在同一分片)
-- orders 和 order_items 都按 user_id 分片
SELECT o.order_no, oi.product_name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = 100;
-- 这条可以路由到单个分片
-- 解决方案 B:全局表(每个分片都有完整数据)
-- 适合数据量小的维度表(如商品分类、地区等)
-- 解决方案 C:应用层 JOIN
-- 先查 orders 获取 order_id 列表
-- 再查 order_items(按 order_id 或 user_id 路由)
-- 场景 3: 聚合查询
SELECT
COUNT(*) as total_orders,
SUM(total_amount) as total_amount,
AVG(total_amount) as avg_amount
FROM orders
WHERE created_at >= '2024-01-01';
-- 实际执行:每个分片返回聚合结果,中间件合并
-- ds_0: SELECT COUNT(*), SUM(total_amount) FROM orders_0 WHERE ...
-- ds_0: SELECT COUNT(*), SUM(total_amount) FROM orders_1 WHERE ...
-- ...
-- 合并: total = sum(各分片 COUNT)
-- sum = sum(各分片 SUM)
-- avg = total_sum / total_count数据迁移方案
-- 数据迁移:从单库迁移到分库分表
-- 步骤 1: 创建分片表结构
-- 在 ds_0, ds_1 中创建 orders_0 ~ orders_3
-- 步骤 2: 双写阶段(写入新旧两套库)
-- 应用层同时写入原库和新的分片库
-- 步骤 3: 历史数据迁移
-- 迁移脚本(按 user_id 分片)
INSERT INTO ds_0.orders_0
SELECT * FROM old_db.orders
WHERE user_id % 2 = 0 AND (user_id / 2) % 4 = 0;
INSERT INTO ds_0.orders_1
SELECT * FROM old_db.orders
WHERE user_id % 2 = 0 AND (user_id / 2) % 4 = 1;
INSERT INTO ds_0.orders_2
SELECT * FROM old_db.orders
WHERE user_id % 2 = 0 AND (user_id / 2) % 4 = 2;
INSERT INTO ds_0.orders_3
SELECT * FROM old_db.orders
WHERE user_id % 2 = 0 AND (user_id / 2) % 4 = 3;
INSERT INTO ds_1.orders_0
SELECT * FROM old_db.orders
WHERE user_id % 2 = 1 AND (user_id / 2) % 4 = 0;
-- ... 以此类推
-- 步骤 4: 数据校验
SELECT COUNT(*) FROM old_db.orders;
-- 对比
SELECT SUM(cnt) FROM (
SELECT COUNT(*) as cnt FROM ds_0.orders_0
UNION ALL SELECT COUNT(*) FROM ds_0.orders_1
UNION ALL SELECT COUNT(*) FROM ds_0.orders_2
UNION ALL SELECT COUNT(*) FROM ds_0.orders_3
UNION ALL SELECT COUNT(*) FROM ds_1.orders_0
UNION ALL SELECT COUNT(*) FROM ds_1.orders_1
UNION ALL SELECT COUNT(*) FROM ds_1.orders_2
UNION ALL SELECT COUNT(*) FROM ds_1.orders_3
) t;
-- 步骤 5: 切换读流量到新库
-- 步骤 6: 停止双写,切换写流量
-- 步骤 7: 下线旧库(保留一段时间以防需要回滚)# Python 数据迁移工具
import pymysql
from dataclasses import dataclass
@dataclass
class ShardConfig:
host: str
port: int
database: str
user: str
password: str
class DataMigration:
"""分库分表数据迁移工具"""
def __init__(self, source_config: dict, shard_configs: list):
self.source = pymysql.connect(**source_config)
self.shards = [
pymysql.connect(**cfg) for cfg in shard_configs
]
self.db_count = len(shard_configs)
self.table_count = 4 # 每个库的表数
def get_route(self, user_id: int) -> tuple:
"""计算分片路由"""
db_idx = user_id % self.db_count
table_idx = (user_id // self.db_count) % self.table_count
return db_idx, table_idx
def migrate_table(self, source_table: str, target_table: str,
batch_size: int = 1000):
"""迁移单张表数据"""
offset = 0
total_migrated = 0
while True:
with self.source.cursor() as cursor:
cursor.execute(
f"SELECT * FROM {source_table} "
f"ORDER BY id LIMIT {batch_size} OFFSET {offset}"
)
rows = cursor.fetchall()
if not rows:
break
for row in rows:
user_id = row[2] # 假设第3列是 user_id
db_idx, table_idx = self.get_route(user_id)
target_table_name = f"{target_table}_{table_idx}"
with self.shards[db_idx].cursor() as cursor:
placeholders = ",".join(["%s"] * len(row))
cursor.execute(
f"INSERT INTO {target_table_name} VALUES ({placeholders})",
row
)
self.shards[db_idx].commit()
total_migrated += len(rows)
offset += batch_size
print(f"已迁移 {total_migrated} 条记录")
return total_migrated
def verify_migration(self, source_table: str, target_prefix: str):
"""验证迁移数据完整性"""
with self.source.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM {source_table}")
source_count = cursor.fetchone()[0]
target_count = 0
for i, shard in enumerate(self.shards):
for j in range(self.table_count):
with shard.cursor() as cursor:
cursor.execute(f"SELECT COUNT(*) FROM {target_prefix}_{j}")
target_count += cursor.fetchone()[0]
if source_count == target_count:
print(f"验证通过: 源表 {source_count} 条 = 目标表 {target_count} 条")
else:
print(f"验证失败: 源表 {source_count} 条 != 目标表 {target_count} 条")
return source_count == target_count扩容方案
-- 分库分表扩容方案
-- 方案 1: 倍数扩容(推荐)
-- 初始: 2库 x 4表 = 8 分片
-- 扩容: 4库 x 4表 = 16 分片
-- 原理:分库数翻倍,每个新库的数据来自旧库的一半
-- ds_0 的数据拆分到 ds_0 和 ds_2
-- ds_1 的数据拆分到 ds_1 和 ds_3
-- 数据迁移(以 ds_0 -> ds_0 + ds_2 为例)
-- ds_0 保留 user_id % 4 == 0 的数据
-- ds_2 接收 user_id % 4 == 2 的数据
-- 步骤:
-- 1. 部署 ds_2, ds_3(新库)
-- 2. 修改路由规则 user_id % 4
-- 3. 迁移数据
-- 4. 清理旧库中已迁移的数据
-- 方案 2: 一致性哈希扩容
-- 添加新节点时只迁移部分数据
-- 数据迁移量 ≈ 总数据量 / (旧节点数 + 1)
-- 方案 3: 垂直 + 水平组合扩容
-- 先垂直拆分(不同业务到不同库)
-- 再对热点业务做水平拆分分片路由监控
-- 分片路由监控 SQL
-- 1. 各分片数据量监控
SELECT 'ds_0.orders_0' as shard, COUNT(*) as rows_count FROM ds_0.orders_0
UNION ALL
SELECT 'ds_0.orders_1', COUNT(*) FROM ds_0.orders_1
UNION ALL
SELECT 'ds_0.orders_2', COUNT(*) FROM ds_0.orders_2
UNION ALL
SELECT 'ds_0.orders_3', COUNT(*) FROM ds_0.orders_3
UNION ALL
SELECT 'ds_1.orders_0', COUNT(*) FROM ds_1.orders_0
UNION ALL
SELECT 'ds_1.orders_1', COUNT(*) FROM ds_1.orders_1
UNION ALL
SELECT 'ds_1.orders_2', COUNT(*) FROM ds_1.orders_2
UNION ALL
SELECT 'ds_1.orders_3', COUNT(*) FROM ds_1.orders_3;
-- 2. 分片数据倾斜检测
-- 理想情况各分片数据量差异不超过 10%
-- 超过 20% 说明分片键选择有问题
-- 3. 慢查询监控
SELECT
shard_name,
query_text,
execution_time_ms,
scan_rows
FROM sharding_monitor
WHERE execution_time_ms > 1000
ORDER BY execution_time_ms DESC
LIMIT 20;
-- 4. 跨分片查询检测
-- 记录所有包含多个分片的查询
-- 优化方向:尽量让查询落到单个分片优点
缺点
性能注意事项
- 分片键选择:选择高基数字段(如 user_id),避免低基数字段(如 status)
- 分片数量:不宜过多,8-32 个分片通常足够,过多增加管理复杂度
- 分页查询:深度分页性能差,建议使用游标分页
- 聚合查询:全局 COUNT/SUM/AVG 需要扫描所有分片,考虑用汇总表
- 连接池:每个分片独立的连接池,总连接数 = 分片数 x 每分片连接数
- 批量插入:大量数据插入时注意各分片的负载均衡
总结
分库分表是数据库水平扩展的核心方案,ShardingSphere 提供了成熟的中间件支持。选择合适的分片策略(Hash/Range/一致性哈希)和分片键是成功的关键。务必在分库分表之前优先考虑读写分离、缓存、索引优化等方案,因为分库分表引入的复杂度是显著的。
关键知识点
- 垂直拆分 vs 水平拆分 — 垂直按业务分、水平按数据行分
- Hash 分片 — 数据分布均匀,扩容需迁移大量数据
- 范围分片 — 适合时序数据,但有热点问题
- 一致性哈希 — 扩容只需迁移少量数据
- 分布式主键 — Snowflake 是最常用的方案
- 绑定表 — 相同分片键的表路由到同一分片,避免跨分片 JOIN
- 双写迁移 — 数据迁移的标准流程
- ShardingSphere — Java 生态最成熟的分片中间件
常见误区
- 过早分片:数据量不到百万级就分库分表,增加不必要的复杂度
- 分片键选错:用低基数字段(如 status)做分片键,导致数据倾斜
- 忽视跨分片查询:未评估业务查询模式,上线后大量跨分片查询
- 无限分片:分片数过多(如 256 个),运维成本远超收益
- 不做数据校验:迁移后不校验数据完整性,导致数据丢失
- 忘记全局表:维度表没有配置为全局表,导致跨分片 JOIN
进阶路线
- 入门:理解垂直/水平拆分原理,配置 ShardingSphere 基础分片
- 进阶:分布式主键生成、跨分片查询优化、数据迁移
- 高级:分片扩容方案、分布式事务、一致性哈希
- 专家:自动分片调度、数据自平衡、多租户分片
适用场景
- 单表数据超过 500 万行的业务系统
- 高并发写入场景(如电商订单、日志采集)
- 多租户 SaaS 平台
- 大数据量的历史数据归档
- 读写分离无法满足性能需求的场景
落地建议
- 第一步:评估当前数据量和增长趋势,确认是否需要分片
- 第二步:选择分片键和分片策略,验证数据分布均匀性
- 第三步:搭建 ShardingSphere 环境,编写分片配置
- 第四步:制定数据迁移方案,小规模验证
- 第五步:灰度切换,监控分片路由和性能
- 持续:监控各分片数据量和性能,规划扩容
排错清单
复盘问题
- 当前各分片的数据分布是否均匀?最大分片和最小分片的数据量差异是多少?
- 跨分片查询占比多少?是否有优化空间?
- 最近一次扩容是什么时候?扩容过程是否顺利?
- 分片键的选择是否合理?是否有数据倾斜?
- 备份恢复方案是否验证过?恢复时间是否满足 RTO?
- ShardingSphere 的配置是否有冗余或错误?
