MySQL中的MVCC

MySQL中的MVCC

MVCC的概念

MVCC: Multi-Version Concurrency Control,即多版本并发控制.
是乐观锁的一种实现方式.

并发事务存在的问题:

  1. 更新丢失(Lost Update):多个事务同时更新同一行时,最后的更新会覆盖之前的更新。
  2. 脏读(Dirty Reads):一个事务对记录的未提交修改被其他事务读取到。
  3. 不可重复读(Non-Repeatable Reads):一个事务内多次查询相同记录结果不一致。
  4. 幻读(Phantom Reads):一个事务重新查询之前检索过的数据,发现出现新的数据。

解决:

  • 加读写锁。
  • 一致性快照读(MVCC)。

特点

  • 用来提高数据库高并发场景下的吞吐性能。
  • MySQL中InnoDB引擎支持MVCC。
  • 比加行锁效率高,开销低。
  • 读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用。
  • 可以基于乐观锁悲观锁实现。
  • 使用行级锁(row_level_lock),而非行锁(innodb_row_lock).
  • 同一个事务能够看到数据一致的视图.
  • 事务开始的时间不同,看到相同表的数据可能不同.

基本原理

  • 通过保留某个时间点的快照实现的.

基本特征

  • 每行数据都存在一个版本,每次数据更新时都更新该版本.
  • 修改数据时复制当前版本的数据进行修改,各个事务之间互不影响.
  • 保存时比较版本号,成功(commit)则覆盖原记录,失败则放弃(rollback).

InnoDB存储引擎MVCC实现策略

细节:

  • 每一行保存两个隐藏列:当前行创建时版本号删除时版本号.
  • 版本号是系统版本号,每开始一个新事务,系统版本号自增.而事务的版本号为事务开始时的系统版本号.
  • 每个事务有自己的版本号.

MVCC下的InnoDB的增删改查

插入数据

  1. 设记录的版本号为当前事务的版本号。
  2. 向表中插入数据。
  3. create version设置为当前事务的版本号,delete version为空。

更新操作

  1. 将旧的记录标记为已删除,delete version为当前事务版本号。
  2. 插入一行新的记录,create version为当前事务版本号,delete version为当前版本号。

删除操作

  1. 将待删除的行的delete version设置为当前事务版本号。

查询操作

记录需满足两个条件:

  • delete version为空或者设置的版本号大于当前事务的版本号(即:删除操作发生在当前事务之后)
  • create version小于等于当前事务版本号(即:记录创建在当前事务之前)

注:

  1. MVCC只适用于MySQL中的读已提交(Read Committed)和可重复读(Repeatable Read)。
  2. Read uncommitted存在脏读,即:读到未提交事务的数据行。
  3. 串行化是对表加锁。

InnoDB MVCC 实现原理

实现方式:

  • 每一行记录都有两个隐藏列:DATA_TRX_IDDATA_ROLL_PTR。(若没有主键,则还有一个隐藏主键)
  • DATA_TRX_ID:记录最近更新这条记录的事务ID(6字节)
  • DATA_ROLL_PTR:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)
  • DB_ROW_ID:行标识(隐藏单增ID),没有主键时主动生成(6字节)

多事务并发操作数据

特征:

  • 不同事务对同一行的更新操作产生多个版本
  • 通过回滚指针将这些版本链接成一条Undo Log链

更新操作流程:

  1. 将待操作的行加排他锁
  2. 将该行原本的值拷贝到Undo Log中,DB_TRX_IDDB_ROLL_PTR保持不变。(形成历史版本)
  3. 修改该行的值,更新该行的DATA_TRX_ID为当前操作事务的事务ID,将DATA_ROLL_PTR指向第二步拷贝到Undo Log链中的旧版本记录。(通过DB_ROLL_PTR可以找到历史记录)
  4. 记录Redo Log,包括Undo Log中的修改。
  • INSERT操作:产生新的记录,其DATA_TRX_ID为当前插入记录的事务ID。
  • DELETE操作:软删除,将DATA_TRX_ID记录下删除该记录的事务ID,真正删除操作在事务提交时完成。

一致性读的实现

  • RU隔离级别下 ==> 直接读取版本的最新记录。
  • SERIALIZABLE隔离级别 ==> 通过加锁互斥访问数据实现。
  • RC和RR隔离级别 ==> 使用版本链(ReadView,可读视图)

RR下的ReadView生成

特点:

  • 每个事务首次执行SELECT语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView。
  • 每个事务后续的SELECT操作复用其之前生成的ReadView
  • UPDATE,DELETE,INSERT对一致性读snapshot无影响。

示例:事务A,B同时操作同一行数据

  • 若事务A的第一个SELECT在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT操作也无法读到事务B修改后的数据。
  • 若事务A的第一个SELECT在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。

RC下的ReadView生成

特点:

  • 每次SELECT执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView。
  • ReadView的组成:(当前活跃事务ID列表,称为m_ids)
    • 最小值为up_limit_id:最先开始的事务。
    • 最大值为low_limit_id:最后开始的事务。
  • ID越小,事务开始的越早;ID越大,事务开始的越迟。
  • 若被访问版本的trx_id小于up_limit_id == > 生成该版本的事务在ReadView生成前就已提交 == > 该版本可以被当前事务访问。
  • 若被访问版本的trx_id大于low_limit_id == > 生成该版本的事务在ReadView生成之后才提交 == > 该版本不可被当前事务访问 == > 通过Undo Log找到之前的版本重新判断。
  • 若被访问的版本在up_limit_idlow_limit_id之间 == > 需要判断trix_id是否在m_ids中存在 == > 若存在,则生成该版本的事务还在活跃,则该版本不可访问,可由Undo Log找到之前的版本进行重新判断;若不存在,则创建ReadView时该版本对应的事务已提交,可以访问该版本。
  • 找到记录后,还要判断delete_flag是否为true,若为true,则该记录已被删除,不返回;若为false,则记录可以返回。

注:对于ID较大的事务较ID较小的事务先提交的情况,即事务发生晚但提交的早

  • RC的本质:每一条SELECT都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关
  • RR的本质:第一条SELECT生成ReadView前,已经提交的事务的修改可见。

参考:

原文地址:https://www.cnblogs.com/truestoriesavici01/p/13224749.html