事务(Transaction) 是数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很有可能被破坏。事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。
InnoDB存储引擎中的事务完全符合ACID的特性。ACID是以下4个词的缩写:
- 原子性(atomicity)
- 一致性(consistency)
- 隔离性(isolation)
- 持久性(durability)
事务隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log 和 undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
重做日志(Redo)
重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。
为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。
InnoDB存储引擎允许用户手工设置非持久性的情况发生,以此提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。由于并非强制在事务提交时进行一次fsync操作,显然这可以显著提高数据库的性能。但是当数据库发生宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。
参数innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。
-
1:该参数的默认值为1,表示事务提交时必须调用一次fsync操作。还可以设置该参数的值为0和2。
-
0:表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而在master thread中每1秒会进行一次重做日志文件的fsync操作。
-
2:表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作。在这个设置下,当MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。
测试表的sql
|
|
测试的过程p_load
|
|
当innodb_flush_log_at_trx_commit为默认值1时,执行结果:
|
|
使用ProcessMonitor查看写文件的情况,可以发现mysql进程在疯狂刷盘,因为每次提交都会去写重做日志。
将参数innodb_flush_log_at_trx_commit设置为0,然后再调用p_load,插入50000条数据,发现执行时间缩短为了6秒。
同时使用ProcessMonitor查看写文件的次数,约为1266次,明显少于之前的十多万次。
而形成这个现象的主要原因是:后者大大减少了fsync的次数,从而提高了数据库执行的性能。虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或2来提高事务提交的性能,但是需要牢记的是,这种设置方法丧失了事务的ACID特性。
重做日志块
在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。
若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。
重做日志组
log group为重做日志组,其中有多个重做日志文件。虽然源码中已支持log group的镜像功能,但是在ha_innobase.cc文件中禁止了该功能。因此InnoDB存储引擎实际只有一个log group。
log group是一个逻辑上的概念,并没有一个实际存储的物理文件来表示log group信息。log group由多个重做日志文件组成,每个log group中的日志文件大小是相同的,且在InnoDB 1.2版本之前,重做日志文件的总大小要小于4GB(不能等于4GB)。从InnoDB 1.2版本开始重做日志文件总大小的限制提高为了512GB。InnoSQL版本的InnoDB存储引擎在1.1版本就支持大于4GB的重做日志。
重做日志文件中存储的就是之前在log buffer中保存的log block,因此其也是根据块的方式进行物理存储的管理,每个块的大小与log block一样,同样为512字节。在InnoDB存储引擎运行过程中,log buffer根据一定的规则将内存中的log block刷新到磁盘。这个规则具体是:
- 事务提交时
- 当log buffer中有一半的内存空间已经被使用时
- log checkpoint时
对于log block的写入追加(append)在redo log file的最后部分,当一个redo log file被写满时,会接着写入下一个redo log file,其使用方式为round-robin。
LSN
LSN是Log Sequence Number的缩写,其代表的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:
- 重做日志写入的总量
- checkpoint的位置
- 页的版本
LSN表示事务写入重做日志的字节的总量。例如当前重做日志的LSN为1 000,有一个事务T1写入了100字节的重做日志,那么LSN就变为了1100,若又有事务T2写入了200字节的重做日志,那么LSN就变为了1300。可见LSN记录的是重做日志的总量,其单位为字节。
可以使用 show engine innodb status\G;
查看innodb存储引擎的状态,其中LOG部分有如下面的输出:
|
|
Log sequence number表示当前的LSN,Log flushed up to表示刷新到重做日志文件的LSN,Last checkpoint at表示刷新到磁盘的LSN。
虽然在上面的例子中,Log sequence number和Log flushed up to的值是相同的,但是在实际生产环境中,该值有可能是不同的。因为在一个事务中从日志缓冲刷新到重做日志文件并不只是在事务提交时发生,每秒都会有从日志缓冲刷新到重做日志文件的动作。
恢复
InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志,要快很多。与此同时,InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度。
由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。对于图7-12中的例子,当数据库在checkpoint的LSN为10 000时发生宕机,恢复操作仅恢复LSN 10000~13000范围内的日志。
InnoDB存储引擎的重做日志是物理日志,因此其恢复速度较之二进制日志恢复快得多。例如对于INSERT操作,其记录的是每个页上的变化。
Undo日志
重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。**但是事务有时还需要进行回滚操作,这时就需要undo。**因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。
与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。
用户通常对undo有这样的误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子——但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
例如,用户执行了一个INSERT 10W条记录的事务,这个事务会导致分配一个新的段,即表空间会增大。在用户执行ROLLBACK时,会将插入的事务进行回滚,但是表空间的大小并不会因此而收缩。因此,当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也需要持久性的保护。
undo类似与git操作中的回滚操作,这个回滚就是生成一段跟原来操作相反的操作,然后叠加到当前的操作。
与undo相关的参数主要包括3,可以使用 show variables like 'innodb_undo%'
查看这些参数。
-
innodb_undo_directory: 参数innodb_undo_directory用于设置rollback segment文件所在的路径。这意味着rollback segment可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为“.”,表示当前InnoDB存储引擎的目录。
-
**innodb_undo_logs:**参数innodb_undo_logs用来设置rollback segment的个数,默认值为128。在InnoDB1.2版本中,该参数用来替换之前版本的参数innodb_rollback_segments。
-
**innodb_undo_tablespaces:**参数innodb_undo_tablespaces用来设置构成rollback segment文件的数量,这样rollback segment可以较为平均地分布在多个文件中。设置该参数后,会在路径innodb_undo_directory看到undo为前缀的文件,该文件就代表rollback segment文件。
Binlog日志
purge
delete和update操作可能并不直接删除原有的数据。而真正删除这行记录的操作其实被“延时”了,最终在purge操作中完成。
purge用于最终完成delete和update操作。这样设计是因为InnoDB存储引擎支持MVCC,所以记录不能在事务提交时立即进行处理。这时其他事物可能正在引用这行,故InnoDB存储引擎需要保存记录之前的版本。而是否可以删除该条记录通过purge来进行判断。若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作。可见,purge操作是清理之前的delete和update操作,将上述操作“最终”完成。而实际执行的操作为delete操作,清理之前行记录的版本。
事务控制语句
在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行COMMIT操作。因此要显式地开启一个事务需使用命令BEGIN、START TRANSACTION,或者执行命令SET AUTOCOMMIT=0,禁用当前会话的自动提交。每个数据库厂商自动提交的设置都不相同,每个DBA或开发人员需要非常明白这一点,这对之后的SQL编程会有非凡的意义,因此用户不能以之前的经验来判断MySQL数据库的运行方式。在具体介绍其含义之前,先来看看用户可以使用哪些事务控制语句。
-
**START TRANSACTION | BEGIN:**显式地开启一个事务。
-
**COMMIT:**要想使用这个语句的最简形式,只需发出COMMIT。也可以更详细一些,写为COMMIT WORK,不过这二者几乎是等价的。COMMIT会提交事务,并使得已对数据库做的所有修改成为永久性的。
-
**ROLLBACK:**要想使用这个语句的最简形式,只需发出ROLLBACK。同样地,也可以写为ROLLBACK WORK,但是二者几乎是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
-
**SAVEPOINT identifier∶**SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT。
-
**RELEASE SAVEPOINT identifier:**删除一个事务的保存点,当没有一个保存点执行这句语句时,会抛出一个异常。
-
**ROLLBACK TO[SAVEPOINT]identifier:**这个语句与SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。例如可以发出两条UPDATE语句,后面跟一个SAVEPOINT,然后又是两条DELETE语句。如果执行DELETE语句期间出现了某种异常情况,并且捕获到这个异常,同时发出了ROLLBACK TO SAVEPOINT命令,事务就会回滚到指定的SAVEPOINT,撤销DELETE完成的所有工作,而UPDATE语句完成的工作不受影响。
-
**SET TRANSACTION:**这个语句用来设置事务的隔离级别。InnoDB存储引擎提供的事务隔离级别有:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。
START TRANSACTION、BEGIN语句都可以在MySQL命令行下显式地开启一个事务。但是在存储过程中,MySQL数据库的分析器会自动将BEGIN识别为BEGIN…END,因此在存储过程中只能使用START TRANSACTION语句来开启一个事务。
COMMIT和COMMIT WORK语句基本是一致的,都是用来提交事务。不同之处在于COMMIT WORK用来控制事务结束后的行为是CHAIN还是RELEASE的。如果是CHAIN方式,那么事务就变成了链事务。
用户可以通过参数completion_type来进行控制,该参数默认为0,表示没有任何操作。在这种设置下COMMIT和COMMIT WORK是完全等价的。当参数completion_type的值为1时,COMMIT WORK等同于COMMIT AND CHAIN,表示马上自动开启一个相同隔离级别的事务。
可以使用命令 SHOW VARIABLES LIKE 'completion_type'
查看该参数
事务隔离级别
InnoDB存储引擎默认支持的隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。
查看当前会话的隔离级别:
|
|
在InnoDB存储引擎中,可以使用命令SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL XXX
来设置当前会话或全局的事务隔离级别,其中XXX表示要设置的隔离级别,例如:
|
|
如果想在MySQL数据库启动时就设置事务的默认隔离级别,那就需要修改MySQL的配置文件,在[mysqld]中添加如下行:
|
|