Entity Framework
解决并发冲突

解决并发冲突

⚠️

该文档可能已过期。

乐观并发,包括乐观地尝试将实体保存到数据库,希望数据在加载实体后未发生更改。

如果事实证明数据已更改,则会引发异常,必须在尝试再次保存之前解决冲突。


何时发生

以下表(表名:ConcurrencyModels)为例:

列名类型特性
IdGuid主键
Valueint默认(客户端优先)
ClientWinValueint客户端优先
DatabaseWinValueint数据库优先
RowVersionint并发检查

现有数据记录:

IdValueClientWinValueDatabaseWinValueRowVersion
314ce3b7-e6b9-46a4-81a4-b5b1406531890000

假设,现有两个客户端同时修改该条数据记录:

UPDATE `ConcurrencyModels` 
SET `Value` = 1, ClientWinValue = 1, DatabaseWinValue = 1, RowVersion = 100 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 0;
UPDATE `ConcurrencyModels` 
SET `Value` = 2, ClientWinValue = 2, DatabaseWinValue = 2, RowVersion = 200 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 0;

任意一条更新语句执行后(受影响条目为 1),RowVersion 值将会改变为新值,这将导致另一条更新语句执行后不会任何效果(受影响条目为 0)。


EFCore 在处理并发冲突时,若受影响条目为 0,则会抛出异常(DbUpdateConcurrencyException):

'The database operation was expected to affect 1 row(s), but actually affected 0 row(s); '

乐观解决冲突

以上文为例,若 RowVersion = 100 的更新语句先执行,受影响条目则为 1

UPDATE `ConcurrencyModels` 
SET `Value` = 1, ClientWinValue = 1, DatabaseWinValue = 1, RowVersion = 100 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 0;
IdValueClientWinValueDatabaseWinValueRowVersion
314ce3b7-e6b9-46a4-81a4-b5b1406531890
1
0
1
0
1
0
100

那么,执行另一条语句 RowVersion = 200 执行时,受影响条目则为 0

UPDATE `ConcurrencyModels` 
SET `Value` = 2, ClientWinValue = 2, DatabaseWinValue = 2, RowVersion = 200 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 0;
IdValueClientWinValueDatabaseWinValueRowVersion
314ce3b7-e6b9-46a4-81a4-b5b140653189111100

对于发生更新冲突的语句,由于部分值已经发生改变,通常会有 客户端优先数据库优先 两种处理方式:

  • 客户端优先(ClientWins):使用新值覆盖已存储的值。
  • 数据库优先(DatabaseWins):保留已存储的值。

为了正确更新条目,我们必须修正第二条语句后进行重试:

客户端优先:

UPDATE `ConcurrencyModels` 
SET `Value` = 2, ClientWinValue = 2, DatabaseWinValue = 2, RowVersion = 200 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 100;
IdValueClientWinValueDatabaseWinValueRowVersion
314ce3b7-e6b9-46a4-81a4-b5b1406531891
2
1
2
1
2
100
200

数据库优先:

UPDATE `ConcurrencyModels` 
SET `Value` = 2, ClientWinValue = 2, RowVersion = 200 
WHERE Id = '314ce3b7-e6b9-46a4-81a4-b5b140653189' AND RowVersion = 100;
IdValueClientWinValueDatabaseWinValueRowVersion
314ce3b7-e6b9-46a4-81a4-b5b1406531891
2
1
2
1100
200

使用自动解决冲突

  1. DbContext 实现 IConcurrencyResolvableContext 接口:

    public class ApplicationDbContext : IConcurrencyResolvableContext
    {
        // Larger values will reduce update conflicts,
        // but will make more retries.
        // It is recommended to set a smaller value according to the actual situation.
        
        // 越大的值将减少更新冲突,但会进行更多重试。
        // 建议根据实际情况设置更小的值。
        
        public int MaxConcurrencyRetry => 3;
    }
  2. 标记需要解决冲突的模型:

    使用特性 ConcurrencyResolvable 标记模型类,ConcurrencyPolicy 标记字段:

    [ConcurrencyResolvable]
    public class ConcurrencyModel
    {
        [Key]
        public Guid Id { get; set; }
     
        [ConcurrencyCheck]
        public int RowVersion { get; set; }
     
        public int Value { get; set; }
     
        [ConcurrencyPolicy(ConcurrencyResolvingMode.DatabaseWins)]
        public int DatabaseWinValue { get; set; }
     
        [ConcurrencyPolicy(ConcurrencyResolvingMode.ClientWins)]
        public int ClientWinValue { get; set; }
    }
    未使用 ConcurrencyPolicy 标记的字段默认为 客户端优先(ClientWins),故此示例中 ValueClientWinValue 具有相同特性。
  3. 重写 DbContext 方法 SaveChangesSaveChangesAsync

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        return LinqSharpEF.SaveChanges(this, base.SaveChanges, acceptAllChangesOnSuccess);
    }
     
    public override Task<int> SaveChangesAsync(
        bool acceptAllChangesOnSuccess, 
        CancellationToken cancellationToken = default)
    {
        return LinqSharpEF.SaveChangesAsync(this, base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken);
    }