MySQL事务

MySQL中的事务

事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存,是访问并更新数据库中各种数据项的一个执行单元。

事务的特性:ACID

A(Atomicity)原子性

原子性是指整个数据库事务是不可分割的工作单元,即要么都做,要么都不做。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

C(consistency)一致性

一致性指事务将数据库从一种状态转变为下一种一致的状态,在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。

I(isolation)隔离性

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

D(durability)持久性

事务一旦提交,其结果就是永久性的。即使发生宕机,数据库也能将数据恢复。

事务的实现

事务隔离性的实现

MySQL通过锁实现了事务隔离性特性。

锁的定义

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。

锁的类型

InnoDB存储引擎支持行级锁,实现了两种类型的行级锁:

  • 共享锁(S Lock),允许事务读取一行数据。
  • 排他锁(X Lock),允许事务删除或更新一行数据。

如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。如果事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——共享锁和排他锁不兼容。兼容性如下:

| X | S
-|-|-
X | 不兼容 | 不兼容
S | 不兼容 | 兼容
X锁与其他都不兼容,只有S锁相互兼容,S、X锁都是行级锁。

InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一个额外的锁方式,称之为意向锁/意向锁是指将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。若将上锁的对象看成一棵树,对最底层行级别加锁,首先需要对上层粗粒度的对象(数据库、表)加意向锁。层次结构

比如针对记录r加X锁,需要分别对数据库A,表、页上加意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例:在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已经存在S锁,之后事务需要对记录r在表1上加IX,由于不兼容,所以该事务需要等待表锁操作的完成。

InnoDB存储引擎支持意向锁设计比较简单,意向锁即为表级别的锁。设计目的主要是为了在一个事物中解析下一行将被请求的锁类型。所支持的两种意向锁如下:

  • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
  • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁

表级意向锁与行级意向锁兼容性如下:

IS IX S X
IS 兼容 兼容 兼容
IX 兼容 兼容 不兼容
S 兼容 不兼容 兼容
X 不兼容 不兼容 不兼容
一致性非锁定读

MySQL通过行多版本控制的方式来实现一致性的非锁定读。读取正在进行update或者delete的数据行时,实际上读取的此行的快照数据,从而避免了其他事物锁定当前行的情况下读等待(等待行锁的释放)的情况。非锁定读极大地提高了数据库的并发性,读取不会占用和等待表上的锁。但是在不同的事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本,称之为行多版本技术。由此带来的并发控制称之为多版本并发控制。

在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB存储引擎使用非锁定的一致性读。然而对于快照数据的定义不相同:在READ COMMITTED隔离级别下,非一致性读总是读取被锁定行的最新一份快照数据;在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

MVCC(多版本并发控制)和MySQL事务隔离级别,将专门介绍。

一致性锁定度

MySQL的InnoDB存储引擎默认的隔离级别为REPEATABLE READ,select操作使用一致性非锁定读。某些情况下,用户需要显示地针对数据库读取操作进行加锁保证数据逻辑的一致性。InnoDB存储引擎对于select语句支持两种一致性的锁定读操作:

  • select … for update

  • select … lock in share mode

    select … for update对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。select … lock in share mode读读取的行记录加一个S锁,其他事物可以向被锁定的行加S锁,如果加X锁,将会被阻塞。

    对于一致性非锁定读,即使读取的行已经被执行了 select … for update,也是可以进行读取的。select … for update和select … lock in share mode必须在一个事务中,事务提交了,锁也就释放了。

锁的算法
  1. Record Lock:单个行记录上的锁
    Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
  2. Gap Lock:间隙所,锁定一个范围,但不包含记录本身
  3. Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
    Next-Key Lock结合了Gap Lock和Record Lock的一种锁定算法,设计的目的是为了解决幻读(以后介绍)。当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。
锁问题
  1. 脏读
    提到脏读,就涉及到脏数据和脏页的概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的。在刷新到磁盘之前,日志都已经被写入到了重做日志文件。脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交。
    对于脏页的读取,是正确的。脏页是指数据库实例内存和磁盘的异步造成的,不会影响数据的一致性(脏页最终都会刷新到磁盘中)。
    脏数据是指未提交的数据,也就是两个事务,其中一个事务对数据进行了修改但是还未提交,另一个事务读取了未提交的数据。这就违反了数据库的隔离性。
  2. 不可重复读
    不可重复读是指在同一个事务多次读取同一数据集合,结果集有差异的情况。两个事务,其中一个事务访问了一个数据集合,另一个事务同样也访问了同一个数据集合,并做了一些DML操作。第一个事务在进行读取时,返回了与第一次不一致的结果,这就发生了在同一个事务内两次读到的数据是不一样的情况。这就是不可重复读。
    脏读是读取到未提交的数据,不可重复读读到的是已经提交的数据,但是违反了数据库事务一致性的要求。
  3. 幻读
    幻读是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。幻读主要是针对读取结果条目数不同。
  4. 更新丢失
    更新丢失就是一个事物的更新操作会被另一个事物的更新操作所覆盖,从而导致数据的不一致。
死锁

死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。如果没有外力介入,事务都将无法推进下去。针对死锁问题,有两种解决方案:

  1. 死锁检测
    数据库一般采用wait-for graph(等待图)的方式来进行死锁检测,这是一种比较积极主动的死锁检测方式,起到了一个预防的作用。数据库会保存两种信息:1,锁的信息链表;2,事务等待链表。这两种链表可以构造一张图,若存在回路,就代表存在死锁,因此资源间互相发生等待。每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常InnoDB存储引擎选择回滚undo量最小的事务。
  2. 超时机制
    当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。InnoDB存储引擎中的innodb_lock_wait_timeout用来设置超时时间。

事务原子性、一致性、持久性的实现

MySQL通过redo log和undo log实现了A、C、D的特性。redo log称为重做日志,用来保证事物的原子性和持久性,。undo log用来保证事物的一致性,帮助事务回滚及MVCC功能。redo和undo都可以视为一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。redo通常记录的是物理日志,记录的是页的物理修改操作;undo是逻辑日志,根据每行记录进行记录。

redo
  1. 概念和功能
    重做日志用来实现事务的持久性,即事务的ACID中的D。由两部分组成:一是内存中的重做日志缓冲(redo log buffer),是易失的;二是重做日志文件(redo log file),其是持久的。
    InnoDB存储引擎通过Force Log at Commit机制实现事物的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才算完成。redo log是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。
    为了确保每次重做日志都写入重做日志文件,每次都将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。重做日志缓冲先写入文件系统缓冲,为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。
    innodb_flush_log_at_trx_commit控制重做日志刷新到磁盘的策略。1代表事务提交时必须调用一次fsync操作;0表示事务提交时不进行写入重做日志操作,这个操作在master thread中完成,master thread每1秒会进行一次重做日志文件的fsync操作;2表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作,数据库宕机但是操作系统不宕机时,并不会导致事物的丢失。
undo
  1. 概念和功能
    事务如果需要回滚,就需要undo。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo。这样用户执行事务失败,或者主动发起rollback语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。
    undo是逻辑日志,只是将数据库逻辑的恢复到原来的样子。所有的修改都逻辑地取消了,但是数据结构和页本身在回滚之后可能不相同。
    InnoDB存储引擎回滚时,它实际上做的是与先前相反的工作。对于每一个INSERT,InndoDB存储引擎会完成一个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 log格式
    1. insert undo log。指在insert操作过程中产生的undo log。因为insert操作的记录支队事务本身可见,对其他事务不可见,所以该undo log可以在事务提交后直接删除,不需要purge操作。
  3. 2 update undo log。指的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
坚持原创分享