上面代码运行后,只有李四会被更新到数据库中,王五因为并发冲突且异常捕获后没有进行任何处理而不会存入数据库。
3. 数据库和客户端合并获胜
这种方式是最复杂的,需要合并数据库和客户端的数据,如果用到此方法我们需要谨记如下两点:
- 如果原始值与数据库中的值不通,就说明数据库中的值已经被其他客户端更新,这时必须放弃当前的更新,保留数据库的更新;
- 如果原始值与数据库的值相同,代表不会发生并发冲突,按照正常处理流程处理即可。
同样,我们将上面的例子按照上面两点进行修改:
class Program { static void Main(string[] args) { int userId = 1; using (var db = new EfContext()) { using (var ef = new EfContext()) { User user1 = db.Users.FirstOrDefault(p => p.Id == userId); User user2 = ef.Users.FirstOrDefault(p => p.Id == userId); user1.Name = "李四"; db.SaveChanges(); try { user2.Name = "王五"; ef.SaveChanges(); } catch (DbUpdateConcurrencyException e) { Retry(ef, handleDbUpdateConcurrencyException: exception => { exception = (e as DbUpdateConcurrencyException).Entries; foreach (var item in exception) { Object dv = item.GetDatabaseValues(); Object ov = item.OriginalValues(); item.OriginalValues.SetValues(dv); dv.PropertyNames.Where(property => !object.Equals(ov[property], dv[property])).ToList().ForEach(property => item.Property(property).IsModified = false); } }); } } } } }
二、方法三
前面两种方法都是利用 SaveChanges 捕获并发异常,其实我们也可以自定义 SaveChanges 的扩展方法来处理并发异常。下面我们就来看一下具体的两种策略。
1. 普通策略
这个策略非常简单,就是利用循环来实现重试机制,代码如下:
public static partial class DbContextExtensions { public static int SaveChanges(this DbContext dbContext, Action<IEnumerable<DbEntityEntry>> action, int retryCount = 3) { if (retryCount <= 0) { throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0"); } for (int retry=1;retry<retryCount;retry++) { try { } catch (DbUpdateConcurrencyException e) when (retry < retryCount) { resolveConficts(e.Entries); } } return dbContext.SaveChanges(); } }
2. 高级策略
在 .NET 中已经有开发人员帮我们开发出了强大的工具 Polly ,Polly 是一个 .NET 弹性和瞬态故障处理库,允许开发人员以 Fluent 和线程安全的方式来实现重试、断路、超时、隔离和回退策略。
- 首先我们需要定义一个枚举类型
public enum RefreshConflict { StoreWins, ClientWins, MergeClientAndStore }
- 然后根据不同的获胜模式来刷新数据库的值
public static class RefreshEFStateExtensions { public static EntityEntry Refresh(this EntityEntry tracking, RefreshConflict refreshMode) { switch (refreshMode) { case RefreshConflict.StoreWins: { tracking.Reload(); break; } case RefreshConflict.ClientWins: { PropertyValues databaseValues = tracking.GetDatabaseValues(); if (databaseValues == null) { tracking.State = EntityState.Detached; } else { tracking.OriginalValues.SetValues(databaseValues); } break; } case RefreshConflict.MergeClientAndStore: { PropertyValues databaseValues = tracking.GetDatabaseValues(); if (databaseValues == null) { tracking.State = EntityState.Detached; } else { //当实体被更新时,刷新数据库原始值 PropertyValues originalValues = tracking.OriginalValues.Clone(); tracking.OriginalValues.SetValues(databaseValues); //如果数据库中对于属性有不同的值保留数据库中的值 #if SelfDefine databaseValues.PropertyNames // Navigation properties are not included. .Where(property => !object.Equals(originalValues[property], databaseValues[property])) .ForEach(property => tracking.Property(property).IsModified = false); #else databaseValues.Properties .Where(property => !object.Equals(originalValues[property.Name], databaseValues[property.Name])) .ToList() .ForEach(property => tracking.Property(property.Name).IsModified = false); #endif } break; } } return tracking; } }
- 最后定义刷新状态的方法
public static partial class DbContextExtensions { public static int SaveChanges(this DbContext context, RefreshConflict refreshMode, int retryCount = 3) { if (retryCount <= 0) { throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0"); } return context.SaveChanges( conflicts => conflicts.ToList().ForEach(tracking => tracking.Refresh(refreshMode)), retryCount); } public static int SaveChanges( this DbContext context, RefreshConflict refreshMode, RetryStrategy retryStrategy) => context.SaveChanges( conflicts => conflicts.ToList().ForEach(tracking => tracking.Refresh(refreshMode)), retryStrategy); }
到这里 Entity Framework 解决并发冲突的方案已经讲完了,上面这几种方案都是固定的写法,大家可以直接将上面的代码复制进项目中使用。