【MySQL—原理】事务

事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一。

事务的ACID特性

理论上说,事务有着极其严格的定义,它必须同时满足四个特性,即通常所说的事务的ACID特性。值得注意的是,虽然理论上定义了严格的事务要求,但是有些数据库厂商出于各种目的,并没有严格去满足事务的ACID标准。

A(Atomicity),原子性。原子性指整个数据库事务是不可分割的工作单位,只有事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

C(consistency),一致性。一致性主要是指对数据有特定的预期状态,事务中对任何数据的更改都必须满足这些状态约束(或者恒等条件)。在事务开始之前和事务结束以后,数据的完整性没有被破坏。

I(isolation),隔离性。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。

D(durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。

原子性、隔离性和持久性是数据库自身的属性,而一致性更多是应用层的属性。应用程序可能借助数据库提供的原子性和隔离性,来达到一致性,但一致性本身并不源于数据库。因此,字母C其实并不应该属于ACID。

事务分类

从事务理论的角度来说,可以把事务分为以下几种类型:

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

扁平事务(Flat Transaction)是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMITWORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚。

带有保存点的扁平事务(Flat Transactions with Savepoint),除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(Savepoint)用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。

保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。

链事务(Chained Transaction)可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,因为其保存点是易失的(volatile),而非持久的(persistent)。

链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。

链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近一个的保存点。对于锁的处理,两者也不相同。链事务在执行COMMIT后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

嵌套事务(Nested Transaction)是一个层次结构框架。由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制每一个局部的变换。

分布式事务(Distributed Transactions)通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。

对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持。

事务的实现

事务隔离性由锁来实现,原子性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log称为回滚日志,用来帮助事务回滚及MVCC的功能。

有的DBA或许会认为undo是redo的逆过程,其实不然。redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录

redo

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。在InnoDB存储引擎中,有关事务的日志由两部分组成,即redo log和undo log。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。

参数innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数的默认值为1,表示事务提交时必须调用一次fsync操作。还可以设置该参数的值为0和2。0表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而在master thread中每1秒会进行一次重做日志文件的fsync操作。2表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作。在这个设置下,当MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。

虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或2来提高事务提交的性能,但是需要牢记的是,这种设置方法丧失了事务的ACID特性。

在MySQL数据库中还有一种二进制日志(binlog),其用来进行POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。

首先,重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。

其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL语句或者对行的逻辑修改。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。

此外,两种日志记录写入磁盘的时间点不同。二进制日志只在事务提交完成后进行一次写入。而InnoDB存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。

在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。

undo

在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。

与redo不同,undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment),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也需要持久性的保护

事务操作

在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行COMMIT操作。在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。

InnoDB存储引擎中的事务都是原子的,这说明下述两种情况:构成事务的每条语句都会提交(成为永久),或者所有语句都回滚。这种保护还延伸到单个的语句。一条语句要么完全成功,要么完全回滚(注意,这里说的是语句回滚)。因此一条语句失败并抛出异常时,并不会导致先前已经执行的语句自动回滚。所有的执行都会得到保留,必须由用户自己来决定是否对其进行提交或回滚的操作。

一个容易犯的错误是ROLLBACK TO SAVEPOINT,虽然有ROLLBACK,但其并不是真正地结束一个事务,因此即使执行了ROLLBACK TO SAVEPOINT,之后也需要显式地运行COMMIT或ROLLBACK命令。

隐式提交的SQL语句

以下这些SQL语句会产生一个隐式的提交操作,即执行完这些语句后,会有一个隐式的COMMIT操作。

  • DDL语句:ALTER DATABASE...UPGRADE DATA DIRECTORY NAME,ALTER EVENT,ALTER PROCEDURE,ALTER TABLE,ALTER VIEW,CREATE DATABASE,CREATEEVENT,CREATE INDEX,CREATE PROCEDURE,CREATE TABLE,CREATE TRIGGER,CREATE VIEW,DROP DATABASE,DROP EVENT,DROP INDEX,DROP PROCEDURE,DROP TABLE,DROP TRIGGER,DROP VIEW,RENAME TABLE,TRUNCATE TABLE。
  • 用来隐式地修改MySQL架构的操作:CREATE USER、DROP USER、GRANT、RENAMEUSER、REVOKE、SET PASSWORD。
  • 管理语句:ANALYZE TABLE、CACHE INDEX、CHECK TABLE、LOAD INDEX INTOCACHE、OPTIMIZE TABLE、REPAIR TABLE。

注:TRUNCATE TABLE语句是DDL语句,因此虽然和对整张表执行DELETE的结果是一样的,但它不能被回滚。

事务统计

MySQL中内置了两个变量com_commit和com_rollback用于保存提交和回滚的事务统计数量,所以可以使用公式(com_commit+com_rollback)/time来计算TPS。但是利用这种方法进行计算的前提是所有的事务必须都是显式提交的(使用START TRANSACTION | BEGIN语句开启的事务),如果存在隐式地提交和回滚(默认autocommit=1),不会计算到com_commit和com_rollback变量中

事务的隔离级别

ISO和ANIS SQL标准制定了四种事务隔离级别的标准,但是很少有数据库厂商遵循这些标准。比如Oracle数据库就不支持READ UNCOMMITTED和REPEATABLE READ的事务隔离级别。

SQL标准定义的四个隔离级别为:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLEREAD

InnoDB存储引擎默认支持的隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,由于使用了Next-Key Lock锁算法,所以可以避免幻读的产生,这与其他数据库系统(如Microsoft SQL Server数据库)是不同的。所以说,InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED。

在InnoDB存储引擎中,可以使用以下命令来设置当前会话或全局的事务隔离级别:

SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL 
READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE

如果想在MySQL数据库启动时就设置事务的默认隔离级别,那就需要修改MySQL的配置文件,在[mysqld]中添加如下行:

[mysqld]
transaction-isolation = READ-COMMITTED

查看当前会话的事务隔离级别,可以使用:

SELECT @@tx_isolation\G

你可能感兴趣的