使用“即时消息服务框架”(iMSF)实现分布式事务的三阶段提交协议(电商创建订单的示例)

1,示例解决方案介绍

在上一篇 《消息服务框架(MSF)应用实例之分布式事务三阶段提交协议的实现》中,我们分析了分布式事务的三阶段提交协议的原理,现在我们来看看如何使用消息服务框架(MSF)来具体实现并且看用它来实现的一些优势。

首先,从Github克隆项目源码,地址:https://github.com/bluedoctor/MSF-DistTransExample

解决方案如下图:

我们看到解决方案有4个项目:

  1. DistTransClient:分布式事务示例的客户端,它调用“订单服务”,创建一个订单,服务会返回创建结果是成功还是失败;
  2. DistTransDto:包含商品,订单和订单详情的实体类型接口以及相关的接口实现;
  3. DistTransServices:包含订单服务,商品服务和分布式事务控制器服务;
  4. TistTransApp:本测试的宿主程序项目,主要用于安装消息服务框架的服务宿主程序,以及启动订单,商品和分布式事务控制器的服务进程。

 2,创建订单的业务简介

2.1,基本概念

下面先介绍本示例要解决的业务,并通过这个业务来分析分布式事务的执行过程。

在本示例中,使用的是电商系统最常见的业务场景:下单业务,它的业务流程也概括起来比较简单:

创建订单:

  1. 生成订单基本信息;
  2. 生成订单项目明细(已购商品清单):
    1. 检查库存是否足够
    2. 扣减库存

 当然,在具体的电商业务系统中,下单业务比较复杂,特别是对库存的扣减方式,但大体的业务流程就是这样的,我们今天的重点是研究这个下单过程在分布式环境下如何实现。

2.2,微服务架构

假设我们的电商平台使用微服务架构的,包含了用户服务,商品服务,订单服务和支付服务,这4个服务在下单业务中的功能分别如下:

  • 用户服务:检查当前用户是否有效,查询用户的相关信息,比如用户姓名,联系电话等;
  • 订单服务:生成订单,包括结合用户服务的用户信息,生成订单基本信息;结合商品服务,生成订单项目明细;
  • 商品服务:向订单服务返回商品的相关信息,并返回库存是否可用,如果可用就扣减库存;
  • 支付服务:由第三方提供,但参与创建订单的流程,用户下单后需要用户去第三方支付系统完成支付,然后支付服务回调订单服务,完成有效订单确认。

 下面是这4个服务在创建订单的业务流程图:

上图中,支付服务是第三方提供的服务,需要用户在创建订单后跳转调用,所以本质上不是订单服务直接调用,订单服务需要提供一个支付完成的回调通知接口,完成有效订单的确认。 而用户服务作为服务调用的发起方,它会传递必要的信息给订单服务,因此,对于“创建订单”这个具体的业务功能,它涉及的需要同时进行操作的只有创建订单和扣减库存这两个子业务,并且要求这2个子业务操作具有原子性,即要么同时成功,要么同时失败撤销,所以这两个操作组成一个事务操作,在我们当前的场景中,它是一个分布式事务。

2.3,分布式事务中的微服务容器

在本例中,我们使用消息服务框架(MSF)来实现分布式事务,为了更加真实的模拟微服务架构,我们将创建订单相关的服务划分为3个独立的进程,这些进程就是MSF.Host服务容器,这里分为3个服务容器:

  • 协调器服务容器:运行分布式事务协调器服务;
  • 订单服务容器:运行订单服务和分布式事务控制器组件;
  • 商品服务容器:运行商品服务和分布式事务控制器组件。

下面是这3个服务容器的进程调用关系图:

 

 

3,创建订单的分布式事务流程

下面来看创建订单的分布式事务处理过程,为简单起见,只讨论正常的流程,其中异常的流程,请参考原文对于3阶段提供分布式事务的具体原理。

1,客户端调用订单服务的创建订单方法;(上图步骤1)

2,订单服务实例化,接受一个订单号,用户号,要购买的商品清单3个参数来创建订单;(上图步骤1)

3,创建订单的方法向分布式事务控制器进行本地事务注册,传入创建订单的事务方法(委托);(上图步骤2)

4,创建订单的事务方法远程调用商品服务,更新商品库存;(上图步骤3)

5,商品服务的更新商品库存方法向分布式事务控制器进行本地事务注册,传入具体更新库存的事务方法(委托);(上图步骤4)

6,商品服务执行完成更新库存的方法,向订单服务返回必要的信息,准备好提交事务;(上图步骤5)

7,订单服务收到商品服务的返回信息,构建好订单和订单明细,准备好提交事务;(上图步骤6)

8,分布式事务控制器检测到注册的各事务资源服务器(商品服务和订单服务)都已经准备好提交事务,向它们发出提交指令;

9,商品服务和订单服务收到提交指令,提交本地事务,事务资源服务方法执行完成;(上图步骤7,8)

10,分布式事务控制器收到事务资源服务器的反馈,登记本次分布式事务执行完成;

11,订单服务标记创建订单成功,向客户端返回信息。

4,分布式事务服务和组件

4.1,分布式事务控制器

分布式事务控制器是提供给事务资源服务使用的组件,在本示例中是类 DTController,它提供了如下重要方法:

  • 检查并开启一个分布式事务控制器对象
  • 移除一个事务控制器
  • 累计事务资源服务器
  • 获取分布式事务的状态
  • 3阶段分布式事务请求函数
  • 提交事务的方法
  • 回滚事务的方法

其中“3阶段分布式事务请求函数”,是事务控制器对象重要的函数,它负责对“3阶段分布式事务”的各个阶段进行流程控制,其中每一阶段,都要和“分布式事务协调服务”进行通信,接受它的指令,完成本地事务资源的控制,比如是提交还是回滚事务资源。下面我们看看它主要的代码:

 在上面的函数中,MSF的客户端服务访问代理类 Proxy 对象它请求的是“分布式事务协调服务”,即名字为“DTCService”的远程服务;Proxy的RequestService 方法的最后一个参数,表示服务调用过程中,服务端回调的客户端函数,在这个回调函数中,提供了3阶段分布式事务协议中的各种指令的响应处理,包括:

  • CanCommit--询问本地事务是否可以提交;
  • PreCommit--预提交指令;
  • Abort--撤销事务的指令;
  • DoCommit--提交事务的指令。

Proxy对象的RequestService 方法它是一个异步方法,所以调用它之后代码会立即向下执行,因此我们用 TaskCompletionSource 对象将异步方法的结果获取过程作为一个任务来处理,这样便可以阻塞异步方法的执行并等待执行完的结果,如果这个过程中发生了错误,就立即回滚事务,即下面的代码:

            try
            {
                tcs.Task.Wait();
                return tcs.Task.Result;
            }
            catch (Exception ex)
            {
                PrintLog("MSF DTC({0}) Task Error:{1}", transIdentity,ex.Message);
                TryRollback(dbHelper);
            }

在当前方法 DistTrans3PCRequest 的第二个和第三个参数中,都使用了 AdoHelper类型的参数,它是SOD框架基础的 数据访问帮助类,它的“事务计数器” (TransactionCount属性)有助于正确的开启事务,化解嵌套的事务,避免用户的 transFunction 方法内部开启和提交事务,将事务的最终提交动作交给当前分布式事务控制器。

 

4.2,分布式事务协调服务

 分布式事务控制器在执行本地事务方法的前后,需要有一个分布式事务协调服务来协调它的执行过程,这个协调过程包括以下功能:

  • (提供给控制器)调用指定标识的分布式事务,直到事务执行完成;
  • 管理系统的分布式事务阶段,向控制器推送(回调)系统的分布式事务状态;
  • 分布式事务协调服务需要运行在独立服务进程中,所以它可以协调多个分布式事务控制器的工作。

下面是本服务的具体代码实现,比较简单:

/// <summary>
    /// 分布式事务协调器服务,基于3PC过程。
    /// </summary>
    public class DTCService:ServiceBase
    {
        private int TransactionResourceCount;
        private DistTrans3PCState CurrentDTCState;

        //private static System.Collections.Concurrent.ConcurrentBag<DistTransInfo> DTResourceList = new System.Collections.Concurrent.ConcurrentBag<DistTransInfo>();

        /// <summary>
        /// 参加指定标识的分布式事务,直到事务执行完成。一个分布式事务包含若干本地事务
        /// </summary>
        /// <param name="identity">标识一个分布式事务</param>
        /// <returns></returns>
        public bool AttendTransaction(string identity)
        {
            DistTransInfo info = new DistTransInfo();
            info.ClientIdentity = base.CurrentContext.Request.ClientIdentity;
            info.CurrentDTCState = DistTrans3PCState.CanCommit;
            info.LastStateTime = DateTime.Now;
            info.TransIdentity = identity;
            //DTResourceList.Add(info);
            DateTime dtcStart = DateTime.Now;
            //获取一个当前事务标识的协调器线程
            DTController controller = DTController.CheckStartController(identity);

            CurrentDTCState = DistTrans3PCState.CanCommit;
            while (CurrentDTCState != DistTrans3PCState.Completed)
            {
                //获取资源服务器的事务状态,资源服务器可能自身或者因为网络情况出错
                if (!SendDTCState(info, controller, identity))
                    break;
            }
            SendDTCState(info, controller, identity);
            DTController.RemoveController(identity);
            Console.WriteLine("DTC Current Use time:{0}(s)",DateTime.Now.Subtract(dtcStart).TotalSeconds);
            return true;
        }

        private bool SendDTCState(DistTransInfo info, DTController controller, string identity)
        {
            string clientIdentity = string.Format("[{0}:{1}-{2}]", base.CurrentContext.Request.ClientIP, 
                base.CurrentContext.Request.ClientPort, 
                base.CurrentContext.Request.ClientIdentity);
            try
            {
                Console.WriteLine("DTC Service Callback {0} Message:{1}", clientIdentity, CurrentDTCState);
                info.CurrentDTCState = base.CurrentContext.CallBackFunction<DistTrans3PCState, DistTrans3PCState>(CurrentDTCState);
                info.LastStateTime = DateTime.Now;
                CurrentDTCState = controller.GetDTCState(info.CurrentDTCState);
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine("DTC Service Callback {0}  Error:{1}", clientIdentity, ex.Message);
                return false;
            }
        }
        


        public override bool ProcessRequest(IServiceContext context)
        {
            return base.ProcessRequest(context);
        }
    }

 在本服务中,通过 base.CurrentContext.CallBackFunction 方法回调分布式控制器,将当前阶段系统的分布式状态告诉控制器。

5,创建订单相关服务

5.1,订单服务

订单服务方法首先它要实例化一个分布式事务控制器对象,在控制器对象里面完成创建订单的事务操作,它会首先调用商品服务去更新相应的商品库存数并取得相关的商品信息,然后接着构造订单和订单明细,具体代码如下:

 /// <summary>
        /// 生成订单的服务方法
        /// </summary>
        /// <param name="orderId">订单号</param>
        /// <param name="userId">用户号</param>
        /// <param name="buyItems">购买的商品简要清单</param>
        /// <returns>订单是否创建成功</returns>
        public bool CreateOrder(int orderId,int userId,IEnumerable<BuyProductDto> buyItems)
        {
            //在分布式事务的发起端,需要先定义分布式事务标识:
            string DT_Identity = System.Guid.NewGuid().ToString();
            productProxy.RegisterData = DT_Identity;

            //使用3阶段提交的分布式事务,保存订单到数据库
            OrderDbContext context = new OrderDbContext();

            DTController controller = new DTController(DT_Identity);
            return controller.DistTrans3PCRequest<bool>(DTS_Proxy, 
                context.CurrentDataBase,
                db =>
                {
                    //先请求商品服务,扣减库存,并获取商品的仓库信息
                    ServiceRequest request = new ServiceRequest();
                    request.ServiceName = "ProductService";
                    request.MethodName = "UpdateProductOnhand";
                    request.Parameters = new object[] { DT_Identity, buyItems };
                    List<SellProductDto> sellProducts = productProxy.RequestServiceAsync<List<SellProductDto>>(request).Result;

                    #region 构造订单明细和订单对象
                    //
                    productProxy.Connect();
                    List<OrderItemEntity> orderItems = new List<OrderItemEntity>();
                    OrderEntity order = new OrderEntity()
                    {
                        ID = orderId,
                        OwnerID = userId,
                        OrderTime = DateTime.Now,
                        OrderName = "Prudoct:"
                    };
                    foreach (BuyProductDto item in buyItems)
                    {
                        //注意:在商品数据库上,前面更新商品,但还没有提交事务,下面这个查询直接使用的话会导致查询等待,因为SQLSERVER的事务隔离级别是这样的
                        //所以 GetProductInfo 的实现需要注意。
                        //ProductDto product = this.GetProductInfo(item.ProductId).Result;
                        ProductDto product = this.GetProductInfoSync(item.ProductId);

                        OrderItemEntity temp = new OrderItemEntity()
                        {
                            OrderID = orderId,
                            ProductID = product.ID,
                            BuyNumber = item.BuyNumber,
                            OnePrice = product.Price,
                            ProductName = product.ProductName
                        };
                        temp.StoreHouse = (from i in sellProducts where i.ProductId == temp.ProductID select i.StoreHouse).FirstOrDefault();

                        orderItems.Add(temp);
                        order.OrderName += "," + temp.ProductName;
                        order.AmountPrice += temp.OnePrice * temp.BuyNumber;
                    }
                    //
                    //关闭商品服务订阅者连接
                    productProxy.Close();

                    #endregion

                    //保存订单数据到数据库
                    context.Add<OrderEntity>(order);
                    context.AddList<OrderItemEntity>(orderItems);
                    return true;
                });
        }

注意在上面的方法中,我们创建订单的代码并没有直接提交或者回滚事务,而是通过控制器的 DistTrans3PCRequest 方法传入了一个AdoHelper对象,由控制器来决定提交或者回滚事务。 其它相关代码请看Github上的源码。

5.2,商品服务

商品服务比较简单,这里只列出订单服务需要直接调用的 UpdateProductOnhand方法,具体代码如下:

public class ProductService:ServiceBase
{
  //其它代码略

        /// <summary>
        /// 更新商品库存,并返回商品售卖简要信息
        /// </summary>
        /// <param name="transIdentity">分布式事务标识</param>
        /// <param name="buyItems">购买的商品精简信息</param>
        /// <returns></returns>
        public List<SellProductDto> UpdateProductOnhand(string transIdentity, IEnumerable<BuyProductDto> buyItems)
        {
            ProductDbContext context = new ProductDbContext();
            DTController controller = new DTController(transIdentity);
            return controller.DistTrans3PCRequest<List<SellProductDto>>(DTS_Proxy,
                context.CurrentDataBase,
                c =>
                {
                    return InnerUpdateProductOnhand(context,buyItems);
                });
           
        }
}

可以看到,商品服务的更新商品库存数的方法内部也实例化了一个分布式事务控制器对象,然后在它里面执行具体的本地事务操作。其它具体代码略。

需要注意的是,订单服务在事务执行过程中,多次调用了商品服务的其它方法,这些方法会操作数据库,如果这些商品服务操作的表正好是更新商品库存的方法使用的表,此时如果两个方法操作的数据库连接不是同一个事务的连接,那么会导致死锁。所以商品服务需要设置会话状态来正确存储和访问连接对象,如下代码:

public class ProductService:ServiceBase
{
  //其它代码略

        private List<SellProductDto> InnerUpdateProductOnhand(ProductDbContext context, IEnumerable<BuyProductDto> buyItems)
        {
            List<SellProductDto> result = new List<SellProductDto>();

            foreach (BuyProductDto item in buyItems)
            {
                ProductEntity entity = new ProductEntity()
                {
                    ID = item.ProductId,
                    Onhand= item.BuyNumber
                };
                OQL q = OQL.From(entity)
                    .UpdateSelf('-', entity.Onhand)
                    .Where(cmp => cmp.EqualValue(entity.ID) & cmp.Comparer(entity.Onhand, ">=", item.BuyNumber))
                    .END;


                int count = context.ProductQuery.ExecuteOql(q);
                SellProductDto sell = new SellProductDto();
                sell.BuyNumber = item.BuyNumber;
                sell.ProductId = item.ProductId;
                //修改库存成功,才能得到发货地
                if (count > 0)
                    sell.StoreHouse = this.GetStoreHouse(item.ProductId);
                result.Add(sell);
            }
            base.CurrentContext.Session.Set<ProductDbContext>("DbContext", context);
            Console.WriteLine("----------1,-Session ID:{0}----------", base.CurrentContext.Session.SessionID);
            return result;
        }

        public override bool ProcessRequest(IServiceContext context)
        {
            context.SessionRequired = true;
            //客户端(订单服务)将使用事务标识作为连接的 RegisterData,因此采用这种会话模式
            context.SessionModel = SessionModel.RegisterData;
            return base.ProcessRequest(context);
        }
}

 

5.3,客户端下单

前面我们讨论了分布式事务控制器,分布式事务协调服务,订单服务和商品服务的具体实现,现在,我们终于可以看看客户端如何调用订单服务来创建一个订单了,请看代码:

 private static void TestCreateOrder(Proxy client)
        {
            List<BuyProductDto> buyProducts = new List<BuyProductDto>();
            buyProducts.Add(new BuyProductDto() {  ProductId=1, BuyNumber=3});
            buyProducts.Add(new BuyProductDto() { ProductId =2, BuyNumber = 1 });

            int orderId = 2000;
            int userId = 100;

            ServiceRequest request = new ServiceRequest();
            request.ServiceName = "OrderService";
            request.MethodName = "CreateOrder";
            request.Parameters = new object[] { orderId,userId, buyProducts };

            bool result=client.RequestServiceAsync<bool>(request).Result;
            if(result)
                Console.WriteLine("创建订单成功,订单号:{0}",orderId);
            else
                Console.WriteLine("创建订单失败,订单号:{0}", orderId);
        }

上面的方法构造了一个准备购买的商品清单,这就是电商“购物车”的简化版本,另外为了简便起见,我们直接设定了一个订单号和用户号,用这种方式来调用创建订单的功能。

由于我们的订单号固定的,所以我们的测试程序第一次会创建成功订单,而第二次就会失败,正好可以用它来观察系统的执行情况。

6,创建订单的分布式事务测试

6.1,测试环境简介:

为了简化测试环境,所有服务实例都运行在一台PC机器上,包括数据。测试机器的性能如下:

  • CPU:Inter i7-4790 4.00GHz;
  • 内存:16GB,可用内存:8.7GB
  • 测试开发环境:VS2017 社区版
  • 数据库:SqlServer 2008 R2

打开VS开发环境,按F5以调试模式编译运行,设置多启动项目:

  • DistTransClient
  • TistTransApp

测试项目 TistTransApp下面的配置文件 PdfNetEF.MessageServiceHost.exe.config    配置内容如下:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="IOCConfigFile" value=".\IOCConfig.xml" />
    <add key="ServerIP" value="127.0.0.1" />
    <add key="ServerPort" value="12345" />
    <add key="ProductUri" value="net.tcp://127.0.0.1:12306"/>
    <add key="OrderUri" value="net.tcp://127.0.0.1:12308"/>
    <!--MSF_DTS_Uri 分布式事务控制器服务连接地址-->
    <add key="MSF_DTS_Uri" value="net.tcp://127.0.0.1:12345"/>
    <!-- 全局缓存配置
    GlobalCacheProvider="CacheServer" 将使用分布式的缓存服务器,这时候需要配置 CacheConfigFile,其它值将使用本地的缓存
    CacheConfigFile :缓存服务器的地址的配置文件,也就是本 ServiceHost 运行的另外一些实例
    -->
    <add key="GlobalCacheProvider" value="" />
    <add key="CacheConfigFile" value="CacheServerCfg.xml" />
    <!-- 全局缓存配置结束 -->
    <!--PDF.NET SQL 日志记录配置(for 4.0)开始
        记录执行的SQL语句,关闭此功能请将SaveCommandLog 设置为False,或者设置DataLogFile 为空;
        如果DataLogFile 的路径中包括~符号,表示SQL日志路径为当前Web应用程序的根目录;
        如果DataLogFile 不为空且为有效的路径,当系统执行SQL出现了错误,即使SaveCommandLog 设置为False,会且仅仅记录出错的这些SQL语句;
        如果DataLogFile 不为空且为有效的路径,且SaveCommandLog 设置为True,则会记录所有的SQL查询。
        在正式生产环境中,如果不需要调试系统,请将SaveCommandLog 设置为False 。
    -->
    <add key="SaveCommandLog" value="False" />
    <add key="DataLogFile" value=".\SqlLog.txt" />
    <!--LogExecutedTime 需要记录的时间,如果该值等于0会记录所有查询,否则只记录大于该时间的查询。单位毫秒。-->
    <add key="LogExecutedTime" value="0" />
    <!--PDF.NET SQL 日志记录配置 结束-->
    <add key="ClientSettingsProvider.ServiceUri" value="" />
  </appSettings>
  <connectionStrings>
    <!--SOD for SQL Server ,框架会自动创建需要的库  -->
    <add name="OrdersDb" connectionString="Data Source=.;Initial Catalog=OrdersDb;Integrated Security=True" providerName="SqlServer"/>
    <add name="ProductsDb" connectionString="Data Source=.;Initial Catalog=ProductsDb;Integrated Security=True" providerName="SqlServer"/>
   
    <!-- SOD for SQL Server LocalDB  
      注意:请将下面的连接字符串,修改为你VS 里面打开的数据库文件的连接字符串 
    <add name="OrdersDb" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=~\DataBase\OrdersDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
    <add name="ProductsDb" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=~\DataBase\ProductsDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
    -->
    <!-- MSSQLLocalDB 连接示例
    <add name="OrdersDb" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=E:\Git\MSF-DistTransExample\Host\DataBase\OrdersDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
    <add name="ProductsDb" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=E:\Git\MSF-DistTransExample\Host\DataBase\ProductsDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
    -->
   <!-- SOD for Access 2007 ,2013,2016
   <add name="OrdersDb" connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=~\DataBase\OrdersDb.accdb;Persist Security Info=False;" providerName="Access"/>
    <add name="ProductsDb" connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=~\DataBase\Products.accdb;Persist Security Info=False;" providerName="Access"/>
    -->
   <!-- SOD for Access 2000,2003
   <add name="OrdersDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=~\DataBase\OrdersDb.mdb;Persist Security Info=False;" providerName="Access"/>
    <add name="ProductsDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=~\DataBase\Products.mdb;Persist Security Info=False;" providerName="Access"/>
    -->
    <!-- SOD for SQLite 
   <add name="OrdersDb" connectionString="Data Source=DataBase\OrdersDb.db;" providerName="PWMIS.DataProvider.Data.SQLite,PWMIS.SQLiteClient"/>
    <add name="ProductsDb" connectionString="Data Source=DataBase\Products.db;" providerName="PWMIS.DataProvider.Data.SQLite,PWMIS.SQLiteClient"/>
    -->
  </connectionStrings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
  </startup>
  <system.web>
    <membership defaultProvider="ClientAuthenticationMembershipProvider">
      <providers>
        <add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
      </providers>
    </membership>
    <roleManager defaultProvider="ClientRoleProvider" enabled="true">
      <providers>
        <add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
      </providers>
    </roleManager>
  </system.web>
</configuration>
View Code

配置文件中配置了多种数据库连接方式,根据你的情况具体选择。当前是SqlServer.

然后,按照下图输入相关的信息:

由于我现在的测试环境是SQLSERVER数据库,所以不需要初始化数据库。选择启动事务协调器,测试程序会帮我们启动 协调器服务宿主进程,商品服务宿主进程和订单服务宿主进程。之后,我们在客户端控制台输入 12308,这是订单服务的端口号,接着客户端就会调用订单服务准备创建订单。

6.2,测试结果

下面是各种情况下的测试结果,分为订单创建成功和创建失败两种情况。注意我们在分析真正的测试数据之前,要先跑一次服务进行预热,也就是先进行一次测试,取第二次以后的测试结果。

6.2.1,订单创建成功:

分布式协调服务:

 

[2018-01-31 17:13:45.807]订阅消息-- From: 127.0.0.1:53276
[2018-01-31 17:13:45.807]正在处理服务请求--From: 127.0.0.1:53276,Identity:WMI2114256838
>>[PMID:1]Service://DTCService/AttendTransaction/System.String=1b975548-afac-4e7a-be6d-5821bce38ce7
DTC Service Callback [127.0.0.1:53276-WMI2114256838] Message:CanCommit
[2018-01-31 17:13:45.853]订阅消息-- From: 127.0.0.1:53278
[2018-01-31 17:13:45.854]正在处理服务请求--From: 127.0.0.1:53278,Identity:WMI2114256838
>>[PMID:1]Service://DTCService/AttendTransaction/System.String=1b975548-afac-4e7a-be6d-5821bce38ce7
DTC Service Callback [127.0.0.1:53278-WMI2114256838] Message:CanCommit
DTC Service Callback [127.0.0.1:53276-WMI2114256838] Message:PreCommit
DTC Service Callback [127.0.0.1:53278-WMI2114256838] Message:PreCommit
DTC Service Callback [127.0.0.1:53278-WMI2114256838] Message:DoCommit
DTC Service Callback [127.0.0.1:53278-WMI2114256838] Message:Completed
DTC Current Use time:0.042516(s)
[2018-01-31 17:13:45.897]请求处理完毕(43.0236ms)--To: 127.0.0.1:53278,Identity:WMI2114256838
>>[PMID:1]消息长度:4字节 -------
result:True
Reponse Message OK.
DTC Service Callback [127.0.0.1:53276-WMI2114256838] Message:DoCommit
[2018-01-31 17:13:45.898]取消订阅-- From: 127.0.0.1:53278
DTC Service Callback [127.0.0.1:53276-WMI2114256838] Message:Completed
DTC Current Use time:0.1009371(s)
[2018-01-31 17:13:45.909]请求处理完毕(101.9327ms)--To: 127.0.0.1:53276,Identity:WMI2114256838
>>[PMID:1]消息长度:4字节 -------
result:True
Reponse Message OK.
[2018-01-31 17:13:45.912]取消订阅-- From: 127.0.0.1:53276

 

订单服务:

[2018-01-31 17:13:45.798]订阅消息-- From: 127.0.0.1:53275
[2018-01-31 17:13:45.801]正在处理服务请求--From: 127.0.0.1:53275,Identity:WMI2114256838
>>[PMID:1]Service://OrderService/CreateOrder/System.Int32=2000&System.Int32=100&System.Collections.Generic.List`1[[DistTransDto.BuyProductDto, DistTransDto, Version%Eqv;1.0.0.0, Culture%Eqv;neutral, PublicKeyToken%Eqv;null]]=[{"ProductId":1,"BuyNumber":3},{"ProductI
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.809 receive DTC Controller state:CanCommit
[2018-01-31 17:13:45.879]请求处理完毕(77.9367ms)--To: 127.0.0.1:53275,Identity:WMI2114256838
>>[PMID:1]消息长度:4字节 -------
result:True
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.879 receive DTC Controller state:PreCommit
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 1PC,Child moniter task has started at time:17:13:45.879
Reponse Message OK.
[2018-01-31 17:13:45.888]取消订阅-- From: 127.0.0.1:53275
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 2PC,Child moniter task has started at time:17:13:45.888
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 1PC,Child moniter task find DistTrans3PCState has changed,Now is ACK_Yes_2PC,task break!
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.898 receive DTC Controller state:DoCommit
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Try Commit..
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Try Commit..OK
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.903 receive DTC Controller state:Completed
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 3PC Request Completed,use time:0.1019383 seconds.
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 2PC,Child moniter task find DistTrans3PCState has changed,Now is Completed,task break!
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Controller Process Reuslt:True,Receive time:17:13:45.913

 

商品服务:

[2018-01-31 17:13:45.848]正在处理服务请求--From: 127.0.0.1:53277,Identity:WMI2114256838
>>[PMID:1]Service://ProductService/UpdateProductOnhand/System.String=1b975548-afac-4e7a-be6d-5821bce38ce7&System.Collections.Generic.List`1[[DistTransDto.BuyProductDto, DistTransDto, Version%Eqv;1.0.0.0, Culture%Eqv;neutral, PublicKeyToken%Eqv;null]]=[{"ProductId":1
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.855 receive DTC Controller state:CanCommit
----------1,-Session ID:1b975548-afac-4e7a-be6d-5821bce38ce7----------
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 1PC,Child moniter task has started at time:17:13:45.856
[2018-01-31 17:13:45.856]请求处理完毕(8.011ms)--To: 127.0.0.1:53277,Identity:WMI2114256838
>>[PMID:1]消息长度:97字节 -------
result:[{"StoreHouse":"广州","ProductId":1,"BuyNumber":3},{"StoreHouse":"广州","ProductId":2,"BuyNumber":1}]
Reponse Message OK.
[2018-01-31 17:13:45.857]取消订阅-- From: 127.0.0.1:53277
[2018-01-31 17:13:45.858]订阅消息-- From: 127.0.0.1:53277
[2018-01-31 17:13:45.867]正在处理服务请求--From: 127.0.0.1:53277,Identity:WMI2114256838
>>[RMID:0]Service://ProductService/GetProductInfo/System.Int32=1
---------2,--Session ID:1b975548-afac-4e7a-be6d-5821bce38ce7----------
[2018-01-31 17:13:45.868]请求处理完毕(1.0005ms)--To: 127.0.0.1:53277,Identity:WMI2114256838
>>[RMID:0]消息长度:53字节 -------
result:{"ID":1,"Onhand":88,"Price":10.0,"ProductName":"商品0"}
[2018-01-31 17:13:45.869]正在处理服务请求--From: 127.0.0.1:53277,Identity:WMI2114256838
>>[RMID:0]Service://ProductService/GetProductInfo/System.Int32=2
---------2,--Session ID:1b975548-afac-4e7a-be6d-5821bce38ce7----------
[2018-01-31 17:13:45.869]请求处理完毕(0.5005ms)--To: 127.0.0.1:53277,Identity:WMI2114256838
>>[RMID:0]消息长度:53字节 -------
result:{"ID":2,"Onhand":96,"Price":11.0,"ProductName":"商品1"}
[2018-01-31 17:13:45.870]取消订阅-- From: 127.0.0.1:53277
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.888 receive DTC Controller state:PreCommit
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 2PC,Child moniter task has started at time:17:13:45.889
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.890 receive DTC Controller state:DoCommit
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Try Commit..
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Try Commit..OK
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Resource at 17:13:45.895 receive DTC Controller state:Completed
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 3PC Request Completed,use time:0.0470229 seconds.
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 1PC,Child moniter task find DistTrans3PCState has changed,Now is Completed,task break!
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) Controller Process Reuslt:True,Receive time:17:13:45.900
MSF DTC(1b975548-afac-4e7a-be6d-5821bce38ce7) 2PC,Child moniter task find DistTrans3PCState has changed,Now is Completed,task break!

 

性能总结:

 订单创建成功的情况下,分布式协调器服务总共耗时 0.042516(s),订单服务耗时0.1019383秒,商品服务耗时0.0470229秒。

总体上,执行一个创建订单的分布式事务,耗时在50毫秒以内。

6.2.2,订单创建失败:

分布式协调服务:

[2018-01-31 17:04:11.669]订阅消息-- From: 127.0.0.1:53201
[2018-01-31 17:04:11.670]正在处理服务请求--From: 127.0.0.1:53201,Identity:WMI2114256838
>>[PMID:1]Service://DTCService/AttendTransaction/System.String=76d175cc-5d40-4d05-adfb-94158b5c2215
DTC Service Callback [127.0.0.1:53201-WMI2114256838] Message:CanCommit
[2018-01-31 17:04:11.679]订阅消息-- From: 127.0.0.1:53203
[2018-01-31 17:04:11.680]正在处理服务请求--From: 127.0.0.1:53203,Identity:WMI2114256838
>>[PMID:1]Service://DTCService/AttendTransaction/System.String=76d175cc-5d40-4d05-adfb-94158b5c2215
DTC Service Callback [127.0.0.1:53203-WMI2114256838] Message:CanCommit
DTC Service Callback [127.0.0.1:53201-WMI2114256838] Message:Abort
DTC Service Callback [127.0.0.1:53201-WMI2114256838] Message:Completed
DTC Service Callback [127.0.0.1:53203-WMI2114256838] Message:Abort
DTC Current Use time:0.0434914(s)
[2018-01-31 17:04:11.715]请求处理完毕(45.0015ms)--To: 127.0.0.1:53201,Identity:WMI2114256838
>>[PMID:1]消息长度:4字节 -------
result:True
Reponse Message OK.
DTC Service Callback [127.0.0.1:53203-WMI2114256838] Message:Completed
[2018-01-31 17:04:11.717]取消订阅-- From: 127.0.0.1:53201
DTC Current Use time:0.0400005(s)
[2018-01-31 17:04:11.724]请求处理完毕(44.4941ms)--To: 127.0.0.1:53203,Identity:WMI2114256838
>>[PMID:1]消息长度:4字节 -------
result:True
Reponse Message OK.
[2018-01-31 17:04:11.731]取消订阅-- From: 127.0.0.1:53203

 

订单服务:

[2018-01-31 17:04:11.662]订阅消息-- From: 127.0.0.1:53200
[2018-01-31 17:04:11.665]正在处理服务请求--From: 127.0.0.1:53200,Identity:WMI2114256838
>>[PMID:1]Service://OrderService/CreateOrder/System.Int32=2000&System.Int32=100&System.Collections.Generic.List`1[[DistTransDto.BuyProductDto, DistTransDto, Version%Eqv;1.0.0.0, Culture%Eqv;neutral, PublicKeyToken%Eqv;null]]=[{"ProductId":1,"BuyNumber":3},{"ProductI
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.672 receive DTC Controller state:CanCommit
PDF.NET AdoHelper Query Error:
DataBase ErrorMessage:;违反了 PRIMARY KEY 约束 'PK__Orders__2CE8FBFB7F60ED59'。不能在对象 'dbo.Orders' 中插入重复键。
语句已终止。
SQL:INSERT INTO [Orders]([OerderID],[OrderName],[AmountPrice],[OwnerID],[OrderTime]) VALUES (@P0,@P1,@P2,@P3,@P4)
CommandType:Text
Parameters:
Parameter["@P0"]        =       "2000"                          //DbType=Int32
Parameter["@P1"]        =       "Prudoct:,商品0,商品1"
//DbType=String
Parameter["@P2"]        =       "41"                    //DbType=Single
Parameter["@P3"]        =       "100"                   //DbType=Int32
Parameter["@P4"]        =       "2018-1-31 17:04:11"                    //DbType=DateTime

MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 1PC,Child moniter task has started at time:17:04:11.710
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Task Error:发生一个或多个错误。
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.711 receive DTC Controller state:Abort
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..OK
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..
[2018-01-31 17:04:11.712]请求处理完毕(46.5004ms)--To: 127.0.0.1:53200,Identity:WMI2114256838
>>[PMID:1]消息长度:5字节 -------
result:False
Reponse Message OK.
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..OK
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.714 receive DTC Controller state:Completed
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 3PC Request Completed,use time:0.0469998 seconds.
[2018-01-31 17:04:11.716]取消订阅-- From: 127.0.0.1:53200
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Controller Process Reuslt:True,Receive time:17:04:11.719
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 1PC,Child moniter task find DistTrans3PCState has changed,Now is Completed,task break!

 

商品服务:

[2018-01-31 17:04:11.674]订阅消息-- From: 127.0.0.1:53202
[2018-01-31 17:04:11.675]正在处理服务请求--From: 127.0.0.1:53202,Identity:WMI2114256838
>>[PMID:1]Service://ProductService/UpdateProductOnhand/System.String=76d175cc-5d40-4d05-adfb-94158b5c2215&System.Collections.Generic.List`1[[DistTransDto.BuyProductDto, DistTransDto, Version%Eqv;1.0.0.0, Culture%Eqv;neutral, PublicKeyToken%Eqv;null]]=[{"ProductId":1
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.681 receive DTC Controller state:CanCommit
----------1,-Session ID:76d175cc-5d40-4d05-adfb-94158b5c2215----------
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 1PC,Child moniter task has started at time:17:04:11.682
[2018-01-31 17:04:11.682]请求处理完毕(7.5003ms)--To: 127.0.0.1:53202,Identity:WMI2114256838
>>[PMID:1]消息长度:97字节 -------
result:[{"StoreHouse":"广州","ProductId":1,"BuyNumber":3},{"StoreHouse":"广州","ProductId":2,"BuyNumber":1}]
Reponse Message OK.
[2018-01-31 17:04:11.685]取消订阅-- From: 127.0.0.1:53202
[2018-01-31 17:04:11.686]订阅消息-- From: 127.0.0.1:53202
[2018-01-31 17:04:11.687]正在处理服务请求--From: 127.0.0.1:53202,Identity:WMI2114256838
>>[RMID:0]Service://ProductService/GetProductInfo/System.Int32=1
---------2,--Session ID:76d175cc-5d40-4d05-adfb-94158b5c2215----------
[2018-01-31 17:04:11.688]请求处理完毕(1.5019ms)--To: 127.0.0.1:53202,Identity:WMI2114256838
>>[RMID:0]消息长度:53字节 -------
result:{"ID":1,"Onhand":88,"Price":10.0,"ProductName":"商品0"}
[2018-01-31 17:04:11.690]正在处理服务请求--From: 127.0.0.1:53202,Identity:WMI2114256838
>>[RMID:0]Service://ProductService/GetProductInfo/System.Int32=2
---------2,--Session ID:76d175cc-5d40-4d05-adfb-94158b5c2215----------
[2018-01-31 17:04:11.694]请求处理完毕(4ms)--To: 127.0.0.1:53202,Identity:WMI2114256838
>>[RMID:0]消息长度:53字节 -------
result:{"ID":2,"Onhand":96,"Price":11.0,"ProductName":"商品1"}
[2018-01-31 17:04:11.694]取消订阅-- From: 127.0.0.1:53202
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.714 receive DTC Controller state:Abort
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Try Rollback..OK
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Resource at 17:04:11.717 receive DTC Controller state:Completed
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 3PC Request Completed,use time:0.0410005 seconds.
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) 1PC,Child moniter task find DistTrans3PCState has changed,Now is Completed,task break!
MSF DTC(76d175cc-5d40-4d05-adfb-94158b5c2215) Controller Process Reuslt:True,Receive time:17:04:11.731

 

性能总结:

 订单创建成功的情况下,分布式协调器服务总共耗时 0.0434914(s),订单服务耗时0.0469998秒,商品服务耗时0.0410005秒。

总体上,执行一个创建订单的分布式事务,耗时在50毫秒以内。

6.2.3,总体性能总结:

 从上面的测试结果看到,不论是订单创建成功提交事务,还是订单创建失败回滚事务,总体上事务执行时间都在50毫秒以内,多次测试也没用发现某个事务节点严重等待耗时的情况。

7,并发下单性能测试

上面测试单个分布式事务执行在50毫秒以内,那么并发执行性能怎么样呢?

可以将客户端的代码稍加改造,如下:

 private static void TestCreateOrder(Proxy client)
        {
            List<BuyProductDto> buyProducts = new List<BuyProductDto>();
            buyProducts.Add(new BuyProductDto() {  ProductId=1, BuyNumber=3});
            buyProducts.Add(new BuyProductDto() { ProductId =2, BuyNumber = 1 });

            int orderId = 7000;
            int userId = 100;

            ServiceRequest request = new ServiceRequest();
            request.ServiceName = "OrderService";
            request.MethodName = "CreateOrder";
            request.Parameters = new object[] { orderId,userId, buyProducts };

            bool result=client.RequestServiceAsync<bool>(request).Result;
            if(result)
                Console.WriteLine("创建订单成功,订单号:{0}",orderId);
            else
                Console.WriteLine("创建订单失败,订单号:{0}", orderId);

            Console.WriteLine("------开始并发下单测试,按任意键继续---------");
            Console.ReadLine();

            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            int taskCount = 2;
            List<Task> tasks = new List<Task>();
            for (int i = 1; i <= taskCount; i++)
            {
                Proxy client1 = new Proxy();
                client1.ServiceBaseUri = client.ServiceBaseUri;

                ServiceRequest request1 = new ServiceRequest();
                request1.ServiceName = "OrderService";
                request1.MethodName = "CreateOrder";
                request1.Parameters = new object[] { orderId+i, userId, buyProducts };
                var task = client1.RequestServiceAsync<bool>(request1);
                tasks.Add(task);
                Console.WriteLine("添加第 {0}个任务.",i);
            }
            Console.WriteLine("{0} 个订单请求任务创建完成,开始等待所有任务执行完成!",taskCount);
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("所有任务执行完成!");
            sw.Stop();
            Console.WriteLine("总耗时:{0}(s),TPS:{1}",sw.Elapsed.TotalSeconds,(double)taskCount /sw.Elapsed.TotalSeconds);

        }

上面程序中,变量 taskCount 表示要并发下单的任务数,TPS表示每秒处理的事务数,是一个常用的性能指标单位。

先以2个并发下单任务数测试,结果如下:

------开始并发下单测试,按任意键继续---------

添加第 1个任务.
添加第 2个任务.
2 个订单请求任务创建完成,开始等待所有任务执行完成!
所有任务执行完成!
总耗时:0.0503977(s),TPS:39.6843506747332

TPS接近40个,还可以;

再以3个并发任务数测试,结果如下:

------开始并发下单测试,按任意键继续---------

添加第 1个任务.
添加第 2个任务.
添加第 3个任务.
3 个订单请求任务创建完成,开始等待所有任务执行完成!
所有任务执行完成!
总耗时:0.3463996(s),TPS:8.66051808373913

3个并发后,性能下降很快,只有8个多TPS了。

直接测试10个并发,结果如下:

------开始并发下单测试,按任意键继续---------

添加第 1个任务.
添加第 2个任务.
添加第 3个任务.
添加第 4个任务.
添加第 5个任务.
添加第 6个任务.
添加第 7个任务.
添加第 8个任务.
添加第 9个任务.
添加第 10个任务.
10 个订单请求任务创建完成,开始等待所有任务执行完成!
所有任务执行完成!
总耗时:8.7288772(s),TPS:1.14562271537054

到10个并发后,TPS下降的很厉害,只有1个多了。

一直测试到50个并发,TPS也只有1个多,初步结论在10个以上并发TPS只能有1个多,看来在高并发下,分布式事务的性能的确不理想。

不过,本次测试的电商下单业务逻辑稍微有点复杂,其中构造订单的过程中需要反复查询几次商品库的信息,而且还有插入订单明细的操作,在数据库并发访问的时候很容易引起表锁,这也是性能下降很明显的原因。

 如果是银行跨行转账这样比较简单的例子,可能性能要高些,大家可以自己去做个测试。

8,消息服务框架的分布式事务总结

消息服务框架(MSF)成功的实现了基于3阶段提交的分布式事务协议,并且事务执行性能在分布式环境下是可以接受的。

当前实现过程中,利用消息服务框架的长连接特性,它可以及时的发现网络异常情况而不会出现出现“傻等”的问题(等到超时),这可以保证分布式事务执行的可靠性和效率。

为什么长连接能够改善分布式事务的效率?

你可以这样理解,有A,B,C三个分布式服务,它们需要完成一致性的操作,常规的做法是用复杂的分布式事务框架,但是,如果有一个M节点,它将 A,B,C连接起来并且不中断,调用它们的服务是不是像调用本地方法一个道理?这样,只需要在M节点开启和提交事务,就等于完成了分布式事务了。

 iMSF的分布式事务基于本地事务实现的,充分利用了iMSF的长连接通信能力,使得分布式事务就像是本地事务一样。

分布式事务在高并发下性能表现不理想,我们在实际项目中需要注意这个问题,但这不是iMSF的特例,而是分布式事务普遍的问题。因此,要解决高性能问题,不二之选是在系统设计的时候就考虑消息驱动模式,使用Actor并发模型,iMSF框架支持Actor模型。

 

posted on 2018-01-31 18:26  深蓝医生  阅读(4161)  评论(6编辑  收藏  举报

导航