从 MongoDB 迁移到 ProtonBase 指南
概述
MongoDB 是一种文档数据库,它所具备的可扩展性和灵活性可以满足您对查询和索引的需求。本文为您介绍如何从 MongoDB 迁移至 ProtonBase,包括数据同步配置、应用改造示例和常见问题解决方案。
有关 MongoDB 的更多信息,请参见 MongoDB 是什么? (opens in a new tab)
准备工作
环境要求
- MongoDB 3.6 或更高版本
- ProtonBase 数据库实例
- 网络连通性(建议内网连接以获得最佳性能)
- Teleport 数据同步工具访问权限
权限配置
在 MongoDB 中创建专门用于数据同步的用户:
简单赋权(推荐)
可以到 Database 级别进行赋权,赋权后可进行批量读和 changeStream 流式读:
use test;
db.createUser({
  user: "protonbase",
  pwd: "xxxxxx",
  roles: [
    {
      role: "read",
      db: "test",
    }
  ]
});复杂赋权
可以针对具体的 collection 和操作进行精细赋权:
use test;
db.createRole({
  role: "readAndWatchSpecificCollection",
  privileges: [
    {
      resource: { db: "test", collection: "a" },
      actions: ["find", "changeStream"]
    }
  ],
  roles: []
});
 
db.createUser({
  user: "protonbase",
  pwd: "xxxxx",
  roles: [
    { role: "readAndWatchSpecificCollection", db: "test" }
  ]
});网络配置
参考 数据同步网络配置
迁移步骤
1. 评估源数据库
在开始迁移之前,需要对 MongoDB 数据库结构进行全面评估:
// 查看数据库列表
show dbs;
 
// 切换到目标数据库
use your_database;
 
// 查看集合列表
show collections;
 
// 查看集合统计信息
db.collection_name.stats();
 
// 抽样查看文档结构
db.collection_name.findOne();2. 准备目标环境
在 ProtonBase 中创建相应的数据库和表结构:
-- 在 ProtonBase 中创建数据库
CREATE DATABASE ecommerce;
 
-- 创建用户和权限
CREATE USER "mongodb_migration" WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE ecommerce TO "mongodb_migration";3. 数据同步配置
3.1 新建数据迁移任务
- 登录 ProtonBase 控制台 ProtonBase (opens in a new tab)
- 进入到相应的 data center
- 在左侧导航栏,单击 数据同步 --> 数据导入
- 在 数据导入 页面,单击右上角的 + 数据导入作业
3.2 配置源和目标
在 选择源和目标 页面,配置各项参数:
MongoDB 源配置:
| 参数 | 描述 | 
|---|---|
| 连接方式 | 直接连接需要源端配置公网访问,并且将 Teleport 的 IP 加入到白名单。 Tunnel 参考网络配置 | 
| 域名[:端口] | 数据库连接信息,包括端口,类似 mongodbserver1:27017,mongodbserver2:27018,mongodbserver3:27019 | 
| 用户名 | 用于同步的用户名,需要参考准备工作中的权限设置 | 
| 密码 | 用户的密码 | 
| Auth Source | 用于进行身份验证的数据库 | 
| Replica Set | 副本集名 (opens in a new tab) | 
| Read Preference | 设置的读策略 (opens in a new tab) | 
| Read Concern | 设置的读关注 (opens in a new tab) | 
ProtonBase 目标配置:
| 参数 | 描述 | 
|---|---|
| 连接方式 | 可以直接选择当前 data center 内的 warebase 或者通过域名端口方式访问其他区域的 warebase | 
| WareBase | 选择需要同步的 WareBase | 
| 用户 | 用于同步的用户 | 
配置完上述参数后,需要确定 Teleport 能够连接到数据源及目标端,可以通过 Teleport 页面的连接测试功能测试网络连接。
3.3 选择同步对象
在 选择同步对象 页面,选择需要同步的数据库:
| 参数 | 描述 | 
|---|---|
| 对象类型 | 需要同步的对象类型,默认同步 Collection | 
| 同步所需的 DDL 操作 | 允许同步的 DDL 操作 | 
| 同步所需的 DML 操作 | 允许同步的 DDL 操作 | 
| 目标表类型 | 可以配置默认目标端的表的存储类型,具体可以参考表结构设计 | 
| 同步外键 | 是否同步外键,MongoDB 忽略该选项 | 
| 同步索引 | 是否同步索引 | 
| 同步唯一索引 | 是否同步唯一索引 | 
| Drop 策略 | 删除表的策略,包括 - RESTRICT,将会进行默认的 drop 操作。 - CASCADE,将会进行 DROP CASCADE 操作。 - RENAME,将会对目标表进行 rename 操作。 | 
3.4 设置映射规则
在 设置映射规则 页面,查看映射规则及字段映射,参考 同步对象映射。
由于 MongoDB 存放的是 JSON 内容,可以在字段映射的时候修改字段映射规则。
3.5 设置同步策略
在 设置策略 页面,查看数据同步模式及脏数据处理策略。
4. 启动和监控迁移
启动同步作业后,查看同步状态。同步过程中的状态可以参考作业操作和状态。
5. 验证数据一致性
检查 MongoDB 的数据情况:
/* 1 */
{
    "_id" : ObjectId("672888ffc6fca18f4856a781"),
    "name" : "robert",
    "email" : "robert.xiao@protonbase.io"
}
 
/* 2 */
{
    "_id" : ObjectId("6728891dc6fca18f4856a782"),
    "name" : "tiny",
    "email" : "tiny.chen@protonbase.io"
}在 MongoDB 新增数据:
/* 3 */
{
    "_id" : ObjectId("6729c0a4c6fca18f4856a7c0"),
    "name" : "eric",
    "email" : "eric@protonbase.io"
}检查同步的数据是否正确。
应用改造
改造时需注意:
- 替换 MongoDB 驱动(如 mongodb-driver-sync)为 PostgreSQL 驱动(如org.postgresql.Driver)。
- 在应用中将 MongoDB 的集合操作改为对 PostgreSQL 表的操作。
- 如果使用 ORM 工具,推荐使用支持 PostgreSQL 的 ORM(如 Hibernate、Spring Data JPA 等)。
连接配置更新
将应用程序中的数据库连接配置从 MongoDB 更新为 ProtonBase:
# 原 MongoDB 配置
spring.data.mongodb.uri=mongodb://username:password@mongodb-host:27017/database
 
# 新 ProtonBase 配置
spring.datasource.url=jdbc:postgresql://protonbase-host:5432/database
spring.datasource.username=protonbase_user
spring.datasource.password=protonbase_password
spring.datasource.driver-class-name=org.postgresql.Driver数据模型转换
MongoDB 的文档结构需要转换为关系型表结构或使用 JSON 类型字段:
MongoDB 文档示例
{
  "_id": ObjectId("..."),
  "name": "Alice",
  "email": "alice@example.com",
  "orders": [
    {
      "orderId": "ORD001",
      "total": 99.99,
      "items": [
        {"productId": "P001", "quantity": 2}
      ]
    }
  ]
}ProtonBase 表结构方案一:使用 JSONB 字段
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100),
    profile JSONB
);
 
-- 插入数据
INSERT INTO users (name, email, profile) VALUES (
    'Alice',
    'alice@example.com',
    '{
      "orders": [
        {
          "orderId": "ORD001",
          "total": 99.99,
          "items": [
            {"productId": "P001", "quantity": 2}
          ]
        }
      ]
    }'::JSONB
);ProtonBase 表结构方案二:关系型表结构
-- 用户表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);
 
-- 订单表
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    order_id VARCHAR(50),
    total NUMERIC(10, 2)
);
 
-- 订单项表
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(id),
    product_id VARCHAR(50),
    quantity INTEGER
);代码示例
插入数据
MongoDB 示例(Java)
MongoCollection<Document> collection = database.getCollection("users");
Document doc = new Document("name", "Alice")
                    .append("age", 25)
                    .append("city", "New York");
collection.insertOne(doc);PostgreSQL 示例(Java)
String query = "INSERT INTO users (name, age, city) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query)) {
    stmt.setString(1, "Alice");
    stmt.setInt(2, 25);
    stmt.setString(3, "New York");
    stmt.executeUpdate();
}查询数据
单条件查询
MongoDB 示例(Java)
Document query = new Document("name", "Alice");
Document result = collection.find(query).first();
if (result != null) {
    System.out.println(result.toJson());
}PostgreSQL 示例(Java)
String query = "SELECT * FROM users WHERE name = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query)) {
    stmt.setString(1, "Alice");
    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            System.out.println("Name: " + rs.getString("name"));
            System.out.println("Age: " + rs.getInt("age"));
            System.out.println("City: " + rs.getString("city"));
        }
    }
}多条件查询
MongoDB 示例(Java)
Document query = new Document("age", new Document("$gte", 20))
                     .append("city", "New York");
FindIterable<Document> results = collection.find(query);
for (Document doc : results) {
    System.out.println(doc.toJson());
}PostgreSQL 示例(Java)
String query = "SELECT * FROM users WHERE age >= ? AND city = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query)) {
    stmt.setInt(1, 20);
    stmt.setString(2, "New York");
    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            System.out.println("Name: " + rs.getString("name"));
            System.out.println("Age: " + rs.getInt("age"));
            System.out.println("City: " + rs.getString("city"));
        }
    }
}更新数据
MongoDB 示例(Java)
Document query = new Document("name", "Alice");
Document update = new Document("$set", new Document("age", 26));
collection.updateOne(query, update);PostgreSQL 示例(Java)
String query = "UPDATE users SET age = ? WHERE name = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query)) {
    stmt.setInt(1, 26);
    stmt.setString(2, "Alice");
    stmt.executeUpdate();
}删除数据
MongoDB 示例(Java)
Document query = new Document("name", "Alice");
collection.deleteOne(query);PostgreSQL 示例(Java)
String query = "DELETE FROM users WHERE name = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query)) {
    stmt.setString(1, "Alice");
    stmt.executeUpdate();
}聚合操作
MongoDB 示例(Java)
List<Bson> pipeline = Arrays.asList(
    Aggregates.group("$city", Accumulators.sum("count", 1))
);
AggregateIterable<Document> results = collection.aggregate(pipeline);
for (Document doc : results) {
    System.out.println(doc.toJson());
}PostgreSQL 示例(Java)
String query = "SELECT city, COUNT(*) AS count FROM users GROUP BY city";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement stmt = conn.prepareStatement(query);
     ResultSet rs = stmt.executeQuery()) {
    while (rs.next()) {
        System.out.println("City: " + rs.getString("city") + ", Count: " + rs.getInt("count"));
    }
}迁移最佳实践
1. 迁移前准备
- 
数据评估: - 评估 MongoDB 数据库大小和集合数量
- 分析文档结构复杂度
- 识别大文档和嵌套结构
 
- 
制定迁移计划: - 确定迁移时间窗口(低峰期)
- 准备回滚方案
- 制定详细的测试计划
- 安排团队培训和知识转移
 
2. 迁移过程中
- 
分阶段迁移: - 先迁移非关键业务数据
- 逐步迁移核心业务数据
- 并行运行新旧系统进行验证
 
- 
实时监控: - 监控数据同步延迟
- 监控系统性能指标
- 记录迁移过程中的问题和解决方案
 
3. 迁移后优化
- 
性能调优: - 根据查询模式创建合适的索引
- 优化表存储模式(行存/列存/混存)
- 调整系统参数以适应工作负载
 
- 
安全加固: - 配置 IP 白名单限制访问
- 设置细粒度的用户权限
- 启用审计日志记录关键操作
 
常见问题与解决方法
数据模型设计差异
问题:MongoDB 的文档结构允许嵌套字段,PostgreSQL 中则需转换为多表结构或使用 JSON 类型字段。
解决方案:
- 如果嵌套层次简单,直接用 PostgreSQL 的 JSON或JSONB类型字段存储嵌套数据。
- 对复杂嵌套字段,设计关系型表结构,并通过外键关联实现。
示例: MongoDB 文档:
{ "name": "Alice", "orders": [{ "id": 1, "total": 50 }] }PostgreSQL 表:
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT, total NUMERIC, FOREIGN KEY (user_id) REFERENCES users(id));数据类型映射
问题:MongoDB 的动态类型与 PostgreSQL 的强类型系统不匹配。
解决方案:
- 使用 JSONB 类型存储动态结构数据
- 在应用层进行类型转换和验证
- 建立数据验证规则确保一致性
查询性能优化
问题:从 MongoDB 的文档查询转换为关系型查询可能影响性能。
解决方案:
- 合理设计索引策略
- 使用适当的连接查询
- 考虑使用物化视图优化复杂查询
事务处理
问题:MongoDB 的文档级原子性与 PostgreSQL 的事务处理机制不同。
解决方案:
- 使用 PostgreSQL 的事务功能确保数据一致性
- 在应用层实现分布式事务处理(如需要)
- 合理设计业务逻辑避免长事务
通过遵循这些步骤和最佳实践,您可以顺利完成从 MongoDB 到 ProtonBase 的迁移,并充分利用 ProtonBase 的分布式数据库优势。