数据库事务指的是一组数据操做,事务内的操做要么就是所有成功,要么就是所有失败,什么都不作,其实不是没作,是可能作了一部分可是只要有一步失败,就要回滚全部操做,有点一不作二不休的意思。html
在 MySQL 中,事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统,但并非全部的引擎都支持事务。好比 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要缘由之一。前端
SQL 事务的四大特性中原子性、一致性、持久性都比较好理解。但事务的隔离级别确实比较难的,今天主要聊聊 MySQL 事务的隔离性。java
SQL 标准的事务隔离从低到高级别依次是:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。级别越高,效率越低。面试
SQL 事务隔离级别的设计就是为了能最大限度的解决并发问题:算法
SQL 不一样的事务隔离级别能解决的并发问题也不同,以下表所示:只有串行化的隔离级别解决了所有这 3 个问题,其余的 3 个隔离级别都有缺陷。sql
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
PS:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住知足条件的行,解决幻读须要锁表数据库
这么说可能有点难以理解,举个栗子。仍是以前的表结构以及表数据编程
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`age` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
复制代码
假设如今,我要同时启动两个食物,一个事务 A 查询 id = 2 的学生的 age,一个事务 B 更新 id = 2 的学生的 age。流程以下,在四种隔离级别下的 X一、X二、X3 的值分别是怎样的呢?设计模式
那为何会出现这样的结果呢?事务隔离级别究竟是怎么实现的呢?数组
事务隔离级别是怎么是实现的呢?我在极客时间丁奇老师的课上找到了答案:
实际上,数据库里面会建立一个视图,访问的时候以视图的逻辑结果为准。在 “可重复读” 隔离级别下,这个视图是在事务启动时建立的,整个事务存在期间都用这个视图。在 “读提交” 隔离级别下,这个视图是在每一个 SQL 语句开始执行的时候建立的。这里须要注意的是,“读未提交” 隔离级别下直接返回记录上的最新值,没有视图概念;而 “串行化” 隔离级别下直接用加锁的方式来避免并行访问。
不一样的数据库默认设置的事务隔离级别也大不同,Oracle 数据库的默认隔离级别是读提交,而 MySQL 是可重复读。因此,当你的系统须要把数据库从 Oracle 迁移到 MySQL 时,请把级别设置成与搬迁以前的(读提交)一致,避免出现不可预测的问题。
# 查看事务隔离级别
5.7.20 以前
SELECT @@transaction_isolation
show variables like 'transaction_isolation';
# 5.7.20 以及以后
SELECT @@tx_isolation
show variables like 'tx_isolation'
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
复制代码
修改隔离级别语句格式是:set [做用域] transaction isolation level [事务隔离级别]
其中做用域可选:SESSION(会话)、GLOBAL(全局);隔离级别就是上面提到的 4 种,不区分大小写。
例如:设置全局隔离级别为读提交
set global transaction isolation level read committed;
复制代码
MySQL 的事务启动有如下几种方式:
# 更新学生名字
START TRANSACTION;
update student set name = '张三' where id = 2;
commit;
复制代码
set autocommit = 0,这个命令会将线程的自动提交关掉。意味着若是你只执行一个 select 语句,这个事务就启动了,并且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开链接。
set autocommit = 1,表示 MySQL 自动开启和提交事务。 好比执行一个 update 语句,语句只完成后就自动提交了。不须要显示的使用 begin、commit 来开启和提交事务。因此当咱们执行多个语句的时候,就须要手动的用 begin、commit 来开启和提交事务。
start transaction with consistent snapshot;上面提到的 begin/start transaction 命令并非一个事务的起点,在执行到它们以后的第一个操做 InnoDB 表的语句,事务才真正启动。若是你想要立刻启动一个事务,可使用 start transaction with consistent snapshot 命令。 第一种启动方式,一致性视图是在执行第一个快照读语句时建立的; 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时建立的。
理解了隔离级别,那事务的隔离是怎么实现的呢?要想理解事务隔离,先得了解 MVCC 多版本的并发控制这个概念。而 MVCC 又依赖于 undo log 和 read view 实现。
百度上的解释是这样的:
MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的方法,通常在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC 使得数据库读不会对数据加锁,普通的 SELECT 请求不会加锁,提升了数据库的并发处理能力;数据库写才会加锁。 借助 MVCC,数据库能够实现 READ COMMITTED,REPEATABLE READ 等隔离级别,用户能够查看当前数据的前一个或者前几个历史版本,保证了 ACID 中的 I 特性(隔离性)。
MVCC 只在 REPEATABLE READ 和 READ COMMITIED 两个隔离级别下工做。其余两个隔离级别都和 MVCC 不兼容 ,由于 READ UNCOMMITIED 老是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对全部读取的行都加锁。
InnDB 中每一个事务都有一个惟一的事务 ID,记为 transaction_id。它在事务开始时向 InnDB 申请,按照时间前后严格递增。
而每行数据其实都有多个版本,这就依赖 undo log 来实现了。每次事务更新数据就会生成一个新的数据版本,并把 transaction_id 记为 row trx_id。同时旧的数据版本会保留在 undo log 中,并且新的版本会记录旧版本的回滚指针,经过它直接拿到上一个版本。
因此,InnDB 中的 MVCC 实际上是经过在每行记录后面保存两个隐藏的列来实现的。一列是事务 ID:trx_id;另外一列是回滚指针:roll_pt。
回滚日志保存了事务发生以前的数据的一个版本,能够用于回滚,同时能够提供多版本并发控制下的读(MVCC),也即非锁定读。
根据操做的不一样,undo log 分为两种: insert undo log 和 update undo log。
insert 操做产生的 undo log,由于 insert 操做记录没有历史版本只对当前事务自己可见,对于其余事务此记录不可见,因此 insert undo log 能够在事务提交后直接删除而不须要进行 purge 操做。
purge 的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages
因此,插入数据时。它的初始状态是这样的:
UPDATE 和 DELETE 操做产生的 Undo log 都属于同一类型:update_undo。(update 能够视为 insert 新数据到原位置,delete 旧数据,undo log 暂时保留旧数据)。
事务提交时放到 history list 上,没有事务要用到这些回滚日志,即系统中没有比这个回滚日志更早的版本时,purge 线程将进行最后的删除操做。
一个事务修改当前数据:
另外一个事务修改数据:
这样的同一条记录在数据库中存在多个版本,就是上面提到的多版本并发控制 MVCC。
另外,借助 undo log 经过回滚能够回到上一个版本状态。好比要回到 V1 只须要顺序执行两次回滚便可。
read view 是 InnDB 在实现 MVCC 时用到的一致性读视图,用于支持 RC(读提交)以及 RR(可重复读)隔离级别的实现。
read view 不是真实存在的,只是一个概念,undo log 才是它的体现。它主要是经过版本和 undolog 计算出来的。做用是决定事务能看到哪些数据。
每一个事务或者语句有本身的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图肯定数据版本的可见性。
read view 中主要包含当前系统中还有哪些活跃的读写事务,在实现上 InnDB 为每一个事务构造了一个数组,用来保存这个事务启动瞬间,当前正活跃(还未提交)的事务。
前面说了事务 ID 随时间严格递增的,把系统中已提交的事务 ID 的最大值记为数组的低水位,已建立过的事务 ID + 1记为高水位。
这个视图数组和高水位就组成了当前事务的一致性视图(read view)
这个数组画个图,长这样:
规则以下:
第三点我在看教程的时候也有点疑惑,好在有热心网友解答:
落在绿色区域意味着是事务 ID 在低水位和高水位这个范围里面,而真正是否可见,看绿色区域是否有这个值。若是绿色区域没有这个事务 ID,则可见,若是有,则不可见。在这个范围里面并不意味着这个范围就有这个值,好比 [1,2,3,5],4 在这个数组 1-5 的范围里,却没在这个数组里面。
这样说可能有点难以理解,我假设一个场景:三个事务对同一条数据进行查询更新等操做,为此画了张图以方便理解:
原始数据仍是下图这样的,对 id = 2 的张三进行信息的更新:
针对上图,我想提个问题。**分别在 RC(读提交)以及 RR(可重复读)隔离级别下,T4 和 T5 时间点的查询 age 值分别是多少呢?T4 更新的值又是多少呢?**思考片刻,相信你们都有本身的答案。答案在文末,但愿你们能带着本身的疑问继续读下去。
RR 级别下,查询只认可在事务启动前就已经提交完成的数据,一旦启动事务就会建视图。因此使用 start transaction with consistent snapshot 命令,立刻就会建视图。
如今假设:
在这种隔离级别下,他们建立视图的时刻以下:
根据上图得,事务 A 的视图数组是[2,3];事务 B 的视图数组是 [2,3,4];事务 C 的视图数组是[2,3,4,5]。分析一波:
这样执行下来,虽然期间这一行数据被修改过,可是事务 A 不论在何时查询,看到这行数据的结果都是一致的,因此咱们称之为一致性读。
其实视图是否可见主要看建立视图和提交的时机,总结下规律:
事务 B 的 update 语句,若是按照上图的一致性读,好像结果不大对?
以下图周明,B 的视图数组是先生成的,以后事务 C 才提交。那就应该看不见 C 修改的 age = 23 呀?最后 B 怎么得出 24 了?
没错,若是 B 在更新以前执行查询语句,那返回的结果确定是 age = 22。问题是更新就不能在历史版本更新了呀,不然 C 的更新不就丢失了?
因此,更新有个规则:更新数据都是先读后写(读是更新语句执行,不是咱们手动执行),读的就是当前版本的值,叫当前读;而咱们普通的查询语句就叫快照读。
所以,在更新时,当前读读到的是 age = 23,更新以后就成 24 啦。
除了更新语句,查询语句若是加锁也是当前读。若是把事务 A 的查询语句 select age from t where id = 2 改一下,加上锁(lock in mode 或者 for update),也均可以获得当前版本 4 返回的 age = 24
下面就是加了锁的 select 语句:
select age from t where id = 2 lock in mode;
select age from t where id = 2 for update;
复制代码
假设事务 C 不立刻提交,可是 age = 23 版本已生成。事务 B 的更新将会怎么走呢?
事务 C 还没提交,写锁还没释放,可是事务 B 的更新必需要当前读且必须加锁。因此事务 B 就阻塞了,必须等到事务 C 提交,释放锁才能继续当前的读。
在读提交隔离级别下,查询只认可在语句启动前就已经提交完成的数据;每个语句执行以前都会从新算出一个新的视图。
注意:在上图的表格中用于启动事务的是 start transaction with consistent snapshot 命令,它会建立一个持续整个事务的视图。因此,在 RC 级别下,这命令其实不起做用。等效于普通的 start transaction(在执行 sql 语句以前才算是启动了事务)。因此,事务 B 的更新实际上是在事务 C 以后的,它还没真正启动事务,而 C 已提交。
如今假设:
在这种隔离级别下,他们建立视图的时刻以下:
根据上图得,事务 A 的视图数组是[2,3,4],但它的高水位是 6或者更大(已建立事务 ID + 1);事务 B 的视图数组是 [2,4];事务 C 的视图数组是 [2,5]。分析一波:
本文详细聊了事务的方方面面,好比:四大特性、隔离级别、解决的并发问题、如何设置、查看隔离级别、如何启动事务等。除此之外,还深刻了解了 RR 和 RC 两个级别的隔离是怎么实现的?包括详解 MVCC、undo log 和 read view 是怎么配合实现 MVCC 的。最后还聊了快照读、当前读等等。能够说,事务相关的知识点都在这了。看完这一篇还不懂的话,你来捶我呀!
好啦,以上就是狗哥关于数据库事务的总结。感谢各技术社区大佬们的付出,尤为是极客时间,真的牛逼。若是说我看得更远,那是由于我站在大家的肩膀上。但愿这篇文章对你有帮助,咱们下篇文章见~
若是看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索JavaFish,关注后回复电子书送你 1000+ 本编程电子书 ,包括 C、C++、Java、Python、GO、Linux、Git、数据库、设计模式、前端、人工智能、面试相关、数据结构与算法以及计算机基础,详情看下图。回复1024送你一套完整的 java 视频教程。