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. SecureCRT连接本地的Vmware虚拟机(CentOS)时提示连接超时“Connection timed out”

    测试了一下,直接在Vmware的VM里面可以ping通宿主机. 但是宿主机无法ping通VM. 后面发现是本地的网络设置里面的vmware的NAT的网卡设置了手工填写地址和DNS. 修改为自动获取.问 ...

  2. centos yum源配置 与yum配置文件

    参考博客 http://www.cnblogs.com/mchina/archive/2013/01/04/2842275.html 1.centos . yum配置文件在目录 /etc/yum.re ...

  3. day 10 字符编码和文件处理 细节整理

    pycharm是文本编辑器. 大概理解为:  输出到屏幕上的时候,是解码过的字符串,用 decode 处理的时候要编码成相应的流, encode 成你要用的格式就可以了 1 .字符编码: 字符==== ...

  4. 一些JavaScript技巧

    1.获取浏览器的高度和宽度(不包括工具栏和滚动条): var w=window.innerWidth //现代浏览器 || document.documentElement.clientWidth / ...

  5. 解决ios手机上传竖拍照片旋转90度问题

    html5+canvas进行移动端手机照片上传时,发现ios手机上传竖拍照片会逆时针旋转90度,横拍照片无此问题:Android手机没这个问题. 因此解决这个问题的思路是:获取到照片拍摄的方向角,对非 ...

  6. vue2.0 配置build项目打包目录、资源文件(assets\static)打包目录

    vue项目默认的打包路径:根目录下的dist文件夹下: 但是在项目开发中,我们肯定希望项目提交到svn目录或者git目录下,否则每次复制过去,太麻烦了: 那怎么配置打包路径呢?下面来看看: 我们找到打 ...

  7. php 抽象类abstract

    程序中,有些类的作用只是用来继承,无须实例化: 为了满足类的这种需求,php提供了抽象类的概念 ,关键词abstract: 抽象类原则: 抽象类不能被实例化 有抽象方法的类一定是抽象类:类必须要abs ...

  8. 嵌入式linux下wifi网卡的使用(二)——应用程序iw编译

    首先编译iw,Iw支持两种加密/认证方式.第一种是OPEN/OPEN 第二种是WEP/WEP在网上下载iw源码,发现iw的编译需要依赖libnl库(这个库是为了方便应用程序使用netlink借口而开发 ...

  9. 调用本地摄像头拍照(H5和画布)

    关于H5 和 画布 调用本地摄像头拍照功能的实现 1.代码的实现(html部分) <input type="button" title="开启摄像头" v ...

  10. java web 之 listen 与 filter

    一.Listener监听器 Javaweb开发中的监听器,是用于监听web常见对象 HttpServletRequest HttpSession ServletContext 监听它们的创建与销毁.属 ...