单表 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();
    }
})();
相关推荐
苏打水com8 分钟前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
sorryhc24 分钟前
如何设计一个架构良好的前端请求库?
前端·javascript·架构
间彧1 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧1 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng3 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6013 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring
Lisonseekpan3 小时前
Guava Cache 高性能本地缓存库详解与使用案例
java·spring boot·后端·缓存·guava