【Spring连载】使用Spring Data访问 MongoDB(七)----Sessions & Transactions
从3.6版本开始,MongoDB支持会话的概念。会话的使用启用了MongoDB的 因果一致性(Causal Consistency)模型,该模型保证操作以尊重其因果关系的顺序运行。这些实例分为ServerSession实例和ClientSession实例。在本节中,当我们谈到session时,我们指的是ClientSession。
客户端会话内的操作不会与会话外的操作隔离。
MongoOperations和ReactiveMongoOperations都提供了将ClientSession绑定到操作的网关方法。MongoCollection和MongoDatabase使用实现了MongoDB的集合和数据库接口的会话代理对象,因此你不需要在每个调用上添加会话。这意味着对MongoCollection#find()的潜在调用被委托给MongoCollection#find(ClientSession)。
(Reactive)MongoOperations#getCollection等方法返回native MongoDB Java Driver网关对象(如MongoCollection),这些对象本身为ClientSession提供了专用方法。这些方法不是会话代理的。当直接与MongoCollection或MongoDatabase交互时,应该在需要的地方提供ClientSession,而不是通过对MongoOperations的#execute回调之一提供。
一、ClientSession支持
下面的例子展示了会话的用法:
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
1. 从服务器获取新的会话。
2. 像以前一样使用MongoOperation方法。自动应用ClientSession。
3. 确保关闭ClientSession。
4. 关闭会话。
在处理DBRef实例时,尤其是延迟加载的实例,在加载所有数据之前不要关闭ClientSession是很重要的。否则,lazy fetch失败。
二、MongoDB事务
从版本4开始,MongoDB支持事务。事务建立在会话(Sessions)之上,因此需要一个active ClientSession。
除非在应用程序上下文中指定MongoTransactionManager,否则事务支持是关闭的。你可以使用setSessionSynchronization(ALWAYS)来参与正在进行的non-native MongoDB事务。
为了获得对事务的完全编程式控制,你可能需要在MongoOperations上使用会话回调。
以下示例显示了编程式事务控制:
编程式事务
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
1. 获取新的ClientSession。
2. 启动事务。
3. 如果一切如预期,提交更改。
4. 如果出问题了,把所有东西都倒回去。
5. 完成后不要忘记关闭会话。
前面的示例使你能够完全控制事务行为,同时在回调中使用会话范围的MongoOperations实例,以确保会话传递到每个服务器调用。为了避免这种方法带来的一些开销,可以使用TransactionTemplate来消除手动事务流的一些噪音。
三、使用TransactionTemplate / TransactionalOperator的事务
Spring Data MongoDB事务同时支持TransactionTemplate和TransactionalOperator。
使用TransactionTemplate/TransactionalOperator的事务
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", // ...
};
});
1. 在Template API配置期间启用事务同步。
2. 使用提供的PlatformTransactionManager创建TransactionTemplate。
3. 在回调中,已经注册了ClientSession和事务。
在运行时更改MongoTemplate的状态(你可能会认为在前面列表的第1项中是可能的)可能会导致线程和可见性问题。
四、使用MongoTransactionManager/ReactiveMongoTransactionManager的事务
MongoTransactionManager/ReactiveMongoTransactionManager是通往众所周知的Spring事务支持的网关。它允许应用程序使用Spring的托管事务功能。MongoTransactionManager将ClientSession绑定到线程,而ReactiveMongoTransactionManager则使用ReactorContext。MongoTemplate检测会话并相应地对与事务相关联的这些资源进行操作。MongoTemplate还可以参与其他正在进行的事务。以下示例显示如何使用MongoTransactionManager创建和使用事务:
使用MongoTransactionManager/ReactiveMongoTransactionManager的事务
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@Bean
MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { --------1
return new MongoTransactionManager(dbFactory);
}
// ...
}
@Component
public class StateService {
@Transactional
void someBusinessFunction(Step step) { --------2
template.insert(step);
process(step);
template.update(Step.class).apply(Update.set("state", // ...
};
});
1. 在应用程序上下文中注册MongoTransactionManager。
2. 将方法标记为事务性方法。
@Transactional(readOnly = true)通知MongoTransactionManager也启动一个将ClientSession添加到传出请求的事务。
五、事务内部的特殊行为
在事务内部,MongoDB服务器的行为略有不同。
5.1 连接设置
MongoDB drivers 提供了一个专用的副本集(replica set)名称配置选项,使driver进入自动检测模式。此选项有助于在事务期间识别主复制集节点和命令路由。
请确保将replicaSet添加到MongoDB URI中。有关更多详细信息,请参阅连接字符串选项。
5.2 集合操作
MongoDB不支持事务中的集合操作,例如创建集合。这也会影响首次使用时的动态集合创建。因此,确保所有必需的结构都已就位。
5.3 暂态错误
MongoDB可以为事务操作过程中出现的错误添加特殊标签。这些可能表示暂时性故障,这些故障可能仅通过重试操作而消失。出于这些目的,我们强烈建议使用Spring Retry。然而,可以覆盖MongoTransactionManager#doCommit(MongoTransactionObject) ,以实现MongoDB参考手册中概述的重试提交操作行为。
5.4 计数count
MongoDB count是根据收集统计数据进行操作的,这些统计数据可能无法反映事务中的实际情况。当在多document事务内部发出count命令时,服务器以错误50851进行响应。一旦MongoTemplate检测到active事务,所有公开的count()方法都会使用$match和$count运算符转换并委托给聚合框架,从而保留查询设置,如collation。
在聚合count帮助程序内部使用地理命令时会应用限制。以下运算符不能使用,必须用其他运算符替换:
- $where→ $expr
- $near→ $geoWithin with $center
- $nearSphere→ $geoWithin with $centerSphere
使用Criteria.near(…)和Criteria.nearSphere(…)的查询必须重写为Criteria.within(…)各自的Criteria.withinSphere(…)。这同样适用于存储库查询方法中的near query关键字,该方法必须更改为within。另请参阅MongoDB JIRA ticket DRIVERS-518以获取更多参考。
以下代码段显示了会话绑定闭包中的count使用情况:
session.startTransaction();
template.withSession(session)
.execute(action -> {
action.count(query(where("state").is("active")), Step.class)
...
上面的代码片段由下面的命令中实现:
db.collection.aggregate(
[
{ $match: { state: "active" } },
{ $count: "totalEntityCount" }
]
)
而不是:
db.collection.find( { state: "active" } ).count()