EFCore 并发冲突
一、前言
首先我们来了解一下什么是并发冲突。
所谓的并发冲突就是,多个线程同时执行一个操作,例如同时修改数据表,导致数据变更后无法正常保存。
并发分为:悲观并发和乐观并发
悲观并发:两个线程同时修改数据库的同一张表,A进入修改,B就不能修改,只能等待A改完,B才能进入修改。
乐观并发:A修改,B也可以修改,如果在A保存之后B再保存他的修改,此时系统检测到数据库中记录与B刚进入时不一致,B保存时会抛出异常,修改失败。
乐观并发的基本出发点是:当保存数据的时候抱着一种乐观的态度,不期望发生并发冲突,即使万一发生并发冲突,也能捕捉到冲突异常,然后根据策略解决冲突。
EF使用的就是乐观并发,所以在做数据处理时我们特别注意。
接下来以我自己遇到的问题为例。
二、场景
在我的项目里有一个功能,第一步先删除数据,再添加数据,然后模拟两次使用这个功能,很不巧的是模拟的两次存在操作相同的数据,其中一次能够操作成功,但是另一次由于前面有写数据被删了,导致我第二次再想删就出现并发冲突了。
调用的时候出现这个错:Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
官网给出的解决方案是通过捕获 DbUpdateConcurrencyException 异常来解决,下面是官网给出的例子
官网:https://docs.microsoft.com/zh-cn/ef/core/saving/concurrency
using (var context = new PersonContext()) { // Fetch a person from database and change phone number var person = context.People.Single(p => p.PersonId == 1); person.PhoneNumber = "555-555-5555"; // Change the person's name in the database to simulate a concurrency conflict context.Database.ExecuteSqlRaw( "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1"); var saved = false; while (!saved) { try { // Attempt to save changes to the database context.SaveChanges(); saved = true; } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { if (entry.Entity is Person) { var proposedValues = entry.CurrentValues; //当前值 var databaseValues = entry.GetDatabaseValues(); //数据库中的值 foreach (var property in proposedValues.Properties) { var proposedValue = proposedValues[property]; var databaseValue = databaseValues[property]; // TODO: decide which value should be written to database // proposedValues[property] = <value to be saved>; } // Refresh original values to bypass next concurrency check entry.OriginalValues.SetValues(databaseValues); //将数据库中的值更新到内存原始数据中,这里可能会有疑问,为什么不是把当前值跟你进去,这是因为OriginalValues存的是原始值,也就是所有改动之前的数据 } else { throw new NotSupportedException( "Don't know how to handle concurrency conflicts for " + entry.Metadata.Name); } } } } }
要根据自己的实际情况去改造,因为我的操作中包含删除数据,所以GetDatabaseValues会出现为空的情况,所以我采用的是另一种方式
bool saved = false; while (!saved) { try { context.SaveChanges(); saved = true; } catch(DbUpdateConcurrencyException ex) { var s = ex.Entries.Single(); s.Reload(); //刷新数据,直到更新成功 } }
上段代码中的Reload方法就是去数据库查询一次实体的最新值更新到缓存中,在数据库中的操作可以看出来