老生常谈的Mysql事务与MVCC

什么是事务?事务是并发控制的基本单位。事务是一个操作序列,这些操作要么都执行,要么都不执行。每个事务执行结束都应该保证数据的一致性

事务的四大特性

  • 原子性
    • 原子性指整个数据库的事务是一个不可分割的工作单位,要么都执行成功,要么都不执行。事务中若有一个操作执行失败,需要将事务中的所有操作恢复到执行事务之前的状态
  • 一致性
    • 指事务将数据库从一种状态转变成为下一种一致性的状态。也就是事务执行前后,两种状态是一样的,数据库的完整性约束不会被破坏。
    • 一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见
  • 隔离性
    • 事务与事务之间是相互隔离的,互相不会影响到对方的数据状态。
  • 持久性
    • 事务一旦提交,就是永久的。事务的执行会被持久化

事务的隔离级别

  1. 读未提交:一个事务还没提交时,他做的变更就能被其他事务看到
  2. 读提交:一个事务提交之后,他做的变更才会被其他事务看到
  3. 可重复读(默认):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的
  4. 串行化:对同一记录,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行

并发导致的问题

  • 脏读
    • 当数据库中事务A正在修改一个数据但是还未提交或回滚,另一个事务B来读取了修改后的内容并且使用了,之后事务A提交了,此时就引起了脏读。(仅会发生在读未提交的隔离级别)
  • 不可重复读
    • 在一个事务A中多次操作数据,在事务操作过程中(没有最终提交),事务B也才做了处理,并且该值发生了改变,这时候就会导致A在事务操作的时候,发现数据与第一次不一样了。(仅会发生在读未提交、读提交的隔离级别)
  • 幻读
    • 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就成为幻读。
    • 幻读是指当事务不是独立执行的时候发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好像发生了幻觉一样,一般解决幻读的方法是增加范围锁,锁定检索范围为只读,这样就避免了幻读。

不可重复读与幻读区别

  • 不可重复读的重点是修改

同样的条件,读取过的数据再次读取发现值不一样。

  • 幻读的重点在于新增或者删除

同样的条件,两次读取的记录数量不同。
从解决办法来看:
不可重复读只要在事务内锁住满足条件的那条记录
幻读需要锁住满足条件的记录及其相近的记录,需要锁住一个范围

MVCC

  • 当前读:读取的数据库记录都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是一种悲观锁操作。insert、delete、update及select for update、select lock in share mode都属于当前读。
  • 快照读:基于多版本并发控制也即MVCC,快照读读取数据并不是当前最新的数据,可能是之前历史版本的数据(事务开始前的数据),可以解决幻读等问题。普通的select查询就是属于快照读。

MVCC即多版本并发控制,主要是为了提高数据库的并发性能。MVCC是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念。这个概念需要具体功能去实现,这个具体实现就是快照读。
一般通过两种组合提升并发性能

  • MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突

原理

主要是版本链、undo日志、read view

  1. 版本链:db_row_id是数据库默认为该行记录生成的唯一隐式主键,db_trx_id是当前操作该记录的事务ID,而db_roll_pointer是一个回滚指针,用于配合undo日志,指向上一个旧版本。对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值
  2. undo log:Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
  3. read view(读视图):主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

事务隔离级别与MVCC

上面所讲的Read View用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。
1.RC隔离级别下,是每个快照读都会生成并获取最新的Read View;
2.RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。
幻读问题解决:
快照读:通过MVCC来进行控制的,不用加锁。也就是select等不需要改变数据的操作,MVCC保证你读的数据是事务开始前的数据。
当前读:通过next-key锁(行锁+gap锁)来解决问题的。
注:MVCC控制不能完全解决幻读问题,当同一个事务中两次快照读中间执行了一次当前读,那么当前读就可能还存在幻读。

两阶段提交

最后三步:

  1. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  2. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  3. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

这其中redolog的先prepare和后commit也就是两阶段提交。

简单总结

  1. 读未提交是最低级别,存在所有并发问题。
  2. 读提交通过MVCC解决脏读问题(就是修改了数据还没提交,但是其他事务读取的时候还能读到),MVCC保证每次 快照读 的时候建立一个 读视图 ,并且只有在提交之后才能被记录到版本链中。但是还存在不可重复读和幻读问题。
  3. 可重复读通过 MVCC 保证**每个事务第一个快照读的时候创建一个 读视图 **,保证同一个事务内的重复快照读,读到的是同样的版本数据。解决了不可重复读的问题(同一个事务中两次读取一行数据的中间,有其他事务修改数据并且提交),因为其他事务的修改只会往版本链后面增加记录,但你在这个事务中只会读一开始的那个版本。
  4. 在可重复读的级别下也无法完全解决幻读问题(两次读取(范围)的中间,产生了新的数据),MVCC只能解决 快照读 的幻读问题,同样是第一个快照读创建读视图。但是在一个事务中执行 当前读 操作的时候,当前读可能会和别的事务提交的新插入的数据冲突,所以也有幻读。通过加间隙锁解决
  5. 当同一个事务内两次快照读之间存在当前读的情况下,读视图会重新生成,导致幻读的产生。
  6. 串行化可以完全解决所有问题。
posted @ 2022-04-13 09:37  aaayi  阅读(58)  评论(0编辑  收藏  举报