MONGO
permitted SchemaTypes
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
- Map
Valid schema options new Schema({...}, options}
- autoIndex #true
- autoCreate
- bufferCommands
- bufferTimeoutMS
- capped
- collection
- discriminatorKey
- id
- _id
- minimize
- read
- writeConcern
- shardKey
- strict
- strictQuery
- toJSON
- toObject
- typeKey
- useNestedStrict
- validateBeforeSave
- versionKey
- optimisticConcurrency
- collation
- timeseries
- selectPopulatedPaths
- skipVersioning
- timestamps
- storeSubdocValidationError
MongoDB connect
// MongoDB+srv connection string for cloud-based databases such as MongoDB Atlas
const uri = "mongodb+srv://username:password@cluster0.example.com/mydb";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
// Advanced Connection String Options
mongodb://username:password@host1:27017,host2:27018,host3:27019/mydb?replicaSet=myReplicaSet&authSource=admin&ssl=true&connectTimeoutMS=300000&socketTimeoutMS=30000&readPreference=primaryPreferred
// enable TLS on a MongoDB server
mongod --tlsMode requireTLS --tlsCertificateKeyFile /path/to/your/cert.pem
// check db size
use yourDatabaseName
var stats = db.stats()
print("Database Size: " + (stats.dataSize / (1024 * 1024 * 1024)).toFixed(2) + " GB"); // Database Size: 1.75 GB
print("Storage Size: " + (stats.storageSize / (1024 * 1024 * 1024)).toFixed(2) + " GB"); // Storage Size: 2.00 GB
// db.collection.stats()
{
size: 12345646,
totalIndexSize: 1345641561,
}
// user access
$ mongo
$ use admin
db.createUser({
user: "myAdminUser",
pwd: "myAdminPassword",
roles: [{ role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase"]
});
db.createUser({
user: "myReadOnlyUser",
pwd: "password",
roles: [
{ role: "read", db: "myDatabase" },
]
});
db.grantRolesToUser('myReadOnlyUser', [
{ role: 'readWrite', db: 'myDatabase' }
])
db.createRole({
role: 'readAndBackup',
privileges: [
{ resource: { db: 'myDatabase', collection: '' }, actions: [ 'find', 'backup' ] }
],
roles: []
})
db.grantRolesToUser('myReadOnlyUser', ['readAndBackup'])
db.revokeRolesFromUser('myReadOnlyUser', ['readWrite'])
// view user roles
$ use myDatabase
$ db.getUsers()
$ db.getRoles()
// change user password
$ db.changeUserPassword("root", "newpassword")
$ db.updateUser("root", {pwd: "newRootPassword"});
// then connect
$ mongo -u myAdminUser -p myAdminPassword --authenticationDatabase admin
// Constructing ObjectId with Specific Time
const date = new Date('2023-01-01T00:00:00Z');
const objectIdFromDate = ObjectId(Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000');
const timestamp = document._id.getTimestamp();
// returns JavaScript `Date` object
// advanced date query
db.collection.aggregate([
{
$project: {
year: { $year: '$date' },
month: { $month: '$date' },
day: { $dayOfMonth: '$date' },
hour: { $hour: '$date' }
}
},
{
$match: {
hour: 10
}
}
])
db.collection.findAndModify({
query: { <query> },
update: { <update> },
remove: <boolean>,
new: <boolean>,
upsert: <boolean>,
sort: { <fields> },
fields: { <fields> }
})
{
"_id": 1,
"title": "First Post",
"comments": [
{ "author": "Jane", "text": "Great post!" }
]
}
db.posts.findOneAndUpdate(
{ _id: 1, "comments.author": "Jane" },
{ $set: { "comments.$.author": "John" } },
{ returnNewDocument: true }
);
mongodb array operations
$push: Adds an element to an array.$addToSet: Adds an element to an array if it doesn’t exist already.$pull: Removes an element from an array.$pop: Removes the first or last element in an array.
mongodb index operation
| The ESR (Equality, Sort, Range) Rule
Single Field: Indexing individual fields in a document.Compound: Indexing multiple fields within a document.Multikey: Indexing fields that contain array values.Geospatial: Indexing geospatial data for location-based querying.Text: Indexing text for search functionality.
// create single field index
db.users.createIndex({ username: 1 })
// Verifying the Index
db.users.getIndexes()
// To check the size of your indexes
db.collection.totalIndexSize()
// 4617080000 // 4.3 gigabytes
// create compound index
db.users.createIndex({ username: 1, email: 1 })
//
Executing Mongodb file script & geneal management
// Access shell
mongo
// check configuration of the MongoDB server
db.serverCmdLineOpts()
// Load and execute the script
load("/path/to/your/queries.js")
// Passing Command-Line Arguments
mongo --nodb --shell --eval "var name='MongoDB'; var age=10" /path/to/your/queries_with_args.js
// Add a cron job that runs 'queries.js' every day at 3 a.m.
0 3 * * * mongo database_name /path/to/your/queries.js
// Stopping MongoDB on Unix-like systems
sudo service mongod stop
// Stopping MongoDB on Windows
net stop MongoDB
// Move the existing data to the new directory, ensuring permissions are adequate
sudo mv /data/db /new/directory/path
sudo chown -R mongodb:mongodb /new/directory/path
// restart with new dbpath
mongod --dbpath /new/directory/path
// For Windows, edit the MongoDB/config-file and adjust the dbpath value:
dbpath=C:\new\directory\path
// mv db by script
#!/bin/bash
stop_mongodb() {
echo "Stopping MongoDB service..."
sudo service mongod stop
}
move_data_files() {
echo "Moving MongoDB data files..."
sudo mv /data/db /new/directory/path
sudo chown -R mongodb:mongodb /new/directory/path
}
update_db_path() {
echo "Updating dbPath in MongoDB config..."
sudo sed -i 's|/data/db|/new/directory/path|g' /etc/mongod.conf
}
start_mongodb() {
echo "Starting MongoDB service with new dbPath..."
sudo service mongod start
}
stop_mongodb
move_data_files
update_db_path
start_mongodb
echo "MongoDB data migration completed."
Sync to ES
replica configuration
rs.status()
rs.reconfig({ _id: "rs0", members: [{ _id: 0, host: "192.168.1.9:8094" }, { _id: 1, host: "192.168.1.9:8095" }, { _id: 2, host: "192.168.1.9:8096" }]}, { force: true })
mongodump & mongorestore on a replica set
mongorestore --uri="mongodb://192.168.0.58:8054,192.168.0.58:8055,192.168.0.58:8056/dbanme?replicaSet=rs0" --gzip dump/
# to restore single collection
mongorestore --db mydbname --collection mycollection dump/mydbname/mycollection.bson
mongodump --uri="mongodb://<db_user>:<db_passwd>@106.14.254.1:8054,106.14.254.1:8055,106.14.254.1:8056/dbanme?replicaSet=rs0" --collection=invoices --out=dump
db transaction
const session = db.getMongo().startSession()
session.startTransaction()
try {
db.collection("users").insertOne({ name: 'John Doe', age: 30 })
session.commitTransaction()
} catch (error) {
session.abortTransaction()
throw error
} finally {
session.endTransaction()
}
[db operate]
# login
mongosh <dbname> -u uname -p 'password' --authenticationDatabase <dbname>
# drop database
mongo <dbname> --eval "db.dropDatabase()"
# in mongodb console
> use mydb;
> db.dropDatabase();
docker-compose
version: "3.6"
services:
mongo1:
image: "mongo:4.2"
container_name: mongo1
hostname: mongo1.example.com
command: ["--replSet", "rs", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
ports:
- 8031:27017
volumes:
- /mnt/ssd/mongo-replica/mongod.conf:/etc/mongod.conf
- /mnt/ssd/mongo-replica/node1:/data/db
networks:
my-net:
ipv4_address: 172.16.8.6
mongo2:
image: "mongo:4.2"
container_name: mongo2
hostname: mongo2.example.com
command: ["--replSet", "rs", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
ports:
- 8032:27017
volumes:
- /mnt/ssd/mongo-replica/mongod.conf:/etc/mongod.conf
- /mnt/ssd/mongo-replica/node2:/data/db
networks:
my-net:
ipv4_address: 172.16.8.7
mongo3:
image: "mongo:4.2"
container_name: mongo3
hostname: mongo3.example.com
command: ["--replSet", "rs", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
ports:
- 8033:27017
volumes:
- /mnt/ssd/mongo-replica/mongod.conf:/etc/mongod.conf
- /mnt/ssd/mongo-replica/node3:/data/db
networks:
my-net:
ipv4_address: 172.16.8.8
postgres:
image: postgres:alpine
container_name: postgres
environment:
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
- POSTGRES_DB=$POSTGRES_DB
- POSTGRES_USER=$POSTGRES_USER
restart: always
volumes:
- /mnt/ssd/postgres:/var/lib/postgresql/data
ports:
- "15432:5432"
networks:
my-net:
ipv4_address: 172.16.8.9
networks:
my-net:
name: "net_optimize"
ipam:
driver: default
config:
- subnet: "172.16.8.0/24"
Backup & Restore
#!/bin/bash
# ================= 配置区域 =================
# 1. 宿主机备份保存路径 (请修改为你实际的路径)
BACKUP_DIR="/opt/backup/mongodb"
# 2. 容器名称 (对应 docker-compose.yml 中的 container_name)
CONTAINER_NAME="mongodb_prod"
# 3. 数据库连接信息
# 如果要备份所有库,请留空 DB_NAME="",并去掉下文命令中的 --db 选项
DB_NAME="my_database_name"
DB_USER="admin"
DB_PASS="YourSuperStrongPassword123!"
# 4. 保留天数
RETENTION_DAYS=30
# ===========================================
# 获取当前日期,用于生成唯一文件名
DATE=$(date +%Y%m%d_%H%M%S)
# 确保备份目录存在
mkdir -p "$BACKUP_DIR"
# 定义备份文件名
if [ -z "$DB_NAME" ]; then
FILE_NAME="mongo_all_${DATE}.gz"
else
FILE_NAME="mongo_${DB_NAME}_${DATE}.gz"
fi
# 完整的备份文件路径
ARCHIVE_PATH="$BACKUP_DIR/$FILE_NAME"
echo "[$(date)] 开始备份数据库: ${DB_NAME:-ALL} ..."
# 执行备份命令
# 解释:
# docker exec: 在容器内执行命令
# mongodump --archive --gzip: 直接输出压缩后的归档流,而不是生成文件夹
# > "$ARCHIVE_PATH": 将容器输出的流直接写入宿主机文件
if [ -z "$DB_NAME" ]; then
# 备份所有数据库
docker exec "$CONTAINER_NAME" \
mongodump --username "$DB_USER" --password "$DB_PASS" \
--authenticationDatabase admin \
--archive --gzip > "$ARCHIVE_PATH"
else
# 备份指定数据库
docker exec "$CONTAINER_NAME" \
mongodump --username "$DB_USER" --password "$DB_PASS" \
--authenticationDatabase admin \
--db "$DB_NAME" \
--archive --gzip > "$ARCHIVE_PATH"
fi
# 检查备份是否成功 (检查文件大小是否大于0)
if [ -s "$ARCHIVE_PATH" ]; then
echo "[$(date)] 备份成功! 文件保存路径: $ARCHIVE_PATH"
else
echo "[$(date)] 备份失败! 文件为空。"
rm -f "$ARCHIVE_PATH"
exit 1
fi
# ================= 清理旧文件 =================
echo "[$(date)] 开始清理 ${RETENTION_DAYS} 天前的旧备份..."
# find 命令解释:
# $BACKUP_DIR: 查找目录
# -name "*.gz": 只查找 .gz 结尾的文件 (防止误删)
# -type f: 只查找文件
# -mtime +$RETENTION_DAYS: 修改时间超过指定天数
# -delete: 直接删除
find "$BACKUP_DIR" -name "*.gz" -type f -mtime +$RETENTION_DAYS -delete
echo "[$(date)] 清理完成。"
echo "----------------------------------------------------"
设置自动化定时任务 (Crontab)
# 打开当前用户的定时任务编辑器
crontab -e
# 在文件末尾添加以下行(请确保路径是绝对路径)
# 每天凌晨 3:00 执行备份,并将日志输出到 backup.log
0 3 * * * /path/to/your/mongo_backup.sh >> /path/to/your/backup.log 2>&1
恢复指定的文件到容器中
# 注意:--drop 表示恢复前删除目标库中已存在的同名集合(慎用,根据需求决定)
docker exec -i mongodb_prod \
mongorestore --username admin --password YourSuperStrongPassword123! \
--authenticationDatabase admin \
--db my_database_name \
--drop \
--gzip \
--archive < /opt/backup/mongodb/mongo_backup_20231126.gz
自动同步至阿里云OSS
- 第一步:安装并配置 ossutil (只需执行一次)
# 1. 下载 ossutil
wget https://gosspublic.alicdn.com/ossutil/1.7.16/ossutil64
# 2. 赋予执行权限
chmod 755 ossutil64
# 3. 移动到系统路径 (这样在脚本里就能直接用 'ossutil64' 命令了)
sudo mv ossutil64 /usr/bin/ossutil
# 4. 配置你的 OSS 账号 (交互式配置,需输入 endpoint, accessKeyID, secret)
ossutil config
- 第二步:更新备份脚本 (mongo_backup_oss.sh)
#!/bin/bash
# ================= 基础配置区域 =================
# 1. 宿主机备份保存路径
BACKUP_DIR="/opt/backup/mongodb"
# 2. 容器名称
CONTAINER_NAME="mongodb_prod"
# 3. 数据库连接信息
DB_NAME="my_database_name" # 留空 "" 则备份所有库
DB_USER="admin"
DB_PASS="YourSuperStrongPassword123!"
# 4. 本地保留天数 (超过这几天的本地文件会被删除)
RETENTION_DAYS=30
# ================= 阿里云 OSS 配置区域 =================
# 5. 是否开启 OSS 上传 (true/false)
ENABLE_OSS_UPLOAD=true
# 6. OSS Bucket 路径 (格式: oss://你的Bucket名称/目录/)
# 注意:末尾最好带上 /,表示这是一个目录
OSS_TARGET_DIR="oss://my-backup-bucket/mongo-backups/"
# 7. OSS 配置文件路径 (默认是 ~/.ossutilconfig,一般不用改)
OSS_CONFIG_FILE="$HOME/.ossutilconfig"
# ====================================================
# 获取当前日期
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# 定义文件名
if [ -z "$DB_NAME" ]; then
FILE_NAME="mongo_all_${DATE}.gz"
else
FILE_NAME="mongo_${DB_NAME}_${DATE}.gz"
fi
ARCHIVE_PATH="$BACKUP_DIR/$FILE_NAME"
echo "----------------------------------------------------"
echo "[$(date)] 任务开始: 备份数据库 ${DB_NAME:-ALL}"
# 1. 执行数据库备份
if [ -z "$DB_NAME" ]; then
docker exec "$CONTAINER_NAME" \
mongodump --username "$DB_USER" --password "$DB_PASS" \
--authenticationDatabase admin \
--archive --gzip > "$ARCHIVE_PATH"
else
docker exec "$CONTAINER_NAME" \
mongodump --username "$DB_USER" --password "$DB_PASS" \
--authenticationDatabase admin \
--db "$DB_NAME" \
--archive --gzip > "$ARCHIVE_PATH"
fi
# 2. 检查本地备份是否成功
if [ -s "$ARCHIVE_PATH" ]; then
echo "[$(date)] 本地备份成功: $ARCHIVE_PATH"
else
echo "[$(date)] 错误: 备份文件为空或生成失败!"
rm -f "$ARCHIVE_PATH"
exit 1
fi
# 3. 上传到阿里云 OSS
if [ "$ENABLE_OSS_UPLOAD" = true ]; then
echo "[$(date)] 开始上传到阿里云 OSS..."
# 检查 ossutil 是否安装
if ! command -v ossutil &> /dev/null; then
echo "[$(date)] 错误: 未找到 ossutil 命令,跳过上传。请先安装 ossutil。"
else
# 执行上传命令
# -c: 指定配置文件
# -f: 强制覆盖(虽然文件名带时间戳一般不会重复)
ossutil cp "$ARCHIVE_PATH" "$OSS_TARGET_DIR" -c "$OSS_CONFIG_FILE" -f
# 检查上传返回值 ($? 为 0 表示成功)
if [ $? -eq 0 ]; then
echo "[$(date)] OSS 上传成功: ${OSS_TARGET_DIR}${FILE_NAME}"
else
echo "[$(date)] 错误: OSS 上传失败!请检查网络或配置。"
# 这里可以选择是否退出,或者继续执行清理逻辑
# exit 1
fi
fi
else
echo "[$(date)] OSS 上传已关闭,跳过。"
fi
# 4. 清理本地旧文件
echo "[$(date)] 检查并清理 ${RETENTION_DAYS} 天前的本地旧备份..."
find "$BACKUP_DIR" -name "*.gz" -type f -mtime +$RETENTION_DAYS -delete
echo "[$(date)] 任务全部完成。"
echo "----------------------------------------------------"
- 脚本里的 RETENTION_DAYS=30 只会删除本地服务器上的旧文件。
- OSS 上的文件会一直增加。强烈建议去阿里云 OSS 控制台 -> Bucket -> 基础设置 -> 生命周期规则,设置一条规则:“自动删除 30 天前的 Object”,这样云端也会自动清理,不用写脚本去删云端文件。
开启单节点副本集(支持事务)
开启 MongoDB 的单节点副本集 (Single Node Replica Set) 是支持事务 (Transactions) 的前提条件。
虽然只有一个节点,但为了满足 MongoDB 对副本集安全性的要求(节点间通信认证),我们需要额外配置一个 KeyFile(密钥文件),即使只有一个节点,这也是开启 --auth 和 --replSet 并存时的必要步骤。
以下是具体的操作步骤:
第一步:生成 KeyFile (密钥文件)
在宿主机(存放 docker-compose.yml 的目录)执行以下命令。这会生成一个包含随机内容的密钥文件,并设置正确的权限。
⚠️ 权限非常重要:KeyFile 的权限必须是 400 (只读),且文件拥有者必须是 MongoDB 用户 (通常 ID 为 999),否则容器会启动失败。
# 1. 生成随机密钥内容
openssl rand -base64 756 > mongo-keyfile
# 2. 修改文件权限 (必须是 600 或 400)
chmod 400 mongo-keyfile
# 3. 修改文件所有者 (Docker 容器内 Mongo 用户 ID 通常是 999)
# 注意:如果你在 Mac 或 Windows 上,这一步可能需要跳过或通过 Docker 内部执行 chown
sudo chown 999:999 mongo-keyfile
第二步:修改 docker-compose.yml
我们需要做三件事:
- 挂载 KeyFile 到容器。
- 修改启动命令,添加
--replSet和--keyFile。 - 增加 hosts 映射(可选,但在某些驱动程序连接时有帮助)。
version: '3.8'
services:
mongodb:
image: mongo:7.0
container_name: mongodb_prod
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: admin
ports:
- "27017:27017"
volumes:
- ./mongo_data:/data/db
- /etc/localtime:/etc/localtime:ro
# [新增] 挂载密钥文件到容器内部,并设为只读
- ./mongo-keyfile:/opt/keyfile/mongo-keyfile:ro
# [修改] 启动命令:
# --replSet rs0: 开启副本集,名字叫 rs0
# --keyFile ...: 指定密钥文件路径
# --bind_ip_all: 允许外部连接
command: ["mongod", "--replSet", "rs0", "--keyFile", "/opt/keyfile/mongo-keyfile", "--bind_ip_all"]
# [新增] 确保容器内部能解析自己的主机名 (生产环境推荐)
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
第三步:初始化副本集 (只需执行一次)
修改配置并重新启动容器后,MongoDB 已经运行在副本集模式下,但它还不知道自己是谁,也没有被“初始化”。你需要手动执行一条命令来激活它。
重启容器:
docker-compose down docker-compose up -d进入容器并初始化: 执行以下单行命令,它会登录数据库并运行
rs.initiate()。(请将
YourSuperStrongPassword123!替换为你.env文件里的真实密码)docker exec -it mongodb_prod mongosh -u admin -p YourSuperStrongPassword123! --authenticationDatabase admin --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })'如果成功,你会看到输出
{ "ok" : 1 }。注意:初始化后,你可能需要等待 10-20 秒,按回车键,你会发现命令行提示符从
>变成了rs0 [DIRECT: PRIMARY] >,这代表当前节点已经变成主节点 (Primary)。
第四步:验证与连接
1. 验证状态
你可以进入容器查看副本集状态:
docker exec -it mongodb_prod mongosh -u admin -p YourSuperStrongPassword123! --authenticationDatabase admin --eval "rs.status()"
2. 更新连接字符串 (Connection String)
现在你的 MongoDB 支持事务了。在你的应用程序代码中,连接字符串最好加上 replicaSet 参数:
mongodb://admin:密码@你的IP:27017/你的库名?authSource=admin&replicaSet=rs0
常见问题排查
Error: Permissions on /opt/keyfile/mongo-keyfile are too open: 这说明第一步的权限没设置好。MongoDB 要求 KeyFile 权限必须极其严格。
- 解决方法:在宿主机执行
sudo chmod 400 mongo-keyfile和sudo chown 999:999 mongo-keyfile。如果是在 Windows Docker Desktop 上,文件权限映射比较麻烦,建议通过 DockerfileCOPY进去并RUN chmod,或者在 Linux 虚拟机/WSL2 中操作。
- 解决方法:在宿主机执行
Transaction numbers are only allowed on a replica set: 这说明你虽然配置了,但没有执行第三步的
rs.initiate()。
nodejs 正确处理mongodb事务
在 Node.js 中使用 MongoDB 事务,最推荐的方式是使用 withTransaction 辅助函数。
它不仅代码更简洁,而且自动处理了重试逻辑(例如:遇到瞬时的网络抖动或临时的写入冲突时,它会自动重试提交,而不需要你写复杂的 try-catch 循环)。
以下是基于 Mongoose(Node.js 最常用的 ODM)的生产级代码示例。
1. 场景假设
假设我们要实现一个经典的 “电商下单” 场景,需要同时完成两件事:
- 创建订单 (Orders)
- 扣减库存 (Products)
如果扣减库存失败(比如库存不足),订单必须回滚,不能创建。
2. Mongoose 完整代码示例
const mongoose = require('mongoose');
const { Schema } = mongoose;
// --- 1. 定义模型 (简化版) ---
const ProductSchema = new Schema({
name: String,
stock: Number
});
const OrderSchema = new Schema({
productName: String,
quantity: Number,
status: String
});
const Product = mongoose.model('Product', ProductSchema);
const Order = mongoose.model('Order', OrderSchema);
// --- 2. 事务核心逻辑 ---
async function createOrderWithTransaction(productId, quantity) {
// A. 开启一个会话 (Session)
const session = await mongoose.startSession();
try {
// B. 使用 withTransaction (推荐方式)
// 这个回调函数内的所有操作,要么全成功,要么全回滚
await session.withTransaction(async () => {
// -------------------------------------------------------
// 步骤 1: 扣减库存
// -------------------------------------------------------
// ⚠️ 关键点 1: 必须在查询/更新操作中传入 { session }
const updateResult = await Product.updateOne(
{ _id: productId, stock: { $gte: quantity } }, // 确保库存充足
{ $inc: { stock: -quantity } }
).session(session); // Mongoose 语法:链式调用 .session()
if (updateResult.modifiedCount === 0) {
// 如果没有修改成功,说明库存不足或产品不存在
// 抛出错误会触发 withTransaction 的自动回滚
throw new Error('库存不足或产品不存在,交易终止');
}
// -------------------------------------------------------
// 步骤 2: 创建订单
// -------------------------------------------------------
// ⚠️ 关键点 2: Model.create 也是支持事务的
// 注意:在事务中,create 第一个参数建议传数组 [{...}]
// 第二个参数传 { session } 选项
await Order.create([{
productName: 'iPhone 15',
quantity: quantity,
status: 'created'
}], { session: session });
console.log('--- 事务内部逻辑执行完成,准备提交 ---');
});
// 代码运行到这里,说明事务已自动 commit 成功
console.log('✅ 订单创建成功,库存已扣减');
} catch (error) {
// 代码运行到这里,说明事务已自动 abort (回滚)
console.error('❌ 交易失败:', error.message);
// 这里可以做额外的错误处理,比如返回给前端错误信息
} finally {
// C. 无论成功失败,必须结束会话
await session.endSession();
}
}
// --- 3. 连接并测试 ---
async function run() {
await mongoose.connect('mongodb://admin:YourPass@localhost:27017/shop?authSource=admin&replicaSet=rs0');
// 初始化一个产品用于测试
const p = await Product.create({ name: 'iPhone 15', stock: 10 });
console.log(`初始库存: 10`);
// 尝试购买 2 个
await createOrderWithTransaction(p._id, 2);
// 再次查询库存
const currentP = await Product.findById(p._id);
console.log(`当前库存: ${currentP.stock}`);
}
run();
3. 三个致命的“坑” (务必注意)
在使用 MongoDB 事务时,新手最容易犯这三个错误,导致事务失效(即数据依然被部分写入了):
- 忘记传递 Session:
* **错误写法**:`await Product.updateOne(...)` (这会直接写入数据库,不受事务控制!)
* **正确写法**:`await Product.updateOne(...).session(session)`
* **Mongoose 规则**:
* 查询/更新操作:`.session(session)`
* `save()` 操作:`doc.save({ session })`
* `create()` 操作:`Model.create([docs], { session })` (注意 create 最好包裹在数组里)
- 没有创建集合:
* MongoDB 的事务**不能**在事务内创建新的 Collection(表)。
* **解决方法**:在代码启动时,或者部署时,确保相关的 Collection 已经存在。如果 `Orders` 表不存在,事务执行到 `Order.create` 时会报错。
- 连接字符串未加 ReplicaSet:
* 你的 URI 必须包含 `replicaSet=rs0`(或你设置的名字),否则驱动程序不知道这是一个副本集,无法开启 Session。
4. 如果使用原生 MongoDB Driver (非 Mongoose)
如果你不使用 Mongoose,而是使用官方的 mongodb 包,逻辑是一样的,只是 API 传参方式略有不同:
const { MongoClient } = require('mongodb');
async function runNative() {
const client = new MongoClient('mongodb://admin:pass@localhost:27017/?replicaSet=rs0');
await client.connect();
const session = client.startSession();
try {
await session.withTransaction(async () => {
const db = client.db('shop');
const products = db.collection('products');
const orders = db.collection('orders');
// 原生驱动要在 options 对象里明确传入 session
await products.updateOne(
{ _id: 1 },
{ $inc: { stock: -1 } },
{ session } // <--- 这里
);
await orders.insertOne(
{ item: 'apple', qty: 1 },
{ session } // <--- 这里
);
});
console.log("事务成功");
} finally {
await session.endSession();
await client.close();
}
}
慢查询
MongoDB 慢查询 (Slow Query) 是指执行时间超过了预设阈值(默认通常是 100 毫秒)的操作。
简单来说,就是数据库里的**“拖油瓶”**。它们不仅导致当前请求变慢,严重时还会占用过多的 CPU 和 IO 资源,导致整个数据库卡顿甚至宕机。
以下是关于 MongoDB 慢查询的深度解析,包括概念、原理、适用场景及处理方法。
1. 核心概念
什么是“慢”?
“慢”是一个相对概念。在 MongoDB 中,你可以定义“超过多少毫秒算慢”。
- 默认阈值:100ms。
- 配置参数:
slowOpThresholdMs。
谁在记录?
MongoDB 有一个内置的分析器叫 Database Profiler。它有三个级别:
- Level 0:关闭(默认)。
- Level 1:只记录慢查询(生产环境推荐)。
- Level 2:记录所有查询(仅用于调试,生产开启会严重拖慢性能)。
当开启 Level 1 时,所有超过阈值的操作都会被记录到 system.profile 集合(这是一个特殊的固定集合 capped collection)以及 MongoDB 的日志文件 (mongod.log) 中。
2. 为什么会出现慢查询?(常见原因)
- 全表扫描 (COLLSCAN):
- 最常见的原因。查询条件没有命中索引,导致 MongoDB 必须逐行检查每一条数据。
- 比喻:在一本没有目录的书中找一句话,只能从第一页翻到最后一页。
- 内存排序 (Sort without Index):
- 查询结果很大,且排序字段没有索引。MongoDB 必须在内存中进行排序,如果数据量超过 32MB(默认限制),查询直接报错或极慢。
- 返回数据量过大:
- 一次性查询几十万条数据,或者单条文档体积太大(MB 级别),网络传输和反序列化耗时久。
- 锁争用:
- 高并发写入时,写锁(Write Lock)阻塞了读操作。
3. 适用场景 (什么时候关注它?)
慢查询分析在以下场景中至关重要:
A. 生产环境性能监控 (Routine Monitoring)
- 场景:API 接口响应偶尔变慢,或者用户投诉加载圈一直在转。
- 作用:通过查看慢查询日志,快速定位是哪一条 SQL(MongoDB Query)拖慢了接口。通常能发现 90% 的性能问题都是因为漏建索引。
B. CPU 飙升排查 (CPU Spikes)
- 场景:MongoDB 服务器 CPU 突然飙升到 90% - 100%。
- 作用:CPU 高通常不是因为并发量大,而是因为出现了“烂查询”。一条全表扫描就可能吃掉一个 CPU 核心。通过
db.currentOp()或慢查询日志,找到那个正在消耗 CPU 的语句并 kill 掉。
C. 新功能上线验收 (Code Review)
- 场景:开发发布了新版本代码,涉及新的查询逻辑。
- 作用:上线初期密切关注慢查询日志。如果新功能触发了大量的慢查询,说明开发人员可能写了低效的查询代码,或者忘记加索引,需要立即回滚或热修复。
D. 容量规划 (Capacity Planning)
- 场景:查询已经优化到了极致(都走了索引),但依然很慢。
- 作用:这表明单机性能到达瓶颈,需要考虑升级硬件(更多 RAM)或者进行分片(Sharding)。
4. 实战:如何捕获和分析
第一步:开启慢查询记录
在生产环境,我们通常设置为记录超过 100ms 的操作。
// 在 mongosh 中执行
// 1. 设置慢查询阈值为 100ms
db.setProfilingLevel(1, { slowms: 100 })
// 2. 确认设置
db.getProfilingStatus()
第二步:查找慢查询
你可以直接查询 system.profile 集合:
// 查询最近 10 条慢查询,按时间倒序
db.system.profile.find().sort({$natural:-1}).limit(10).pretty()
第三步:读懂日志 (关键指标)
当你看到一条慢查询记录时,重点关注以下字段:
| 字段 | 含义 | 警报信号 |
|---|---|---|
op |
操作类型 (query, update, remove...) | - |
ns |
命名空间 (数据库.集合) | 确认是哪个表 |
millis |
耗时 (毫秒) | 越高越严重 |
planSummary |
执行计划摘要 | 出现 COLLSCAN (全表扫描) 必死;理想是 IXSCAN (索引扫描) |
docsExamined |
扫描了多少个文档 | 如果扫描数 >> 返回数 (nreturned),说明索引效率低 |
keysExamined |
扫描了多少个索引键 | - |
5. 解决方案示例
假设你发现了一条慢查询:
db.users.find({ age: 25, city: "Beijing" })
日志显示 planSummary: COLLSCAN,耗时 500ms。
解决步骤:
- 分析:字段
age和city没有索引,导致全表扫描。 - 验证:使用
explain()确认。javascript db.users.find({ age: 25, city: "Beijing" }).explain("executionStats") - 优化:创建复合索引。
javascript // 遵循最左前缀原则 db.users.createIndex({ age: 1, city: 1 }) - 复查:再次运行查询,耗时应该降至 <10ms,且
planSummary变为IXSCAN。
Page Source