netcore中数据库事务隔离详解——以dapper和mysql为例(代码片段)

lonelyxmas lonelyxmas     2023-02-20     151

关键词:

原文:Net Core中数据库事务隔离详解——以Dapper和Mysql为例

事务隔离级别

.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 [email protected]", 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 [email protected]";
        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 [email protected] where [email protected]",
                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时,执行结果如下:

技术分享图片

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


mysql事务隔离级别详解

前两天面试,问到了四种隔离级别,当时觉得大多数数据库都为readcommitted,结果没想到mysql是个例外。在此做一下隔离级别和各种数据库锁的使用。首先说一下ACID四大特性:四大特性    · 原子性  事务... 查看详情

spring中的事务传播属性详解

...ON_DEFAULT这是一个PlatfromTransactionManager默认的隔离级别使用数据库默认的事 查看详情

spring事务详解初探讨

一、什么是事务维基百科:数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。理解:事务(Transaction)是数据库区别于文件系统的重要特性之一。传统关系型数据库设... 查看详情

mysql中事务四大特性的实现详解

...修改必须和其余的事务隔离持久性Duration:事务完成后,对数据库中数据的修改会持久化存储事务的四种隔离级别:隔离级别脏读不可重复读幻读未提交读ReadUncommitted可能可能可能已提交读ReadCommitted不可能可能可能可重复读RepeatableRe... 查看详情

mysql中事务四大特性的实现详解

...修改必须和其余的事务隔离持久性Duration:事务完成后,对数据库中数据的修改会持久化存储事务的四种隔离级别:隔离级别脏读不可重复读幻读未提交读ReadUncommitted可能可能可能已提交读ReadCommitted不可能可能可能可重复读RepeatableRe... 查看详情

mysql-innodb事务隔离级别-repeatableread详解

...net/dong976209075/article/details/8802778经验总结:Python使用MySQLdb数据库后,如使用多线程,每个线程创建一个db链接,然后再各自创建一个游标cursor,其中第一个线程读一个表中数据为空,第二个写入该表一条数据并提交,第一个线程... 查看详情

mysql事务隔离级别锁相关的详解

MySQLInnoDB引擎中的事务详解!事务是什么?事务就是可以使一组操作要么全部成功要么全部失败,事务的目的是为了保证数据的最终一致性。举个例子:张三给李四的微信转了1000块红包。那自然李四的微信余额会扣减1000块,而... 查看详情

事务的隔离级别

定义在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。用途事务的隔离级别是为了保证并发读取数据的正确性。我们的应用在并发处理请求时,对数据库的访问也是并发的,这样就会产生多个事务同... 查看详情

mysql事务详解

.../personsiglewine/p/11507866.htmlmysql事务在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。一般来说,事务需要满足4个条件(ACID)... 查看详情

2.事务隔离级别

...低,安全性依次提高。2.1事务隔离的实现原理在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级 查看详情

spring事务管理(详解+实例)

...-回滚规则-事务超时-是否只读   -只读有利于数据库优化###编程式事务和声明式事务----编程式事务更精确,声明式事务更简单-声明式事务是基于AOP的###引用----[Spring事务管理(详解+实例)](https://blog.csdn.net/trigl/article/d... 查看详情

mysql中事务以及事务隔离级别的详解(代码片段)

...分享一下给大家,👉点击跳转到网站一、MySQL中事务的理解二、演示事务的基本使用DROPTABLEt27;--1.创建一张测试表CREATETABLEt27(idINT,`name`VARCHAR(32));--2.开启事务STARTTRA 查看详情

mysql的事务详解(代码片段)

...重复读3.幻读四.MySQL的四个隔离级别一.事务的业务场景在数据库中事务(transac 查看详情

mysql事务隔离级别详解(代码片段)

...和收款人的总额应该是不变的。**隔离性:**并发访问数据库时,一个用户的事务不应该被其他事务所影响,各并发事务之间数据库是独立的。**持久性:**一个事务被提交后,它对数据库中数据的改变是持久的... 查看详情

sqlalchemy多线程下事务隔离机制详解(代码片段)

...之后,对订单对象的数据进行修改,然后执行commit,查询数据库发现数据没有更新,且后台日志没有任何的报错**错误代码:**fromapp.extimportdbfromflaskimportcurrent_appfromconcurrent.futuresimportThreadPoolExec 查看详情

spring有几种事务隔离级别?

Spring中的事务隔离级别和数据库中的事务隔离级别稍有不同,以MySQL为例,MySQL的InnoDB引擎中的事务隔离级别有4种,而Spring中却包含了5种事务隔离级别。1.什么是事务隔离级别?事务隔离级别是对事务4大特性中隔... 查看详情

redis事务详解(代码片段)

...事务隔离级别_YaoYong_BigData的博客-CSDN博客一、结合关系型数据库的事务来看看Redis中事务有什么不同Redis事务是指将多条命令加入队列,一次批量执行多条命令,每条命令会按顺序执行,事务执行过程中不会受客户端... 查看详情

数据库事务特性和隔离级别

1.事务的定义  数据库事务是数据库管理系统执行过程中的一个逻辑单位,有一个有限的数据库操作序列完成。以“A账户向B账户汇钱”为例,一个事务是下面一个操作序列:  a.从A账号中把余额读出来。  b.对A账号做减... 查看详情