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 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
时,执行结果如下:
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
枚举值中还提供了另外三种隔离级别:Chaos
、Snapshot
、Unspecified
由于这种事务隔离级别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为例的更多相关文章
- Spring中的事务管理详解
在这里主要介绍Spring对事务管理的一些理论知识,实战方面参考上一篇博文: http://www.cnblogs.com/longshiyVip/p/5061547.html 1. 事务简介: 事务 ...
- 【MySQL 读书笔记】RR(REPEATABLE-READ)事务隔离详解
这篇我觉得有点难度,我会更慢的更详细的分析一些 case . MySQL 的默认事务隔离级别和其他几个主流数据库隔离级别不同,他的事务隔离级别是 RR(REPEATABLE-READ) 其他的主流数据 ...
- 数据库事务ACID详解(转载)
转载自:http://blog.csdn.net/shuaihj/article/details/14163713 谈谈数据库的ACID 一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行 ...
- Android中数据库的操作流程详解
Android中数据库的操作方法: 1.Android平台提供了一个数据库辅助类来创建或打开数据库. 这个辅助类继承自SQLiteOpenHelper类.继承和扩展SQLiteOpenHelper类主 ...
- .NET Core 中依赖注入框架详解 Autofac
本文将通过演示一个Console应用程序和一个ASP.NET Core Web应用程序来说明依赖注入框架Autofac是如何使用的 Autofac相比.NET Core原生的注入方式提供了强大的功能, ...
- Asp.Net Core 中的HTTP协议详解
1.前言 好久没写博客了,最近虽然没什么假期,但是却比以前还忙!工作.工作.工作,就像赶集似的,聚在一起.对于Web开发人员来说,深入了解HTTP有助于我们开发出更好.更高的Web应用程序.当应用程序 ...
- Java中线程的锁和数据库中的事务隔离级别
当涉及到两个或多个线程操作同一个资源时,就会出现锁的问题. 数据库中的某一条记录或者是某一个对象中的字段,可以修改,也可以读取,一般情况下,读取的那个方法应该加锁(即用synchronized互斥), ...
- 重新学习MySQL数据库9:Innodb中的事务隔离级别和锁的关系
重新学习MySQL数据库9:Innodb中的事务隔离级别和锁的关系 Innodb中的事务隔离级别和锁的关系 前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁 ...
- spring事务管理(详解和实例)
原文地址: 参考地址:https://blog.csdn.net/yuanlaishini2010/article/details/45792069 写这篇博客之前我首先读了<Spring in ...
随机推荐
- 老男孩最新Python全栈开发视频教程(92天全)重点内容梳理笔记 看完就是全栈开发工程师
为什么要写这个系列博客呢? 说来讽刺,91年生人的我,同龄人大多有一份事业,或者有一个家庭了.而我,念了次985大学,年少轻狂,在大学期间迷信创业,觉得大学里的许多课程如同吃翔一样学了几乎一辈子都用不 ...
- 一个高性能异步socket封装库的实现思路 (c#)
前言 socket是软件之间通讯最常用的一种方式.c#实现socket通讯有很多中方法,其中效率最高就是异步通讯. 异步通讯实际是利用windows完成端口(IOCP)来处理的,关于完成端口实现原理, ...
- Python之文件的基本操作
在python中,对文件的基本操作一共有如下四种: 1.打开文件 file_obj = open("文件路径","模式") 常用的打开文件模式有: r:以只读方 ...
- JAVAFX-5事件总结
事件监听 在RIA 或者说 桌面客户端gui android 开发中,事件的机制是必须的要学习了解的, 分类处理类型 在Java GUI 和swing中,事件通常通过实现listener的接口函数,并 ...
- 【二十】mysqli基于面向过程与面向对象的编程
面向过程的方式 musqli扩展库操作mysql数据库步骤: 1.获取连接并选择数据库 //语法 mysqli_connect(host,username,password,dbname,port,s ...
- php数据分页显示基础
一:分页原理: 所谓分页显示,也就是将数据库中的结果集认为的分成一段一段的来显示,需要两个初始的参数: 每页多少条记录 ($PageSize)? 当前是第几页($CurrentPageID)? 还有其 ...
- 运行时动态库:not found 及介绍-linux的-Wl,-rpath命令
---此文章同步自我的CSDN博客--- 一.运行时动态库:not found 今天在使用linux编写c/c++程序时,需要用到第三方的动态库文件.刚开始编译完后,运行提示找不到动态库文件.我就 ...
- MySQL管理员珍藏:十大必备工具盘点
作者:dongdongzzcs 第1页: [IT168 专稿]本文的作者Daniel Nichter是MySQL工具的开发者,他为MySQL管理员推荐了十款必备工具.以下是全文内容: MySQL是一套 ...
- CCF-201409-1-相邻数对
问题描述 试题编号: 201409-1 试题名称: 相邻数对 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 给定n个不同的整数,问这些数中有多少对整数,它们的值正好相差1. ...
- 将DLL文件直接封装进exe执行文件中(C#)
前言:由于项目需要,需制作一个注册机,将个人PC的MAC值和硬盘序列号与软件进行绑定,由于笔者的C++不是很好,所以采用C#进行开发.但在采用C#的时候,获取硬盘的MAC值和序列号的时候又不是很准确, ...