Mysql:分库分表后怎么解决分布式访问的问题
·
分表后的分布式访问是核心挑战。核心思路是:将对业务透明的数据路由和聚合逻辑,下沉到专门的数据访问层。有三大主流方案,本文将通过一个具体的电商订单分表例子来详解。
假设我们有一个订单表,按订单ID哈希分成了4张表:
-
orders_0(order_id % 4 = 0) -
orders_1(order_id % 4 = 1) -
orders_2(order_id % 4 = 2) -
orders_3(order_id % 4 = 3)
方案一:代理中间件模式(推荐,对应用透明)
架构图
应用程序 → 代理中间件 → 分表0/1/2/3
(无感知) (路由+聚合)
1. ShardingSphere-Proxy(推荐)
像一个“数据库翻译官”,应用把它当成一个MySQL用。
-- 应用看到的(逻辑表)
CREATE TABLE orders (
order_id BIGINT,
user_id INT,
amount DECIMAL
);
-- ShardingSphere自动路由到物理表
-- 应用查询(完全无感知)
SELECT * FROM orders WHERE order_id = 123;
-- Proxy自动转换为:
SELECT * FROM orders_3 WHERE order_id = 123; -- 123 % 4 = 3
-- 跨分片查询也自动处理
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;
-- Proxy查询所有分表,在内存中聚合结果
部署示例:
# proxy的配置片段
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_${0..3}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_hash
shardingAlgorithms:
order_hash:
type: HASH_MOD
props:
sharding-count: 4
优点:应用零改造,SQL兼容性好。
缺点:代理层可能成为性能瓶颈,需要高可用部署。
2. MyCat / ProxySQL
老牌中间件,配置类似:
<!-- MyCat的schema配置 -->
<schema name="shop" checkSQLschema="false">
<table name="orders" dataNode="dn0,dn1,dn2,dn3" rule="order_hash"/>
</schema>
<dataNode name="dn0" dataHost="host1" database="db0" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0">
<heartbeat>select user()</heartbeat>
<writeHost host="master" url="192.168.0.1:3306"/>
</dataHost>
方案二:客户端SDK模式(高性能,侵入应用)
架构图
应用程序 → JDBC驱动(增强版) → 分表0/1/2/3
(需少量配置) (集成在驱动中)
ShardingSphere-JDBC(最流行)
在应用内完成路由,无代理层网络开销。
// Spring Boot配置
spring:
shardingsphere:
datasource:
names: ds0, ds1
ds0: ...
ds1: ...
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..1}
key-generate-strategy: # 分布式ID生成
column: order_id
key-generator-name: snowflake
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order_hash
sharding-algorithms:
order_hash:
type: HASH_MOD
props:
sharding-count: 4
// 业务代码完全不变
@Repository
public class OrderDao {
public Order getById(Long orderId) {
// 框架自动路由到正确的分表
return jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE order_id = ?",
orderId // 框架根据这个值计算分表
);
}
}
复杂查询示例:
// 1. 范围查询(触发全表扫描)
List<Order> orders = orderDao.findByCreateTimeBetween(
startTime, endTime);
// 框架会查询所有分表:orders_0,1,2,3,然后合并结果
// 2. 跨分片事务
@ShardingTransactionType(TransactionType.XA) // 启用分布式事务
@Transactional
public void transferOrder(Long fromOrderId, Long toOrderId) {
// 可能涉及两个不同分表的更新
updateOrderStatus(fromOrderId, "CANCELLED");
updateOrderStatus(toOrderId, "CREATED");
}
优点:性能最好,无代理层网络跳转。
缺点:侵入应用,升级需要重启应用,对连接池有影响。
方案三:微服务+数据分治模式(现代架构)
架构图
订单服务 → 订单表0
用户服务 → 订单表1
支付服务 → 订单表2
(按业务域垂直分库+水平分表)
按业务域分治
# 不同服务管理不同分表
order-service: # 管理尾号0,1的订单
datasource:
- orders_0
- orders_1
user-order-service: # 管理用户维度的订单(尾号2)
datasource:
- orders_2
archive-service: # 管理归档订单(尾号3)
datasource:
- orders_3
API聚合层
// 在API网关或BFF层聚合
@GetMapping("/orders/{userId}")
public List<Order> getUserOrders(@PathVariable Long userId) {
// 并行查询不同服务
CompletableFuture<List<Order>> future1 =
orderService.getOrders(userId);
CompletableFuture<List<Order>> future2 =
userOrderService.getOrders(userId);
// 合并结果
return CompletableFuture.allOf(future1, future2)
.thenApply(v -> {
List<Order> result = new ArrayList<>();
result.addAll(future1.join());
result.addAll(future2.join());
return result;
}).join();
}
优点:彻底解耦,按业务伸缩。
缺点:架构复杂,需要服务治理,数据一致性挑战大。
必须解决的三大核心问题
问题1:分布式ID生成
分表后,数据库自增ID不再适用。
// 方案1:Snowflake算法(推荐)
public class SnowflakeIdGenerator {
// 生成64位ID:时间戳(41位)+机器ID(10位)+序列号(12位)
// 示例:1541815603606036480(全局唯一、趋势递增)
}
// 方案2:数据库号段模式
CREATE TABLE id_generator (
biz_tag VARCHAR(128) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL
);
-- 每次获取一批ID:UPDATE ... max_id = max_id + step
// 方案3:Redis原子操作
Long id = redis.incr("order_id_counter");
// 结合业务前缀:ORDER_20240101_00000001
问题2:跨分片查询
方案A:应用层聚合(推荐)
public List<Order> searchOrders(OrderQuery query) {
List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
// 并行查询所有分片
for (int i = 0; i < 4; i++) {
String table = "orders_" + i;
futures.add(CompletableFuture.supplyAsync(() ->
queryShard(table, query)
));
}
// 合并结果
return futures.stream()
.flatMap(future -> future.join().stream())
.sorted(Comparator.comparing(Order::getCreateTime).reversed())
.limit(query.getPageSize())
.collect(Collectors.toList());
}
方案B:建立全局二级索引
-- 针对非分片键的查询,建立单独的索引表
CREATE TABLE order_user_index (
user_id BIGINT,
order_id BIGINT, -- 分片键
shard_id TINYINT, -- 分片编号
PRIMARY KEY(user_id, order_id)
);
-- 查询流程:
-- 1. 查询索引表:SELECT shard_id, order_id FROM order_user_index WHERE user_id=?
-- 2. 到对应分表查询详情
方案C:异步聚合到宽表/ES
// 使用CDC(Debezium/Canal)同步到ES
订单表分片 → Binlog → CDC采集 → Kafka → ES索引
// 查询直接走ES
List<Order> orders = elasticsearchTemplate.search(
QueryBuilders.boolQuery()
.must(termQuery("user_id", userId))
.must(rangeQuery("amount").gte(100)),
Order.class
);
问题3:分布式事务
场景:下单扣库存
-- 订单在分表1,库存表在另一个库
解决方案:
// 方案1:Seata AT模式(推荐)
@GlobalTransactional
public void placeOrder(OrderDTO order) {
// 1. 扣减库存(库存服务)
inventoryService.reduce(order.getSkuId(), order.getQuantity());
// 2. 创建订单(订单服务,可能跨分片)
orderService.create(order); // 可能需要操作多个分表
// 3. 扣减余额(账户服务)
accountService.deduct(order.getUserId(), order.getAmount());
// Seata保证这三个操作的原子性
}
// 方案2:基于消息的最终一致
public void placeOrder(OrderDTO order) {
// 1. 预扣库存(本地事务)
boolean success = inventoryService.tryReduce(order);
if (success) {
// 2. 发送创建订单消息
rocketMQTemplate.send("order_topic", order);
// 3. 消费者异步创建订单(可能重试)
}
}
选型决策指南
根据团队规模选择
小型团队 (3-5人):
- 推荐: ShardingSphere-Proxy
- 理由: 运维简单,应用无需改造
中型团队 (5-20人):
- 推荐: ShardingSphere-JDBC + 配置中心
- 理由: 性能更好,灵活性高
大型团队 (20+人):
- 推荐: 微服务分治 + 专门的DAL层
- 理由: 按业务域自治,便于多团队协作
根据业务阶段选择
初创期 (数据量 < 1000万):
- 建议: 先不分表,做好监控
- 优化: 索引、缓存、读写分离
成长期 (1000万 ~ 1亿):
- 建议: 垂直分表 + 简单水平分表
- 方案: ShardingSphere-JDBC,2-4个分片
成熟期 (> 1亿):
- 建议: 完整的分布式数据架构
- 方案: 多级分片 + 全局索引 + 分布式事务
根据查询模式选择
-- 场景1:主键/分片键查询占80%以上
-- 方案:客户端SDK模式(ShardingSphere-JDBC)
-- 场景2:复杂查询多,多维度查询
-- 方案:代理模式 + ES/CK搜索引擎
-- 场景3:实时性要求不高,偏分析
-- 方案:分库分表 + 数据同步到数仓
实施步骤(推荐路径)
步骤1:评估与准备
-- 1. 分析现有表
SELECT
table_name,
table_rows,
data_length/1024/1024 as data_mb,
index_length/1024/1024 as index_mb
FROM information_schema.tables
WHERE table_schema = 'your_db';
-- 2. 分析查询模式
-- 使用慢查询日志或性能模式
SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%orders%'
ORDER BY sum_timer_wait DESC;
步骤2:渐进式实施
// 阶段1:双写过渡
public void createOrder(Order order) {
// 同时写入老表和新分表
jdbcTemplate.update("INSERT INTO orders VALUES (?,?,?)", ...);
jdbcTemplate.update("INSERT INTO orders_" + shardKey + " VALUES (?,?,?)", ...);
// 验证数据一致性
assert oldData.equals(newData);
}
// 阶段2:读切流(逐步迁移读流量)
// 阶段3:写切流(最终完全切到分表)
步骤3:监控与优化
监控指标:
- 分片均衡度: 各分表数据量差异 < 20%
- 查询响应时间: P95 < 100ms
- 跨分片查询比例: < 5%
- 分布式事务成功率: > 99.9%
告警规则:
- 单个分片数据量超过阈值
- 跨分片查询耗时突增
- 分布式事务失败率升高
总结:分表只是手段,不是目的。在分表前,先考虑是否可以:优化查询、增加索引、读写分离、使用缓存。只有当这些手段都用尽,且数据量确实达到瓶颈时,才需要分表。
更多推荐

所有评论(0)