什么是事务
顾名思义,事务就是对一组事情的操作,要么把这件事办成了,要么这事儿就失败了;通俗来讲,事务就是一组sql语句的集合,要么这组sql全都执行成功,要么就全都执行失败;事务不是Mysql支持的,是InnoDB搜索引擎提供的。
描述事务最经典的例子就是账户转钱,A要给B转1000元钱操作如下:
1、检查A账户中余额是否高于1000元钱。
2、从A账户中减去1000元钱。
3、给B账户中增加1000元钱。
以上三个操作必须打包在一个事务当中,该事务要么成功(A账户减少1000,B账户增加1000),要么失败(A,B账户余额不变),不应该出现第三种现象。
所以这也是引入事务的原因:事务会把数据库从一种一致状态转换为另一种一致状态,在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不会保存。
事务的特性
InnoDB存储引擎中的事务完全符合ACID的特性,ACID是以下四个单词的缩写:
原子性(atomicity)
一致性(consistency)
隔离性(isolation)
持久性(durability)
原子性
原子是自然界非常小的单位,我们可以看成它是不可再分的,同时它也是事务的一个特征,任何一个事务都可以想象成一个原子,表示其不可再分。只有事务中所有的数据库操作都执行成功,才算整个事务成功,事务中任何一个sql语句执行失败,已经执行成功的sql语句也必须撤销,数据库状态应该退回到执行事务前的状态。
注意:如果事务中的操作都是只读的,保持原子性比较简单,一发生错误,要么重试,要么返回错误代码即可,如果当前事务中存在插入或者更新操作,一旦失败,就会引起数据状态的变化,因此要保护系统并发用户访问受影响的部分数据。
一致性
指数据库中数据在事务操作前和操作后都必须满足业务规则约束,也就是A、B账户的总金额在转账前后必须一致,二者的总金额加起来不能多也不能少,如果有不一致,则必须是短暂的,且只有在事务提交前才会出现的。
再举一个例子,在表中有一个字段为姓名,是唯一的约束,即在表中姓名不能重复,如果有一个事务对姓名字段进行了修改,在事务提交后,表中的姓名变得非唯一了,这就破坏了事务一致性的要求,因为事务将数据库从一种状态变成了一种不一致的状态。
隔离性
隔离性还有其他称呼,比如并发控制、可串行化、锁等。通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在转账的例子中,A向B转账时,C同时向A转账,如果同时进行,则A和B之间的一致行则不能满足,所以,当A和B执行事务的过程中,其他事务是不能访问或修改当前相关的数值。
持久性
一旦事务提交,其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。
注意:只能从事务本身的角度来保证结果是持久性,当事务提交后,所有的变化都是永久的,即使数据库崩溃需要恢复时,也可以保证恢复后的数据都不会丢失,但是如果不是数据库本身发生了问题,而是一些外部的原因,比如物理因素,自然灾害导致数据库服务器爆炸,那所有的数据可能都会丢失,因此持久性保证系统的高可靠性(High Reliability),而不是高可用性(High Availability)。
可靠性:指的是服务连续无故障运行的时间,无故障运行时间越长,可靠性就越高
可用性:在一段时间内,正常工作时间/这段时间=可用性。
一个服务器可以正常工作十年没有问题,则它的可靠性为十年,但是如果服务器损坏,需要一年来维护,则可用性就为90%。
事务的分类
从事务理论的角度来说,可以将事务分为以下几种类型:
扁平事务(Flat Transactions)
带有保存点的扁平事务(Flat Transactions With Savepoints)
链事务(Chained Transactions)
嵌套事务(Nested Transactions)
分布式事务(Distributed Transactions)
扁平事务
扁平事务是事务类型中最简单最常用的一种事务,在扁平事务中,所有的操作都处于同一层次,事务开始到提交(回滚)之间操作是原子性的,要么都执行,要么都回滚。扁平事务是应用程序成为原子操作的基本组成模块。下图展示了扁平事务的三种结果:
扁平事务就是咱们平常说的事务,如果执行的操作有误,就会全部回滚。现在假设当前扁平事务有1000条sql,但执行到最后一条sql时失败了,这时就会全部回滚,即使前999条sql执行成功也会进行回滚,这样代价太大了,于是就有了带保存点的扁平事务。
带有保存点的扁平事务
带有保存点的扁平事务和扁平事务的区别就是有多个保存点,保存点用来通知系统应该记住事务当前的状态,以便之后发生错误时,事务能回到保存点当时的状态,这就解决了扁平事务全部回滚代价大的缺点。(为什么说这两个事务的区别是带有保存点的扁平事务有多个保存点,其实扁平事务也隐式的设置了一个保存点,这个保存点只存在于事务开始时的位置,如果出现问题,直接回到起点)下面是带有保存点的扁平事务使用:
上图灰色部分表示已经被回滚过的没有执行的事务。当用BEGIN WORK 开启一个事务的时候,隐式的包含了一个保存点,当事务通过ROLLBACK WORK:2 命令回滚时,事务就会回到 SAVE WORK :2,然后继续执行,执行到ROLLBACK WORK:7再次回滚,回滚到SAVE WORK 7 处,再继续执行,直到事务提交完毕,除了灰色部分,其他操作都已经被提交。
注意:保存点是递增的,执行完了保存点2之后,下一个保存点是3,然后是4,即使3,4被回滚了,下一个保存点依然从4之后开始,也就是5,并不会重新从3开始。
链事务
上面说到了带有保存点的扁平事务,在执行该类型事务时,如果系统发生了崩溃,则所有的保存点都会消失,因为保存点是易失的(volatile),而非持久的(persistent),也就是当进行恢复时,事务还是要从开始处重新执行,而不会从最近的一个保存点继续执行。
于是就有了链式事务:在提交一个事务时,紧接着将上下文处理传递给下一个要开始的事务,也就是当前提交事务的操作和开始下一个事务的操作将合并成为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。下图展示了链式事务的工作方式:
链式事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务可以回滚到任意正确的保存点,而链式事务只能回滚到最近的一个保存点,并且回滚只限于当前事务,就像是带有保存点的扁平事务的当前保存点之前,上一个保存点之后为一个事务。然后这些被分割的事务通过触发器进行连接成为链事务。
嵌套事务
嵌套事务是一个层次结构的框架,由一个顶层事务(top-level transaction)控制着各个层次的事务,顶层事务之下嵌套的事务被成为子事务(subtransaction),其控制每一个局部的变化,嵌套事务结构层次如下:
在嵌套事务中,实际的工作都是叶子节点来完成的,即只有叶子节点的事务才能访问数据库、发送消息、获取其他类型的数据。高层事务仅负责逻辑控制,决定何时调用相关的的子事务。
对嵌套事务的定义:
1、嵌套事务是由若干事务组成的一棵树,子事务既可以是嵌套事务,也可以是扁平事务。
2、处在叶子节点的事务是扁平事务,但是每个子事务从根到叶子节点的距离可以是不相同的。
3、位于根节点的事务成为顶层事务,其他事务成为子事务,事务的前驱(predecessor)为父事务(parent),事务的下一层称为儿子事务(child)。
4、子事务既可以提交也可以回滚,但是它的提交操作并不马上生效,除非其父事务已经提交,所以子事务都是在顶层事务提交之后才会真正的提交。
5、树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留ACI特性,不具有D的特性。
注意:对于InnoDB存储引擎来说,是支持扁平事务、带有保存点的扁平事务、链式事务、分布式事务的,并不原生支持嵌套事务。但可以通过带有保存点的事务来模拟串行的嵌套事务。
分布式事务
分布式事务通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中不同的节点。
还是拿上面转账的例子来说,用户在ATM机处进行转账操作,将招商银行的钱转账到工商银行,ATM机视为节点A,招商银行后台的数据库视为节点B,工商银行的后台数据库视为节点C。这里就需要使用分布式事务,因为节点A不能通过调用一台数据库就能完成任务,需要访问网络中两个节点的数据库,每个节点的数据库执行的事务是扁平事务。
事务的实现
事务的隔离性通过锁来实现,原子性、一致性、持久性通过数据库的redo log 和 undo log 来完成。redo log 称为重做日志,用来保证事务的原子性和持久性,undo log 用来保证事务的一致性。
实现隔离性
隔离性是通过锁来进行实现的
实现原子性
要想保证事务的原子性,就需要在执行发生异常时,对已经执行的操作进行回滚。在InnoDB中,恢复机制是通过回滚日志(undo log)来实现的,所有事务进行的修改都会先记录到这个回滚日志中。
那么是如何通过undo log来实现回滚的呢?当我们对数据库进行修改时,InnoDB存储引擎不但会产生redo log 还会产生 undo log,如果用户执行的事务或者语句由于某种原因失败了,可以利用ROLLBACK语句进行回滚,这个回滚操作就是利用undo log中的信息,所以 undo log 中存放的是表之前的记录。通过undo log 就可以保证该事务中某个操作失败的话就全部回滚,使这整个事务失败,来保证原子性。
回滚日志除了能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
undo log 简介
undo log是逻辑日志,并不能将数据库物理地恢复到执行语句或事务之前的样子,这是因为在多用户并发系统中,可能会有上千个并发事务,如果一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改,就不能将这个页回滚到事务开始的样子,这样会影响其他的事务。
例如:用户执行了一个INSERT 10w条记录的事务,这个事务会导致分配了一个新的段,即表空间会增大,当再执行ROLLBACK时,会将插入的事务进行回滚,但是表空间不可能再缩小,物理上不会改变。因此,当InnoDB存储引擎发生回滚时,它实际做的是与先前相反的工作,对于每个INSERT,InnoDB存储引擎会完成一个DELETE(只限于事务提交之前);对于DELETE,存储引擎会执行一个INSERT;对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去。最后一点,undo log 会产生redo log,这是因为 undo log 也需要持久性的保护。除此之外 ,undo log 还可以实现非锁定读,详细见上面实现隔离性中博客地址。
undo log 存储管理
InnoDB对 undo 的管理采用段的方式,段存放于共享表空间中。InnoDB存储引擎有rollback segment(回滚段),每个回滚段中记录了1024个undo log segment(段),每一个undo log segment 代表一个事务。
在InnoDB1.1版本之前(不包括1.1版本),只有一个rollback segment,因此支持 1024个事务,从1.1版本开始InnoDB支持最大128个rollback segment,也就是将同时在线的事务限制提高到了128 * 1024。
事务在undo log segment 分配页并写入undo log 的这个过程同样需要写入重做日志,当事务提交时,InnoDB存储引擎会做两件事情:
1、将undo log 放入列表中,以提供之后的purge 操作
2、判断undo log 所在的页是否可以重用,若可以分配给下个事务使用
事务提交后并不能马上删除 undo log 及undo log 所在的页,这是因为可能还有其他事务需要通过undo log 来得到行记录之前的版本(一致性非锁定读),所以事务提交时将undo log 放入一个链表中,是否可以最终删除 undo log 及undo log 所在页由purge 线程来判断。
undo log 格式
在InnoDB存储引擎中,undo log 分为:
1、insert undo log
2、update undo log
insert undo log 是指在insert 操作中产生的undo log,因为是insert操作的记录,只对事务本身可见,对其他事务不可见(事务隔离性的要求),故该undo log 可以在事务提交后直接删除,因为其他事务不可能读到还未插入的数据,这条数据根据就不存在。
上图中*表示对存储字段进行了压缩,next记录的是下一个undo log 的位置,通过该next的字节可以知道一个undo log所占的空间字节数;尾部的start记录的是undo log 的开始的位置,占两个字节;type_cmpl占用一个字节,记录的是undo 的类型,对于 insert undo log,该值永远是11;undo no 记录事务的ID;table_id记录 undo log所对应的表对象;剩下的部分记录了所有主键的列和值,在rollback操作时,根据这些值可以定位到具体的记录,然后进行直接删除。update undo log 记录的是delete 和update 操作产生的undo log 可能需要提供MVCC 机制,因此不能在事务提交时就进行删除,提交时放入 undo log 链表,等待purge 线程进行最后的删除。update undo log 结构如下:
next、start、undo_no、table_id与之前介绍的insert undo log 部分相同,update_vector 表示update操作导致发生改变的列。每个修改的列信息都要记录到undo log中。
实现持久性
redo log 组成
实现持久性是通过redo log(重做日志)来实现的,其由两个部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。
redo log 工作原理
在将redo log 写入磁盘之前,先将重做日志缓冲写入重做日志文件,然后再进行一次fsync(同步文件到存储设备)操作,才将日志写入到磁盘当中做持久化,由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。如果在数据库数据发生改变时,直接修改到磁盘中,那整个过程的IO成本是非常高的,所以当一条数据需要更新的时候,InnoDB先把记录写入到文件系统缓存,然后在适当的时候将记录更新到磁盘里面。
redo log 是固定大小的,比如可以配置一组4个文件,每个文件大小是1GB,总共就是4GB,从头开始写,写到末尾就又回到开头循环写,如下图所示:
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。如果write pos追上checkpoint,表示日志满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
调整redo log 同步机制
但是在redo log 写入到磁盘之前,数据库突然发生宕机,由于部分日志未刷新到磁盘中,因此会丢失最后一段时间的事务。所以参数 innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数默认为1,表示事务提交时必须调用一次fsync操作,还可以设置参数0和2。0表示事务提交时不进行写入重做日志操作,该操作仅在master thread 中完成,而master thread 中每 1 秒进行一次fsync操作。2表示事务提交时将重做日志写入重做日志文件,但仅仅写入文件系统缓存中,不进行fsync操作,在这个设置下,当mysql 数据库发生宕机时,并不会导致事务的丢失,当操作系统发生宕机时,重启数据库会丢失未从文件系统刷新到重做日志文件那部分事务。
打开mysql ,输入命令,可看默认是1:
更改命令:
不同的设置对插入50w行数据的速度如下:
redo log 与 bin log 区别
除了redo log之外,mysql中还有一种二进制日志为归档日志bin log,bin log 是一个二进制格式的文件,用于记录用户对数据库更新的SQL语句信息,查询的内容不会进行记录。它与redo log 的区别如下:
1、redo log 是InnoDB引擎特有的,bin log 是Mysql 的Server层实现的,所有引擎都可以使用。
2、redo log 是物理日志记录的是对每个页的修改,bin log 和undo log 一样,都是逻辑日志,记录的是SQL语句。
3、redo log 是循环写的,空间固定会用完;bin log 是可以追加写入得,当文件达到一定大小后悔切换到下一个,并不会覆盖以前的日志。
4、两种日志记录写入磁盘的时间点不同,bin log 只在事务提交完成后进行一次写入,而redo log 在事务进行中就不断地写入,表现为日志并不是随事务提交的顺序进行写入的。
上图中,bin log 只是在事务提交时记录的,并且对于每一个事务,只包含对应事务的一个日志;对于redo log,因为记录的是物理日志,所以每个事务对应多个日志条目,并且事务的redo log 写入是并发的,并不是在事务提交时写入,所以在文件中记录的顺序并不是事务开始时的顺序,*T2 *T3 *T1 表示的是事务提交时的日志。
redo log 格式
InnoDB存储引擎的存储管理是基于页的,故重做日志格式也是基于页的,虽然有着不同的重做日志格式,但是它们有着通用的头部格式,如下图所示:
redo_log_type:重做日志的类型。
space:表空间的ID。
page_no:页的偏移量。
redo log body 部分,根据重做日志类型的不同吗,会有不同的存储内容,对于页上记录的插入和删除操作,对应如下的格式:
实现一致性
数据库中的一致性是通过约束来实现的。如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。对于这个概念,它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的前后以及过程中不会违背对数据完整性的约束,所有对数据库写入的操作都应该是合法的,并不能产生不合法的数据状态。还是最上面的例子,更新数据库前主键都是唯一的,那么更新完改数据库之后主键还必须是唯一的。而第二层意思其实是指逻辑上的对于开发者的要求,我们要在代码中写出正确的事务逻辑,比如银行转账,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求。
事务的隔离级别
在说事务的隔离级别之前先来简单描述一下脏读、不可重复读、幻读、可重复读。
脏读:脏读是一个事务读取到了其他事务还没有提交的数据。
不可重复读:指在一个事务中,读到了其他事务针对就数据的修改记录(常见的操作就是update 或者 delete 语句)。
幻读:指在一个事务中,读取到了其他事务新增的数据,仿佛出现了幻影的现象(常见的操作是insert语句)。
可重复读:是mysql默认的事务隔离级别,它消除了脏读、不可重复读、幻读的现象,保证了事务的一致性。
SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
下面是按照时间的顺序执行两个事务的行为:
mysql> 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提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。隔离级别与脏读、幻读等的对应关系: