1.mongodb实现事务的方法
mongoDB数据库中操作单个文档总是原子性的,然而,涉及多个文档的操作,通常被作为一个“事务”,而不是原子性的。因为文档可以是相当复杂并且包含多个嵌套文档,单文档的原子性对许多实际用例提供了支持。尽管单文档操作是原子性的,在某些情况下,需要多文档事务。在这些情况下,使用两阶段提交,提供这些类型的多文档更新支持。MongoDB的两阶段提交实际上是通过使用另外一个表存储中间状态控制事务的原子性和一致性。官网解释:https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/
2.mongodb实现事务的例子
以从A账户转100给B帐户作为例子来看如何通过两阶段提交的方式实现事务
2.1 转账初始化
a.初始化两个帐号A,B:
db.accounts.insert( [ { _id: "A", balance: 1000, pendingTransactions: [] }, { _id: "B", balance: 1000, pendingTransactions: [] } ] )
b.使用另外一个表初始化事务状态(包含两个账户_id,转账金额,当前状态,上次修改时间):
db.transactions.insert( { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() } )
2.2 开始事务
a.设置事务状态为pending状态并设置修改时间:
var t = db.transactions.findAndModify( { query: { state: "initial" }, update: { $set: { state: "pending"}, $currentDate: { lastModified: true } }, new: true } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1,如果nMatched和nModified是0,重新执行2.1步骤开启一个新的事务
b.更新账户A减少100,加入了查询条件pendingTransactions,通过事务_id避免事务的重复执行
db.accounts.update( { _id: t.source, pendingTransactions: { $ne: t._id } }, { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
c.更新账户B增加100,加入了查询条件pendingTransactions,通过事务_id避免事务的重复执行
db.accounts.update( { _id: t.destination, pendingTransactions: { $ne: t._id } }, { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
2.3 更新事务已经生效
a.更新事务执行状态为已生效以及时间为当前时间
db.transactions.update( { _id: t._id, state: "pending" }, { $set: { state: "applied" }, $currentDate: { lastModified: true } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
b.更新A账户并删掉依赖的事务_id
db.accounts.update( { _id: t.source, pendingTransactions: t._id }, { $pull: { pendingTransactions: t._id } } )
c.更新B账户并删掉依赖的事务_id
db.accounts.update( { _id: t.destination, pendingTransactions: t._id }, { $pull: { pendingTransactions: t._id } } )
2.4 更新事务为已完成
a.更新事务执行状态为已完成以及时间为当前时间
db.transactions.update( { _id: t._id, state: "applied" }, { $set: { state: "done" }, $currentDate: { lastModified: true } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
2.5 事务失败后的恢复
2.5.1 pending阶段失败的恢复
如果在上面的2.2步骤中的事务失败了,当前状态是pending状态,恢复方式是重新执行2.2步骤中的更新用户账户信息步骤,例如每隔三十分钟检查之前还处于pending的事务,重新执行2.2步骤中的更新用户账户信息步骤:
var dateThreshold = new Date(); dateThreshold.setMinutes(dateThreshold.getMinutes() - 30); var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );
2.5.2 applied阶段失败的恢复
如果在上面2.3步骤中的事务失败了,当前状态是applied状态,恢复方式是重新执行2.3步骤中的删除账户依赖的事务_id,例如每隔三十分钟检查之前还处于pending的事务,重新执行2.3步骤中的删除账户依赖的事务_id:
var dateThreshold = new Date(); dateThreshold.setMinutes(dateThreshold.getMinutes() - 30); var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );
2.6 事务的回滚
2.6.1 pending阶段的回滚
如果事务处在pending阶段需要回滚,需要执行以下步骤
a.更新事务状态为正在取消以及修改时间:
db.transactions.update( { _id: t._id, state: "pending" }, { $set: { state: "canceling" }, $currentDate: { lastModified: true } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
b.账户B执行与以前相反的操作,增加pendingTransactions条件的目的是只回滚已经执行过的操作:
db.accounts.update( { _id: t.destination, pendingTransactions: t._id }, { $inc: { balance: -t.value }, $pull: { pendingTransactions: t._id } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
c.账户A执行与以前相反的操作,增加pendingTransactions条件的目的是只回滚已经执行过的操作:
db.accounts.update( { _id: t.source, pendingTransactions: t._id }, { $inc: { balance: t.value}, $pull: { pendingTransactions: t._id } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
d.更新事务执行状态为已取消:
db.transactions.update( { _id: t._id, state: "canceling" }, { $set: { state: "cancelled" }, $currentDate: { lastModified: true } } )
如果更新成功,会返回WriteResult对象中的nMatched和nModified的值为1
2.6.2 applied阶段的回滚
如果事务处在applied阶段需要回滚,需要执行以下步骤
a.把当前事务执行结束
b.再开启一个新的事务,把事务执行的源和目标互换,执行一个相反的事务