本篇博客参考掘金小册——MySQL 是怎样运行的:从根儿上理解 MySQL数据库
虽然咱们不是DBA,可能对数据库没那么了解,可是对于数据库中的索引、事务、锁,咱们仍是必需要有一个较为浅显的认识,今天我就和你们聊聊事务。bash
说到事务,不得不提到转帐的事情,几乎全部的关于事务的文章都会提到这个老掉牙的案例,我也不例外。服务器
转帐在数据库层面能够简单的抽象成两个部分:session
若是先从本身的帐户中扣除转帐金额,再往对方帐户中增长转帐金额,扣除执行成功,增长执行失败,那本身的帐户白白少了100块,欲哭无泪。架构
若是先往对方帐户中增长转帐金额,再从本身的帐户中扣除转帐金额,增长执行成功,扣除执行失败,那对方帐户白白增长了100块,本身的帐户也没有扣钱,喜大普奔。并发
不论是让你欲哭无泪,仍是喜大普奔,银行都不会容忍这样的事情发生,他们会引入事务来解决这类问题。性能
四种特性,简称ACID,其中最很差理解的就是一致性,有很多人认为原子性、隔离性、持久性就是为了保证一致性,咱们也不搞学术研究,一致性到底该怎么解释,到底怎么定义一致性,就看各位看官的了。spa
从某个角度来讲,咱们能够控制的、或者说须要研究的只有隔离性这一个特性,而要控制隔离性,几乎只有调整隔离级别这一个手段,下面咱们就来看看事务的隔离级别。翻译
数据库是一个客户端/服务器架构的软件,每一个客户端与服务器链接后,就会产生一个session(会话),客户端和服务器的交互就是在session中进行的,理论上来讲,若是服务器同时只能处理一个事务,其余的事务都排队等待,当该事务提交后,服务器才处理下一个事务,这样才真正具备“隔离性”,什么问题都没有了,可是若是是这样,性能就太差了,在性能和隔离性之间,只能作一些平衡,因此数据库提供了好几个隔离级别供咱们选择。指针
在讲隔离级别以前,咱们先来看看事务并发执行会遇到什么问题。
为了保证下面的叙述能够顺利进行,咱们要先建一张表:
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`grade` int(11) DEFAULT NULL COMMENT '年级',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
复制代码
若是sessionB在回滚事务的时候把sessionA的修改也给回滚了,致使sessionA的提交丢失了,这种现象就被称为“脏写”。sessionA会一脸懵逼,我明明修改了数据,也提交了数据,为何数据没有变化呢。
咱们知道了在并发执行事务的时候,会遇到什么问题,有些问题比较严重,有些问题比较轻微,通常来讲,咱们认为按照严重性排序是这样的:
脏写>脏读>不可重复读>幻读
在SQL标准定义中,设定了四种隔离级别,来解决上述的问题:
由于脏写的问题实在太严重了,在任何隔离级别下,都不会有脏写的问题。
前面说的都是开胃菜,相信大部分小伙伴对于上述内容都是手到擒来,因此我连如何修改事务隔离级别都没有介绍,各类实验也都没有作,就是要把大量的时间、文字投入到这一部份内容中来。
MVCC,全称是Mutil-Version Concurrency Control,翻译成中文是多版本并发控制,MySQL就利用了MVCC来判断在一个事务中,哪一个数据能够被读出来,哪一个数据不能被读出来。
在看MVCC以前,咱们有必要知道另一个知识点,数据库存储一行行数据,是分为两个部分来存储的,一个是数据行的额外信息(本篇博客不涉及),一个是真实的数据记录,MySQL会为每一行真实数据记录添加两三个隐藏的字段:
以下图所示:
在这里须要着重说明下事务id,当咱们开启一个事务,并不会立刻得到事务id,哪怕咱们在事务中执行select语句,也是没有事务id的(事务id为0),只有执行insert/update/delete语句才能得到事务id,这一点尤其重要。
其中和MVCC紧密相关的是transaction_id和roll_pointer两个字段,在开发过程当中,咱们无需关心,可是要研究MVCC,咱们必须关心。
若是有相似这样的一行数据:
实际上,roll_pointer并非空的,若是真要解释,须要绕一大圈,理解成空的,问题也不大。
当咱们开启事务,对这条数据进行修改,会变成这样:
有点感受了吧,这就像一个单向链表,称之为“版本链”,最上面的数据是这个数据的最新版本,roll_pointer指向这个数据的旧版本,给人的感受就是一行数据有多个版本,是否是符合“多版本并发控制”中的“多版本”这个概念, 那么“并发控制”又是怎么作到的呢,别急,继续往下看。
哎,下面又要引出一个新的概念:ReadView。
对于READ UNCOMMITTED来讲,能够读取到其余事务尚未提交的数据,因此直接把这个数据的最新版本读出来就能够了,对于SERIALIZABLE来讲,是用加锁的方式来访问记录。
剩下的就是READ COMMITTED和REPEATABLE READ,这两个事务隔离级别都要保证读到的数据是其余事务已经提交的,也就是不能无脑把一行数据的最新版本给读出来了,可是这两个仍是有必定的区别,最核心的问题就在于“我到底能够读取这个数据的哪一个版本”。
为了解决这个问题,ReadView的概念就出现了,ReadView包含四个比较重要的内容:
有了这个ReadView,只要按照下面的判断方式就能够解决“我到底能够读取这个数据的哪一个版本”这个千古难题了:
若是某个数据的最新版本不能够被读出来,就顺着roll_pointer找到该数据的上一个版本,继续作如上的判断,以此类推,若是第一个版本也不可见的话,表明该数据对当前事务彻底不可见,查询结果就不包含这条记录了。
看完上面的描述,是否是以为“云里雾里”,“不知所云”,甚至“脑阔疼,整我的都很差了”。
咱们换个方法来解释,看会不会更容易理解点:
上面我比较简单的解释了下ReadView,用了两种方式来讲明如何判断当前数据版本是否可见,不知道各位看官是否是有了一个比较模糊的概念,有了ReadView的基本概念,咱们就能够具体看下READ COMMITTED、REPEATABLE READ这两个事务隔离级别为何读到的数据是不一样的,以及上述规则是如何应用的。
假设,如今系统只有一个活跃的事务T,事务id是100,事务中修改了数据,可是尚未提交,造成的版本链是这样的:
如今A事务启动,而且执行了select语句,此时会建立出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。
为何m_ids只有一个,为何creator_trx_id是0?这里再次强调下,只有在事务中执行insert/update/delete语句才能得到事务id。
那么A事务执行的select语句会读到什么数据呢?
因此读到的数据的name是“地底王”。
咱们把事务T提交了,事务A再次执行select语句,此时,事务A再次建立出ReadView,m_ids是【】,min_trx_id是0, max_trx_id是101,creator_trx_id是0。
由于事务T已经提交了,因此没有活跃的事务。
那么事务A第二次执行select语句又会读到什么数据呢?
因此读到的数据的name是“梦境地底王”。
假设,如今系统只有一个活跃的事务T,事务id是100,事务中修改了数据,可是尚未提交,造成的版本链是这样的:
如今A事务启动,而且执行了select语句,此时会建立出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。
那么A事务执行的select语句会读到什么数据呢?
因此读到的数据的name是“地底王”。
细心的你,必定发现了,这里我就是复制粘贴,由于在REPEATABLE READ事务隔离级别下,事务A首次执行select语句建立出来的ReadView和在READ COMMITTED事务隔离级别下,事务A首次执行select语句建立出来的ReadView是同样的,因此判断流程也是同样的,因此我就偷懒了,copy走起。
随后,事务T提交了事务,因为REPEATABLE READ是首次读取数据才会建立ReadView,因此事务A再次执行select语句,不会再建立ReadView,用的仍是上一次的ReadView,因此判断流程和上面也是同样的,因此读到的name仍是“地底王”。
本篇博客到这里就结束了。