背景
目标
优化 ItemInFolder 单表读写性能,在当前数据库未支持分片单情况下,拓展分表
设计
一致性哈希分表
一致性哈希(Consistent Hashing)是一种用于分布式系统中的哈希算法,旨在将数据均匀地分布到多个节点上,同时在节点的增加或减少时尽可能减少数据的重新分配。这种方法特别适合动态变化的分布式系统,比如缓存系统、分布式数据库等。
一致性哈希的基本原理
-
哈希环:一致性哈希将整个哈希空间看作一个环(通常是 0 到 2^32-1 之间的整数)。节点和数据项通过哈希函数映射到这个环上。
-
节点的哈希值:每个节点(比如数据库分片、缓存服务器)通过哈希函数计算出一个哈希值,并映射到哈希环上。
-
数据项的哈希值:每个数据项(比如一个键值对)也通过哈希函数计算出一个哈希值,并映射到哈希环上。
-
数据分配:每个数据项存储在顺时针方向上遇到的第一个节点上。这意味着,数据项的哈希值落在两个节点的哈希值之间时,数据项将分配给第二个节点。
举例说明
假设有四个节点 A、B、C、D 它们的哈希值分别为 10、20、30、40,构成一个哈希环:
scss
0 ---> A(10) ---> B(20) ---> C(30) ---> D(40) ---> 0
现在有一个数据项,它的哈希值为 25,根据顺时针找到的第一个节点是 C,所以数据项将存储在节点 C 上。
增加或移除节点
一致性哈希的一个重要特点是,当增加或移除节点时,只需重新分配少量数据。
- 增加节点:假设在哈希值为 35 处增加一个节点 E,只有介于 30 到 35 之间的数据项需要重新分配到新节点 E 上,其它数据项不受影响。
- 移除节点:假设移除节点 C,原本存储在 C 上的数据项将重新分配到节点 D 上,其它数据项不受影响。
虚拟节点
为了进一步平衡负载,一致性哈希引入了虚拟节点的概念。每个实际节点对应多个虚拟节点,这些虚拟节点均匀分布在哈希环上。
- 虚拟节点的映射:每个实际节点被分配多个虚拟节点,这些虚拟节点的哈希值映射到哈希环上。
- 数据项的分配:数据项根据虚拟节点的哈希值进行分配,找到对应的实际节点。
一致性哈希代码实现
kotlin
const murmurhash = require('murmurhash');
// 一致性哈希类,用于管理虚拟节点和分片
class ConsistentHashing {
constructor(numberOfReplicas, numberOfShards) {
this.numberOfReplicas = numberOfReplicas; // 每个实际分片的虚拟节点数
this.numberOfShards = numberOfShards; // 实际分片的数量
this.ring = new Map(); // 哈希环,存储虚拟节点到实际分片的映射
this.sortedKeys = []; // 排序后的虚拟节点哈希值数组
// 初始化环并添加初始的分片节点
for (let i = 0; i < numberOfShards; i++) {
this.addShard(i);
}
}
// 使用 MurmurHash v3 作为哈希函数
hashFunction(key) {
return murmurhash.v3(key);
}
// 添加分片节点和其虚拟节点到哈希环
addShard(shardId) {
for (let i = 0; i < this.numberOfReplicas; i++) {
const virtualNodeKey = this.hashFunction(shardId + '-' + i);
this.ring.set(virtualNodeKey, shardId);
this.sortedKeys.push(virtualNodeKey);
}
this.sortedKeys.sort((a, b) => a - b); // 保持虚拟节点有序
}
// 根据键值找到对应的分片
getShard(key) {
if (this.ring.size === 0) {
return null;
}
const hash = this.hashFunction(key);
for (let i = 0; i < this.sortedKeys.length; i++) {
if (hash <= this.sortedKeys[i]) {
return this.ring.get(this.sortedKeys[i]);
}
}
return this.ring.get(this.sortedKeys[0]);
}
}
MurmurHash是一种非加密型哈希函数,由Austin Appleby于2008年设计,具有高效、分布均匀、低碰撞率的特点。MurmurHash v3是MurmurHash家族中的一个版本,适用于生成散列值,尤其在分布式系统中被广泛应用。
MurmurHash v3 的基本原理
-
输入处理:将输入数据分块处理,每块的大小为4字节(32位)。
-
初始化种子:使用一个32位或64位的种子值来初始化散列值。这个种子值提供了哈希函数的可配置性。
-
混合操作:对于每个块,应用一系列的混合操作,这些操作包括乘法、移位和异或操作。混合操作的设计旨在使输出哈希值均匀分布。
-
尾部处理:如果输入数据的长度不是4的倍数,会有少量数据剩余,这些数据被称为尾部。MurmurHash v3 对尾部数据进行单独处理,确保所有输入数据都影响最终的哈希值。
-
最终混合:在处理完所有块和尾部后,对哈希值进行最后的混合操作,使得即使输入数据非常相似(如仅一位不同),输出的哈希值也会显著不同。
测试哈希一致性 & 新增分表后的数据迁移
最小化数据迁移
rust
// 测试函数
function testConsistentHashing() {
const ch = new ConsistentHashing(10, 5); // 100 个虚拟节点, 10 个分片
const keys = ['sshy5it5sj','wgcp7fh7kv','fu79r1s0hn','wcqt10hq51','kposlkduhj','d1ocddk8ln','0z8dnoxjoz','9m38b9mm66','n2dmkg8sjj','nmdxw2yce2','otf5oa3kc2','gnnc7msqqc','oyrf8lo5lq','hjtn3m5zk4','23z07aro3p','4wq8lchvf3','zu2bot00o8','u926x48rc0','56mrm6hlzi','erukqnbgv3'];
const initialMapping = keys.map(key => ({ key, shard: ch.getShard(key) }));
// 添加新的分片
ch.addShard(10);
const newMapping = keys.map(key => ({ key, shard: ch.getShard(key) }));
console.log('Initial Mapping:');
initialMapping.forEach(mapping => console.log(`${mapping.key} -> ItemInFolder_${mapping.shard}`));
console.log('New Mapping:');
newMapping.forEach(mapping => console.log(`${mapping.key} -> ItemInFolder_${initialMapping.find(x => x.key === mapping.key).shard} -> ItemInFolder_${mapping.shard}`));
// Check which keys were remapped
const remappedKeys = keys.filter((key, index) => initialMapping[index].shard !== newMapping[index].shard);
console.log('Remapped Keys:');
remappedKeys.forEach(key => console.log(key));
}
testConsistentHashing();
>>>
key -> originShard. -> newShard
sshy5it5sj -> ItemInFolder_1 -> ItemInFolder_1
wgcp7fh7kv -> ItemInFolder_0 -> ItemInFolder_10
fu79r1s0hn -> ItemInFolder_3 -> ItemInFolder_3
wcqt10hq51 -> ItemInFolder_2 -> ItemInFolder_2
kposlkduhj -> ItemInFolder_0 -> ItemInFolder_0
d1ocddk8ln -> ItemInFolder_4 -> ItemInFolder_4
0z8dnoxjoz -> ItemInFolder_2 -> ItemInFolder_2
9m38b9mm66 -> ItemInFolder_3 -> ItemInFolder_3
n2dmkg8sjj -> ItemInFolder_4 -> ItemInFolder_4
nmdxw2yce2 -> ItemInFolder_3 -> ItemInFolder_3
otf5oa3kc2 -> ItemInFolder_0 -> ItemInFolder_10
gnnc7msqqc -> ItemInFolder_2 -> ItemInFolder_2
oyrf8lo5lq -> ItemInFolder_2 -> ItemInFolder_2
hjtn3m5zk4 -> ItemInFolder_3 -> ItemInFolder_3
23z07aro3p -> ItemInFolder_1 -> ItemInFolder_1
4wq8lchvf3 -> ItemInFolder_3 -> ItemInFolder_3
zu2bot00o8 -> ItemInFolder_1 -> ItemInFolder_1
u926x48rc0 -> ItemInFolder_3 -> ItemInFolder_3
56mrm6hlzi -> ItemInFolder_2 -> ItemInFolder_2
erukqnbgv3 -> ItemInFolder_1 -> ItemInFolder_1
>>>Remapped Keys:
wgcp7fh7kv
otf5oa3kc2
结论:一致性哈希通过虚拟节点的设计,确保在增加分表节点时,仅有部分数据需要重新分配,从而减少迁移量。
开发实施
- ItemInFolder 表迁移分表,根据 ParentFolderId 哈希分表
- 目前单表 3亿+ 数据,暂定分 30 个表,每个子表 1 千万+ 数据
-
保留单表,同步写入分表
-
封装 Dao,调整所有查询操作:单表查询 > 分表查
数据迁移分表
ini
const murmurhash = require('murmurhash');
class ConsistentHashing {
constructor(numberOfReplicas, numberOfShards) {
this.numberOfReplicas = numberOfReplicas;
this.numberOfShards = numberOfShards;
this.ring = new Map();
this.sortedKeys = [];
for (let i = 0; i < numberOfShards; i++) {
this.addShard(i);
}
}
hashFunction(key) {
return murmurhash.v3(key);
}
addShard(shardId) {
for (let i = 0; i < this.numberOfReplicas; i++) {
const virtualNodeKey = this.hashFunction(shardId + '-' + i);
this.ring.set(virtualNodeKey, shardId);
this.sortedKeys.push(virtualNodeKey);
}
this.sortedKeys.sort((a, b) => a - b);
}
removeShard(shardId) {
for (let i = 0; i < this.numberOfReplicas; i++) {
const virtualNodeKey = this.hashFunction(shardId + '-' + i);
this.ring.delete(virtualNodeKey);
const index = this.sortedKeys.indexOf(virtualNodeKey);
if (index !== -1) {
this.sortedKeys.splice(index, 1);
}
}
}
getShard(key) {
if (this.ring.size === 0) {
return null;
}
const hash = this.hashFunction(key);
for (let i = 0; i < this.sortedKeys.length; i++) {
if (hash <= this.sortedKeys[i]) {
return this.ring.get(this.sortedKeys[i]);
}
}
return this.ring.get(this.sortedKeys[0]);
}
}
// 迁移数据函数
async function migrateData(db, oldCollection, newCollections) {
const numberOfReplicas = 100;
const numberOfShards = 10;
const ch = new ConsistentHashing(numberOfReplicas, numberOfShards);
// 获取现有数据的分表映射
const cursor = db.collection(oldCollection).find();
const initialMapping = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
const shard = ch.getShard(doc.parentFolderId);
initialMapping.push({ doc, shard });
}
// 添加新的分表节点
ch.addShard(numberOfShards);
// 获取新的分表映射并迁移数据
for (const { doc, shard } of initialMapping) {
const newShard = ch.getShard(doc.parentFolderId);
if (newShard !== shard) {
await db.collection(newCollections[newShard]).insertOne(doc);
await db.collection(oldCollection).deleteOne({ _id: doc._id });
}
}
}
// 使用示例
(async () => {
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
try {
await client.connect();
const db = client.db('yourDatabase');
const oldCollection = 'ItemInFolder';
const newCollections = [
'ItemInFolder_0', 'ItemInFolder_1', 'ItemInFolder_2', 'ItemInFolder_3',
'ItemInFolder_4', 'ItemInFolder_5', 'ItemInFolder_6', 'ItemInFolder_7',
'ItemInFolder_8', 'ItemInFolder_9', 'ItemInFolder_10'
];
await migrateData(db, oldCollection, newCollections);
console.log('Data migration completed successfully.');
} catch (err) {
console.error(err);
} finally {
await client.close();
}
})();