MONGO

permitted SchemaTypes

Valid schema options new Schema({...}, options}

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

mongodb index operation

| The ESR (Equality, Sort, Range) Rule

// 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

# 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
#!/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 "----------------------------------------------------"

开启单节点副本集(支持事务)

开启 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

我们需要做三件事:

  1. 挂载 KeyFile 到容器。
  2. 修改启动命令,添加 --replSet--keyFile
  3. 增加 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 已经运行在副本集模式下,但它还不知道自己是谁,也没有被“初始化”。你需要手动执行一条命令来激活它。

  1. 重启容器:

    docker-compose down
    docker-compose up -d
    
  2. 进入容器并初始化: 执行以下单行命令,它会登录数据库并运行 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

常见问题排查


nodejs 正确处理mongodb事务

在 Node.js 中使用 MongoDB 事务,最推荐的方式是使用 withTransaction 辅助函数。

它不仅代码更简洁,而且自动处理了重试逻辑(例如:遇到瞬时的网络抖动或临时的写入冲突时,它会自动重试提交,而不需要你写复杂的 try-catch 循环)。

以下是基于 Mongoose(Node.js 最常用的 ODM)的生产级代码示例。


1. 场景假设

假设我们要实现一个经典的 “电商下单” 场景,需要同时完成两件事:

  1. 创建订单 (Orders)
  2. 扣减库存 (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 事务时,新手最容易犯这三个错误,导致事务失效(即数据依然被部分写入了):

  1. 忘记传递 Session
  * **错误写法**:`await Product.updateOne(...)` (这会直接写入数据库,不受事务控制!)
  * **正确写法**:`await Product.updateOne(...).session(session)`
  * **Mongoose 规则**:
      * 查询/更新操作:`.session(session)`
      * `save()` 操作:`doc.save({ session })`
      * `create()` 操作:`Model.create([docs], { session })` (注意 create 最好包裹在数组里)
  1. 没有创建集合
  * MongoDB 的事务**不能**在事务内创建新的 Collection(表)。
  * **解决方法**:在代码启动时,或者部署时,确保相关的 Collection 已经存在。如果 `Orders` 表不存在,事务执行到 `Order.create` 时会报错。
  1. 连接字符串未加 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 中,你可以定义“超过多少毫秒算慢”。

谁在记录?

MongoDB 有一个内置的分析器叫 Database Profiler。它有三个级别:

当开启 Level 1 时,所有超过阈值的操作都会被记录到 system.profile 集合(这是一个特殊的固定集合 capped collection)以及 MongoDB 的日志文件 (mongod.log) 中。


2. 为什么会出现慢查询?(常见原因)

  1. 全表扫描 (COLLSCAN)
    • 最常见的原因。查询条件没有命中索引,导致 MongoDB 必须逐行检查每一条数据。
    • 比喻:在一本没有目录的书中找一句话,只能从第一页翻到最后一页。
  2. 内存排序 (Sort without Index)
    • 查询结果很大,且排序字段没有索引。MongoDB 必须在内存中进行排序,如果数据量超过 32MB(默认限制),查询直接报错或极慢。
  3. 返回数据量过大
    • 一次性查询几十万条数据,或者单条文档体积太大(MB 级别),网络传输和反序列化耗时久。
  4. 锁争用
    • 高并发写入时,写锁(Write Lock)阻塞了读操作。

3. 适用场景 (什么时候关注它?)

慢查询分析在以下场景中至关重要:

A. 生产环境性能监控 (Routine Monitoring)

B. CPU 飙升排查 (CPU Spikes)

C. 新功能上线验收 (Code Review)

D. 容量规划 (Capacity Planning)


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。

解决步骤:

  1. 分析:字段 agecity 没有索引,导致全表扫描。
  2. 验证:使用 explain() 确认。 javascript db.users.find({ age: 25, city: "Beijing" }).explain("executionStats")
  3. 优化:创建复合索引。 javascript // 遵循最左前缀原则 db.users.createIndex({ age: 1, city: 1 })
  4. 复查:再次运行查询,耗时应该降至 <10ms,且 planSummary 变为 IXSCAN


Page Source