Kivi

没有什么远大理想,只是永远都不会满足而已


  • 首页

  • 关于

  • 标签

  • 归档

mongodb两段提交

发表于 2017-06-03 更新于 2017-07-08 分类于 mongodb 阅读次数:
本文字数: 4.8k 阅读时长 ≈ 4 分钟

基础

mongodb所有的针对单个文档的操作都具有原子性,但是当一个操作涉及多个文档更新的时候,是非原子性的。但在生产环境中,是存在多文档更新的需求的,一般情况下,包含以下两个方面:

  • 原子性:如果任何一个操作失败,所有已经进行的操作全部回滚,并且中断接下来的所有操作
  • 一致性:如果操作执行过程遭遇中断,例如断电,数据库要有能力保持数据一致性

为了解决多文档更新事务性问题,可以使用两段提交的方式处理。两段提交可以保证数据是一致的,而且可以保证遇到错误之后可以恢复,但是在执行期间,文档本身处于待定状态

注意:因为mongodb只有单文档操作具有事务性,两段提交只能提供类似原子操作的语义,而不是提供完善的事务机制

应用示例

场景:

银行账户A转账给银行账户B

该场景会使用两个集合:

  • accounts 记录用户信息
  • transactions 记录转账事务状态

初始化账户信息

1
2
3
4
5
6
db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
)

初始化转账记录表

1
2
3
db.transactions.insert(
{ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)

查找转账事务

1
var t = db.transactions.findOne( { state: "initial" } )

将转账事务状态由initial改为pending

1
2
3
4
5
6
7
db.transactions.update(
{ _id: t._id, state: "initial" },
{
$set: { state: "pending" },
$currentDate: { lastModified: true }
}
)

将事物状态记录到账户信息中

1
2
3
4
5
6
7
8
9
db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)

db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)

将转账事务状态由pending改为applied

1
2
3
4
5
6
7
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "applied" },
$currentDate: { lastModified: true }
}
)

将事务状态从账户信息中移除

1
2
3
4
5
6
7
8
9
db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)

db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)

将转账事务状态由applied改为done

1
2
3
4
5
6
7
db.transactions.update(
{ _id: t._id, state: "applied" },
{
$set: { state: "done" },
$currentDate: { lastModified: true }
}
)

以上就是整个转账流程

故障恢复

转账流程不是最重要的,最重要的是从故障恢复整个转账流程。下面介绍如何从各个阶段恢复转账流程

从pending状态恢复

pending状态就是确定要开始转账,而且已经开始更新AB账户的数据了,但是从这里到转账状态变为applied期间,出现了问题

以下是从pending状态恢复的过程

找到至少30分钟都未更新的而且state是pending的转账状态

1
2
3
4
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );

然后从进行转账这一步骤重新执行即可

从applied状态恢复

找到至少30分钟都未更新的而且state是applied的转账状态

1
2
3
4
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );

然后从更新用户信息这一步骤重新执行即可

故障回滚

从applied状态进行回滚

当转账进行到applied状态时,不推荐回滚了,可以直接恢复这个转账事务,然后再新建一个退款的事务,把钱再转回去

从pending状态回滚

将转账状态更新为canceling

1
2
3
4
5
6
7
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "canceling" },
$currentDate: { lastModified: true }
}
)

取消AB账户的信息修改

1
2
3
4
5
6
7
db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{
$inc: { balance: -t.value },
$pull: { pendingTransactions: t._id }
}
)
1
2
3
4
5
6
7
db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{
$inc: { balance: t.value},
$pull: { pendingTransactions: t._id }
}
)

将转账状态改为canceled

1
2
3
4
5
6
7
db.transactions.update(
{ _id: t._id, state: "canceling" },
{
$set: { state: "cancelled" },
$currentDate: { lastModified: true }
}
)

多个应用冲突考虑

通常来说,事务的存在,就能保证多个应用共同启动而且不会影响到数据的一致性。上述的两段提交过程中,根据state字段的值去更新事务状态,就能保证在多应用情况下重复操作产生的问题。(因为mongodb的单条更新操作是原子性的,一条更新成功了,其他所有基于state的查询没法查到数据)

举个例子:app1和app2同时去更新一个转账事务的状态,但是app1先于app2,当app1更新完成之后,事务状态已经被更新为pending,这时候app2再去更新,会因为查询条件不匹配而终止当前事务的执行。

简单来说,解决多应用问题最重要的一点就是保证在同一时刻,同一个事物只能由同一个应用处理。为了保证这一点,可以加一个功能类似于state的字段application,用于标识当前事务属于哪个应用。

例如下面的代码,将事务由initial状态改为pending状态

1
2
3
4
5
6
7
8
9
10
11
t = 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
10
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

db.transactions.find(
{
application: "App1",
state: "pending",
lastModified: { $lt: dateThreshold }
}
)

总结

以上就是通过两步提交在mongodb中实现多谢如操作事务的原型,在生产环境中情况可能会更加复杂,所以必须根据实际情况而定

参考链接

mogodb doc Perform Two Phase Commits
# mongodb
mongos cursor 的使用
天云山·北京平谷
  • 文章目录
  • 站点概览
kivi

kivi

nodejs | server
58 日志
17 分类
32 标签
RSS
  1. 1. 基础
  2. 2. 应用示例
    1. 2.1. 场景:
    2. 2.2. 初始化账户信息
    3. 2.3. 初始化转账记录表
    4. 2.4. 查找转账事务
    5. 2.5. 将转账事务状态由initial改为pending
    6. 2.6. 将事物状态记录到账户信息中
    7. 2.7. 将转账事务状态由pending改为applied
    8. 2.8. 将事务状态从账户信息中移除
    9. 2.9. 将转账事务状态由applied改为done
  3. 3. 故障恢复
    1. 3.1. 从pending状态恢复
      1. 3.1.1. 找到至少30分钟都未更新的而且state是pending的转账状态
      2. 3.1.2. 然后从进行转账这一步骤重新执行即可
    2. 3.2. 从applied状态恢复
      1. 3.2.1. 找到至少30分钟都未更新的而且state是applied的转账状态
      2. 3.2.2. 然后从更新用户信息这一步骤重新执行即可
  4. 4. 故障回滚
    1. 4.1. 从applied状态进行回滚
    2. 4.2. 从pending状态回滚
      1. 4.2.1. 将转账状态更新为canceling
      2. 4.2.2. 取消AB账户的信息修改
      3. 4.2.3. 将转账状态改为canceled
  5. 5. 多个应用冲突考虑
  6. 6. 总结
  7. 7. 参考链接
© 2019 kivi | 173k | 2:37
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Pisces v7.3.0
|