事务

4.0 版中的新功能。

在 mongodb 中, 对单个文档的操作是原子的。

由于您可以使用嵌入文档和数组来捕获单个文档结构中的数据之间的关系, 而不是跨多个文档和集合进行规范化, 因此这种单文档原子性无需多文档交易适用于许多实际使用案例。

但是, 对于需要对多个文档进行更新的原子性或多个文档读取之间的一致性的情况, mongodb 提供了对副本集执行多文档事务的能力。

多文档事务可以跨多个操作、集合、数据库和文档使用。

多文档事务提供了一个 “要么全有要么全无” 的命题。

提交事务时, 将保存事务中所做的所有数据更改。如果事务中的任何操作失败, 事务将中止, 并且事务中所做的所有数据更改都将被丢弃, 而不会变得可见。在事务提交之前, 事务中的任何写入操作在事务外部都不可见。

重要

在大多数情况下, 多文档事务比单个文档写入带来更高的性能成本, 多文档事务的可用性不应取代有效的架构设计。

在许多情况下, 非规范化数据模型 (嵌入式文档和数组) 将继续是数据和用例的最佳选择。

也就是说, 在许多情况下, 适当地建模数据将最大限度地减少对多文档事务的需求。

事务和副本集

多文档事务仅适用于副本集。

共享群集的事务安排在 mongodb 4.2。

为我们的产品描述的任何特性或功能的开发、发布和时间安排仍由我们自行决定。

这些信息只是为了概述我们的一般产品方向, 在做出购买决定时不应依赖这些信息, 也不应依赖这些信息来承诺、承诺或法律义务来交付任何材料、代码或功能。

功能兼容性版本 (fcv)

副本集的所有成员的功能兼容性版本 (fcv) 必须为4.0 或更高版本。

若要检查成员的 fcv, 请连接到该成员并运行以下命令:

db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

存储引擎

多文档事务仅适用于使用 wiredtiger 存储引擎的部署。

多文档事务不适用于使用内存中存储引擎或弃用的 mmapv1 存储引擎的部署。

交易和运营

对于交易记录:

  • 您可以在现有集合上指定 read/cred (crud) 操作。集合可以位于不同的数据库中。

  • 无法读取到配置数据库、管理数据库或本地数据库中的集合。

  • 不能写入系统。

  • 不能返回支持的操作的查询计划 (即解释)。

  • 对于在事务之外创建的游标, 不能在事务中调用 getmore。

  • 对于在事务中创建的游标, 不能在事务之外调用 getmore。

在多文档事务中, 不允许影响数据库目录的操作, 如创建或删除集合或索引。

例如, 多文档事务不能包含将导致创建新集合的插入操作。请参阅受限制的操作。

提示

在启动事务之前立即创建或删除集合时, 如果在事务中访问集合, 则发出 “考虑多数” 的创建或删除操作, 以确保事务可以获得所需的锁。

计数操作

若要在事务中执行计数操作, 请使用 $count 聚合阶段或 $group (具有 $sum 表达式) 聚合阶段。

与4.0 功能兼容的 mongodb 驱动程序提供了一个集合级 api 计数文档 (筛选器、选项) 作为帮助器方法, 使用带有 $sum 表达式的 $group 来执行计数。

4.0 驱动程序已弃用 count() api。

从 mongodb 4.0.3 开始, mongo shell 提供了 db.collection.countDocuments() 帮助器方法, 该方法使用具有 $sum 表达式的 $group 来执行计数。

信息运营

在事务中允许信息命令, 如 master 命令、建筑信息命令、连接状态 (及其帮助器方法);

但是, 它们不能是事务中的第一个操作。

受限制的操作

在多文档事务中不允许执行以下操作:

影响数据库目录的操作, 如创建或删除集合或索引。例如, 多文档事务不能包含将导致创建新集合的插入操作。

还排除了列表集合和列表索引命令及其帮助器方法。

非 crud 和非信息操作, 如创建用户、getParameter、计数等及其助手。

事务和安全

如果使用访问控制运行, 则必须具有事务中操作的权限。

如果使用审核运行, 则仍将审核中止的事务中的操作。但是, 没有任何审核事件指示事务中止。

如果使用 $external 身份验证用户 (即 kerberos、ldap、x.509 用户), 则用户名不能大于10k 字节。

事务和 session

事务与会话相关联。也就是说, 您启动会话的事务。在任何给定时间, 您最多可以有一个会话的打开事务。

重要

使用驱动程序时, 必须将会话传递给事务中的每个操作。

Transactions and the mongo Shell

Session.startTransaction()
Session.commitTransaction()
Session.abortTransaction()

和 sql 时候类似的

就是:

try{
    Session.startTransaction();
    Session.commitTransaction()
}catch(Ex ex) {
    Session.abortTransaction()
}

事务和可重试写入

高度可用的应用

无论数据库系统如何, 无论是 mongodb 还是关系数据库, 应用程序都应采取措施来处理事务提交过程中的错误, 并合并事务的重试逻辑。

重试事务

事务中的单个写入操作不可重试, 无论重试写入是否设置为 true。

如果操作遇到错误, 返回的错误可能有一个错误标签数组字段。

如果错误是瞬态错误, 则错误标签数组字段包含 “瞬态事务处理错误” 作为元素, 并且可以重试整个事务。

例如, 下面的帮助程序运行一个函数, 并在遇到 “瞬态事务处理错误” 时重试该函数:

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            print("Transaction aborted. Caught exception during transaction.");

            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes( "TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

重试提交操作

提交操作是可重写的写入操作。如果提交操作遇到错误, mongodb 驱动程序将重试该操作一次, 而不考虑重试写入是否设置为 true。

如果提交操作遇到错误, mongodb 将返回一个错误与错误标签数组字段。如果错误是瞬态提交错误, 则错误标签数组字段包含 “未知事务评估结果” 作为元素, 并且可以重试提交操作。

除了 mongodb 驱动程序提供的单个重试行为外, 应用程序应采取措施处理事务提交过程中的 “未知事务执行结果” 错误。

例如, 下面的帮助程序提交事务, 并在遇到 “未知事务提交结果” 时重试:

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes( "UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

重试事务并执行操作

将逻辑合并为重试事务的瞬态错误并重试提交, 完整的代码示例是:

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}

原子

多文档事务是原子的:

提交事务时, 事务中所做的所有数据更改都将保存并显示在事务外部。

在事务提交之前, 事务中所做的数据更改在事务外部不可见。

当事务中止时, 事务中所做的所有数据更改都将被丢弃, 而不会变得可见。

例如, 如果事务中的任何操作失败, 事务将中止, 并且事务中所做的所有数据更改都将被丢弃, 而不会变得可见。

交易选项 (阅读一致编写-阅读偏好)

阅读关注

多文档事务支持改为 “快照”、”本地” 和 “多数”:

对于 “本地” 和 “多数” 的解读关注, mongodb 有时可能会取代更强烈的阅读关注。具体来说, 在 mongodb 4.0 中, 所有多文档事务都有 “快照” 隔离。有关详细信息, 请参阅读取关注 “本地” 和事务以及读取关注 “多数” 和事务。

对于 “多数” 读取关注, 如果事务提交与写关注 “多数”, 事务操作保证有读取多数提交的数据。否则, “多数” 读取关注项不提供读取操作读取多数提交的数据的保证。

对于 “快照” 读取关注, 如果事务提交时带有写关注 “多数”, 则事务操作保证已从多数提交数据的快照中读取。否则, “快照” 读取注意事项不能保证读取操作使用了多数提交数据的快照。

您可以在事务级别设置读取关注, 而不是在单个操作级别设置。事务中的操作将使用事务级别读取关注。在事务中忽略集合和数据库级别上设置的任何读取关注点。如果显式指定了事务级读取关注项, 则事务中也会忽略客户端级别的读取关注项。

您可以在事务开始时设置事务读取关注。

如果在事务开始时未指定, 则事务使用会话级别的读取关注, 或者, 如果未设置, 则客户端级别读取关注。

写关注

您可以在事务级别设置写入关注, 而不是在单个操作级别设置。

在提交时, 事务使用事务级别的写入关注来提交写入操作。

事务中的单个操作忽略写入问题。不要在事务中显式设置单个写入操作的写入关注。

您可以在事务开始时设置事务提交的写入关注。

如果在事务开始时未指定, 事务将使用提交的会话级别写入关注, 如果未设置, 则使用客户端级别写入关注项。 写入关心 w: 0 不支持事务。

如果您使用 w:1 写问题提交, 如果存在故障转移, 则可以回滚事务。

如果事务提交的写关注 “多数”, 并指定了读取关注 “快照” 读取关注, 则事务操作保证已从多数提交的数据的快照中读取。否则, “快照” 读取注意事项不能保证读取操作使用了多数提交数据的快照。

如果事务提交与写关注 “多数”, 并指定阅读关注 “多数” 阅读关注, 事务操作保证有读取多数提交的数据。否则, “多数” 读取关注项不提供读取操作读取多数提交的数据的保证。

生产注意事项

功能兼容性

副本集的所有成员的功能兼容性版本必须为4.0 或更大版本。

运行时限制

默认情况下, 事务的运行时必须小于一分钟。您可以使用事务性终身限制秒修改此限制。超过此限制的事务被视为已过期, 并将被定期清理过程中止。

oplog 尺寸限制

当事务提交时, 如果事务包含任何写入操作, 则会创建一个 oplog (操作日志) 项。

也就是说, 事务中的单个操作没有相应的 oplog 条目。

相反, 单个 oplog 条目包含事务中的所有写入操作。

事务的 oplog 条目必须在 bson 文档大小限制16mb 的范围内。

WiredTiger Cache

要防止存储缓存压力固定在系统中, 请执行以下操作:

  1. 放弃事务时, 中止该事务。

  2. 当您在事务中的单个操作过程中遇到错误时, 中止事务并重试事务。

  3. 事务性生存期限制秒还可确保定期中止过期事务, 以减轻存储缓存压力。

默认情况下, 事务最多等待5毫秒以获取事务中的操作所需的锁。如果事务无法在5毫秒内获取所需的锁, 则事务将中止。

事务和锁定

默认情况下, 事务最多等待5毫秒以获取事务中的操作所需的锁。如果事务无法在5毫秒内获取所需的锁, 则事务将中止。

事务在中止或提交时释放所有锁。

提示

在启动事务之前立即创建或删除集合时, 如果在事务中访问集合, 则发出 “考虑多数” 的创建或删除操作, 以确保事务可以获得所需的锁。

锁定请求超时

您可以使用maxTransactionLockRequestTimeoutMillis参数来调整事务等待获取锁的时间。

增加maxTransactionLockRequestTimeoutMillis允许事务中的操作等待指定的时间来获取所需的锁。

这有助于避免在瞬时并发锁定获取(例如快速运行的元数据操作)上的事务中止。

但是,这可能会延迟死锁事务操作的中止。

您还可以通过将maxTransactionLockRequestTimeoutMillis设置为-1来使用特定于操作的超时。

等待DDL操作和事务

如果正在进行多文档事务,则影响相同数据库的新DDL操作会在事务后面等待。

虽然存在这些挂起的DDL操作,但是与挂起的DDL操作访问同一数据库的新事务无法获取所需的锁,并且在等待maxTransactionLockRequestTimeoutMillis后将中止。此外,访问同一数据库的新非事务操作将阻塞,直到达到其maxTimeMS限制。

为了说明,比较以下两种情况:

考虑一种情况,即正在进行的事务对hr数据库中的employees集合执行各种CRUD操作。当该事务正在进行时,可以启动并完成访问hr数据库中的foobar集合的单独事务。

但是,请考虑这样一种情况:正在进行的事务对hr数据库中的雇员集合执行各种CRUD操作,并且发出单独的DDL操作以在hr数据库中的蓬松集合上创建索引。

DDL操作等待事务完成。

当DDL操作处于挂起状态时,新事务会尝试访问hr数据库中的foobar集合。

如果DDL操作对于maxTransactionLockRequestTimeoutMillis保持挂起,则新事务将中止。

正在进行的事务和写冲突

如果正在进行多文档事务并且事务外部的写入修改了事务中的操作稍后尝试修改的文档,则事务因写入冲突而中止。

如果多文档事务正在进行并且已锁定以修改文档,则当事务外部的写入尝试修改同一文档时,写入将等待直到事务结束。

正在进行的事务和过时的读取

读取操作采用意图锁定。

因此,事务内的读取操作可以返回过时数据。

例如,请考虑以下顺序:

1)事务正在进行中

2)事务外部的写入删除文档

3)事务内部的读取操作能够读取现在删除的文档,因为操作正在使用快照从写之前。

要避免单个文档的事务内部过时读取,可以使用 db.collection.findOneAndUpdate() 方法。

例如:

session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

employeesCollection = session.getDatabase("hr").employees;

employeeDoc = employeesCollection.findOneAndUpdate(
   { _id: 1, employee: 1, status: "Active" },
   { $set: { employee: 1 } },
   { returnNewDocument: true }
);

参考资料

https://docs.mongodb.com/manual/core/transactions/

https://docs.mongodb.com/manual/core/transactions-production-consideration/