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

  • 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 [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时,执行结果如下:

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

原文地址:https://www.cnblogs.com/vipyoumay/p/8134434.html

时间: 2024-10-12 21:41:50

Net Core中数据库事务隔离详解——以Dapper和Mysql为例的相关文章

数据库 查询方法详解 以学生老师信息表为例

create table Student--3rd再次执行 ( Sno int primary key not null,--学号主键 Sname varchar(50) not null,--学生姓名 Ssex varchar(50) not null,--学生性别 Sbirthday datetime,--出生年月 Class int--班级 ) truncate table Student--清空表格 insert into Student values(108,'曾华','男','197

SQLServer中服务器角色和数据库角色权限详解

SQLServer中服务器角色和数据库角色权限详解 by e-online 26. 九月 2009 18:55 coming from http://blog.csdn.net/e_online/article/details/4597957 角色 当几个用户需要在某个特定的数据库中执行类似的动作时(这里没有相应的Windows用户组),就可以向该数据库中添加一个角色(role).数据库角色指定了可以访问相同数据库对象的一组数据库用户. 数据库角色的成员可以分为如下几类: Windows用户组或

Greenplum数据库中系统表pg_class详解

Greenplum数据库中系统表pg_class详解 pg_ class 可以说是数据字典最重要的-一个表了,它保存着所有表.视图.序列.索引的原数据信息,每一个DDL/DML操作都必须跟这个表发生联系, 其表结构详情,如下图: 原文地址:https://www.cnblogs.com/lizm166/p/12102500.html

Redis的事务功能详解

Redis的事务功能详解 MULTI.EXEC.DISCARD和WATCH命令是Redis事务功能的基础.Redis事务允许在一次单独的步骤中执行一组命令,并且可以保证如下两个重要事项: >Redis会将一个事务中的所有命令序列化,然后按顺序执行.Redis不可能在一个Redis事务的执行过程中插入执行另一个客户端发出的请求.这样便能保证Redis将这些命令作为一个单独的隔离操作执行. > 在一个Redis事务中,Redis要么执行其中的所有命令,要么什么都不执行.因此,Redis事务能够保证

Centos7安装mariadb galera cluster数据库集群 & 详解

#Galera集群特点 集群之间无延时,同步复制.而master-slave主从异步复制,存在延迟. active-active多主,集群内部服务器都是同时写,必须等所有集群内所有数据库都完成数据写入,才会反馈完成,所以不存在数据丢失的情况. 集群节点自动故障转移,如果集群中单个节点故障,失效节点会自动被清除. 扩展方便,只要将新的节点添加到集群,新节点自动复制数据. #Galera集群原理     #主要通过galera插件保证数据的一致性,该数据复制的过程是可认证的复制,原理如下: #解析

spring声明式事务配置详解

spring声明式事务配置详解 君子不器 2013年06月16日 编程世界 5273次阅读 查看评论 理解Spring的声明式事务管理实现 本节的目的是消除与使用声明式事务管理有关的神秘性.简单点儿总是好的,这份参考文档只是告诉你给你的类加上@Transactional注解,在配置文件中添加('<tx:annotation-driven/>')行,然后期望你理解整个过程是怎么工作的.此节讲述Spring的声明式事务管理内部的工作机制,以帮助你在面对事务相关的问题时不至于误入迷途,回朔到上游平静

数据库优化举例详解

数据库优化举例详解 林涛 发表于:2016-3-16 1:01 分类:XSQL/程序/等 标签:mysql,mysql优化 112次 数据库是所有架构中不可缺少的一环,一旦数据库出现性能问题,那对整个系统都会来带灾难性的后果.并且数据库一旦出现问题,由于数据库天生有状态(分主从)带数据(一般还不小),所以出问题之后的恢复时间一般不太可控,所以,对数据库的优化是需要我们花费很多精力去做的. 硬件层优化 这一层最简单,最近几年相信大家对SSD这个名词并不陌生,其超高的IOPS在刚出现在大家视野中的时

重新学习MySQL数据库7:详解MyIsam与InnoDB引擎的锁实现

重新学习Mysql数据库7:详解MyIsam与InnoDB引擎的锁实现 说到锁机制之前,先来看看Mysql的存储引擎,毕竟不同的引擎的锁机制也随着不同. 三类常见引擎: MyIsam :不支持事务,不支持外键,所以访问速度快.锁机制是表锁,支持全文索引 InnoDB :支持事务.支持外键,所以对比MyISAM,InnoDB的处理效率差一些,并要占更多的磁盘空间保留数据和索引.锁机制是行锁,不支持全文索引(5.6以上支持) Memory:数据是存放在内存中的,默认哈希索引,非常适合存储临时数据,服

MySQL数据库事务隔离级别(Transaction Isolation Level)

今天在学习JDBC的时候看到了关于MySql的事务的隔离级别的问题,感觉内容挺高级的,所以记录一篇文章,以备后面使用. 数据库隔离级别有四种,应用<高性能mysql>一书中的说明: 然后说说修改事务隔离级别的方法: 1.全局修改,修改mysql.ini配置文件,在最后加上 1 #可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE. 2 [mysqld] 3 transaction-isolation = R