分库分表
如果用 userId 分表,要用merchantId 查询的话,如何查询?
维护索引表:用一张单独的索引表存储 merchantId -> userId,查询时先查索引表拿到 userId。
自己手写实现 一致性哈希分表
一、核心原理(3 句话记住)
- 把 userId 哈希成一个 0 ~ 2^32-1 的整数
- 把 分表(表名后缀 0、1、2、3...) 也哈希后放到哈希环上
- userId 顺时针找最近的表 → 得到表序号(0、1、2...)
关键:加表只影响少量数据
二、手写实现一致性哈希(完整可复制)
结构
- 哈希环(排序的 Map)
- 物理节点(真实表:0、1、2、3)
- 虚拟节点(让数据分布更均匀)
- 路由方法:输入 userId → 输出表序号
完整代码
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* 自己实现 一致性哈希分表(单库多表)
* 作用:userId -> 表序号(0、1、2、3...)
*/
public class ConsistentHashTableRouter {
// 哈希环:key=哈希值,value=节点(真实节点+虚拟节点)
private final SortedMap<Long, Integer> hashRing = new TreeMap<>();
// 虚拟节点数量(让数据分布更均匀,推荐 10~20)
private static final int VIRTUAL_NODE_NUM = 10;
// 真实表数量(如 4 张表:0、1、2、3)
private final int tableCount;
// 构造器:初始化哈希环
public ConsistentHashTableRouter(int tableCount) {
this.tableCount = tableCount;
initRing();
}
// 初始化环:加入真实表 + 虚拟节点
private void initRing() {
for (int tableIdx = 0; tableIdx < tableCount; tableIdx++) {
// 加入真实节点
long realHash = hash("real-node-" + tableIdx);
hashRing.put(realHash, tableIdx);
// 加入虚拟节点
for (int v = 0; v < VIRTUAL_NODE_NUM; v++) {
long virtualHash = hash("virtual-node-" + tableIdx + "-" + v);
hashRing.put(virtualHash, tableIdx);
}
}
}
// 哈希函数(MurmurHash 风格,简单版)
private long hash(String key) {
byte[] bytes = key.getBytes(StandardCharsets.UTF_8);
long h = 0;
for (byte b : bytes) {
h = (h * 31 + (b & 0xFF)) & 0xFFFFFFFFL;
}
return h;
}
// 核心路由:userId -> 表序号
public int getTableIndex(long userId) {
String key = String.valueOf(userId);
long userHash = hash(key);
// 获取比 userHash 大的所有环节点
SortedMap<Long, Integer> tailMap = hashRing.tailMap(userHash);
if (tailMap.isEmpty()) {
// 环尾 → 回到环头
return hashRing.get(hashRing.firstKey());
}
// 顺时针取第一个
return tailMap.get(tailMap.firstKey());
}
// 测试
public static void main(String[] args) {
// 4 张表
ConsistentHashTableRouter router = new ConsistentHashTableRouter(4);
long userId = 10086L;
int tableIndex = router.getTableIndex(userId);
// 输出:t_order_2
System.out.println("路由到表:t_order_" + tableIndex);
}
}
三、这个代码能干什么?
你传入 userId
它直接返回 表序号
例如:
userId=10086 → 表 2 userId=12345 → 表 0 userId=999 → 表 3
最终表名:
"t_order_" + 表序号
四、扩容(加表)怎么操作?
原来 4 张表 → 现在 5 张表
- 新建表 t_order_4
- 创建新的 ConsistentHashTableRouter (5)
- 把新环加载
- 迁移 1/5 数据(只需要迁移很少)
90% 的 userId 路由不变!