文章目录
- 前言
- 一、事务介绍
- 二、事务的四大特性
- 三、事务的隔离性
- 四、事务隔离的实现
前言
我们在实际开发中,执行某个业务,肯定不是简单的操作某一句SQL语句,而是多条SQL语句。那么这多条SQL语句必须是全部成功执行,或者全部失败。才能保证业务逻辑的正确。因此这便引出了事务。接下来我们对MySQL的事务进行深入的了解学习
一、事务介绍
事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的,并且也不是所有的引擎都支持事务的,比如MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一。
本文将会以InnoDB为例,将对MySQL对事务的支持进行深入学习。
对MySQL有基本了解的,提到事务,肯定会想到ACID(Atomicity、Consistency、lsolation、Durability,即原子性、一致性、隔离性、持久性)。
接下来我们先对这四大特性进行一个简单了解
二、事务的四大特性
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
- 一致性:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 比如,用户A和用户B在银行分别有800元和600元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
- 持久性:事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
了解了事务这四大特性之后,那么这四大特性分别是怎么实现的呢,这里简答介绍下:
以InnoDB引擎为例,来介绍如何保证事务的这四个特性的
- 持久性是通过redo log(重做日志)来保证的
- 原子性是通过undo log(回滚日志)来保证的
- 隔离性是通过MVCC(多版本并发控制)或锁机制来保证的
- 一致性则是通过持久性+原子性+隔离性来保证
而这其中最重要,也是经常被考查的就是隔离性。接下来重点学习下事务的隔离性
三、事务的隔离性
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
在介绍隔离级别之前,我们先介绍下这三个可能出现的问题:
- 脏读:如果一个事务读到了另一个未提交事务修改过的数据,就意味着发生了脏读现象;(毕竟未提交过的事务都有随时可能发生回滚操作)
- 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了不可重复读现象(可能是其他事务修改了这条数据,并进行了提交)前后读取的数据不一致
- 幻读:在一个事务内多次查询某个符合查询条件的记录数量,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了幻读现象(可能是其他事务增添或者删除了记录并进行了提交)前后读取的记录数量不一致
为了解决这些可能出现的问题,便设计处了事务的隔离级别,其中包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。
在介绍这几个隔离级别之前,不知道读者是否有个疑问,既然知道了事务存在这些可能发生的问题,我们直接设计出一个隔离级别,一起把这三个可能出现的问题解决了不就行了吗,为什么还要设计出这么多个隔离级别啊。
为此,我们先来解答一下这个问题。那是因为隔离级别有高低之分,隔离级别越高,性能越低。因此很多时候,我们都要在二者之间寻找一个平衡点。这几个隔离级别隔离水平高低排序如下:
解决了这个疑惑之后,接下来为各个隔离级别进行介绍:
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到;
- 读提交:一个事务提交之后,它做的变更才会被其他事务看到
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的
- 串行化:对同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
为了彻底搞清楚这几个隔离级别,这里用一个例子进行说明。
假设数据表T中只有一列,其中一行的值为1,如下SQL语句:
create table T(c int) engine=InnoDB;
insert into T(c) values(1);
然后按照时间顺序执行下面两个事务:
接下来,我们看看在不同的隔离级别下,事务A会有哪些不同的返回结果,即V1、V2、V3的返回值分别是什么
- 若隔离级别是“读未提交”,则V1的值是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此,V2、V3也都是2
- 若隔离级别是“读提交”,则V1是1,V2的值是2.事务B的更新在提交后才能被A看到。所以,V3的值也是2
- 若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的
- 若隔离级别是 “串行化”,则在事务B执行“将1改成2”的时候,会被锁住(被事务A的读锁锁住)。直到事务A提交后,事务B才可以继续执行。所以从A的角度看,V1、V2值是1,V3的值是2。
通过例子了解了事务的隔离性,那么我们对每个隔离级别可以解决的问题应该也清楚了。
为了清楚记忆,构建下图(针对不同的隔离级别,并发事务可能发生的现象):
通过上述,可以清楚认识到在不同的隔离级别下,数据库的行为是不同的。既然开发者设计出来,肯定有它的适用场景,根据实际开发的业务判断即可。
如下,举个“可重复读”的场景:数据校对逻辑的案例
假设在管理一个个人银行账号表。一个表存了每个月月底的余额,一个表存了账单明细。这时候要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。这时肯定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。
这时候使用“可重复读”隔离级别就很方便
了解了事务的隔离性,接下来我们再深入学习以下,看看这事务的隔离性是怎么实现的
四、事务隔离的实现
这里先总体说下实现。在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
总体了解了之后,我们再来学习下事务隔离具体的实现,这里以“可重复读”为具体说明:
在MySQL中,每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
假设一个值从1被按顺序改成2、 3、 4,则在回滚日志里就会有类型下面的记录:
从上图,我们看见了一个新的东西:Read View。没接触过的肯定不知道。接下来我们先介绍下它。
如下图所示:
Read View的四个重要字段:
- m_ids:指的是在创建Read View时,当前数据库中活跃事务的事务id列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
- min_trx_id:指的是在创建Read View时,当前数据库中活跃事务中事务id最小的事务,也就是m_ids的最小值
- max_trx_id:这个并不是m_ids的最大值,而是创建Read View时当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id值+1;
- creator_trx_id:指的是创建该Read View的事务的事务id。
知道了Read View的字段,还要知道聚簇索引记录中的两个隐藏列。
如下图所示,每个行记录中包括的两个隐藏列:
对于InnoDB存储引擎的数据库表,聚簇索引记录包含的隐藏列解释如下:
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务id记录在trx_id隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录
在创建Read View后,可以将记录中的trx_id划分为这三种情况:
了解了这之后,接着看开头把表数据的值从1改成2,然后3、 4。那么当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。从上面的数据更新图可以看出,在视图A、B、C里面,这一个记录的值分别是1、2、 4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于上面数据更新图里,对于read-view A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
这是不知道你发现问题了没有。设想,如果我们数据更新的比较频繁,那么回滚日志中的数据不能一直都保留吧,那么什么时候才会删除记录呢?
系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的read-view的时候。
此时在使用时有个建议:
尽量不要使用长事务,因为长事务就意味着系统里面会存在很老的事务视图。并且这些事务随时可能访问数据库里面任何数据,所以这个事务提交之前,数据库里面它可能用到回滚记录都必须保留,这就会导致大量占用存储空间。