在分布式数据库架构中,当单表数据量突破千万级别时,查询、写入的性能都会出现明显下滑,此时通常会采用表分片的方式将数据分散存储到多个物理节点上。分片之后,如何根据业务传入的键值快速、精准地定位到目标分片,是路由规则设计的核心目标,避免出现全分片扫描的情况,浪费系统资源。

常见分片路由规则类型
哈希取模路由
哈希取模是最简单的路由规则,先对分片键值做哈希计算,再对分片总数取模,得到的结果就是目标分片的编号。这种规则实现简单,数据分布相对均匀,适合分片数量固定的场景。
实现逻辑如下:
// 分片总数
private static final int SHARD_COUNT = 4;
/**
* 根据键值计算目标分片编号
* @param shardKey 分片键值,比如用户ID
* @return 分片编号,范围0到SHARD_COUNT-1
*/
public static int hashModRoute(long shardKey) {
// 对键值取哈希后和分片总数取模
return (int) (Math.abs(shardKey) % SHARD_COUNT);
}
范围分片路由
范围分片是按照键值的区间划分分片,比如用户ID在1-1000000的存储到分片0,1000001-2000000的存储到分片1,以此类推。这种规则适合键值有明显递增趋势的场景,方便做区间查询,但可能出现数据分布不均匀的问题。
实现逻辑如下:
// 每个分片存储的键值范围大小
private static final long RANGE_STEP = 1000000;
/**
* 根据键值计算目标分片编号
* @param shardKey 分片键值
* @return 分片编号
*/
public static int rangeRoute(long shardKey) {
// 按照范围步长计算分片编号,向下取整
return (int) (shardKey / RANGE_STEP);
}
一致性哈希路由
一致性哈希路由可以解决分片数量动态变化时的数据迁移问题,它将哈希空间组织成一个虚拟的环,分片和键值都映射到环上,键值顺时针找到的第一个分片就是目标分片。适合分片数量需要动态扩容缩容的场景。
简化实现逻辑如下:
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHashRoute {
// 虚拟节点到真实分片的映射
private SortedMap<Integer, Integer> hashRing = new TreeMap<>();
// 每个真实分片对应的虚拟节点数量
private static final int VIRTUAL_NODE_COUNT = 100;
/**
* 初始化哈希环,添加分片节点
* @param shardIds 真实分片ID集合
*/
public void initRing(Integer[] shardIds) {
for (Integer shardId : shardIds) {
for (int i = 0; i < VIRTUAL_NODE_COUNT; i++) {
// 生成虚拟节点哈希值,这里用简单拼接后哈希模拟
int virtualHash = (shardId + "_" + i).hashCode();
hashRing.put(virtualHash, shardId);
}
}
}
/**
* 根据键值查找目标分片
* @param shardKey 分片键值
* @return 真实分片ID,没有找到返回null
*/
public Integer findShard(long shardKey) {
int keyHash = Long.valueOf(shardKey).hashCode();
// 找到大于等于键值哈希的虚拟节点
SortedMap<Integer, Integer> tailMap = hashRing.tailMap(keyHash);
if (tailMap.isEmpty()) {
// 没有更大的节点,回到环的第一个节点
return hashRing.get(hashRing.firstKey());
}
return tailMap.get(tailMap.firstKey());
}
}
SQL路由规则的实际落地
在实际的数据库中间件中,路由规则通常会和SQL解析结合,先解析SQL中的分片键条件,再调用对应的路由算法得到目标分片,最后将SQL转发到对应的分片执行。以下是一个简单的SQL路由转发示例:
import java.util.ArrayList;
import java.util.List;
public class SqlRouteDemo {
// 模拟分片对应的数据库连接地址
private static final List<String> SHARD_ADDRESSES = new ArrayList<>();
static {
SHARD_ADDRESSES.add("192.168.0.1:3306/db_shard_0");
SHARD_ADDRESSES.add("192.168.0.1:3306/db_shard_1");
SHARD_ADDRESSES.add("192.168.0.1:3306/db_shard_2");
SHARD_ADDRESSES.add("192.168.0.1:3306/db_shard_3");
}
/**
* 模拟根据路由结果转发SQL到目标分片执行
* @param shardKey 分片键值
* @param sql 原始SQL语句
*/
public static void routeSql(long shardKey, String sql) {
// 使用哈希取模路由计算分片编号
int shardIndex = hashModRoute(shardKey);
String targetAddress = SHARD_ADDRESSES.get(shardIndex);
System.out.println("SQL将转发到分片:" + targetAddress);
System.out.println("执行SQL:" + sql);
// 实际场景中这里会建立到目标地址的数据库连接执行SQL
}
private static int hashModRoute(long shardKey) {
return (int) (Math.abs(shardKey) % SHARD_ADDRESSES.size());
}
public static void main(String[] args) {
// 模拟查询用户ID为12345的用户数据
long userId = 12345;
String querySql = "SELECT * FROM user WHERE user_id = " + userId;
routeSql(userId, querySql);
}
}
路由规则选择建议
不同的业务场景适合不同的路由规则:
- 如果分片数量固定,业务查询大多是单键值查询,优先选择哈希取模路由,实现简单且数据分布均匀
- 如果键值有明显的时间或者递增趋势,且经常需要做区间范围查询,优先选择范围分片路由
- 如果业务需要频繁扩容缩容分片节点,优先选择一致性哈希路由,减少数据迁移的量
需要注意的是,分片键的选择也非常重要,尽量选择高频查询条件中的字段作为分片键,避免路由时出现无法定位分片的情况,导致全分片扫描。如果SQL中没有包含分片键条件,通常会需要遍历所有分片执行查询,性能会明显下降。