基础
mongodb所有的针对单个文档的操作都具有原子性,但是当一个操作涉及多个文档更新的时候,是非原子性的。但在生产环境中,是存在多文档更新的需求的,一般情况下,包含以下两个方面:
- 原子性:如果任何一个操作失败,所有已经进行的操作全部回滚,并且中断接下来的所有操作
- 一致性:如果操作执行过程遭遇中断,例如断电,数据库要有能力保持数据一致性
为了解决多文档更新事务性问题,可以使用两段提交的方式处理。两段提交可以保证数据是一致的,而且可以保证遇到错误之后可以恢复,但是在执行期间,文档本身处于待定状态
注意:因为mongodb只有单文档操作具有事务性,两段提交只能提供类似原子操作的语义,而不是提供完善的事务机制
应用示例
场景:
银行账户A转账给银行账户B
该场景会使用两个集合:
- accounts 记录用户信息
- transactions 记录转账事务状态
初始化账户信息
1 | db.accounts.insert( |
初始化转账记录表
1 | db.transactions.insert( |
查找转账事务
1 | var t = db.transactions.findOne( { state: "initial" } ) |
将转账事务状态由initial改为pending
1 | db.transactions.update( |
将事物状态记录到账户信息中
1 | db.accounts.update( |
将转账事务状态由pending改为applied
1 | db.transactions.update( |
将事务状态从账户信息中移除
1 | db.accounts.update( |
将转账事务状态由applied改为done
1 | db.transactions.update( |
以上就是整个转账流程
故障恢复
转账流程不是最重要的,最重要的是从故障恢复整个转账流程。下面介绍如何从各个阶段恢复转账流程
从pending
状态恢复
pending
状态就是确定要开始转账,而且已经开始更新AB账户的数据了,但是从这里到转账状态变为applied
期间,出现了问题
以下是从pending
状态恢复的过程
找到至少30分钟都未更新的而且state是pending
的转账状态
1 | var dateThreshold = new Date(); |
然后从进行转账
这一步骤重新执行即可
从applied
状态恢复
找到至少30分钟都未更新的而且state是applied
的转账状态
1 | var dateThreshold = new Date(); |
然后从更新用户信息
这一步骤重新执行即可
故障回滚
从applied
状态进行回滚
当转账进行到applied
状态时,不推荐回滚了,可以直接恢复这个转账事务,然后再新建一个退款的事务,把钱再转回去
从pending
状态回滚
将转账状态更新为canceling
1 | db.transactions.update( |
取消AB账户的信息修改
1 | db.accounts.update( |
1 | db.accounts.update( |
将转账状态改为canceled
1 | db.transactions.update( |
多个应用冲突考虑
通常来说,事务的存在,就能保证多个应用共同启动而且不会影响到数据的一致性。上述的两段提交过程中,根据state
字段的值去更新事务状态,就能保证在多应用情况下重复操作产生的问题。(因为mongodb的单条更新操作是原子性的,一条更新成功了,其他所有基于state
的查询没法查到数据)
举个例子:app1和app2同时去更新一个转账事务的状态,但是app1先于app2,当app1更新完成之后,事务状态已经被更新为pending
,这时候app2再去更新,会因为查询条件不匹配而终止当前事务的执行。
简单来说,解决多应用问题最重要的一点就是保证在同一时刻,同一个事物只能由同一个应用处理。为了保证这一点,可以加一个功能类似于state
的字段application
,用于标识当前事务属于哪个应用。
例如下面的代码,将事务由initial
状态改为pending
状态1
2
3
4
5
6
7
8
9
10
11t = db.transactions.findAndModify(
{
query: { state: "initial", application: { $exists: false } },
update:
{
$set: { state: "pending", application: "App1" },
$currentDate: { lastModified: true }
},
new: true
}
)
如果本次事务执行失败了,可以使用上面的恢复步骤进行恢复,但是必须保证恢复时也是由原来的应用去执行,例如1
2
3
4
5
6
7
8
9
10var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find(
{
application: "App1",
state: "pending",
lastModified: { $lt: dateThreshold }
}
)
总结
以上就是通过两步提交在mongodb中实现多谢如操作事务的原型,在生产环境中情况可能会更加复杂,所以必须根据实际情况而定