会话与事务
自 3.6 版本起,MongoDB 支持会话概念。使用会话可启用 MongoDB 的因果一致性模型,该模型保证按照尊重操作间因果关系的顺序运行操作。会话分为 ServerSession 实例和 ClientSession 实例。在本节中,当我们提及会话时,指的是 ClientSession。
客户端会话内的操作与会话外的操作之间不相互隔离。
MongoOperations 与 ReactiveMongoOperations 都提供了将 ClientSession 绑定到操作的网关方法。MongoCollection 和 MongoDatabase 使用实现了 MongoDB 集合与数据库接口的会话代理对象,因此您无需在每次调用时都添加会话。这意味着对 MongoCollection#find() 的潜在调用会被委托给 MongoCollection#find(ClientSession)。
诸如 (Reactive)MongoOperations#getCollection 之类的方法返回原生 MongoDB Java 驱动程序的网关对象(例如 MongoCollection),这些对象本身提供了用于 ClientSession 的专用方法。这些方法不是会话代理。当您直接与 MongoCollection 或 MongoDatabase 交互(而非通过 MongoOperations 上的 #execute 回调)时,应在需要处提供 ClientSession。
ClientSession 支持
以下示例展示了会话的用法:
- Imperative
- Reactive
ClientSessionOptions sessionOptions = ClientSessionOptions.builder()
.causallyConsistent(true)
.build();
ClientSession session = client.startSession(sessionOptions); 1
template.withSession(() -> session)
.execute(action -> {
Query query = query(where("name").is("Durzo Blint"));
Person durzo = action.findOne(query, Person.class); 2
Person azoth = new Person("Kylar Stern");
azoth.setMaster(durzo);
action.insert(azoth); 3
return azoth;
});
session.close() 4
Obtain a new session from the server.
Use
MongoOperationmethods as before. TheClientSessiongets applied automatically.Make sure to close the
ClientSession.Close the session.
When dealing with DBRef instances, especially lazily loaded ones, it is essential to not close the ClientSession before all data is loaded. Otherwise, lazy fetch fails.
ClientSessionOptions sessionOptions = ClientSessionOptions.builder()
.causallyConsistent(true)
.build();
Publisher<ClientSession> session = client.startSession(sessionOptions); 1
template.withSession(session)
.execute(action -> {
Query query = query(where("name").is("Durzo Blint"));
return action.findOne(query, Person.class)
.flatMap(durzo -> {
Person azoth = new Person("Kylar Stern");
azoth.setMaster(durzo);
return action.insert(azoth); 2
});
}, ClientSession::close) 3
.subscribe(); 4
Obtain a
Publisherfor new session retrieval.Use
ReactiveMongoOperationmethods as before. TheClientSessionis obtained and applied automatically.Make sure to close the
ClientSession.Nothing happens until you subscribe. See the Project Reactor Reference Guide for details.
By using a Publisher that provides the actual session, you can defer session acquisition to the point of actual subscription. Still, you need to close the session when done, so as to not pollute the server with stale sessions. Use the doFinally hook on execute to call ClientSession#close() when you no longer need the session. If you prefer having more control over the session itself, you can obtain the ClientSession through the driver and provide it through a Supplier.
Reactive use of ClientSession is limited to Template API usage. There’s currently no session integration with reactive repositories.
MongoDB 事务
除非您在应用上下文中指定了 MongoTransactionManager,否则事务支持处于禁用状态。您可以使用 setSessionSynchronization(ALWAYS) 来参与正在进行的非原生 MongoDB 事务。
要获得对事务的完全程序化控制,您可能希望使用 MongoOperations 上的会话回调。
以下示例展示了程序化事务控制:
- Imperative
- Reactive
ClientSession session = client.startSession(options); 1
template.withSession(session)
.execute(action -> {
session.startTransaction(); 2
try {
Step step = // ...;
action.insert(step);
process(step);
action.update(Step.class).apply(Update.set("state", // ...
session.commitTransaction(); 3
} catch (RuntimeException e) {
session.abortTransaction(); 4
}
}, ClientSession::close) 5
Obtain a new
ClientSession.Start the transaction.
If everything works out as expected, commit the changes.
Something broke, so roll back everything.
Do not forget to close the session when done.
The preceding example lets you have full control over transactional behavior while using the session scoped MongoOperations instance within the callback to ensure the session is passed on to every server call. To avoid some of the overhead that comes with this approach, you can use a TransactionTemplate to take away some of the noise of manual transaction flow.
Mono<DeleteResult> result = Mono
.from(client.startSession()) 1
.flatMap(session -> {
session.startTransaction(); 2
return Mono.from(collection.deleteMany(session, ...)) 3
.onErrorResume(e -> Mono.from(session.abortTransaction()).then(Mono.error(e))) 4
.flatMap(val -> Mono.from(session.commitTransaction()).then(Mono.just(val))) 5
.doFinally(signal -> session.close()); 6
});
First we obviously need to initiate the session.
Once we have the
ClientSessionat hand, start the transaction.Operate within the transaction by passing on the
ClientSessionto the operation.If the operations completes exceptionally, we need to stop the transaction and preserve the error.
Or of course, commit the changes in case of success. Still preserving the operations result.
Lastly, we need to make sure to close the session.
The culprit of the above operation is in keeping the main flows DeleteResult instead of the transaction outcome published via either commitTransaction() or abortTransaction(), which leads to a rather complicated setup.
Unless you specify a ReactiveMongoTransactionManager within your application context, transaction support is DISABLED. You can use setSessionSynchronization(ALWAYS) to participate in ongoing non-native MongoDB transactions.
使用 TransactionTemplate / TransactionalOperator 的事务
Spring Data MongoDB 事务同时支持 TransactionTemplate 和 TransactionalOperator。
- Imperative
- Reactive
template.setSessionSynchronization(ALWAYS); 1
// ...
TransactionTemplate txTemplate = new TransactionTemplate(anyTxManager); 2
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) { 3
Step step = // ...;
template.insert(step);
process(step);
template.update(Step.class).apply(Update.set("state", // ...
}
});
Enable transaction synchronization during Template API configuration.
Create the
TransactionTemplateusing the providedPlatformTransactionManager.Within the callback the
ClientSessionand transaction are already registered.
Changing state of MongoTemplate during runtime (as you might think would be possible in item 1 of the preceding listing) can cause threading and visibility issues.
template.setSessionSynchronization(ALWAYS); 1
// ...
TransactionalOperator rxtx = TransactionalOperator.create(anyTxManager,
new DefaultTransactionDefinition()); 2
Step step = // ...;
template.insert(step);
Mono<Void> process(step)
.then(template.update(Step.class).apply(Update.set("state", …))
.as(rxtx::transactional) 3
.then();
Enable transaction synchronization for Transactional participation.
Create the
TransactionalOperatorusing the providedReactiveTransactionManager.TransactionalOperator.transactional(…)provides transaction management for all upstream operations.
使用 MongoTransactionManager 与 ReactiveMongoTransactionManager 的事务
MongoTransactionManager / ReactiveMongoTransactionManager 是通往熟知的 Spring 事务支持的网关。它让应用能够使用 Spring 的受管事务特性。MongoTransactionManager 将 ClientSession 绑定到线程,而 ReactiveMongoTransactionManager 则为此使用 ReactorContext。MongoTemplate 会检测到该会话,并相应地对与事务关联的这些资源进行操作。MongoTemplate 也可以参与其他正在进行的事务。以下示例展示了如何使用 MongoTransactionManager 创建和使用事务:
- Imperative
- Reactive
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@Bean
MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { 1
return new MongoTransactionManager(dbFactory);
}
@Bean
MongoTemplate mongoTemplate(MongoDatabaseFactory dbFactory) { 1
return new MongoTemplate(dbFactory);
}
// ...
}
@Component
public class StateService {
@Transactional
void someBusinessFunction(Step step) { 2
template.insert(step);
process(step);
template.update(Step.class).apply(Update.set("state", // ...
};
});
Register
MongoTransactionManagerin the application context. Also, make sure to use the sameMongoDatabaseFactorywhen creatingMongoTemplateto participate in transactions in the scope of the sameMongoDatabaseFactory.Mark methods as transactional.
@Transactional(readOnly = true) advises MongoTransactionManager to also start a transaction that adds the ClientSession to outgoing requests.
@Configuration
public class Config extends AbstractReactiveMongoConfiguration {
@Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory factory) { 1
return new ReactiveMongoTransactionManager(factory);
}
@Bean
ReactiveMongoTemplate reactiveMongoTemplate(ReactiveMongoDatabaseFactory dbFactory) { 1
return new ReactiveMongoTemplate(dbFactory);
}
// ...
}
@Service
public class StateService {
@Transactional
Mono<UpdateResult> someBusinessFunction(Step step) { 2
return template.insert(step)
.then(process(step))
.then(template.update(Step.class).apply(Update.set("state", …));
};
});
Register
ReactiveMongoTransactionManagerin the application context. Also, make sure to use the sameReactiveMongoDatabaseFactorywhen creatingReactiveMongoTemplateto participate in transactions in the scope of the sameReactiveMongoDatabaseFactory.Mark methods as transactional.
@Transactional(readOnly = true) advises ReactiveMongoTransactionManager to also start a transaction that adds the ClientSession to outgoing requests.
控制 MongoDB 专属事务选项
事务性服务方法可能需要特定的事务选项才能运行事务。Spring Data MongoDB 的事务管理器支持对事务标签求值,例如 @Transactional(label = { "mongo:readConcern=available" })。
默认情况下,使用 mongo: 前缀的标签命名空间由默认配置的 MongoTransactionOptionsResolver 求值。事务标签由 TransactionAttribute 提供,并可通过 TransactionTemplate 和 TransactionalOperator 供程序化事务控制使用。由于 @Transactional(label = …) 具有声明式特性,它提供了一个良好的起点,同时也可作为文档使用。
目前支持以下选项:
最大提交时间
控制 commitTransaction 操作在服务端的最大执行时间。该值的格式对应于 Duration.parse(…) 所用的 ISO-8601 持续时间格式。
用法:mongo:maxCommitTime=PT1S
读关注
设置事务的读关注。
用法:mongo:readConcern=LOCAL|MAJORITY|LINEARIZABLE|SNAPSHOT|AVAILABLE
读取偏好
设置事务的读取偏好。
用法:mongo:readPreference=PRIMARY|SECONDARY|SECONDARY_PREFERRED|PRIMARY_PREFERRED|NEAREST
写关注
设置事务的写关注。
用法:mongo:writeConcern=ACKNOWLEDGED|W1|W2|W3|UNACKNOWLEDGED|JOURNALED|MAJORITY
加入外部事务的嵌套事务不会影响初始事务选项,因为事务已经启动。事务选项仅在启动新事务时应用。
事务内部的特殊行为
在事务内部,MongoDB 服务端的行为略有不同。
连接设置
MongoDB 驱动程序提供了一个专用的副本集名称配置选项,可将驱动程序切换为自动检测模式。该选项有助于在事务期间识别主副本集节点并进行命令路由。
请务必将 replicaSet 添加到 MongoDB URI 中。更多详情请参阅连接字符串选项。
集合操作
MongoDB 不支持在事务内进行集合操作(例如创建集合)。这也会影响首次使用时动态进行的集合创建。因此,请确保所有所需结构都已就位。
瞬时错误
MongoDB 会为事务操作期间抛出的错误添加特殊标签。这些标签可能表示瞬时故障,仅通过重试操作即可消除。我们强烈建议为此使用 Spring Retry。尽管如此,您也可以重写 MongoTransactionManager#doCommit(MongoTransactionObject),以实现 MongoDB 参考手册中所述的重试提交操作行为。
计数
MongoDB count 基于集合统计信息运行,而这些统计信息可能无法反映事务内的实际情况。在多文档事务内发出 count 命令时,服务端会返回_错误 50851_。一旦 MongoTemplate 检测到活动事务,所有暴露的 count() 方法都会被转换,并使用 $match 和 $count 操作符委托给聚合框架,同时保留 Query 设置(例如 collation)。
在聚合计数助手内部使用地理命令时存在限制。以下操作符不能使用,必须替换为其他操作符:
-
$where→$expr -
$near→ 带$center的$geoWithin -
$nearSphere→ 带$centerSphere的$geoWithin
使用 Criteria.near(…) 和 Criteria.nearSphere(…) 的查询必须分别重写为 Criteria.within(…) 和 Criteria.withinSphere(…)。仓库查询方法中的 near 查询关键词也同样必须改为 within。另请参阅 MongoDB JIRA 工单 DRIVERS-518 以了解更多参考。
以下片段展示了在会话绑定闭包内的 count 用法:
session.startTransaction();
template.withSession(session)
.execute(ops -> {
return ops.count(query(where("state").is("active")), Step.class)
});
上述片段会具体化为以下命令:
db.collection.aggregate(
[
{ $match: { state: "active" } },
{ $count: "totalEntityCount" }
]
)
而非:
db.collection.find( { state: "active" } ).count()