mysql技术内幕-事务
发表于:2025-03-16 | 分类: 学习

事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。

innodb事务完全符合ACID特性,ACID是以下4个词的缩写:

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

认识事务

  1. 概述
    事务可由一条非常简单的sql语句组成,也可以由一组复杂的sql语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区别于文件的重要特征之一。
    以下为事务的ACID特性以及相关概念:
    A(atomicity),原子性。原子性是指整个数据库事务最不可分割的工作单位。只有事务所有的数据操作都成功,才算整个事务成功。
    C(consistency),一致性。一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。
    I(isolation),隔离性。隔离性还有其他称呼,如并发控制(concurrency control)、可串行化(serializability),锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务不可见,通常是用锁来实现。
    D(durability),持久性。事务一旦提交,其结果是永久性的。即使发生了宕机等故障,数据库也能将数据恢复。要注意的是,只能从事务本身的角度来保证结果的永久性。

  2. 分类
    事务可分为以下几类:

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

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

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

    对于扁平的事务来说,其隐式的设置了一个保存点。然而在整个事务中,只有这一个保存点,因此,回滚只能回滚到事务开始时的状态。保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。

    ***链事务(Chained Transaction )***可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃实时,所有的保存点都将消失,因为保存点是易失的(volatile),而非持久的(persistent)。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。

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

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

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

    嵌套事务的定义:

    1)嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务

    2)处在叶节点的事务是扁平事务。但是每个子事务从根到叶节点的距离可以是不同的。

    3)位于根节点的事务称为顶层事务,其他事务称为子事务,事务的前驱称为父事务,事务的下一层称为儿子事务。

    4)子事务既可以提交也可以回滚。但是它的提交操作并不马上生效,除非其父事务已经提交。因此可以推论,任何子事务都在顶层事务提交后才真正的提交。

    5)树中的任意一个事务的回滚都会引起它所有的子事务一同回滚,故子事务仅保留A、C、I特性,不具有D特性。

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

事务的实现

原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性,undo log用来保证事务的一致性。

redo和undo的作用可以视为是一种恢复操作,redo恢复提交事务修改的页操作,undo回滚行记录到某个特顶版本。两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

  1. redo
    基本概念
    重做日志用来实现事务的持久性,即事务ACID中的D。由两部分组成:一是内存中的重做日志缓冲(redo log buffer),是易失的;二是重做日志文件(redo log file),是持久的。
    innodb通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。innodb中,重做日志由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来保证事务回滚及MVCC的功能。redo log基本都是顺序写的,数据库运行时不需要对redo的文件进行读取操作。undo log是需要进行随机读写的。
    为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,innodb都要调用一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。
    参数innodb_flush_log_at_trx_commit用来控制重做日志刷新磁盘的策略。默认值是1,表示事务提交时必须调用一次fsync操作。设置值为0时表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而master thread中每1秒会进行一次重做日志文件的fsync操作。设置值为2时,表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作,在这个设置下只有操作系统宕机,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。
    mysql中还有一种二进制日志(binlog),用来进行POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。这个和重做日志有很大的区别,首先重做日志是在innodb存储引擎层产生,二进制日志是在mysql数据库上层产生的,mysql中任何存储引擎对于数据库的更改都会产生二进制日志。其次两种日志的内容形式不同,二进制日志是一种逻辑日志,记录的是对应的sql语句,重做日志是物理格式日志,记录的是对于每个页的修改。这两种日志的写入磁盘时间点也不同。

    log block
    innodb中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要两次写技术。

    重做日志块除了日志块本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。重做日志头占用12字节,重做日志尾占用8字节。故每个重做日志块实际存储大小为492字节。log block header由4部分组成,如下表:
    ![](assets/img/mysql/log block header.jpg)

    log group
    log group为重做日志组,其中有很多个重做日志文件。innodb实际只有一个log group。
    log group是一个逻辑上的概念,没有一个实际存储的物理文件来表示log group信息。log group由多个重做日志文件组成,每个log group中的日志文件大小是相同的。
    在innodb运行过程中,log buffer根据一定的规则将内存中的log block刷新到磁盘,以下是具体规则:

    • 事务提交时
    • 当log buffer中有一半的内存空间已经被使用时
    • log checkpoint时

    对于log block的写入追加到redo log file的最后部分,当一个redo log file被写满时,会接着写下一个redo log file,使用的方式为round-robin。

    重做日志格式
    innodb重做日志格式是基于页的。头部格式为:

    头部格式由以下三部分组成:

    • redo_log_type:重做日志类型
    • space:表空间ID
    • page_no:页的偏移量

    LSN
    LSN是Log Sequence Number的缩写,代表地是日志序列号。innodb中,LSN占用8字节,单调递增。LSN含义如下:

    • 重做日志写入的总量
    • checkpoint的位置
    • 页的版本

    LSN表示事务写入重做日志的字节总量,单位为字节。

    恢复
    innodb在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。

  2. undo
    基本概念
    重做日志记录了事务的行为,可以很好的通过其对页进行“重做操作”。但事务有时还需要进行回滚操作,这时就需要undo。对数据库进行修改时innodb不但会产生redo,还会产生一定量的undo。这样如果事务失败了或ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。
    undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。
    undo log会产生redo log,也就是undo log的产生伴随着redo log的产生,这是因为undo log也需要持久性的保护。
    undo的存储管理
    innodb对undo的管理采用段的方式。这个段与之前的段有所不同,innodb由rollback segment,每个回滚段中记录了1024个undo log segment,每个undo log segment中进行undo页的申请。共享表空间偏移量为5的页(0,5)记录了所有rollback segment header所在的页,这个页的类型为FIL_PAGE_TYPE_SYS.
    可通过以下参数设置rollback segment:

    • innodb_undo_directory
    • innodb_undo_logs
    • innodb_undo_tablespaces

    事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志。事务提交时innodb会做两件事情:

    • 将undo log放入列表中,以供之后的purge操作
    • 判断undo log所在的页是否可以重用,若可以分配给下个事务使用

    undo log格式
    在innodb中,undo log分为:

    • insert undo log
    • update undo log

    insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其他事务不可见,故该undo log可以在事务提交后直接删除。不需要进行purge操作。下图展示了insert undo log的格式
    ![](assets/img/mysql/insert undo log.jpg)
    上图中*表示存储字段的压缩。insert 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,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。update undo log结构如下图:
    ![](assets/img/mysql/update undo log.jpg)
    next、start、undo_no、table_id与之前的insert undo log部分相同。type_cmpl有以下可能的值:

    • 12 TRX_UNDO_UPD_EXIST_REC更新non-delete-mark的记录
    • 13 TRX_UNDO_UPD_DEL_REC将delete的记录标记为not delete
    • 14 TRX_UNDO_DEL_MARK_REC将记录标记为delete

    接下来的update_vector表示update操作导致发生改变的列。

    purge

    purge用于最终完成delete和uodate操作。是否删除一条记录需要通过purge来进行判断。若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作。
    innodb_purge_batch_size用来设置每次purge操作需要清理的unodo page数量。
    innodb_max_purge_lag用来控制history list的长度,长度大于该参数时,会“延缓”DML操作。参数默认值为0.,表示不做任何限制。

    group commit

    group commit可以保证一次fsync刷新可以确保多个事务日志被写入文件,这样就大大的减少了磁盘的压力,从而提高了整体数据库的性能。
    mysql5.6开始采用Binary Log Group Commit(BLGC),BLGC的实现方式是将事务提交的过程分为几个步骤来完成。如图所示:

    在mysql上层进行提交时首先按顺序将其放入一个队列中,队列第一个事务称为leader,其他事务称为follower,leader控制着follower的行为,BLGC的步骤分为以下三个阶段:

    • Flush阶段,将每个事务的二进制文件写入磁盘
    • Sync阶段,将内存中的二进制日志刷新到磁盘,若队列中有多个事务,仅一次gsync操作就能完成二进制日志的写入,这就是BLGC。
    • Commit阶段,leader根据顺序调用存储引擎层事务的提交,innodb就支持group commit,因此修复了原先由于锁prepare_commit_mutex导致group commit失效的问题。

    当一组事务进行commit阶段时,其他新事务可以进行flush阶段,从而使group commit不断生效。

事务控制语句

可以使用以下控制语句

  • START TRANSACTION | BEGIN:显示开启一个事务
  • COMMIT:可以写作COMMIT或COMMIT WORK,这两者几乎等价。COMMIT会提交事务,并使得已对数据库做的所有修改称为永久性。
  • ROLLBACK:可以写作ROLLBACK或ROLLBACK WORK,二者几乎等价。回滚会结束用户事务,并撤销正在进行的所有未提交的修改
  • SAVEPOINT identifier:SAVEPOINT 允许在事务中创建一个保存点,一个事务可以有多个SAVEPOINT 。
  • RELEASE SAVEPOINT identifier:删除一个事务的保存点,当没有保存点执行这个语句时,会抛出一个异常。
  • ROLLBACK TO[SAVEPOINT] identifier:这个语句与SAVEPOINT 一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。
  • SET TRANSACTION:这个语句用来设置事务的隔离级别。innodb隔离级别有:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。
  • START TRANSACTION、BEGIN语句都可以在mysql命令行下显式地开启一个事务。但是在存储过程中mysql会将BEGIN识别为BEGIN…END,因此存储过程只能使用STRAT TRANSACTION来开启一个事务。
  • COMMIT和COMMIT WORK语句基本一致,都是用来提交事务。不同的是COMMIT WORK用来控制事务结束后的行为是CHAIN还是RELEASE的。如果是CHAIN,事务就变成了链事务。

隐式提交的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,RENAME 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

计算事务操作的统计

计算每秒事务处理的能力(Transaction Per Second,TPS)的方法是(com_commit+com_rollback)/time。使用这个方法的前提是:所有的事务都必须显式提交,如果有隐式提交和回滚,不会计算到com_commit和com_rollback中。
mysql还有两个参数handle_commit和handle_rollback用于事务统计操作。

事务的隔离级别

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

  • READ UNCOMMITTED:浏览访问,仅针对事务。
  • READ COMMITTED:游标稳定
  • REPEATABLE READ:是2.9999°的隔离,没有幻读保护
  • SERIALIZABLE:隔离或3°的隔离。

innodb默认支持的隔离级别是REPEATABLE READ,与标准sql不同的是,innodb在REPEATABLE READ事务隔离级别下,使用Next-Key锁的算法,因此避免幻读的产生。

分布式事务

  1. MySql数据库分布式事务
    innodb提供了对XA事务的支持,通过XA事务来支持分布式事务的实现。分布式事务是指允许多个独立的事务资源参与到一个全局事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对事务的ACID要求又提高了。在使用分布式事务时,innodb隔离级别必须是SERIALIZABLE。
    XA事务允许不同数据库之间的分布式事务,只要参与在全局事务中的节点都支持XA事务。
    XA事务由一个或多个资源管理器、一个事务管理器以及一个应用程序组成。

    • 资源管理器(Resource Managers):提供访问事务资源的方法。通常一个数据库就是一个资源管理器
    • 事务管理器(Transaction Manager):协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通信
    • 应用程序(Application Program):定义事务的边界,指定全局事务中的操作

    在mysql的分布式事务中,资源管理器就是mysql的数据库,事务管理器为连接mysql服务器的客户端。下图为一个分布式事务的模型

    分布式事务使用两段式提交(to-phase commit)的方式。在第一阶段,所有参与全局事务的节点都开始准备(PREPARE),告诉事务管理其他们准备好提交了。在第二阶段,事务管理器告诉资源管理器执行ROLLBACK还是COMMIT。如果任何一个节点显示不能提交,则所有节点都被告知需要回滚。

  2. 内部XA事务
    在mysql存储引擎与插件之间,有或者在存储引擎与存储引擎之间还存在另外一种分布式事务,称之为内部XA事务。
    最为常见的内部XA事务存在于binlog与innodb存储引擎之间。

不好的事务习惯

  1. 在循环中提交
  2. 使用自动提交
  3. 使用自动回滚

长事务

对于长事务,有时可以通过转化为小批量的事务来进行处理。当事务发生错误时,只需要回滚一部分数据,然后接着上次已完成的事务继续进行。

上一篇:
mysql技术内幕-备份与恢复
下一篇:
mysql技术内幕-锁