Optymistyczna współbieżność
W środowisku wieloużytkownikowym istnieją dwa modele aktualizowania danych w bazie danych: optymistyczna współbieżność i pesymistyczna współbieżność. Obiekt DataSet został zaprojektowany tak, aby zachęcić do korzystania z optymistycznej współbieżności w przypadku długotrwałych działań, takich jak komunikacja zdalna danych i interakcja z danymi.
Pesymistyczna współbieżność polega na zablokowaniu wierszy w źródle danych, aby uniemożliwić innym użytkownikom modyfikowanie danych w sposób, który ma wpływ na bieżącego użytkownika. W modelu pesymistycznym, gdy użytkownik wykonuje akcję, która powoduje zastosowanie blokady, inni użytkownicy nie mogą wykonywać akcji, które mogłyby powodować konflikt z blokadą, dopóki właściciel blokady go nie zwolni. Ten model jest używany głównie w środowiskach, w których występuje duża rywalizacja o dane, dzięki czemu koszt ochrony danych za pomocą blokad jest mniejszy niż koszt wycofywania transakcji, jeśli wystąpią konflikty współbieżności.
W związku z tym w modelu pesymistycznej współbieżności użytkownik, który aktualizuje wiersz, ustanawia blokadę. Dopóki użytkownik nie zakończy aktualizacji i zwolni blokadę, nikt inny nie może zmienić tego wiersza. Z tego powodu pesymistyczna współbieżność jest najlepiej zaimplementowana, gdy czasy blokady będą krótkie, podobnie jak w programowym przetwarzaniu rekordów. Pesymistyczna współbieżność nie jest skalowalną opcją, gdy użytkownicy wchodzą w interakcję z danymi i powodują zablokowanie rekordów przez stosunkowo duże okresy czasu.
Uwaga
Jeśli musisz zaktualizować wiele wierszy w tej samej operacji, utworzenie transakcji jest bardziej skalowalną opcją niż używanie pesymistycznego blokowania.
Z kolei użytkownicy korzystający z optymistycznej współbieżności nie blokują wiersza podczas odczytywania go. Gdy użytkownik chce zaktualizować wiersz, aplikacja musi określić, czy inny użytkownik zmienił wiersz od czasu jego odczytania. Optymistyczna współbieżność jest zwykle używana w środowiskach o niskiej rywalizacji o dane. Optymistyczna współbieżność zwiększa wydajność, ponieważ nie jest wymagane blokowanie rekordów, a blokowanie rekordów wymaga dodatkowych zasobów serwera. Ponadto w celu zachowania blokad rekordów wymagane jest trwałe połączenie z serwerem bazy danych. Ponieważ tak nie jest w przypadku optymistycznego modelu współbieżności, połączenia z serwerem są bezpłatne do obsługi większej liczby klientów w krótszym czasie.
W modelu optymistycznej współbieżności uważa się, że wystąpiło naruszenie, jeśli po otrzymaniu przez użytkownika wartości z bazy danych inny użytkownik modyfikuje wartość, zanim pierwszy użytkownik próbował go zmodyfikować. Jak serwer rozwiązuje naruszenie współbieżności, najlepiej jest pokazać, opisując najpierw poniższy przykład.
W poniższych tabelach przedstawiono przykład optymistycznej współbieżności.
O godzinie 13:00 użytkownik User1 odczytuje wiersz z bazy danych z następującymi wartościami:
CustID LastName FirstName
101 Smith Bob
Nazwa kolumny | Oryginalna wartość | Bieżąca wartość | Wartość w bazie danych |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Robert | Robert | Robert |
O godzinie 13:01 użytkownik User2 odczytuje ten sam wiersz.
O godzinie 13:03 użytkownik User2 zmienia wartość FirstName z "Bob" na "Robert" i aktualizuje bazę danych.
Nazwa kolumny | Oryginalna wartość | Bieżąca wartość | Wartość w bazie danych |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Robert | Robert | Robert |
Aktualizacja powiedzie się, ponieważ wartości w bazie danych w czasie aktualizacji są zgodne z oryginalnymi wartościami, które ma użytkownik User2.
O godzinie 13:05 użytkownik User1 zmienia imię "Boba" na "Jamesa" i próbuje zaktualizować wiersz.
Nazwa kolumny | Oryginalna wartość | Bieżąca wartość | Wartość w bazie danych |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Robert | James | Robert |
W tym momencie użytkownik User1 napotyka optymistyczne naruszenie współbieżności, ponieważ wartość w bazie danych ("Robert") nie jest już zgodna z oryginalną wartością oczekiwaną przez użytkownika User1 ("Bob"). Naruszenie współbieżności po prostu informuje o tym, że aktualizacja nie powiodła się. Teraz należy podjąć decyzję, czy zastąpić zmiany wprowadzone przez użytkownika User2 zmianami dostarczonymi przez użytkownika User1, czy anulować zmiany według użytkownika User1.
Testowanie pod kątem optymistycznych naruszeń współbieżności
Istnieje kilka technik testowania pod kątem optymistycznego naruszenia współbieżności. Jednym z nich jest dołączenie kolumny znacznika czasu w tabeli. Bazy danych często udostępniają funkcje sygnatury czasowej, które mogą służyć do identyfikowania daty i godziny ostatniej aktualizacji rekordu. Korzystając z tej techniki, kolumna znacznika czasu jest zawarta w definicji tabeli. Za każdym razem, gdy rekord jest aktualizowany, znacznik czasu jest aktualizowany w celu odzwierciedlenia bieżącej daty i godziny. W teście pod kątem optymistycznych naruszeń współbieżności kolumna sygnatury czasowej jest zwracana z dowolnym zapytaniem zawartości tabeli. Podczas próby aktualizacji wartość znacznika czasu w bazie danych jest porównywana z oryginalną wartością znacznika czasu zawartą w zmodyfikowanym wierszu. Jeśli są one zgodne, aktualizacja zostanie wykonana, a kolumna sygnatury czasowej zostanie zaktualizowana o bieżący czas, aby odzwierciedlić aktualizację. Jeśli nie są one zgodne, wystąpiło optymistyczne naruszenie współbieżności.
Inną techniką testowania pod kątem optymistycznego naruszenia współbieżności jest sprawdzenie, czy wszystkie oryginalne wartości kolumn w wierszu nadal pasują do tych znalezionych w bazie danych. Rozważmy na przykład następujące zapytanie:
SELECT Col1, Col2, Col3 FROM Table1
Aby przetestować optymistyczne naruszenie współbieżności podczas aktualizowania wiersza w tabeli Table1, należy wydać następującą instrukcję UPDATE:
UPDATE Table1 Set Col1 = @NewCol1Value,
Set Col2 = @NewCol2Value,
Set Col3 = @NewCol3Value
WHERE Col1 = @OldCol1Value AND
Col2 = @OldCol2Value AND
Col3 = @OldCol3Value
Jeśli oryginalne wartości są zgodne z wartościami w bazie danych, aktualizacja jest wykonywana. Jeśli wartość została zmodyfikowana, aktualizacja nie zmodyfikuje wiersza, ponieważ klauzula WHERE nie znajdzie dopasowania.
Należy pamiętać, że zaleca się zawsze zwracanie unikatowej wartości klucza podstawowego w zapytaniu. W przeciwnym razie poprzednia instrukcja UPDATE może zaktualizować więcej niż jeden wiersz, co może nie być twoim zamiarem.
Jeśli kolumna w źródle danych zezwala na wartości null, może być konieczne rozszerzenie klauzuli WHERE w celu sprawdzenia pasującego odwołania o wartości null w tabeli lokalnej i w źródle danych. Na przykład poniższa instrukcja UPDATE sprawdza, czy odwołanie o wartości null w wierszu lokalnym nadal pasuje do odwołania o wartości null w źródle danych lub że wartość w wierszu lokalnym nadal odpowiada wartości w źródle danych.
UPDATE Table1 Set Col1 = @NewVal1
WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1
Możesz również zastosować mniej restrykcyjne kryteria podczas korzystania z optymistycznego modelu współbieżności. Na przykład użycie tylko kolumn klucza podstawowego w klauzuli WHERE powoduje zastąpienie danych niezależnie od tego, czy inne kolumny zostały zaktualizowane od ostatniego zapytania. Można również zastosować klauzulę WHERE tylko do określonych kolumn, co powoduje zastąpienie danych, chyba że określone pola zostały zaktualizowane od czasu ostatniego zapytania.
Zdarzenie DataAdapter.RowUpdated
Zdarzenie DataAdapter RowUpdated obiektu może być używane w połączeniu z opisanymi wcześniej technikami, aby przekazać powiadomienie do aplikacji optymistycznych naruszeń współbieżności. WierszUpdated występuje po każdej próbie zaktualizowania zmodyfikowanego wiersza z zestawu danych. Dzięki temu można dodać specjalny kod obsługi, w tym przetwarzanie w przypadku wystąpienia wyjątku, dodawanie niestandardowych informacji o błędach, dodawanie logiki ponawiania itd. Obiekt RowUpdatedEventArgs zwraca właściwość RecordsAffected zawierającą liczbę wierszy, na które ma wpływ określone polecenie aktualizacji zmodyfikowanego wiersza w tabeli. Ustawiając polecenie aktualizacji w celu przetestowania optymistycznej współbieżności, właściwość RecordsAffected zwróci w rezultacie wartość 0, gdy wystąpiło optymistyczne naruszenie współbieżności, ponieważ nie zaktualizowano żadnych rekordów. W takim przypadku zgłaszany jest wyjątek. Zdarzenie RowUpdated umożliwia obsługę tego wystąpienia i unikanie wyjątku, ustawiając odpowiednią wartość RowUpdatedEventArgs.Status , taką jak UpdateStatus.SkipCurrentRow. Aby uzyskać więcej informacji na temat zdarzenia RowUpdated , zobacz Obsługa zdarzeń DataAdapter.
Opcjonalnie możesz ustawić właściwość DataAdapter.ContinueUpdateOnError na wartość true, przed wywołaniem polecenia Update i odpowiedzieć na informacje o błędzie przechowywane we właściwości RowError określonego wiersza po zakończeniu aktualizacji. Aby uzyskać więcej informacji, zobacz Informacje o błędzie wiersza.
Przykład optymistycznej współbieżności
Poniżej przedstawiono prosty przykład, który ustawia polecenie UpdateCommand elementu DataAdapter do testowania pod kątem optymistycznej współbieżności, a następnie używa zdarzenia RowUpdated do testowania pod kątem optymistycznych naruszeń współbieżności. Po napotkaniu optymistycznego naruszenia współbieżności aplikacja ustawia wartość RowError wiersza, dla którego wydano aktualizację, aby odzwierciedlić optymistyczne naruszenie współbieżności.
Należy pamiętać, że wartości parametrów przekazane do klauzuli WHERE polecenia UPDATE są mapowane na oryginalne wartości odpowiednich kolumn.
' 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;
}
}