[MySQL八股]事务特性、隔离级别、MVCC

事务特性、隔离级别、MVCC

事务特性(ACID)

  • A(Atomicity):原子性。事务是不可分割的最小操作单元,要么全部成功,要么全部失败。(通过undo log实现)
  • C(consistency):一致性。事务完成时,必须使所有的数据都保持一致状态。(通过undo log实现)

undolog:记录的是逻辑日志,当事务回滚时,通过逆操作来恢复数据。逻辑日志记录的是数据库事务的逻辑操作。它描述了事务对数据库所做的更改,例如INSERT、UPDATE或DELETE操作。它的作用包括两个:提供回滚与MVCC

  • D(Durability):持久性。事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。(通过redo log实现)

redo log 记录的是数据页的物理变化,可以理解为物理日志,一旦服务宕机,可以用来同步数据。物理日志记录的是数据库存储空间的物理修改。它通常记录数据页(或数据块)的物理地址和这些页上的具体更改。

  • I(Isolation): 隔离性。数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下进行。(MVCC多版本并发控制实现)

MVCC因为MVCC被问得多,也更加复杂,因此放在第三节MVCC单独讲。

事务并发问题

在这里插入图片描述
那么,如何解决并发事务的问题呢?答案是对事务进行隔离解决。隔离级别越高,数据越安全,但是性能越低。

事务的隔离级别定义了事务之间的隔离程度,即一个事务内部的操作对其他并发事务的可见性程度。SQL标准定义了四个事务隔离级别,从低到高依次是:

  1. 读未提交(Read Uncommitted):一个事务可以读取另一个未提交事务的数据。
  2. 读已提交(Read Committed):一个事务只能读取已经提交事务所做的修改。这是大多数数据库系统的默认隔离级别(但MySQL的默认隔离级别是REPEATABLE READ)。
  3. 可重复读(Repeatable Read):对同一字段的多次读取结果都是一致的。在这个级别下,InnoDB使用MVCC来实现。
  4. 串行化(Serializable):最高的隔离级别,所有的事务依次逐个执行,这样事务之间就不可能产生干扰。
    在这里插入图片描述

事务的隔离级别可以通过MVCC来实现。下一节,我们详细介绍MVCC。

MVCC(多版本并发控制)

MVCC是一种并发控制的方法,它允许非锁定读操作,即读操作不会阻塞写操作,写操作也不会阻塞读操作。MVCC是通过保存数据在某个时间点的快照来实现的,这样读操作就可以读取该时间点之前的数据版本,而写操作可以修改当前的数据版本。

在InnoDB存储引擎中,MVCC是通过Undo日志来实现的。当一个事务修改数据时,它不会直接修改原始数据,而是生成一个新的数据版本,并将旧的数据版本保存在Undo日志中。这样,其他事务在读取数据时,就可以通过Undo日志找到它所需的数据版本。

简而言之,MVCC

  • 定义是:MVCC是一种并发控制的方法,它通过对数据库中的每个数据行保存多个版本的信息,来允许多个事务同时读取和修改数据,而不会相互干扰。
  • 目标是:提高数据库的并发性能,减少数据冲突和不一致性的发生。

MVCC的实现原理

MVCC的实现包括以下部分:

  1. 版本链(Versioning Chain)

    • 在InnoDB引擎表中,聚簇索引记录的每行数据都包含两个隐藏列:trx_idroll_pointer
    • trx_id:用于存储每次对某条聚簇索引记录进行修改的事务ID。
    • roll_pointer:是一个指针,指向这条聚簇索引记录的上一个版本在Undo日志中的位置。当记录被修改时,旧版本会被写入Undo日志,并通过roll_pointer与新版本相连,形成版本链。
  2. Undo日志(Undo Log)

    • Undo日志用于存储数据的旧版本,以便在需要时进行回滚操作或支持MVCC的非锁定读。
    • 当记录被修改时,旧的数据版本会被写入Undo日志,并且新版本的记录通过roll_pointer指向旧版本。
  3. Read-View

    • Read-View是一个数据结构,它包含了当前系统中活跃的事务列表(即已经开始但尚未提交的事务)。
    • 当一个事务需要读取数据时,它会使用Read-View来判断哪些版本的数据对当前事务是可见的。
    • Read-View通过比较事务ID和活跃事务列表来决定数据版本的可见性。

    那么它具体是怎么决定版本的可见性的呢?

    首先我们要了解,Read-View的组成如下:

    • creator_trx_id:创建这个Read-View的事务ID。
    • trx_ids:在生成Read-View时当前系统中活跃的读写事务的事务ID列表。
    • up_limit_id:活跃的事务中最小的事务ID。
    • low_limit_id:表示生成Read-View时系统中应该分配给下一个事务的ID值。注意,它并不是trx_ids中的最大值,而是系统中事务ID的下一个可能值。

    接下来,我们就可以进行版本可见性的判断了

    • 当一个事务需要读取数据时,它会检查被访问数据记录的trx_id(即创建该数据版本的事务ID)。

    • 如果被访问版本的trx_id小于up_limit_id(即trx_ids中的最小值),说明生成该版本的事务在Read-View生成前就已经提交了,因此该版本对当前事务可见。

    • 如果被访问版本的trx_id大于low_limit_id,说明生成该版本的事务在Read-View生成后才生成,因此该版本对当前事务不可见。

    • 如果被访问版本的trx_idup_limit_idlow_limit_id之间(包括这两个值),则需要进一步检查:

      如果trx_idtrx_ids列表中,说明在生成Read-View时,生成该版本的事务仍然是活跃的,因此该版本不可见。

      如果trx_id不在trx_ids列表中,说明在生成Read-View时,生成该版本的事务已经提交,因此该版本可见。

  4. 事务ID和版本比较

    • 当一个事务尝试读取一条记录时,它会检查该记录的trx_id与Read-View中的事务ID列表。
    • 如果trx_id小于列表中最小的事务ID,则这个数据版本对当前事务是可见的(因为修改它的事务在当前事务开始前已经提交了)。
    • 如果trx_id在列表中的最小和最大事务ID之间,则需要进一步检查该事务ID是否在活跃事务列表中。如果在,说明修改该记录的事务尚未提交,因此这个数据版本对当前事务不可见;如果不在,说明修改该记录的事务已经提交,因此这个数据版本对当前事务是可见的。
    • 如果trx_id大于列表中最大的事务ID,则这个数据版本是在当前事务开始后产生的,因此不可见。
  5. 非锁定读

    • MVCC允许非锁定读操作,即读操作不会阻塞写操作,写操作也不会阻塞读操作。
    • 这是因为读操作只是简单地读取数据的一个历史版本(通过版本链和Undo日志),而不是直接读取最新版本的数据。
  6. 写操作

    • 当一个事务执行写操作时,它不会直接覆盖原始数据,而是创建一个新的数据版本,并将旧版本保留在Undo日志中。
    • 新版本的数据通过trx_idroll_pointer与旧版本相连,形成版本链。

这么说其实是非常晦涩难懂的,接下来我会举个例子,帮助我们理解

假设我们有一个简单的银行转账系统,其中有一个账户表(Account),包含账户ID(ID)、账户余额(Balance)等字段。现在有两个事务T1和T2几乎同时发生,T1要查询账户余额,而T2要修改账户余额(即转账)。

初始状态

账户表(Account)中有一条记录,ID为1,Balance为1000。

事务T1(查询账户余额)
  1. 开始事务:T1启动,并获得一个唯一的事务ID,例如T1_ID = 100。
  2. 执行查询:T1执行SELECT Balance FROM Account WHERE ID = 1; 此时,由于没有其他事务修改过该记录,T1读取到的是当前最新的版本,即Balance = 1000。
  3. 快照读:T1的这次读操作是快照读,因为它读取的是数据的一个历史版本(在这个例子中,因为是最新版本,所以看起来和当前版本一样)。但重要的是,T1读取的这个版本是在T1事务开始时的一个数据快照。
事务T2(修改账户余额)
  1. 开始事务:T2启动,并获得一个唯一的事务ID,例如T2_ID = 101。
  2. 执行修改:T2执行UPDATE Account SET Balance = Balance - 200 WHERE ID = 1; 这将账户余额从1000减少到800。
  3. 版本管理
    • 数据库系统不会直接修改原始数据行的Balance字段,而是为该行创建一个新的版本。新版本包含新的Balance值(800)和T2的事务ID(T2_ID = 101)。
    • 原始版本的数据(Balance = 1000)被写入Undo日志,并通过回滚指针(roll_pointer)与新版本相连,形成版本链。
事务T1(继续执行)
  1. 再次查询:假设T1在T2提交之后再次执行相同的查询操作。
  2. 版本可见性判断
    • 数据库系统检查T1的Read-View(包含当前活跃事务列表和最小、最大事务ID)。
    • 由于T2已经提交(假设T2_ID = 101小于Read-View中的最大事务ID,且不在活跃事务列表中),因此T2所做的修改对T1是可见的。
    • 但是,因为T1是基于其开始时的数据快照进行读取的,所以T1仍然看到Balance = 1000(这是T1开始时的数据版本)。
  3. 结果:T1的第二次查询结果仍然是Balance = 1000,尽管T2已经修改了该值。

写在最后

这一篇内容比较多,也比较难懂,大家下去可以多消化一下。根据博主的经验,特别是MVCC这一块被问得很多,如果说不清,说得没有逻辑,在面试中肯定是会扣分。所以我们要形成自己的理解链条,能够保证在面试的高压中能够用清晰的条理去回答问题,而不是死记硬背。

----本文图片与文字部分参考黑马程序员

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值