第七章 事务(上)
7.1 认识事务
7.1.1 概述
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区别与文件系统的重要区别之一。理论上说,事务有着极其严格的定义,它必须同时满足四个特性,即通常所说的事务的ACID特性。但是数据库厂商处于各种目的,并没有严格去满足事务的ACID标准。对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE ,完全遵循和满足事务的ACID特性。
A(Atomicity),原子性。原子性是指整个数据库事务是一个不可分割的工作单位。只有事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
C(Consistency),一致性。一致性是指事务从一种状态转变为下一种一致的状态,在事务开始之前和事务结束以后,数据库完整性约束没有被破坏。例如,在表中有一个字段为姓名,为唯一性约束。如果有一个事务对姓名字段进行了修改,但是在事务提交或事务发生回滚之后,表中姓名字段变得非唯一了,这就破坏了事务的一致性要求,即事务将数据库从一种状态变成了一种不一致的状态。
D(Durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availablity)。
7.1.2 分类
从事务理论的角度来说,可用把事务分为以下几种类型:
- 扁平事务(Flat Transactions)
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
- 链事务(Chained Transactionss)
- 嵌套事务(Nested Transactions)
- 分布式事务(Distributed Transactions)
扁平事务(Flat Transaction)
事务类型中最简单也是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其有BEGIN WORK开始,有COMMIT WORK或ROLLBACK WORK结束,其间的操作都是原子的,要么都执行,要么都回滚。扁平事务的主要限制是不能提交或者回滚事务的某一部分,或分几个步骤提交。如果要支持有计划的回滚操作,那么就不需要终止整个事务。因此就出现了带有保存点的扁平事务
带有保存点的扁平事务
除了支持扁平事务外,允许事务在执行过程中回滚到同一个事务中较早的一个状态。保存点用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。保存带你在事务内部是递增的,这意味着ROLLBACK WORK不影响保存点的计数,并且单调递增的编号能保持事务执行的整个历史过程,包括在执行过程中想法的改变。
链事务
可视为保存点模式的一种变种。带有保存点的扁平事务,当系统发生崩溃是,所有保存点都将消失,因为其保存点是易失的,而非持久的。这意味着当进行恢复是,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务能看到上一个事务的结果,就好像在一个事务中进行一样。
链事务和带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务的回滚仅限于当前事务,即只能恢复到最近一个保存点。对于锁的处理,两者也不同。链事务在执行commit之后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
嵌套事务
有一个顶层事务控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务,其控制每一个局部的变换。
1.嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务
2.处在叶节点的事务是扁平事务。但是每个子事务从根到叶节点的距离可以是不同的。
3.位于根节点的事务称为顶层事务,其他事务称为子事务。事务的前驱称为父事务,事务的下一层称为儿子事务。
4.子事务既可以提交,也可以回滚。但是它的提交并不马上生效,除非其父事务已经提交。因此可以推出,任何子事务都在顶层事务提交之后才能真正的提交。
5.树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留A,C,I 特性,不具有D的特性
分布式事务
通常是在一个分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
7.2 事务的实现
事务的隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
7.2.1 redo
1.基本概念
重做日志用来实现事务的持久性。其右两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才算完成。这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚即MVCC功能。redo log 基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读取的。
为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。这是因为重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一个fsync操作,由于fsync的效率取决于磁盘的性能,所以磁盘的性能决定了事务提交的性能,也就是数据库的性能。
参数innodb_flush_log_at_trx_commit用来控制日志刷新到磁盘的策略。
当该参数值为0时,表示事务提交时不进行写入重做日志的操作,这个操作仅在Master Thread中完成,而在Master Thread中每1秒会进行一次重做日志文件的fsync操作。
当该参数值为1(默认)时,表示事务提交时必须调用一次fsync操作。
当该参数值为2时,表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作。在这个设置下,当MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库后会丢失文件系统缓存刷新到重做日志文件那部分事务。
虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或者2来提高事务提交的性能,但是需要牢记的是,这种设置方法丧失了事务的ACID特性。
在MySQL数据库中还有一种二进制日志(bin log),其用来进行point-in-time的恢复以及主从复制(Replication)环境的建立。
重做日志和二进制日志的区别是:
首先,重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中任何存储引擎对于数据库的更改都会产生二进制日志。
其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。
此外,两种日志记录写入磁盘的时间点不同,如图7-6所示。二进制日志只在事务提交完成后进行一次写入。而InnoDB存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的
2. log block
在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓冲,重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块都是512字节。
若一个页中产生的重做日志数量大于512字节,那么需要分隔为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区块大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要double write技术。
3.log group
log group为重做日志租,其中有多个重做日志文件。log group是一个逻辑上的概念,并没有一个实际存储的物理文件来表示log group 信息。log group由多个重做日志文件组成,每个log group中日志文件的大小是相同的。重做日志文件中存储的就是之前在log buffer中保存的log block,因此其也是根据块的方式进行物理存储的管理,每个块的大小与log block一样,同样为512字节。在InnoDB存储引擎运行过程中,log buffer根据一定的规则将内存中log block刷新到磁盘。这个规则就是:
- 事务提交时
- 当log buffer中有一半的内存空间已经被使用时
- log checkpoint 时
5. LSN
LSN是Log Sequence Number的缩写,其代表的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:
- 重做日志写入的总量
- checkpoint的位置
- 页的版本
LSN表示事务写入重做日志的字节的总量。LSN不仅记录在重做日志中,还存在每个页中。在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN,在页中,LSN表示该页最后刷新时LSN的大小,因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。例如,页P1的LSN为1000,而数据库启动时,InnoDB检测到写入重做日志中的LSN为1300,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到P1页中。同样的,对于重做日志中LSN小于P1页的LSN,不需要进行重做,因为P1页的LSN表示页已经刷新到该位置。
6. 恢复
InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志要快得多。由于checkpoint表示已经刷新到磁盘上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。
7.2.2 undo
1.基本概念
事务有时候需要进行回滚操作,这是就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或者语句由于某种因为失败了,又或者用一条rollback语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。
redo存放在重做日志文件中,与redo不同,undo存放在数据库内部的一个特殊段(segement)中,这个段称为undo段(undo segement)。undo段位于共享表空间内。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。
InnoDB存储引擎回滚时,它实际上做的是与先前相反的工作。对于每个insert,innodb存储引擎会执行一个delete;对于每一个delete,innodb存储引擎会执行一个insert;对于每一个update,innodb存储引擎会执行一个相反的update,将修改前的行放回去。
除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成的。当用户读取一行记录时,若该行记录已经被其他事务占用了,当前事务可以通过undo读取之前的行版本信息,以此来实现非锁定读。
undo log会产生redo log,也就是undo log的产生会伴随redo log的产生,这是因为undo log也需要持久性的保护。
2. undo存储管理
InnoDB存储引擎对undo的管理同样采用段的方式。但是这个段和之前介绍的端有所不同。首先InnoDB存储引擎有rollback segement,每个回滚段中记录了1024个undo log segement,而在每个undo log segement段中进行段的申请。
事务在undo log segement分配页并写入undo log的这个过程同样需要写入重做日志。当事务提交时,InnoDB存储引擎会做以下两件事情:
- 将undo log放入列表中,以供之后的purge操作
- 判断undo log所在的页是否可以重用,若可以分配给下个事务使用。
事务提交后并不能马上删除undo log及undo log所在的页。这是因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务提交时将undo log放入一个链表中,是否可以最终删除undo log及undo log所在页有purge线程来判断。
判断undo log所在页是否可以重用的标准是判断undo页使用的空间是否小于4/3 ,若是则表示该undo 页可以被重用,之后新的undo log记录在当前undo log的后面。
3. undo log格式
在InnoDB存储引擎中,undo log分为:
- insert undo log
- update undo log
insert undo log是指在insert操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见(事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作。
update undo log记录的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
delete操作并不直接删除记录,而只是将记录标记为已删除,也就是将记录的delete flag 设置为 1 。而记录最终的删除是在purge操作中完成的。
update主键的操作其实分两步完成。首先将原主键记录标为已删除,之后再插入一条新的记录。
7.2.3 purge
delete和update操作可能并不直接删除原有的数据。例如对于delete from t where a = 1;
,表 t 上列 a 有聚集索引,列 b 上有辅助索引。对于上述delete操作,仅是将主键列等于1的记录delete flag 设置为 1,记录并没有被删除,即记录还是存在于B+树中。其次,对于辅助索引上 a 等于 1,b等于 1 的记录同样没有做任何处理,甚至没有产生undo log。而真正删除这行记录的操作其实被 "延时" 了,最终在purge操作中完成。
**7.2.4 group commit **
为了提供 fsync 的效率,当前数据库都提供了 group commit 的功能,即一次fsync可以刷新确保多个事务日志被写入文件。对于InnoDB存储引擎来说,事务提交时会进行两个阶段的操作:
1)修改内存中事务对应的信息,并且将日志写入重做日志缓冲。
2)调用fsync将确保日志都从重做日志缓冲写入磁盘。
步骤 2)相对步骤 1)是一个较慢的过程,这是因为存储引擎需要与磁盘打交道。当有事务进行这个过程时,其他事务可以进行步骤 1)的操作,正在提交的事务完成提交操作后,再次进行步骤 2),可以将多个事务的重做日志通过一次fsync刷新到磁盘,这样就大大地减少了磁盘的压力,从而提高了数据库的整体性能。对于写入或者更新较为频繁的操作,group commit的效果尤为明显。
7.3 事务控制语句
在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行commit操作。因此要显示地开启一个事务需要使用命令BEGIN,START TRANSACTION,或者执行命令 SET AUTOCOMMIT = 0,禁用当前会话的自动提交。
事务控制语句:
- start transaction|begin:显式的开启一个事务。
- commit:提交事务
- rollback:回滚事务,并撤销正在进行的所有未提交的修改。
- savepoint indentifier:在事务中创建一个保存点
- release savepoint identifier:删除一个事务的保存点
- rollback to identifier:回滚到某个保存点
- set transaction:用来设置事务的隔离级别
在存储过程总只能使用 start transaction 语句来开启一个事务
commit和commit work基于基本上是一致的,都是用来提交事务。不同之处在于commit work用来控制事务结束之后的行为是chain还是release的。如果是chain方式,那么事务就变成了链事务。
可以通过参数completion_type来进行控制,该参数默认为0,表示没有任何操作,在这种设置下commit和commit work是完全等价的。当该参数值为1时,commit work等同于commit and chain,表示马上自动开启一个相同隔离级别的事务。当该参数值为2时,commit work等同于commit and release。在事务提交之后会自动断开与服务器的连接。
** 7.4 隐式提交的SQL语句 **
以下这些SQL语句会产生一个隐式的提交操作,即执行完这些语句之后,会有一个隐式的commit操作。
DDL语句:alter database...upgrade data directory name,alter event,alter procedure,alter table,alter view,create database,create event,create index,create procedure,create table,create trigger,create view,drop database,drop event,drop index,drop procedure,drop table,drop trigger,drop view,pename table,truncate table。
用来隐式地修改MySQL架构的操作:create user,drop user,grant,rename user,revoke,set password
管理语句:analyze table,cache index,check table,load index into cache,optimize table,repair table。
此外,truncate table语句是DDL语句,虽然和对整张表执行delete的结果是一样的,但它是不能回滚的。