在一個基於SOA架構的分布式系統體系中,服務(Service)成為了基本的功能提供單元,無論與業務 流程無關的基礎功能,還是具體的業務邏輯,均實現在相應的服務之中。服務對外提供統一的接口,服 務之間采用標准的通信方式進行交互,各個單一的服務精又有效的組合、編排成為一個有機的整體。在 這樣一個分布式系統中某個活動(Activity)的實現往往需要跨越單個服務的邊界,如何協調多個服務 之間的關系使之為活動功能的實現服務,涉及到SOA一個重要的課題:服務協作(Service Coordination )。而具體來講,一個分布式的活動可能會執行幾秒鐘,比如銀行轉帳;也可能執行幾分鐘、幾個小時 、幾天甚至更長,比如移民局處理移民的申請。事務,無疑是屬於短暫運行服務協作(Short-Running Service Coordination)的范疇。
一、 什麼是事務(Transaction)
事務提供一種機制將一個活動涉及的所有操作納入到一個不可分割的執行單元,組成事務的所有操作 只有在所有操作均能正常執行的情況下方能提交,只要其中任一操作執行失敗,都將導致整個事務的回 滾。簡單地說,事務提供一種“要麼什麼都不做,要麼做全套(All or Nothing)”機制。事務具有如 下四個屬性,根據其首字母,我們一般將其稱為事務的ACID四大屬性:
* 原子性(Atomicity):“原子”這個詞的本義就是不可分割的意思,事務的原子性的含義是:一 個事務的所有操作被捆綁成一個整體,所有的操作要麼全部執行,要麼都不執行;
* 一致性(Consistence):事務的原子性確保一個事務不會破環數據的一致性,如果事務成功提交 ,數據的狀態是組成事務的所有操作按照事先編排的方式執行的結果,數據狀態具有一致性;如果事務 任何一個中間步驟出錯,整個事務回滾並將數據回復到原來的狀態,數據狀態仍然具有一致性。所以, 事務只會將數據狀態從一個一致性狀態轉換到另一個一致性狀態;
* 隔離性(Isolation):從事務的外部來看,事務的一致性實現了數據在兩個一致性狀態之間的轉 換,但是從事務內部來看,組成事務的各個操作是按照一定的邏輯順序執行的,所以數據具有位於兩個 一致性狀態的“中間狀態”。但是,這種中間狀態被隔離於事務內部,對於事務外部是不可見的;
* 持久性(Durability):持久性的意思是一旦成功提交,基於持久化資源(比如數據庫)的數據將 會被持久化,對數據的改變是永久性的。
事務最初來源於數據庫管理系統(DBMS),反映的是對存儲於數據庫中的數據操作。除了主流的關系 型數據庫管理系統,比如SQL Server,Oracle和DB2等提供對事務的支持,基於事務的數據操作方式也可 以應用到其他一些數據存儲資源,比如MSMQ。自Windows Vista開始將文件系統(NTFS)以至於注冊表納 入了事務型資源(Transactional Resource)的范疇。
二、 事務的顯式控制
雖然事務型資源家族成員越來越多,但是不可否認的是,數據庫還是我們使用頻率最高的事務型資源 。對於稍微有一定經驗的開發人員,應該都在存儲過程(Stored Procedure)中編寫過基於事務的SQL, 或者編寫過基於ADO.NET事務的代碼,對事務的進一步介紹就從這裡說起。
1、SQL中的事務處理
無論是基於SQL Server的T-SQL,抑或是基於Oracle的PL-SQL都對事務提供了原生的支持,有意思的 是T-SQL中的T本身指的就是事務(Transaction)。以T-SQL為例,我們可以通過如下三個SQL語句實現事 務的啟動、提交與回滾:
* BEGIN TRANSACTION: 開始一個事務;
* COMMIT TRANSACTION:提交所有位於BEGIN TRANSACTION和COMMIT TRANSACTION之間的操作;
* ROLLBACK TRANSACTION:回滾所有位於BEGIN TRANSACTION和COMMIT TRANSACTION之間的操作。
我們舉一個很典型的基於事務型操作的例子:銀行轉帳,而且這個例子將會貫穿於本章的始終。為此 ,我們先創建一個最為簡單的用於存儲帳戶的數據表:T_ACCOUNT,整個表近僅僅包括三個字段(ID、 NAME和BALANCE),它們分別代表銀行帳號的ID、名稱和余額。創建該表的T-SQL 如下:
1: CREATE TABLE [dbo].[T_ACCOUNT](
2: [ID] VARCHAR (50) PRIMARY KEY,
3: [NAME] NVARCHAR(50) NOT NULL,
4: [BALANCE] FLOAT NOT NULL)
5: GO
銀行轉帳是一個簡單的復合型操作,由兩個基本的操作構成:存儲和提取,即從一個帳戶中提取相應 金額出入另一個帳戶。對數據完整性的要求是我們必須將這兩個單一的操作納入同一個事務。如果我們 通過一個存儲過程來完成整個轉帳的流程,具體的SQL應該采用下面的寫法:
1: CREATE Procedure P_TRANSFER
2: (
3: @fromAccount VARCHAR(50),
4: @toAccount VARCHAR (50),
5: @amount FLOAT
6: )
7: AS
8:
9: --確保帳戶存在性
10: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @fromAccount)
11: BEGIN
12: RAISERROR ('AccountNotExists',16,1)
13: RETURN
14: END
15: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @toAccount)
16: BEGIN
17: RAISERROR ('AccountNotExists',16,1)
18: RETURN
19: END
20: --確保余額充足性
21: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @fromAccount AND BALANCE >= @amount)
22: BEGIN
23: RAISERROR ('LackofBalance',16,1)
24: RETURN
25: END
26: --轉帳
27: BEGIN TRANSACTION
28: UPDATE [dbo].[T_ACCOUNT] SET BALANCE = BALANCE - @amount WHERE ID = @fromAccount
29: IF @@ERROR <> 0
30: BEGIN
31: ROLLBACK TRANSACTION
32: END
33: UPDATE [dbo].[T_ACCOUNT] SET BALANCE = BALANCE + @amount WHERE ID = @toAccount
34: IF @@ERROR <> 0
35: BEGIN
36: ROLLBACK TRANSACTION
37: END
38: COMMIT TRANSACTION
39: GO
2、 ADO.NET事務控制
無論是T-SQL,還是PL-SQL,抑或是其他數據庫管理系統對標准SQL的擴展,不僅僅是提供基於標准 SQL的DDL(Data Definition Language)和DML(Data Manipulation Language),還提供了對函數、存 儲過程和流程控制的支持。SQL Server至2005起,甚至實現了與CLR(Common Language Runtime)的集 成,使開發人員可以使用任何一種.NET語言編寫編寫函數或者存儲過程。毫無誇張地說,你可以通過SQL 實現任何業務邏輯。
但是,在大多數情況我們並不這麼做,我們更多地還是將SQL作為最基本的數據操作語言在使用。對 於.NET開發者來說,我們還是習慣將復雜的邏輯和流程控制實現在通過C#或者VB.NET這樣的面相對象編 程語言編寫的程序中。究其原因,我覺得主要有兩點:
* 面相對象的語言更能容易地實現復雜的邏輯:較之SQL這種基於集合記錄的語言,面相對象的語言 更加接近於我們真實的世界,通過面相對象的方式模擬具體的邏輯更加貼近於人類的思維方式。此外, 通過面相對語言本身的一些特性,我們可以更加容易地應用各種設計模式和思想;
* 將太多邏輯運算的執行放在數據庫中不利於應用的擴展:從部屬的角度來講,數據操作運算負載到 具體的服務器中,以一個典型的分布式Web應用為例,Web服務器(承載Web應用)、應用服務器(承載各 種服務)和數據庫服務器均可以承載最終對邏輯的運算。但是,從可擴展性(或者可伸縮性)上考慮, 將主要的計算放在前兩者比放在數據庫更具優勢。如果我們將密集的運算(這種運算需要占用更多的CPU 時間和內存)遷移到Web服務器或者應用服務器,我們可以通過負載均衡(Load Balance)將其分流到多 台服務器上面,這個服務器機群可以根據負載情況進行動態地配置。但是,數據庫服務器對負載均衡的 支持就不那麼容易。
正因為如此,對於事務的控制,較之采用SQL的實現方式,我們使用得最多的還是采用基於面相對象 語言編程的方式。對於.NET開發人員,我們可以直接利用ADO.NET將基於單個數據庫連接的多個操作納入 同一個事務之中。同樣以上面的銀行轉帳事務為例,這次我們將整個轉帳作為一個服務 (BankingService)的一個操作(Transfer)。下面的代碼通過一種與具體數據庫類型無關的ADO.NET編 程模式實現了整個銀行轉帳操作,最終的轉帳通過調用一個存儲過程實現:
1: public class BankingService : IBankingService
2: {
3: //其 他操作
4: public void Transfer(string fromAccountId, string toAccountId, double amount)
5: {
6: string connectionStringName = "BankingDb";
7: string connectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
8: string providerName = ConfigurationManager.ConnectionStrings [connectionStringName].ProviderName;
9: DbProviderFactory dbProviderFactory = DbProviderFactories.GetFactory(providerName);
10: using (DbConnection connection = dbProviderFactory.CreateConnection())
11: {
12: connection.ConnectionString = connectionString;
13: DbCommand command = connection.CreateCommand();
14: command.CommandText = "P_TRANSFER";
15: command.CommandType = CommandType.StoredProcedure;
16:
17: DbParameter parameter = dbProviderFactory.CreateParameter();
18: parameter.ParameterName = BuildParameterName("fromAccount");
19: parameter.Value = fromAccountId;
20: command.Parameters.Add(parameter);
21:
22: parameter = dbProviderFactory.CreateParameter();
23: parameter.ParameterName = BuildParameterName("toAccount");
24: parameter.Value = toAccountId;
25: command.Parameters.Add (parameter);
26:
27: parameter = dbProviderFactory.CreateParameter();
28: parameter.ParameterName = BuildParameterName("amount");
29: parameter.Value = amount;
30: command.Parameters.Add (parameter);
31:
32: connection.Open();
33: using (DbTransaction transaction = connection.BeginTransaction())
34: {
35: command.Transaction = transaction;
36: try
37: {
38: command.ExecuteNonQuery();
39: transaction.Commit();
40: }
41: catch
42: {
43: transaction.Rollback();
44: throw;
45: }
46: }
47: }
48: }
49: }
注:為了使上面一段代碼能夠同時用於不同的數據庫類型,比如SQL Server和Oracle,我通過提取連 接字符串配置中的數據庫提供者(DbProvider)名稱,借此創建相應的 DbProviderFactory對象。所有 ADO.NET對象,包括DbConnection、DbCommand、DbParameter以及 DbTransaction均通過 DbProviderFactory創建,所以並不和具體的數據庫類型綁定在一起。此外,基於不同數據庫類型的存儲 過程的參數命名各不相同,比如 SQL Server的參數會添加”@”前綴,為此我將對參數名稱的解析實現 在一個單獨的方法(BuildParameterName)之中。
3、事務的顯式控制限定於對單一資源的訪問
通過在SQL中進行事務的控制,只能將基於某一段SQL語句的操作納入到一個單一的事務中;如果采用 基於ADO.NET的數據控制,被納入到同一個事務的操作僅僅限於某個數據庫連接。換句話說,上面介紹的 這兩種對事務的顯式控制僅僅限於對單一的本地資源的控制。
我們將事務的概念引入服務,倘若我們將一個單一的服務操作作為一個事務,如果采用上述的顯式事 務控制的方式,那麼整個服務操作只能涉及一個單一的事務資源。服務於存取的資源關系如圖1所以。
圖1 本地事務對單一資源的控制
上述的這種基於某個服務單一本地資源的訪問的事務,被稱為本地事務(Local Transaction),在 一個基於SOA分布式應用環境下,我們需要的同時能將多個資源、多個服務進行統一協作的分布式事務( Distributed Transaction)。接下來,我們來介紹幾種典型的分布式事務應用的場景。
三、分布式事務(Distributed Transaction)應用場景
對於一個分布式事務(Distributed Transaction)來講,事務的參與者分布於網絡環境中的不同的 節點。也就是說,我們可以將多個事務資源納入到一個單一的事務之中,並且這些事務資源可以分布到 不同的機器上。這些承載分布式資源的機器可能是出於同一個網絡中,也可能處於不同的網絡中。甚至 說,某個事務資源本質上就是一個通過HTTP 訪問的單純的Internet資源。
站在SOA的角度來看分布式事務,意味著將服務的某個服務操作視為一個單一的事務。該服務操作可 能會訪問不止一個事務資源(比如訪問兩個不同的數據庫服務器),也可能調用另一個服務。下面介紹 了三個典型的分布式事務應用場景,先從最簡單的說起。
1、將對多個資源的訪問納入同一事務
第一個分布式事務應用場景最簡單,即一個服務操作並不會調用另一個服務,但是服務操作涉及到對 多個事務資源的訪問。當一個服務操作訪問不同的數據庫服務器,比如兩台SQL Server,或者一台SQL Server和一台Oracle Server;當一個服務操作訪問的是相同數據庫,但是相應的數據庫訪問時基於不同 的數據連接;當一個服務操作處理訪問數據庫資源,還需要訪問其他份數據庫的事務資源,就需要采用 分布式事務來對所有的事務參與者進行協作了。圖2反映了這樣的分布式應用場景。
圖2 單一服務對多個事務資源的訪問
2、將對各個服務的調用納入同一事務
對於上面介紹的分布式應用場景,盡管一個服務操作會訪問多個事務資源,但是畢竟整個事務還是控 制在單一的服務內部。如果一個服務操作需要調用另外一個服務,這是的事務就需要跨越多個服務了。 在這種情況下,起始於某個服務的事務在調用另外一個服務的時候,需要以某種機制流轉到另外一個服 務,以使被調用的服務訪問的資源自動加入進來。
圖3
反映了這樣一個跨越多個服務的分布式事務。
圖3 跨越多個服務的事務
3、 將對多個資源和服務的訪問納入同一個事務
如果將上面這兩種場景(一個服務可以調用多個事務資源,也可以調用其他服務)結合在一起,對此 進行延伸,整個事務的參與者將會組成如圖4
所示的樹形拓撲結構。在一個基於分布式事務的服務調用中,事務的發起者和提交均系同一個,它可 以是整個調用的客戶端,也可以是客戶端最先調用的那個服務。
圖4 基於SOA分布式事務拓撲結構
較之基於單一資源訪問的本地事務,分布式事務的實現機制要復雜得多。Windows平台提供了基於DTC 分布式事務基礎架構,下一篇文章中我將對針對該架構模型詳細介紹分布式事務時如何工作的。