1. 引言
在开发中,事务处理非常常见,数据库作为存储数据的重要工具之一,同样也需要支持事务处理。而在mongoDB 4.0之前,它的事务处理一直不够完善,只支持单个文档的事务处理,无法支持多文档的事务。而mongoDB 4.0则推出了对多文档事务的支持。然而,使用事务处理涉及到问题的可能性也增加了,尤其是在事务处理失败回滚的情况下。本文就是探究mongoDB 4.0事务回滚的辛酸历程。
2. mongoDB 4.0事务回滚
2.1 mongoDB 4.0多文档事务介绍
mongoDB 4.0多文档事务的引入是为了解决多个文档之间的事务关系。在mongodb中,多文档事务可以涉及一个或多个集合,就是如果在一个事务中要修改多个集合的数据,那么这些修改必须要作为一个本地事务来执行。这里说的“本地事务”是指非分布式事务,即应用程序的操作发生在单一数据库节点上,不跨越多个分片。
2.2 事务失败回滚的场景
在事务执行过程中,如果任意一个操作失败了,那么已成功的操作都需要回滚,保证数据的一致性。下面举一个mongoDB 4.0事务失败回滚的场景:
在一个事务包含多个文档操作的过程中,如果其中某一个文档的操作失败,那么已在事务中成功执行的其他文档操作都要进行回滚。以下是一个相关的代码示例:
session = client.start_session()
with session.start_transaction():
try:
collection1.update_one({"_id": 1}, {"$set": {"price": 200}})
collection2.update_one({"_id": 2}, {"$set": {"price": 400}})
collection3.update_one({"_id": 3}, {"$set": {"price": 600}})
inv_collection.insert_one({"item": "cd", "qty": 10})
inv_collection.update_one({"item": "cd"}, {"$inc": {"qty": -3}})
except:
session.abort_transaction()
上述代码中,collection1、collection2、collection3 都执行了update操作,而在inv_collection中执行了insert和update操作。如果在其中的任意一个操作失败,那么之前执行的操作都需要回滚。
2.3 事务失败回滚的辛酸事例
假设一个开发中的实际场景:在进行银行账户的转账时,一个事务包含两个操作:操作1从用户A的账户中取出50元,操作2将50元转入到用户B的账户。如果在进行操作1时,mongoDB连接异常断开,那么A账户中已扣除的50元不能回滚,B账户中还没有增加50元的操作也不能实现。这是因为mongoDB在进行事务回滚的过程中,无法回滚在外部的动作,比如现实世界中的银行账户。
在实现转账事务的过程中,我们并不想用户A的账户有负数余额出现。所以,控制用户A的账户余额的代码例子如下:
def transfer_within_transaction(client, f_acc_id, t_acc_id, amount):
session = client.start_session()
with session.start_transaction():
try:
from_account = client.banking_db.accounts.find_one({"_id": f_acc_id}, session=session)
if from_account["balance"] - amount < 0:
raise ValueError("Insufficient funds in account {}".format(f_acc_id))
res = client.banking_db.accounts.update_one({"_id": f_acc_id}, {"$inc": {"balance": -amount}}, session=session)
if res.modified_count == 0:
raise ValueError("Could not find account {}".format(f_acc_id))
res = client.banking_db.accounts.update_one({"_id": t_acc_id}, {"$inc": {"balance": amount}}, session=session)
if res.modified_count == 0:
raise ValueError("Could not find account {}".format(t_acc_id))
except:
session.abort_transaction()
print("Transaction aborted: {}".format(sys.exc_info()))
from_account = client.banking_db.accounts.find_one({"_id": f_acc_id})
to_account = client.banking_db.accounts.find_one({"_id": t_acc_id})
print("Balance of {} after transfer: ${}".format(from_account["_id"], from_account["balance"]))
print("Balance of {} after transfer: ${}".format(to_account["_id"], to_account["balance"]))
上述代码中的transfer_within_transaction维护了session对象,在分配session之后开启了一个mongoDB事务。即使开启了事务并分配了session,实际的数据库操作还是非常不确定的。如果在事务执行过程中出现问题,我们需要中止事务并进行回滚操作。因此,在try-catch块中捕获异常,如果异常抛出则通过session.abort_transaction() function来中止事务并回滚:
except:
session.abort_transaction()
print("Transaction aborted: {}".format(sys.exc_info()))
然而,在这里回滚并不能解决所有的问题。如果在异常发生的时候,实际上用户A的账户已经扣除了相应的50元,这时无论如何对mongoDB的回滚操作都无法将已取的50元退回。因此,在使用mongoDB数据库进行事务操作时,非常重要的地方在于确保所有的账户能够被事务地监测并操作,同时保证回滚操作对用户A和B的账户余额都有所影响。
3. 结论
本文探究了mongoDB 4.0事务回滚的辛酸历程。在mongoDB 4.0版本之前,mongoDB只支持单个文档的事务处理,无法支持多文档的事务。而mongoDB 4.0则推出了对多文档事务的支持,在事务执行过程中,如果任意一个操作失败了,那么已成功的操作都需要回滚,保证数据的一致性。但mongoDB在进行事务回滚的过程中,无法回滚在外部的动作,因此,在使用mongoDB数据库进行事务操作时,必须确保所有的账户能够被事务地监测并操作,并且保证回滚操作能对所有账户的余额都有所影响。