一. 相关概念
前面系列中的章节的: 第二十二节: 以SQLServer为例介绍数据库自有的锁机制(共享锁、更新锁、排它锁等)和事务隔离级别 介绍了各种锁以及事务的隔离级别,是从数据库的角度进行介绍的,本章节是通过EF Core为载体,介绍事务隔离级别和相关问题,与上述章节有些许重复的内容。
1. 什么是事务
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。
2. 事务的特征
事务具有 4 个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称:ACID。
(1).原子性:指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一。 • 全部执行成功 • 全部执行失败,任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成.
(2).一致性:事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。比如:如果从 A 账户转账到 B 账户,不可能因为 A 账户扣了钱,而 B 账户没有加钱,无论 A 和 B 怎么转账,系统中总额是固定的,不可能因为 A 和 B 转账导致系统总额缺斤少两。
(3).隔离性:指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。(详见下面隔离级别)
(4).持久性:事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
二. 事务隔离级别以及引发的问题
1. 隔离级别
(1).读未提交(READ_UNCOMMITTED):读未提交,该隔离级别允许脏读取,其隔离级别是最低的。换句话说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务, 因此还没有提交事务;而以此同时,允许另一个事务也能够访问该数据。
【引发的问题:脏读】
(2).读已提交(READ_COMMITTED) :事务执行的时候只能获取到其它事务已经提交的数据,获取不到未提交的数据。
【解决了“脏读”,但是解决不了“不可重复读”】
(3).可重复读(REPEATABLE_READ):保证在事务处理过程中,多次读取同一个数据时,该数据的值和事务开始时刻是一致的。
【解决了“脏读”和“不可重复度”,但是解决不了“幻读”】
(4).顺序读(SERIALIZABLE):最严格的事务隔离级别。它要求所有的事务排队顺序执行,即事务只能一个接一个地处理,不能并发。
【解决上述所有情况】
注:4 种事务隔离级别从上往下,级别越高,并发性越差,安全性就越来越高。一般数据默认级别是读已提交或可重复读。
PS:常见数据库的默认级别:
①:MySQL 数据库的默认隔离级别是 Repeatable read 级别。
②:Oracle数据库中,只支持 Seralizable 和 Read committed级别,默认的是 Read committed 级别。
③:SQL Server 数据库中,默认的是 Read committed(读已提交) 级别。
2.引发的问题
(1).脏读(Dirty Read):一个事务读取另外一个事务还没有提交的数据叫脏读(事务T1修改了一行数据,但是还没有提交,这时候事务T2也读取了这条数据,当T2要对这个数据进行增加或修改操作并提交的时候,T1已经提交了,这个时候T2原先读取的数据和数据库现在的数据是不一样的,那么事务T2读取的数据就是脏的。)。
(2).不可重复读(Unrepeatable Read):如果一个用户在一个事务中多次读取一条数据,而另外一个用户则同时更新啦这条数据,造成第一个用户多次读取数据不一致。 (其他解释:不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。比如:事务T1读取某一数据,事务T2读取并修改了该数据,T1再次读取该数据,便得到了不同的结果。)
(3).幻读(Phantom Read):是指同一个事务内多次查询返回的“数据条数”不一样(比如增加了或者减少了行记录)。比如同一个事务A内第一次查询时候有n条记录,但是第二次同等条件下查询却又n+1条 记录,这就好像产生了幻觉,为啥两次结果不一样那。其实和不可重复读一样,发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据。不同在于不可重复读是同一个记录的数据内容被修改了,幻读是数据行记录变多了或者少了。
总结:
脏读是指一个事务读取到了其他事务没有提交的数据,不可重复读是指一个事务内多次根据同一个查询条件查询出来的“同一行记录的值不一样”,幻读是指一个事务内多次 根据同个条件查出来的记录行数不一样。为了解决事务并发带来的问题,才有了事务规范中的四个事务隔离级别,不同隔离级别对上面问题部分或者全部做了避免。
3. 案例测试
(1).脏读测试:
(前提:初始值userAge均为1000的且id为01 和 02 两条数据)
事务1两条数据分别减去500,正常的等待事务提交后,这两条数据的userAge的值应该均为500;将事务2设置成读未提交(IsolationLevel.ReadUncommitted 即允许脏读), 查出来的结果是:500,1000,即脏读数据。
代码分享
1 { 2 //1.事先准备删除所有数据,插入两条指定数据 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事务1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 var data1 = db.T_UserInfor.Find("01"); 19 data1.userAge -= 500; 20 db.SaveChanges(); 21 22 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 23 24 var data2 = db.T_UserInfor.Find("02"); 25 data2.userAge -= 500; 26 db.SaveChanges(); 27 28 transaction.Commit(); 29 30 } 31 catch (Exception ex) 32 { 33 34 Console.WriteLine(ex.Message); 35 } 36 } 37 } 38 }); 39 //事务2 40 Task.Run(() => 41 { 42 using (var db = new EFDB01Context()) 43 { 44 //设置成“读未提交” 45 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) 46 { 47 try 48 { 49 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 50 var data1 = db.T_UserInfor.Find("01"); 51 var data2 = db.T_UserInfor.Find("02"); 52 53 Console.WriteLine($"01 userAge is {data1.userAge}"); 54 Console.WriteLine($"02 userAge is {data2.userAge}"); 55 56 } 57 catch (Exception ex) 58 { 59 60 Console.WriteLine(ex.Message); 61 } 62 } 63 } 64 }); 65 }
避免脏读:将事务2设置成读已提交(IsolationLevel.ReadCommitted 或者不设置,SQLServer默认就是读已提交),则事务2需要等待事务1执行完才能读取,读出来的两条数据的均为500,即避免了脏读。
代码分享
1 { 2 //1.事先准备删除所有数据,插入两条指定数据 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事务1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 var data1 = db.T_UserInfor.Find("01"); 19 data1.userAge -= 500; 20 db.SaveChanges(); 21 22 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 23 24 var data2 = db.T_UserInfor.Find("02"); 25 data2.userAge -= 500; 26 db.SaveChanges(); 27 28 transaction.Commit(); 29 30 } 31 catch (Exception ex) 32 { 33 34 Console.WriteLine(ex.Message); 35 } 36 } 37 } 38 }); 39 //事务2 40 Task.Run(() => 41 { 42 using (var db = new EFDB01Context()) 43 { 44 //设置成“读已提交”,或者不设置,SQLServer默认就是读已提交 45 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadCommitted)) 46 { 47 try 48 { 49 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 50 var data1 = db.T_UserInfor.Find("01"); 51 var data2 = db.T_UserInfor.Find("02"); 52 53 Console.WriteLine($"01 userAge is {data1.userAge}"); 54 Console.WriteLine($"02 userAge is {data2.userAge}"); 55 56 } 57 catch (Exception ex) 58 { 59 60 Console.WriteLine(ex.Message); 61 } 62 } 63 } 64 }); 65 }
(2).不可重复读测试:
(前提:初始值userAge均为1000的且id为01 和 02 两条数据)
事务1中5s后将01数据的userAge的值由1000改为500,事务2中在“读已提交”的情况下两次读取的01的数据分别是1000,500,即为不可重复读。
代码分享
1 { 2 { 3 //1.事先准备删除所有数据,插入两条指定数据 4 using (EFDB01Context db = new EFDB01Context()) 5 { 6 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 8 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 9 } 10 //事务1 11 Task.Run(() => 12 { 13 using (var db = new EFDB01Context()) 14 { 15 using (var transaction = db.Database.BeginTransaction()) 16 { 17 try 18 { 19 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 20 21 var data1 = db.T_UserInfor.Find("01"); 22 data1.userAge -= 500; 23 db.SaveChanges(); 24 25 transaction.Commit(); 26 27 } 28 catch (Exception ex) 29 { 30 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事务2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //设置成“读已提交”,或者不设置,SQLServer默认就是读已提交 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadCommitted)) 43 { 44 try 45 { 46 //一定要加上这句,否则下面的第二个Find不读取数据库 47 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 48 49 var data1 = db.T_UserInfor.Find("01"); 50 Console.WriteLine($"01 userAge is {data1.userAge}"); 51 52 Task.Delay(TimeSpan.FromSeconds(6)).Wait(); 53 54 var data2 = db.T_UserInfor.Find("01"); 55 Console.WriteLine($"01 userAge is {data2.userAge}"); 56 57 } 58 catch (Exception ex) 59 { 60 61 Console.WriteLine(ex.Message); 62 } 63 } 64 } 65 }); 66 } 67 }
避免不可重复读:将事务2设置成“可重复读”(IsolationLevel.RepeatableRead),事务2两次读取的数据均为1000,避免了不可重复读。(但事务2第二次读取的数据和数据库实际数据已经不一样了,数据库中是500)
代码分享
1 { 2 { 3 //1.事先准备删除所有数据,插入两条指定数据 4 using (EFDB01Context db = new EFDB01Context()) 5 { 6 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 8 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 9 } 10 //事务1 11 Task.Run(() => 12 { 13 using (var db = new EFDB01Context()) 14 { 15 using (var transaction = db.Database.BeginTransaction()) 16 { 17 try 18 { 19 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 20 21 var data1 = db.T_UserInfor.Find("01"); 22 data1.userAge -= 500; 23 db.SaveChanges(); 24 25 transaction.Commit(); 26 27 } 28 catch (Exception ex) 29 { 30 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事务2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //设置成“可重复读” 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.RepeatableRead)) 43 { 44 try 45 { 46 //一定要加上这句,否则下面的第二个Find不读取数据库 47 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 48 49 var data1 = db.T_UserInfor.Find("01"); 50 Console.WriteLine($"01 userAge is {data1.userAge}"); 51 52 Task.Delay(TimeSpan.FromSeconds(6)).Wait(); 53 54 var data2 = db.T_UserInfor.Find("01"); 55 Console.WriteLine($"01 userAge is {data2.userAge}"); 56 57 } 58 catch (Exception ex) 59 { 60 61 Console.WriteLine(ex.Message); 62 } 63 } 64 } 65 }); 66 } 67 }
(3).幻读测试
(前提:初始值均为500和1000的两条数据)
事务1中5s后分别再次插入两条500的数据,事务2上去就读取userAge=500的数据,读取出来1条(即初始插入的那条),10s后再次读取userAge=500的数据,则读出来了3条, 事务2至始至终在同一个事务中并没有提交,针对同一条件却前后查出来了不同条数的数据,这就是幻读。
代码分享:
1 { 2 //1.事先准备删除所有数据,插入两条指定数据 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',500,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事务1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 19 20 db.Add(new T_UserInfor() { id = "03", userAge = 500 }); 21 db.SaveChanges(); 22 23 db.Add(new T_UserInfor() { id = "04", userAge = 500 }); 24 db.SaveChanges(); 25 26 transaction.Commit(); 27 28 } 29 catch (Exception ex) 30 { 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事务2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //设置成“可重复读” 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.RepeatableRead)) 43 { 44 try 45 { 46 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 47 48 var list1 = db.T_UserInfor.Where(a => a.userAge == 500); 49 Console.WriteLine("下面是数据"); 50 foreach (var d1 in list1) 51 { 52 Console.WriteLine($"{d1.id}---{d1.userAge}"); 53 } 54 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 55 56 var list2 = db.T_UserInfor.Where(a => a.userAge == 500); 57 Console.WriteLine("下面是数据"); 58 foreach (var d2 in list2) 59 { 60 Console.WriteLine($"{d2.id}---{d2.userAge}"); 61 } 62 63 } 64 catch (Exception ex) 65 { 66 67 Console.WriteLine(ex.Message); 68 } 69 } 70 } 71 }); 72 }
避免幻读:将事务2设置成“顺序读”(IsolationLevel.Serializable),事务2第一次读取出来1条数据,第二次也读取出来1条数据,避免了幻读现象。(但事务2第二次读取的条数和数据库实际条数已经不一样了,数据库中应该是3条)
代码分享:
1 { 2 //1.事先准备删除所有数据,插入两条指定数据 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',500,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事务1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 19 20 db.Add(new T_UserInfor() { id = "03", userAge = 500 }); 21 db.SaveChanges(); 22 23 db.Add(new T_UserInfor() { id = "04", userAge = 500 }); 24 db.SaveChanges(); 25 26 transaction.Commit(); 27 28 } 29 catch (Exception ex) 30 { 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事务2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //设置成“顺序读(SERIALIZABLE)” 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.Serializable)) 43 { 44 try 45 { 46 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 47 48 var list1 = db.T_UserInfor.Where(a => a.userAge == 500); 49 Console.WriteLine("下面是数据"); 50 foreach (var d1 in list1) 51 { 52 Console.WriteLine($"{d1.id}---{d1.userAge}"); 53 } 54 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 55 56 var list2 = db.T_UserInfor.Where(a => a.userAge == 500); 57 Console.WriteLine("下面是数据"); 58 foreach (var d2 in list2) 59 { 60 Console.WriteLine($"{d2.id}---{d2.userAge}"); 61 } 62 63 } 64 catch (Exception ex) 65 { 66 67 Console.WriteLine(ex.Message); 68 } 69 } 70 } 71 }); 72 }
参考链接:
https://www.jianshu.com/p/f7ac1b22e899
https://www.cnblogs.com/tudou1223/p/7954432.html
三. 死锁
详见开头之前的章节,此处不再重复介绍了
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。