Поделиться через


Оптимистическая конкуренция

В многопользовательской среде существуют две модели для обновления данных в базе данных: оптимистичная конкурентность и пессимистичная конкурентность. Объект DataSet предназначен для использования оптимистичной синхронизации для длительных процессов, таких как передача данных на расстоянии и взаимодействие с данными.

Пессимистическая согласованность включает блокировку строк в базе данных, чтобы предотвратить изменение данных другими пользователями таким образом, который может повлиять на текущего пользователя. В пессимистической модели, когда пользователь выполняет действие, которое приводит к применению блокировки, другие пользователи не могут выполнять действия, конфликтующие с блокировкой, пока владелец блокировки не выпустит его. Эта модель в первую очередь используется в средах, в которых существует интенсивная конкуренция за данные, поэтому затраты на защиту данных с помощью блокировок меньше затрат на откат транзакций, если возникают конфликты конкурентности.

Поэтому в пессимистической модели параллелизма пользователь, который обновляет строку, устанавливает блокировку. Пока пользователь не завершит обновление и не выпустит блокировку, никто не сможет изменить эту строку. По этой причине пессимистичная конкуренция лучше всего реализуется, когда время блокировки будет коротким, например при программной обработке записей. Пессимистичная конкуренция не является масштабируемым вариантом, когда пользователи взаимодействуют с данными и вызывают блокировку записей на относительно большие периоды времени.

Замечание

Если необходимо обновить несколько строк в одной операции, то создание транзакции является более масштабируемым вариантом, чем использование пессимистичной блокировки.

В отличие от этого, пользователи, использующие оптимистическую конкуренцию, не блокируют строку при чтении. Когда пользователь хочет обновить строку, приложение должно определить, изменил ли другой пользователь строку после его чтения. Оптимистическая конкуренция обычно используется в средах с низкой конкурентностью за данные. Оптимистическая конкуренция улучшает производительность, поскольку не требуется блокировка записей, которая требует дополнительных ресурсов сервера. Кроме того, для поддержания блокировок записей требуется постоянное подключение к серверу базы данных. Так как это не так в оптимистической модели параллелизма, подключения к серверу могут обслуживать большее количество клиентов за меньшее время.

В модели оптимистического параллелизма нарушение считается нарушением, если после того, как пользователь получает значение из базы данных, другой пользователь изменяет значение до того, как первый пользователь попытается изменить его. Как сервер разрешает нарушение параллелизма, лучше всего показано, в первую очередь описывая следующий пример.

В следующих таблицах приведен пример оптимистического параллелизма.

В 1:00 вечера User1 считывает строку из базы данных со следующими значениями:

CustID Фамилия Имя

101 Смит Боб

Имя столбца Исходное значение Текущее значение Значение в базе данных
Идентификатор клиента 101 101 101
Фамилия Иванов Иванов Иванов
Имя Боб Боб Боб

В 1:01 пользователь2 считывает ту же строку.

В 1:03 пользователь 2 изменяет имя firstName с "Боб" на "Роберт" и обновляет базу данных.

Имя столбца Исходное значение Текущее значение Значение в базе данных
Идентификатор клиента 101 101 101
Фамилия Иванов Иванов Иванов
Имя Боб Роберт Боб

Обновление завершается успешно, так как значения в базе данных на момент обновления соответствуют начальным значениям, которые у User2.

В 1:05 user1 изменяет имя "Боб" на "Джеймс" и пытается обновить строку.

Имя столбца Исходное значение Текущее значение Значение в базе данных
Идентификатор клиента 101 101 101
Фамилия Иванов Иванов Иванов
Имя Боб Джеймс Роберт

На этом этапе User1 сталкивается с нарушением оптимистического параллелизма, так как значение в базе данных ("Роберт") больше не соответствует исходному значению, которое пользователь1 ожидал ("Боб"). Конфликт при параллельной обработке указывает на неудачное обновление. Теперь необходимо принять решение о том, следует ли перезаписать изменения, предоставленные User2, изменениями, предоставленными User1, или отменить изменения, внесенные пользователем 1.

Тестирование для нарушений оптимистического параллелизма

Существует несколько методов тестирования для нарушения оптимистического параллелизма. Один из них включает в себя столбец метки времени в таблице. Базы данных обычно предоставляют функции метки времени, которые можно использовать для идентификации даты и времени последнего обновления записи. С помощью этого метода столбец метки времени включается в определение таблицы. При обновлении записи метка времени обновляется, чтобы отразить текущую дату и время. В тесте на выявление нарушений оптимистического параллелизма столбец метки времени возвращается при каждом запросе содержимого таблицы. При попытке обновления значение метки времени в базе данных сравнивается с исходным значением метки времени, содержащимся в измененной строке. Если они совпадают, обновление выполняется и столбец метки времени обновляется с текущим временем, чтобы отразить обновление. Если они не соответствуют, произошло нарушение оптимистического параллелизма.

Другой способ тестирования для нарушения оптимистического параллелизма заключается в том, чтобы убедиться, что все исходные значения столбцов в строке по-прежнему соответствуют значениям, найденным в базе данных. Например, рассмотрим следующий запрос:

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 проверяет, соответствует ли в локальной строке пустая ссылка на источник данных или значение в локальной строке по-прежнему соответствует значению в источнике данных.

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

Вы также можете применить менее строгие критерии при использовании модели оптимистического параллелизма. Например, использование только первичных ключевых столбцов в предложении WHERE приводит к перезаписи данных независимо от того, были ли обновлены другие столбцы с момента последнего запроса. Также можно применить предложение WHERE только к определенным столбцам, что приводит к перезаписи данных, если не были обновлены определенные поля с момента последнего запроса.

Событие DataAdapter.RowUpdated

Событие DataAdapter объекта можно использовать вместе с методами, описанными ранее, для предоставления уведомления приложению о нарушениях оптимистического параллелизма. RowUpdated возникает после каждой попытки обновить измененную строку из набора данных. Это позволяет добавлять специальный код обработки, включая обработку при возникновении исключения, добавление пользовательских сведений об ошибках, добавление логики повторных попыток и т. д. Объект RowUpdatedEventArgs возвращает свойство RecordsAffected , содержащее количество строк, затронутых определенной командой обновления для измененной строки в таблице. Установив команду обновления для проверки оптимистического параллелизма, свойство RecordsAffected возвращает значение 0 при возникновении нарушения оптимистического параллелизма, так как записи не были обновлены. Если это так, генерируется исключение. Событие RowUpdated позволяет обработать это событие и избежать исключения, установив значение RowUpdatedEventArgs.Status, например UpdateStatus.SkipCurrentRow. Дополнительные сведения о событии RowUpdated см. в разделе "Обработка событий DataAdapter".

При необходимости можно установить DataAdapter.ContinueUpdateOnError на true, прежде чем вызывать Update, и обрабатывать информацию об ошибке, хранящуюся в свойстве RowError определенной строки после завершения Update. Дополнительные сведения см. в разделе "Сведения об ошибке строки".

Пример оптимистического параллелизма

Ниже приведен простой пример, который задает UpdateCommand объекта DataAdapter для проверки оптимистичного параллелизма, а затем использует событие RowUpdated для тестирования на наличие нарушений оптимистического параллелизма. При обнаружении нарушения оптимистического параллелизма приложение задает RowError строки, для которой было выдано обновление для отражения нарушения оптимистического параллелизма.

Обратите внимание, что значения параметров, передаваемые предложению WHERE команды UPDATE, сопоставляются с исходными значениями соответствующих столбцов.

' 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;  
  }  
}  

См. также