• MongoDB模拟多文档事务操作


    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步,为参与事务的两个实体创建唯一的事务文档。

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

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

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

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

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

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

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

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

    具体操作是:移除第3步中向AB两个账户待处理事务链表添加的事务文档_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 与当前时间的值,如果二者差值超过设定的阈值,即判定超时。

     

    示例

    考虑了部分情形,实际情况比实例所考虑的情形要复杂。此外MongoDB3.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 > 0 && 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 > 0 && 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 > 0 && 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(0,0,100));
                
                //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,0,100);
                //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;
                }
            }

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

    转载与引用请注明出处。

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

  • 相关阅读:
    [LeetCode] Count and Say
    [LeetCode] Search Insert Position
    [LeetCode] Substring with Concatenation of All Words
    [LeetCode] Divide Two Integers
    笔记本清灰后组装后出现蓝屏,并不断的循环重新启动。
    自学MVC看这里——全网最全ASP.NET MVC 教程汇总
    sqlzoo练习答案--SELECT names/zh
    iOS Autolayout情况下,ViewController嵌套时,childViewController的Frame异常问题
    atitit. 文件上传带进度条 atiUP 设计 java c# php
    POJ 3237 Tree 树链剖分
  • 原文地址:https://www.cnblogs.com/hdwgxz/p/8336693.html
Copyright © 2020-2023  润新知