前言
随着业务规模的快速增长,单库单表的架构模式逐渐暴露出性能瓶颈、存储限制和维护困难等问题。分库分表作为解决数据库扩展性的核心方案,已成为大型互联网系统架构设计中的重要组成部分。本文将系统性地介绍分库分表的核心概念、实施准则、拆分规则和最佳实践,帮助读者在实际项目中做出正确的架构决策。
一、基础概念解析
1.1 分库分表的核心定义
分库分表是指将原本存储在单个数据库实例中的数据,按照一定的规则分散存储到多个数据库实例或多个数据表中,从而达到提升系统性能、扩展存储容量、降低单点压力的目的。
核心解决的扩展性问题
| 问题类型 | 具体表现 | 分库分表解决方案 |
|---|---|---|
| 性能瓶颈 | 单表数据量超过千万级,查询响应时间显著增加 | 通过分表减少单表数据量,提升查询效率 |
| 存储限制 | 单实例磁盘空间不足,无法继续扩容 | 通过分库将数据分散到多个实例,突破存储上限 |
| 连接数瓶颈 | 数据库连接数达到上限,影响并发处理能力 | 通过分库分摊连接压力,提升整体并发能力 |
| IO压力 | 单实例IO负载过高,导致读写性能下降 | 通过分库分表将IO分散到多个物理设备 |
| 维护困难 | 大表备份、恢复、索引重建耗时过长 | 小表维护成本更低,可并行操作 |
1.2 垂直拆分 vs 水平拆分
垂直拆分(Vertical Splitting)
垂直拆分是基于业务逻辑或数据表结构进行的拆分,分为垂直分库 和垂直分表两种形式。
垂直分库:
- 按照业务模块将不同的表分配到不同的数据库中
- 示例:用户库、订单库、商品库、支付库等
垂直分表:
- 将大表中不常用的字段拆分到新表中
- 示例:将用户基础信息和用户扩展信息分离
sql
-- 垂直分表示例
-- 原表:user
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
address TEXT,
profile TEXT,
create_time DATETIME
);
-- 拆分后:基础信息表
CREATE TABLE user_basic (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
create_time DATETIME
);
-- 拆分后:扩展信息表
CREATE TABLE user_profile (
user_id BIGINT PRIMARY KEY,
address TEXT,
profile TEXT,
update_time DATETIME
);
优点:
- 拆分规则清晰,实施相对简单
- 能够解决单表字段过多的问题
- 便于业务模块的独立扩展
缺点:
- 部分业务场景下存在跨库事务问题
- 无法解决单表数据量过大的问题
适用场景:
- 业务模块边界清晰,耦合度低
- 单表字段过多(超过20-30个字段)
- 不同模块的访问频率差异较大
水平拆分(Horizontal Splitting)
水平拆分是根据数据的行进行拆分,将数据按照某种规则分散到多个数据库或表中。
水平分库:
- 将同一表的数据分散到多个数据库中
- 示例:将订单表按用户ID哈希分散到8个数据库中
水平分表:
- 将同一库中的表数据分散到多个表中
- 示例:将用户表按ID范围分散到user_0、user_1、user_2...中
sql
-- 水平分表示例
-- 原表:order
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
amount DECIMAL(10,2),
status INT,
create_time DATETIME
);
-- 拆分后:按user_id哈希分表
CREATE TABLE order_0 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
amount DECIMAL(10,2),
status INT,
create_time DATETIME
);
CREATE TABLE order_1 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
amount DECIMAL(10,2),
status INT,
create_time DATETIME
);
CREATE TABLE order_2 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
amount DECIMAL(10,2),
status INT,
create_time DATETIME
);
优点:
- 能够有效解决单表数据量过大的问题
- 提升查询性能,减少单表数据扫描
- 便于数据迁移和扩容
缺点:
- 跨表分页查询复杂度增加
- 需要引入额外的路由层
- 数据一致性维护难度增加
适用场景:
- 单表数据量超过千万级
- 写入量达到单实例瓶颈
- 需要横向扩展系统容量
1.3 分库与分表的关联性与独立性
关联性:
- 分库通常伴随着分表,两者经常同时使用
- 分库是物理层面的拆分,分表是逻辑层面的拆分
- 分库分表的设计需要综合考虑,形成统一的策略
独立性:
- 分库可以独立于分表实施,仅解决实例层面的问题
- 分表可以在单一数据库中独立实施,解决单表问题
- 在某些场景下,可以只分库不分表,或只分表不分库
java
// 分库分表策略组合示例
public class ShardingStrategy {
// 只分库不分表:按用户ID哈希分到8个库
public int getDatabaseShard(Long userId) {
return (int) (userId % 8);
}
// 只分表不分库:按时间范围分表
public String getTableShard(Date createTime) {
String month = new SimpleDateFormat("yyyyMM").format(createTime);
return "order_" + month;
}
// 分库分表:先分库再分表
public String getFullTableName(Long userId, Date createTime) {
int dbShard = getDatabaseShard(userId);
String tableShard = getTableShard(createTime);
return "db" + dbShard + ".order_" + tableShard;
}
}
二、实施准则与依据
2.1 分库分表的决策依据
数据量阈值参考
| 指标 | 阈值 | 说明 |
|---|---|---|
| 单表数据量 | 1000万-2000万行 | 超过此范围,查询性能明显下降 |
| 单表存储容量 | 10GB-50GB | 根据数据库类型和硬件配置调整 |
| 单库连接数 | 接近数据库最大连接数的70% | 需要考虑分库分表连接池压力 |
| QPS/TPS | 单实例QPS > 5000 或 TPS > 1000 | 根据业务类型和数据库性能评估 |
性能指标参考
sql
-- 查询性能分析示例
EXPLAIN SELECT * FROM user WHERE id = 123456;
-- 关注:type是否为const或eq_ref、rows扫描行数、Extra是否包含Using index
-- 慢查询分析
SELECT
sql_text,
exec_count,
avg_time,
max_time
FROM mysql.slow_log
WHERE avg_time > 1000 -- 超过1秒的查询
ORDER BY avg_time DESC
LIMIT 10;
业务增长预期
短期预期(3-6个月):
- 评估当前数据增长速度
- 预测峰值压力和存储需求
- 制定分库分表实施时间表
中长期预期(1-2年):
- 考虑业务爆发性增长的可能性
- 设计具有弹性的分片策略
- 规划多级扩容方案
2.2 实施前提条件与准备工作
技术前提条件
-
完善的监控体系
- 数据库性能监控(QPS、TPS、慢查询、连接数)
- 应用层监控(响应时间、错误率、调用链)
- 资源监控(CPU、内存、磁盘IO、网络)
-
代码层面的改造能力
- 访问层需要支持分库分表的路由逻辑
- ORM框架需要支持多数据源配置
- 需要引入分布式事务解决方案
-
运维能力准备
- 数据库备份与恢复方案
- 数据迁移与验证工具
- 故障快速切换机制
准备工作清单
□ 业务场景分析:梳理核心业务的访问模式
□ 数据特征分析:统计数据量、增长趋势、热点数据
□ 技术选型:选择合适的分库分表中间件(ShardingSphere、MyCAT等)
□ 环境准备:部署分库分表测试环境
□ 测试验证:功能测试、性能测试、压力测试
□ 应急预案:制定回滚方案和应急处理流程
□ 文档完善:更新架构文档、操作手册、应急预案
2.3 分库分表的挑战与规避策略
主要挑战
| 挑战类型 | 具体表现 | 影响 |
|---|---|---|
| 分布式事务 | 跨库事务一致性难以保证 | 数据一致性问题 |
| 跨库关联查询 | JOIN操作性能急剧下降 | 查询效率降低 |
| 分页查询 | 跨表分页结果不准确 | 用户体验差 |
| 全局唯一ID | 分布式环境下的ID生成 | 业务逻辑复杂 |
| 数据迁移 | 在线数据迁移困难 | 业务中断风险 |
| 扩容复杂 | 重新分片涉及大量数据移动 | 运维成本高 |
规避策略
1. 分布式事务处理
java
// 基于最终一致性的方案
public class OrderService {
@Autowired
private MessageProducer messageProducer;
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountMapper accountMapper;
public void createOrder(Order order) {
// 1. 创建订单(主库操作)
orderMapper.insert(order);
// 2. 发送消息到MQ
OrderMessage message = new OrderMessage();
message.setOrderId(order.getId());
message.setUserId(order.getUserId());
message.setAmount(order.getAmount());
messageProducer.send(message);
// 3. 账户服务消费消息处理(异步)
// accountService.decreaseAccount(message);
}
}
2. 跨库联表查询优化
- 数据冗余:在相关表中冗余必要字段
- 应用层JOIN:在应用层进行数据组装
- 宽表设计:将关联数据设计到同一分片
3. 分页查询优化
java
// 使用ID范围分页
public List<Order> getOrdersByPage(Long lastId, int pageSize) {
return orderMapper.selectPage(
"WHERE id > #{lastId} ORDER BY id LIMIT #{pageSize}",
lastId, pageSize
);
}
// 使用缓存+游标方式
public CursorResult<Order> getOrdersWithCursor(String cursor, int pageSize) {
// 使用Redis存储游标状态
String cursorKey = "order_cursor:" + cursor;
// 分片查询并组装结果
return cursorResult;
}
三、核心拆分规则
3.1 分片策略详解
1. 范围分片(Range Sharding)
根据数据的某个连续范围字段进行分片,常用于时间、数值型字段。
实现方式:
sql
-- 按用户ID范围分片
-- user_0: 1-1000000
-- user_1: 1000001-2000000
-- user_2: 2000001-3000000
-- 按时间范围分片
CREATE TABLE order_202401 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
create_time DATETIME,
-- 其他字段
KEY idx_create_time (create_time)
) PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
Java路由实现:
java
public class RangeShardingStrategy {
private final List<RangeConfig> rangeConfigs;
public RangeShardingStrategy() {
this.rangeConfigs = Arrays.asList(
new RangeConfig(1L, 1000000L, "user_0"),
new RangeConfig(1000001L, 2000000L, "user_1"),
new RangeConfig(2000001L, 3000000L, "user_2")
);
}
public String getTableName(Long userId) {
for (RangeConfig config : rangeConfigs) {
if (userId >= config.getMin() && userId <= config.getMax()) {
return config.getTableName();
}
}
throw new IllegalArgumentException("User ID out of range");
}
static class RangeConfig {
private Long min;
private Long max;
private String tableName;
// constructor and getters
}
}
优点:
- 查询范围数据效率高
- 扩容相对简单,只需新增范围
- 数据分布可预测
缺点:
- 可能存在数据分布不均
- 范围边界可能出现热点
- 不适合随机访问场景
适用场景:
- 数据有明显的时间或数值分布特征
- 经常需要进行范围查询
- 数据量相对均匀分布
2. 哈希分片(Hash Sharding)
通过对分片键进行哈希计算,将数据均匀分散到各个分片。
实现方式:
java
public class HashShardingStrategy {
private final int shardCount;
public HashShardingStrategy(int shardCount) {
this.shardCount = shardCount;
}
// 简单哈希分片
public int getShard(Long shardKey) {
return (int) (shardKey % shardCount);
}
// 更均匀的哈希分片
public int getConsistentHashShard(Long shardKey) {
int hash = hashFunction(shardKey.toString());
return Math.abs(hash) % shardCount;
}
private int hashFunction(String key) {
// FNV哈希算法
final int FNV_32_PRIME = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < key.length(); i++) {
hash = (hash ^ key.charAt(i)) * FNV_32_PRIME;
}
return hash;
}
// 获取完整表名
public String getTableName(Long userId) {
int shard = getConsistentHashShard(userId);
return "user_" + shard;
}
}
分片键选择原则:
- 选择查询频率高的字段作为分片键
- 分片键的值应该具有高基数(重复率低)
- 避免选择变化频繁的字段
优点:
- 数据分布均匀
- 查询性能稳定
- 适合高并发写入场景
缺点:
- 范围查询性能较差
- 扩容时需要重新哈希,数据迁移复杂
- 分片键选择不当可能导致热点问题
适用场景:
- 数据分布均匀,无明显的访问模式
- 高并发写入场景
- 需要负载均衡的读多写少场景
3. 一致性哈希(Consistent Hashing)
解决传统哈希分片在扩容时需要大量数据迁移的问题。
实现方式:
java
public class ConsistentHashing {
private final TreeMap<Long, String> virtualNodes = new TreeMap<>();
private final int virtualNodeCount = 150; // 每个物理节点对应的虚拟节点数
public void addNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
String virtualNode = node + "#" + i;
int hash = getHash(virtualNode);
virtualNodes.put((long) hash, node);
}
}
public void removeNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
String virtualNode = node + "#" + i;
int hash = getHash(virtualNode);
virtualNodes.remove((long) hash);
}
}
public String getNode(String key) {
if (virtualNodes.isEmpty()) {
return null;
}
int hash = getHash(key);
// 顺时针查找第一个大于等于hash的虚拟节点
Map.Entry<Long, String> entry = virtualNodes.ceilingEntry((long) hash);
if (entry == null) {
// 如果没找到,返回第一个节点(环形结构)
entry = virtualNodes.firstEntry();
}
return entry.getValue();
}
private int getHash(String key) {
// 使用MurmurHash算法
return MurmurHash.hash32(key);
}
}
优点:
- 扩容时只影响部分数据迁移
- 节点故障时自动路由到其他节点
- 具有较好的负载均衡性
缺点:
- 实现复杂度较高
- 虚拟节点数量选择影响性能
- 需要维护环状结构
适用场景:
- 需要频繁扩缩容的系统
- 分布式缓存系统
- 对数据迁移敏感的业务
4. 按业务模块分片
根据业务逻辑将相关的数据表分配到同一个数据库中。
架构示例:
数据库集群
├── 用户库 (db_user)
│ ├── user_basic # 用户基础信息
│ ├── user_profile # 用户扩展信息
│ ├── user_address # 用户地址信息
│ └── user_social # 用户社交信息
│
├── 订单库 (db_order)
│ ├── order_main # 订单主表
│ ├── order_detail # 订单详情
│ ├── order_payment # 支付信息
│ └── order_log # 订单日志
│
├── 商品库 (db_product)
│ ├── product_info # 商品信息
│ ├── product_category # 商品分类
│ ├── product_inventory # 库存信息
│ └── product_sku # SKU信息
│
└── 支付库 (db_payment)
├── payment_record # 支付记录
├── payment_refund # 退款记录
└── payment_channel # 支付渠道
配置示例:
yaml
# ShardingSphere 配置
sharding:
tables:
user_basic:
actualDataNodes: db_user.user_basic
user_profile:
actualDataNodes: db_user.user_profile
order_main:
actualDataNodes: db_order.order_main
order_detail:
actualDataNodes: db_order.order_detail
product_info:
actualDataNodes: db_product.product_info
payment_record:
actualDataNodes: db_payment.payment_record
优点:
- 业务边界清晰,便于维护
- 跨库关联查询需求减少
- 不同业务可以独立扩容
缺点:
- 可能造成负载不均
- 某些业务模块可能成为瓶颈
- 需要准确的业务边界识别
适用场景:
- 微服务架构
- 业务模块耦合度低
- 不同业务访问模式差异大
3.2 路由规则设计与实现
路由规则设计原则
- 单一分片键原则:尽量避免复杂的多键分片
- 可预测性原则:相同的分片键必须路由到相同的目标
- 扩展性原则:路由规则应支持动态调整
- 性能优先原则:路由计算必须高效
ShardingSphere路由配置
yaml
# ShardingSphere 分片配置示例
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
ds0:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/db0
username: root
password: password
ds1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/db1
username: root
password: password
ds2:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/db2
username: root
password: password
ds3:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/db3
username: root
password: password
sharding:
tables:
t_order:
actualDataNodes: ds$->{0..3}.t_order_$->{0..3}
databaseStrategy:
standard:
shardingColumn: user_id
preciseAlgorithmClassName: com.example.DbShardingAlgorithm
tableStrategy:
standard:
shardingColumn: order_id
preciseAlgorithmClassName: com.example.TableShardingAlgorithm
keyGenerator:
type: SNOWFLAKE
column: order_id
自定义分片算法实现
java
public class DbShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long userId = shardingValue.getValue();
// 数据库分片:userId % 4
int dbIndex = (int) (userId % 4);
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(dbIndex))) {
return targetName;
}
}
throw new IllegalArgumentException("No database found for userId: " + userId);
}
}
public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long orderId = shardingValue.getValue();
// 表分片:orderId % 4
int tableIndex = (int) (orderId % 4);
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(tableIndex))) {
return targetName;
}
}
throw new IllegalArgumentException("No table found for orderId: " + orderId);
}
}
复合分片策略
java
public class ComplexShardingStrategy {
/**
* 复合分片:先按业务模块分库,再按哈希分表
*/
public String getShardingResult(String businessModule, Long shardKey) {
// 第一步:按业务模块确定数据库
String database = getDatabaseByModule(businessModule);
// 第二步:按哈希确定表
String table = getTableByHash(shardKey);
return database + "." + table;
}
private String getDatabaseByModule(String businessModule) {
Map<String, String> moduleToDb = Map.of(
"user", "db_user",
"order", "db_order",
"product", "db_product"
);
return moduleToDb.getOrDefault(businessModule, "db_default");
}
private String getTableByHash(Long shardKey) {
int tableIndex = Math.abs(shardKey.hashCode()) % 8;
return "table_" + tableIndex;
}
}
四、最佳实践建议
4.1 分库分表后的事务处理策略
1. 本地事务 + 消息队列(最终一致性)
java
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageProducer messageProducer;
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 订单创建流程:本地事务 + 异步消息
*/
@Override
public void createOrder(OrderDTO orderDTO) {
transactionTemplate.execute(status -> {
// 1. 本地事务:创建订单
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
orderMapper.insert(order);
// 2. 发送订单创建消息
OrderEvent event = new OrderEvent();
event.setOrderId(order.getId());
event.setUserId(order.getUserId());
event.setAmount(order.getAmount());
event.setType("ORDER_CREATED");
messageProducer.send(event);
return order.getId();
});
}
}
// 库存服务消费消息
@KafkaListener(topics = "order-topic")
public void handleOrderEvent(OrderEvent event) {
if ("ORDER_CREATED".equals(event.getType())) {
// 扣减库存
inventoryService.deductInventory(event);
}
}
2. TCC事务(Try-Confirm-Cancel)
java
@Service
public class TccOrderService {
/**
* Try阶段:预留资源
*/
@Transactional
public void tryCreateOrder(Order order) {
// 1. 冻结订单金额
accountService.freezeAmount(order.getUserId(), order.getAmount());
// 2. 预扣库存
productService.reserveInventory(order.getProductId(), order.getQuantity());
// 3. 记录TCC事务日志
tccTransactionLog.recordTry(order.getId());
}
/**
* Confirm阶段:确认执行
*/
@Transactional
public void confirmCreateOrder(Long orderId) {
// 1. 扣除冻结金额
accountService.deductFrozenAmount(orderId);
// 2. 扣减实际库存
productService.deductReservedInventory(orderId);
// 3. 更新TCC事务状态
tccTransactionLog.updateStatus(orderId, "CONFIRMED");
}
/**
* Cancel阶段:取消执行
*/
@Transactional
public void cancelCreateOrder(Long orderId) {
// 1. 释放冻结金额
accountService.releaseFrozenAmount(orderId);
// 2. 释放预留库存
productService.releaseReservedInventory(orderId);
// 3. 更新TCC事务状态
tccTransactionLog.updateStatus(orderId, "CANCELLED");
}
}
3. Saga事务(补偿机制)
java
@Service
public class SagaOrderService {
private List<SagaStep> sagaSteps = new ArrayList<>();
public void executeSaga(Order order) {
List<SagaStep> executedSteps = new ArrayList<>();
try {
// 按顺序执行各个步骤
for (SagaStep step : sagaSteps) {
step.execute(order);
executedSteps.add(step);
}
} catch (Exception e) {
// 发生异常,按相反顺序执行补偿
Collections.reverse(executedSteps);
for (SagaStep step : executedSteps) {
try {
step.compensate(order);
} catch (Exception compensateException) {
log.error("补偿失败", compensateException);
}
}
throw e;
}
}
}
interface SagaStep {
void execute(Order order);
void compensate(Order order);
}
4.2 分布式ID生成方案对比
常用方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 生成简单,无单点问题 | 占用空间大,无序 | 小规模系统,非数值型ID |
| 数据库自增 | 递增有序,实现简单 | 单点问题,性能瓶颈 | 单库场景,小规模系统 |
| Redis生成 | 性能较好,支持递增 | 依赖Redis,集群复杂 | 中小规模系统,需要有序ID |
| Snowflake | 高性能,分布式,有序 | 依赖机器时钟,ID较长 | 大规模分布式系统 |
| 号段模式 | 性能极佳,降低DB压力 | ID不连续,浪费空间 | 高并发写入场景 |
4.3 跨库联表查询解决方案
1. 数据冗余(字段冗余)
sql
-- 原始设计
-- 用户表:user
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100)
);
-- 订单表:order
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
amount DECIMAL(10,2)
);
-- 冗余优化:在订单表中冗余用户名
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
user_id BIGINT,
username VARCHAR(50), -- 冗余字段
order_no VARCHAR(50),
amount DECIMAL(10,2)
);
优点:
- 查询性能提升,避免JOIN
- 实现简单,代码改动小
缺点:
- 数据冗余,占用额外空间
- 需要保证数据一致性
一致性维护:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageProducer messageProducer;
/**
* 更新用户信息时,同步更新订单表中的冗余字段
*/
@Transactional
public void updateUsername(Long userId, String newUsername) {
// 1. 更新用户表
userMapper.updateUsername(userId, newUsername);
// 2. 发送消息通知更新相关表
UserUpdateEvent event = new UserUpdateEvent();
event.setUserId(userId);
event.setNewUsername(newUsername);
messageProducer.send(event);
}
}
// 订单服务消费消息
@KafkaListener(topics = "user-update-topic")
public void handleUserUpdate(UserUpdateEvent event) {
orderMapper.updateUsernameByUserId(event.getUserId(), event.getNewUsername());
}
2. 应用层JOIN
java
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private ProductMapper productMapper;
/**
* 应用层JOIN:先查订单,再批量关联查询
*/
public OrderDetailVO getOrderDetail(Long orderId) {
// 1. 查询订单基本信息
Order order = orderMapper.selectById(orderId);
if (order == null) {
return null;
}
// 2. 根据userId查询用户信息
User user = userMapper.selectById(order.getUserId());
// 3. 根据productId查询商品信息
Product product = productMapper.selectById(order.getProductId());
// 4. 组装VO对象
OrderDetailVO vo = new OrderDetailVO();
vo.setOrderId(order.getId());
vo.setOrderNo(order.getOrderNo());
vo.setAmount(order.getAmount());
if (user != null) {
vo.setUsername(user.getUsername());
vo.setUserPhone(user.getPhone());
}
if (product != null) {
vo.setProductName(product.getName());
vo.setProductPrice(product.getPrice());
}
return vo;
}
/**
* 批量查询优化:减少数据库查询次数
*/
public List<OrderDetailVO> batchGetOrderDetails(List<Long> orderIds) {
if (orderIds.isEmpty()) {
return Collections.emptyList();
}
// 1. 批量查询订单
List<Order> orders = orderMapper.batchSelectByIds(orderIds);
// 2. 提取所有userId和productId
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
Set<Long> productIds = orders.stream()
.map(Order::getProductId)
.collect(Collectors.toSet());
// 3. 批量查询用户和商品
Map<Long, User> userMap = userMapper.batchSelectByIds(userIds).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
Map<Long, Product> productMap = productMapper.batchSelectByIds(productIds).stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
// 4. 组装结果
return orders.stream().map(order -> {
OrderDetailVO vo = new OrderDetailVO();
vo.setOrderId(order.getId());
vo.setOrderNo(order.getOrderNo());
vo.setAmount(order.getAmount());
User user = userMap.get(order.getUserId());
if (user != null) {
vo.setUsername(user.getUsername());
}
Product product = productMap.get(order.getProductId());
if (product != null) {
vo.setProductName(product.getName());
}
return vo;
}).collect(Collectors.toList());
}
}
3. 数据聚合中间件
java
@Service
public class DataAggregationService {
@Autowired
private ShardingDataSource shardingDataSource;
/**
* 并行查询多个分片,聚合结果
*/
public <T> List<T> aggregateQuery(String sql, ResultSetExtractor<T> extractor) {
List<DataSource> dataSources = getAllDataSources();
// 并行查询
List<CompletableFuture<List<T>>> futures = dataSources.stream()
.map(dataSource -> CompletableFuture.supplyAsync(() -> {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
return jdbcTemplate.query(sql, extractor);
}, asyncExecutor))
.collect(Collectors.toList());
// 等待所有查询完成并合并结果
return futures.stream()
.flatMap(future -> future.join().stream())
.collect(Collectors.toList());
}
/**
* 跨库分页查询
*/
public PageResult<Order> crossDbPageQuery(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
// 1. 从每个分片查询指定数量的数据
List<List<Order>> shardResults = new ArrayList<>();
for (DataSource dataSource : getAllDataSources()) {
String sql = "SELECT * FROM `order` ORDER BY id DESC LIMIT ? OFFSET ?";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List<Order> orders = jdbcTemplate.query(sql,
new Object[]{pageSize * 2, offset}, orderRowMapper);
shardResults.add(orders);
}
// 2. 合并所有结果
List<Order> allOrders = shardResults.stream()
.flatMap(List::stream)
.sorted(Comparator.comparing(Order::getId).reversed())
.collect(Collectors.toList());
// 3. 分页截取
int total = allOrders.size();
List<Order> pageData = allOrders.stream()
.skip(offset)
.limit(pageSize)
.collect(Collectors.toList());
return new PageResult<>(pageData, total, pageNum, pageSize);
}
}
4.4 扩容与数据迁移策略
1. 在线数据迁移方案
java
@Service
public class DataMigrationService {
@Autowired
private ShardingDataSource sourceDataSource;
@Autowired
private ShardingDataSource targetDataSource;
/**
* 在线数据迁移主流程
*/
public void migrateData(String tableName, int sourceShardCount, int targetShardCount) {
// 1. 数据同步:开启双写
enableDualWrite(tableName, sourceShardCount, targetShardCount);
// 2. 历史数据迁移
migrateHistoryData(tableName, sourceShardCount, targetShardCount);
// 3. 数据校验
validateDataConsistency(tableName, sourceShardCount, targetShardCount);
// 4. 切换流量
switchTraffic(tableName, sourceShardCount, targetShardCount);
// 5. 清理旧数据
cleanupOldData(tableName, sourceShardCount);
}
/**
* 开启双写模式
*/
private void enableDualWrite(String tableName, int sourceShardCount, int targetShardCount) {
log.info("开启双写模式: {}", tableName);
// 在应用层配置中添加目标分片,同时写入新旧分片
shardingConfig.addShard(tableName, targetShardCount);
shardingConfig.enableDualWriteMode(tableName, true);
}
/**
* 历史数据迁移
*/
private void migrateHistoryData(String tableName, int sourceShardCount, int targetShardCount) {
log.info("开始迁移历史数据: {}", tableName);
int batchSize = 1000;
for (int i = 0; i < sourceShardCount; i++) {
long lastId = 0;
while (true) {
String sql = String.format(
"SELECT * FROM %s_%d WHERE id > ? ORDER BY id LIMIT %d",
tableName, i, batchSize
);
List<Map<String, Object>> records = querySourceData(sql, lastId);
if (records.isEmpty()) {
break;
}
// 批量写入目标分片
batchInsertTargetData(records, tableName, targetShardCount);
lastId = (Long) records.get(records.size() - 1).get("id");
log.info("迁移进度: shard={}, lastId={}", i, lastId);
}
}
}
/**
* 数据一致性校验
*/
private void validateDataConsistency(String tableName, int sourceShardCount, int targetShardCount) {
log.info("开始数据校验: {}", tableName);
for (int i = 0; i < sourceShardCount; i++) {
// 比较源分片和目标分片的数据量
long sourceCount = getRecordCount(tableName + "_" + i);
long targetCount = getTargetRecordCount(tableName, i, targetShardCount);
if (sourceCount != targetCount) {
throw new RuntimeException(String.format(
"数据校验失败: shard=%d, source=%d, target=%d",
i, sourceCount, targetCount
));
}
}
log.info("数据校验通过: {}", tableName);
}
/**
* 流量切换
*/
private void switchTraffic(String tableName, int sourceShardCount, int targetShardCount) {
log.info("开始切换流量: {}", tableName);
// 1. 灰度切换:先切换10%的流量
shardingConfig.updateShardWeight(tableName, sourceShardCount, 0.9);
shardingConfig.updateShardWeight(tableName, targetShardCount, 0.1);
// 2. 观察一段时间,确认无异常
monitorMetrics(tableName);
// 3. 逐步增加目标分片的权重
for (int weight = 20; weight <= 100; weight += 20) {
shardingConfig.updateShardWeight(tableName, targetShardCount, weight / 100.0);
shardingConfig.updateShardWeight(tableName, sourceShardCount, (100 - weight) / 100.0);
monitorMetrics(tableName);
}
// 4. 完全切换到新分片
shardingConfig.removeShard(tableName, sourceShardCount);
log.info("流量切换完成: {}", tableName);
}
}
2. 双写方案实现
java
@Component
public class DualWriteInterceptor implements Interceptor {
@Autowired
private ShardingConfig shardingConfig;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 判断是否需要双写
if (needDualWrite(ms)) {
return dualWrite(invocation, ms, parameter);
}
return invocation.proceed();
}
/**
* 双写实现
*/
private Object dualWrite(Invocation invocation, MappedStatement ms, Object parameter) {
String tableName = extractTableName(ms.getId());
try {
// 1. 执行原写入操作
Object result = invocation.proceed();
// 2. 异步写入目标分片
asyncExecutor.execute(() -> {
try {
writeToTargetShard(ms, parameter, tableName);
} catch (Exception e) {
log.error("双写失败,记录到重试队列", e);
retryQueue.offer(new RetryTask(ms, parameter, tableName));
}
});
return result;
} catch (Exception e) {
log.error("双写主操作失败", e);
throw e;
}
}
/**
* 写入目标分片
*/
private void writeToTargetShard(MappedStatement ms, Object parameter, String tableName) {
List<String> targetShards = shardingConfig.getTargetShards(tableName);
for (String shard : targetShards) {
try {
// 获取目标分片的数据源
DataSource targetDataSource = shardingConfig.getDataSource(shard);
// 执行写入
executeWrite(ms, parameter, targetDataSource);
} catch (Exception e) {
log.error("写入目标分片失败: {}", shard, e);
throw e;
}
}
}
}
3. 扩容决策矩阵
┌─────────────────────────────────────────────────────────────┐
│ 分库分表扩容决策矩阵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 当前容量 │ 数据增长 │ 业务重要性 │ 扩容方案 │
│────────────────────────────────────────────────────────────│
│ <70% │ 低 │ 低 │ 监控观察,暂不扩容 │
│ <70% │ 高 │ 低 │ 规划扩容,3-6个月实施 │
│ <70% │ 高 │ 高 │ 提前扩容,1-3个月实施 │
│ 70-90% │ 低 │ 低 │ 制定扩容计划,准备实施 │
│ 70-90% │ 低 │ 高 │ 立即启动扩容 │
│ 70-90% │ 高 │ 高 │ 紧急扩容,7×24小时监控 │
│ >90% │ - │ - │ 立即执行紧急扩容 │
│ │
├─────────────────────────────────────────────────────────────┤
│ 扩容方案选择: │
│ │
│ 1. 水平扩容(推荐优先): │
│ - 增加分片数量 │
│ - 适用:数据量大,访问模式均匀 │
│ │
│ 2. 垂直扩容: │
│ - 升级硬件配置 │
│ - 适用:暂时性瓶颈,成本可接受 │
│ │
│ 3. 架构优化: │
│ - 引入缓存层 │
│ - 读写分离 │
│ - 异步处理 │
│ │
└─────────────────────────────────────────────────────────────┘
五、分库分表方案选择决策流程
┌──────────────────────────────────────────────────────────────┐
│ 分库分表决策流程图 │
└──────────────────────────────────────────────────────────────┘
┌───────────────────┐
│ 开始评估 │
└────────┬──────────┘
│
▼
┌───────────────────┐ 是 ┌───────────────────┐
│ 是否存在性能瓶颈? │──────────►│ 性能瓶颈类型 │
└────────┬──────────┘ │ - 查询慢 │
│ │ - 连接数满 │
│ 否 │ - IO压力大 │
│ │ - 存储空间不足 │
▼ └────────┬──────────┘
┌───────────────────┐ │
│ 监控观察,暂不实施 │ ▼
└───────────────────┘ ┌───────────────────┐
│ 是否可垂直拆分? │
└────────┬──────────┘
│
┌──────────────────┴──────────────────┐
│ 是 │ 否
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ 垂直拆分 │ │ 数据量评估 │
│ - 按业务模块拆分 │ │ - 单表数据量 │
│ - 按冷热数据拆分 │ │ - 单库数据量 │
└────────┬──────────┘ └────────┬──────────┘
│ │
▼ │ 否
┌───────────────────┐ │
│ 是否仍需分片? │ ▼
└────────┬──────────┘ ┌───────────────────┐
│ │ 监控观察,暂不实施 │
│ 是 └───────────────────┘
▼
┌───────────────────┐
│ 选择水平拆分 │
└────────┬──────────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ 访问模式分析 │ │ 数据特征分析 │
│ - 读多写少 │ │ - 均匀分布 │
│ - 写多读少 │ │ - 热点数据 │
│ - 读写均衡 │ │ - 时间有序 │
└────────┬──────────┘ └────────┬──────────┘
│ │
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ 选择分片策略 │ │ 选择分片策略 │
│ - 范围分片 │ │ - 哈希分片 │
│ - 一致性哈希 │ │ - 复合分片 │
└────────┬──────────┘ └────────┬──────────┘
│ │
└───────────┬───────────┘
│
▼
┌───────────────────┐
│ 确定分片数量 │
│ - 初始分片数 │
│ - 预留扩容空间 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 技术方案设计 │
│ - 中间件选型 │
│ - 路由规则设计 │
│ - 分布式事务方案 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 实施计划制定 │
│ - 迁移策略 │
│ - 验证方案 │
│ - 回滚方案 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 执行实施 │
│ - 测试环境验证 │
│ - 灰度发布 │
│ - 全量切换 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 监控优化 │
│ - 性能监控 │
│ - 问题定位 │
│ - 持续优化 │
└────────┬──────────┘
│
▼
┌───────────────────┐
│ 完成 │
└───────────────────┘
六、总结与建议
6.1 核心要点回顾
-
分库分表是解决数据库扩展性的关键方案,但不是万能药,需要根据实际业务场景选择。
-
垂直拆分适用于业务边界清晰的场景,水平拆分适用于数据量大的场景,两者可以结合使用。
-
分片策略的选择至关重要,需要综合考虑访问模式、数据特征、扩展需求等因素。
-
分布式事务和跨库查询是主要挑战,需要通过最终一致性、数据冗余、应用层处理等方案解决。
-
在线扩容需要谨慎规划,建议采用双写+渐进式迁移的方案,确保业务不受影响。
6.2 实施建议
-
不要过早优化:只有在真正遇到性能瓶颈时才考虑分库分表,避免过度设计。
-
渐进式实施:可以先进行垂直拆分,再考虑水平拆分,逐步演进。
-
充分的测试验证:在测试环境中充分验证分库分表方案,确保功能正确性和性能达标。
-
完善的监控体系:建立完善的监控体系,及时发现问题并优化。
-
团队技能提升:确保团队具备分库分表相关的技术能力和运维经验。
6.3 常见误区规避
-
避免过度分片:分片数量不宜过多,增加管理复杂度。
-
不要忽视成本:分库分表会增加硬件成本、开发成本、运维成本。
-
避免频繁变更分片策略:一旦确定分片策略,尽量避免频繁变更。
-
不要忽视数据一致性:分布式环境下数据一致性更加复杂,需要重点关注。
-
避免盲目跟风:不是所有系统都需要分库分表,根据实际需求选择。