|
把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称之为一个事务。
有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。
事务的语法
开启事务
BEGIN [WORK];
START TRANSACTION;?
在START TRANSACTION语句后边跟随几个修饰符,就是它们几个:
READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
READ WRITE:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
WITH CONSISTENT SNAPSHOT:启动一致性读(先不用关心啥是个一致性读,后边的章节才会唠叨)。
提交事务
COMMIT [WORK];
手动中止事务
如果我们写了几条语句之后发现上边的某条语句写错了,我们可以手动的使用下边这个语句来将数据库恢复到事务执行之前的样子:
ROLLBACK [WORK]
ROLLBACK语句是我们程序员手动的去回滚事务时才去使用的,如果事务在执行过程中遇到了某些错误而无法继续执行的话,事务自身会自动的回滚。?
支持事务的存储引擎
目前只有InnoDB和NDB存储引擎支持事务。
如果某个事务中包含了修改使用不支持事务的存储引擎的表,那么对该使用不支持事务的存储引擎的表所做的修改将无法进行回滚。
自动提交
MySQL中有一个系统变量autocommit,默认值为ON,也就是说默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。
关闭这种自动提交的两个方法:
隐式提交
当事务不会进行自动提交的时候,如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了COMMIT语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交,这些会导致事务隐式提交的语句包括:?
-
定义或修改数据库对象的数据定义语言(Data definition language,缩写为:DDL)。 所谓的数据库对象,指的就是数据库、表、视图、存储过程等等这些东西。当我们使用CREATE、ALTER、DROP等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务,就像这样: BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条语句
... # 事务中的其它语句
CREATE TABLE ... # 此语句会隐式的提交前边语句所属于的事务
-
隐式使用或修改mysql数据库中的表 当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。 -
事务控制或关于锁定的语句 当我们在一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务,比如这样: BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条语句
... # 事务中的其它语句
BEGIN; # 此语句会隐式的提交前边语句所属于的事务
或者当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。 或者使用LOCK TABLES、UNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务。 -
加载数据的语句 比如我们使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。 -
关于MySQL复制的一些语句 使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。 -
其它的一些语句 使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、?LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。
保存点
就是在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:
SAVEPOINT 保存点名称;
想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK和SAVEPOINT是可有可无的):
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
删除某个保存点,可以使用这个语句:
RELEASE SAVEPOINT 保存点名称;
redo日志
如果在事务提交后发生了宕机等意外,我们只在内存中的Buffer Pool修改了页面,磁盘中未修改。如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
-
刷新一个完整的数据页太浪费了 有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太浪费了。 -
随机IO刷起来比较慢 一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,倒霉催的是该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改成2我们只需要记录一下:
将第0号表空间的100号页面的偏移量为1000处的值更新为2。
这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为重做日志,英文名为redo log,我们也可以土洋结合,称之为redo日志。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo日志刷新到磁盘的好处如下:?
?
redo日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥就好了,设计InnoDB的大叔把这种极其简单的redo日志称之为物理日志,并且根据在页面中写入数据的多少划分了几种不同的redo日志类型:。
新的类型的redo日志既包含物理层面的意思,也包含逻辑层面的意思,具体指:
redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。?
在执行语句的过程中产生的redo日志被设计InnoDB的大叔人为的划分成了若干个不可分割的组。
乐观插入:该数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,那么事情很简单,直接把记录插入到这个数据页中,记录一条类型为MLOG_COMP_REC_INSERT的redo日志就好了。
悲观插入:该数据页剩余的空闲空间不足,遇到这种情况要进行所谓的页分裂操作,也就是新建一个叶子节点,然后把原先数据页中的一部分记录复制到这个新的数据页中,然后再把记录插入进去,把这个叶子节点插入到叶子节点链表中,最后还要在内节点中添加一条目录项记录指向这个新创建的页面。很显然,这个过程要对多个页面进行修改,也就意味着会产生多条redo日志。
针对类似于悲观插入,规定在执行这些需要保证原子性的操作时必须以组的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。
redo日志的原子性是靠分组来保证的。
如何把这些redo日志划分到一个组里边儿呢?
在该组中的最后一条redo日志后边加上一条特殊类型的redo日志,该类型名称为MLOG_MULTI_REC_END,type字段对应的十进制数字为31,该类型的redo日志结构很简单,只有一个type字段。? ? ? ? 在系统崩溃重启进行恢复时,只有当解析到类型为MLOG_MULTI_REC_END的redo日志,才认为解析到了一组完整的redo日志,才会进行恢复。否则的话直接放弃前边解析到的redo日志。

?redo日志缓冲区
在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,翻译成中文就是redo日志缓冲区,我们也可以简称为log buffer。这片内存空间被划分成若干个连续的redo log block。

?不同的事务可能是并发执行的,所以事务T1、T2之间的mtr可能是交替执行的。
?
redo日志文件
mtr运行过程中产生的一组redo日志在mtr结束时会被复制到log buffer中,可是这些日志总在内存里呆着也不是个办法,在一些情况下它们会被刷新到磁盘里,比如:
-
log buffer空间不足时 log buffer的大小是有限的(通过系统变量innodb_log_buffer_size指定),如果不停的往这个有限大小的log buffer里塞入日志,很快它就会被填满。设计InnoDB的大叔认为如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 -
事务提交时 我们前边说过之所以使用redo日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘。 -
将某个脏页刷新到磁盘前,会保证先将该脏页对应的 redo 日志刷新到磁盘中(再一次 强调,redo 日志是顺序刷新的,所以在将某个脏页对应的 redo 日志从 redo log buffer 刷新到磁盘时,也会保证将在其之前产生的 redo 日志也刷新到磁盘)。 -
后台线程不停的刷刷刷 后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。 -
正常关闭服务器时 -
做所谓的checkpoint时(我们现在没介绍过checkpoint的概念,稍后会仔细唠叨,稍安勿躁) -
其他的一些情况...
redo日志文件组
MySQL的数据目录(使用SHOW VARIABLES LIKE 'datadir'查看)下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。
磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。
?Log Sequence Number
记录已经写入的redo日志量(包括了写到log buffer而没有刷新到磁盘的日志),设计了一个称之为Log Sequence Number的全局变量。
在向log buffer中写入redo日志时不是一条一条写入的,而是以一个mtr生成的一组redo日志为单位进行写入的。而且实际上是把日志内容写在了log block body处。但是在统计lsn的增长量时,是按照实际写入的日志量加上占用的log block header和log block trailer来计算的。
redo日志是首先写到log buffer中,之后才会被刷新到磁盘上的redo日志文件。
flushed_to_disk_lsn:表示刷新到磁盘中的redo日志量的全局变量。
当有新的redo日志写入到log buffer时,首先lsn的值会增长,但flushed_to_disk_lsn不变,随后随着不断有log buffer中的日志被刷新到磁盘上,flushed_to_disk_lsn的值也跟着增长。如果两者的值相同时,说明log buffer中的所有redo日志都已经刷新到磁盘中了。
checkpoint
判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。即一个事务,如果他的redo日志已经写到日志了,而他修改的脏页还在缓冲池中,则代表redo日志在磁盘空间是不可以被覆盖的。如果脏页已刷新到了磁盘中,说明可以被覆盖。
全局变量checkpoint_lsn来代表当前系统中可以被覆盖的redo日志总量是多少,这个变量初始值也是8704。
脏页被刷新到了磁盘,mtr_1生成的redo日志就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint。
崩溃恢复
万一数据库挂了,那redo日志可是个宝了,我们就可以在重启时根据redo日志中的记录就可以将页面恢复到系统崩溃前的状态。
确定恢复的起点
checkpoint_lsn之前的redo日志对应的脏页都已经被刷新到磁盘中了,既然它们已经被刷盘,我们就没必要恢复它们了。对于checkpoint_lsn之后的redo日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,我们不能确定,所以需要从checkpoint_lsn开始读取redo日志来恢复页面。
需拿到最近发生的checkpoint对应的checkpoint_lsn值以及它在redo日志文件组中的偏移量checkpoint_offset。
确定恢复的终点
对于被填满的block来说,LOG_BLOCK_HDR_DATA_LEN值永远为512。如果该属性的值不为512,那么就是它了,它就是此次崩溃恢复中需要扫描的最后一个block。
怎么恢复

?按照redo日志的顺序依次扫描checkpoint_lsn之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来。
加快恢复过程:1.使用哈希表,2.跳过已经刷新到磁盘的页面

?undo日志
|