一. 相关概念
前面系列中的章节的: 第二十二节: 以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):第一个事务读取第二个事务正在更新的数据,如果第二个事务还没有更新完成,那么第一个事务读取的数据将是一半为更新过的,一半还没更新过的数据。
(2).不可重复读(Unrepeatable Read):如果一个用户在一个事务中多次读取一条数据,而另外一个用户则同时更新啦这条数据,造成第一个用户多次读取数据不一致。
(3).幻读(Phantom Read):指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果集。
3. 案例测试
(前提:初始值userAge均为1000的且id为01 和 02 两条数据)
(1).脏读测试:事务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).不可重复读测试:事务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,避免了不可重复读。(但第二次数据和数据库已经不一样了,数据库中是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).幻读测试
有点问题,需要在什么场景下测试??
三. 死锁
详见开头之前的章节,此处不再重复介绍了
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。