前言

之前有关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 = ;
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 = ;
Blog blog1 = readerWriter1.Query<Blog>(id);
Blog blog2 = readerWriter2.Query<Blog>(id); readerWriter1.Commit(() =>
{
blog1.Name = nameof(readerWriter1);
blog1.Count = ;
}); readerWriter2.Commit(() =>
{
blog2.Name = nameof(readerWriter2);
blog2.Count = ;
});
}

此时再来调用该方法:

            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 = ;
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 = )
{
change();
for (int retry = ; 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 = ;
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 = ;
}); Console.WriteLine($"更新blog1后行版本:{blog1.RowVersionString}");
Console.WriteLine("----------------------------------------------------------"); readerWriter2.Commit(
change: () =>
{
blog2.Name = nameof(readerWriter2);
blog2.Count = ;
},
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);
Console.WriteLine("----------------------------------------------------------"); resolveConflict(tracking);
}); Blog resolved = readerWriter3.Query<Blog>(id); var resolvedValue = $"查询并发解析后中的值: ({resolved.Name}, {resolved.Count}, {resolved.Id},{resolved.RowVersionString})";
Console.WriteLine(resolvedValue);
}

接下来我们实例化三个上下文,稍后我会一一进行解释,避免看文章的童鞋看晕了几个值。

            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);

【温馨提示】:有很多童鞋在用.net core时控制台时会遇见中文乱码的问题,主要是.net core都需要安装包来进行,以此来说明不再依赖本地程序集达到更好的跨平台,真正实现模块化,所以需要在控制台注册中文编码包,如下:

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

同时需要安装如下包:

System.Text.Encoding.CodePages

在注册后还需要设置控制台输出行编码为GB2312

 Console.OutputEncoding = Encoding.GetEncoding("GB2312");

接下来进行方法调用:

            Concurrency.UpdateBlog(d1, d2, d3, (d) =>
{
PropertyValues databaseValues = d.GetDatabaseValues();
if (databaseValues == null)
{
d.State = EntityState.Detached;
}
else
{
d.OriginalValues.SetValues(databaseValues);
}
});

此时控制台打印和数据库更新值如下:

我们从调用方法开始解释:

当执行到更新上下文一中的Name值时为readerWriter1都没有问题,此时行版本将变为 0x0000000000833 ,当执行到上下文2中时此时去更新Name值时,此时会根据主键和行版本 0x0000000000832 去数据库中查找值,此时却没找到,然后将执行并发异常,最终开始执行 resolveConflict(tracking); 来解析冲突,然后就来到了上述图片所示,此时databaseValues中所存的值就是readrWriter1,也就是说此时数据库中的原始值变为了Name = 'readerWriter1',我们需要做的是将数据库中的Name = 'readerWriter1'设置为原始值,这样下次再去解析冲突会根据主键id = 1和行版本0x00000000833去查找,此时找到该行数据最终进行更新到数据库,所以结果如上图所给。到这里你是不是就觉得难道就这么结束了吗?NO,这个是最简单的一个场景,上述还只是两个并发客户端,如果是多个根本无法保证能够完全解析并发,同时中间还存在一个问题,我们到底是让客户端更新值获胜还是让数据库中原始值获胜呢,这又是一个问题,如果我们完全不借助SQL语句或者存储过程来执行事务的话,这个将是个很严重的问题,比如在秒杀场景中,产品只有1000个,那么每次都让客户端获胜,好吧,那就导致库存溢出的问题,那就呵呵了,还有一个很大的问题则是合并,如果有多个并发请求过来可能我们只需要对于产品中的数量进行并发控制,其他的数据更新完全可以进行合并,这又是一个问题,那么到底该如何解决呢,请继续往下看终极解决方案。

EntityFramweork Core高并发获胜者初级版解析

EntityFramework Core并发数据库获胜

既然是数据库中获胜那么对于客户端出现的并发异常我们就不需要进行解析,此时我们只需要终止异常直接返回值即可,如下定义方法:

        public int DatabaseWin(Action change, Action<DbUpdateConcurrencyException> handleException)
{
change();
try
{
return context.SaveChanges();
}
catch (DbUpdateConcurrencyException exception)
{
return ;
} }

接着我们在UpdateBlog方法中在上下文2中提交数据时调用上述方法并无需再进行并发解析,如下:

            readerWriter2.DatabaseWin(
change: () =>
{
blog2.Name = nameof(readerWriter2);
blog2.Count = ;
},
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);
Console.WriteLine("----------------------------------------------------------"); //resolveConflict(tracking);
});

此时打印和数据库中值如下:

上述就无需再多讲了,根本没有去解析异常。

EntityFramework Core并发客户端获胜

上一大节我们演示的则是客户端获胜,这里我们只需要设置异常解析的值即可解决问题,封装一个方法,如下:

        public static void ClientWins(
DbQueryCommit readerWriter1, DbQueryCommit readerWriter2, DbQueryCommit readerWriter3) =>
UpdateBlog(readerWriter1, readerWriter2, readerWriter3, resolveConflict: tracking =>
{
PropertyValues databaseValues = tracking.GetDatabaseValues();
tracking.OriginalValues.SetValues(databaseValues); Console.WriteLine(tracking.State);
Console.WriteLine(tracking.Property(nameof(Blog.Count)).IsModified);
Console.WriteLine(tracking.Property(nameof(Blog.Name)).IsModified);
Console.WriteLine(tracking.Property(nameof(Blog.Id)).IsModified);
});

结果就不再演示和之前演示结果等同。我们将重点放在客户端和数据库值合并的问题,请继续往下看。

EntityFramework Core并发数据库和客户端合并

当出现并发时我们对前者使其客户端获胜而后者对于前者未有的属性则进行更新,所以我们需要首先对数据库原始值克隆一份,然后将其客户端获胜,然后将原始值和客户端属性进行比较,若数据库中的属性在原始值中的属性中没有,我们则将数据库中的值不进行更新,此时将导致当前并发中的值进行更新则呈现出我们所说客户端和数据库值进行合并更新,如下首先克隆:

            PropertyValues originalValues = tracking.OriginalValues.Clone();
PropertyValues databaseValues = tracking.GetDatabaseValues(); tracking.OriginalValues.SetValues(databaseValues);

比较原始值和数据库中的属性进行比较判断,不存在则不更新。

                databaseValues.Properties
.Where(property => !object.Equals(originalValues[property.Name], databaseValues[property.Name]))
.ToList()
.ForEach(property => tracking.Property(property.Name).IsModified = false);

最终我们定义如下合并方法:

        public static void MergeClientAndDatabase(
DbQueryCommit readerWriter1, DbQueryCommit readerWriter2, DbQueryCommit readerWriter3) =>
UpdateBlog(readerWriter1, readerWriter2, readerWriter3, resolveConflict: tracking =>
{
PropertyValues originalValues = tracking.OriginalValues.Clone();
PropertyValues databaseValues = tracking.GetDatabaseValues(); tracking.OriginalValues.SetValues(databaseValues); #if selfDefine
databaseValues.PropertyNames
.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 Console.WriteLine(tracking.State);
Console.WriteLine(tracking.Property(nameof(Blog.Count)).IsModified);
Console.WriteLine(tracking.Property(nameof(Blog.Name)).IsModified);
Console.WriteLine(tracking.Property(nameof(Blog.Id)).IsModified);
});

此时我们再在UpdateBlog方法添加二者不同的属性,如下:

            readerWriter1.Commit(() =>
{
blog1.Name = nameof(readerWriter1);
blog1.Count = 3;
}); Console.WriteLine($"更新blog1后行版本:{blog1.RowVersionString}");
Console.WriteLine("----------------------------------------------------------"); readerWriter2.Commit(
change: () =>
{
blog2.Name = nameof(readerWriter2);
blog2.Count = 4;
blog2.Url = "http://www.cnblogs.com/CreateMyself";
}.......

为了便于阅读者观察和对比,我们给出数据库中默认的初始值,如下:

好了到了这里关于EF Core中并发内容算是全部结束,别着急,还剩下最后一点内容,那就是终极解决并发方案,请继续往下看。

EntityFramework Core并发高级终极版解决方案

我们定义一个名为 RefreshConflict 枚举,当提交时定义是否为数据库或者客户端或者数据库和客户端数据合并:

    public enum RefreshConflict
{
StoreWins, ClientWins, MergeClientAndStore
}

根据上述不同的获胜模式来刷新数据库中的值,我们定义如下刷新状态扩展方法:

    public static class RefreshEFStateExtensions
{
public static EntityEntry Refresh(this EntityEntry tracking,
RefreshConflict refreshMode)
{
switch (refreshMode)
{
case RefreshConflict.StoreWins:
{
//当实体被删除时,重新加载设置追踪状态为Detached
//当实体被更新时,重新加载设置追踪状态为Unchanged
tracking.Reload();
break;
}
case RefreshConflict.ClientWins:
{
PropertyValues databaseValues = tracking.GetDatabaseValues();
if (databaseValues == null)
{
//当实体被删除时,设置追踪状态为Detached,当然此时客户端无所谓获胜
tracking.State = EntityState.Detached;
}
else
{
//当实体被更新时,刷新数据库原始值
tracking.OriginalValues.SetValues(databaseValues);
}
break;
}
case RefreshConflict.MergeClientAndStore:
{
PropertyValues databaseValues = tracking.GetDatabaseValues();
if (databaseValues == null)
{
/*当实体被删除时,设置追踪状态为Detached,当然此时客户端没有合并的数据
并设置追踪状态为Detached
*/
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 int SaveChanges(
this DbContext context, Action<IEnumerable<EntityEntry>> resolveConflicts, int retryCount = )
{
if (retryCount <= )
{
throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0.");
} for (int retry = ; retry < retryCount; retry++)
{
try
{
return context.SaveChanges();
}
catch (DbUpdateConcurrencyException exception) when (retry < retryCount)
{
resolveConflicts(exception.Entries);
}
}
return context.SaveChanges();
}

另外找到一种重试机制包,安装如下程序包。

EnterpriseLibrary.TransientFaultHandling.Core

我们来简单看一个例子。我们自定义实现需要继承自该程序包中重试策略类 RetryStrategy ,此时需要实现内置如下抽象方法:

public abstract ShouldRetry GetShouldRetry();

最终我们自定义如下实现方法:

    public class ConcurrentcyStrategy : RetryStrategy
{
public ConcurrentcyStrategy(string name, bool firstFastRetry) : base(name, firstFastRetry)
{ } private bool ConcurrentcyShouldRetry(int retryCount, Exception lastException, out TimeSpan delay)
{
if (retryCount <= )
{
throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0");
}
if (lastException is ArgumentNullException)
{
return true;
}
return true;
}
public override ShouldRetry GetShouldRetry()
{
var shouldRetry = new ShouldRetry(ConcurrentcyShouldRetry);
return shouldRetry;
}
}

上述是定义策略类,接下来我们需要实现 ITransientErrorDetectionStrategy 接口来实现需要获取到的异常类,定义如下:

    public class TransientErrorDetection<TException> : ITransientErrorDetectionStrategy
where TException : Exception
{
public bool IsTransient(Exception ex) => ex is TException;
}

最后则是检测到我们所定义的异常并解析重试解析异常,如下:

    public class TransientDetectionExample
{
public int TransientDetectionTest(Func<string, bool> str, RetryStrategy retryStrategy)
{
RetryPolicy retryPolicy = new RetryPolicy(
errorDetectionStrategy: new TransientDetection<ArgumentException>(),
retryStrategy: retryStrategy);
retryPolicy.Retrying += (sender, e) =>
str(((ArgumentNullException)e.LastException).StackTrace);
return retryPolicy.ExecuteAction(RetryCalcu);
}
public int RetryCalcu()
{
return -;
}
}

我们给出如下测试数据,并给出参数为空,观察是否结果会执行RetryCalcu并返回-1:

            var stratrgy = new ConcurrentcyStrategy("test", true);
var isNull = string.Empty;
var example = new TransientDetectionExample();
var result = example.TransientDetectionTest(d => isNull.Contains(""), stratrgy);

所以此时基于上述情况我们可以利用现有的轮子来实现重试机制重载一个SaveChanges方法,最终重试机制我们可以定义如下另一个重载方法:

    public class TransientDetection<TException> : ITransientErrorDetectionStrategy
where TException : Exception
{
public bool IsTransient(Exception ex) => ex is TException;
} public static partial class DbContextExtensions
{
public static int SaveChanges(
this DbContext context, Action<IEnumerable<EntityEntry>> resolveConflicts, int retryCount = )
{
if (retryCount <= )
{
throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0");
} for (int retry = ; retry < retryCount; retry++)
{
try
{
return context.SaveChanges();
}
catch (DbUpdateConcurrencyException exception) when (retry < retryCount)
{
resolveConflicts(exception.Entries);
}
}
return context.SaveChanges();
}
public static int SaveChanges(
this DbContext context, Action<IEnumerable<EntityEntry>> resolveConflicts, RetryStrategy retryStrategy)
{
RetryPolicy retryPolicy = new RetryPolicy(
errorDetectionStrategy: new TransientDetection<DbUpdateConcurrencyException>(),
retryStrategy: retryStrategy);
retryPolicy.Retrying += (sender, e) =>
resolveConflicts(((DbUpdateConcurrencyException)e.LastException).Entries);
return retryPolicy.ExecuteAction(context.SaveChanges);
}
}

同上我们最终提交数据也分别对应两个方法,一个是自定义重试三次,一个利用轮子重试机制,如下:

    public static partial class DbContextExtensions
{
public static int SaveChanges(this DbContext context, RefreshConflict refreshMode, int retryCount = )
{
if (retryCount <= )
{
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); }

接下来我们来分别演示客户端获胜、数据库获胜以及客户端和数据库合并情况。首先我们放一张数据库默认数据以便对比:

EntityFramework Core并发客户端获胜

            var efContext1 = new EFCoreContext();
var efContext2 = new EFCoreContext(); var b1 = efContext1.Blogs.Find();
var b2 = efContext2.Blogs.Find(); b1.Name = nameof(efContext1);
efContext1.SaveChanges(); b2.Name = nameof(efContext2);
b2.Url = "http://www.cnblogs.com/CreateSelf";
efContext2.SaveChanges(RefreshConflict.ClientWins);

上述我们看到数据库中的值完全更新为在上下文2中的数据。

EntityFramework Core并发数据库获胜

            var efContext1 = new EFCoreContext();
var efContext2 = new EFCoreContext(); var b1 = efContext1.Blogs.Find();
var b2 = efContext2.Blogs.Find(); b1.Name = nameof(efContext1);
efContext1.SaveChanges(); b2.Name = nameof(efContext2);
b2.Url = "http://www.cnblogs.com/CreateSelf";
efContext2.SaveChanges(RefreshConflict.StoreWins);

此时我们看到数据库中的值为上下文1中的数据。

EntityFramework Core并发客户端和数据库合并

            var efContext1 = new EFCoreContext();
var efContext2 = new EFCoreContext(); var b1 = efContext1.Blogs.Find();
var b2 = efContext2.Blogs.Find(); b1.Name = nameof(efContext1);
b1.Count = ;
efContext1.SaveChanges(); b2.Name = nameof(efContext2);
b1.Count = ;
b2.Url = "http://www.cnblogs.com/CreateSelf";
efContext2.SaveChanges(RefreshConflict.
MergeClientAndStore);

上述我们看到数据库中的Name和Count是上下文1中的值,而Url则为上下文2中的值。

关于重试机制找到一个比较强大的轮子:https://github.com/App-vNext/Polly 看到一直在更新目前已经支持.net core。看如下加星应该是不错。

没有深入研究该重试机制,就稍微了解了下进行如下重试操作:

        public int Commit(Action change, Action<DbUpdateConcurrencyException> handleException, int retryCount = )
{
change(); Policy
.Handle<DbUpdateConcurrencyException>(ex => ex.Entries.Count > )
.Or<ArgumentException>(ex => ex.ParamName == "exception")
.WaitAndRetry(, retryAttempt => TimeSpan.FromSeconds())
.Execute(() => context.SaveChanges()); return context.SaveChanges();
}

同样调用上述UpdateBlog方法,上下文2中数据如下:

            readerWriter2.Commit(
change: () =>
{
blog2.Name = nameof(readerWriter2);
blog2.Count = ;
blog2.Url = "http://www.cnblogs.com/CreateMyself";
},
handleException: exception =>
............

结果成功更新,利用这个比之前演示的那个更佳,但是发现当执行到这个方法单步执行时会出现如下错误,不知为何:

总结

貌似这篇是有史以来写的最长的一篇博客了,上述关于EF中的并发演示利用行版本的形式,利用并发Token也一致,不过是配置不同罢了,关于EntityFramework Core并发到这里是完全结束,花了许多时间去研究和查资料,受益匪浅,不同的并发策略都已给出,就看具体应用场景了,希望对阅读本文的你有所帮助。see u,我们下节继续开始进入SQL Server性能优化系列,敬请期待。

EntityFramework Core高并发深挖详解,一纸长文,你准备好了吗?的更多相关文章

  1. EntityFramework Core并发深挖详解,一纸长文,你准备好看完了吗?

    前言 之前有关EF并发探讨过几次,但是呢,博主感觉还是有问题,为什么会觉得有问题,其实就是理解不够透彻罢了,于是在项目中都是用的存储过程或者SQL语句来实现,利用放假时间好好补补EF Core并发的问 ...

  2. Redis高并发分布式锁详解

    为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...

  3. Net Core中数据库事务隔离详解——以Dapper和Mysql为例

    Net Core中数据库事务隔离详解--以Dapper和Mysql为例 事务隔离级别 准备工作 Read uncommitted 读未提交 Read committed 读取提交内容 Repeatab ...

  4. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  5. Java并发关键字Volatile 详解

    Java并发关键字Volatile 详解 问题引出: 1.Volatile是什么? 2.Volatile有哪些特性? 3.Volatile每个特性的底层实现原理是什么? 相关内容补充: 缓存一致性协议 ...

  6. BM算法  Boyer-Moore高质量实现代码详解与算法详解

    Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...

  7. [转帖]ASP.NET Core 中间件(Middleware)详解

    ASP.NET Core 中间件(Middleware)详解   本文为官方文档译文,官方文档现已非机器翻译 https://docs.microsoft.com/zh-cn/aspnet/core/ ...

  8. EntityFramework Core解决并发详解

    前言 对过年已经无感,不过还是有很多闲暇时间来学学东西,这一点是极好的,好了,本节我们来讲讲EntityFramewoek Core中的并发问题. 话题(EntityFramework Core并发) ...

  9. Centos7之pacemaker高可用安装配置详解

    申明: centos7的pacemaker与6使用的方法不一致,即使用centos6.x的方法在centos7.x上面配置pacemaker不能成功. 因此openstack 上面的centos7.1 ...

随机推荐

  1. Java包装类缓存

    1.基本概念 在jdk1.5及之后的版本中,Java在5大包装类中(Byte,Charactor,Short,Integer,Long)增加了相应的私有静态成员内部类为相应包装类对象提供缓存机制,In ...

  2. unity3d为什么会有三种脚本语言?

    相信这个问题多多少少会令许多初学者感到困惑,因为他们不知道应该选择哪种语言好,但是都会从以下几个方面进行考虑: 1.学习成本.哪门语言让我快速上手. 2.文档帮助.说白了就是出了问题,有没有人能解决. ...

  3. mysql 命令备份

    导出要用到MySQL的mysqldump工具,基本用法是:    shell> mysqldump [OPTIONS] database [tables]    如果你不给定任何表,整个数据库将 ...

  4. 规范 : disable account

    前台的cookies在后台会去拿account出来,之后在filter status = disable的 用户在登入使用界面请求一个ajax,这时发现是401没有权限,这通常是admin把用户的ac ...

  5. 使用jQuery快速高效制作网页特效-----------------------------之jQuery事件与动画

    1.基础事件 分为三个事件 1.1 window事件 所谓window事件,就是当用户执行某些会影响浏览器的操作时,而触发的事件. 1.2 鼠标事件 鼠标事件顾名思义就是当用户在文档上移动或单击鼠标时 ...

  6. MySQL学习分享-->日期时间类型

    日期时间类型 ①如果要用来表示年月日时分秒,一般使用datetime类型: ②如果要用来表示年月日,一般使用date类型: ③如果要表示时分秒,一般使用time类型: ④如果只是表示年份,一般使用ye ...

  7. 出现http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException异常

    解决方案 1.在eclipse的菜单中,进入Window > Preferences > Java > Installed JREs > Execution Environme ...

  8. React-Native 开发(一) Android环境部署,Hello react-native

    前提: 一个小web前端,完全不会android 跟iOS 的开发,首次接触,有很多不懂的问题.请见谅 环境: win7 成果:                           一.SDK安装 提 ...

  9. PHP随机数安全

    0x00 rand()函数 rand()的随机数默认最大32767,可以用于爆破这里不再举例. 0x01 mt_rand()和mt_srand()函数 mt_srand()函数用于播种,PHP 4.2 ...

  10. 基于Modbus的C#串口调试开发

    说明:本文主要研究的是使用C# WinForm开发的串口调试软件(其中包含Modbus协议相关操作).Modbus相关协议可以查阅百度文库等,可参考: <http://wenku.baidu.c ...