MySQL事务与MVCC

108 阅读9分钟

数据库事务

什么是数据库事务/事务四大特性

事务: 一系列sql语句, 要么全成功, 要么全失败.

  • 原子性 (Atomicity) : 事务是不可分割的最小单元, n个连续操作失败了一个, 前面的操作回滚 (要么都成功, 要么都失败)
    • 原子性通过undolog回滚来实现
  • 一致性(Consistency) : 操作前后, 数据库的数据是一致的, 完整性和一致性不能遭到破坏.
    • 举例说明:张三向李四转100元,转账前和转账后的数据是正确的状态,这就叫一致性,如果出现张三转出100元,李四账号没有增加100元这就是了数据错误,就没有达到一致性。 张三和李四账户的余额加起来都是一个恒定的值.
    • 保证了其他三个特性, 一致性就自然实现了.
  • 持久性 (Durability) : 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的, 无法撤销
    • redolog来实现
  • 隔离性 (Isolation) : 多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离, 保证每个事务不受并发影响, 独立执行.
    • mvcc+锁 配合undolog来实现

只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的

隔离性产生的问题

  • 脏读: 一个事务读取到另一个事务未提交的数据

    • 在事务A执行过程中,事务A对数据资源进行了修改,事务B读取了事务A修改后的数据。
    • 由于某些原因,事务A并没有完成提交,发生了RollBack操作,则事务B读取的数据就是脏数据。
  • 不可重复读:

    • 事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致。
    • 在同一个事务中,前后两次读取的数据不一致的现象就是不可重复读(Nonrepeatable Read)。
  • 幻读:

    • 事务A按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了幻觉。(由于解决了不可重复读, 所以该事务读取不到别的事务已提交的数据)
    • 幻读和不可重复读有些类似,但是幻读强调的是集合的增减,而不是单条数据的更新。(比如第一次读是有0条数据, 但是第二次读却有了1条数据).

事务的隔离级别

为了解决以上的问题,主流的关系型数据库都会提供四种事务的隔离级别。事务隔离级别从低到高分别是:读未提交、读已提交、可重复读、串行化。

事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。所以在设置数据库的事务隔离级别时需要做一下权衡,MySQL默认是可重复读的级别

  • 读未提交

    • 读未提交(Read Uncommitted),是最低的隔离级别,所有的事务都可以看到其他未提交的事务的执行结果。只能防止第一类更新丢失,不能解决脏读,可重复读,幻读,所以很少应用于实际项目。
  • 读已提交

    • 读已提交(Read Committed), 在该隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果。可以防止脏读和第一类更新丢失,但是不能解决可重复读和幻读的问题。
  • 可重复读

    • 可重复读(Repeatable Read),MySQL默认的隔离级别。可重复读是快照读, 在该隔离级别下,一个事务多次读同一个数据, 实际上读的是数据快照, 其他事务修改数据在当前事务是不可见的, 这样就可以保证在同一个事务内两次读到的数据是一样的。可以防止脏读、不可重复读、第一类更新丢失、第二类更新丢失的问题,不过还是会出现幻读。
  • 串行化

    • 串行化(Serializable),这是最高的隔离级别。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行(会阻塞)。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常不会用这个隔离级别。

注意: 事务的隔离级别越高, 数据安全性就越高, 但是执行效率越低. 事务的隔离级别越低, 执行效率就越高, 但是数据安全性就越低.

MVCC

什么是 MVCC

MVCC全称Multi-Version Concurrency Control, 多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突, 具体实现就是快照读, 快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的隐式字段、undo log日志、readView。

MVCC 可以为数据库解决什么问题

在并发读写数据库时, 可以做到在 读 (select) 操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能.

同时还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题

MVCC 的实现原理

MVCC的具体实现,依赖于数据库记录中的隐式字段、undo log日志、readView。

在内部实现中,InnoDB 通过数据行的 DB_TRX_ID(最近更新的事务id) 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR(回滚指针) 找到 undo log 版本链中的历史版本。这就是快照读

每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

ReadView

ReadView中记录了

  • 活跃事务的事务 id 列表,注意是一个列表, “活跃事务”指的就是,启动了但还没提交的事务
  • 创建该 Read View 的事务的事务 id
  • 活跃事务中事务 id 最小的事务
  • 全局事务中最大的事务 id 值 + 1

通过比较当前事务id和ReadView中记录的事务id, 就能知道该版本的记录对当前事务是否可见.

MVCC是怎么解决不可重复读的

在RC读已提交下, 在事务中每一次执行快照读时生成ReadView, 这也就造成了每次读取就有不同ReadView, 那么就会读到已提交的事务修改的内容, 造成不可重复读的问题.

解决RR不可重复读主要靠readview, 在隔离级别为可重复读时, 仅在事务中第一次执行快照读时生成ReadView, 后续复用该ReadView.

由于后续复用了ReadView, 所以数据对当前事务的可见性和第一次是一样的, 所以从undolog中读到的数据快照和第一次是一样的, 即便过程中有其他事务修改, 当前事务也读不到.

MVCC是怎么防止幻读的

InnoDB存储引擎在 RR 级别下通过 MVCCNext-key Lock(临键锁) 来解决幻读问题

快照读: 执行普通 select,此时会以 MVCC 快照读的方式读取数据

避免加锁通过MVCC来进行控制, 使其他事务所做的更新对当前事务不可见, 从而防止幻读.

在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”

当前读: 执行 select...for update/lock in share mode、insert、update、delete 等当前读

这些语句执行前都会查询最新版本的数据, 所以是当前读. 通过临键锁next-key-lock锁住空隙, 防止其他事务在查询的范围内插入数据, 从而防止幻读.

在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用 Next-key Lock 临键锁来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读

可重复读能完全避免幻读吗

MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生

例1

  • 事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来
  • 然后事务 B 插入一条 id = 5 的记录,并且提交了事务
  • 事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了

例2

  • 事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
  • 事务 B 往插入一个 id= 200 的记录并提交;
  • 事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读