MongoDB 主从读写分离实现示例
在高并发业务场景中,数据库读写压力往往不均衡,读操作占比通常远高于写操作。通过 MongoDB 主从读写分离架构,可以将写请求路由到主节点,读请求分发到从节点,有效提升数据库整体吞吐量,降低单节点负载。本文将介绍基于 MongoDB 副本集实现读写分离的核心原理与代码示例。
一、核心原理说明
MongoDB 副本集(Replica Set)由多个节点组成,包含 1 个主节点(Primary)和多个从节点(Secondary)。主节点负责处理所有写操作,从节点通过复制主节点的 oplog 同步数据,默认从节点不处理读请求,需要显式配置读偏好(Read Preference)才能将读请求路由到从节点。
常用的读偏好模式包括:
primary:默认模式,所有读请求都发送到主节点,数据一致性最高
primaryPreferred:优先主节点读取,主节点不可用时才从从节点读取
secondary:所有读请求都发送到从节点,主节点不处理读请求
:优先从节点读取,从节点不可用时才从主节点读取
nearest:从网络延迟最低的节点(主从都可能)读取
实现读写分离的核心就是为不同的操作指定合适的读偏好,写操作默认走主节点,读操作根据需求选择从节点相关的读偏好模式。
二、环境准备
假设已经搭建好一个包含 3 个节点的 MongoDB 副本集,节点信息如下:
| 节点角色 | 连接地址 |
|---|---|
| 主节点(Primary) | mongodb://192.168.1.10:27017 |
| 从节点1(Secondary) | mongodb://192.168.1.11:27017 |
| 从节点2(Secondary) | mongodb://192.168.1.12:27017 |
副本集名称为 rs0,测试数据库为 test_db,测试集合为 user。
三、Node.js 实现读写分离示例
以下示例使用 MongoDB 官方 Node.js 驱动 mongodb 实现读写分离,需要确保已经安装依赖:npm install mongodb。
3.1 基础连接与配置
首先建立副本集连接,指定副本集名称,后续操作根据类型设置不同的读偏好。
const { MongoClient } = require('mongodb');
// 副本集连接字符串,包含所有节点地址和副本集名称
const uri = 'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/test_db?replicaSet=rs0';
const client = new MongoClient(uri);
async function main() {
try {
// 建立数据库连接
await client.connect();
console.log('MongoDB 副本集连接成功');
const db = client.db('test_db');
const userCollection = db.collection('user');
// 后续读写操作在此处编写
} catch (err) {
console.error('操作异常:', err);
} finally {
// 实际业务中可根据需求决定是否关闭连接,长连接场景可保持连接不关闭
// await client.close();
}
}
main();3.2 写操作实现
写操作默认会路由到主节点,无需额外配置读偏好,直接执行插入、更新、删除等操作即可。
async function writeData(collection) {
try {
// 插入单条数据,写操作自动路由到主节点
const insertResult = await collection.insertOne name: '张三',
age: 25,
createTime: new Date()
});
console.log('写操作成功,插入数据ID:', insertResult.insertedId);
// 更新数据
const updateResult = await collection.updateOne(
{ name: '张三' },
{ $set: { age: 26 } }
);
console.log('更新操作影响条数:', updateResult.modifiedCount);
} catch (err) {
console.error('写操作异常:', err);
}
}3.3 读操作实现(路由到从节点)
读操作需要显式指定读偏好为 secondary 或 secondaryPreferred,实现读请求分发到从节点。
async function readDataFromSecondary(collection) {
try {
// 指定读偏好为 secondary,读请求路由到从节点
const readResult = await collection.find(
{ name: '张三' },
{ readPreference: 'secondary' } // 设置读偏好
).toArray();
console.log('从从节点读取到的数据:', readResult);
} catch (err) {
console.error('从从节点读操作异常:', err);
}
}
async function readDataFromSecondaryPreferred(collection) {
try {
// 指定读偏好为 secondaryPreferred,优先从从节点读取,从节点不可用时走主节点
const readResult = await collection.find(
{ age: { $gt: 20 } },
{ readPreference: 'secondaryPreferred' } // 设置读偏好
).toArray();
console.log('优先从从节点读取到的数据:', readResult);
} catch (err) {
console.error('secondaryPreferred 读操作异常:', err);
}
}3.4 完整调用示例
const { MongoClient } = require('mongodb');
const uri = 'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/test_db?replicaSet=rs0';
const client = new MongoClient(uri);
async function main() {
try {
await client.connect();
console.log('MongoDB 副本集连接成功');
const db = client.db('test_db');
const userCollection = db.collection('user');
// 执行写操作(主节点)
await writeData(userCollection);
// 等待从节点同步数据,实际业务中可根据需求调整等待逻辑或使用写关注保证同步
await new Promise(resolve => setTimeout(resolve, 1000));
// 执行读操作(从节点)
await readDataFromSecondary(userCollection);
// 执行优先从从节点读取的操作
await readDataFromSecondaryPreferred(userCollection);
} catch (err) {
console.error('操作异常:', err);
} finally {
await client.close();
}
}
async function writeData(collection) {
try {
const insertResult = await collection.insertOne({
name: '张三',
age: 25,
createTime: new Date()
});
console.log('写操作成功,插入数据ID:', insertResult.insertedId);
} catch (err) {
console.error('写操作异常:', err);
}
}
async function readDataFromSecondary(collection) {
try {
const readResult = await collection.find(
{ name: '张三' },
{ readPreference: 'secondary' }
).toArray();
console.log('从从节点读取到的数据:', readResult);
} catch (err) {
console.error('从从节点读操作异常:', err);
}
}
async function readDataFromSecondaryPreferred(collection) {
try {
const readResult = await collection.find(
{ age: { $gt: 20 } },
{ readPreference: 'secondaryPreferred' }
).toArray();
console.log('优先从从节点读取到的数据:', readResult);
} catch (err) {
console.error('secondaryPreferred 读操作异常:', err);
}
}
main();四、Python 实现读写分离示例
如果使用 Python 开发,可以使用 pymongo 驱动实现,先安装依赖:pip install pymongo。
from pymongo import MongoClient, ReadPreference
# 副本集连接,指定副本集名称
client = MongoClient(
'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/test_db?replicaSet=rs0'
)
db = client['test_db']
user_collection = db['user']
# 写操作,默认路由到主节点
insert_result = user_collection.insert_one({
'name': '李四',
'age': 30,
'createTime': '2024-01-01'
})
print(f'写操作成功,插入数据ID: {insert_result.inserted_id}')
# 读操作,指定读偏好为从节点
read_result = user_collection.find(
{'name': '李四'},
read_preference=ReadPreference.SECONDARY
).to_list(length=None)
print(f'从从节点读取到的数据: {read_result}')
# 优先从从节点读取的读操作
read_result2 = user_collection.find(
{'age': {'$gt': 20}},
read_preference=ReadPreference.SECONDARY_PREFERRED
).to_list(length=None)
print(f'优先从从节点读取到的数据: {read_result2}')
client.close()五、注意事项
1. 从节点数据存在同步延迟,如果业务需要强一致性读,不要使用从节点读偏好,应使用默认的 primary 模式。
2. 副本集节点故障时,MongoDB 会自动触发主节点选举,驱动会自动感知拓扑变化,无需手动调整连接配置。
3. 生产环境中建议根据实际情况选择合适的读偏好,比如对实时性要求不高的查询(如统计数据、历史记录查询)可以使用从节点读,核心交易类查询建议使用主节点读。
4. 连接字符串中可以配置更多参数,如连接超时时间、socket 超时时间等,可根据业务需求调整,参考官方文档 https://www.ipipp.com 查看完整的连接参数说明。