一、关于事务回滚数据模型和数据库不对应问题
1.在使用事务时,无论是使用DbContextTransaction,还是使用TransactionScope,如果在事务中出现异常而回滚,都有可能出现这种情况,数据库数据已经回滚,但是实体模型缓存没有回滚。出现数据的不一致行。
2.这种情况出现的原因:
1.EF中对于查询的实体对象在内存中有缓存,用于数据的状态跟踪,提升性能。
2.在程序中使用相同的数据上下文。
二、解决方案
解决方案1.在程序中不使用同一个数据库上下文
也就是说事务回滚后,放弃当前使用上下文。
这种方式需要注意,对于事务最好使用TransactionScope,因为在同一DbContextTransaction事务中有两个上线文实例会出现死锁。
代码实例:
Test1 _context = new Test1();
Test1 _context2 = new Test1();
using (TransactionScope tran = new TransactionScope())
{
try
{
//1.修改省
Area province = _context.Areas.FirstOrDefault(q => q.AreaLevel == 1);
province.AreaName = province.AreaName + "1";
_context.SaveChanges();
Console.WriteLine(_context2.Areas.FirstOrDefault(q => q.AreaLevel == 1).AreaName);
//2.修改市
Area city = _context2.Areas.FirstOrDefault(q => q.AreaLevel == 2);
city.AreaName = city.AreaName + "1";
_context2.SaveChanges();
//抛出异常
throw new Exception("测试事务异常");
tran.Complete();
}
catch (Exception ex)
{
Console.WriteLine("执行出错:" + ex.Message);
}
}
//此位置获取的数据和数据库对应
Test1 _context3 = new Test1();
Console.WriteLine(_context3.Areas.FirstOrDefault(q => q.AreaLevel == 1).AreaName);
对于目前版本6.0,在事务执行失败后,放弃当前上下文,是EF官方给的一个方案,不知道将来是否会有更好的解决方案。
具体查看:https://msdn.microsoft.com/en-us/library/dn456833(v=vs.113).aspx
解决方案2:如果在事务异常后还要使用当前上下文,并且程序中数据量不是很大或者不需要太考虑性能的情况下,可以手动遍历所有的对象,设置为未跟踪来处理。
//手动修改所有缓存模型对象的状态为未跟踪
foreach (var item in _context.ChangeTracker.Entries())
{
item.State = System.Data.Entity.EntityState.Deleted;
}
实例代码:
Test1 _context = new Test1();
try
{
using (TransactionScope tran = new TransactionScope())
{
Area province = _context.Areas
.FirstOrDefault(q => q.AreaLevel == 1);
province.AreaName = province.AreaName + "1";
_context.SaveChanges();
Console.WriteLine(_context.Areas
.FirstOrDefault(q => q.AreaLevel == 1).AreaName);
throw new Exception("测试");
tran.Complete();
}
}
catch (Exception)
{
}
//手动修改所有缓存模型对象的状态为未跟踪
foreach (var item in _context.ChangeTracker.Entries())
{
item.State = System.Data.Entity.EntityState.Deleted;
}
Console.WriteLine(_context.Areas.AsNoTracking()
.FirstOrDefault(q => q.AreaLevel == 1).AreaName);
三、对于只有一个数据库上下文的解决方案实例
1.定义处理提交和回滚类,在回滚时手动清空跟踪状态
public class EFTransaction : IDisposable
{
/// <summary>
/// 当前事务对象
/// </summary>
private DbContextTransaction tran = null;
public EFTransaction(DbContextTransaction tran)
{
this.tran = tran;
}
/// <summary>
/// 提交
/// </summary>
public void Commit()
{
tran.Commit();
}
/// <summary>
/// 混滚操作
/// </summary>
public void Rollback()
{
Console.WriteLine("执行回滚操作");
tran.Rollback();
//执行其他处理
foreach (var item in Program._Context.ChangeTracker.Entries())
{
item.State = System.Data.Entity.EntityState.Detached;
}
}
public void Dispose()
{
Console.WriteLine("正在释放资源");
tran.Dispose();
}
}
2.在使用事务时,操作自定义类的对象
public static MenuModel _Context = new MenuModel();
static void TestSix()
{
//使用事务扩展
using (var tran = new EFTransaction(_Context.Database.BeginTransaction()))
{
try
{
Menu.Menu first = _Context.Menus.First();
Console.WriteLine(first.MenuName);
first.MenuName = "abc";
first.Model.ModelName = "123";
_Context.SaveChanges();
throw new Exception("测试异常");
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
}
}
Console.WriteLine(_Context.Menus.First().MenuName);//如果不清除跟踪状态,返回abc,和数据库不一致
}
四、在目前EF6.0的版本中,对于事务异常官方说明:
更新日期:2016年10月23日
一般来说,当连接失败时,回滚当前事务.。但是,如果在事务提交时连接被删除,则事务的结果状态未知.。看到这个博客张贴更多的细节。
目前EF不提供任何特殊的工具来处理这种情况。的sqlazureexecutionstrategy不会重试操作如果这样失败。
有几种方法来处理这个问题:
选项1 -什么都不做
在事务提交过程中连接失败的可能性很低,所以如果实际发生这种情况,您的应用程序可能会失败,这可能是可以接受的.。
选项2 -使用数据库重置状态
放弃当前的DbContext
创建一个新的DbContext和从数据库恢复应用程序的状态。
通知用户最近的操作可能没有成功地完成
选项3 -手动跟踪事务
将一个非跟踪表添加到用于跟踪事务状态的数据库中.。
在每个事务开始时将一行插入到表中.。
如果在提交过程中连接失败,请检查数据库中相应行的存在.。
如果该行存在,则继续正常运行,因为事务已成功提交.
如果行不存在,请使用执行策略重试当前操作.。
如果提交成功,请删除相应的行以避免表的增长.。
更多: