共用方式為


樂觀並發控制

在多用戶環境中,有兩種模式可更新資料庫中的數據:樂觀並行控制和悲觀並行控制。 對象 DataSet 的設計目的是鼓勵長時間執行的活動使用開放式並行存取,例如遠端數據與與數據互動。

悲觀並行牽涉到鎖定數據源的數據列,以防止其他使用者以影響目前使用者的方式修改數據。 在悲觀模型中,當使用者執行導致套用鎖定的動作時,其他使用者將無法執行與鎖定衝突的動作,直到鎖定擁有者放開鎖定為止。 此模型主要用於數據競爭繁重的環境中,因此,如果發生並行衝突,保護具有鎖定的數據的成本會小於復原交易的成本。

因此,在悲觀並行模型中,更新資料列的使用者會建立鎖。 在使用者完成更新並釋放鎖定之前,沒有人可以變更該數據列。 因此,當鎖定時間較短時,最好實施悲觀併發控制,例如以程式化方式處理記錄。 當使用者與資料互動時,如果導致記錄長時間被鎖定,悲觀並行存取就不是一個可擴展的選擇。

備註

如果您需要更新相同作業中的多個數據列,則建立交易比使用悲觀鎖定更可調整的選項。

相較之下,使用樂觀並發控制的使用者在讀取數據列時不會鎖定該列。 當使用者想要更新資料列時,應用程式必須判斷另一位使用者自讀取后是否已變更數據列。 樂觀並發控制通常用於數據爭用低的環境中。 開放式並行存取可改善效能,因為不需要鎖定記錄,而且鎖定記錄需要額外的伺服器資源。 此外,為了維護記錄鎖定,需要與資料庫伺服器的持續連線。 因為在樂觀並發模型中情況不同,與伺服器的連線可以在較短的時間內自由地為更多的用戶端提供服務。

在樂觀並行控制模型中,若使用者從資料庫收到值後,其他使用者在第一位使用者嘗試修改該值之前修改了該值,就會視為違規已發生。 伺服器如何解決並行違規,最好先描述下列範例。

下表遵循樂觀並發控制的範例。

下午 1:00,User1 會從資料庫中讀取具有下列值的數據列:

CustID 姓氏 名字

101 史密斯鮑勃

欄位名稱 原始值 目前的值 資料庫中的值
客戶編號 101 101 101
姓氏 史密斯 史密斯 史密斯
名字 鮑勃 鮑勃 鮑勃

下午 1:01,User2 會讀取相同的數據列。

下午1:03,User2 從「Bob」改 FirstName 為「Robert」並更新資料庫。

欄位名稱 原始值 目前的值 資料庫中的值
客戶編號 101 101 101
姓氏 史密斯 史密斯 史密斯
名字 鮑勃 羅伯特 鮑勃

更新成功,因為更新時資料庫中的值符合User2擁有的原始值。

下午 1:05,User1 將 “Bob” 的名字變更為 “James”,並嘗試更新數據列。

欄位名稱 原始值 目前的值 資料庫中的值
客戶編號 101 101 101
姓氏 史密斯 史密斯 史密斯
名字 鮑勃 詹姆斯 羅伯特

此時,User1 遇到樂觀並發違規,因為資料庫中的值("Robert")不再與 User1 預期的原始值("Bob")相符。 並行違規只會讓您知道更新失敗。 現在必須決定是否要以 User1 提供的變更覆寫 User2 所提供的變更,或取消 User1 所做的變更。

測試樂觀並發控制違反

有數種技術可用來測試樂觀並發違規。 其中一個涉及在數據表中包含時間戳數據行。 資料庫通常會提供時間戳功能,可用來識別上次更新記錄的日期和時間。 使用這項技術,時間戳數據行會包含在數據表定義中。 每當記錄更新時,時間戳就會更新,以反映目前的日期和時間。 在樂觀並行控制測試中,任何查詢數據表內容時,會返回時間戳欄位。 嘗試更新時,資料庫中的時間戳值會與修改數據列中所包含的原始時間戳值進行比較。 如果相符,則進行更新,並將時間戳欄位更新為當前時間,以反映此次更新。 如果它們不相符,就會發生樂觀並行控制違反。

另一種測試樂觀並發控制違規的技術是驗證資料列中所有原始欄位值是否仍然符合資料庫中找到的值。 例如,請考慮下列查詢:

SELECT Col1, Col2, Col3 FROM Table1

若要在 Table1 中更新資料列時測試樂觀並行違規,您會發出下列 UPDATE 語句:

UPDATE Table1 Set Col1 = @NewCol1Value,
              Set Col2 = @NewCol2Value,
              Set Col3 = @NewCol3Value
WHERE Col1 = @OldCol1Value AND
      Col2 = @OldCol2Value AND
      Col3 = @OldCol3Value

只要原始值符合資料庫中的值,就會執行更新。 當某個值已被修改時,更新操作不會修改資料列,因為 WHERE 子句找不到匹配的項目。

請注意,建議您一律在查詢中傳回唯一的主鍵值。 否則,上述 UPDATE 語句可能會更新一個以上的數據列,這可能不是您的意圖。

如果您數據源中的某欄允許 Null,您可能需要擴充您的 WHERE 子句,以檢查本機數據表和數據源中是否有相符的 Null 參照。 例如,下列 UPDATE 語句會驗證本機數據列中的 Null 參考仍然符合數據源的 Null 參考,或本機數據列中的值仍符合數據源的值。

UPDATE Table1 Set Col1 = @NewVal1
  WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1

使用樂觀並發模型時,您也可以選擇套用較不嚴苛的標準。 例如,只使用 WHERE 子句中的主鍵數據行會導致覆寫數據,而不論上次查詢之後是否有其他數據行已更新。 您也可以只將 WHERE 子句套用至特定數據行,除非上次查詢特定欄位之後已更新特定欄位,否則會導致數據遭到覆寫。

DataAdapter.RowUpdated 事件

RowUpdated 物件的 DataAdapter 事件可與前述技術結合使用,以通知您的應用程式發生樂觀鎖衝突。 RowUpdated每次嘗試從DataSet中更新Modified列後都會發生。 這可讓您新增特殊處理程序代碼,包括例外狀況發生時處理、新增自定義錯誤資訊、新增重試邏輯等等。 物件 RowUpdatedEventArgs 會回傳 RecordsAffected 一個屬性,包含特定更新指令對資料表中修改過的列所影響的列數。 透過將更新指令設定為測試樂觀並行,當發生樂觀並行違規時,屬性 RecordsAffected 會回傳 0,因為沒有更新任何紀錄。 如果是這種情況,則會擲回例外狀況。 該 RowUpdated 事件讓你能處理此事件,並透過設定適當的 RowUpdatedEventArgs.Status 值(例如 UpdateStatus.SkipCurrentRow)來避免例外。 欲了解更多事件 RowUpdated 資訊,請參閱 處理資料適配器事件

你可以選擇在呼叫 Update 前將 DataAdapter.ContinueUpdateOnError 設為 true,然後在Update完成後,回應儲存在特定資料列屬性RowError中的錯誤資訊。 如需詳細資訊,請參閱 數據列錯誤資訊

樂觀並行運算範例

以下是一個簡單的範例,將 UpdateCommand 設置在 DataAdapter 上來測試樂觀並行性,然後利用 RowUpdated 事件來測試樂觀並行性違反。 當遇到樂觀併發違規時,應用程式將設定更新所在的資料列的 RowError,以反映這一違規。

請注意,傳遞給 UPDATE 指令 WHERE 子句的參數值會映射到 Original 各自欄位的值。

' Assumes connection is a valid SqlConnection.
Dim adapter As SqlDataAdapter = New SqlDataAdapter( _
  "SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", _
  connection)

' The Update command checks for optimistic concurrency violations
' in the WHERE clause.
adapter.UpdateCommand = New SqlCommand("UPDATE Customers " &
  "(CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " & _
  "WHERE CustomerID = @oldCustomerID AND CompanyName = " &
  "@oldCompanyName", connection)
adapter.UpdateCommand.Parameters.Add( _
  "@CustomerID", SqlDbType.NChar, 5, "CustomerID")
adapter.UpdateCommand.Parameters.Add( _
  "@CompanyName", SqlDbType.NVarChar, 30, "CompanyName")

' Pass the original values to the WHERE clause parameters.
Dim parameter As SqlParameter = adapter.UpdateCommand.Parameters.Add( _
  "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID")
parameter.SourceVersion = DataRowVersion.Original
parameter = adapter.UpdateCommand.Parameters.Add( _
  "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName")
parameter.SourceVersion = DataRowVersion.Original

' Add the RowUpdated event handler.
AddHandler adapter.RowUpdated, New SqlRowUpdatedEventHandler( _
  AddressOf OnRowUpdated)

Dim dataSet As DataSet = New DataSet()
adapter.Fill(dataSet, "Customers")

' Modify the DataSet contents.
adapter.Update(dataSet, "Customers")

Dim dataRow As DataRow

For Each dataRow In dataSet.Tables("Customers").Rows
    If dataRow.HasErrors Then
       Console.WriteLine(dataRow (0) & vbCrLf & dataRow.RowError)
    End If
Next

Private Shared Sub OnRowUpdated( _
  sender As object, args As SqlRowUpdatedEventArgs)
   If args.RecordsAffected = 0
      args.Row.RowError = "Optimistic Concurrency Violation!"
      args.Status = UpdateStatus.SkipCurrentRow
   End If
End Sub
// Assumes connection is a valid SqlConnection.
SqlDataAdapter adapter = new SqlDataAdapter(
  "SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID",
  connection);

// The Update command checks for optimistic concurrency violations
// in the WHERE clause.
adapter.UpdateCommand = new SqlCommand("UPDATE Customers Set CustomerID = @CustomerID, CompanyName = @CompanyName " +
   "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", connection);
adapter.UpdateCommand.Parameters.Add(
  "@CustomerID", SqlDbType.NChar, 5, "CustomerID");
adapter.UpdateCommand.Parameters.Add(
  "@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");

// Pass the original values to the WHERE clause parameters.
SqlParameter parameter = adapter.UpdateCommand.Parameters.Add(
  "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");
parameter.SourceVersion = DataRowVersion.Original;
parameter = adapter.UpdateCommand.Parameters.Add(
  "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");
parameter.SourceVersion = DataRowVersion.Original;

// Add the RowUpdated event handler.
adapter.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);

DataSet dataSet = new DataSet();
adapter.Fill(dataSet, "Customers");

// Modify the DataSet contents.

adapter.Update(dataSet, "Customers");

foreach (DataRow dataRow in dataSet.Tables["Customers"].Rows)
{
    if (dataRow.HasErrors)
       Console.WriteLine(dataRow [0] + "\n" + dataRow.RowError);
}

protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)
{
  if (args.RecordsAffected == 0)
  {
    args.Row.RowError = "Optimistic Concurrency Violation Encountered";
    args.Status = UpdateStatus.SkipCurrentRow;
  }
}

另請參閱