单表 3亿+ 数据如何分表

背景

目标

优化 ItemInFolder 单表读写性能,在当前数据库未支持分片单情况下,拓展分表

设计

一致性哈希分表

一致性哈希(Consistent Hashing)是一种用于分布式系统中的哈希算法,旨在将数据均匀地分布到多个节点上,同时在节点的增加或减少时尽可能减少数据的重新分配。这种方法特别适合动态变化的分布式系统,比如缓存系统、分布式数据库等。

一致性哈希的基本原理

  1. 哈希环:一致性哈希将整个哈希空间看作一个环(通常是 0 到 2^32-1 之间的整数)。节点和数据项通过哈希函数映射到这个环上。

  2. 节点的哈希值:每个节点(比如数据库分片、缓存服务器)通过哈希函数计算出一个哈希值,并映射到哈希环上。

  3. 数据项的哈希值:每个数据项(比如一个键值对)也通过哈希函数计算出一个哈希值,并映射到哈希环上。

  4. 数据分配:每个数据项存储在顺时针方向上遇到的第一个节点上。这意味着,数据项的哈希值落在两个节点的哈希值之间时,数据项将分配给第二个节点。

举例说明

假设有四个节点 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 的基本原理

  1. 输入处理:将输入数据分块处理,每块的大小为4字节(32位)。

  2. 初始化种子:使用一个32位或64位的种子值来初始化散列值。这个种子值提供了哈希函数的可配置性。

  3. 混合操作:对于每个块,应用一系列的混合操作,这些操作包括乘法、移位和异或操作。混合操作的设计旨在使输出哈希值均匀分布。

  4. 尾部处理:如果输入数据的长度不是4的倍数,会有少量数据剩余,这些数据被称为尾部。MurmurHash v3 对尾部数据进行单独处理,确保所有输入数据都影响最终的哈希值。

  5. 最终混合:在处理完所有块和尾部后,对哈希值进行最后的混合操作,使得即使输入数据非常相似(如仅一位不同),输出的哈希值也会显著不同。

测试哈希一致性 & 新增分表后的数据迁移

最小化数据迁移

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

结论:一致性哈希通过虚拟节点的设计,确保在增加分表节点时,仅有部分数据需要重新分配,从而减少迁移量。

开发实施

  1. ItemInFolder 表迁移分表,根据 ParentFolderId 哈希分表
  • 目前单表 3亿+ 数据,暂定分 30 个表,每个子表 1 千万+ 数据
  1. 保留单表,同步写入分表

  2. 封装 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();
    }
})();
相关推荐
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml46 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠7 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#