發布日期: 12/15/2005 | 更新日期: 12/15/2005
本頁內容
關于 SOA 設計原則系列文章
本文是系列文章中的第一篇,集中討論如何設計更有效的 Web 服務。此開篇文章是與 Microsoft 的 patterns & practices 團隊合作完成的,今后我們還會與 Microsoft 內部和外部的其他團隊和個人合作。
讀者應慎用“SOA 設計原則系列”開發的示例代碼。示例代碼僅用于演示目的,鼓勵讀者研究、編譯和學習這些代碼。讀者應避免在生產環境中嘗試使用任何示例代碼。如果讀者決定在生產環境中使用任何示例代碼,MICROSOFT 對其造成的損害不承擔任何責任。
示例代碼的系統要求:
? |
Microsoft Windows XP(示例未在其他平臺上測試)
|
? |
Microsoft .NET Framework 1.1
|
? |
Microsoft Internet Information Server 6.0
|
? |
Microsoft SQL Server 或 Microsoft Access
|
? |
Microsoft Visual Studio 2003 不是必需的,但如果使用會使總體效果更佳
|
雖然 Microsoft 對于示例代碼不提供支持,但本文的作者仍歡迎您給予反饋。
注意 由于 SOA 正在快速發展,隨著它的不斷成熟,本系列中的文章和示例代碼可能會有修訂。
簡介面向服務的體系結構 (SOA)
目前,SOA 已成為廣為人知而且存在些分歧的縮寫詞。如果要兩個人給 SOA 下個定義,可能會有兩個差別懸殊、甚至可能是截然對立的答案。有人將 SOA 描述為啟動業務的 IT 基礎結構,而其他人則認為 SOA 可以提高 IT 的效率。在許多方面,SOA 有點像 John Godfrey Saxe 的那首盲人與象的詩:每個人對那頭象都有不同的描述,因為他們都受到了各自經驗的影響(例如,摸到象鼻子的人認為它是條蛇,而摸到象牙的人則認為它是矛)。相對而言,Saxe 先生的象還是很容易描述的,因為它畢竟是真實存在的實體:而 SOA 更加難以描述,因為設計思想是無法以物理形式表現的。
SOA 是用于從自治服務構建系統的體系結構方法。有了 SOA,集成更具前瞻性,而不是事后反思:最終的解決方案可能由以不同編程語言開發的服務組成,由具有各種安全模型和業務流程的不同平臺托管。盡管這一概念聽起來驚人的復雜,它卻并不是新生事物:有人認為,SOA 是從基于先前可用技術設計和開發分布式系統的經驗演化而來的。與 SOA 相關的許多概念,如服務、發現和后期綁定,都與 CORBA 和 DCOM 有關。同樣,許多服務設計原則與早期基于封裝、抽象和定義明確的接口的 OOA/OOD 技術有著許多共同之處。
縮寫詞 SOA 提出了一個很明顯的問題:到底什么是服務?簡而言之,服務即可以通過定義明確的消息交換進行交互的程序。服務的設計必須考慮可用性和穩定性。服務的構建要考量持久性,而服務配置和聚合的構建則要考量變化性。靈活性經常上升為 SOA 的一個最大優勢:與受底層單一性應用程序的約束、最小的變動也要幾個星期才能實現的組織相比,在松散耦合的基礎結構上實現業務流程的組織對于變動要開放得多。松散耦合的系統會帶來松散耦合的業務流程,因為業務流程不再受底層基礎結構局限性的約束。服務及其關聯接口必須保持穩定,以便能夠重新配置或重新聚合來滿足日新月異的業務需求。服務的穩定性依賴于基于標準的接口和定義明確的消息:即,用于消息定義的 SOAP 和 XML 架構。如果服務用于執行簡單而精確的功能、幾乎不知道消息是如何傳遞過來的或是如何檢索出來的,那么它在更大的 SOA 基礎結構中被重用的可能性更大。如前所述,在我們設計和構建可重復使用的 Web 服務時,應當回顧有關封裝和接口設計的 OO 設計原則。通過進一步理解常說的面向服務的“四條原則”,我們可以將這些 OO 原則擴展到 Web 服務領域。
原則 1:邊界是顯式的
通過跨越定義明確的邊界進行顯式消息傳遞,服務得以彼此交互。有時候,跨越服務邊界可能要耗費很大的成本,這要視地理、信任或執行因素而定。邊界是指服務的公共接口與其內部專用實現之間的界線。服務的邊界通過 WSDL 發布,可能包括說明特定服務之期望的聲明。跨越邊界之所以被認為是代價高昂的任務,是有多種原因的,下面列出其中幾個:
? |
目標服務的物理位置可能是未知的因素。
|
? |
安全和信任模型可能會在每次跨越邊界時發生改變。
|
? |
在服務的公共表示和專用表示之間封送和轉換數據可能需要依賴額外的資源:其中一些資源可能在服務之外。
|
? |
服務的構建要考量持久性,而服務配置的構建則要考量變化性。這一事實暗示著:由于網絡重新配置或者遷移到另一個物理位置,可靠的服務的性能可能會突然降低。
|
? |
服務的使用者通常不知道專用的內部過程是如何實現的。特定服務的使用者對正使用的服務的性能只能進行有限的控制。
|
面向服務的集成(英文)模式告訴我們“服務調用可能會受到網絡滯后、網絡故障和分布式系統故障的影響,而本地實現則不會。要預先考慮使用遠程對象接口的影響,就必須編寫大量的錯誤檢測和更正邏輯。”盡管我們認為跨越邊界是代價高昂的過程,但在部署用于將此類邊界跨越減至最少的本地方法時,我們還是要格外謹慎。實現單一性本地方法和對象的系統可能會獲得性能的改善,但功能性卻與先前定義的服務完全一樣(此項技術在 OOP 中稱為“剪切與粘貼”,同樣存在服務版本更新的風險)。
關于第一條 SO 原則,有幾點原則需要銘記在心:
? |
弄清邊界。服務提供一個合約來定義其提供的公共接口。與服務的所有交互都通過公共接口進行。接口由公共進程和公共數據表示組成。公共進程是通向服務內部的入口點,而公共數據表示則是指該進程使用的消息。如果我們使用 WSDL 代表一個簡單的合約,則 <message> 代表公共數據,而 <portType> 代表公共進程。文章“外部數據與內部數據”(英文)更詳細地研究了這些問題。
|
? |
服務應易于使用。設計服務時,開發人員應使其易于其他開發人員使用。設計的服務接口(合約)也應允許服務在不中斷與現有使用者之間的合約的情況下進一步發展。(這一主題將在本系列的后續文章中更詳細地討論。)
|
? |
避免使用 RPC 接口。應采用顯式消息傳遞,而避免使用 RPC 之類的模型。這種方法將使用者與服務實現的內部隔離開來,使開發人員可以集中精力改進他們的服務,同時將對服務使用者的影響降至最低(使用公共消息而不是公用的方法進行封裝)。
|
? |
盡量減小服務的表面積。服務的公共接口越多,就越難以使用和維護。應當少提供服務的定義明確的公共接口。這些接口應該相對簡單,主要用于接受定義明確的輸入消息并以同樣定義明確的輸出消息進行響應。這些接口一旦定義,即應保持不變。這些接口提供服務必須支持的“恒定不變”的設計要求,為服務專用的內部實現充當門面。
|
? |
內部(專用)實現的細節不應泄露到服務邊界之外。如果將實現細節泄露到服務邊界,很可能會使服務與服務使用者之間的耦合更加緊密。服務使用者不應當獲知服務實現的內部情況,因為這樣會使服務的版本更新或升級受到限制。本文的“反模式”部分就此問題提供了一個詳細的示例。
|
原則 2:服務具有自治性
服務是獨立進行部署、版本控制和管理的實體。開發人員應避免對服務邊界之間的空間進行假設,因為此空間比邊界本身更容易改變。例如,服務邊界應當是不變的,只有這樣才能將版本更新對使用者的影響降至最低。雖然服務邊界是相當穩定的,但策略、物理位置或網絡拓撲等服務部署選項卻很可能發生改變。
服務可以通過 URI 動態尋址,使其底層位置和部署拓撲可以在幾乎不影響服務本身的情況下改變或演化(服務的通信通道也是如此)。盡管這些更改對服務沒什么影響,但它們對使用服務的應用程序卻有著破壞性的影響。如果您今天使用的服務明天被移動到新西蘭,將會怎樣呢?響應時間的改變可能會對服務的使用者造成計劃之外或意料之外的影響。服務設計者對于服務的使用方式應當采取謹慎的態度:服務將失敗,其相關的行為(服務級別)可能會被更改。適當級別的例外處理和補償邏輯必須與任何服務調用相關聯。此外,服務使用者可能需要修改其策略,以聲明要使用的最短服務響應時間。例如,服務使用者可以對安全、性能、事務及許多其他因素請求不同的服務級別。可配置的策略允許單個服務支持多個有關服務調用的 SLA(而其他策略可能主要關注版本更新、本地化及其他問題)。服務級的通信性能期望始終是自治,因為服務彼此之間不需要熟悉對方的內部實現。
不止是服務使用者應該對性能采取謹慎的態度:服務提供者在預測其服務的使用方式時也應同樣謹慎。應該預料到服務使用者有時候會失敗,卻又不通知服務本身。服務提供者也不能信任使用者總是“為所應為”。例如,使用者可能會嘗試使用不正常的/惡意的消息進行通信,或者嘗試違反成功實現服務交互所必需的其他策略。無論用戶意圖如何,服務內部都必須嘗試對此類不恰當的使用進行補償。
雖然服務是自治的,但任何服務都不是孤島。基于 SOA 的解決方案是不規則的,由許多為特定解決方案配置的服務組成。從自治的角度思考,您很快就會發現面向服務的環境中并沒有主領機構:編排服務中的“指揮器”概念是錯誤的(進一步意味著,跨服務的“回滾”概念也是錯誤的:但這最好留待在另一篇文章中討論)。實現自治服務的關鍵是隔離和去耦合。服務都彼此獨立地設計和部署,并且只能使用合約驅動的消息和策略進行通信。
至于其他服務設計原則,我們可以學習過去 OO 設計的經驗。Peter Herzum 及 Oliver Sims 就 Business Component Factories 的著作提供了對自治組件特性的一些有趣見解。雖然他們大部分的工作最適于大粒度、基于組件的解決方案,但其基本設計原則仍然適用于服務設計。
鑒于這些考量,在此提供一些有助于確保符合第二條 SO 原則的簡單設計原則:
? |
服務的部署和版本控制應獨立于部署和使用它們的系統。
|
? |
合約的設計應符合以下假設,即一旦公布即不可修改。這種方法迫使開發人員在其架構設計中構建靈活性。
|
? |
采取謹慎的態度,使服務免于故障。從使用者的角度,規劃服務可用性和性能的不可靠級別。從提供者的角度,預計服務會被誤用(故意或其他方式),預計服務使用者會出現故障:而服務可能得不到通知。
|
原則 3:服務共享架構和合約,但不共享類
如上所述,服務交互應當只以服務的策略、架構和基于合約的行為為基礎。服務的合約通常使用 WSDL 定義,而服務聚合的合約則可以使用 BPEL 定義(進而,對聚合的每個服務使用 WSDL)。
大多數開發人員定義類來代表特定問題空間中的各種實體(例如,“客戶”、“訂單”和“產品”)。類將行為與數據(消息)組合到一個特定于編程語言或平臺的構造函數。服務打破了這種模式,最大程度提高了靈活性和互操作性。使用基于 XML 架構的消息進行通信的服務獨立于編程語言與平臺,從而拓寬了互操作性的級別。架構用于定義消息的結構和內容,而服務合約則用于定義服務本身的行為。
總起來說,服務的合約由以下元素組成:
? |
使用“XML 架構”定義的消息交換格式。
|
? |
使用 WSDL 定義的“消息交換模式”(MEP)。
|
? |
使用 WS-Policy 定義的功能和要求。
|
? |
BPEL 可以用作業務流程級合約,用以聚合多個服務。
|
服務使用者將依靠服務的合約來調用服務及與服務交互。鑒于這種依賴性,服務合約必須長期保持穩定。在利用 XML 架構 (xsd:any) 和 SOAP 處理模型(可選標頭)的可擴展性的同時,合約的設計應盡可能明確。
“第三條原則”的最大挑戰是其持久性。服務合約一旦公布,要想修改它而又盡可能減小對現有服務使用者的影響,是極其困難的。內部數據表示與外部數據表示之間的界線對于特定服務的成功部署和重用至關重要。公共數據(在服務之間傳遞的數據)應基于組織或縱向標準,確保能夠跨不同服務被廣泛接受。私有數據(服務內部的數據)封裝在服務之內。在某種程度上,服務就像是實施電子商務交易的組織的縮微代表。如同組織必須將外來采購訂單映射為其內部 PO 格式一樣,服務也必須將通過合約達成一致的數據表示映射為其內部格式。與前面一樣,我們可以再次使用 OO 數據封裝經驗來闡明類似的概念:服務的內部數據表示只能通過服務合約處理。Pat Helland 在“外部數據與內部數據”(英文)中探討了幾個關于公共數據表示與私有數據表示的問題。
鑒于這些考量,在此提供一些有助于確保符合第三條 SO 原則的簡單設計原則:
? |
確保服務合約保持穩定,以將對服務使用者的影響降至最低。這里的合約指公共數據表示(數據)、消息交換模式 (WSDL) 和可配置的功能和服務級別(策略)。
|
? |
合約的設計應盡可能明確,以將誤解減至最少。此外,應通過 XML 語法和 SOAP 處理模型的可擴展性使合約能夠適應未來服務的版本更新。
|
? |
避免使公共數據表示與私有數據表示之間的界線混淆不清。使用者不應看到服務的內部數據格式,而其公共數據架構應不可改變(最好基于組織標準、事實標準或行業標準)。
|
? |
當不可避免要更改服務合約時,要對服務進行版本控制。這種方法對現有使用者實現的破壞程度最小。
|
原則 4:服務兼容性基于策略
盡管它往往被認為是最不為人所了解的原則,但對于實現靈活的 Web 服務,它或許是最有力的。單純依靠 WSDL 無法交流某些業務交互要求。可以使用策略表達式將結構兼容性(交流的內容)與語義兼容性(如何交流消息或者將消息交流給誰)分隔開來。
服務提供者的操作要求可以通過計算機能識別的策略表達式來表現。策略表達式提供一組可以配置的可互操作語義,用以控制特定服務的行為和期望。WS-Policy 規范定義了一個計算機能識別的策略框架,它可以表達服務級策略,使它們得以在運行時被發現或實施。例如,政府安全服務可能需要一個實施特定服務級別的策略(例如,必須對照恐怖分子識別系統對符合指定條件的護照像片進行交叉檢查)。與此項服務關聯的策略信息可以用于許多與實施背景檢查有關的其他情形或服務。WS-Policy 無需任何額外代碼即可用于實現這些要求。本例闡明策略框架如何在提供業務定義和執行的聲明編程模型的同時,提供有關服務要求的附加信息。
策略聲明可以確定哪些行為是策略主體的要求(或功能)。(在上面的例子中,聲明是指對照恐怖分子識別系統進行背景檢查。)聲明提供特定于域的語義,并最終在各縱向行業的單獨、特定于域的規范之內定義(建立 WS-Policy “框架”概念)。
盡管策略驅動的服務仍然在不斷發展,但開發人員仍應確保其策略聲明在服務期望和服務語義兼容性方面盡可能明確。
SOA 模式與反模式
既然您已經對 SOA 概念(包括 SO 設計原則)有所了解,就讓我們開始將學習到的知識付諸實踐吧。本文余下篇幅將介紹兩個“反模式”和三個“模式”。這些“反模式”和“模式”都建立在前述概念的基礎之上。
為什么使用模式和反模式?
人們的思考和交流往往遵循一些模式。幾部關于模式語言的書籍的作者 Christopher Alexander 將模式定義為“由在特定的非任意上下文中不斷出現的具體形式概括出來的抽象概念”。可通過模式和模式語言描述最佳實踐方法、已被認可的設計并記錄過去經驗,在某種程度上供他人從中學習。模式是快速理解設計原則和它們所應用的各種環境的有效方法。而如您所料,反模式與模式相對。模式提供實踐證明的指導原則和最佳經驗,而反模式則闡明常見的設計缺陷,用于吸取他人的教訓。本文余下的篇幅簡略介紹兩個反模式和三個模式,它們都可以幫助開發更有效的 Web 服務。
本文中的反模式和模式遵循下列格式:
? |
上下文:模式或反模式的簡要背景。提供上下文是為了幫助讀者在徹底實例化之前發現可能應用模式或識別出反模式特征的機會。
|
? |
問題:簡單的描述,旨在制定與模式或反模式相關的目標。
|
? |
影響因素:應用特定模式或識別反模式時必須考慮的其他事項。
|
? |
解決方案:對特定反模式的解決方案或應用相關模式的必要步驟的描述。
|
? |
故障現象與后果:對于反模式,是指導致反模式存在的因素。對于模式,故障現象與后果可以描述在應用相關模式之前要考慮的其他因素和事項。
|
反模式中還包括關于如何改進相關設計缺陷的其他建議。
關于代碼示例的說明
? |
每個模式的代碼示例都可以下載。
|
? |
每個示例均已包裝為安裝文件 (MSI),并且提供說明示例的安裝和配置方法的 README 文件。
|
? |
如上所述,讀者應慎用示例代碼。示例代碼僅用于演示目的,盡管我們鼓勵讀者研究、編譯和學習這些代碼,但讀者應避免在生產環境中嘗試使用任何示例代碼。如果讀者決定在生產環境中使用任何示例代碼,MICROSOFT 對其造成的損害不承擔任何責任。
|
反模式 1:CRUDy 接口
? |
上下文:
? |
您需要為新公司 SOA 項目設計 Web 服務。
|
|
? |
問題:
|
? |
影響因素:
? |
自 Visual Basic 5 后,您一直是 Visual Basic 開發人員,“知道”如何創建組件。
|
? |
這是您的第一個 SOA 項目。
|
? |
您所在的組織希望其他平臺應用程序使用您的服務。
|
|
? |
解決方案:
? |
就像過去設計組件接口那樣設計服務接口。
|
? |
創建實現 CRUD(創建、讀取、更新、刪除)操作的服務。
|
? |
示例代碼段如代碼清單 1 所示。
|
|
代碼清單
1. Visual Basic .NET CRUD
服務示例
<WebMethod()> _
Public Sub Create( ByVal CompanyName As String, ByVal
ContactName As String, ByVal ContactTitle As String,
ByVal Address As String, ByVal City As String, ByVal
State As String, ByVal Zip As String, ByVal Country As
String, ByVal Telephone As String, ByVal Fax As String)
<WebMethod()> _
Public Function MoveNext() As Boolean
End Function
<WebMethod()> _
Public Function Current() As Object
End Function
<WebMethod()> _
Public Sub UpdateContactName( ByVal NewName as String)
End Sub
<WebMethod()> _
Public Function CommitChanges()
End Function
故障現象與后果:
? |
此接口設計鼓勵 RPC 形式的行為,即調用 Create、MoveNext 等等,而不是發送用于指示要采取哪些操作的定義明確的消息。這違反了第一條(定義明確的邊界)和第三條(只共享架構)原則。
|
? |
接口的通信很可能過度頻繁而瑣碎,因為使用者可能需要調用兩三個方法才能完成其工作。
|
? |
對 Create 方法使用 Sub 意味著使用者無從知道操作是成功還是失敗。設計服務時,始終要牢記使用者的期望:使用者需要知道什么?
|
? |
CRUD 操作對于 Web 服務是錯誤的分解級別。CRUD 操作可以在服務內部或不同服務之間實現,但是不應以這種方式提供給使用者。有些服務允許內部(私有)功能提供給服務公共接口,這就是一例。
|
? |
此接口意味著將發生有狀態的交互,如枚舉(參見 MoveNext 和 Current 函數)。
|
? |
抽象類型(如 Current 函數返回的 Object)將導致弱合約。這是違反第三條原則(只共享架構)的又一例。
|
? |
這是一個非常危險的服務,因為它可能會使底層數據處于不一致的狀態。如果使用者添加新的“聯系人”(或者更新現有“聯系人”),而從不調用 CommitChanges 函數,那樣會發生什么情況呢?如前所述,服務提供者不能信任使用者總是“為所應為”。
|
按以下列方式更改服務后,可以避免上列風險和問題:
? |
將服務接口改為使用 XML 架構進行通信。架構還可以包括目標服務操作(例如,“新建聯系人架構”或“更改聯系人架構”)。開發自己的架構之前,開發人員應參考現有行業標準。滿足您需要的架構可能已經存在。
|
? |
將數據處理封裝在私有方法之內,只能使用傳遞給公共接口的架構訪問。
|
? |
確保服務使用者收到包含請求狀態的確認。
|
反模式 2:Loosey Goosey
? |
上下文:
? |
要構建一個基于服務的解決方案。
|
? |
您的組織對服務的重用很重視。
|
|
? |
問題:
|
? |
影響因素:
? |
您計劃在解決方案的所有級別提供集成點。
|
? |
其他組需要訪問您的數據庫。
|
|
? |
解決方案:
? |
將您的服務接口設計為“數據級”(參見“代碼清單 2”)。
|
? |
重視后期綁定來設計高擴展性的接口。
|
|
代碼清單
2. Visual Basic .NET
數據級服務示例
<WebMethod()> _
Public Function QueryDatabase( ByVal Database as String,
SQLQuery as string) As DataSet
<WebMethod()> _
Public Function Execute( ByVal Command as Integer,
Arguments as string) As Boolean
故障現象與后果:
? |
實際上沒有合約存在。服務使用者不知道如何使用服務(例如,什么是有效的“Command”參數和編碼期望等等)。
|
? |
接口接受消息時太過自由。合約既不清晰,又存在嚴重的安全風險,容易受到 SQL 插入代碼攻擊。
|
? |
合約沒有提供足夠的信息讓使用者知道如何使用服務。如果使用者必須讀取服務簽名之外的一些信息才能了解如何使用服務,那么應檢查服務的分解。
|
? |
使用者需要在使用 Web 服務之前熟悉數據庫和表格結構。這將導致服務提供者和使用者之間發生緊耦合。
|
? |
由于依賴后期綁定和在同一服務內邊界之間的編碼/解碼,服務的運行性能大大降低。
|
按以下列方式更改服務后,可以避免上列風險和問題:
? |
將服務合約改為使用定義明確的 XML 架構進行通信。架構不應提供有關底層數據倉庫的任何信息。架構的語義應該為服務使用者提供上下文,使它們能夠了解服務的目的。(這一方法還可以提高服務的性能。)
|
? |
數據庫交互應封裝在私有方法之內,使使用者與數據庫及其關聯表格的詳細信息隔離。
|
? |
確保服務使用者收到包含請求狀態的確認。
|
模式 1:文檔處理器
此模式的示例代碼可以下載。
? |
上下文:
? |
要構建一個基于服務的解決方案。
|
? |
應符合 SO 設計原則。
|
|
? |
問題:
? |
如何創建簡單易用、定義明確而又符合 SO 設計原則的合約?
|
|
? |
影響因素:
? |
服務的合約應鼓勵以文檔為中心的設計思路。
|
? |
合約應清晰地定義服務語義(避免 Loosey Goosey 反模式)。
|
? |
合約應將實現封裝在服務之內或之后來促進松散耦合(避免“CRUDy 接口”反模式)。服務合約是不得泄露有關內部實現的詳細信息的邊界(回憶第一條設計原則)。
|
? |
服務必須符合 WS-I Basic Profile(英文),以允許支持 Web 服務的任何平臺或編程語言使用。
|
? |
服務應作為一個完整的工作單元來表示業務流程(避免類似“CRUDy 接口”反模式中的有狀態假設)。如前所述,服務提供者不能信任服務使用者總是“為所應為”。服務不應依靠調用另一服務的用戶在發生異常時“回滾”或“修復”狀況。
|
|
? |
解決方案:
? |
定義或重用 XML 架構以表示服務的請求和響應消息。確保與服務之間的所有公共交互都使用這些架構。(有關示例代碼段,請參閱“代碼清單 3”。)
|
? |
直接從架構生成對象,以加速開發。有時候,這稱為“合約至上”方法。利用“合約至上”的方法,應在開發實際服務之前定義服務合約。然后可以使用服務合約生成服務代碼。“合約至上”對于減少互操作性阻礙是有效的方法,因為它建立在數十年經驗的基礎之上(CORBA、COM 和 DCE 都使用接口語言)。Web 服務有時采取“合約末位”方法,因為簡單的解決方案通常使用 SOAP 而不是 WSDL。無論如何,許多開發環境提供對“合約至上”的簡單支持,而 WSCF(英文)和 XSD Object Code Generator(XSD 對象代碼生成器)(英文)之類的工具可用于幫助進一步自動化此過程。
|
? |
如前所述,服務提供者不能期望使用者以特定方式調用或使用其服務。這意味著對給定事務使用補償過程是可行的,但是服務提供者不應依靠服務使用者觸發補償。預訂模式(見下)為事務提供最自治的保護,消除了對服務使用者的依賴。
|
|
代碼清單
3. C#
文檔處理器服務示例
[WebMethod()]
public FindCustomerByCountryResponse FindCustomersByCountry(
FindCustomerByCountry request)
{
...
}
故障現象與后果:
使用此簡單模式定義的服務將符合 SO 設計原則:
? |
以文檔為中心的 Web 服務更容易映射到業務流程,因為使用者往往將業務流程看成發送和接收文檔(注意,文檔可能代表業務事件,而不是實際的業務文檔)。
|
? |
服務邊界相當于在公共數據表示與私有數據表示之間進行轉換時的清晰界線。
|
? |
服務的實現細節被封裝起來,不讓使用者知悉。
|
? |
采取“合約至上”方法有助于確保服務的高度可互操作性。
|
? |
以文檔為中心的服務易于改進,因為所有交互都通過消息發生,而不是通過硬編碼的 RPC 方法。
|
對于此方法,有一些小問題應該注意:
? |
對于從內部表示到外部表示的數據傳輸,性能可能會成為一個問題。
|
? |
服務使用者必須將其數據表示映射到服務使用的架構。某些使用者可能會發現映射到服務架構是一個得不償失的過程。
|
模式 2:冪等消息
此模式的示例代碼可以下載。
? |
上下文:
? |
應符合 SO 設計原則。
|
? |
要構建一個基于服務的事務性解決方案。
|
? |
如果要多次傳送同一條消息,必須能夠補償服務(例如,消息應當是冪等的)。
|
|
? |
問題:
|
? |
影響因素:
? |
除了定義服務的合約,不能期望從使用者獲得更多信息。
|
? |
要實現需要頻繁、可靠更新的事務性數據庫系統。
|
? |
可靠的消息傳遞平臺可能并不能完全解決問題,因為使用者可能仍會發送同一請求的多個副本。
|
|
? |
解決方案:
? |
服務合約(架構)應要求使用者的消息帶有“工作單元”標識符(此后稱為 UOW ID)。參見“圖 1”示例。
|
? |
合約不能要求隨著時間的變化 UOW ID 始終唯一。
|
? |
無論具有同一 UOW ID 值的消息數量有多少,UOW ID 始終代表僅執行一次的工作單元。
|
? |
服務將使用 UOW ID 確定工作單元在啟動之前是否已經執行過。處理與已經完成的工作關聯的 UOW ID 時,有三個可能的選項:
? |
返回響應的緩存副本。
|
? |
重新處理消息,就好像從未收到第一個請求一樣。
|
? |
引發異常,返回錯誤消息。
|
|
|
圖
1.
冪等消息模式
? |
故障現象與后果:
冪等消息傳送是一個棘手的問題。處理重復 UOW ID 有三個選項,對其中的每個選項都有若干要考慮的地方:
1.
|
返回緩存的響應:此選項要求服務將可能的響應緩存維護一段給定時間。超時值/緩存刷新的確定應該由關聯業務流程執行。其他還必須考慮的問題:
? |
如果當前值與緩存值不同怎么辦?
|
? |
如果響應錯誤怎么辦?
|
? |
如果使用者重復使用無關工作單元的 UOW ID 怎么辦?
|
|
2.
|
再次處理消息:雖然這對簡單的讀操作可能無害,但對寫操作(例如,發票付款)則可能有害。如果在第一次收到 UOW ID 時對它的處理導致錯誤將會怎么樣??再次處理似乎將導致同樣的錯誤(這種情況有時稱為“有毒消息循環”)。
|
3.
|
引發異常:這種情況下,服務使用者可能未收到服務的原始響應,而只是再次重新發送同一 UOW ID(可能因為預期響應超時)。如果最后一個發送的消息丟失,使用者將如何接收您的響應?
|
UOW ID 應該是請求架構的組成部分,將重復請求綁定到相關業務流程中。UOW ID 也可能是作為自定義 SOAP 標頭添加的,從而使重復處理成為總體消息處理基礎結構的組成部分。使用者的 URI 也應包括在內,以幫助檢測重入。可以自動維護修改的審核追蹤,使修改請求跟蹤能夠返回給定的 UOW ID。最后,與緩存“刷新”關聯的問題可以通過反映收到請求時的響應來解決。緩存管理是超出本文范圍的又一個難題。(建議有興趣詳細了解緩存問題的讀者查看 MSDN 文章并發處理:設計服務與其代理之間的交互(英文)。
由于服務不再相信使用者能“為所應為”,故支持冪等消息傳送提高了服務自治性(第一條設計原則)。對于此方法,有一些小問題應該注意:
|
? |
使用此模式的服務將有可能使用大容量持久存儲,以滿足響應的緩存需求。
|
? |
由于管理緩存,可能會對服務性能造成嚴重影響。
|
模式 3:預訂
此模式的示例代碼可以下載。
? |
上下文:
? |
應符合 SO 設計原則。
|
? |
您有一個打算通過 Web 服務呈現給使用者的復雜的業務流程。該業務流程運行時間長,一個事務就要持續幾個小時。
|
|
? |
問題:
|
? |
影響因素:
? |
無法共享分布式事務。
|
? |
上述業務流程需要若干消息才能完成。
|
? |
消息交換所需時間從幾秒鐘到幾個小時不等。
|
|
? |
解決方案:
? |
消息引起試探性操作:這些試探性操作為原子事務,使數據庫保持一致狀態。
|
? |
可能的結果有三種,它們與這些試探性操作中的每個操作都相關:
? |
試探性操作(通過完成消息交換)被確認。
|
? |
試探性操作被取消,或者是因為產生了故障(隱式),或者是被一個參與者取消(顯式)。
|
? |
試探性操作(消息交換)未在期望的服務級別內完成(超時)。
|
|
? |
要跟蹤每個試探性操作的狀態,必須定義一個對話框。如果為每個對話框指定一個唯一標識符(“預訂 ID”)和過期時間戳,可使服務“記住”每個對話框停止的位置。圖 2 顯示此模式的說明。
|
? |
如果服務使用者試圖確認先前注冊的預訂,則該使用者必須提供有效的“預訂 ID”。缺少“預訂 ID”或者“預訂 ID”無效的確認請求將被服務拒絕。
|
? |
過期時間戳使服務可以在超過給定時間段之后,使未確認的預訂“過期”。
|
|
圖
2.
預訂模式
? |
故障現象與后果:
? |
指定唯一的“預訂 ID”和過期時間戳可確保數據一致性問題由服務及其關聯業務規則處理。如前所述,服務不能信任使用者“為所應為”。
|
? |
“預訂 ID”用于跟蹤給定對話框的狀態。
|
? |
過期時間戳用于以可預測方式處理超時或者丟失的消息。
|
|
“預訂 ID”和過期時間戳應該是“確認請求”架構的一部分,將預訂處理綁定到實際業務過程中。服務為每個預訂請求生成“預訂 ID”和過期時間戳,使服務能夠定期檢查未確認的預訂并使其“過期”。此模式可以與“冪等消息”模式組合,進而將服務與重復的預訂請求隔離。
對于此方法,有一些小問題應該注意:
? |
處理預訂請求的業務規則必須定義清晰。將如何處理“預訂超量”?
|
? |
我們有點被原子兩段提交模型擾亂了,常常試圖將其應用于不很適合的場合(例如,運行時間長的過程)。運行時間長的過程必須保持組成它們的原子過程的一致性。運行時間長的過程中的工作隔離不是件簡單的事情,像“預訂”這樣的模式是解決此問題的簡單嘗試。
|
結論
四條“面向服務”原則提供了一組分立的基本原則,可用于指導服務開發工作。本文提供了幾個模式和反模式,旨在說明這些原則如何影響服務設計。我們還提供了一些其他準則,用于幫助確保您將來的服務設計和開發工作獲得成功:
? |
分解服務時,根據現有文檔和已知業務事件為業務過程建模。
|
? |
雖然服務接口靈活很重要,但要避免太過靈活,以防底層服務合約變得不清晰。
|
? |
不要期望服務使用者“為所應為”。如果服務要求使用者以預先定義好的方式執行一系列步驟,請借助于模式(例如,預訂)幫助強制執行這些步驟。
|
? |
永遠不要使服務或其關聯資源處于不一致的狀態。
|
還有許多其他設計問題與 Web 服務關聯。本系列的后續文章將討論諸如版本控制、服務分解和策略驅動的服務配置等問題。
轉到原英文頁面
原文地址:http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/SOADesign.mspx?mfr=true