1. 程式人生 > >Net Core中數據庫事務隔離詳解——以Dapper和Mysql為例

Net Core中數據庫事務隔離詳解——以Dapper和Mysql為例

事務 ring 增刪改 tostring 測試 stc efault 多個 log

  • Net Core中數據庫事務隔離詳解——以Dapper和Mysql為例
    • 事務隔離級別
      • 準備工作
      • Read uncommitted 讀未提交
      • Read committed 讀取提交內容
      • Repeatable read (可重讀)
      • Serializable 序列化
    • 總結

事務隔離級別

.NET Core中的IDbConnection接口提供了BeginTransaction方法作為執行事務,BeginTransaction方法提供了兩個重載,一個不需要參數BeginTransaction()默認事務隔離級別為RepeatableRead;另一個BeginTransaction(IsolationLevel il)

可以根據業務需求來修改事務隔離級別。由於Dapper是對IDbConnection的擴展,所以Dapper在執行增刪除改查時所有用到的事務需要由外部來定義。事務執行時與數據庫之間的交互如下:

技術分享圖片

從WireShark抓取的數據包來看程序和數據交互步驟依次是:建立連接-->設置數據庫隔離級別-->告訴數據庫一個事務開始-->執行數據增刪查改-->提交事務-->斷開連接

準備工作

準備數據庫:Mysql (筆者這裏是:MySql 5.7.20 社區版)

創建數據庫並創建數據表,創建數據表的腳本如下:


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;

創建.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,請看上篇。

Read uncommitted 讀未提交

允許臟讀,即不發布共享鎖,也不接受獨占鎖。意思是:事務A可以讀取事務B未提交的數據。

優點:查詢速度快

缺點:容易造成臟讀,如果事務A在中途回滾

以下為執行臟讀的測試代碼片斷:


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 插入數據 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 查詢數據 Start");
            var result = connection2.QueryFirstOrDefault<Post>("select * from posts where id=@Id", new { id = id }, transaction2);
            //如果result為Null 則程序會報異常
            Console.WriteLine("transaction2 查詢結事 返回結果: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、當執行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事務1和事務2都設置為ReadUncommitted時結果如下:

技術分享圖片

當事務1回滾以後,數據庫並沒有事務1添加的數據,所以事務2獲取的數據是臟數據。

2、當執行RunDirtyRead(IsolationLevel.Serializable,IsolationLevel.ReadUncommitted),即事務1隔離級別為Serializble,事務2的隔離級別設置為ReadUncommitted,結果如下:

技術分享圖片

3、當執行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadCommitted);,即事務1隔離級別為ReadUncommitted,事務2的隔離級別為Readcommitted,結果如下:

技術分享圖片

結論:當事務2(即取數據事務)隔離級別設置為ReadUncommitted,那麽不管事務1隔離級別為哪一種,事務2都能將事務1未提交的數據得到;但是測試結果可以看出當事務2為ReadCommitted則獲取不到事務1未提交的數據從而導致程序異常。

Read committed 讀取提交內容

這是大多數數據庫默認的隔離級別,但是,不是MySQL的默認隔離級別。讀取數據時保持共享鎖,以避免臟讀,但是在事務結束前可以更改數據。

優點:解決了臟讀的問題

缺點:一個事務未結束被另一個事務把數據修改後導致兩次請求的數據不一致

測試重復讀代碼片斷:


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 第一次查詢結束,結果: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 結果:Id={0},Counter1={1}", detail2.Id, detail2.Counter1);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

在事務1中detail1中得到的Counter1為1,事務2中將Counter1的值修改為2,事務1中detail2得到的Counter1的值也會變為2

下面分幾種情況來測試:

1、當事務1和事務2都為ReadCommitted時,結果如下:

技術分享圖片

技術分享圖片

2、當事務1和事務2隔離級別都為RepeatableRead時,執行結果如下:

技術分享圖片s

3、當事務1隔離級別為RepeatableRead,事務2隔離級別為ReadCommitted時執行結果如下:

技術分享圖片

4、當事務1隔離級別為ReadCommitted,事務2隔離級別為RepeatableRead時執行結果如下:

技術分享圖片

結論:當事務1隔離級別為ReadCommitted時數據可重復讀,當事務1隔離級別為RepeatableRead時可以不可重復讀,不管事務2隔離級別為哪一種不受影響。

註:在RepeatableRead隔離級別下雖然事務1兩次獲取的數據一致,但是事務2已經是將數據庫中的數據進行了修改,如果事務1對該條數據進行修改則會對事務2的數據進行覆蓋。

Repeatable read (可重讀)

這是MySQL默認的隔離級別,它確保同一事務的多個實例在並發讀取數據時,會看到同樣的數據行(目標數據行不會被修改)。

優點:解決了不可重復讀和臟讀問題

缺點:幻讀

測試幻讀代碼

 
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 第一次查詢數據庫 Start");
        var detail1 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第一次查詢數據庫 End 查詢條數:{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 執行插入數據 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 執行插入數據 End 返回受影響行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查詢數據庫 Start");
        var detail2 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第二次查詢數據庫 End 查詢條數:{0}", detail2.Count);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

分別對幾種情況進行測試:

1、事務1和事務2隔離級別都為RepeatableRead,結果如下:

技術分享圖片

2、事務1和事務2隔離級別都為Serializable,結果如下:

技術分享圖片

3、當事務1的隔離級別為Serializable,事務2的隔離級別為RepeatableRead時,執行結果如下:

技術分享圖片

4、當事務1的隔離級別為RepeatableRead,事務2的隔離級別為Serializable時,執行結果如下:

技術分享圖片

結論:當事務隔離級別為RepeatableRead時雖然兩次獲取數據條數相同,但是事務2是正常將數據插入到數據庫當中的。當事務1隔離級別為Serializable程序異常,原因接下來將會講到。

Serializable 序列化

這是最高的事務隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。

優點:解決幻讀

缺點:在每個讀的數據行上都加了共享鎖,可能導致大量的超時和鎖競爭

當執行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.Serializable)或執行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.RepeatableRead)時代碼都會報異常,是因為Serializable隔離級別下強制事務以串行方式執行,由於這裏是一個主線程上第一個事務未完時執行了第二個事務,但是第二個事務必須等到第一個事務執行完成後才參執行,所以就會導致程序報超時異常。這裏將代碼作如下修改:


using (var connection1 = new MySqlConnection(connStr))
{
    connection1.Open();
    Console.WriteLine("transaction1 {0} Start", transaction1Level);
    var transaction1 = connection1.BeginTransaction(transaction1Level);
    Console.WriteLine("transaction1 第一次查詢數據庫 Start");
    var detail1 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第一次查詢數據庫 End 查詢條數:{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 執行插入數據 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 執行插入數據 End 返回受影響行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
    }));
    thread.Start();
    //為了證明兩個事務是串行執行的,這裏讓主線程睡5秒
    Thread.Sleep(5000);
    Console.WriteLine("transaction1 第二次查詢數據庫 Start");
    var detail2 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第二次查詢數據庫 End 查詢條數:{0}", detail2.Count);
    transaction1.Commit();
    Console.WriteLine("transaction1 {0} End", transaction1Level);
}

執行結果如下:

技術分享圖片

技術分享圖片

結論:當事務1隔離級別為Serializable時對後面的事務的增刪改改操作進行強制排序。避免數據出錯造成不必要的麻煩。

註:在.NET Core中IsolationLevel枚舉值中還提供了另外三種隔離級別:ChaosSnapshotUnspecified由於這種事務隔離級別MySql不支持設置時會報異常:

技術分享圖片

總結

本節通過Dapper對MySql中事務的四種隔離級別下進行測試,並且指出事務之間的相互關系和問題以供大家參考。

1、事務1隔離級別為ReadUncommitted時,可以讀取其它任何事務隔離級別下未提交的數據

2、事務1隔離級別為ReadCommitted時,不可以讀取其它事務未提交的數據,但是允許其它事務對數據表進行查詢、添加、修改和刪除;並且可以將其它事務增刪改重新獲取出來。

3、事務1隔離級別為RepeatableRead時,不可以讀取其它事務未提交的數據,但是允許其它事務對數據表進行查詢、添加、修改和刪除;但是其它事務的增刪改不影響事務1的查詢結果

4、事務1隔離級別為Serializable時,對其它事務對數據庫的修改(增刪改)強制串行處理。

臟讀 重復讀 幻讀
Read uncommitted
Read committed 不會
Repeatable read 不會 不會
Serializable 不會 不會 不會

作者:xdpie 出處:http://www.cnblogs.com/vipyoumay/p/8134434.html

Net Core中數據庫事務隔離詳解——以Dapper和Mysql為例