Skip to content

1. 引言

MongoDB在4.0版本引入了对多文档事务的支持,这一特性使得开发者能够在一个事务中操作多个文档和集合,确保这些操作的原子性和一致性。这对于构建复杂的应用程序和保证数据完整性至关重要。

什么是事务

事务是数据库一系列操作的集合,视为一个逻辑单元。当全部操作成功时,事务才算操作成功;如果其中某一操作失败,则视为整体失败,数据会回滚到未执行前的状态。

假设某玩家消费100金币买了一个装备

sql
BEGIN TRANSACTION;
-- 开始事务,一系列操作

-- 账户金币减掉100
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 装备库存减1
UPDATE products SET stock = stock - 1 WHERE product_id = 456 AND stock > 0;
-- 用户装备增加
INSERT INTO weapons (account_id, product_id) VALUES (1, 456);

-- 提交事务
COMMIT;

如果玩家的金币扣了,但装备已经没有库存,购买失败,则整个操作失败,扣掉的金币要还回去。

ACID特性

ACID是数据库事务四个重要特征

  1. 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不执行。如果事务在执行过程中遇到错误,所有已经执行的操作都必须回滚到事务开始之前的状态。这确保了数据库不会处于部分更新的状态。 例如:一个银行转账操作,如果扣款成功但未成功入账,整个操作会被回滚,账户余额恢复到初始状态。
  2. 一致性(Consistency):事务执行前后,数据库都必须保持一致的状态。所有的业务规则、约束和触发器都必须得到满足,确保数据库从一个有效状态转变到另一个有效状态。 例如:转账操作前后,账户总余额应保持不变。
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的操作对其他事务是隔离的。事务的中间状态对其他事务是不可见的,只有在事务提交后,其结果才会对其他事务可见。 例如:一个用户在读取数据时不会看到其他未提交事务的修改。
  4. 持久性(Durability):一旦事务提交,它对数据库的改变是永久性的,即使系统发生故障,也不会丢失。事务的结果会被持久地保存到存储介质中。 例如:已完成的转账记录在系统重启后仍然存在。

2. MongoDB事务简介

MongoDB在4.0版本引入了对多文档事务的支持,使开发者能够在同一个事务中操作多个文档和集合,保证这些操作的原子性和一致性。

历史版本

  • MongoDB 3.0及之前:仅支持单文档级别的原子操作,不支持多文档事务
  • MongoDB 4.0:引入了副本集环境下的多文档事务支持
  • MongoDB 4.2:扩展了对分片集群环境下的多文档事务支持。

3. 使用事务

启动和提交事务

  • 启动会话:MongoClient.startSession()
  • 启动事务:在会话对象上调用 startTransaction() 方法来启动事务
  • 执行操作:在会话中执行一系列数据库操作(插入、更新、删除等)
  • 提交事务:在成功完成所有操作后调用 session.commitTransaction() 提交事务
  • 回滚事务:如果遇到错误,调用 session.abortTransaction() 回滚事务
  • 结束会话:session.endSession()

示例代码

以下代码演示了如何在MongoDB中使用事务

js
try {
    await client.connect();
    const session = client.startSession();
    session.startTransaction();

    try {
        const accountsCollection = client.db("test").collection("accounts");
        const productsCollection = client.db("test").collection("products");
        const weaponsCollection = client.db("test").collection("weapons");

        // 扣金币操作
        const accountUpdateResult = await accountsCollection.updateOne(
            { account_id: 1 },
            { $inc: { balance: -100 } },
            { session }
        );

        if (accountUpdateResult.matchedCount === 0 || accountUpdateResult.modifiedCount === 0) {
            throw new Error("Account update failed or no account found.");
        }

        // 减少库存
        const productUpdateResult = await productsCollection.updateOne(
            { product_id: 456, stock: { $gt: 0 } },
            { $inc: { stock: -1 } },
            { session }
        );

        if (productUpdateResult.matchedCount === 0 || productUpdateResult.modifiedCount === 0) {
            throw new Error("Product update failed or no product found.");
        }

        // 增加用户装备
        const weaponInsertResult = await weaponsCollection.insertOne(
            { account_id: 1, product_id: 456 },
            { session }
        );

        if (weaponInsertResult.insertedCount === 0) {
            throw new Error("Weapon insert failed.");
        }

        await session.commitTransaction();
        console.log("Transaction committed.");
    } catch (error) {
        await session.abortTransaction();
        console.log("Transaction aborted due to an error: ", error);
    } finally {
        session.endSession();
    }

4. 锁的基本概念

事务的实现离不开锁的作用,数据库的锁是一种机制,用于控制数据并发访问时,保持数据的一致性和完整性,防止并发读写数据造成的竞争和冲突

什么是锁

锁是数据库中用于管理并发访问和维护数据一致性的一种机制。通过锁,可以确保多个事务在并发执行时不会相互干扰,从而保证数据的一致性和完整性。

图书馆里来了一些新书,好些人去借书,当你在书架一行行的找时,突然来了个人,一下就拿走了几本,碰巧就有你找的那本书,于是你把那人揍了一顿。

第二天图书馆发布了新规矩,一个书架只能同时一个人能借书,排队拿钥匙,有钥匙的人可去找书,不管有没找到,其他人不能进去找,只有等到钥匙归还后,第二人才能接着去找书。

这就是锁,避免了冲突的发生,但同时也带来了性能的问题。

锁的分类方式多种多样,主要根据锁定粒度、锁的模式、持有时间、特性、作用范围和实现机制等因素进行分类。

MongoDB支持锁的模式

  1. 共享锁(S):用于读操作,允许多个事务并发读取。
  2. 排他锁(X):用于写操作,阻止其他事务读取或写入。
  3. 意向共享锁(IS):用于表示有读取的意图,允许多个意向共享锁和意向排他锁。
  4. 意向排他锁(IX):用于表示有写入的意图,允许多个意向排他锁,阻止共享锁和排他锁。

锁的影响

优点:

  • 锁的设计保证了数据的一致性和完整性,并能有效控制并发操作。

缺点:

  • 性能开销,上锁和解锁需要时间,在高并发下,多个事务争抢相同资源,会增加等待时间,降低性能
  • 资源开销,维护锁的信息,即需要消耗内存资源,也需要CPU资源
  • 死锁,多个事务在相互等待对方释放锁时,则会造成死锁

5. 事务的其它依赖

锁只是事务实现的一部分,完整实现还依赖于日志记录、版本控制等其他机制。

日志记录(Write-Ahead Logging, WAL)

日志记录是指在事务对数据库进行修改之前,先将这些修改操作记录到日志中。这样,即使在系统崩溃后,数据库也可以根据日志来恢复未完成的事务,确保数据的一致性和持久性。

作用

  • 原子性:如果事务失败,可以使用日志回滚操作,将数据库恢复到事务开始之前的状态。
  • 持久性:一旦事务提交,其日志记录会被持久化到磁盘,即使系统崩溃,事务的结果也不会丢失。

版本控制(Multi-Version Concurrency Control, MVCC)

通过维护数据的多个版本来管理并发控制。每当一个事务对数据进行修改时,系统会创建一个新版本的数据,而旧版本仍然保留。这使得读操作可以读取旧版本的数据,而不受正在进行的写操作的影响,从而提高并发性能。

作用

  • 隔离性:不同事务可以读取数据的不同版本,从而避免读写冲突。
  • 一致性:每个事务在其开始时获取数据的快照,确保在整个事务过程中读取到一致的数据。

6. 事务优化

1. 事务尽可能短

事务太长会占用锁的时间,影响其他并发操作。因此,大事务应尽量拆分为小事务,并确保小事务的执行时间尽可能短。

2. 减少锁的范围和争用

过多锁会影响性能,减少锁的使用,并精细化使用锁,能用文档锁就不使用集合锁。

3. 避免高冲突的操作

高并发访问同一资源容易造成锁冲突,甚至死锁。可以使用队列来处理频繁抢占的资源,并将数据分区或分片,减少并发冲突。

4. 提前校验和准备

能在外部获取的数据,应提前准备好,缩短事务运行时间,同时避免在事务中进行不必要的数据校验。

5. 使用适当的隔离级别

较高的隔离级别会导致其他锁的延迟,所以尽量选择合适的读级别锁或写级别锁。

总结

通过设计良好的数据模型、适当的索引、精简的事务逻辑以及合适的读写关注级别,开发者可以有效提升MongoDB事务的执行效率。在实际项目中,监控和调优事务性能也是必不可少的步骤,能够帮助识别潜在问题并及时改进。

总之,充分理解MongoDB事务的工作原理和优化方法,将有助于开发者构建更可靠、更高效的数据库应用。希望本篇文章能为您在MongoDB事务的应用和优化上提供有价值的参考和指导。

上次更新于: