Mongodb不支持多文档原子性操作,因此依据两阶段提交协议(Two Phase Commits protocol)来模拟事务。

以两个银行账户之间的转账行为为例,来说明如何实现多文档间的事务操作。

为实现多文档间的事务操作,定义一个事务文档TransactionDocument,储存在事务集合TransactionCollection中

public class TransactionDocument2
{
public object _id { set; get; }
//原账户
public string Source { set; get; }
//目标账户
public string Destination { set; get; }
//转账金额
public decimal Value { set; get; }
//执行状态(初始化initial, 执行操作pending, 完成操作applied, 事务结束done, 正在取消操作canceling, 完成取消canceled)
public string State { set; get; }
//最后修改日期
public DateTime LastModified { set; get; }
}

银行账户的结构为:

public class Account
{
/// <summary>
/// 账号
/// </summary>
public string _id { set; get; }
/// <summary>
/// 账户余额
/// </summary>
public decimal Balance { set; get; }
/// <summary>
/// 待处理事务链表
/// </summary>
public List<object> PendingTransactions { set; get; }
}

转账的主流程为:

第0步,为参与事务的两个实体创建唯一的事务文档。

为A、B两个账户创建唯一的事务文档,事务文档的_id值为A、B账户_id值的组合。

第1步,在TransactionCollection集合中找到状态为"initial"的事务文档。

对于A、B两个账户间的转账操作,只能有一个事务文档。这样做是为了防止多个客户端同时对一个账户执行修改操作,只有一个这种事务文档,那么当AB间的转账行为开始时,事务文档的状态为“pending”,而事务开始要查找的是状态为“initial”的事务文档,因此不会获得这样的事务文档,也就不会执行任何操作,只有当AB转账操作完成后才有可能再次执行类似的操作。

第2步,第1步执行成功的前提下,将事务文档状态由“initial”更改为“pending”。

第3步,第2步执行成功的前提下,对两个账户应用事务,执行转账。

对两个账户应用事务的具体操作就是向A、B两个账户的待处理事务链表中添加事务文档_id。

第4步,第3步执行成功的前提下,将事务文档状态由“pending”更改为“applied”。

第5步,第4步执行成功的前提下,移除事务标识。

具体操作是:移除第3步中向A、B两个账户的待处理事务链表中添加的事务文档_id。

第6步,第5步执行成功的前提下,将事务文档状态由“applied”更改为“done”。

第7步,不论第6步是否执行成功,将事务文档状态“done”更改为“initial”。

看似在第6步将“applied”更改为“initial”也是可以的,但是如果在这之间在加入一个“done”状态会带来更大的好处,例如,可以定时扫描TransactionCollection集合,批量将状态为“done”的事务文档状态改为“initial”,而不是在第6步执行完成以后立即执行第7步。

辅助流程

针对上述主流程的每一步加以分析,找出需要辅助流程介入的位置。

对于第0步:

如果创建不成功不会产生任何影响。

对于第1步:

如果没有找到,不会产生任何影响。

对于第2步:

如果事务文档状态修改不成功,不会产生任何影响。

对于第3步:

如果执行转账失败,A账户的钱已被扣除V,但B没有收到V,回滚到之前的状态。

如果在指定的超时时间内没有完成则,执行从错误中恢复策略。

对于第4步:

如果修改事务文档状态失败,设置执行超时时间Th4,重复执行此步骤,如果超时时间已到达,但未完成,执行从错误中恢复策略。

对于第5步:

如果移除事务标识失败,设置执行超时时间Th5,重复执行此步骤,如果超时时间已到达,但未完成,执行从错误中恢复策略。

对于第6步:

如果移除事务标识失败,设置执行超时时间Th6,重复执行此步骤,如果超时时间已到达,但未完成,执行从错误中恢复策略。

回滚的步骤为:

第1步,将事务文档状态由“pending”更改为“canceling”。

第2步,账户余额还原为操作之前的状态,删除两个账户的待处理事务链表中的事务文档_id.

第3步,将事务文档状态由“canceling”更改为“cancelled”。

从错误中恢复策略

通过重复执行需要此策略的那一步操作即可达到目的。可以选择异步执行错误恢复机制。

超时检测

比较事务文档的LastModified 与当前时间的值,如果二者差值超过设定的阈值,即判定超时。

示例

考虑了部分情形,实际情况比实例所考虑的情形要复杂。此外MongoDB从3.4版本开始支持decimal类型,不过在字段上添加BsonRepresentation(BsonType.Decimal128)特性

事务文档和账户文档相应地修改为

public class TransactionDocumentP
{
.......
//转账金额
[BsonRepresentation(BsonType.Decimal128)]
public decimal Value { set; get; }
......
} public class AccountP
{
......
[BsonRepresentation(BsonType.Decimal128)]
public decimal Balance { set; get; }
......
}

操作的集合

        //事务文档集合
private string TransactionCollectionName = "TransactionCollection";
//账户集合
private string AccountsCollectionName = "UserAccounts";
private MongoDBService mongoDBService = new MongoDBService("mongodb://localhost:27017/TestDB?maxPoolSize=100&minPoolSize=10",
"TestDB");

主流程方法:

1 为参与事务的两个实体创建唯一的事务文档

private void PrepareTransfer(decimal value, string source, string destination)
{
//创建事务文档
TransactionDocumentP tDoc = new TransactionDocumentP
{
_id = string.Format("{0}For{1}", source, destination),
State = "initial",
LastModified = DateTime.Now,
Value = value,
Source = source,
Destination = destination
};
FilterDefinitionBuilder<TransactionDocumentP> filterBuilder = Builders<TransactionDocumentP>.Filter;
FilterDefinition<TransactionDocumentP> filter1 = filterBuilder.Eq(doc => doc._id, tDoc._id);
if (mongoDBService.ExistDocument(TransactionCollectionName, filter1))
{
return;
}
//将事务文档插入事务集合
mongoDBService.Insert(TransactionCollectionName, tDoc);
}

2 找到状态为"initial"的事务文档

private TransactionDocumentP RetrieveTransaction()
{
FilterDefinitionBuilder<TransactionDocumentP> filterBuilder = Builders<TransactionDocumentP>.Filter;
FilterDefinition<TransactionDocumentP> filter = filterBuilder.Eq(doc => doc.State, "initial"); return mongoDBService.Single(TransactionCollectionName, filter);
}

3 执行转账与应用事务

private bool ApplyTransaction(TransactionDocumentP t, decimal value, string source, string destination)
{
FilterDefinitionBuilder<AccountP> filterBuilderS = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterS1 = filterBuilderS.Eq(doc => doc._id, source);
var updateS = Builders<AccountP>.Update.Inc(m => m.Balance, -value).Push(m => m.PendingTransactions, t._id);
UpdateResult updateResultS = mongoDBService.DocumentUpdate(AccountsCollectionName, filterS1, updateS); bool isSuss = updateResultS.ModifiedCount > && updateResultS.ModifiedCount == updateResultS.MatchedCount;
if(isSuss)
{
FilterDefinitionBuilder<AccountP> filterBuilderD = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterD1 = filterBuilderD.Eq(doc => doc._id, destination);
var updateD = Builders<AccountP>.Update.Inc(m => m.Balance, value).Push(m => m.PendingTransactions, t._id);
UpdateResult updateResultD = mongoDBService.DocumentUpdate(AccountsCollectionName, filterD1, updateD);
isSuss = updateResultD.ModifiedCount > && updateResultD.ModifiedCount == updateResultD.MatchedCount;
} return isSuss;
}

4更新两个账户的待处理事务链表,移除事务标识,超时跳出

private bool UpdateAccount(TransactionDocumentP t, string source, string destination, TimeSpan maxTxnTime)
{
FilterDefinitionBuilder<AccountP> filterBuilderS = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterS = filterBuilderS.Eq(doc => doc._id, source);
var updateS = Builders<AccountP>.Update.Pull(doc => doc.PendingTransactions, t._id);
bool isSucc = mongoDBService.UpdateOne(AccountsCollectionName, filterS, updateS);
while (true)
{
if (isSucc) break;
bool timeOut = CheckTimeOut(t, maxTxnTime);
if (timeOut) break;
isSucc = mongoDBService.UpdateOne(AccountsCollectionName, filterS, updateS);
}
if (!isSucc)
{
return isSucc;
} FilterDefinitionBuilder<AccountP> filterBuilderD = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterD = filterBuilderD.Eq(doc => doc._id, destination);
var updateD = Builders<AccountP>.Update.Pull(doc => doc.PendingTransactions, t._id);
isSucc = mongoDBService.UpdateOne(AccountsCollectionName, filterD, updateD);
while (true)
{
if (isSucc) break;
bool timeOut = CheckTimeOut(t, maxTxnTime);
if (timeOut) break;
isSucc = mongoDBService.UpdateOne(AccountsCollectionName, filterD, updateD);
}
return isSucc;
}

5 更新事务文档状态

private bool UpdateTransactionState(TransactionDocumentP t, string oldState, string newState)
{
if (t == null)
{
return false;
}
FilterDefinitionBuilder<TransactionDocumentP> filterBuilder = Builders<TransactionDocumentP>.Filter;
FilterDefinition<TransactionDocumentP> filter1 = filterBuilder.Eq(doc => doc._id, t._id);
FilterDefinition<TransactionDocumentP> filter2 = filterBuilder.Eq(doc => doc.State, oldState);
FilterDefinition<TransactionDocumentP> filter = filterBuilder.And(new FilterDefinition<TransactionDocumentP>[] { filter1, filter2 }); var update = Builders<TransactionDocumentP>.Update.Set(m => m.State, newState).Set(m =>m.LastModified,DateTime.Now);
UpdateResult updateResult = mongoDBService.DocumentUpdate(TransactionCollectionName, filter, update); return updateResult.ModifiedCount > && updateResult.ModifiedCount == updateResult.MatchedCount;
}
检验超时版本:
private bool ReUpdateTransactionState(TransactionDocumentP t, string oldState, string newState,TimeSpan maxTxnTime)
{
bool isSucc = UpdateTransactionState(t, oldState, newState);
while (true)
{
if (isSucc) break;
bool timeOut = CheckTimeOut(t, maxTxnTime);
if (timeOut) break;
isSucc = UpdateTransactionState(t, oldState, newState);
}
return isSucc;
}

辅助方法:

1 检测超时

超时只能应对一般的短时网络故障,对于长时间的故障这种办法行不通。

private bool CheckTimeOut(TransactionDocumentP t, TimeSpan maxTxnTime)
{
DateTime cutOff = DateTime.Now - maxTxnTime;
FilterDefinitionBuilder<TransactionDocumentP> filterBuilder = Builders<TransactionDocumentP>.Filter;
FilterDefinition<TransactionDocumentP> filter = filterBuilder.Lt(doc => doc.LastModified, cutOff);
var tranDoc = mongoDBService.Single(TransactionCollectionName, filter);
return tranDoc == null ? true : false;
}

2 回滚操作

private void RollbackOperations(TransactionDocumentP t,string source, string destination)
{
//1 将事务文档状态由pending更新为canceling.
ReUpdateTransactionState(t, "pending", "canceling", new TimeSpan(,,)); //2 账户余额回滚.
FilterDefinitionBuilder<AccountP> filterBuilderS = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterS1 = filterBuilderS.Eq(doc => doc._id, t.Source);//source
FilterDefinition<AccountP> filterS2 = filterBuilderS.Where(doc => doc.PendingTransactions.Contains(t._id));
FilterDefinition<AccountP> filterS = filterBuilderS.And(new FilterDefinition<AccountP>[] { filterS1, filterS2 });
var updateS = Builders<AccountP>.Update.Inc(m => m.Balance, t.Value).Pull(m => m.PendingTransactions, t._id);
bool isSuccess = mongoDBService.UpdateOne(AccountsCollectionName, filterS, updateS); if(isSuccess)
{
FilterDefinitionBuilder<AccountP> filterBuilderD = Builders<AccountP>.Filter;
FilterDefinition<AccountP> filterD1 = filterBuilderD.Eq(doc => doc._id, t.Destination);//source
FilterDefinition<AccountP> filterD2 = filterBuilderD.Where(doc => doc.PendingTransactions.Contains(t._id));
FilterDefinition<AccountP> filterD = filterBuilderD.And(new FilterDefinition<AccountP>[] { filterD1, filterD2 });
var updateD = Builders<AccountP>.Update.Inc(m => m.Balance, -t.Value).Pull(m => m.PendingTransactions, t._id);
isSuccess = mongoDBService.UpdateOne(AccountsCollectionName, filterD, updateD);
} if (isSuccess)
{
//3 将事务文档状态由canceling更新为cancelled.
UpdateTransactionState(t, "canceling", "cancelled");
}
}

组织流程:

public void Process(decimal value, string source, string destination)
{
//超时时间
TimeSpan tSpan = new TimeSpan(,,);
//0 为参与事务的两个实体创建唯一的事务文档
PrepareTransfer(value,source,destination); //1 找到状态为"initial"的事务文档
TransactionDocumentP t2 = RetrieveTransaction(); //2 将事务文档状态由“initial”更改为“pending”,超时跳出
bool initial_pending = ReUpdateTransactionState(t2, "initial", "pending", tSpan);
if (!initial_pending)
{
return;
}
//3 执行转账
bool isSuccessAp = ApplyTransaction(t2, value, source, destination);
if (!isSuccessAp)
{
//回滚
RollbackOperations(t2, source, destination);
return;
} //4 将事务文档状态由“pending”更改为“applied”
bool pending_applied = ReUpdateTransactionState(t2, "pending", "applied", tSpan);
if (!pending_applied)
{
return;
} //5 更新两个账户的待处理事务链表,移除事务标识,超时跳出
bool update = UpdateAccount(t2, source, destination, tSpan);
if (!update)
{
return;
} //6 将事务文档状态由“applied”更改为“done”
bool applied_done = ReUpdateTransactionState(t2, "applied", "done", tSpan);
if (!applied_done)
{
return;
} //7 将事务文档状态由“done”更改为“initial”
bool done_initial = ReUpdateTransactionState(t2, "done", "initial", tSpan);
if (!done_initial)
{
return;
}
}

-----------------------------------------------------------------------------------------

转载与引用请注明出处。

时间仓促,水平有限,如有不当之处,欢迎指正。

MongoDB模拟多文档事务操作的更多相关文章

  1. MongoDB .Net Driver(C#驱动) - 内嵌数组/嵌入文档的操作(增加、删除、修改、查询(Linq 分页))

    目录 一.前言 1. 运行环境 二.前期准备工作 1. 创建 MongoDBContext MongoDb操作上下文类 2.创建测试类 3.创建测试代码 三.内嵌数组增加元素操作 1.Update.S ...

  2. MongoDB入门---文档查询操作之条件查询&and查询&or查询

    经过前几天的学习之路,今天终于到了重头戏了.那就是文档查询操作.话不多说哈,直接看下语法: db.collection.find(query, projection) query :可选,使用查询操作 ...

  3. MongoDB数据库、集合、文档的操作

    MongoDB系列第一课:MongDB简介 MongoDB系列第二课:MongDB环境搭建 MongoDB系列第三课:MongDB用户管理 MongoDB系列第四课:MongoDB数据库.集合.文档的 ...

  4. MongoDB(9)- 文档查询操作之 find() 的简单入门

    find() MongoDB 中查询文档使用 find() find() 方法以非结构化的方式来显示所要查询的文档 语法格式 db.collection.find(query, projection) ...

  5. c# word文档的操作

    参考https://blog.csdn.net/ruby97/article/details/7406806 Word对象模型  (.Net Perspective) 本文主要针对在Visual St ...

  6. SpringMVC MongoDB之“基本文档查询(Query、BasicQuery)”

    一.简介 spring Data  MongoDB提供了org.springframework.data.mongodb.core.MongoTemplate对MongoDB的CRUD的操作,上一篇我 ...

  7. Mongodb:修改文档结构后出现错误:Element '***' does not match any field or property of class ***.

    Mongodb:修改文档结构后出现错误:Element '***' does not match any field or property of class ***. Mongodb是一种面向文档的 ...

  8. C# 使用XmlDocument类对XML文档进行操作

    原创地址:http://www.cnblogs.com/jfzhu/archive/2012/11/19/2778098.html 转载请注明出处 W3C制定了XML DOM标准.很多编程语言中多提供 ...

  9. JS对文档进行操作

    对文档进行操作   创建节点 追加节点 删除节点 任务及例子 总结 对DOM的修改是,构建动态网页的关键.使用下面列举的方法,我们可以创建新的网页并且动态进行更改. 更多的DOM操作方法请查 DOM1 ...

随机推荐

  1. Docker(五):Docker高级网络配置

    1.容器跨主机多子网方案 网络设计如下: 主机1:10.110.52.38 容器1: 192.168.0.1 vlan1 容器2: 192.168.0.2 vlan2 主机2:10.110.52.66 ...

  2. SharedPreferences 存List集合,模拟数据库,随时存取

    PS:SharedPreferences只要稍微学过一点就会用,他本身通过创建一个Editor对象,来存储提交,而editor可以存的格式为 他里面可以存一个Set<String> Set ...

  3. ORACLE的锁机制

    数据库是一个多用户使用的共享资源.当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况.若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性. 加锁是实现数据 ...

  4. 使用trim_galore软件遇到的问题

    我的原始测序数据是双端测序,在用trim_galore软件去接头的这一步,使用的命令行是 time nohup trim_galore R17002628-SKOV3-m6A_combined_R1. ...

  5. Maven的下载、安装与环境配置

    在创建一个项目时,搭建环境往往是编写具体代码的先决条件,而获取到所有需要的jar包是其中的重中之重.起初,人们在需要jar包的时候总会在网上四处查找,而且如果不知道某jar包版本的更迭,写出的代码或许 ...

  6. chromedriver与chrome版本映射列表

    chromedriver与chrome版本映射列表: chromedriver版本 支持的Chrome版本 v2.30 v58-60 v2.29 v56-58 v2.28 v55-57 v2.27 v ...

  7. rpc之thrift

    rpc之thrift 一.介绍 thrift是一个rpc(remove procedure call)框架,可以实现不同的语言(java.c++.js.python.ruby.c#等)之间的相互调用. ...

  8. TurnipBit-MicroPython开发板:跟孩子一起DIY跳动的心

    天是越来越热了,小心脏也是越跳越快啊,为了表达现在激动的心情,必须做个激动的心开始跳动.紧接着就开始带领大家做个激动的心. 首先说说要借助的平台,这次仅仅需要借助一块TurnipBit开发板. Tur ...

  9. Python模块学习------ 多线程threading(1)

    # Method 1: 创建一个Thread实例,传给它一个函数:import threading from time import sleep, ctime loops = [4,2] def lo ...

  10. 【转】Python 爬虫的工具列表【预】

    这个列表包含与网页抓取和数据处理的Python库 网络 通用 urllib -网络库(stdlib). requests -网络库. grab – 网络库(基于pycurl). pycurl – 网络 ...