分布式事务

 

 

1、名词解释

一、事务:它是一个操做序列,这些操做要么都执行,要么都不执行,它是一个不可分割的工做单位。html

二、事务特性(ACID):接触事务,都是从事务的ACID开始,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。sql

三、事务涉及的对象数据库

    资源:应用程序存储和获取数据的地方,能够是数据库,文件,也能够是内存。若是是应用程序的事务块代码中涉及到的数据库,文件,内存,那这些资源就称为事务型资源c#

    资源管理器:在事务模型中,应用不是直接访问资源,而是经过中间介访问资源,这个中间介就叫资源管理器跨域

                     资源分为可持久化资源(对应了持久化资源管理),易失资源(对应了易失资源管理器)。网络

   事务管理器:实现事务的开始、提交、回滚。架构

四、事务提高:并发

    轻量级事务管理器:做用于开启事务的应用程序域,只能包含一个持久化资源,若是再添加一个持久化资源,将被轻量级事务管理器忽略。app

                             可是能够登记多个易失资源。目前轻量级事务管理器只支持SQL 2005以及SQL2005以上的版本持久化资源。分布式

    内核事务管理器:在 Vista、Windows Server 200八、WIN7操做系统中,引入了内核事务管理器。

                          内核事务管理器主要是把文件管理(NTFS文件系统)和注册表管理归入事务范畴。咱们将那些支持事务的文件系统和注册表叫做事务型文件系统(TxF)

                         和事务型注册表(TxR)。 之因此称为内核事务管理器,是因它运行在内核模式上, 而不是在用户模式上。一样内核事务管理器只支持一个持久化资源。

    分布式事务协调器:每一台电脑上只有一个分布式事务协调器,它管理了当前计算机的全部事务资源。它能够跨程序域,跨进程,跨机器,跨网络来执行事务。

                          当事务跨机器时,每台机器的分布式事务协调器按照相应的协议工做,实现对整个事务的管理,它对应的事务管理协议有Ole-Tx和

                           WS-Atomic Transaction(WS-AT)这些。 分布式事务协调器可以管理一个分布式事务涉及的全部事务型资源,无论具体的事务型资源分布在何处。

    事务提高:事务是一个动态执行的操做序列,在整个过程当中,不可能预知资源的登记状况。因此轻量级事务管理器做为默认的事务管理器,随着事务的逐步执行,

                  若是涉及到内核事务资源,那么将提高为内核事务管理器。若是出现对多个事务资源的访问,或者当前事务涉及跨域(调用另一个服务),就会提高

                  为分布式事务协调器。Windows采用事务提高机制进行事务管理器的选择。

    

 

2、事务的分类

一、本地事务: 轻量级事务管理器,内核事务管理器都只支持本地事务。本地事务相对简单,这儿不做重点简述。

二、分布式事务:理解分布式事务是怎样实现的,事务提交树是关键。

    事务提交树:事务提交树的是事务初始化服务所在的机器的DTC,它在整个事务提交过程当中充当着总协调者,又被称为全局提交协调器。

                    资源管理器充当着事务提交树的叶子节点,它们的父结点为本机的DTC,分布于不一样机器的DTC按照事务的传播路径造成了上下级关系。

 

    在一个分布式事务中,事务的初始化和提交是属于一个对象,只有最初开始的事务才能被提交,咱们将这种能被初始化和提交的事务称做可提交事务

    随着参与者逐个登记到事务中,它们本地的事务实际上依赖着这个最初开始的事务,因此咱们称这种事务叫依赖事务

   

3、示例

   一、文件事务

  在MSDN上对文件事务有详细的阐述  使用文件系统事务加强您的应用程序  咱们若是是仔细阅读这篇文章不难发现他提供了一个.exe类型文件的下载。先把这个TxF2007_07.exe文件下载到本地硬盘,执行它,能够获得一个关于 c#的 KtmIntegration.csproj 的项目,咱们用visual studio来打开这个项目,而且从新重成这个项目,能够获得一个KtmIntegration.dll文件。

        在你要实现的文件事务的项目中引入这个.dll文件,那你就能够很顺利的实现文件事务的操做了。

    具体代码:

using System;
using System.IO;
using System.Transactions;
using Microsoft.KtmIntegration;

namespace Exercise.WebLocalTransaction
{
    public partial class Test02 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            using (TransactionScope transactionScope = new TransactionScope())
            {
                try
                {
                    FileStream stream = TransactedFile.Open(
                        @"D:/Sam Xiao.txt"
                        , FileMode.OpenOrCreate
                        , FileAccess.ReadWrite
                        , FileShare.ReadWrite);
                    StreamWriter writer = new StreamWriter(stream);
                    writer.WriteLine(String.Concat("执行一个事务代码:",DateTime.Now.ToString(),Environment.NewLine));
                    writer.Close();
                    //int x = 0;
                    //int y = 10;
                    //int z = y / x;
                    transactionScope.Complete();
                }
                catch
                {

                }
            }
        }
    }
}

         不要忘记了引入using Microsoft.KtmIntegration;名称空间。在段代码没有异常的状况下,咱们能够看到D盘里顺利建立了一个关于Sam Xiao.txt的文件。

         咱们故意在这段代码中抛出一个被0整除的异常,那么整个操做就会回滚。

二、分布式事务

         在.net平台上,主要是经过WCF的手段来实现程序的分布式开发。

         在WCF事务体系:主要解决了事务在服务中的流转,以及解决服务内部直接或间接访问事务型资源的协做。

       用WCF来演示事务的时候,要选择好WCF的绑定类型,有一部份绑定是不支持WCF的事务传播的。咱们选择wsHttpBinding 来作WCF的事务演示。

     

一、首先定义好WCF的服务契约
[ServiceContract(Name = "IBankingService")] public interface IBankingService { [TransactionFlow(TransactionFlowOption.Mandatory)] [OperationContract(Name = "Transfer")] void Transfer(string fromAccountId, string toAccountId, double amount); [TransactionFlow(TransactionFlowOption.Mandatory)] [OperationContract(Name = "Pay")] bool Pay(String accountID, double amount); [TransactionFlow(TransactionFlowOption.Mandatory)] [OperationContract(Name = "Receipt")] bool Receipt(String accountID, double amount); }

二、实现WCF的服务
[ServiceBehavior(TransactionIsolationLevel = System.Transactions.IsolationLevel.Serializable)]
public class BankingService : IBankingService
{
    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public void Transfer(string fromAccountId, string toAccountId, double amount)
    {
        throw new NotImplementedException();
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public bool Pay(string accountID, double amount)
    {
        throw new NotImplementedException();
    }

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
    public bool Receipt(string accountID, double amount)
    {
        throw new NotImplementedException();
    }
}

3,WCF宿主配置
<system.serviceModel>
      <behaviors>
          <serviceBehaviors>
              <behavior name="sBehaviorConfig">
                  <serviceMetadata httpGetEnabled="true" />
                  <serviceDebug includeExceptionDetailInFaults="true" />
              </behavior>
          </serviceBehaviors>
      </behaviors>

      <bindings>
        <wsHttpBinding>
          <binding name="wshttpConfig" transactionFlow="true" >
            <security mode="None" />
          </binding>
        </wsHttpBinding>
      </bindings>
     
      <services>
        <service name="Exercise.Service.BankingService" behaviorConfiguration="sBehaviorConfig">
          <endpoint address="mex"  binding="wsHttpBinding" bindingConfiguration="wshttpConfig" contract="Exercise.Contract.IBankingService"></endpoint>
        </service>
      </services>
      <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

4,WCF客户端配置
<system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="WSHttpBinding_IBankingService" transactionFlow="true">
          <security mode="None" />
        </binding>
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:9100/BankingService.svc/mex"
          binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IBankingService"
          contract="Exercise.Contract.IBankingService" name="WSHttpBinding_IBankingService" />
    </client>
  </system.serviceModel>


5,调用服务
IBankingService bankService = WcfProxy.CreateProxy<IBankingService>("WSHttpBinding_IBankingService");
protected void Page_Load(object sender, EventArgs e)
{
    using (TransactionScope transactionScope = new TransactionScope())
    {
        bankService.Pay("111", 50);
        bankService.Receipt("222", 50);
        transactionScope.Complete();
    }
}

 

以上内容来自:http://www.cnblogs.com/xcj26/archive/2013/12/23/3469373.html 感谢做者的贡献!

 

4、使用消息系统避免分布式事务

一、场景

      当一个表数据更新后,怎么保证另外一个表的数据也必需要更新成功?   

      例如:

     从支付宝转帐1万块钱到余额宝:支付宝扣除1万以后,若是正好系统挂掉,这时余额宝帐户并无增长1万,数据就会出现不一致情况了  。同理,在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?   在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增长一条记录外,还得去商家帐户表中找到这个商家并扣除广告费吧,怎么保证?

 二、本地事务的解决方法    

      支付宝帐户表:A(id,userId,amount)

  • 余额宝帐户表:B(id,userId,amount)

  • 用户的userId=1;

 

      若是系统规模较小,数据表都在一个数据库实例上,上述本地事务方式能够很好地运行;可是若是系统规模较大,好比支付宝帐户表和余额宝帐户表显然不会在同一个数据库实例上,他们每每分布在不一样的物理节点上,这时本地事务已经失去用武之地。

 

三、分布式解决方法

      两阶段提交协议(Two-phase Commit,2PC)常常被用来实现分布式事务。通常分为协调器C和若干事务执行者Si两种角色,这里的事务执行者就是具体的数据库,协调器能够和事务执行器在一台机器上。

     

  1. 应用程序(client)发起一个开始请求到TC;
  2. TC先将<prepare>消息写到本地日志,以后向全部的Si发起<prepare>消息。以支付宝转帐到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应帐目扣款1万,TC给B的prepare消息是通知余额宝数据库相应帐目增长1w。为何在执行任务前须要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证 的效果,若是没有本地日志(凭证),出问题容易死无对证;
  3. Si收到<prepare>消息后,执行具体本机事务,但不会进行commit,若是成功返回<yes>,不成功返回<no>。同理,返回前都应把要返回的消息写到日志里,看成凭证。
  4. TC收集全部执行器返回的消息,若是全部执行器都返回yes,那么给全部执行器发生送commit消息,执行器收到commit后执行本地事务的commit操做;若是有任一个执行器返回no,那么给全部执行器发送abort消息,执行器收到abort消息后执行事务abort操做。

     注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一个Si从故障中恢复后,先检查本机的日志,若是已收到<commit >,则提交,若是<abort >则回滚。若是是<yes>,则再向TC询问一下,肯定下一步。若是什么都没有,则极可能在<prepare>阶段Si就崩溃了,所以须要回滚。

 

      该解决方法存在问题: 性能实在是太差,根本不适合高并发的系统。

  1.  两阶段提交涉及屡次节点间的网络通讯,通讯时间太长;
  2. 事务时间相对于变长了,锁定的资源的时间也变长了,形成资源等待时间也增长不少。

正是因为分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,每每经过其余途径来解决数据一致性问题。

 

四、使用消息队列来避免分布式事务

在饭馆点了菜并付了钱后,他们并不会直接把你点的菜给你,而是给你一张小票,而后让你拿着小票到出货区排队去取。为何他们要将付钱和取货两个动做分开呢? 缘由不少,其中一个很重要的缘由是为了使他们接待能力加强(并发量更高)。

 

仍是回到咱们的问题,只要这张小票在,你最终是能拿到点的菜的。同理转帐服务也是如此,当支付宝帐户扣除1万后,咱们只要生成一个凭证(消息)便可,这个凭证(消息)上写着“让余额宝帐户增长 1万”,只要这个凭证(消息)能可靠保存,咱们最终是能够拿着这个凭证(消息)让余额宝帐户增长1万的,即咱们能依靠这个凭证(消息)完成最终一致性。

 

4.1 如何可靠保存凭证(消息)

    有两种方法:业务与消息耦合的方式、业务与消息解耦方式。

    4.1.1 业务与消息耦合的方式

       支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。     

Begin transaction

   update A set amount=amount-10000 where userId=1; 

    insert into message(userId, amount,status) values(1, 10000, 1); 

End transaction

commit;

 

上述事务能保证只要支付宝帐户里被扣了钱,消息必定能保存下来。 

当上述事务提交成功后,咱们经过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。

 

    4.1.2 业务与消息解耦方式

     上述保存消息的方式使得消息数据和业务数据紧耦合在一块儿,从架构上看不够优雅,并且容易诱发其余问题。为了解耦,能够采用如下方式:

  1. 支付宝在扣款事务提交以前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;
  2. 当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在获得确认发送指令后,实时消息服务才真正发送该消息;
  3. 当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在获得取消发送指令后,该消息将不会被发送;
  4. 对于那些未确认的消息或者取消的消息,须要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。

         为何须要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而致使消息不能被发送。

     该方法优势:消息数据独立存储,下降业务系统与消息系统间的耦合;

     该方法缺点:一次消息发送须要两次请求;业务处理服务须要实现消息状态回查接口。

 

4.2 如何解决消息重复投递的问题

     还有一个很严重的问题就是消息重复投递,以咱们支付宝转帐到余额宝为例,若是相同的消息被重复投递两次,那么咱们余额宝帐户将会增长2万而不是1万了。 

     为何相同的消息会被重复投递? 好比余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常状况下支付宝应该要删除消息msg,但若是支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。 

     解决方法很简单,在余额宝这边增长消息应用状态表(message_apply),通俗来讲就是个帐本,用于记录消息的消费状况,每次来一个消息,在真正执行以前,先去消息应用状态表中查询一遍,若是找到说明是重复消息,丢弃便可,若是没找到才执行,同时插入到消息应用状态表(同一事务)。

for each msg in queue 
  
   Begin transaction 

       select count(*) as cnt from message_apply where msg_id=msg.msg_id; 

       if cnt==0 then 

               update B set amount=amount+10000 where userId=1; 

       insert into message_apply(msg_id) values(msg.msg_id); 

End transaction 

commit;
相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息