理解MySQL事务

201 阅读6分钟

什么是事务(Transaction)?

可以理解为,将一步或者几步数据库操作序列作为一个逻辑执行单元,然后可以将这个单元视为一个最小执行单元,每一个最小执行单元最后只有两个结果,执行成功或者执行失败,换而言之,这一系列的数据库操作只能全部成功或者全部失败,即使只有一个操作执行失败也视为执行失败,这个也叫做事务的原子性。

image.png 事务一执行成功,事务二执行失败。对于已提交的事务来说,该事务对数据库所做的修改将永久生效,如果中途发生中断或者错误,那么该事务期间对数据库所做的修改将会回滚到没有执行该事务之前的状态。

事务的特性(ACID)

  • 原子性(Atomicity):一个事务种的所有操作,要么全部完成,要么不完成。如果中途发生中断或者错误,就会被回滚到事务开始前的状态。
  • 一致性(Consistency):数据库的完整性不会因为事务的执行而受到破坏,也就是数据库中只包含事务提交后的结果,事务的一致性是由原子性保证的。
  • 隔离性(Isolation):事务之间的执行互不干扰,隔离性防止多个事务并发执行时由于交叉执行而导致的数据的不一致性。
  • 持久性(Durability):事务一旦提交后对数据的修改就是永久的,对数据所做的任何改变,都要记录到永久存储器中。

InnoDB引擎通过什么技术来保证事务的这四个特性的呢?

  • 原子性和持久性通过redo log来保证
  • 一致性是通过undo log来保证
  • 隔离性是通过MVCC或者锁机制来保证

并行事务会引发什么问题?

MySQL服务端允许多个客户端连接,所以在同时处理多个事务的时候就可能出现脏读(dirty read)不可重复读(non-repeatable read)、**幻读(phantom read)**的问题

  • 脏读(dirty read) : 一个事务读到了另一个未提交事务修改过的数据
  • 不可重复读(non-repeatable read) :在一个事务内多次读取同一个数据,出现前后两次读到的数据不一样的情况
  • 幻读(phantom read) :在一个事务内多次查询某个符合查询条件的记录数量,出现前后两次查询到的记录数量不一样的情况

这三个问题的严重性:脏读>不可重复读>幻读

SQL标准提出了四种隔离级别:

  • 读未提交(read uncommitted) : 一个事务未提交时,其它事务能够看见其做的改变。
  • 读已提交(read committed) : 一个事务提交后,其它事务才能看到其做的改变。
  • 可重复读(repeatable read) : 一个事务执行过程中看到的数据跟其启动时看到的是一致的。(MySQL InnoDB默认的隔离级别)
  • 串行化(serializable) : 对记录加上读写锁,多个事务排队执行。

不同隔离级别发生的现象不一样:

读未提交读已提交可重复读串行化
脏读、不可重复读、幻读不可重复读、幻读幻读

解决幻读问题不建议将隔离级别升级到串行化,这样会导致性能降低,InnoDB引擎的默认隔离级别是可重复读,其可以通过next-key lock锁来锁住记录之间的"间隙"和记录本身,防止其它事务在这个记录之间插入新的记录,从而避免了幻读现象。

这四种隔离级别是如何实现的呢?

  • 读未提交(read uncommitted) : 直接读取
  • 读已提交(read committed) : 在每一个读取数据前都生成一个Read View
  • 可重复读(repeatable read) : 在启动事务时生成一个Read View,整一个事务期间都在用这个Read View
  • 串行化(serializable) : 通过添加读写锁的方式避免并行访问

可重复读隔离级别是如何实现的?

上面说到可重复读隔离界别是在启动事务时生成一个Read View,那么首先得了解什么是Read View 什么是Read View?

Read View中有四个重要字段

  1. m_ids : 指的是创建Read View时当前数据库中活跃且未提交事务的事务id列表
  2. min_trx_id : 指的是创建Read View时当前数据库中活跃且未提交的事务中最小事务的事务id,也即是m_ids中的最小值
  3. max_trx_id : 创建Read View时当前数据库中应该给下一个事务的id值
  4. creator_trx_id : 指的是创建该Read View 的事务的事务id

对于使用InnoDB存储引擎的数据库列表,它的聚簇索引记录中都包含下面两个隐藏列:

  • trx_id : 当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务id记录在trx_id隐藏列表里面
  • roll_pointer : 每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是个指针,指向每一个旧版本的记录。

可重复读隔离级别就是在启动时创建了Read View,然后在事务期间读取数据的时候,在找到数据后,会将该记录的trx_id 和该事务的Read View里的字段做个比较,如果记录的trx_id比该事务的Read View中的creator_trx_id要小,且不在m_ids列表里,意味着这条记录在该事务前已经提交,所以该记录对该事务可见。如果记录的trx_id比该事务的Read View中的creator_trx_id要大,且在m_ids列表里,这意味着该事务读到的是和自己同时启动的另一个事务修改到的数据,这时候就沿着undo log往下找旧版本,直到找到trx_id小于或等于该事务id的第一条记录。

读已提交隔离级别是如何实现的?

与可重复读隔离级别类似,只不过创建Read View的时机不同,可重复读隔离级别是在事务启动时创建Read View,而读已提交隔离级别是在每一次读操作之前都会生成一个新的Read View。在读取数据时,如果记录的trx_id比该事务的Read View中的creator_trx_id要大,且在m_ids列表里,这意味着该事务读到的是和自己同时启动的另一个事务修改到的数据,这时候就沿着undo log往下找,如果记录的trx_id比该事务的Read View中的creator_trx_id要大,且不在m_ids列表里,这意味着该事务读到的是和自己同时启动的另一个事务修改到的数据而且已经提交过了,于是可以对此记录进行读取

MVCC

通过记录版本控制链来控制并发事务访问同一个记录时的行为叫做MVCC(多版本并发控制),可重复读隔离级别和读已提交隔离级别是通过事务的Read View里的字段和记录中的两个隐藏列的对比来控制并发事务访问同一个记录。