在本地調(diào)用時,清單3所示的服務接口可能能夠正常工作。不過,如果服務是在遠程位置向使用者提供的,則該服務在常見使用場景中的性能可能會很差。例如,在使用服務檢索數(shù)據(jù)來填充顯示書的目錄項的屏幕時,將有必要進行多個獨立的遠程調(diào)用,以檢索書名、作者和出版日期。進行這些調(diào)用可能會有很大的性能損失。遠程服務應提供粗粒度的操作,以在單個調(diào)用中檢索關于某本書的所有信息。
可遠程調(diào)用的服務的這個設計原則得到了廣泛的認可;我們在此處強調(diào)此原則的目的在于說明被封裝的服務調(diào)用細節(jié)可能給我們?nèi)绾芜x擇設計方法帶來很大的影響。我們認為,同步調(diào)用和異步調(diào)用之間的選擇也可能對服務接口設計有類似的影響。
這就引入了一個重要的問題:設計服務時,什么決定了所使用的調(diào)用方式?服務設計人員是否可以自由選擇本地調(diào)用和遠程調(diào)用、同步調(diào)用和異步調(diào)用?我們建議 SOA 應對這方面進行說明。我們提出此建議有兩個原因。首先,我們希望通過確保一致性提高易用性;編排流程時,服務最好具有可預測的特征。其次,我們希望通過將使用者與提供者分離來提高靈活性。通過鼓勵進行遠程調(diào)用,我們可以進行位置、平臺和編程語言分離。通過鼓勵進行異步調(diào)用,我們可以分離使用者和提供者的可用性特征。
如果SOA要具有描述性,是否應聲明所有服務都應設計為允許遠程、異步調(diào)用?我們建議對此描述性采用更為細粒度的方法?赡艿姆⻊疹愋桶ㄌ峁I(yè)務相關較多的操作,如PlaceOrder,也包括技術(shù)性側(cè)重較多的操作,如CheckUserInRole。SOA完全應該對不同的服務類別進行不同的描述。我們預期將更多地調(diào)用與業(yè)務相關的操作,而技術(shù)操作完全可能采用本地調(diào)用的方式。
服務具有無狀態(tài)接口
我們在服務應設計為可重用中提到,應該將服務設計為可伸縮且可部署到高可用性基礎結(jié)構(gòu)中。此總體原則的一個推論就是,服務不應為有狀態(tài)型的。即,它們不應依賴于使用者和提供者間長期存在的關系,操作調(diào)用也不應隱式地依賴于前一個調(diào)用。為了說明這一點,我們以下面的電話轉(zhuǎn)換為例:
清單4. 有狀態(tài)轉(zhuǎn)換
Q:What is Dave’s account balance?
A: It’s £320
Q:What’s his credit limit?
A:It’s £2,000
此示例演示了轉(zhuǎn)換的兩個有狀態(tài)方面。第二個問題通過使用“his”引用第一個問題。這個示例中的操作依賴于轉(zhuǎn)換上下文,F(xiàn)在讓我們考慮一下所提供的應答。請注意,響應中沒有上下文信息。應當只有在詢問者知道所詢問的問題時,這個應答才有意義。在此示例中,要求使用者維護對話狀態(tài),以便解釋所得到的應答。這兩個有狀態(tài)關系(連續(xù)的調(diào)用之間和請求與響應之間的關系)都與SOA服務設計有關。
首先,我們考慮一下依賴于前一操作建立的上下文的操作。假如這是一個與呼叫中心的交互。只要與同一個操作人員對話,對話就可以有效地結(jié)束。但我們假設呼叫被中斷了,如下所示:
清單5. 被中斷的有狀態(tài)轉(zhuǎn)換
Q:What is Dave’s account balance?
Operator 1: It’s £320
An interruption occurs, and the caller talks to a different operator.
Q:What’s his credit limit?
Operator 2: Who?
中斷導致上下文丟失,因此第二個問題是沒有意義的。就這個電話對話而言,我們可以通過重新建立上下文而抵消中斷帶來的后果:“我在問Dave的銀行帳戶的信息,您能告訴我他的信用額度嗎?”不過,在可擴展服務調(diào)用領域,有狀態(tài)對話通常更為麻煩,在此領域中,重新建立上下文可能在技術(shù)上不可行,或者可能帶來很大的性能開銷。
通常,構(gòu)建可伸縮的可靠基礎結(jié)構(gòu)與允許有狀態(tài)交互之間有緊密的關系。創(chuàng)建支持有狀態(tài)的服務調(diào)用的SOA基礎結(jié)構(gòu)在技術(shù)上可行。可以使用的技術(shù)包括:
·使用Http Cookie維護會話上下文
·使用有狀態(tài)會話EJB;Bean的句柄在SOAP Header中傳遞
不過,我們必須仔細考慮最終基礎結(jié)構(gòu)的可伸縮性和可靠性。是否要求使用關聯(lián)性?即,相同的使用者發(fā)出的連續(xù)請求是否必須交付到相同的提供者實例?要求使用關聯(lián)性是一種有狀態(tài)性與可伸縮性及可靠性沖突的情況。如果基礎結(jié)構(gòu)可以隨意將請求提交到多個提供者實例中的一個,就可簡化負載平衡,而各個提供者實例的可靠性要求就可緩和。
如果沒有關聯(lián)性需求,且允許基礎結(jié)構(gòu)將一個使用者的連續(xù)請求交付到不同的提供者實例,則任何會話狀態(tài)必須對所有提供者實例可用。應用服務器基礎結(jié)構(gòu)提供會話復制機制。此類機制可以用于提供會話狀態(tài),但使用它們會有性能損失。而且,我們的Web開發(fā)經(jīng)驗表明,如果沒有可靠的指南,開發(fā)人員將可以隨意使用會話狀態(tài);過度使用HTTP會話通常是導致性能低下的常見原因。請參閱“Performance Analysis for Java Web Sites”(作者:joines、Willenborg和Hygh,第59頁—60頁,Addison-Wesley ISBN 0201844540)。
我們強烈建議,服務應設計為可避免維護會話上下文的需求。
現(xiàn)在,讓我們考慮一下對話的其他有狀態(tài)方面以及請求和響應間的關系。我們是否要采用上面的電話對話方式進行服務設計,依賴會話上下文來解釋響應“What is Dave’s credit limit?”——“£320”——然后我們將對SOA基礎結(jié)構(gòu)再次進行約束。
基礎結(jié)構(gòu)必須適應各種可能性,如某些使用者無法在臨時停機的情況下保留其會話狀態(tài)。
我們可以通過將服務設計為在響應中包含合適的關聯(lián)信息,從而避免會話狀態(tài)的需求,例如以下的響應:
清單6. 包含關聯(lián)信息的對話
Q: What is Dave’s credit limit?
A: Dave’s credit limit is £2000
該響應既標識人員又提供具體的數(shù)據(jù)。當包裝遺留系統(tǒng)時,通常由適配器負責提供此類關聯(lián)信息,F(xiàn)有同步API完全有可能不提供關聯(lián)數(shù)據(jù)。在響應中包含關聯(lián)信息之所以是很好的做法,有很多原因。首先,它簡化了彈性可伸縮解決方案的構(gòu)造,還能提供很大的診斷幫助,且在不可能向原始請求程序交付錯誤響應時非常重要。未交付的消息可能放置在錯誤隊列上,每個此類消息的解釋都要求使用上下文信息。
總之,仔細地進行服務設計可以避免對狀態(tài)對話的需求,從而簡化可靠的可伸縮 SOA 基礎結(jié)構(gòu)的實現(xiàn)。
服務應使用狀態(tài)事務建模
前面給出了一個總的建議,以避免依賴對話狀態(tài),但我們應當記住,有用的計算機系統(tǒng)通常將為有狀態(tài)的;通常反映了業(yè)務對象的生命周期。
例如,考慮購物中的一個訂單的生命周期:創(chuàng)建訂單。從用戶的角度來看,創(chuàng)建了一個空的購物車。用戶將隨后向訂單添加物品,即將其放入購物車中。最后提交訂單,然后訂單將被傳送給配送部門。圖1顯示了對此生命周期建模的簡化狀態(tài)轉(zhuǎn)換圖。
圖1. 訂單生命周期狀態(tài)轉(zhuǎn)換
該模型說明了一些有狀態(tài)的行為。例如,我們看到,在處于Open狀態(tài)時可以向訂單添加行式項目,但在提交后就不能了。
讓我們考慮以下 Order 服務的設計。我們可以采用如清單7所示的接口。
清單7. Order服務設計
OrderService {
void addLineItemToOrder(int orderId, int productId, int quantity)
void assignOrderToPacker(int packerId)
int createOrder(int customerId) // returns id of new order
int packItemForOrder(int orderId, int quantity) // returns quantity left to ship
boolean shipOrder(int orderId) // returns whether all order is now shipped
void submitOrder(int orderId)
// ... query operations elided ...
我們要考慮此接口的易用性。(更現(xiàn)實一些,我們應該考慮那些具有更多方法的完整接口,如用于列出和刪除行式項目的方法。)如果沒有狀態(tài)圖供參考,即使查看我們的這個小示例,也非常難于分清方法應該按照何種順序進行調(diào)用。因此我們認為,服務設計人員必須進行一定的工作,以簡化使用者的任務。我們提供了一些可能的技術(shù)。
首先,考慮操作和參數(shù)的名稱。我們上面的示例中的名稱是經(jīng)過細心選擇的,并進行一定的努力,以推導出方法的可能調(diào)用順序。請比較清單8和清單9中的示例,這兩個代碼片段幾乎完全一樣,只是操作和參數(shù)的名稱不同。
清單8. 不合理選擇的操作和參數(shù)名稱
ZettuylService {
int wibble(int wibId, int wobId, String which);
int wobble(int quibId);
boolean wrubble(int wibId);
void quibble(int widId)
void quash(int wibId)
Stuff[] getStuff(int wibId );
void quite(int wibId);
Things[] getThings(int wibId);
void hinge(int wibId, intwobId);
int henge(int wibId , Stuff someStuff)
}
清單9. 合理選擇的操作和參數(shù)名稱
ExpenseService {
int approveClaimItem(int claimid, int itemid, String comment);
int createClaim(String userId);
boolean auditClaim(int claimid);
void approveClaim(claimid)
void returnClaim(claimid)
ClaimItemDetails[] getClaimItems(int claimid );
void payClaim(int claimid);
ClaimErrors[] validateClaim(int claimid);
void removeClaimItem(int claimid, int itemid);
int addClaimItem(int claimid, ClaimItemDetails details)
}
清單8中的名稱都是難以理解的。而清單9中選擇的名稱說明了服務的目的,并可以減少很多操作應按特定順序調(diào)用的情況。例如,createClaim()將在approveClaim()前使用,而后者又將在payClaim()前使用。因此,正如我們在前面的命名服務時應以最大化易用性為目標所指出的,名稱的選擇對易用性影響非常大。
其次,前面的Order的狀態(tài)轉(zhuǎn)換圖可清楚說明訂單的有狀態(tài)行為。該圖提供了有用的說明信息,顯示了訂單的狀態(tài)以及每個狀態(tài)中相應的操作。
增加易用性的第二項技術(shù)是,要記住并非所有記錄的值都可以通過服務接口定義實現(xiàn)最佳的交付。記錄良好的WSDL文件很有價值,但一起提供的關系圖和示例也有很大的價值。
增加易用性的另一項技術(shù)是,創(chuàng)建反映業(yè)務對象生命周期的狀態(tài)的服務接口。在我們費用申領示例中,每筆費用申領的生命周期都包含四個狀態(tài),如圖2中所示。
圖2. expense對象的四個狀態(tài)
這些狀態(tài)之所以重要,有兩個原因。第一,每個狀態(tài)基本上都與不同的系統(tǒng)用戶相關。例如,當費用申領處于構(gòu)建(building)狀態(tài)時,主要系統(tǒng)用戶是輸入費用申領詳細信息的申領人,而在審核(auditing)狀態(tài)中,則是由具有批準權(quán)利的人對費用申領進行檢查。
第二,主要狀態(tài)之間的轉(zhuǎn)換通常反映了不同IT系統(tǒng)之間的數(shù)據(jù)流。例如,在構(gòu)建(building)狀態(tài)期間,可以在用戶的工作站上使用一個瘦客戶端應用程序來捕獲費用申領。提交后,費用申領將傳遞給費用申領處理系統(tǒng),而當?shù)玫脚鷾屎,費用申領將傳遞給另外一個系統(tǒng),即支付系統(tǒng)。在傳遞過程中,我們需要提到的是,如果實現(xiàn)的確要將數(shù)據(jù)從一個系統(tǒng)傳遞到另一個系統(tǒng),則需要尤為注意負責在系統(tǒng)之間進行傳輸?shù)牟僮鳎ㄔ谖覀兊氖纠袨閟ubmitClaim()和approveClaim())。它們的實現(xiàn)將需要對兩個系統(tǒng)進行更新,而這樣很容易喪失兩個系統(tǒng)中任意一個的可用性。這些方法的實現(xiàn)將可以通過使用異步排隊機制得到改善。
由于業(yè)務對象狀態(tài)常常能同時反映業(yè)務和技術(shù)兩方面的內(nèi)容,因此完全可以將原始ExpenseClaimService拆分為適應每個狀態(tài)的多個服務。我們可以得到如清單10中所示的服務。
清單10. 根據(jù)狀態(tài)劃分服務
ClaimEntryService {
createClaim(String userId);
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimid);
void removeClaimItem(int claimid, int itemid);
int addClaimItem(int claimid, ClaimItemDetails details)
int submitClaim(int claimid);
}
ClaimApprovalService {
int approveClaimItem(int claimid, int itemid, String comment);
void approveClaim(claimid)
void returnClaim(claimid)
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimid);
}
ClaimPaymentService {
void payClaim(int claimid);
}
通過這種方式,能更方便地理解每個服務。而且,將接口這樣劃分將可能非常適合服務(或服務集)的開發(fā)、部署、維護和使用方式。這些服務很可能針對不同的使用者,可以由獨立的開發(fā)團隊進行開發(fā),可以分開部署,因而具有分離的版本周期。換句話說,通過將重點放在對象生命周期上,我們就可以建立具有恰當粒度的服務。
服務操作設計原則
前面我們討論了服務的總體設計方面的問題,接下來就要討論一下各個服務操作的設計了。
操作表示業(yè)務動作。
我們已經(jīng)指出,總的原則是,我們應該優(yōu)先對服務和操作使用業(yè)務領域的名稱,使用動詞作為操作名稱。對于操作,我們將這個建議進一步深化:應當使用具體的業(yè)務含義而不是泛型操作對操作進行定義。例如,不要使用泛泛的updateCustomerDetails操作,而要創(chuàng)建ChangeCustomerAddress、RecordCustomerMarriage和AddalternativeCustomerContactNumber之類的操作。此方法具有以下好處:
·操作與具體業(yè)務場景對應。此類場景可能不僅是簡單的更新數(shù)據(jù)庫中的記錄。例如,更改地址或婚姻狀況可以要求生成正式的文檔,而將要求系統(tǒng)記錄該文檔的詳細信息——或掃描版本。如果使用不太具體的操作(如updateCustomerDetails),則較難實現(xiàn)此類業(yè)務場景。
·各個操作接口將非常簡單,且易于理解,從而提高了易用性。
·每個操作的更新單元得到了清楚的定義(在我們的示例中為地址、婚姻狀況和電話號碼)。在實現(xiàn)具有高并發(fā)性要求的系統(tǒng)時,我們可以基于操作的要求采用更細粒度的鎖定策略,從而減少資源爭用。
操作應采用粗粒度參數(shù)
在討論操作參數(shù)時,同樣要面對粒度的問題。請比較清單11和清單12中所示的createNewCustomer操作的兩個接口。
清單11. 采用細粒度參數(shù)的createNewCustomer操作接口
int createNewCustomer(String familyName,
String givenName,
String initials,
int age
String address1
String address2
String postcode
// ...
)
清單12. 采用單個粗粒度參數(shù)的createNewCustomer操作接口
int createNewCustomer( CustomerDetails newDetails)
清單11顯示了一個具有很多細粒度參數(shù)的操作。而在清單12中的操作則采用結(jié)構(gòu)化類型作為單個粗粒度參數(shù)。我們之所以建議使用粗粒度參數(shù),有兩個原因。首先,它們提供了創(chuàng)建靈活操作的機會,支持在不干擾現(xiàn)有使用者的情況下提供新版本的操作。其次,具有大量類型相似的參數(shù)的操作易于在從第三代語言代碼進行調(diào)用時出現(xiàn)轉(zhuǎn)換錯誤。相反,當數(shù)據(jù)放置在所使用的結(jié)構(gòu)化類型的顯式方法(如setGivenName()和setInitials())中時,此方法出錯的幾率更小。
操作設計應考慮并發(fā)性
傳統(tǒng)的事務型編程模型(如Entity EntERPrise Java Bean(Entity Bean)所支持的編程模型)允許實現(xiàn)數(shù)據(jù)庫更新,因此其數(shù)據(jù)庫鎖定方式如清單13中所示。
清單13. 事務型編程模型
Begin Transaction
Retrieve data from database - locking record
Modify values
update database record with modified values
Commit Transaction - unlocking record
請注意,數(shù)據(jù)庫鎖定從第1行檢索時一直保持到第5行的提交操作。這樣以一定的延遲確保了正確的并發(fā)行為。如果我們希望設計一個提供數(shù)據(jù)庫更新功能的服務,則可以提供與清單8中的第2行和第4行的檢索和寫入操作對應的操作。不過,我們強烈建議,不要在高度分離且可能異步的SOA基礎結(jié)構(gòu)中的連續(xù)調(diào)用間保持鎖定。我們建議采用樂觀鎖定策略,將并發(fā)控制的責任委派給相應的應用程序邏輯。
樂觀鎖定策略中的更新請求可以解釋為“以下是基于記錄XYZ的V版本的一些記錄更新。請僅在從我讀取該記錄后沒有人進行修改的情況下進行更改!
以下是為清單12所示的同一個模型使用了數(shù)據(jù)庫觸發(fā)器和修訂計數(shù)器的樂觀鎖定實現(xiàn)。該實現(xiàn)要求執(zhí)行以下步驟:
·向要使用樂觀鎖定的表中添加一個額外的整數(shù)列;該列保存修訂計數(shù)器。
·向數(shù)據(jù)庫添加觸發(fā)器,以便對該表中的記錄的每個更新都會導致修訂計數(shù)器遞增。
·所有檢索操作均會返回包含修訂計數(shù)器的數(shù)據(jù)項。
·所有更新操作都必須包含從檢索獲得的修訂計數(shù)器。
·更新操作實現(xiàn)必須對數(shù)據(jù)庫記錄進行限定的更新操作,如“如果修訂計數(shù)器等于...則更新記錄...”如果其間對記錄進行了任何修改,此更新操作將失敗——在其間,如果出現(xiàn)了更新,則會觸發(fā)更新觸發(fā)器,因此會修改修訂計數(shù)器。
·如果由于其他使用者在其間進行了更新而導致更新失敗,則將向使用者報告一個特定的錯誤。
請注意,此實現(xiàn)在更新時要求使用者提供正確的修訂計數(shù)器;進行糾正的責任分散到數(shù)據(jù)庫、提供者和使用者三者身上。另外,還請注意,此實現(xiàn)設計真的非常樂觀;如果爭用的概率很低,就能很好地工作。如果可能出現(xiàn)更新沖突,則重試的性能開銷將非常大。另外,還需要一些其他可能的樂觀鎖定策略和詳細設計,以制定合適的并發(fā)方案。
考慮到管理并發(fā)更新的相對復雜性,我們提出一個相關的建議:盡可能使用無狀態(tài)語義。例如,與實現(xiàn)等效的“Retrieve record”-“Write record”兩個操作(使用者會在檢索和寫入操作間使值遞增)相比,可能實現(xiàn)具有良好并發(fā)行為的單個操作“Increment balance by X”更為容易。
結(jié)束語
本文的主要目的是強調(diào)在面向服務的體系結(jié)構(gòu)中服務設計的重要性。我們并不認為文中包含了全部設計原則。而是希望通過這些原則能夠說明,每個SOA都需要慎重地為其企業(yè)確定一組恰當?shù)脑瓌t,并隨后確定每個服務創(chuàng)建人員都能遵循這些原則。
關于作者
David Artus是IBM Software Services for WebSphere團隊的成員,在英國IBM Hursley Lab工作。他從1999年就開始提供WebSphere咨詢和指導服務。在1999年加入IBM之前,David曾從事過多個行業(yè)的工作,包括投資金融、旅游和IT咨詢。他所感興趣的領域包括分布式系統(tǒng)的設計、對象技術(shù)和面向服務的體系結(jié)構(gòu)。
轉(zhuǎn)載請注明出處:拓步ERP資訊網(wǎng)http://www.ezxoed.cn/
本文網(wǎng)址:http://www.ezxoed.cn/html/support/1112153780.html