Net Core中数据库事务隔离详解——以Dapper和Mysql为例
事務(wù)隔離級(jí)別
.NET Core中的IDbConnection接口提供了BeginTransaction方法作為執(zhí)行事務(wù),BeginTransaction方法提供了兩個(gè)重載,一個(gè)不需要參數(shù)BeginTransaction()默認(rèn)事務(wù)隔離級(jí)別為RepeatableRead;另一個(gè)BeginTransaction(IsolationLevel il)可以根據(jù)業(yè)務(wù)需求來修改事務(wù)隔離級(jí)別。由于Dapper是對(duì)IDbConnection的擴(kuò)展,所以Dapper在執(zhí)行增刪除改查時(shí)所有用到的事務(wù)需要由外部來定義。事務(wù)執(zhí)行時(shí)與數(shù)據(jù)庫(kù)之間的交互如下:
從WireShark抓取的數(shù)據(jù)包來看程序和數(shù)據(jù)交互步驟依次是:建立連接-->設(shè)置數(shù)據(jù)庫(kù)隔離級(jí)別-->告訴數(shù)據(jù)庫(kù)一個(gè)事務(wù)開始-->執(zhí)行數(shù)據(jù)增刪查改-->提交事務(wù)-->斷開連接
準(zhǔn)備工作
準(zhǔn)備數(shù)據(jù)庫(kù):Mysql (筆者這里是:MySql 5.7.20 社區(qū)版)
創(chuàng)建數(shù)據(jù)庫(kù)并創(chuàng)建數(shù)據(jù)表,創(chuàng)建數(shù)據(jù)表的腳本如下:
CREATE TABLE `posts` ( ?`Id` varchar(255) NOT NULL , ?`Text` longtext NOT NULL, ?`CreationDate` datetime NOT NULL, ?`LastChangeDate` datetime NOT NULL, ?`Counter1` int(11) DEFAULT NULL, ?`Counter2` int(11) DEFAULT NULL, ?`Counter3` int(11) DEFAULT NULL, ?`Counter4` int(11) DEFAULT NULL, ?`Counter5` int(11) DEFAULT NULL, ?`Counter6` int(11) DEFAULT NULL, ?`Counter7` int(11) DEFAULT NULL, ?`Counter8` int(11) DEFAULT NULL, ?`Counter9` int(11) DEFAULT NULL, ?PRIMARY KEY (`Id`) ) ENGINE=InnoDB ?DEFAULT CHARSET=utf8;創(chuàng)建.NET Core Domain類:
[Table("Posts")]public class Post{[Key] ? ?public string Id { get; set; } ? ?public string Text { get; set; } ? ?public DateTime CreationDate { get; set; } ? ?public DateTime LastChangeDate { get; set; } ? ?public int? Counter1 { get; set; } ? ?public int? Counter2 { get; set; } ? ?public int? Counter3 { get; set; } ? ?public int? Counter4 { get; set; } ? ?public int? Counter5 { get; set; } ? ?public int? Counter6 { get; set; } ? ?public int? Counter7 { get; set; } ? ?public int? Counter8 { get; set; } ? ?public int? Counter9 { get; set; }}具體怎樣使用Dapper,請(qǐng)看上篇。
Read uncommitted 讀未提交
允許臟讀,即不發(fā)布共享鎖,也不接受獨(dú)占鎖。意思是:事務(wù)A可以讀取事務(wù)B未提交的數(shù)據(jù)。
優(yōu)點(diǎn):查詢速度快
缺點(diǎn):容易造成臟讀,如果事務(wù)A在中途回滾
以下為執(zhí)行臟讀的測(cè)試代碼片斷:
public static void RunDirtyRead(IsolationLevel transaction1Level,IsolationLevel transaction2Level){ ? ?var id = Guid.NewGuid().ToString(); ? ?using (var connection1 = new MySqlConnection(connStr)){connection1.Open();Console.WriteLine("transaction1 {0} Start",transaction1Level); ? ? ? ?var transaction1 = connection1.BeginTransaction(transaction1Level);Console.WriteLine("transaction1 插入數(shù)據(jù) Start"); ? ? ? ?var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)"; ? ? ? ?var detail1 = connection1.Execute(sql, ? ? ? ?new Post{Id = id,Text = Guid.NewGuid().ToString(),CreationDate = DateTime.Now,LastChangeDate = DateTime.Now},transaction1);Console.WriteLine("transaction1 插入End 返回受影響的行:{0}", detail1); ? ? ? ?using (var connection2 = new MySqlConnection(connStr)){connection2.Open();Console.WriteLine("transaction2 {0} Start",transaction2Level); ? ? ? ? ? ?var transaction2 = connection2.BeginTransaction(transaction2Level);Console.WriteLine("transaction2 查詢數(shù)據(jù) Start"); ? ? ? ? ? ?var result = connection2.QueryFirstOrDefault<Post>("select * from posts where id=@Id", new { id = id }, transaction2); ? ? ? ? ? ?//如果result為Null 則程序會(huì)報(bào)異常Console.WriteLine("transaction2 查詢結(jié)事 返回結(jié)果:Id={0},Text={1}", result.Id, result.Text);transaction2.Commit();Console.WriteLine("transaction2 {0} End",transaction2Level);}transaction1.Rollback();Console.WriteLine("transaction1 {0} Rollback ",transaction1Level);}}1、當(dāng)執(zhí)行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事務(wù)1和事務(wù)2都設(shè)置為ReadUncommitted時(shí)結(jié)果如下:
當(dāng)事務(wù)1回滾以后,數(shù)據(jù)庫(kù)并沒有事務(wù)1添加的數(shù)據(jù),所以事務(wù)2獲取的數(shù)據(jù)是臟數(shù)據(jù)。
2、當(dāng)執(zhí)行RunDirtyRead(IsolationLevel.Serializable,IsolationLevel.ReadUncommitted),即事務(wù)1隔離級(jí)別為Serializble,事務(wù)2的隔離級(jí)別設(shè)置為ReadUncommitted,結(jié)果如下:
3、當(dāng)執(zhí)行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadCommitted);,即事務(wù)1隔離級(jí)別為ReadUncommitted,事務(wù)2的隔離級(jí)別為Readcommitted,結(jié)果如下:
結(jié)論:當(dāng)事務(wù)2(即取數(shù)據(jù)事務(wù))隔離級(jí)別設(shè)置為ReadUncommitted,那么不管事務(wù)1隔離級(jí)別為哪一種,事務(wù)2都能將事務(wù)1未提交的數(shù)據(jù)得到;但是測(cè)試結(jié)果可以看出當(dāng)事務(wù)2為ReadCommitted則獲取不到事務(wù)1未提交的數(shù)據(jù)從而導(dǎo)致程序異常。
Read committed 讀取提交內(nèi)容
這是大多數(shù)數(shù)據(jù)庫(kù)默認(rèn)的隔離級(jí)別,但是,不是MySQL的默認(rèn)隔離級(jí)別。讀取數(shù)據(jù)時(shí)保持共享鎖,以避免臟讀,但是在事務(wù)結(jié)束前可以更改數(shù)據(jù)。
優(yōu)點(diǎn):解決了臟讀的問題
缺點(diǎn):一個(gè)事務(wù)未結(jié)束被另一個(gè)事務(wù)把數(shù)據(jù)修改后導(dǎo)致兩次請(qǐng)求的數(shù)據(jù)不一致
測(cè)試重復(fù)讀代碼片斷:
public static void RunRepeatableRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level){ ? ?using (var connection1 = new MySqlConnection(connStr)){connection1.Open(); ? ? ? ?var id = "c8de065a-3c71-4273-9a12-98c8955a558d";Console.WriteLine("transaction1 {0} Start", transaction1Level); ? ? ? ?var transaction1 = connection1.BeginTransaction(transaction1Level);Console.WriteLine("transaction1 第一次查詢開始"); ? ? ? ?var sql = "select * from posts where id=@Id"; ? ? ? ?var detail1 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);Console.WriteLine("transaction1 第一次查詢結(jié)束,結(jié)果:Id={0},Counter1={1}", detail1.Id, detail1.Counter1); ? ? ? ?using (var connection2 = new MySqlConnection(connStr)){connection2.Open();Console.WriteLine("transaction2 ?{0} Start", transaction2Level); ? ? ? ? ? ?var transaction2 = connection2.BeginTransaction(transaction2Level); ? ? ? ? ? ?var updateCounter1=(detail1.Counter1 ?? 0) + 1;Console.WriteLine("transaction2 ?開始修改Id={0}中Counter1的值修改為:{1}", id,updateCounter1); ? ? ? ? ? ?var result = connection2.Execute( ? ? ? ? ? ? ? ?"update posts set Counter1=@Counter1 where id=@Id", ? ? ? ? ? ? ? ?new { Id = id, Counter1 = updateCounter1 },transaction2);Console.WriteLine("transaction2 修改完成 返回受影響行:{0}", result);transaction2.Commit();Console.WriteLine("transaction2 {0} End", transaction2Level);}Console.WriteLine("transaction1 第二次查詢 Start"); ? ? ? ?var detail2 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);Console.WriteLine("transaction1 第二次查詢 End 結(jié)果:Id={0},Counter1={1}", detail2.Id, detail2.Counter1);transaction1.Commit();Console.WriteLine("transaction1 {0} End", transaction1Level);} }在事務(wù)1中detail1中得到的Counter1為1,事務(wù)2中將Counter1的值修改為2,事務(wù)1中detail2得到的Counter1的值也會(huì)變?yōu)?
下面分幾種情況來測(cè)試:
1、當(dāng)事務(wù)1和事務(wù)2都為ReadCommitted時(shí),結(jié)果如下:
2、當(dāng)事務(wù)1和事務(wù)2隔離級(jí)別都為RepeatableRead時(shí),執(zhí)行結(jié)果如下:
3、當(dāng)事務(wù)1隔離級(jí)別為RepeatableRead,事務(wù)2隔離級(jí)別為ReadCommitted時(shí)執(zhí)行結(jié)果如下:
4、當(dāng)事務(wù)1隔離級(jí)別為ReadCommitted,事務(wù)2隔離級(jí)別為RepeatableRead時(shí)執(zhí)行結(jié)果如下:
結(jié)論:當(dāng)事務(wù)1隔離級(jí)別為ReadCommitted時(shí)數(shù)據(jù)可重復(fù)讀,當(dāng)事務(wù)1隔離級(jí)別為RepeatableRead時(shí)可以不可重復(fù)讀,不管事務(wù)2隔離級(jí)別為哪一種不受影響。
注:在RepeatableRead隔離級(jí)別下雖然事務(wù)1兩次獲取的數(shù)據(jù)一致,但是事務(wù)2已經(jīng)是將數(shù)據(jù)庫(kù)中的數(shù)據(jù)進(jìn)行了修改,如果事務(wù)1對(duì)該條數(shù)據(jù)進(jìn)行修改則會(huì)對(duì)事務(wù)2的數(shù)據(jù)進(jìn)行覆蓋。
Repeatable read (可重讀)
這是MySQL默認(rèn)的隔離級(jí)別,它確保同一事務(wù)的多個(gè)實(shí)例在并發(fā)讀取數(shù)據(jù)時(shí),會(huì)看到同樣的數(shù)據(jù)行(目標(biāo)數(shù)據(jù)行不會(huì)被修改)。
優(yōu)點(diǎn):解決了不可重復(fù)讀和臟讀問題
缺點(diǎn):幻讀
測(cè)試幻讀代碼
public static void RunPhantomRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level){ ? ?using (var connection1 = new MySqlConnection(connStr)){connection1.Open();Console.WriteLine("transaction1 {0} Start", transaction1Level); ? ?
? ?var transaction1 = connection1.BeginTransaction(transaction1Level);Console.WriteLine("transaction1 第一次查詢數(shù)據(jù)庫(kù) Start"); ? ? ?
? ??var detail1 = connection1.Query<Post>("select * from posts").ToList();Console.WriteLine("transaction1 第一次查詢數(shù)據(jù)庫(kù) End 查詢條數(shù):{0}", detail1.Count); ? ?
? ?? ? ?using (var connection2 = new MySqlConnection(connStr)){connection2.Open();Console.WriteLine("transaction2 {0} Start", transaction2Level); ? ? ? ? ?
? ?? ? ??var transaction2 = connection2.BeginTransaction(transaction2Level);Console.WriteLine("transaction2 執(zhí)行插入數(shù)據(jù) Start"); ? ? ? ? ?
? ?? ? ?? ?var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)"; ? ? ? ? ? ?var entity = new Post{Id = Guid.NewGuid().ToString(),Text = Guid.NewGuid().ToString(),CreationDate = DateTime.Now,LastChangeDate = DateTime.Now}; ? ? ? ? ? ?var result = connection2.Execute(sql, entity, transaction2);Console.WriteLine("transaction2 執(zhí)行插入數(shù)據(jù) End 返回受影響行:{0}", result);transaction2.Commit();Console.WriteLine("transaction2 {0} End", transaction2Level);}Console.WriteLine("transaction1 第二次查詢數(shù)據(jù)庫(kù) Start"); ? ? ?
? ?? ??var detail2 = connection1.Query<Post>("select * from posts").ToList();Console.WriteLine("transaction1 第二次查詢數(shù)據(jù)庫(kù) End 查詢條數(shù):{0}", detail2.Count);transaction1.Commit();Console.WriteLine("transaction1 {0} End", transaction1Level);} }
分別對(duì)幾種情況進(jìn)行測(cè)試:
1、事務(wù)1和事務(wù)2隔離級(jí)別都為RepeatableRead,結(jié)果如下:
2、事務(wù)1和事務(wù)2隔離級(jí)別都為Serializable,結(jié)果如下:
3、當(dāng)事務(wù)1的隔離級(jí)別為Serializable,事務(wù)2的隔離級(jí)別為RepeatableRead時(shí),執(zhí)行結(jié)果如下:
4、當(dāng)事務(wù)1的隔離級(jí)別為RepeatableRead,事務(wù)2的隔離級(jí)別為Serializable時(shí),執(zhí)行結(jié)果如下:
結(jié)論:當(dāng)事務(wù)隔離級(jí)別為RepeatableRead時(shí)雖然兩次獲取數(shù)據(jù)條數(shù)相同,但是事務(wù)2是正常將數(shù)據(jù)插入到數(shù)據(jù)庫(kù)當(dāng)中的。當(dāng)事務(wù)1隔離級(jí)別為Serializable程序異常,原因接下來將會(huì)講到。
Serializable 序列化
這是最高的事務(wù)隔離級(jí)別,它通過強(qiáng)制事務(wù)排序,使之不可能相互沖突,從而解決幻讀問題。
優(yōu)點(diǎn):解決幻讀
缺點(diǎn):在每個(gè)讀的數(shù)據(jù)行上都加了共享鎖,可能導(dǎo)致大量的超時(shí)和鎖競(jìng)爭(zhēng)
當(dāng)執(zhí)行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.Serializable)或執(zhí)行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.RepeatableRead)時(shí)代碼都會(huì)報(bào)異常,是因?yàn)镾erializable隔離級(jí)別下強(qiáng)制事務(wù)以串行方式執(zhí)行,由于這里是一個(gè)主線程上第一個(gè)事務(wù)未完時(shí)執(zhí)行了第二個(gè)事務(wù),但是第二個(gè)事務(wù)必須等到第一個(gè)事務(wù)執(zhí)行完成后才參執(zhí)行,所以就會(huì)導(dǎo)致程序報(bào)超時(shí)異常。這里將代碼作如下修改:
using (var connection1 = new MySqlConnection(connStr)) {connection1.Open();Console.WriteLine("transaction1 {0} Start", transaction1Level); ??var transaction1 = connection1.BeginTransaction(transaction1Level);Console.WriteLine("transaction1 第一次查詢數(shù)據(jù)庫(kù) Start"); ?
??var detail1 = connection1.Query<Post>("select * from posts").ToList();Console.WriteLine("transaction1 第一次查詢數(shù)據(jù)庫(kù) End 查詢條數(shù):{0}", detail1.Count);Thread thread = new Thread(new ThreadStart(() =>{ ? ? ? ?using (var connection2 = new MySqlConnection(connStr)){connection2.Open();Console.WriteLine("transaction2 {0} Start", transaction2Level); ? ?
?? ? ? ? ?var transaction2 = connection2.BeginTransaction(transaction2Level);Console.WriteLine("transaction2 執(zhí)行插入數(shù)據(jù) Start"); ? ?
?? ? ? ? ???var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)"; ? ? ? ? ?
?? ? ? ? ? ?var entity = new Post{Id = Guid.NewGuid().ToString(),Text = Guid.NewGuid().ToString(),CreationDate = DateTime.Now,LastChangeDate = DateTime.Now}; ? ? ? ? ? ?var result = connection2.Execute(sql, entity, transaction2);Console.WriteLine("transaction2 執(zhí)行插入數(shù)據(jù) End 返回受影響行:{0}", result);transaction2.Commit();Console.WriteLine("transaction2 {0} End", transaction2Level);}}));thread.Start(); ? ?//為了證明兩個(gè)事務(wù)是串行執(zhí)行的,這里讓主線程睡5秒Thread.Sleep(5000);Console.WriteLine("transaction1 第二次查詢數(shù)據(jù)庫(kù) Start"); ?
?? ?var detail2 = connection1.Query<Post>("select * from posts").ToList();Console.WriteLine("transaction1 第二次查詢數(shù)據(jù)庫(kù) End 查詢條數(shù):{0}", detail2.Count);transaction1.Commit();Console.WriteLine("transaction1 {0} End", transaction1Level); }
執(zhí)行結(jié)果如下:
結(jié)論:當(dāng)事務(wù)1隔離級(jí)別為Serializable時(shí)對(duì)后面的事務(wù)的增刪改改操作進(jìn)行強(qiáng)制排序。避免數(shù)據(jù)出錯(cuò)造成不必要的麻煩。
注:在.NET Core中IsolationLevel枚舉值中還提供了另外三種隔離級(jí)別:Chaos、Snapshot、Unspecified由于這種事務(wù)隔離級(jí)別MySql不支持設(shè)置時(shí)會(huì)報(bào)異常:
總結(jié)
本節(jié)通過Dapper對(duì)MySql中事務(wù)的四種隔離級(jí)別下進(jìn)行測(cè)試,并且指出事務(wù)之間的相互關(guān)系和問題以供大家參考。
1、事務(wù)1隔離級(jí)別為ReadUncommitted時(shí),可以讀取其它任何事務(wù)隔離級(jí)別下未提交的數(shù)據(jù)
2、事務(wù)1隔離級(jí)別為ReadCommitted時(shí),不可以讀取其它事務(wù)未提交的數(shù)據(jù),但是允許其它事務(wù)對(duì)數(shù)據(jù)表進(jìn)行查詢、添加、修改和刪除;并且可以將其它事務(wù)增刪改重新獲取出來。
3、事務(wù)1隔離級(jí)別為RepeatableRead時(shí),不可以讀取其它事務(wù)未提交的數(shù)據(jù),但是允許其它事務(wù)對(duì)數(shù)據(jù)表進(jìn)行查詢、添加、修改和刪除;但是其它事務(wù)的增刪改不影響事務(wù)1的查詢結(jié)果
4、事務(wù)1隔離級(jí)別為Serializable時(shí),對(duì)其它事務(wù)對(duì)數(shù)據(jù)庫(kù)的修改(增刪改)強(qiáng)制串行處理。
| Read uncommitted | 會(huì) | 會(huì) | 會(huì) |
| Read committed | 不會(huì) | 會(huì) | 會(huì) |
| Repeatable read | 不會(huì) | 不會(huì) | 會(huì) |
| Serializable | 不會(huì) | 不會(huì) | 不會(huì) |
原文地址:http://www.cnblogs.com/vipyoumay/p/8134434.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號(hào)文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的Net Core中数据库事务隔离详解——以Dapper和Mysql为例的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 采用Opserver来监控你的ASP.N
- 下一篇: EntityFramework Core