Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL实现事务的ACID

详解MySQL事务的ACID如何实现

作者:码老思

事务(Transaction)是并发控制的基本单位,所谓的事务呢,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位,本文给大家详细介绍了MySQL事务的ACID如何实现,需要的朋友可以参考下

事务是什么?

事务(Transaction)是并发控制的基本单位。所谓的事务呢,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

在介绍事务的特性之前,我们先看下MySQL的逻辑架构,

如上图所示,MySQL服务器逻辑架构从上往下可以分为三层:

后续讨论主要以InnoDB为主。

事务有什么特征?

事务的特性,可以总结为如下4个方面:

谈到事务的四大特性,不得不说一下MySQL事务的隔离机制,在不同的数据库连接中,一个连接的事务并不会影响其他连接,这是基于事务隔离机制实现的。在MySQL中,事务隔离机制分为了四个级别:

上述四个级别,越靠后并发控制度越高,也就是在多线程并发操作的情况下,出现问题的几率越小,但对应的也性能越差,MySQL的事务隔离级别,默认为第三级别:Repeatable read可重复读。

按照严格的标准,只有同时满足ACID特性才是事务;但是目前各大数据库厂商的实现中,真正满足ACID的事务很少。例如MySQL的NDB Cluster事务不满足持久性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性;InnoDB默认事务隔离级别是可重复读,完全满足ACID的特性。因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,解决的方案有两种:

Read Committed隔离级别:每次select都生成一个快照读。 Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。

总结:事务的隔离性由MVCC和锁来实现,而原子性、一致性、持久性通过数据库的redo和undo日志来完成。接下来会详细介绍其实现原理。

MVVC如何实现事务的隔离?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

MVVC是一种用来解决读-写冲突的无锁并发控制,简单总结就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题:在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能;同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

MVVC的实现,依赖4个隐式字段undo日志 ,Read View 来实现的。

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本;delete flag没有展示出来。

undo log

InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。undo log主要分为3种:

这里举一个例子,比如我们想更新Person表中的数据,有两个事务先后对同一行数据进行了修改,那么undo log中,不会仅仅只保存最近修改的旧版本记录,而是通过链表的方式将不同版本连接起来。在下面的例子中,

ReadView

在上面介绍undo log的时候可以看到,undo log中维护了每条数据的多个版本,如果新来的一个事务也访问这同一条数据,如何判断该读取这条数据的哪个版本呢?此时就需要ReadView来做多版本的并发控制,根据查询的时机来选择一个当前事务可见的旧版本数据读取。

当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView一般包含四个核心内容:

可以通过如下的示意图进一步理解ReadView,

假设目前数据库中共有T1~T5这五个事务,T1、T2、T4还在执行,T3已经回滚,T5已经提交,此时当有一条查询语句执行时,就会利用MVCC机制生成一个ReadView,由于前面讲过,单纯由一条select语句组成的事务并不会分配事务ID,因此默认为0,所以目前这个快照的信息如下:

{ "creator_trx_id" : "0", "trx_ids" : "[1,2,4]", "up_limit_id" : "1", "low_limit_id" : "6" }

当我们拿到ReadView之后,如何判断当前的事务能够看到哪些版本的数据,这里会遵循一个可见性算法,简单来讲就是将要被修改数据的最新记录的DB_TRX_ID(即当前事务ID),与ReadView维护的其他事务ID进行比较,来确定当前事务能看到的最新老版本。

这里结合MySQL的算法实现来看,下面是MySQL 8.1里面关于这个可见性算法的实现。可以看到,整体流程如下:

// https://dev.mysql.com/doc/dev/mysql-server/latest/read0types_8h_source.html

/** Check whether the changes by id are visible.
  @param[in]    id      transaction id to check against the view
  @param[in]    name    table name
  @return whether the view sees the modifications of id. */
  [[nodiscard]] bool changes_visible(trx_id_t id, const table_name_t &name) const {
    ut_ad(id > 0);
 
    if (id < m_up_limit_id || id == m_creator_trx_id) {
      return (true);
    }
 
    check_trx_id_sanity(id, name);
 
    if (id >= m_low_limit_id) {
      return (false);
    } else if (m_ids.empty()) {
      return (true);
    }
 
    const ids_t::value_type *p = m_ids.data();
 
    return (!std::binary_search(p, p + m_ids.size(), id));
  }

MVCC原理总结

MVCC主要由下面两个核心功能组成,undo log实现数据的多版本,ReadView实现多版本的并发控制。

这里举一个例子回顾下整个流程:

假设有A和B两个并发事务,其中事务A在修改第一行的数据,而事务B准备读取这条数据,那么B在具体执行过程中,当出现SELECT语句时,会根据MySQL的当前情况生成一个ReadView。

如果undo log中存在某行数据的多个版本,那么在实际中会根据隐藏列roll_ptr依次遍历整个链表,按照上面的流程,找到第一条满足条件的数据并返回。

RC、RR不同级别下的MVVC机制

ReadView是一个事务中只生成一次,还是每次select时都会生成呢?这个问题和MySQL的事务隔离机制有关,RC和RR下的实现有些许不同。

undo log和redo log在事务里面有什么用?

上面介绍了事务隔离性的实现原理,即通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。保证了较好的并发性能。

而事务的原子性、一致性和持久性则是通过事务日志实现,主要就是redo log和undo log。了解完下面这些内容,那就明白了其中的原理和实现。

1. redo log

为什么需要redo log

在 MySQL 中,如果每一次的更新要写进磁盘,这么做会带来严重的性能问题:

因此每当有一条新的数据需要更新时,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。之后,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术

WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。

整个过程如下:

什么是redo log

redo log 是物理日志,记录了某个数据页做了什么修改,比如对A表空间中的B数据页C偏移量的地方做了D更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。

redo log有什么好处

总结来看,有一下两点:

redo log如何写入磁盘?

redo log并不是每次写入都会刷新到数据页,而是采取一定的策略周期性的刷写到磁盘上。所以,它其实包括了两部分,分别是内存中的日志缓冲(redo log buffer)和磁盘上的日志文件(redo log file)

由于MySQL处于用户空间,而用户空间下的缓冲区数据是无法直接写入磁盘的,因为中间必须经过操作系统的内核空间缓冲区(OS Buffer)。所以,redo log buffer 写入 redo logfile 实际上是先写入 OS Buffer,然后操作系统调用 fsync() 函数将日志刷到磁盘。过程如下:

MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

参数值含义
0(延迟写)事务提交时不会将 redo log buffer 中日志写到 os buffer,而是每秒写入os buffer 并调用 fsync() 写入到 redo logfile 中。也就是说设置为 0 时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
1(实时写、实时刷新)事务每次提交都会将 redo log buffer 中的日志写入 os buffer 并调用 fsync() 刷到 redo logfile 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能差。
2(实时写、延迟刷新)每次提交都仅写入到 os buffer,然后是每秒调用 fsync() 将 os buffer 中的日志写入到 redo log file。

三种方案总结如下:

在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:

如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。

总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。 上述两项变量的设置保证了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。

redo log file结构是怎么样的?

InnoDB 的 redo log 是固定大小的。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么 redo log file 可以记录 4GB 的操作。从头开始写。写到末尾又回到开头循环写。如下图:

上图中,write pos 表示 redo log 当前记录的 LSN (逻辑序列号) 位置,一边写一遍后移,写到第 3 号文件末尾后就回到 0 号文件开头; check point 表示数据页更改记录刷盘后对应 redo log 所处的 LSN(逻辑序列号) 位置,也是往后推移并且循环的。

write pos 到 check point 之间的部分是 redo log 的未写区域,可用于记录新的记录;check point 到 write pos 之间是 redo log 已写区域,是待刷盘的数据页更改记录。

当 write pos 追上 check point 时,表示 redo log file 写满了,这时候有就不能执行新的更新。得停下来先擦除一些记录(擦除前要先把记录刷盘),再推动 check point 向前移动,腾出位置再记录新的日志。

2. undo log

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。 buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。

总结回顾

InnoDB通过MVVC、undo log和redo log实现了事务的ACID特性,

以上就是详解MySQL事务的ACID如何实现的详细内容,更多关于MySQL实现事务的ACID的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文