分布式 MySQL
一、分库分表
1.1 什么情况下需要分库分表
| 场景 | 表现 | 解决方案 |
|---|---|---|
| 单表数据量过大 | 查询慢、DDL 锁表时间长 | 分表 |
| 单库并发过高 | 连接数不足、CPU 100% | 分库 |
| 读写 QPS 超出上限 | 单机资源瓶颈 | 分库 + 读写分离 |
| 数据增长太快 | 备份恢复时间过长 | 分库分表 |
1.2 垂直分库
将不同的业务表拆分到不同的数据库实例。
垂直分库前:
一个库:user + order + product + payment
垂直分库后:
user_db:用户表
order_db:订单表
product_db:商品表
payment_db:支付表
特点:
- 按业务领域拆分,天然解耦
- 每个库可以使用不同的硬件配置
- 跨库查询需要应用层处理
1.3 水平分库
将同一张表的数据分布到多个数据库实例。
水平分库(4 个库,按 user_id 取模):
user_db_0:user_id % 4 = 0
user_db_1:user_id % 4 = 1
user_db_2:user_id % 4 = 2
user_db_3:user_id % 4 = 3
分片键选择:
| 分片键 | 优点 | 缺点 |
|---|---|---|
| user_id | 用户维度查询高效,数据分布均匀 | 跨用户查询困难 |
| order_id | 全局唯一,分布均匀 | 按用户查询需路由 |
| time | 范围查询友好 | 热点问题(最近数据集中) |
| hash | 分布均匀 | 范围查询不支持 |
1.4 分库分表策略
sql
-- 策略1:根据 ID 范围分片
-- user_id 1~1000万 → db_0
-- user_id 1001万~2000万 → db_1
-- 策略2:根据哈希取模分片
-- user_id % 4 → db_0 ~ db_3
-- 策略3:根据时间分片
-- 2024年订单 → orders_2024
-- 2025年订单 → orders_2025
-- 策略4:一致性哈希
-- 减少扩容时的数据迁移量
二、中间件使用
2.1 ShardingSphere
ShardingSphere 是 Apache 基金会下的分布式数据库中间件生态。
ShardingSphere-JDBC(客户端模式):
yaml
# 分片规则配置(application.yaml)
spring:
shardingsphere:
datasource:
names: ds0, ds1, ds2, ds3
ds0:
url: jdbc:mysql://192.168.1.100:3306/db_0
ds1:
url: jdbc:mysql://192.168.1.100:3306/db_1
ds2:
url: jdbc:mysql://192.168.1.101:3306/db_2
ds3:
url: jdbc:mysql://192.168.1.101:3306/db_3
sharding:
tables:
t_order:
actual_data_nodes: ds$->{0..3}.t_order_$->{0..15}
table_strategy:
inline:
sharding_column: order_id
algorithm_expression: t_order_$->{order_id % 16}
database_strategy:
inline:
sharding_column: user_id
algorithm_expression: ds$->{user_id % 4}
ShardingSphere-Proxy(服务端模式):
yaml
# server.yaml
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_${0..15}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table_inline
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 4}
table_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 16}
2.2 MyCat
MyCat 是一个基于服务的分布式数据库中间件。
xml
<!-- schema.xml -->
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100">
<table name="t_order" dataNode="dn1,dn2,dn3" rule="order_rule"/>
</schema>
<dataNode name="dn1" dataHost="host1" database="db_0" />
<dataNode name="dn2" dataHost="host2" database="db_1" />
<dataNode name="dn3" dataHost="host3" database="db_2" />
<dataHost name="host1" maxCon="100" minCon="10" balance="1"
writeType="0" dbType="mysql" dbDriver="native">
<writeHost host="hostM1" url="192.168.1.100:3306" user="root" password="123">
<readHost host="hostS1" url="192.168.1.101:3306" user="root" password="123" />
</writeHost>
</dataHost>
<!-- rule.xml -->
<tableRule name="order_rule">
<rule>
<columns>user_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<property name="count">3</property>
</function>
2.3 中间件对比
| 特性 | ShardingSphere-JDBC | ShardingSphere-Proxy | MyCat |
|---|---|---|---|
| 部署方式 | 嵌入应用 | 独立服务 | 独立服务 |
| 性能 | 高(无网络跳转) | 中(有网络开销) | 中 |
| 协议支持 | JDBC | MySQL 协议 | MySQL 协议 |
| 分布式事务 | XA、SEATA | XA、SEATA | 弱 XA |
| 学习成本 | 低 | 中 | 中 |
| 适用场景 | Java 应用 | 多语言、非 Java | 传统 MySQL 代理 |
三、分片规则与路由
3.1 常用分片算法
java
// 1. 取模分片
public class ModShardingAlgorithm {
public String doSharding(Collection<String> availableTargetNames, Long shardingValue) {
int size = availableTargetNames.size();
int index = (int)(shardingValue % size);
// 路由到对应的目标
return availableTargetNames.toArray()[index].toString();
}
}
// 2. 范围分片
public class RangeShardingAlgorithm {
public String doSharding(Long shardingValue) {
if (shardingValue <= 10000000) return "ds_0";
if (shardingValue <= 20000000) return "ds_1";
return "ds_2";
}
}
// 3. 时间分片
public class TimeShardingAlgorithm {
public String doSharding(Date shardingValue) {
Calendar cal = Calendar.getInstance();
cal.setTime(shardingValue);
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1;
return String.format("ds_%d%02d", year, month);
}
}
// 4. 一致性哈希
// 使用 TreeMap 实现环形哈希,减少扩容数据迁移
3.2 路由策略
java
// 标准路由(基于分片键)
public class StandardRouter {
public DataNode route(String sql, ShardingValue shardingValue) {
// SQL: SELECT * FROM t_order WHERE order_id = ?
// 解析出 order_id → 计算分片 → 路由到具体节点
int dbIndex = shardingValue % 4; // 分库
int tableIndex = shardingValue % 16; // 分表
return new DataNode("ds_" + dbIndex, "t_order_" + tableIndex);
}
}
// 广播路由(没有分片键的查询)
public class BroadcastRouter {
public List<String> route() {
// SQL: SELECT COUNT(*) FROM t_order
// 没有分片键需要发送到所有节点
return Arrays.asList("ds_0", "ds_1", "ds_2", "ds_3");
}
}
// 强制路由(Hint)
public class HintRouter {
public DataNode route(int userId) {
// 通过 Hint 指定分片键
return new DataNode("ds_" + (userId % 4), "t_order");
}
}
3.3 跨库跨表查询
sql
-- 场景1:查询用户的所有订单(分片键已知)
-- 根据 user_id 直接路由到具体分片
SELECT * FROM t_order WHERE user_id = 100;
-- 场景2:按订单号查询(分片键未知)
-- 方案A:使用映射表
SELECT user_id FROM order_user_mapping WHERE order_no = '20240101001';
-- 得到 user_id 后再路由
-- 方案B:广播查询(发到所有分片,合并结果)
SELECT * FROM t_order_0 WHERE order_no = '20240101001'
UNION ALL
SELECT * FROM t_order_1 WHERE order_no = '20240101001'
...
-- 场景3:聚合查询
-- 需要从所有分片查询后在中间件/应用层合并
-- SELECT COUNT(*) FROM t_order → 各分片分别COUNT → 汇总求和
-- SELECT * FROM t_order ORDER BY create_time LIMIT 10 → 各分片LIMIT → 合并排序
四、分布式事务
4.1 CAP 与 BASE 理论
CAP 理论:
C(一致性):所有节点数据一致
A(可用性):服务总是可用
P(分区容错性):节点间通信失败仍能工作
三者只能同时满足其二
BASE 理论(最终一致性):
BA(基本可用):系统基本可用
S(软状态):状态可以不一致
E(最终一致性):最终数据会一致
在分布式数据库场景:
通常选择 AP(可用性 + 分区容错性)
通过最终一致性保证数据一致
4.2 XA 事务(强一致性)
sql
-- XA 两阶段提交
-- 阶段1:准备
XA START 'xid1';
UPDATE account SET balance = balance - 100 WHERE id = 1;
XA END 'xid1';
XA PREPARE 'xid1';
XA START 'xid2';
UPDATE account SET balance = balance + 100 WHERE id = 2;
XA END 'xid2';
XA PREPARE 'xid2';
-- 阶段2:提交
XA COMMIT 'xid1';
XA COMMIT 'xid2';
-- 如果某个节点 PREPARE 失败,全部回滚
XA ROLLBACK 'xid1';
XA ROLLBACK 'xid2';
4.3 柔性事务(Seata AT)
java
// Seata AT 模式(自动补偿)
@GlobalTransactional
public void createOrder(OrderDTO order) {
// 1. 扣减库存(DB1)
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 2. 创建订单(DB2)
orderService.create(order);
// 3. 扣减余额(DB3)
accountService.debit(order.getUserId(), order.getAmount());
// 所有操作成功 → 全局提交
// 任何操作失败 → 全局回滚(自动执行补偿 SQL)
}
4.4 事务方案对比
| 方案 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| XA | 强一致性 | 低 | 中 | 重要交易(跨数据库) |
| Seata AT | 最终一致性 | 中 | 低 | 微服务分布式事务 |
| TCC | 最终一致性 | 中 | 高 | 需要灵活控制 |
| 本地消息表 | 最终一致性 | 高 | 中 | 异步消息场景 |
| 可靠消息(RocketMQ) | 最终一致性 | 高 | 中 | 高并发最终一致 |
五、实战案例
案例:订单系统分库分表设计
sql
-- 需求:订单系统,预计日订单量 100 万,年订单量 3.6 亿
-- 分片设计:
-- 分库数量:8(ds_0 ~ ds_7)
-- 分表数量:每个库 16 张(t_order_0 ~ t_order_15)
-- 总分片数:8 × 16 = 128
-- 每个分片承载:3.6亿 / 128 ≈ 280 万行
-- 分片键:user_id(按用户查询是主要场景)
-- 路由规则:
-- 库序号:user_id % 8
-- 表序号:user_id / 8 % 16
-- Java 实现路由
public class OrderRouter {
private static final int DB_COUNT = 8;
private static final int TABLE_COUNT = 16;
public String getTableName(Long userId) {
int dbIndex = (int)(userId % DB_COUNT);
int tableIndex = (int)((userId / DB_COUNT) % TABLE_COUNT);
return String.format("ds_%d.t_order_%d", dbIndex, tableIndex);
}
}
-- 创建分表的模板(脚本生成 128 张表)
-- 每张表结构相同
CREATE TABLE `t_order_0` (
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
`status` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 映射表(解决按订单号查询)
CREATE TABLE `order_user_mapping` (
`order_no` VARCHAR(32) PRIMARY KEY,
`user_id` BIGINT NOT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
总结
| 知识点 | 核心要点 |
|---|---|
| 垂直分库 | 按业务拆分到独立实例 |
| 水平分库 | 按分片键拆到多个实例 |
| 分片策略 | 取模、范围、时间、一致性哈希 |
| 中间件 | ShardingSphere-JDBC(高性能)、ShardingSphere-Proxy(多语言)、MyCat |
| 跨库查询 | 映射表、广播查询、中间件聚合 |
| 分布式事务 | XA 强一致、Seata 最终一致、TCC、消息表 |
| 核心挑战 | 分片键选择、跨分片查询、数据迁移、分布式事务 |