BBleae Sip

将NodeBB的数据从Redis迁移至MongoDB
NodeBB是什么?NodeBB是一个开源的论坛软件,基于Node.JS开发,使用Redis或MongoDB来存储...
扫描右侧二维码阅读全文
28
2018/10

将NodeBB的数据从Redis迁移至MongoDB

NodeBB是什么?

NodeBB是一个开源的论坛软件,基于Node.JS开发,使用Redis或MongoDB来存储数据(详见 https://nodebb.org/

为什么要迁移数据?

比起MongoDB,Redis更适合用来存储缓存而不是当成数据库来用(参考这篇文章

NodeBB虽然可以使用不同的数据库来存储数据,但是官方并没有提供把数据从一种数据库迁移到另一种数据库的工具,于是我写了一个(

下面开始正片

首先我们需要知道,NodeBB使用两种数据库存储时,数据的结构分别是什么以及他们的区别。
搭建两个NodeBB站点,分别使用Redis和MongoDB作为数据库(搭建步骤略过,可以参考官方文档或者中文社区的文档),然后在这两个站点进行一些注册/发帖等操作,让数据库里存储一些数据
(我这里有现成的NodeBB站点,所以就不用搭建新的来演示了)
现在我们连接到数据库来看看里面有什么东西

使用Redis Desktop Manager连接到Redis Server
Redis

使用MongoDB Compass连接到MongoDB
Mongo

对比两个数据库中的内容,不难发现:

  • NodeBB在MongoDB中也是按类似于Redis的方式来存储数据的
  • Redis中的key对应MongoDB中Document的 _key 字段
  • 对于Redis的5种数据类型,在MongoDB中对应的Document分别是

    • 字符串(String):

      {
      _key: Redis中的key,
      data: 字符串的值
      }
    • 哈希(Hash): Redis的Hash和MongoDB的Document结构相似,除了 _key 字段以外基本相同。

      • 例1:这样的一个Hash:

        127.0.0.1:6379>  HGETALL user:1
        1) "username"
        2) "BBleae"
        3) "email"
        4) "bbleae@baka.studio"

        在MongoDB中对应的Document为

        {
        _key: "user:1",
        username:"BBleae",
        email:"bbleae@baka.studio"
        }
      • 例2:
        hash
    • 列表(List):
      list
    • 集合(Set):
      set
    • 有序集合(sorted set):
      zset

代码:

用Node.JS实现数据的迁移(有一些很重要的细节后面会解释)

'use strict'
var Promise = require('bluebird')
var MongoClient = require('mongodb').MongoClient
var redis = require('redis')
Promise.promisifyAll(redis)
var redisClient = redis.createClient({
    host: '127.0.0.1',
    port: '6379'
})

async function start() {
    const url = 'mongodb://localhost:27017'
    const dbName = 'nodebb'
    const client = new MongoClient(url)

    try {
        await client.connect()
        var col = client.db(dbName).collection('objects')
        var keys = await redisClient.keysAsync('*')
        console.log('total keys:', keys.length)
        var inserted = 0
        for (const i of keys) {
            if (i.indexOf('sess:') == -1 && i.indexOf('nodebbpostsearch:') == -1 && i.indexOf('nodebbtopicsearch:') == -1) {
                const type = await redisClient.typeAsync(i)
                switch (type) {
                    case 'hash':
                        let hash = await redisClient.hgetallAsync(i)
                        for (const prop in hash) {
                            if (Number(hash[prop]) != 0 && !Number(hash[prop]))
                                hash[prop] = Number(hash[prop])
                        }
                        hash._key = i
                        await col.insertOne(hash)
                        console.log(++inserted)
                        break
                    case 'zset':
                        let zset = await redisClient.zrangeAsync(i, 0, -1, 'withscores')
                        let zlen = zset.length
                        for (let j = 0; j < zlen; j += 2) {
                            await col.insertOne({ _key: i, value: zset[j], score: Number(zset[j + 1]) })
                        }
                        console.log(++inserted)
                        break
                    case 'set':
                        let set = await redisClient.smembersAsync(i)
                        await col.insertOne({ _key: i, members: set })
                        console.log(++inserted)
                        break
                    case 'string':
                        let str = await redisClient.getAsync(i)
                        await col.insertOne({ _key: i, data: str })
                        console.log(++inserted)
                        break
                    case 'list':
                        let list = await redisClient.lrangeAsync(i, 0, -1)
                        await col.insertOne({ _key: i, array: list })
                        console.log(++inserted)
                        break
                    default:
                        console.log("Unknown key type:", i)
                        break
                }
            }
        }
    } catch (err) {
        console.log(err.stack)
    }

    client.close()
}

start()

一些细节的说明:

if (i.indexOf('sess:') == -1 && i.indexOf('nodebbpostsearch:') == -1 && i.indexOf('nodebbtopicsearch:') == -1)

忽略所有以sess:/nodebbpostsearch:/nodebbtopicsearch:开头的key
sess:开头的是Session存储,NodeBB可以单独使用Redis存储Session,而且即使用MongoDB来存储,也会存储在单独的collection里(而不是名为objects的collection!)
nodebbpostsearch:和nodebbtopicsearch:是NodeBB自带插件 nodebb-plugin-dbsearch 使用的数据,这部分如果用MongoDB存储也会在单独的collection里,另外这部分数据可以在网站后台Reindex重新生成

if (Number(hash[prop]) != 0 && !Number(hash[prop]))
    hash[prop] = Number(hash[prop])
await col.insertOne({ _key: i, value: zset[j], score: Number(zset[j + 1])

这两段代码非常重要! 它们把字符串转换为数字,因为Redis里存储的都是字符串,而如果使用MongoDB存储,有些地方会用到 $inc 这个操作符,对字符串使用 $inc 毫无疑问会报错!

Last modification:October 31st, 2018 at 09:59 pm

Leave a Comment