前言
之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问题,本文比较长,请耐心点看。
EntityFramework Core并发初级版初探
关于并发无非就两种:乐观并发和悲观并发,悲观并发简言之则是当客户端对数据库中同一值进行修改时会造成阻塞,而乐观并发则任何客户端都可以对可以对数据进行查询或者读取,在EF Core中不支持悲观并发,结果则产生并发冲突,所以产生的冲突则需要我们去解决。
为了便于理解我们从基础内容开始讲起,稍安勿躁,我们循序渐进稍后会讲到并发冲突、并发解决、并发高级三个方面的内容。我们建立实体类如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public int Count { get; set; } }
接下来简单配置下映射:
public class EFCoreContext : DbContext { public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(@"Server=.;Database=EFCoreDb;Trusted_Connection=True;"); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>(pc => { pc.ToTable("Blog").HasKey(k => k.Id); pc.Property(p => p.Name).IsRequired(); pc.Property(p => p.Url).IsRequired(); pc.Property(p => p.Count).IsRequired(); }); } }
接下来我们简单封装下进行查询和更新数据的类 DbQueryCommit
public class DbQueryCommit : IDisposable { private readonly EFCoreContext context; public DbQueryCommit(EFCoreContext context) => this.context = context; public TEntity Query<TEntity>(params object[] keys) where TEntity : class => this.context.Set<TEntity>().Find(keys); public int Commit(Action change) { change(); return context.SaveChanges(); } public DbSet<TEntity> Set<TEntity>() where TEntity : class => context.Set<TEntity>(); public void Dispose() => context.Dispose(); }
接下来我们来看看非并发的情况,进行如下查询和修改:
public static void NoCheck( DbQueryCommit readerWriter1, DbQueryCommit readerWriter2, DbQueryCommit readerWriter3) { int id = 1; Blog blog1 = readerWriter1.Query<Blog>(id); Blog blog2 = readerWriter2.Query<Blog>(id); readerWriter1.Commit(() => blog1.Name = nameof(readerWriter1)); readerWriter2.Commit(() => blog2.Name = nameof(readerWriter2)); Blog category3 = readerWriter3.Query<Blog>(id); Console.WriteLine(category3.Name); }
当前博主VS版本为2017,演示该程序在控制台,之前我们有讲过若要进行迁移需要安装 Microsoft.EntityFrameworkCore.Tools.DotNet 程序包,此时我们会发现根本都安装不上,如下:
不知为何错误,此时我们需要在项目文件中手动添加如上程序包,(解决方案来源于:https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet)如下:
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> </ItemGroup>
然后添加程序包 Microsoft.EntityFrameworkCore.Design ,此时我们再来 dotnet restore 则会看到如下执行EF的命令:
接下来我们实例化上下文进行修改数据。
var efContext1 = new EFCoreContext(); var d1 = new DbQueryCommit(efContext1); var efContext2 = new EFCoreContext(); var d2 = new DbQueryCommit(efContext2); var efContext3 = new EFCoreContext(); var d3 = new DbQueryCommit(efContext3); Concurrency.NoCheck(d1, d2, d3);
此时我们在数据库中默认插入一条数据:
此时界面打印最后读取到的Name值如下:
数据库也对应进行了更新,这也充分说明EF Core对于并发为乐观并发:
接下来我们对Name属性定义为并发Token。
pc.Property(p => p.Name).IsRequired().IsConcurrencyToken();
此时为了很好演示各个方法,我们同样再来定义并发方法,如下:
public static void ConcurrencyCheck(DbQueryCommit readerWriter1, DbQueryCommit readerWriter2) { int id = 1; Blog blog1 = readerWriter1.Query<Blog>(id); Blog blog2 = readerWriter2.Query<Blog>(id); readerWriter1.Commit(() => { blog1.Name = nameof(readerWriter1); blog1.Count = 2; }); readerWriter2.Commit(() => { blog2.Name = nameof(readerWriter2); blog2.Count = 2; }); }
此时再来调用该方法:
var efContext1 = new EFCoreContext(); var d1 = new DbQueryCommit(efContext1); var efContext2 = new EFCoreContext(); var d2 = new DbQueryCommit(efContext2); //var efContext3 = new EFCoreContext(); //var d3 = new DbQueryCommit(efContext3); Concurrency.ConcurrencyCheck(d1, d2);
当我们利用两个上下文1和2去读取数据时此时Name = 'Jeffcky',当上下文1更新时在快照中根据主键和Name去查找数据库,查找到Name后并令Name = 'readerWriter1'成功更新,但是上下2去更新Name = 'readerWriter2'时,此时在快照中根据主键和Name去查找数据库,发现不存在该条数据,同时我们设置了并发Token,最终导致出现 DbUpdateConcurrencyException 并发更新异常。解决并发个两点一个是上述设置并发Token,另外一个则是设置行版本,下面我们也来看下,首先我们在类中增加一个行版本的字节属性。
public byte[] RowVersion { get; set; }
同时对该行版本进行映射标识。
pc.Property(p => p.RowVersion).IsRequired().IsRowVersion().ValueGeneratedOnAddOrUpdate();
为了很好演示行版本并发,我们增加一个属性来打印行版本字符串。
public string RowVersionString => $"0x{BitConverter.ToUInt64(RowVersion.Reverse().ToArray(), 0).ToString("X16")}";
同样我们定义一个调用行版本的方法:
public static void RowVersion(DbQueryCommit readerWriter1, DbQueryCommit readerWriter2) { int id = 1; Blog blog1 = readerWriter1.Query<Blog>(id); Console.WriteLine(blog1.RowVersionString); Blog blog2 = readerWriter2.Query<Blog>(id); Console.WriteLine(blog2.RowVersionString); readerWriter1.Commit(() => blog1.Name = nameof(readerWriter1)); Console.WriteLine(blog1.RowVersionString); readerWriter2.Commit(() => readerWriter2.Set<Blog>().Remove(blog2)); }
接下来我们调用演示看看。
var efContext1 = new EFCoreContext(); var d1 = new DbQueryCommit(efContext1); var efContext2 = new EFCoreContext(); var d2 = new DbQueryCommit(efContext2); //var efContext3 = new EFCoreContext(); //var d3 = new DbQueryCommit(efContext3); Concurrency.RowVersion(d1, d2);
我们从上可以明显看出当查出数据库中的行版本值为 0x000000000073 ,接着readerWriter1更新后其行版本增加为 0x000000000074 ,当我们利用readerWriter2去删除查询出id = 1的数据时,此时会根据当前主键和行版本为 0x000000000073 去查找数据库,但是此时没有找到数据,导致同样如上述并发Token一样出现并发异常。
EntityFramework Core并发中级版解析
并发异常我们可以通过 DbUpdateConcurrencyException 来获取,该类继承自 DbUpdateException ,该类中的参数 EntityEntry 为一个集合,利用它则可以获取到对应的数据库中的值以及当前更新值等,所以我们可以自定义并发异常解析,如下:
public class DbUpdateException : Exception { public virtual IReadOnlyList<EntityEntry> Entries { get; } } public class DbUpdateConcurrencyException : DbUpdateException { //TODO }
这里我们需要弄明白存在EntityEntry中的值类型,比如DbUpdateConcurrencyException的参数为exception。我们通过如下则可以获取到被跟踪的实体状态。
var tracking = exception.Entries.Single();
此时存在数据库中的原始值则为如下:
var original = tracking.OriginalValues.ToObject();
而当前需要更新的值则为如下:
var current = tracking.CurrentValues.ToObject();
而数据库中的值则为已经提交更新的值:
var database = '第一次已经更新的对象';
上述既然出现并发异常,接下来我们则需要解析并发异常并解决异常,大部分情况下无论是提交事务失败也好还是对数据进行操作也好都会进行重试机制,所以这里我们解析到并发异常并采取重试机制。之前我们进行提交时定义如下:
public int Commit(Action change) { change(); return context.SaveChanges(); }
此时我们对该方法进行重载,遇到并发异常后并采取重试机制重试三次,如下:
public int Commit(Action change, Action<DbUpdateConcurrencyException> handleException, int retryCount = 3) { change(); for (int retry = 0; retry < retryCount; retry++) { try { return context.SaveChanges(); } catch (DbUpdateConcurrencyException exception) { handleException(exception); } } return context.SaveChanges(); }
然后我们定义一个需要出现并发并调用上述重试机制的更新方法,如下:
public static void UpdateBlog( DbQueryCommit readerWriter1, DbQueryCommit readerWriter2, DbQueryCommit readerWriter3, Action<EntityEntry> resolveConflict) { int id = 1; Blog blog1 = readerWriter1.Query<Blog>(id); Blog blog2 = readerWriter2.Query<Blog>(id); Console.WriteLine($"查询行版本:{blog1.RowVersionString}"); Console.WriteLine("----------------------------------------------------------"); Console.WriteLine($"查询行版本:{blog2.RowVersionString}"); Console.WriteLine("----------------------------------------------------------"); readerWriter1.Commit(() => { blog1.Name = nameof(readerWriter1); blog1.Count = 2; }); Console.WriteLine($"更新blog1后行版本:{blog1.RowVersionString}"); Console.WriteLine("----------------------------------------------------------"); readerWriter2.Commit( change: () => { blog2.Name = nameof(readerWriter2); blog2.Count = 1; }, handleException: exception => { EntityEntry tracking = exception.Entries.Single(); Blog original = (Blog)tracking.OriginalValues.ToObject(); Blog current = (Blog)tracking.CurrentValues.ToObject(); Blog database = blog1; var origin = $"原始值:({original.Name},{original.Count},{original.Id},{original.RowVersionString})"; Console.WriteLine(original); Console.WriteLine("----------------------------------------------------------"); var databaseValue = $"数据库中值:({database.Name},{database.Count},{database.Id},{database.RowVersionString})"; Console.WriteLine(databaseValue); Console.WriteLine("----------------------------------------------------------"); var update = $"更新的值:({current.Name},{current.Count},{current.Id},{current.RowVersionString})"; Console.WriteLine(update);
感谢花时间阅读此篇文章,如果您觉得这篇文章你学到了东西也是为了犒劳下博主的码字不易不妨一下吧,让楼主能喝上一杯咖啡,在此谢过了! 如果您觉得阅读本文对您有帮助,请点一下“”按钮,您的将是我最大的写作动力! 本文版权归作者和博客园共有,来源网址:欢迎各位转载,但是未经作者本人同意,转载文章之后,否则保留追究法律责任的权利。
http://www.cnblogs.com/CreateMyself/p/6613949.html