InnoDB 多版本并发控制(MVCC)原理与读一致性

各位老铁,双击点赞走一波!今天咱们来聊聊InnoDB存储引擎里的MVCC(多版本并发控制),这玩意儿听起来高大上,但其实就像咱们平时用的版本控制系统,比如Git,只不过它玩的是数据库里的数据版本。

开场白:数据库并发的那些烦恼事儿

想象一下,你正在银行柜台存钱,同时另一个人在ATM机上取钱。如果没有一套好的机制,银行的账目可能就会乱成一锅粥,你的钱存不进去,他的钱也取不出来,甚至更糟糕。

这就是数据库并发带来的问题。多个事务同时操作同一份数据,如果处理不当,就会出现各种奇奇怪怪的现象,比如:

  • 脏读(Dirty Read): 你看到了别人还没提交的修改,结果人家后来又回滚了,你白高兴一场,读了个寂寞。就像你偷看了隔壁老王刚写的日记,结果他第二天又撕了,你看到的都是幻觉。
  • 不可重复读(Non-repeatable Read): 你两次读取同一条数据,结果发现不一样了,中间被别人改过了。就像你早上称体重是120斤,晚上再称就变成125斤了,你怀疑人生了。
  • 幻读(Phantom Read): 你两次执行同样的查询,结果返回的记录数不一样了,中间被别人插入或删除了数据。就像你数绵羊,数着数着发现多了一只或者少了一只,你开始怀疑自己的智商了。

这些现象,统称为并发异常,必须想办法解决。而MVCC,就是InnoDB用来解决这些问题的神器之一。

MVCC:给数据搞个版本管理

MVCC的核心思想是:对数据进行多版本管理,读不阻塞写,写不阻塞读。 啥意思呢?

简单来说,就是每当你要修改一条数据时,不是直接在原来的数据上修改,而是创建一个新的版本。这样,其他事务就可以继续读取旧版本的数据,而不会受到你的修改的影响。等到你提交了事务,新版本的数据才会生效。

你可以把数据库想象成一个时间轴,每个数据都拥有多个历史版本,就像电影里的平行宇宙一样。

  • 版本号: 每个版本都有一个唯一的版本号,用来标识它的创建时间。
  • 可见性: 每个事务都有一个“可见性”的概念,它只能看到版本号小于等于自己事务开始时间的版本。也就是说,它只能看到在自己事务开始之前已经提交的数据。

这样,每个事务都像生活在自己的时间线里,互不干扰。

MVCC背后的三大法宝

InnoDB实现MVCC主要依赖于以下三个关键技术:

  1. 隐藏列(Hidden Columns): InnoDB会在每行数据中添加三个隐藏列:

    • DB_TRX_ID:创建或修改这条记录的事务ID。
    • DB_ROLL_PTR:指向这条记录的上一个版本的回滚指针(Rollback Pointer)。
    • DB_ROW_ID:如果表没有主键或唯一索引,InnoDB会自动生成一个行ID。

    这三列对用户是不可见的,但它们是MVCC的核心组成部分。

    列名 类型 描述
    DB_TRX_ID bigint 创建或修改该行的事务ID
    DB_ROLL_PTR bigint 指向上一个版本的回滚指针
    DB_ROW_ID bigint 如果表没有主键或唯一索引,InnoDB自动生成的行ID
  2. Undo Log(回滚日志): Undo Log记录了每次修改之前的旧版本数据。当事务需要回滚或者读取旧版本数据时,InnoDB会通过Undo Log找到对应的版本。

    你可以把Undo Log想象成一个时光机,可以让你回到过去。 每次修改都会生成一个undo log, 存放在特定的回滚段(rollback segment)中, 方便rollback的时候使用, MVCC正是使用undo log来读取旧版本的数据。

  3. Read View(读视图): Read View是事务执行期间InnoDB为该事务创建的一个快照,它包含了当前系统中活跃事务(未提交)的列表。

    Read View决定了事务能看到哪些版本的数据。它的主要作用是判断数据的可见性。

    Read View里面包含了一些重要的信息:

    • trx_ids:当前系统中所有活跃的事务ID列表。
    • up_limit_idtrx_ids列表中最小的事务ID。
    • low_limit_id:当前系统中最大的事务ID + 1。
    • creator_trx_id:创建当前Read View的事务ID。

    InnoDB 使用Read View来判断版本链中的哪个版本对当前事务可见。

Read View 的可见性算法

当事务需要读取某行数据时,InnoDB会根据Read View来判断该行数据的版本是否可见。判断规则如下:

  1. 如果数据的DB_TRX_ID小于up_limit_id,说明该版本是在创建Read View之前就已经提交的事务创建的,对当前事务可见。
  2. 如果数据的DB_TRX_ID大于等于low_limit_id,说明该版本是在创建Read View之后才创建的,对当前事务不可见。
  3. 如果数据的DB_TRX_IDup_limit_idlow_limit_id之间,则需要判断DB_TRX_ID是否在trx_ids列表中。
    • 如果在,说明该版本是由当前Read View创建时活跃的事务创建的,对当前事务不可见(除非DB_TRX_ID等于creator_trx_id,即是当前事务自己创建的)。
    • 如果不在,说明该版本是在创建Read View之前就已经提交的事务创建的,对当前事务可见。

如果当前版本不可见,InnoDB会沿着DB_ROLL_PTR指针,找到上一个版本的数据,然后再次进行可见性判断,直到找到一个可见的版本或者到达版本链的末尾。

举个栗子🌰:MVCC实战演练

假设我们有一张users表,包含idname两个字段。

  1. 初始状态:
    id = 1, name = '张三', DB_TRX_ID = 10, DB_ROLL_PTR = NULL

  2. 事务A(事务ID为20)开始:
    事务A开始时,创建了一个Read View,假设当前的活跃事务列表trx_ids[10, 15]up_limit_id为10,low_limit_id为21,creator_trx_id为20。

  3. 事务B(事务ID为25)修改name李四
    事务B执行UPDATE users SET name = '李四' WHERE id = 1;
    这时,InnoDB会创建一个新的版本:
    id = 1, name = '李四', DB_TRX_ID = 25, DB_ROLL_PTR = 指向上一个版本
    原来的版本仍然存在,并且DB_ROLL_PTR指向了它。

  4. 事务A读取id = 1的记录:
    事务A根据Read View判断,新的版本(DB_TRX_ID = 25)大于low_limit_id(21),不可见。
    然后,InnoDB会沿着DB_ROLL_PTR指针,找到上一个版本(DB_TRX_ID = 10)。
    判断上一个版本,DB_TRX_ID = 10等于up_limit_id = 10, 且10在trx_ids列表里面,但不是creator_trx_id(20), 则版本不可见。
    所以最终事务A读取到的是老的版本,也就是name = '张三'

  5. 事务B提交:
    事务B提交后,新的版本正式生效。

  6. 事务A提交:
    事务A提交后,Read View失效。

通过这个例子,我们可以看到,事务A在事务B修改数据期间,读取到的始终是旧版本的数据,保证了读一致性。

读一致性:MVCC的终极目标

MVCC的核心目标是实现读一致性。 InnoDB 提供了两种读一致性级别:

  1. Read Committed(读已提交): 事务只能读取到已经提交的数据。每次读取都会创建一个新的Read View。
  2. Repeatable Read(可重复读): 事务在整个执行期间,读取到的数据都是一致的。事务开始时创建一个Read View,之后所有的读取都使用这个Read View。

InnoDB 默认的隔离级别是 Repeatable Read,可以防止脏读、不可重复读,但无法防止幻读。

MVCC 与 锁:相辅相成

虽然 MVCC 可以减少锁的使用,提高并发性能,但它并不能完全替代锁。 在某些情况下,仍然需要使用锁来保证数据的一致性。

例如,在更新数据时,InnoDB 仍然需要使用行级锁(Record Lock)来防止多个事务同时修改同一行数据。

总结:MVCC的优势与局限

  • 优势:
    • 提高并发性能:读写不阻塞,减少锁的竞争。
    • 实现读一致性:保证事务读取到的数据是 consistent 的。
  • 局限:
    • 增加了存储空间:需要保存多个版本的数据。
    • 增加了CPU开销:需要进行版本判断和回滚操作。
    • 无法完全替代锁:仍然需要在某些情况下使用锁。

结尾:MVCC,数据库并发的守护神

MVCC 是 InnoDB 存储引擎中一项非常重要的技术,它通过多版本管理和 Read View 机制,实现了读写不阻塞,保证了数据的一致性,为数据库的并发处理提供了强大的支持。

下次当你听到MVCC的时候,别再觉得它高深莫测了,它就像一个默默守护数据库的英雄,在背后默默地工作,保证你的数据安全可靠!

希望今天的讲解能够帮助大家理解 MVCC 的原理。如果觉得有用,记得点赞、评论、转发哦! 咱们下期再见! 👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注