處理並行存取衝突
提示
您可以檢視本文中的 GitHut 範例。
在大部分情況下,資料庫會同時由多個應用程式實例使用,每個實例都會獨立地對數據執行修改。 當相同的數據同時修改時,可能會發生不一致和數據損毀,例如,當兩個用戶端修改相同數據列中的不同數據行時,這些數據行會以某種方式相關。 此頁面討論確保數據在面對這類並行變更時保持一致的機制。
開放式並行存取
EF Core 會實作 開放式並行存取,假設並行衝突相對罕見。 與 悲觀 方法相反,這些方法會預先鎖定數據,然後才繼續修改數據,開放式並行存取不會有任何鎖定,但如果數據自查詢后變更,則數據修改會失敗。 此並行失敗會回報給應用程式,而該應用程式會藉由重試新數據的整個作業來處理它。
在EF Core中,開放式並行存取是藉由將屬性設定為 並行令牌來實作。 當查詢實體時,會載入並追蹤並行令牌,就像任何其他屬性一樣。 然後,在期間 SaveChanges()執行更新或刪除作業時,資料庫上的並行令牌值會與 EF Core 讀取的原始值進行比較。
若要了解運作方式,讓我們假設我們在 SQL Server 上,並使用特殊 Version
屬性定義一般 Person 實體類型:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Timestamp]
public byte[] Version { get; set; }
}
在 SQL Server 中,這會設定並行令牌,以在每次變更數據列時自動變更資料庫(以下提供更多詳細數據)。 在此組態就緒后,讓我們檢查簡單更新作業會發生什麼事:
var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
context.SaveChanges();
- 在第一個步驟中,人員會從資料庫載入;這包括並行令牌,EF 和其餘屬性現在會如往常一樣追蹤。
- 然後,人員實例會以某種方式修改 - 我們變更
FirstName
屬性。 - 然後,我們會指示 EF Core 保存修改。 由於已設定並行令牌,EF Core 會將下列 SQL 傳送至資料庫:
UPDATE [People] SET [FirstName] = @p0
WHERE [PersonId] = @p1 AND [Version] = @p2;
請注意,除了 PersonId
WHERE 子句中的 之外,EF Core 也新增了 條件 Version
;這隻 Version
會在查詢數據行之後尚未變更時修改數據列。
在一般 (“開放式”) 案例中,不會發生並行更新,而且 UPDATE 會順利完成,修改數據列:資料庫向EF Core 回報一個數據列受到UPDATE的影響,如預期般。 不過,如果發生並行更新,UPDATE 會找不到任何相符的數據列和報告,而該數據列和報告會受到影響。 因此,EF Core 會 SaveChanges() 擲回 DbUpdateConcurrencyException,應用程式必須適當攔截並處理。 以下詳細說明如何執行這項操作的技術, 請參閱解決並行衝突。
雖然上述範例討論 現有實體的更新 。 嘗試刪除已同時修改的資料列時,EF 也會擲回 DbUpdateConcurrencyException 。 不過,新增實體時絕不會擲回此例外狀況;如果插入具有相同索引鍵的數據列,資料庫確實可能會引發唯一條件約束違規,但這會導致擲回提供者特定的例外狀況,而不是 DbUpdateConcurrencyException。
原生資料庫產生的並行令牌
在上述程式代碼中,我們使用 [Timestamp]
屬性將屬性對應至 SQL Server rowversion
數據行。 由於 rowversion
在更新數據列時自動變更,因此對於保護整個數據列的最小工作並行令牌非常有用。 將 SQL Server rowversion
資料行設定為並行令牌,如下所示:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Timestamp]
public byte[] Version { get; set; }
}
rowversion
上面顯示的型別是 SQL Server 特定的功能;設定自動更新並行令牌的詳細數據會因資料庫而異,有些資料庫完全不支持它們(例如 SQLite)。 如需確切的詳細數據,請參閱您的提供者檔。
應用程式管理的並行令牌
您可以在應用程式程式代碼中管理它,而不是讓資料庫自動管理並行令牌。 這允許在沒有原生自動更新類型的資料庫上使用開放式並行存取,例如 SQLite。 但即使是在 SQL Server 上,應用程式管理的並行令牌也可以精確控制哪些數據行變更會導致重新產生令牌。 例如,您可能有包含某些快取或非重要值的屬性,而且不希望變更該屬性來觸發並行衝突。
下列會將 GUID 屬性設定為並行令牌:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
[ConcurrencyCheck]
public Guid Version { get; set; }
}
由於此屬性不是資料庫產生的,所以每當保存變更時,您必須在應用程式中指派它:
var person = context.People.Single(b => b.FirstName == "John");
person.FirstName = "Paul";
person.Version = Guid.NewGuid();
context.SaveChanges();
如果您想要一律指派新的 GUID 值,您可以透過 SaveChanges
攔截器執行此動作。 不過,手動管理並行令牌的優點之一是您可以精確控制重新產生時,以避免不需要的並行衝突。
解決並行存取衝突
無論您的並行令牌如何設定,若要實作開放式並行存取,您的應用程式必須正確處理發生並行衝突並 DbUpdateConcurrencyException 擲回的情況;這稱為 解決並行衝突。
其中一個選項是只通知使用者更新因衝突變更而失敗;然後,使用者可以載入新的數據,然後再試一次。 或者,如果您的應用程式正在執行自動更新,在重新查詢數據之後,它可以直接迴圈並立即重試。
解決並行衝突的更複雜方式是 將暫止的變更與資料庫中的新值合併 。 哪些值合併的精確詳細數據取決於應用程式,而進程可能會由使用者介面導向,其中會顯示這兩組值。
有三組值可供協助解決並行存取衝突:
- 「目前值」係指應用程式嘗試寫入至資料庫的值。
- 「原始值」係指在進行任何編輯之前,原先從資料庫擷取到的值。
- 「資料庫值」係指目前儲存在資料庫中的值。
處理並行衝突的一般方法是:
- 在
SaveChanges
期間攔截DbUpdateConcurrencyException
。 - 使用
DbUpdateConcurrencyException.Entries
為受影響的實體準備一組新的變更。 - 重新整理並行存取語彙基元的原始值以反映資料庫中的目前值。
- 重試處理程序,直到沒有發生任何衝突為止。
在下列範例中, Person.FirstName
和 Person.LastName
會設定為並行令牌。 在您包含應用程式特定邏輯以選擇所要儲存值的位置中,有一個 // TODO:
註解。
using var context = new PersonContext();
// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";
// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
"UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");
var saved = false;
while (!saved)
{
try
{
// Attempt to save changes to the database
context.SaveChanges();
saved = true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Person)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
// TODO: decide which value should be written to database
// proposedValues[property] = <value to be saved>;
}
// Refresh original values to bypass next concurrency check
entry.OriginalValues.SetValues(databaseValues);
}
else
{
throw new NotSupportedException(
"Don't know how to handle concurrency conflicts for "
+ entry.Metadata.Name);
}
}
}
}
使用隔離等級進行並行控制
透過並行令牌的開放式並行並行存取並不是確保數據在面對並行變更時保持一致的唯一方式。
確保一致性的機制是 可重複的讀取 交易隔離等級。 在大部分資料庫中,此層級保證交易會在交易啟動時看到資料庫中的數據,而不會受到任何後續並行活動的影響。 從上述取得基本範例時,當我們查詢 Person
以某種方式更新它時,資料庫必須確定沒有其他交易干擾該資料庫數據列,直到交易完成為止。 視您的資料庫實作而定,這會以下列兩種方式之一發生:
- 查詢數據列時,您的交易會取得共享鎖定。 任何嘗試更新數據列的外部交易都會封鎖,直到您的交易完成為止。 這是一種悲觀鎖定形式,由 SQL Server「可重複讀取」隔離等級實作。
- 資料庫不會鎖定,而是允許外部交易更新數據列,但是當您自己的交易嘗試進行更新時,將會引發「串行化」錯誤,指出發生並行衝突。 這是一種開放式鎖定形式,不像 EF 的並行令牌功能,而且是由 SQL Server 快照集隔離等級以及 PostgreSQL 可重複讀取隔離等級實作。
請注意,「可串行化」隔離等級會提供與可重複讀取相同的保證(並新增額外的保證),因此它的運作方式與上述相同。
使用較高的隔離等級來管理並行衝突比較簡單,不需要並行令牌,並提供其他優點:例如,可重複讀取可確保您的交易一律會在交易內的查詢中看到相同的數據,以避免不一致。 不過,此方法確實有其缺點。
首先,如果您的資料庫實作使用鎖定來實作隔離等級,則嘗試修改相同數據列的其他交易必須封鎖整個交易。 這可能會對並行效能產生負面影響(讓您的交易保持簡短!),不過請注意,EF 的機制會擲回例外狀況,並強制您改為重試,這也會造成影響。 這適用於 SQL Server 可重複讀取層級,但不適用於快照集層級,而不會鎖定查詢的數據列。
更重要的是,這種方法需要交易才能跨越所有作業。 例如,如果您查詢 Person
以向用戶顯示其詳細數據,然後等待用戶進行變更,則交易必須保持運作可能很長的時間,在大部分情況下應該避免。 因此,當所有自主作業立即執行,且交易不相依於可能會增加其持續時間的外部輸入時,此機制通常是適當的。
其他資源
如需衝突偵測的 ASP.NET Core 範例,請參閱 EF Core 中的衝突偵測。