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

事務隔離級別

.NET Core中的IDbConnection接口提供了BeginTransaction方法做爲執行事務,BeginTransaction方法提供了兩個重載,一個不須要參數BeginTransaction()默認事務隔離級別爲RepeatableRead;另外一個BeginTransaction(IsolationLevel il)能夠根據業務需求來修改事務隔離級別。因爲Dapper是對IDbConnection的擴展,因此Dapper在執行增刪除改查時全部用到的事務須要由外部來定義。事務執行時與數據庫之間的交互以下:html

2017-12-19-22-43-23

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

準備工做

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

建立數據庫並建立數據表,建立數據表的腳本以下:數據庫

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類:markdown

[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未提交的數據。app

優勢:查詢速度快post

缺點:容易形成髒讀,若是事務A在中途回滾測試

如下爲執行髒讀的測試代碼片段:ui

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);
    }

}

一、當執行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事務1和事務2都設置爲ReadUncommitted時結果以下:

2017-12-22-22-06-49

當事務1回滾之後,數據庫並無事務1添加的數據,因此事務2獲取的數據是髒數據。

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

2017-12-22-22-07-28

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

2017-12-22-22-08-13

結論:當事務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和事務2都爲ReadCommitted時,結果以下:

2017-12-21-21-29-41

2017-12-22-22-08-38

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

2017-12-22-22-08-53

三、當事務1隔離級別爲RepeatableRead,事務2隔離級別爲ReadCommitted時執行結果以下:

2017-12-22-22-09-09

四、當事務1隔離級別爲ReadCommitted,事務2隔離級別爲RepeatableRead時執行結果以下:

2017-12-22-22-09-30

結論:當事務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和事務2隔離級別都爲RepeatableRead,結果以下:

2017-12-22-22-09-46

二、事務1和事務2隔離級別都爲Serializable,結果以下:

2017-12-22-22-10-02

三、當事務1的隔離級別爲Serializable,事務2的隔離級別爲RepeatableRead時,執行結果以下:

2017-12-22-22-10-18

四、當事務1的隔離級別爲RepeatableRead,事務2的隔離級別爲Serializable時,執行結果以下:

2017-12-22-22-10-32

結論:當事務隔離級別爲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);
}

執行結果以下:

2017-12-22-22-11-02

2017-12-22-22-11-13

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

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

2017-12-20-21-31-13

總結

本節經過Dapper對MySql中事務的四種隔離級別下進行測試,而且指出事務之間的相互關係和問題以供你們參考。

一、事務1隔離級別爲ReadUncommitted時,能夠讀取其它任何事務隔離級別下未提交的數據

二、事務1隔離級別爲ReadCommitted時,不能夠讀取其它事務未提交的數據,可是容許其它事務對數據表進行查詢、添加、修改和刪除;而且能夠將其它事務增刪改從新獲取出來。

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

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

髒讀 重複讀 幻讀
Read uncommitted
Read committed 不會
Repeatable read 不會 不會
Serializable 不會 不會 不會

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

相關文章
相關標籤/搜索