閱讀547 返回首頁    go 技術社區[雲棲]


[WCF REST] 解決資源並發修改的一個有效的手段:條件更新(Conditional Update)

條件獲取(Conditional Update)可以避免相同數據的重複傳輸,進而提高性能。條件更新(Conditional Update)用於解決資源並發操作問題。如果我們預先獲取一個資源進行修改或者刪除,條件更新檢驗幫助我們確認資源被獲取出來到針對它的修改/刪除操作被提交的這段時間內是否被其他人改動過。[源代碼從這裏下載]

HTTP為條件更新提供了相應的報頭,我們按照分析條件獲取的方式來分析條件更新在HTTP請求/回複過程中的實現。客戶端第一次向服務端發起針對某個資源的請求,服務端除了將資源數據作為回複消息主體返回之外,會將與資源關聯並且能夠可以用於對其進行對等性判斷的某個值作為回複的ETag報頭,這與條件獲取時一致的。

客戶端通過回複獲得請求的資源和ETag報頭值。對於資源修改操作,客戶端直接針對獲取的資源進行相應的修改,並將修改後的資源以HTTP請求的方式向服務端提交;對於資源刪除操作,則可以指定被刪除資源的唯一標識直接向服務端發送刪除的請求。而之前獲取的ETag指將會作為請求消息的If-Match報頭。

服務端接收到資源修改/刪除請求後先獲取到現有的資源的ETag值,並將此值與請求消息的If-Match報頭值進行比較。如果兩者不一致,則表明試圖被修改/刪除的資源已經被修改了,在這種情況下會直接回複一個HTTP狀態為“412 (Precondition Failed)”的空消息。條件更新同時支持針對PUT、POST和DELETE這三種方法的HTTP請求。

服務端進行條件更新檢測,以及客戶端對If-Match請求報頭的設置都可以通過當前的WebOperationContext來完成。如下麵的代碼片斷所示,表示入棧請求上下文的IncomingWebRequestContext類型具有如下四個CheckConditionalUpdate方法重載用於進行添加更新檢測。

   1: public class IncomingWebRequestContext
   2: {
   3:     //其他成員
   4:     public void CheckConditionalUpdate(Guid entityTag);
   5:     public void CheckConditionalUpdate(int entityTag);
   6:     public void CheckConditionalUpdate(long entityTag);
   7:     public void CheckConditionalUpdate(string entityTag);
   8: }

實現在CheckConditionalUpdate方法中的條件更新檢測具有這樣的邏輯:。

表示出棧請求上下文的OutgoingWebRequestContext類型具有如下一個IfMatch屬性,客戶端可以通過該屬性對請求消息的If-Match報頭進行設置。

   1: public class OutgoingWebRequestContext
   2: {
   3:     //其他成員
   4:     public string IfMatch { get; set; }
   5: }

我們同樣通過對EmployeesService進行相應的改造來模擬如何通過添加更新實現對相同資源的並發操作問題,這次我們修改的是用於獲取指定ID員工信息的Get操作和用於修改員工信息的Update操作。Get操作在返回與指定員工ID匹配的Employee對象之前我們將該對象的哈希碼作為了回複消息的ETag報頭(Employee類型重寫了GetHashCode方法)。

   1: public class EmployeesService : IEmployees
   2: {
   3:     //其他成員
   4:     public Employee Get(string id)
   5:     {
   6:         Employee employee = employees.FirstOrDefault(e => e.Id == id);
   7:         if (null == employee)
   8:         {
   9:             throw new WebFaultException(HttpStatusCode.NotFound);
  10:         }
  11:         WebOperationContext.Current.OutgoingResponse.SetETag(employee.GetHashCode());
  12:         return employee;
  13:     }
  14:     public void Update(Employee employee)
  15:     {
  16:         var existing = employees.FirstOrDefault(e => e.Id == employee.Id);
  17:         if (null == existing)
  18:         {
  19:             throw new WebFaultException(HttpStatusCode.NotFound);
  20:         }
  21:         //模擬並發修改
  22:         existing.Name += Guid.NewGuid().ToString();
  23:  
  24:         WebOperationContext.Current.IncomingRequest.CheckConditionalUpdate(existing.GetHashCode());
  25:         employees.Remove(existing);            
  26:         employees.Add(employee);
  27:         WebOperationContext.Current.OutgoingResponse.SetETag(employee.GetHashCode());
  28:  
  29:     }
  30: }

Update方法中我們通過手工修改相應員工的Name屬性的方式來模擬針對相同員工信息的並發修改。在真正實施修改之前調用當前IncomingWebRequestContext的CheckConditionalUpdate方法進行條件更新檢測,而作為參數傳入的ETag值為代表目前員工的Employee對象的哈希碼。方法的最後我們對回複消息的ETag報頭作了更新。

我們通過手工創建HTTP請求的方式對上述的兩個服務操作進行調用。如下麵的代碼片斷所示,我們首先通過創建的HttpWebRequest對象調用Get操作獲得ID為001的員工信息並將其打印出來。然後創建調用Update操作的HttpWebRequest,並對HTTP方法(POST)和內容類型(application/xml)進行了相應的設置。我們之前針對員工獲取請求得到ETag報頭和員工數據作為本次請求的If-Match報頭和主體。如果調用GetResponse方法拋出WebException異常,並且其回複狀態為PreconditionFailed,則表明試圖修改的員工信息已被另一個用戶修改過了,所以我麼打印“服務端數據已發生變化”字樣。

   1: Uri address = new Uri("https://127.0.0.1:3721/employees/001");
   2: var request = (HttpWebRequest)HttpWebRequest.Create(address);
   3: request.Method = "GET";
   4: var response = (HttpWebResponse)request.GetResponse();
   5: string employee;
   6: using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
   7: {
   8:     employee = reader.ReadToEnd();
   9:     Console.WriteLine("獲取員工信息:");
  10:     Console.WriteLine(employee + "\n");
  11: }
  12: try
  13: {
  14:     address = new Uri("https://127.0.0.1:3721/employees/");
  15:     request = (HttpWebRequest)HttpWebRequest.Create(address);
  16:     request.Method = "POST";
  17:     request.ContentType = "application/xml";
  18:     byte[] buffer = Encoding.UTF8.GetBytes(employee);
  19:     request.GetRequestStream().Write(Encoding.UTF8.GetBytes(employee), 0, buffer.Length);
  20:     request.Headers.Add(HttpRequestHeader.IfMatch, response.Headers[HttpResponseHeader.ETag]);
  21:     Console.WriteLine("修改員工信息:");
  22:     request.GetResponse();
  23: }
  24: catch (WebException ex)
  25: {
  26:     response = ex.Response as HttpWebResponse;
  27:     if (null == response)
  28:     {
  29:         throw;
  30:     }
  31:     if (response.StatusCode == HttpStatusCode.PreconditionFailed)
  32:     {
  33:         Console.WriteLine("服務端數據已發生變化");
  34:     }
  35:     else
  36:     {
  37:         throw;
  38:     }
  39: }

在服務成功寄宿的情況下調用這段程序會在控製台上輸出如下的結果。由於並發錯誤的發生,員工信息其實並沒有被真正修改。

   1: 獲取員工信息:
   2: <Employee xmlns="https://www.artech.com/" xmlns:i="https://www.w3.org/2001/XMLSchema-instance"><Department>開發部</Department><Grade>G7</Grade><Id>001</Id><Name>張三</Name></Employee>
   3:  
   4: 修改員工信息:
   5: 服務端數據已發生變化

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-26 12:04:56

  上一篇:go  新品牌如何開展網絡營銷?
  下一篇:go  WCF REST係列文章匯總(共9篇)