前言

之前有关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. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

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

  3. Java并发关键字Volatile 详解

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

  4. EntityFramework Core并发导致显示插入主键问题

    前言 之前讨论过EntityFramework Core中并发问题,按照官网所给并发冲突解决方案以为没有什么问题,但是在做单元测试时发现too young,too siimple,下面我们一起来看看. ...

  5. EntityFramework Core并发导致显式插入主键问题

    前言 之前讨论过EntityFramework Core中并发问题,按照官网所给并发冲突解决方案以为没有什么问题,但是在做单元测试时发现too young,too simple,下面我们一起来看看. ...

  6. java并发编程 | 线程详解

    个人网站:https://chenmingyu.top/concurrent-thread/ 进程与线程 进程:操作系统在运行一个程序的时候就会为其创建一个进程(比如一个java程序),进程是资源分配 ...

  7. Oracle的数据并发与一致性详解(下)

    上篇介绍了数据并发与一致性的相关概念.以及oracle的事务隔离级别等内容,本篇继续介绍锁机制.自动锁.手动锁.用户自定义锁的相关内容. 请尊重作者劳动成果,转载请标明原文链接: https://ww ...

  8. Oracle的数据并发与一致性详解(上)

    今天想了解下oracle中事务与锁的原理,但百度了半天,发现网上介绍的内容要么太短,要么版本太旧,而且抄袭现象严重,所以干脆查官方帮助文档(oracle 11.2),并将其精华整理成中文,供大家一起学 ...

  9. spring学习笔记---数据库事务并发与锁详解

    多事务运行并发问题 在实际应用中,往往是一台(或多台)服务器向无数客户程序提供服务,当服务器查询数据库获取数据时,如果没有采用必要的隔离机制,可能会存在数据库事务的并发问题,下面是一些常见的并发问题分 ...

随机推荐

  1. 「Android」单例的五种写法

    单例 发现博客园可以很好的设置自己的博客文章的展示,很开心,然后特此发一篇 其实这几种写法大家应该都会的,就权当拿来记录一下吧,以后复习巩固也比较方便. 这篇文章中的代码,来自一篇视频(我想找视频贴上 ...

  2. django rest framework 项目创建

    Django Rest Framework 是一个强大且灵活的工具包,用以构建Web API 为什么要使用Rest Framework Django REST Framework可以在Django的基 ...

  3. (后端)mybatis 模糊查询 mapper.xml的写法(转)

    原文地址:https://blog.csdn.net/sc6231565/article/details/46412765 1. sql中字符串拼接 SELECT * FROM tableName W ...

  4. 关于前端js面向对象编程以及封装组件的思想

    demo-richbase 用来演示怎么使用richbase来制作组件的例子 作为一名前端工程师,写组件的能力至关重要.虽然javascript经常被人嘲笑是个小玩具,但是在一代代大牛的前仆后继的努力 ...

  5. Orchard详解--第四篇 缓存介绍

    Orchard提供了多级缓存支持,它们分别是: 1. 应用程序配置级缓存ICacheManager: 它用来存储应用程序的配置信息并且可以提供一组可扩展的参数来处理缓存过期问题,在Orchard中默认 ...

  6. java 根据实体对象生成 增删改的SQL语句 ModelToSQL

    package com.xxx.utils; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import jav ...

  7. EOS智能合约开发(二):EOS创建和管理钱包

    上节介绍了EOS智能合约开发之EOS环境搭建及启动节点 那么,节点启动后我们要做的第一件事儿是什么呢?就是我们首先要有账号,但是有账号的前提是什么呢?倒不是先创建账号,而是先要有自己的一组私钥,有了私 ...

  8. 百度地图在web中的使用(一)

    百度地图在web中的使用(js) 背景:在公司做一个地理位置的自定义字段,需要用到地图来获取经纬度和地址,在这选择了百度地图 准备工作 注册百度地图开发者,创建应用获取key http://lbsyu ...

  9. php curl参数详解之post方法

    利用记录的URL参数解释,写一个post方法: <?php function do_post($url, $data) { $ch = curl_init(); //设置CURLOPT_RETU ...

  10. 超简单的gif图制作工具

    测试成功了: 软件灵者Gif录制1.0 使用方法注意: 扩展: 安卓端想要制作gif可参考此方法(还没测试):https://blog.csdn.net/u012604745/article/deta ...