使用“訊息服務框架”(MSF)實現分散式事務的三階段提交協議(電商建立訂單的示例)
1,示例解決方案介紹
在上一篇 《訊息服務框架(MSF)應用例項之分散式事務三階段提交協議的實現》中,我們分析了分散式事務的三階段提交協議的原理,現在我們來看看如何使用訊息服務框架(MSF)來具體實現並且看用它來實現的一些優勢。
首先,從Github克隆專案原始碼,地址:https://github.com/bluedoctor/MSF-DistTransExample
解決方案如下圖:
![](https://img.796t.com/res/2022/04-29/02/feb6a1a10a2d8eb9047953a74d4bcc21.png)
我們看到解決方案有4個專案:
- DistTransClient:分散式事務示例的客戶端,它呼叫“訂單服務”,建立一個訂單,服務會返回建立結果是成功還是失敗;
- DistTransDto:包含商品,訂單和訂單詳情的實體型別介面以及相關的介面實現;
- DistTransServices:包含訂單服務,商品服務和分散式事務控制器服務;
- TistTransApp:本測試的宿主程式專案,主要用於安裝訊息服務框架的服務宿主程式,以及啟動訂單,商品和分散式事務控制器的服務程序。
2,建立訂單的業務簡介
2.1,基本概念
下面先介紹本示例要解決的業務,並通過這個業務來分析分散式事務的執行過程。
在本示例中,使用的是電商系統最常見的業務場景:下單業務,它的業務流程也概括起來比較簡單:
建立訂單:
- 生成訂單基本資訊;
- 生成訂單專案明細(已購商品清單):
- 檢查庫存是否足夠
- 扣減庫存
當然,在具體的電商業務系統中,下單業務比較複雜,特別是對庫存的扣減方式,但大體的業務流程就是這樣的,我們今天的重點是研究這個下單過程在分散式環境下如何實現。
2.2,微服務架構
假設我們的電商平臺使用微服務架構的,包含了使用者服務,商品服務,訂單服務和支付服務,這4個服務在下單業務中的功能分別如下:
- 使用者服務:檢查當前使用者是否有效,查詢使用者的相關資訊,比如使用者姓名,聯絡電話等;
- 訂單服務:生成訂單,包括結合使用者服務的使用者資訊,生成訂單基本資訊;結合商品服務,生成訂單專案明細;
- 商品服務:向訂單服務返回商品的相關資訊,並返回庫存是否可用,如果可用就扣減庫存;
- 支付服務:由第三方提供,但參與建立訂單的流程,使用者下單後需要使用者去第三方支付系統完成支付,然後支付服務回撥訂單服務,完成有效訂單確認。
下面是這4個服務在建立訂單的業務流程圖:
![](https://img.796t.com/res/2022/04-29/02/d95ac1787a41446538e2e715c8275cf0.png)
上圖中,支付服務是第三方提供的服務,需要使用者在建立訂單後跳轉呼叫,所以本質上不是訂單服務直接呼叫,訂單服務需要提供一個支付完成的回撥通知介面,完成有效訂單的確認。 而使用者服務作為服務呼叫的發起方,它會傳遞必要的資訊給訂單服務,因此,對於“建立訂單”這個具體的業務功能,它涉及的需要同時進行操作的只有建立訂單和扣減庫存這兩個子業務,並且要求這2個子業務操作具有原子性,即要麼同時成功,要麼同時失敗撤銷,所以這兩個操作組成一個事務操作,在我們當前的場景中,它是一個分散式事務。
2.3,分散式事務中的微服務容器
在本例中,我們使用訊息服務框架(MSF)來實現分散式事務,為了更加真實的模擬微服務架構,我們將建立訂單相關的服務劃分為3個獨立的程序,這些程序就是MSF.Host服務容器,這裡分為3個服務容器:
- 協調器服務容器:執行分散式事務協調器服務;
- 訂單服務容器:執行訂單服務和分散式事務控制器元件;
- 商品服務容器:執行商品服務和分散式事務控制器元件。
下面是這3個服務容器的程序呼叫關係圖:
![](https://img.796t.com/res/2022/04-29/02/054d8d09f5a9934d594fe84fd18d8ec4.png)
3,建立訂單的分散式事務流程
下面來看建立訂單的分散式事務處理過程,為簡單起見,只討論正常的流程,其中異常的流程,請參考原文對於3階段提供分散式事務的具體原理。
![](https://img.796t.com/res/2022/04-29/02/31220b6218f5c24e0f5a67584c36c33c.png)
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階段分散式事務”的各個階段進行流程控制,其中每一階段,都要和“分散式事務協調服務”進行通訊,接受它的指令,完成本地事務資源的控制,比如是提交還是回滾事務資源。下面我們看看它主要的程式碼:
![](https://img.796t.com/res/2022/04-29/02/03dfc18236014c39e6e634aa5a704bcd.png)
在上面的函式中,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=~DataBaseOrdersDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
<add name="ProductsDb" connectionString="Data Source=(LocalDB)MSSQLLocalDB;AttachDbFilename=~DataBaseProductsDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
-->
<!-- MSSQLLocalDB 連線示例
<add name="OrdersDb" connectionString="Data Source=(LocalDB)MSSQLLocalDB;AttachDbFilename=E:GitMSF-DistTransExampleHostDataBaseOrdersDB_data.mdf;Integrated Security=True;Connect Timeout=30" providerName="SqlServer"/>
<add name="ProductsDb" connectionString="Data Source=(LocalDB)MSSQLLocalDB;AttachDbFilename=E:GitMSF-DistTransExampleHostDataBaseProductsDB_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=~DataBaseOrdersDb.accdb;Persist Security Info=False;" providerName="Access"/>
<add name="ProductsDb" connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=~DataBaseProducts.accdb;Persist Security Info=False;" providerName="Access"/>
-->
<!-- SOD for Access 2000,2003
<add name="OrdersDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=~DataBaseOrdersDb.mdb;Persist Security Info=False;" providerName="Access"/>
<add name="ProductsDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=~DataBaseProducts.mdb;Persist Security Info=False;" providerName="Access"/>
-->
<!-- SOD for SQLite
<add name="OrdersDb" connectionString="Data Source=DataBaseOrdersDb.db;" providerName="PWMIS.DataProvider.Data.SQLite,PWMIS.SQLiteClient"/>
<add name="ProductsDb" connectionString="Data Source=DataBaseProducts.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>
配置檔案中配置了多種資料庫連線方式,根據你的情況具體選擇。當前是SqlServer.
然後,按照下圖輸入相關的資訊:
![](https://img.796t.com/res/2022/04-29/02/8dcd26240eb86678c903672a74c1b205.png)
由於我現在的測試環境是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階段提交的分散式事務協議,並且事務執行效能在分散式環境下是可以接受的。
當前實現過程中,利用訊息服務框架的長連線特性,它可以及時的發現網路異常情況而不會出現出現“傻等”的問題(等到超時),這可以保證分散式事務執行的可靠性和效率。
分散式事務在高併發下效能表現不理想,我們在實際專案中需要注意這個問題。