MongoDB 事务

在MongoDB中,对单个文档的操作是原子性的。因为您可以使用嵌入的文档和数组来捕获单个文档结构中的数据之间的关系,而不是跨多个文档和集合进行规范化,所以这种单文档原子性消除了对于需要对多个文档(在单个或多个集合中)进行原子性读写的情况,MongoDB支持多文档事务。使用分布式事务,可以跨多个操作、集合、数据库、文档和碎片使用事务。许多实际用例对多文档事务的需求。

1.事务API

以下示例突出显示了transactions API的关键组件:

该示例使用新的回调API处理事务,事务启动事务,执行指定的操作,并提交(或因错误中止)。新的回调API还为TransientTransactionError或UnknownTransactionCommitResult提交错误合并了重试逻辑。

重要的:

  • 对于MongoDB 4.2部署(副本集和分片集群)上的事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。
  • 在使用驱动程序时,事务中的每个操作都必须与会话相关联(即将会话传递给每个操作)。
/*
  For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g.
  String uri = "mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/admin?replicaSet=myRepl";
  For a sharded cluster, connect to the mongos instances; e.g.
  String uri = "mongodb://mongos0.example.com:27017,mongos1.example.com:27017:27017/admin";
 */

final MongoClient client = MongoClients.create(uri);

/*
    Prereq: Create collections. CRUD operations in transactions must be on existing collections.
 */

client.getDatabase("mydb1").getCollection("foo")
        .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("abc", 0));
client.getDatabase("mydb2").getCollection("bar")
        .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("xyz", 0));

/* Step 1: Start a client session. */

final ClientSession clientSession = client.startSession();

/* Step 2: Optional. Define options to use for the transaction. */

TransactionOptions txnOptions = TransactionOptions.builder()
        .readPreference(ReadPreference.primary())
        .readConcern(ReadConcern.LOCAL)
        .writeConcern(WriteConcern.MAJORITY)
        .build();

/* Step 3: Define the sequence of operations to perform inside the transactions. */

TransactionBody txnBody = new TransactionBody<String>() {
    public String execute() {
        MongoCollection<Document> coll1 = client.getDatabase("mydb1").getCollection("foo");
        MongoCollection<Document> coll2 = client.getDatabase("mydb2").getCollection("bar");

        /*
           Important:: You must pass the session to the operations.
         */
        coll1.insertOne(clientSession, new Document("abc", 1));
        coll2.insertOne(clientSession, new Document("xyz", 999));
        return "Inserted into collections in different databases";
    }
};
try {
    /*
       Step 4: Use .withTransaction() to start a transaction,
       execute the callback, and commit (or abort on error).
    */

    clientSession.withTransaction(txnBody, txnOptions);
} catch (RuntimeException e) {
    // some error handling
} finally {
    clientSession.close();
}

2.事务和原子性

分布式事务和多文档事务
从MongoDB 4.2开始,这两个术语是同义词。分布式事务是指sharded集群和replica sets上的多文档事务。多文档事务(不管是分片集群还是副本集)在MongoDB 4.2中也被称为分布式事务。

对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:

  • 在4.0版本中,MongoDB支持副本集上的多文档事务。
  • 在版本4.2中,MongoDB引入了分布式事务,它在sharded集群上添加了对多文档事务的支持,并合并了对副本集上多文档事务的现有支持。  要在MongoDB 4.2部署(副本集和分片集群)上使用事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。

多文档事务是原子性的(即提供一个“全有或全无”的命题):

  • 当事务提交时,在事务中所做的所有数据更改都会被保存并在事务外部可见。也就是说,事务在回滚其他更改时不会提交某些更改。在事务提交之前,事务中所做的数据更改在事务外部是不可见的。然而,当一个事务写入多个碎片时,并不是所有外部读操作都需要等待提交的事务的结果在碎片中可见。例如,如果提交了一个事务,并且在分片a上可以看到写1,但是在分片B上还不能看到写2,那么外部的read at read关注点“local”可以在看不到写2的情况下读取写1的结果。
  • 当事务中止时,在事务中所做的所有数据更改都将被丢弃,而不会变得可见。例如,如果事务中的任何操作失败,事务将中止,并且在事务中所做的所有数据更改都将被丢弃,而不会变得可见。

重要的:
在大多数情况下,与单个文档写入相比,多文档事务会带来更大的性能成本,而且多文档事务的可用性不应该代替有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)对于您的数据和用例仍然是最优的。也就是说,对于许多场景,适当地对数据建模将最小化对多文档事务的需求。
有关其他事务使用注意事项(如运行时限制和oplog大小限制),请参见生产

 

3. 事务和操作

分布式事务可以跨多个操作、集合、数据库、文档使用,在MongoDB 4.2中可以使用碎片。

事务:

  • 可以在现有集合上指定读/写(CRUD)操作。集合可以在不同的数据库中。有关CRUD操作的列表,请参阅CRUD操作。
  • 你不能写封顶的集合。(在MongoDB 4.2中启动)
  • 无法在配置、管理或本地数据库中读写集合。
  • 您不能向system.* collections写入。
  • 您不能返回支持的操作的查询计划(即explain)。
  • 对于在事务外部创建的游标,不能在事务内部调用getMore。
  • 对于在事务中创建的游标,不能在事务外部调用getMore。
  • 从MongoDB 4.2开始,您不能将kill游标指定为事务中的第一个操作。

在事务中不允许创建或删除集合或索引等影响数据库编目的操作。例如,事务不能包含会导致创建新集合的插入操作。看到限制操作。

提示:
在开始事务之前立即创建或删除集合时,如果在事务中访问了该集合,则使用write关注点“majority”发出创建或删除操作,以确保事务可以获得所需的锁。

3.1 操作数

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

与4.0特性兼容的MongoDB驱动程序提供了一个集合级API countDocuments(filter, options)作为一个助手方法,它使用带有$sum表达式的$group来执行计数。4.0驱动程序已经弃用了count() API。

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

3.2 不同的操作

在事务中执行不同的操作:

  • 对于未切分的集合,可以使用db.collection.distinct()方法/ distinct命令以及$group阶段的聚合管道。
  • 对于切分集合,您不能使用db.collection.distinct()方法或distinct命令。

要查找切分集合的不同值,请使用包含$group stage的聚合管道。例如:

  • 使用而不是db.coll.distinct(“x”)
db.coll.aggregate([
   { $group: { _id: null, distinctValues: { $addToSet: "$x" } } },
   { $project: { _id: 0 } }
])
  • 而不是db.coll。distinct("x", {status: "A"}),使用:
db.coll.aggregate([
   { $match: { status: "A" } },
   { $group: { _id: null, distinctValues: { $addToSet: "$x" } } },
   { $project: { _id: 0 } }
])

该管道将光标返回到文档:

{ "distinctValues" : [ 2, 3, 1 ] }

迭代游标以访问结果文档。

3.3 信息操作

信息命令,如isMaster、buildInfo、connectionStatus(及其助手方法),在事务中是允许的;但是,它们不能是事务中的第一个操作。

3.4 限制操作

以下操作在交易中是不允许的:

  • 影响数据库编目的操作,如创建或删除集合或索引。例如,事务不能包含会导致创建新集合的插入操作。listCollections和listIndexes命令及其辅助方法也被排除在外。
  • 非crud和非信息操作,如createUser、getParameter、count等及其助手。

4. 事务和会话

  • 事务与会话相关联;例如,你为一个会话启动一个事务。
  • 在任何给定时间,一个会话最多可以有一个打开的事务。
  • 在使用驱动程序时,事务中的每个操作都必须与会话相关联。有关详细信息,请参阅您的驱动程序特定文档。
  • 如果一个会话结束,并且它有一个打开的事务,该事务将中止。

5. 读关注点/写关注点/读偏好

5.1 事务和读取首选项

事务中的操作使用事务级读首选项。
使用驱动程序,您可以在事务开始时设置事务级别的read首选项:

  • 如果未设置事务级别的读首选项,则事务将使用会话级别的读首选项。
  • 如果未设置事务级别和会话级别的读首选项,则事务将使用客户端级别的读首选项。默认情况下,客户端读首选项是主选项。

包含读操作的多文档事务必须使用读首选项主。给定事务中的所有操作都必须路由到相同的成员。

5.2 事务和读关注点

事务中的操作使用事务级读关注点。也就是说,在集合和数据库级别上的任何读关注集都将在事务内部被忽略。
您可以在事务开始时设置事务级别的读关注点。

  • 如果事务级别的读关注未设置,事务级别的读关注默认为会话级别的读关注。
  • 如果未设置事务级别和会话级别的读关注,事务级别的读关注默认为客户端级别的读关注。默认情况下,针对主服务器的读操作的客户端读关注是“本地”的。请参阅“事务”和“读取首选项”。

事务支持以下读取关注级别:

"local"

  • Read concern“local”返回节点上可用的最新数据,但是可以回滚。
  • 对于分片集群上的事务,“本地”读关注点不能保证数据来自跨分片的相同快照视图。如果需要快照隔离,则使用“快照”读取关注点。

"majority"

  • 读关心“多数”返回已被大多数复制集成员确认的数据(即数据不能回滚),如果事务以写关心“多数”提交。
  • 如果事务没有对提交使用写关注点“多数”,那么“多数”读关注点就不能保证读操作读了多数提交的数据。
  • 对于分片集群上的事务,“多数”读关注点不能保证数据来自于跨分片的相同快照视图。如果需要快照隔离,则使用“快照”读取关注点。

"snapshot"

  • 如果事务提交了写关注“多数”,那么读关注“快照”将从已提交数据的快照中返回数据。
  • 如果事务没有对提交使用写关注点“多数”,那么“快照”读关注点不能保证读操作使用了多数提交数据的快照。
  • 对于分片集群上的事务,数据的“快照”视图是跨分片同步的。

事务和写事务

事务使用事务级别的写关注点提交写操作。事务中的写操作必须在没有显式写关注规范的情况下发出,并使用默认的写关注。在提交时,使用事务级别的写关注点来提交写。

提示:
不要显式地为事务中的各个写操作设置写关系。为事务中的各个写操作设置写关注点会导致错误。

您可以在事务开始时设置事务级别的写关注点:

  • 如果事务级别的写关注未设置,则事务级别的写关注默认为提交的会话级别的写关注。
  • 如果未设置事务级别的写关注点和会话级别的写关注点,则事务级别的写关注点默认为客户端级别的写关注点。默认情况下,客户端级别的写操作是w: 1。

事务支持所有写关心w值,包括:

w: 1

  • 写关注w: 1在提交被应用到主服务器后返回确认。

    重要的:

    当您使用w: 1提交时,如果出现故障转移,您的事务可以回滚。

  • 当您使用w: 1写关注点提交时,事务级别的“多数”读关注点并不能保证事务中的读操作能够读取多数提交的数据。
  • 当您使用w: 1写关注点提交时,事务级别的“快照”读关注点并不能保证事务中的读操作使用了主要提交数据的快照。

w: "majority"

  • 写关心w:“多数”返回确认后,提交已被应用到大多数(M)投票成员;也就是说,提交已经被应用到初级和(M-1)投票二级。
  • 当您使用w:“多数”写入关注时,事务级别的“多数”读取关注保证操作已经读取了多数提交的数据。对于分片集群上的事务,这个提交了大量数据的视图并不是跨分片同步的。
  • 当您用w:“多数”写关注点提交时,事务级的“快照”读关注点可以保证操作已经从一个同步的大部分提交数据快照中提交。

请注意:
不管为事务指定的写关注点是什么,sharded集群事务的提交操作都包括一些使用{w: "majority", j: true}写关注点的部分。

6. 基本信息

6.1 生产注意事项

有关使用事务的各种生产注意事项,请参阅生产注意事项。此外,或分片集群,请参见生产注意事项(分片集群)。

6.2 裁决者

如果任何事务操作读取或写入包含仲裁程序的碎片,则其写入操作跨越多个碎片的事务将出错并终止。
有关已禁用读关注多数的碎片上的事务限制,请参阅禁用读关注多数。

6.3 Disabled Read Concern Majority

一个3成员的PSA(主-次-仲裁者)副本集或一个带有3成员PSA碎片的分片集群可能已经禁用了read concern majority(——enableMajorityReadConcern false或复制)。enableMajorityReadConcern:false)

6.4 分片的集群,

  • 如果一个事务涉及到一个已经禁用了read concern“majority”的碎片,那么您就不能为事务使用read concern“snapshot”。对于事务,您只能使用read concern“local”或“majority”。如果使用“快照”,则事务错误和中止。
readConcern level 'snapshot' is not supported in sharded clusters when enableMajorityReadConcern=false.
  • 如果事务的任何读或写操作涉及到已禁用读的碎片,那么其写操作跨越多个碎片的事务将出错并终止。

在副本集,

您可以指定读关心“本地”或“多数”或“快照”,即使在副本集中已禁用读关心“多数”。
但是,如果您计划使用禁用的read concern majority切分来过渡到切分集群,那么您可能希望避免使用read concern“snapshot”。

提示:
要检查是否禁用了read关注点“majority”,可以在mongod实例上运行db.serverStatus()并检查存储引擎。supportsCommittedReads字段。如为假,则阅读关系“多数”被禁用。

碎片配置限制

您不能在分片集群上运行事务,分片的writeConcernMajorityJournalDefault设置为false(例如带有使用内存存储引擎的投票成员的分片)。

请注意:
不管为事务指定的写关注点是什么,sharded集群事务的提交操作都包括一些使用{w: "majority", j: true}写关注点的部分。

诊断
MongoDB提供各种事务指标:

Via 

db.serverStatus() method

serverStatus command

Returns transactions metrics.
$currentOp aggregation pipeline

Returns:

  • currentOp美元。如果操作是事务的一部分,则称为事务。
  • 关于作为事务的一部分持有锁的非活动会话的信息。
  • currentOp美元。涉及多个碎片写入的切分事务的twoPhaseCommitCoordinator度量。

db.currentOp() method

currentOp command

Returns:

  • currentOp。如果操作是事务的一部分,则称为事务。
  • currentOp。涉及多个碎片写入的切分事务的twoPhaseCommitCoordinator度量。
mongod and mongos log messages包括关于慢速事务的信息(即超过操作概要的事务)。在TXN日志组件下。

功能兼容版本(FCV)
要使用事务,对于部署的所有成员的featureCompatibilityVersion必须至少:

DeploymentMinimum featureCompatibilityVersion
Replica Set4.0
Sharded Cluster4.2

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

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

存储引擎
从MongoDB 4.2开始,在复制集和分片集群上支持多文档事务,其中:

  • 主要成员使用WiredTiger存储引擎,
  • 次要成员使用WiredTiger存储引擎或内存存储引擎。

在MongoDB 4.0中,只有使用WiredTiger存储引擎的副本集支持事务。

请注意:
您不能在分片集群上运行事务,分片的writeConcernMajorityJournalDefault设置为false,例如带有使用内存存储引擎的投票成员的分片。

一、API驱动

1. 回调API vs核心API

回调API:

  • 启动事务,执行指定的操作,并提交(或因错误中止)。
  • 自动合并“TransientTransactionError”和“UnknownTransactionCommitResult”的错误处理逻辑。

核心API:

  • 需要显式调用来启动事务并提交transact。
  • 没有为“TransientTransactionError”和“UnknownTransactionCommitResult”合并错误处理逻辑,而是提供了为这些错误合并自定义错误处理的灵活性。

2. 回调的API

新的回调API整合了逻辑:

  • 如果事务遇到“TransientTransactionError”,则重试整个事务。
  • 如果提交遇到“UnknownTransactionCommitResult”,则重试提交操作。

重要的

  • 对于MongoDB 4.2部署(副本集和分片集群)上的事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。
  • 在使用驱动程序时,事务中的每个操作都必须与会话相关联(即将会话传递给每个操作)。

该示例使用新的回调API处理事务,事务启动事务,执行指定的操作,并提交(或因错误中止)。新的回调API为“TransientTransactionError”或“UnknownTransactionCommitResult”提交错误合并了重试逻辑。

/*
  For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g.
  String uri = "mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/admin?replicaSet=myRepl";
  For a sharded cluster, connect to the mongos instances; e.g.
  String uri = "mongodb://mongos0.example.com:27017,mongos1.example.com:27017:27017/admin";
 */

final MongoClient client = MongoClients.create(uri);

/*
    Prereq: Create collections. CRUD operations in transactions must be on existing collections.
 */

client.getDatabase("mydb1").getCollection("foo")
        .withWriteConcern(WriteConcern.MAJORITY).insertOne( new Document("abc", 0));
client.getDatabase("mydb2").getCollection("bar")
        .withWriteConcern(WriteConcern.MAJORITY).insertOne( new Document("xyz", 0));

/* Step 1: Start a client session. */

final ClientSession clientSession = client.startSession();

/* Step 2: Optional. Define options to use for the transaction. */

TransactionOptions txnOptions = TransactionOptions.builder()
        .readPreference(ReadPreference.primary())
        .readConcern(ReadConcern.LOCAL)
        .writeConcern(WriteConcern.MAJORITY)
        .build();

/* Step 3: Define the sequence of operations to perform inside the transactions. */

TransactionBody txnBody = new TransactionBody<String>() {
    public String execute() {
        MongoCollection<Document> coll1 = client.getDatabase("mydb1").getCollection("foo");
        MongoCollection<Document> coll2 = client.getDatabase("mydb2").getCollection("bar");

        /*
           Important:: You must pass the session to the operations..
         */

        coll1.insertOne(clientSession, new Document("abc", 1));
        coll2.insertOne(clientSession, new Document("xyz", 999));

        return "Inserted into collections in different databases";
    }
};
try {
    /*
        Step 4: Use .withTransaction() to start a transaction,
        execute the callback, and commit (or abort on error).
     */

    clientSession.withTransaction(txnBody, txnOptions);
} catch (RuntimeException e) {
    // some error handling
} finally {
    clientSession.close();
}

3. 使用核心接口

核心事务API不包含错误重试逻辑标签:

  • “TransientTransactionError”。如果事务中的操作返回一个名为TransientTransactionError的错误,则可以重试整个事务。
  • 为了处理“TransientTransactionError”,应用程序应该显式地合并错误的重试逻辑。
  • “UnknownTransactionCommitResult”。如果提交返回一个标记为“UnknownTransactionCommitResult”的错误,则可以重试提交。为了处理“UnknownTransactionCommitResult”,应用程序应该显式地合并错误的重试逻辑。

下面的例子合并了逻辑,以对暂时错误重试事务,对未知的提交错误重试提交:

重要的:
要将读写操作与事务关联起来,必须将会话传递给事务中的每个操作。

void runTransactionWithRetry(Runnable transactional) {
    while (true) {
        try {
            transactional.run();
            break;
        } catch (MongoException e) {
            System.out.println("Transaction aborted. Caught exception during transaction.");

            if (e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)) {
                System.out.println("TransientTransactionError, aborting transaction and retrying ...");
                continue;
            } else {
                throw e;
            }
        }
    }
}

void commitWithRetry(ClientSession clientSession) {
    while (true) {
        try {
            clientSession.commitTransaction();
            System.out.println("Transaction committed");
            break;
        } catch (MongoException e) {
            // can retry commit
            if (e.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
                System.out.println("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                System.out.println("Exception during commit ...");
                throw e;
            }
        }
    }
}

void updateEmployeeInfo() {

    MongoCollection<Document> employeesCollection = client.getDatabase("hr").getCollection("employees");
    MongoCollection<Document> eventsCollection = client.getDatabase("reporting").getCollection("events");

    TransactionOptions txnOptions = TransactionOptions.builder()
            .readPreference(ReadPreference.primary())
            .readConcern(ReadConcern.MAJORITY)
            .writeConcern(WriteConcern.MAJORITY)
            .build();

    try (ClientSession clientSession = client.startSession()) {
        clientSession.startTransaction(txnOptions);

        employeesCollection.updateOne(clientSession,
                Filters.eq("employee", 3),
                Updates.set("status", "Inactive"));
        eventsCollection.insertOne(clientSession,
                new Document("employee", 3).append("status", new Document("new", "Inactive").append("old", "Active")));

        commitWithRetry(clientSession);
    }
}


void updateEmployeeInfoWithRetry() {
    runTransactionWithRetry(this::updateEmployeeInfo);
}

4.驱动程序版本
对于MongoDB 4.2部署(副本集和分片集群)上的事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序:

对于MongoDB 4.0复制集上的事务,客户端需要为MongoDB 4.0或更高版本更新MongoDB驱动程序。

  • Java 3.8.0
  • Python 3.7.0
  • C 1.11.0
  • C# 2.7
  • Node 3.1.0
  • Ruby 2.6.0
  • Perl 2.0.0
  • PHPC 1.5.0
  • Scala 2.4.0

5.交易错误处理

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

"TransientTransactionError"

不管retrywrite的值是多少,事务中的各个写操作都是不可重试的。如果操作遇到与“TransientTransactionError”标签相关的错误,例如主步骤停止时,可以重试整个事务。

  • 回调API为TransientTransactionError合并了重试逻辑。
  • 核心事务API不包含“TransientTransactionError”的重试逻辑。为了处理“TransientTransactionError”,应用程序应该显式地合并错误的重试逻辑。

"UnknownTransactionCommitResult"

提交操作是可重试的写操作。如果提交操作遇到错误,MongoDB驱动程序会不考虑retrywrite的值重试提交。
如果提交操作遇到一个标记为“UnknownTransactionCommitResult”的错误,可以重试提交。

  • 回调API为“UnknownTransactionCommitResult”合并了重试逻辑。
  • 核心事务API不包含“UnknownTransactionCommitResult”的重试逻辑。为了处理“UnknownTransactionCommitResult”,应用程序应该显式地合并错误的重试逻辑。

驱动程序版本错误

在有多个mongos实例的切分集群上,使用更新为MongoDB 4.0(而不是MongoDB 4.2)的驱动程序执行事务将会失败,并可能导致错误,包括:

请注意:
你的驱动程序可能会返回一个不同的错误。详情请参阅您的司机的文件。

Error CodeError Message
251cannot continue txnId -1 for session ... with txnId 1
50940cannot commit with no participants

对于MongoDB 4.2部署(副本集和分片集群)上的事务,使用针对MongoDB 4.2更新的MongoDB驱动程序

6.附加信息

6.1 mongo Shell Example

以下的mongo shell方法可用于事务:

请注意:
为了简单起见,mongo shell示例省略了重试逻辑和健壮的错误处理。有关在应用程序中合并事务的更实际示例,请参见事务错误处理。


// Prereq: Create collections. CRUD operations in transactions must be on existing collections.
db.getSiblingDB("mydb1").foo.insert( {abc: 0}, { writeConcern: { w: "majority", wtimeout: 2000 } } );
db.getSiblingDB("mydb2").bar.insert( {xyz: 0}, { writeConcern: { w: "majority", wtimeout: 2000 } } );

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

coll1 = session.getDatabase("mydb1").foo;
coll2 = session.getDatabase("mydb2").bar;

// Start a transaction
session.startTransaction( { readConcern: { level: "local" }, writeConcern: { w: "majority" } } );

// Operations inside the transaction
try {
   coll1.insertOne( { abc: 1 } );
   coll2.insertOne( { xyz: 999 } );
} catch (error) {
   // Abort transaction on error
   session.abortTransaction();
   throw error;
}

// Commit the transaction using write concern set at transaction start
session.commitTransaction();

session.endSession();

二、生产注意事项

下面的页面列出了运行事务时的一些生产注意事项。无论您在复制集还是分片集群上运行事务,这些都适用。要在分片集群上运行事务,请参阅生产注意事项(分片集群),以了解特定于分片集群的其他注意事项。

1.可用性

  • 在4.0版本中,MongoDB支持副本集上的多文档事务。
  • 在版本4.2中,MongoDB引入了分布式事务,它在sharded集群上添加了对多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

   要在MongoDB 4.2部署(副本集和分片集群)上使用事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。

分布式事务和多文档事务
从MongoDB 4.2开始,这两个术语是同义词。分布式事务是指sharded集群和replica sets上的多文档事务。多文档事务(不管是分片集群还是副本集)在MongoDB 4.2中也被称为分布式事务。

2. 特征相容

要使用事务,对于部署的所有成员的featureCompatibilityVersion必须至少:

DeploymentMinimum featureCompatibilityVersion
Replica Set4.0
Sharded Cluster4.2

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

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

3.运行时限制

默认情况下,事务的运行时间必须少于一分钟。您可以使用transactionLifetimeLimitSeconds为mongod实例修改这个限制。对于分片集群,必须修改所有分片复制集成员的参数。超过此限制的事务将被视为已过期,并将被定期清理进程中止。

对于分片集群,您还可以在commitTransaction上指定maxTimeMS限制。有关更多信息,请参见切分集群事务的时间限制。

4.Oplog大小限制

从4.2版开始,
MongoDB创建尽可能多的oplog条目来封装一个事务中的所有写操作,而不是为事务中的所有写操作创建一个条目。这消除了单个oplog条目对其所有写操作施加的事务的16MB总大小限制。虽然取消了总大小限制,但是每个oplog条目仍然必须在16MB的BSON文档大小限制之内。

在version 4.0中,
如果事务包含任何写操作,MongoDB在提交时创建一个oplog(操作日志)条目。也就是说,事务中的各个操作没有对应的oplog条目。相反,一个oplog条目包含一个事务中的所有写操作。事务的oplog条目必须在16MB的BSON文档大小限制内。

5.WiredTiger缓存
为了防止存储缓存压力对性能产生负面影响:

  • 当您放弃一个事务时,请中止该事务。
  • 当您在事务的个别操作过程中遇到错误时,请中止并重试事务。

transactionLifetimeLimitSeconds还确保定期中止过期的事务,以减轻存储缓存的压力。

6.事务和安全

  • 如果使用访问控制运行,则必须对事务中的操作具有特权。
  • 如果使用审计运行,中止的事务中的操作仍将被审计。但是,没有表示事务中止的审计事件。

7. 碎片配置限制

您不能在分片集群上运行事务,分片的writeConcernMajorityJournalDefault设置为false(例如带有使用内存存储引擎的投票成员的分片)。

8.切分集群和仲裁程序

如果任何事务操作读取或写入包含仲裁程序的碎片,则其写入操作跨越多个碎片的事务将出错并终止。
请参阅3个成员的主-副-仲裁架构,了解对已禁用read关注多数的碎片的事务限制。

9. 三人Primary-Secondary-Arbiter架构

对于具有主-副-仲裁(PSA)体系结构的三成员副本集或具有三成员PSA碎片的分片集群,您可能已经禁用了read concern“majority”以避免缓存压力。

分片的集群,

  • 如果一个事务涉及到一个已经禁用了read concern“majority”的碎片,那么您就不能为事务使用read concern“snapshot”。对于事务,您只能使用read concern“local”或“majority”。如果使用“快照”,则事务错误和中止。
readConcern level 'snapshot' is not supported in sharded clusters when enableMajorityReadConcern=false.
  • 如果事务的任何读或写操作涉及到已禁用读的碎片,那么其写操作跨越多个碎片的事务将出错并终止。

在副本集,

您可以指定读关心“本地”或“多数”或“快照”,即使在副本集中已禁用读关心“多数”。
但是,如果您计划使用禁用的read concern majority切分来过渡到切分集群,那么您可能希望避免使用read concern“快照”。

提示:
要检查是否禁用了read关注点“majority”,可以在mongod实例上运行db.serverStatus()并检查存储引擎。supportsCommittedReads字段。如为假,则阅读关系“多数”被禁用。

10.获取锁
默认情况下,事务要等待5毫秒才能获得事务中操作所需的锁。如果事务无法在5毫秒内获得所需的锁,则事务将中止。
事务在中止或提交时释放所有锁。

提示:
在开始事务之前立即创建或删除集合时,如果在事务中访问了该集合,则使用write关注点“majority”发出创建或删除操作,以确保事务可以获得所需的锁。

10.1 锁请求超时

您可以使用maxTransactionLockRequestTimeoutMillis参数来调整事务等待获取锁的时间。增加maxTransactionLockRequestTimeoutMillis允许事务中的操作等待指定的时间来获取所需的锁。这可以帮助避免临时并发锁获取(如快速运行的元数据操作)时的事务中止。但是,这可能会延迟死锁事务操作的中止。

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

11.挂起的DDL操作和事务

如果一个多文档事务正在进行中,则影响相同数据库或集合的新DDL操作将在事务之后等待。虽然存在这些挂起的DDL操作,但是访问与挂起的DDL操作相同的数据库或集合的新事务无法获得所需的锁,并且将在等待maxTransactionLockRequestTimeoutMillis之后中止。此外,访问相同数据库或集合的新非事务操作将阻塞,直到达到maxTimeMS限制为止。

考虑以下场景:
需要集合锁的DDL操作

当一个正在进行的事务对hr数据库中的employees集合执行各种CRUD操作时,管理员对employees集合发出db.collection.createIndex() DDL操作。createIndex()要求集合上有一个独占的集合锁。

在进行中的事务完成之前,createIndex()操作必须等待以获取锁。任何影响employees集合并在createIndex()挂起时启动的新事务必须等到createIndex()完成之后。

挂起的createIndex() DDL操作不会影响hr数据库中其他集合上的事务。例如,hr数据库中关于承包方收集的新事务可以正常启动和完成。

需要数据库锁的DDL操作

当一个正在进行的事务对hr数据库中的employees集合执行各种CRUD操作时,管理员会对相同数据库中的contractor集合发出collMod DDL操作。collMod需要在父hr数据库上有一个数据库锁。

在进行中的事务完成之前,collMod操作必须等待以获取锁。当collMod挂起时,任何影响hr数据库或其集合的新事务都必须等待,直到collMod完成。

 

在这两种情况下,如果DDL操作的挂起时间超过maxTransactionLockRequestTimeoutMillis,则挂起的事务将在该操作中止后等待。也就是说,maxTransactionLockRequestTimeoutMillis的值必须至少覆盖进行中的事务和挂起的DDL操作完成所需的时间。

12. 正在进行的事务和写冲突

如果一个事务正在进行中,而事务外部的写操作修改了一个文档,而事务中的某个操作稍后试图修改该文档,则事务将由于写冲突而中止。
如果一个事务正在进行中,并且使用了一个锁来修改文档,那么当事务外部的写操作试图修改相同的文档时,写操作将一直等待,直到事务结束。

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

事务中的读操作可以返回过时的数据。也就是说,事务中的读操作不能保证看到其他已提交事务或非事务性写操作执行的写操作。例如,考虑以下顺序: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 }
);
  • 如果员工文档在事务之外发生了更改,那么事务将中止。
  • 如果员工文档没有更改,事务将返回文档并锁定文档。

14.正在进行的事务和块迁移

块迁移在某些阶段需要独占收集锁。
如果一个正在进行的事务对一个集合有一个锁,并且涉及该集合的块迁移已经启动,那么这些迁移阶段必须等待事务释放对集合的锁,从而影响块迁移的性能。

如果一个块迁移与一个事务交错(例如,如果一个事务在块迁移已经开始的时候启动,并且在事务对集合进行锁定之前完成迁移),则提交和中止期间的事务错误。
根据这两个操作的交错方式,一些示例错误包括(错误消息已被缩写):

  • 来自集群数据放置更改的错误…<名称空间>的迁移提交正在进行中
  • 无法找到shardId块在集群时属于…

15. 提交期间的外部读取

在事务的提交期间,外部读取操作可能尝试读取将被事务修改的相同文档。如果事务写入多个碎片,那么在跨碎片的提交尝试期间

  • 外部读取使用读关注快照或“线性化的”,或者是导致一致的会话的一部分(即包括afterClusterTime),等待事务的所有写都是可见的。
  • 使用其他读关注点的外部读取不是等待事务的所有写都可见,而是读取可用文档的事务前版本。

16.错误

16.1 使用MongoDB 4.0驱动程序
要在MongoDB 4.2部署(副本集和分片集群)上使用事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。
在有多个mongos实例的切分集群上,使用更新为MongoDB 4.0(而不是MongoDB 4.2)的驱动程序执行事务将会失败,并可能导致错误,包括:

请注意:
你的驱动程序可能会返回一个不同的错误。详情请参阅您的司机的文件。

Error CodeError Message
251cannot continue txnId -1 for session ... with txnId 1
50940cannot commit with no participants

三、生产注意事项(分片集群)

从4.2版开始,MongoDB提供了为切分集群执行多文档事务的能力。
下面的页面列出了特定于在分片集群上运行事务的关注点。这些问题是在生产考虑中所列之外的。

1.分片事务和MongoDB驱动程序

对于MongoDB 4.2部署(副本集和分片集群)上的事务,客户端必须使用针对MongoDB 4.2更新的MongoDB驱动程序。
在有多个mongos实例的切分集群上,使用更新为MongoDB 4.0(而不是MongoDB 4.2)的驱动程序执行事务将会失败,并可能导致错误,包括:

请注意:
你的驱动程序可能会返回一个不同的错误。详情请参阅您的司机的文件。

Error CodeError Message
251cannot continue txnId -1 for session ... with txnId 1
50940cannot commit with no participants

2. Performance

单一的碎片
针对单个碎片的事务应该具有与复制集事务相同的性能。
多个碎片
影响多个碎片的事务会导致更大的性能成本。

请注意:
在分片集群中,如果任何涉及的分片包含仲裁程序,则跨多个分片的事务将出错并终止。

时间限制
要指定时间限制,请指定commitTransaction上的maxTimeMS限制。
如果maxTimeMS未指定,MongoDB将使用transactionLifetimeLimitSeconds。
如果指定了maxTimeMS,但是会导致事务超过transactionLifetimeLimitSeconds, MongoDB将使用transactionLifetimeLimitSeconds。
要修改切分集群的transactionLifetimeLimitSeconds,必须修改所有碎片复制集成员的参数。

3.阅读问题

多文档事务支持“本地”、“多数”和“快照”读取关注级别。
对于分片集群上的事务,只有“快照”读关注提供了跨多个分片的一致快照。
有关读取关注点和事务的更多信息,请参见事务和读取关注点。

4.写问题

您不能在分片集群上运行事务,分片的writeConcernMajorityJournalDefault设置为false(例如带有使用内存存储引擎的投票成员的分片)。

请注意:
不管为事务指定的写关注点是什么,sharded集群事务的提交操作都包括一些使用{w: "majority", j: true}写关注点的部分。

5.Arbiters

如果任何事务操作读取或写入包含仲裁程序的碎片,则其写入操作跨越多个碎片的事务将出错并终止。
请参阅三个成员主-副-仲裁碎片,了解对碎片的事务限制,这些碎片已禁用了read concern majority。

6.三个成员的主要-次要-仲裁碎片

对于包含三个成员的PSA碎片的分片集群,您可能已经禁用了read关注点“多数”(例如——enableMajorityReadConcern false或复制)。enableMajorityReadConcern: false)来避免缓存压力。

分片的集群,

  • 如果一个事务涉及到一个已经禁用了read concern“majority”的碎片,那么您就不能为事务使用read concern“snapshot”。对于事务,您只能使用read concern“local”或“majority”。如果使用“快照”,则事务错误和中止。
readConcern level 'snapshot' is not supported in sharded clusters when enableMajorityReadConcern=false.
  • 如果事务的任何读或写操作涉及到已禁用读的碎片,那么其写操作跨越多个碎片的事务将出错并终止。

检查读关心“多数”是否被禁用,

您可以运行db.serverStatus()并检查存储引擎。supportsCommittedReads字段。如为假,则阅读关系“多数”被禁用。

7.备份和恢复

警告:
对于正在处理切分事务的4.2+切分集群,mongodump和mongorestore不能作为备份策略的一部分,因为用mongodump创建的备份不能维护跨切分事务的原子性保证。
对于包含正在进行的分片事务的4.2+分片集群,使用以下协调的备份和恢复进程之一,这些进程确实维护了分片事务的原子性保证:

8. 块迁移

块迁移在某些阶段需要独占收集锁。
如果一个正在进行的事务对一个集合有一个锁,并且涉及该集合的块迁移已经启动,那么这些迁移阶段必须等待事务释放对集合的锁,从而影响块迁移的性能。

如果一个块迁移与一个事务交错(例如,如果一个事务在块迁移已经开始的时候启动,并且在事务对集合进行锁定之前完成迁移),则提交和中止期间的事务错误。
根据这两个操作的交错方式,一些示例错误包括(错误消息已被缩写):

  • 来自集群数据放置更改的错误…<名称空间>的迁移提交正在进行中
  • 无法找到shardId块在集群时属于…

9.提交期间的外部读取

在事务的提交期间,外部读取操作可能尝试读取将被事务修改的相同文档。如果事务写入多个碎片,那么在跨碎片的提交尝试期间

  • 外部读取使用读关注快照或“线性化的”,或者是导致一致的会话的一部分(即包括afterClusterTime),等待事务的所有写都是可见的。
  • 使用其他读关注点的外部读取不是等待事务的所有写都可见,而是读取可用文档的事务前版本。

10.与复制索引构建的交互

对于集合上的复制索引构建(与滚动索引构建相反),一旦针对主复制集成员发出的索引构建完成,次要成员将应用相关的oplog条目并开始索引构建。在构建索引时,次要服务器等待应用任何后续的oplog条目,包括在构建期间提交的分布式事务。如果复制停止的时间比oplog窗口的时间长,则次要服务器将失去同步,需要重新同步才能恢复。

为了最小化分片事务和索引之间的潜在交互,请考虑以下在分片集群上构建索引的策略之一:

  • 在应用程序停止对被索引的集合发出分布式事务的维护窗口期间构建索引。
  • 使用在切分集群上构建索引中描述的滚动索引构建过程构建索引。
  • 增加每个复制集成员上的oplog大小,以减少由于复制索引构建而导致不同步的可能性。

四、事务和操作

事务:

可以在现有集合上指定读/写(CRUD)操作。集合可以在不同的数据库中。有关CRUD操作的列表,请参阅CRUD操作。

  • 你不能写封顶的集合。(在MongoDB 4.2中启动)
  • 无法在配置、管理或本地数据库中读写集合。
  • 您不能向系统写入。*集合。
  • 您不能返回支持的操作的查询计划(即explain)。
  • 对于在事务外部创建的游标,不能在事务内部调用getMore。
  • 对于在事务中创建的游标,不能在事务外部调用getMore。
  • 从MongoDB 4.2开始,您不能将kill游标指定为事务中的第一个操作。

在多文档事务中不允许创建或删除集合或索引等影响数据库编目的操作。例如,多文档事务不能包含会导致创建新集合的插入操作。看到限制操作。

1.多文档事务中支持的操作

CRUD操作
以下的读/写操作在事务中是允许的:

MethodCommandNote
db.collection.aggregate()aggregate

不包括以下阶段:

db.collection.countDocuments() 

排除以下查询操作符表达式:

该方法为查询使用$match聚合阶段,为执行计数使用$sum表达式的$group聚合阶段。

db.collection.distinct()distinct可在未分片的集合上使用。
对于切分集合,使用$group阶段的聚合管道。看到不同的操作。
db.collection.find()find 
 geoSearch 

db.collection.deleteMany()

db.collection.deleteOne()

db.collection.remove()

delete 

db.collection.findOneAndDelete()

db.collection.findOneAndReplace()

db.collection.findOneAndUpdate()

findAndModify对于upsert,仅在对现有集合运行时使用。

db.collection.insertMany()

db.collection.insertOne()

db.collection.insert()

insert仅当对现有集合运行时。
db.collection.save() 如果是插入,则仅在对现有集合运行时使用。

db.collection.updateOne()

db.collection.updateMany()

db.collection.replaceOne()

db.collection.update()

update对于upsert,仅在对现有集合运行时使用。

db.collection.bulkWrite()

Various Bulk Operation Methods

 对于插入操作,仅在对现有集合运行时使用。
对于upsert,仅在对现有集合运行时使用。

更新碎片键值
从MongoDB 4.2开始,您可以通过在事务中或以retryable write的形式发出单文档update/findAndModify操作来更新文档的碎片键值(除非碎片键字段是不可变的_id字段)。有关详细信息,请参见更改文档的碎片键值。

操作数
要在事务中执行计数操作,请使用$count聚合阶段或$group(带有$sum表达式)聚合阶段。
与4.0特性兼容的MongoDB驱动程序提供了一个集合级API countDocuments(filter, options)作为一个助手方法,它使用带有$sum表达式的$group来执行计数。4.0驱动程序已经弃用了count() API。
从MongoDB 4.0.3开始,mongo shell提供了db.collection.countDocuments()帮助方法,该方法使用带有$sum表达式的$group来执行计数。

不同的操作

在事务中执行不同的操作:

  • 对于未切分的集合,可以使用db.collection.distinct()方法/ distinct命令以及$group阶段的聚合管道。
  • 对于切分集合,您不能使用db.collection.distinct()方法或distinct命令。

要查找切分集合的不同值,请使用包含$group stage的聚合管道。例如:

  • 使用而不是db.coll.distinct(“x”)
db.coll.aggregate([
   { $group: { _id: null, distinctValues: { $addToSet: "$x" } } },
   { $project: { _id: 0 } }
])
  • 而不是db.coll。distinct("x", {status: "A"}),使用:
db.coll.aggregate([
   { $match: { status: "A" } },
   { $group: { _id: null, distinctValues: { $addToSet: "$x" } } },
   { $project: { _id: 0 } }
])

该管道将光标返回到文档:

{ "distinctValues" : [ 2, 3, 1 ] }

迭代游标以访问结果文档。

信息操作
信息命令,如isMaster、buildInfo、connectionStatus(及其助手方法),在事务中是允许的;但是,它们不能是事务中的第一个操作。
限制操作

以下操作在交易中是不允许的:

  • 影响数据库编目的操作,如创建或删除集合或索引。例如,事务不能包含会导致创建新集合的插入操作。listCollections和listIndexes命令及其辅助方法也被排除在外。
  • 非crud和非信息操作,如createUser、getParameter、count等及其助手。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值