InnoDB 事务模型

news2024/10/6 3:04:14

文章目录

  • InnoDB 事务模型
    • 事务ACID特性
    • 事务隔离级别
  • 事务操作
  • 事务并发问题
  • 事务数据
  • 读写类型
    • Consistent Nonlocking Reads 快照读
    • Locking Reads 加锁读
  • MVCC 并发控制
    • 实现原理
      • InnoDB 隐藏列
      • Read View
      • Undo log
      • 实现过程
    • MVCC与隔离级别
    • MVCC和辅助索引
  • 幻读
    • 可重复读
    • MVCC会出现幻读的情况


InnoDB 事务模型

事务ACID特性

事务有以下4个特点

  1. 原子性(Atomicity):指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  2. 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另外一个一致性状态,数据还是准确可靠的,多条DML语句同时成功,同时失败
  3. 隔离性(Isolation):事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  4. 持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,数据持久到硬盘文件,事务才算结束,接下来的其他操作和数据库故障不应该对其有任何影响

事务隔离级别

SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  1. 读未提交(read uncommitted):指一个事务还没提交时,它做的变更就能被其他事务看到;
  2. 读提交(read committed):指一个事务提交之后,它做的变更才能被其他事务看到;
  3. 可重复读(repeatable read):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
  4. 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

针对不同的隔离级别,并发事务时可能发生的现象也会不同。也就是说:

  1. 在读未提交隔离级别下,可能发生脏读、不可重复读和幻读现象;
  2. 在读提交隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
  3. 在可重复读隔离级别下,少数情况会发生幻读现象,但是不会发生脏读和不可重复读现象;
  4. 在串行化隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。

不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,有的数据库只实现了其中几种隔离级别, MySQL 虽然支持 4 种隔离级别,但是与SQL标准中规定的各级隔离级别允许发生的现象却有些区别。

MySQL 在可重复读隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用串行化隔离级别来避免幻读现象的发生,因为串行化会很大地影响性能。

Oracle默认隔离级别是读已提交,MySQL默认的隔离级别为可重复读,四个级别逐渐增强,每个级别解决一个问题,如下表所示:
在这里插入图片描述

事务操作

在InnoDB中,用户所有活动都发生在事务内部。如果启用了autocommit模式,则每个 SQL 语句将自己形成一个事务。默认情况下,每个新的会话都开启了autocommit,因此如果每个 SQL 语句未返回错误,则 MySQL 都会在该 SQL 语句之后进行提交。如果一条语句执行出错,则进行提交或回滚,这个取决于该错误。

如果会话在开启时自动提交,但是中途禁用自动提交,则该会话始终具有打开的事务。 COMMIT或ROLLBACK语句结束当前事务,然后开始新的事务。如果禁用了自动提交的会话在没有显式提交最终事务的情况下结束,则 MySQL 将回滚该事务。

某些语句也会结束事务,就像在执行该语句之前已经完成COMMIT一样

COMMIT 表示在当前事务中所做的更改将成为永久性的,并在其他会话中可见。另一方面,ROLLBACK语句会取消当前事务所做的所有修改。 COMMIT和ROLLBACK都会释放在当前事务期间设置的所有InnoDB锁。

事务并发问题

下面介绍几种常见事务并发问题

  1. 脏读:指在一个事务过程里读取了另一个未提交的事务中的数据。比如事务 T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的,即脏数据

  2. 不可重复读:指对于数据库中的某条数据,一个事务范围内多次查询返回不同的数据值,这里不同是指某一条或多条数据的内容前后不一致,但数据条数相同。出现此问题的原因可能是由于在查询间隔,该事物需要用到的数据被另一个事务修改并提交了。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了其他事务提交的数据,如果没有其他事务提交,那么还是相同的值。需要注意的是在某些情况下不可重复读并不是问题

不可重复读可以这样理解:有一个大事务,要执行很长时间;另外有一堆小事务,很快就执行完成。大事务来回查一个数据。一堆小事务串行改这个数据,改完之后提交成功。大事务虽然读到的也是小事务提交之后的内容,但是这个数据经常被小事务改来改去。

  1. 幻读:例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就和脏读不同),所不同的是不可重复读可能发生在update或者delete操作中,而幻读发生在insert操作中

事务数据

https://dev.mysql.com/doc/refman/8.0/en/innodb-information-schema-transactions.html

读写类型

Consistent Nonlocking Reads 快照读

https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html

Consistent Nonlocking Reads 意为一致性非锁定读取,一般把它叫做快照读,它会使用基于某个时间点的快照信息呈现查询结果,而不考虑同时运行的其他事务执行的更改。如果查询到的数据被另一个事务改变了,则根据undo log的内容重建原始数据。快照读避免了加锁,因为加锁会强制事务等待其他事务完成从而导致并发性降低。因为快照读不会对表加任何锁,当某个会话正在对某个表执行快照读操作时,其他会话可以随便修改该表。

快照读是InnoDB在READ COMMITTEDREPEATABLE READ这两个隔离级别下处理SELECT语句的默认模式,也就是说普通的SELECT语句就会执行快照读操作。不同的隔离级别快照的生成时间不同。

  1. 如果使用REPEATABLE READ隔离级别,快照基于执行第一次读取操作的时间。
  2. 使用 READ COMMITTED 隔离级别,将快照重置为每个一致性读取操作的时间。
  3. 其他隔离级别不需使用快照读

本次查询会看到在该时间点之前提交的事务所做的更改,以及稍后或未提交的事务所做的更改。此规则的例外是查询会看到同一事务中先前语句所做的更改。
如果另一个事务在已经分配给你的时间点后删除了一行并提交,你不会看到该行已被删除。 插入和更新的处理方式类似。

此例外会导致以下异常:如果您更新表中的某些行,则 SELECT 会看到所更新行的最新版本,但它也可能会看到任何行的旧版本。 如果其他会话同时更新同一个表,则异常意味着您可能会看到该表处于数据库中从未存在过的状态。

注意:UPDATE和DELETE不使用快照

快照读适用于事务中的普通 SELECT 语句,但不适用于 DML 语句。前面已经说过,UPDATE和DELETE 是加锁读操作

如果一个事务(事务A)插入或修改某些行然后提交该事务,则从另一个并发 REPEATABLE READ 事务(事务B)的 DELETE 或 UPDATE 操作可能会影响那些刚刚提交的行,即使会话无法查询它们(因为会话B也是读自己的快照,所以看不到会话A新增的一行)。 如果一个事务确实更新或删除了由不同事务提交的行,那么这些更改对当前事务是可见的。

以下面的 t1 表为例分别演示两个过程

CREATE TABLE `t1` (
  `c1` varchar(32) NOT NULL,
  `c2` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`c1`)
);

DELETE 操作

SELECT COUNT(c1) FROM t1 WHERE c1 = 'xyz';
-- Returns 0: no rows match.
DELETE FROM t1 WHERE c1 = 'xyz';
-- Deletes several rows recently committed by other transaction.

在这里插入图片描述

UPDATE 操作

SELECT COUNT(c2) FROM t1 WHERE c2 = 'abc';
-- Returns 0: no rows match.
UPDATE t1 SET c2 = 'cba' WHERE c2 = 'abc';
-- Affects 10 rows: another txn just committed 10 rows with 'abc' values.
SELECT COUNT(c2) FROM t1 WHERE c2 = 'cba';
-- Returns 10: this txn can now see the rows it just updated.

在这里插入图片描述

通过提交事务然后执行新的查询或者执行START TRANSACTION WITH CONSISTENT SNAPSHOT来提前时间点。在以下示例中,会话 A 仅在 B 已提交插入的行且 A 也已提交时才能看到 B 插入的行,因此快照的时间点提前了
在这里插入图片描述

如果您想查看数据库的最新状态,可以使用READ COMMITTED隔离级别或使用SELECT * FROM t LOCK IN SHARE MODE;等加锁读操作

使用READ COMMITTED隔离级别,事务中的每个一致性读取都会设置并读取自己的新快照。
使用LOCK IN SHARE MODE则会对读操作加共享锁,SELECT 会阻塞直到包含最新行的事务结束

t1t2
设置手动提交SET autocommit=0;SET autocommit=0;
SELECT * FROM t LOCK IN SHARE MODE;
t2阻塞INSERT INTO t VALUES (3), (4);
t1提交时,t2执行插入动作COMMIT;
t1看不到t2新增的行,
t2可以看到自己新增的行SELECT * FROM t;SELECT * FROM t;
COMMIT;
t1可以看到t2新增的行SELECT * FROM t;

注意:快照读不适用于某些 DDL 语句

  1. DROP TABLE:MySQL 无法使用已删除的表,并且 InnoDB 会破坏该表。
  2. ALTER TABLE :操作会形成原始表的临时副本,并在构建临时副本时删除原始表

当您在事务中重新发出一致读取时,新表中的行不可见,因为在获取事务的快照时这些行不存在。在这种情况下,事务返回错误:ER_TABLE_DEF_CHANGED,“表定义已更改,请重试事务”。

对于未指定 FOR UPDATELOCK IN SHARE MODE 的子句(如 INSERT INTO ... SELECTUPDATE ... (SELECT)CREATE TABLE ... SELECT)中的select,读取的类型会有所不同:

  1. 默认情况下,InnoDB 在这些语句中使用更强的锁,并且 SELECT 部分的行为类似于READ COMMITTED,即每个一致性读操作,即使在同一个事务中,都设置和读取自己的新快照。
  2. 要在这种情况下执行非锁定读取,需要启用innodb_locks_unsafe_for_binlog选项并将事务的隔离级别设置为 READ UNCOMMITTED、READ COMMITTED 或 REPEATABLE READ,以避免对从选定的表中读取的行加锁。

在MySQL 8.0版本innodb_locks_unsafe_for_binlog变量已经被移除了
https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_locks_unsafe_for_binlog

Locking Reads 加锁读

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

Locking Reads 意为锁定读取,也就是对读操作也加锁,本文就简单把它就称为加锁读。常见的 UPDATE/INSERT/DELETE、还有SELECT ... FOR UPDATESELECT ... FOR SHARESELECT ... LOCK IN SHARE MODE 都是加锁读。MySQL 8 中SELECT ... FOR SHARE代替了SELECT ... LOCK IN SHARE MODE,但为了向后兼容,LOCK IN SHARE MODE仍可用,两者是等价的。区别在于FOR SHARE 支持 OF table_nameNOWAITSKIP LOCKED等选项,后面会展开解释。

为什么读操作需要加锁?

如果在同一事务中执行了普通 SELECT 的同时,又执行了 INSERT 或 UPDATE,那么普通的 SELECT 语句无法提供足够的保护,其他的事务可以更新或删除当前事务查询的某个结果。因此需要通过加锁来保证数据一致性

为什么UPDATE要加锁?

比如要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。并且 update 时需要加锁


下面介绍几种常见的加锁读操作

  1. SELECT … LOCK IN SHARE MODE
    在读取的每一行上设置共享锁,其他会话可以读取这些行,但在持有锁的事务提交之前不能修改它们。 如果其中任何行被另一个尚未提交的事务更改,则查询将等待该事务结束,然后使用最新值。
  2. SELECT … FOR UPDATE
    对查询的每一行以及查询使用到的所有的索引项进行加锁,效果等同于更新这些行的UPDATE 语句。其他事务如果想要更新这些被加锁的行,或者执行SELECT … LOCK IN SHARE MODE,以及在某些隔离级别下想要读取这些数据,都会被阻塞。加锁读会忽略Read View中存在的记录上设置的任何锁,不能锁定旧版本的记录,它们是通过undo log中保存的记录在内存中的副本上来重建的。

当事务提交或回滚时,所有由 LOCK IN SHARE MODEFOR UPDATE 查询设置的锁都会被释放。

注意事项

  1. 加锁读可能造成死锁,具体取决于事务的隔离级别
  2. 加锁读操作不允许用于只读事务中的全局表
  3. 只有在禁用自动提交时才能执行加锁读操作,使用START TRANSACTION开始事务或者手动关闭自动提交:set auto commit设置为 0。
  4. 外部语句中的加锁读子句不会锁定嵌套子查询中表的行,除非子查询中还指定了锁定读取子句。 例如,以下语句不会锁定表 t2 中的行:
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;

如果要对t2表上锁则需要在子句中也执行加锁读操作:

SELECT * FROM t1 WHERE c1 =(SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;

MVCC 并发控制

参考:https://blog.csdn.net/SnailMann/article/details/94724197
一般情况下,事务性储存引擎不是只使用表锁,行锁来处理数据,高并发情况下加锁会极大地影响性能,因此会结合其他机制以处理更多的并发问题。MySQL 就是用的 MVCC 机制,MVCC 全称多版本并发控制(Multiversion Currency Control)。MVCC处理高并发能力很强,但系统开销表锁、行锁最大,这是追求高并发付出的代价。

注意:不是所有的隔离级别都会用MVCC实现,MVCC 主要被用在读已提交和可重复读这两个隔离级别下,它通过保存数据的多个版本和事务的可见性控制,实现了事务的隔离性和一致性。

InnoDB是一个多版本存储引擎。它保留有关已更改行的旧版本的信息,以支持事务相关的特性,比如如并发性和回滚。这些信息存储在system tablespace或者undo tablespaces中一个叫做回滚段(rollback segment)的数据结构中,InnoDB使用这些信息来完成在事务回滚时需要进行的一些恢复操作,还可使用这些信息构建数据行的早期版本,以实现快照读。

数据库并发的场景大概分为三种:

  1. 只有读操作:这种情况比较少,只有读操作就不用加锁了,不会有并发问题
  2. 有读有写:最常见的场景,有并发问题,可能遇到脏读,幻读,不可重复读
  3. 只有写操作:有线程安全问题,可能会存在更新丢失问题,比如一个事务覆盖了之前事务的更新结果

现在有下面这一张表:

CREATE TABLE `account` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) DEFAULT NULL COMMENT '用户名',
  `money` decimal(10, 5) COMMENT '金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COMMENT='账户';

实现原理

InnoDB 通过以下方式实现了MVCC:

  1. 在每行记录后面保存两个隐藏的列:一个是行的事务ID(DB_TRX_ID),用于记录创建或修改该行的事务ID;另一个是回滚指针(DB_ROLL_PTR),指向该行可能存在的一个旧版本
  2. 通过 undo 日志记录数据的旧版本信息,以便在需要时能够回滚到旧版本。
  3. 在执行查询时,根据当前事务的ID和每行记录的事务ID以及回滚指针,确定哪些版本的数据对当前事务是可见的。

下面分别介绍这三部分:

InnoDB 隐藏列

InnoDB内部会向数据库中存储的每一行记录添加三个字段

  1. 6字节的DB_TRX_ID字段:此字段表示最后插入或者更新该行的事务ID,删除delete在InnoDB内部会被视为update,被删除的行数据中会有一个特殊的bit位来标记该行被删除
  2. 7字节的DB_ROLL_PTR字段:称为回滚指针,该指针指向写到回滚段的一条undo log记录,如果该行被update,那么该undo log记录将包含更新之前的该行的数据,可用于重建该行数据
  3. 6字节的DB_ROW_ID字段:包含一个单调自增的行ID,该值随新行插入单调递增。没有主键时,因为MYSQL会自动生成一个ROWID作为行的主键值,也就是这个DB_ROW_ID字段,有主键的话就不会自动生成,所以是不会有DB_ROW_ID这个值的。

以上面的 account 表为例,假设一个事务 ID 为 1 的事务来新增了一条数据

INSERT INTO account (name, money) VALUES('zs', 1000);

那么这条数据的样子就类似于下图所示:

在这里插入图片描述
假如表没有主键 id 列,则类似下面这样

在这里插入图片描述

PS: 以上隐藏字段的值都是编的

添加这几列的源码如下(storage\innobase\dict\dict0load.cc)

/** Adds system columns to a table object.
	@param[in,out] table Table
	@param[in] heap Temporary heap 
*/
void dict_table_add_system_columns(dict_table_t *table, mem_heap_t *heap) {
  ut_ad(table);
  /* INSTANT DROP columns are added after system columns, so no need to
  consider drop columns count in following assert. */
  ut_ad(table->n_def == (table->n_cols - table->get_n_sys_cols()));
  ut_ad(table->magic_n == DICT_TABLE_MAGIC_N);
  ut_ad(!table->cached);

  /* NOTE: the system columns MUST be added in the following order
  (so that they can be indexed by the numerical value of DATA_ROW_ID,
  etc.) and as the last columns of the table memory object.
  The clustered index will not always physically contain all system
  columns.
  Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned off
  for these tables. */

  const uint32_t phy_pos = UINT32_UNDEFINED;
  const uint8_t v_added = 0;
  const uint8_t v_dropped = 0;
 
  dict_mem_table_add_col(table, heap, "DB_ROW_ID", DATA_SYS,
                         DATA_ROW_ID | DATA_NOT_NULL, DATA_ROW_ID_LEN, false,
                         phy_pos, v_added, v_dropped);

  dict_mem_table_add_col(table, heap, "DB_TRX_ID", DATA_SYS,
                         DATA_TRX_ID | DATA_NOT_NULL, DATA_TRX_ID_LEN, false,
                         phy_pos, v_added, v_dropped);

  if (!table->is_intrinsic()) {
    dict_mem_table_add_col(table, heap, "DB_ROLL_PTR", DATA_SYS,
                           DATA_ROLL_PTR | DATA_NOT_NULL, DATA_ROLL_PTR_LEN,
                           false, phy_pos, v_added, v_dropped);

    /* This check reminds that if a new system column is added to
    the program, it should be dealt with here */
  }
}

事务ID

当每个事务开启时,都会被分配一个ID,这个ID是按分配的先后顺序递增的,所以越新的事务,ID值越大。在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID。

Read View

ReadView 就是快照读的所谓的快照。Read View 是MySQL中 InnoDB 存储引擎在 MVCC 机制下用于实现快照读的一个关键数据结构。ReadView的存在使得InnoDB存储引擎能够在不加锁的情况下,为不同的事务提供一致性的数据视图,从而提高了数据库的并发性能。

源码基于 mysql-server 8.0.38 版本,可以在 https://github.com/mysql/mysql-server/releases/tag/mysql-8.0.38 这个地址找到,在 \storage\innobase\include\read0types.h 可以看到 ReadView 的定义

constexpr ib_id_t IB_ID_MAX = std::numeric_limits<uint64_t>::max();
/** Maximum transaction identifier */
constexpr trx_id_t TRX_ID_MAX = IB_ID_MAX;

class ReadView {
	/** This is similar to a std::vector but it is not a drop
  	in replacement. It is specific to ReadView. */
	class ids_t {
		...
	}
	
	 private:
	  /** The read should not see any transaction with trx id >= this
	  value. In other words, this is the "high water mark". */
	  trx_id_t m_low_limit_id;
	
	  /** The read should see all trx ids which are strictly
	  smaller (<) than this value.  In other words, this is the
	  low water mark". */
	  trx_id_t m_up_limit_id;
	
	  /** trx id of creating transaction, set to TRX_ID_MAX for free views. */
	  trx_id_t m_creator_trx_id;
	
	  /** Set of RW transactions that was active when this snapshot was taken */
	  ids_t m_ids;
	
	  /** The view does not need to see the undo logs for transactions
	  whose transaction number is strictly smaller (<) than this value:
	  they can be removed in purge if not needed by other views */
	  trx_id_t m_low_limit_no;
}

Read View 结构主要包含以下几个字段:

  1. m_ids:Read View生成时系统正活跃的事务ID列表,包含了在ReadView生成时,系统中所有未提交的事务ID。用于判断某个版本的数据是否对当前事务可见。如果某个数据版本的事务ID在m_ids中,且不等于当前事务ID(m_creator_trx_id),则该版本对当前事务不可见。
  2. m_up_limit_id:记录 m_ids 列表中事务ID最小的ID,用于快速判断一个事务是否在ReadView生成前已经提交。如果某个数据版本的事务ID小于min_trx_id,则表明生成该版本的事务在生成ReadView前已经提交,因此该版本对当前事务可见。
  3. m_low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1。由于事务ID是自增的,这个值用于判断某个数据版本是否在ReadView生成后产生。如果某个数据版本的事务ID大于等于max_trx_id,则表明生成该版本的事务在生成ReadView后才生成,因此该版本对当前事务不可见。
  4. m_creator_trx_id:创建ReadView的事务ID。即当前执行快照读的事务ID。
    在判断数据版本可见性时,如果数据版本的事务ID等于m_creator_trx_id,即使该事务在m_ids中(即未提交),也认为该版本对当前事务可见,因为是自己生成的数据。

Read View 和隔离级别的关系

使用Read View的隔离级别是 REPEATABLE READREAD COMMITTEDREAD UNCOMMITTED

ReadView的生成时机和复用策略与事务的隔离级别有关:

  1. 读已提交(RC):在每一次执行快照读时都会生成新的ReadView。
  2. 可重复读(RR):仅在第一次执行快照读时生成ReadView,后续的快照读会复用同一个ReadView,直到事务结束。

Undo log

Undo log 的作用是对一条记录的所有版本形成一个版本链的形式,还是以上面的 account 表为例
刚开始表中数据为空,假设有 4 个事务一次执行了下面的sql

INSERT INTO account (name, money) VALUES('zs', 1000);  // trx_id = 1
UPDATE account SET money = 2000 WHERE name = 'zs';  // trx_id = 2
UPDATE account SET money = 400 WHERE name = 'zs';	// trx_id = 3
UPDATE account SET money = 200 WHERE name = 'zs';	// trx_id = 4

那么形成的 undo log 链会如下所示:每个 Undolog Record 就是数据的一个版本

在这里插入图片描述

回滚段中的undo log被分为两种类型:insert undo log和update undo log

  1. insert undo log仅在事务回滚中需要,并且在事务提交后会被立即丢弃。
  2. update undo log也会被用在快照读中,在快照读中,当没有InnoDB已为其分配快照,并且需要update undo log中的信息来构建行的早期版本的事务存在后,update undo log中的信息才会被丢弃

建议经常提交事务,包括仅发出快照读的事务,否则,InnoDB无法丢弃update undo log中的数据,回滚段可能会变得太大,从而将其所在的表空间填满

回滚段中undo log记录的物理大小比相应的被插入或更新的行的大小要小,从而可以计算回滚段所需的空间。

purge 操作

InnoDB中,当通过sql执行delete时,行不会马上被物理删除,只有在丢弃为删除而写入的update undo log记录时,才会物理删除相应的行及其索引记录。这个删除操作文档中称为清除(purge),执行此操作所花费的时间通常和执行delete sql所花费的时间差不多

实现过程

MVCC 通过 Read View 来判断事务是否可查到某条数据。即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

当查到一行数据时,mysql需要通过MVCC机制判断当前事务是否能看见该行

/** Build an old version of the row if required.
@param[in,out]  rec           Current row read from the index. This can
                              be modified by this method if an older version
                              needs to be built.
@param[in,out]  offsets       Same as above but pertains to the rec offsets
@param[in,out]  heap          Heap to use if a previous version needs to be
                              built from the undo log.
@param[in,out]  mtr           Mini-transaction covering the read.
@return true if row is visible to the transaction. */
bool Parallel_reader::Scan_ctx::check_visibility(const rec_t *&rec,
                                                 ulint *&offsets,
                                                 mem_heap_t *&heap,
                                                 mtr_t *mtr) {
  const auto table_name = m_config.m_index->table->name;

  // m_trx: 当前事务

  // assert
  ut_ad(!m_trx || m_trx->read_view == nullptr ||
        MVCC::is_view_active(m_trx->read_view));

  if (!m_trx) {
    /* Do nothing */
  } else if (m_trx->read_view != nullptr) {
    // ReadView != null 表示操作是快照读
    auto view = m_trx->read_view;

    if (m_config.m_index->is_clustered()) {
      trx_id_t rec_trx_id;
      // 读取该行记录的 DATA_TRX_ID 列值
      if (m_config.m_index->trx_id_offset > 0) {
        rec_trx_id = trx_read_trx_id(rec + m_config.m_index->trx_id_offset);
      } else {
        rec_trx_id = row_get_rec_trx_id(rec, m_config.m_index, offsets);
      }
	
	  // 当前事务的隔离级别 > TRX_ISO_READ_UNCOMMITTED: RC, RR, Serializable
      if (m_trx->isolation_level > TRX_ISO_READ_UNCOMMITTED &&
          !view->changes_visible(rec_trx_id, table_name)) {
        rec_t *old_vers;
		// view->changes_visible = false 说明需要获取行的旧版本
        row_vers_build_for_consistent_read(rec, mtr, m_config.m_index, &offsets,
                                           view, &heap, heap, &old_vers,
                                           nullptr, nullptr);

        rec = old_vers;

        if (rec == nullptr) {
          return (false);
        }
      }
    } else {
      /* Secondary index scan not supported yet. */
      ut_error;
    }
  }
  
  // 判断行的 delete_bit,行是否被删除
  if (rec_get_deleted_flag(rec, m_config.m_is_compact)) {
    /* This record was deleted in the latest committed version, or it was
    deleted and then reinserted-by-update before purge kicked in. Skip it. */
    return (false);
  }

  ut_ad(!m_trx || m_trx->isolation_level == TRX_ISO_READ_UNCOMMITTED ||
        !rec_offs_any_null_extern(m_config.m_index, rec, offsets));

  return (true);
}

class ReadView {
	......
  /** Check whether transaction id is valid.
  @param[in]    id              transaction id to check
  @param[in]    name            table name */
  static void check_trx_id_sanity(trx_id_t id, const table_name_t &name);
  
  /** Check whether the changes by id are visible.
	  @param[in]    id      transaction id to check against the view  
	  						某行数据的 DB_TRX_ID
	  @param[in]    name    table name
	  @return whether the view sees the modifications of id. 
  */
  [[nodiscard]] bool changes_visible(trx_id_t id,
                                     const table_name_t &name) const {
    ut_ad(id > 0);
	// 1. id < m_up_limit_id : 则表明该行数据在生成 ReadView 前已经提交
	// 所以当前事务的ReadView是肯定能看到这条数据的
	
	// 2.id == m_creator_trx_id :该行数据就是当前事务创建的
	// 所以当前事务的ReadView也是能看到的
    if (id < m_up_limit_id || id == m_creator_trx_id) {
      return (true);
    }

    check_trx_id_sanity(id, name);

	// 操作该行数据的事务是在在生成ReadView后才生成的,无论是新增还是修改
	// 所以ReadView肯定看不到这行数据
    if (id >= m_low_limit_id) {
      return (false);
    } else if (m_ids.empty()) { // 事务列表为空
      return (true);
    }
    const ids_t::value_type *p = m_ids.data();
	// 二分查找:当前行的事务ID是否在当前活跃的事务列表中
    return (!std::binary_search(p, p + m_ids.size(), id));
  }
}

主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 Undo Log 中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链表头节点到链表尾节点,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新的老版本。一般流程如下:

  1. 首先比较 DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  2. 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  3. 判断DB_TRX_ID 是否在活跃事务列表之中,如果在,则代表Read View生成时刻,最后操作该记录的事务还没有提交,Read View是看不见这行数据的;如果不在,则说明,最后操作该记录的事务在Read View生成之前就已经提交了,因此 Read View 是可以看见这行数据的

MVCC与隔离级别

在MySQL中,不同的事务隔离级别对数据的可见性和一致性有不同的要求,而MVCC是实现这些隔离级别的一种重要手段。MVCC主要属于读已提交和可重复读这两个隔离级别下,具体来说:

  1. READ UNCOMMITTED:
    在这个隔离级别下可能出现脏读,事务可以读取到其他事务未提交的数据。无需锁无需mvcc,因为修改数据直接改源数据, 会出现脏读。由于MVCC要求事务只能读取到已提交的数据版本,因此在读未提交隔离级别下,MVCC不生效。
  2. READ COMMITTED:
    在这个隔离级别下,事务只能读取到其他事务已提交的数据。每次查询都会创建ReadView读取数据。MySQL通过MVCC实现了读已提交隔离级别,确保每个事务读取到的数据都是最新的已提交数据。
  3. REPEATABLE READ:
    MySQL的默认隔离级别,在这个隔离级别下,同样的查询只有第一次会创建ReadView读取数据,事务在整个执行过程中看到的数据是一致的,即使其他事务提交了更新,当前事务也不会看到这些更新。
  4. SERIALIZABLE:所有事务都串行执行,因此不需要MVCC来实现这种隔离级别。
struct trx_t {
  enum isolation_level_t {

    /** dirty read: non-locking SELECTs are performed so that we
    do not look at a possible earlier version of a record; thus
    they are not 'consistent' reads under this isolation level;
    otherwise like level 2 */
    READ_UNCOMMITTED,

    /** somewhat Oracle-like isolation, except that in range UPDATE
    and DELETE we must block phantom rows with next-key locks;
    SELECT ... FOR UPDATE and ...  LOCK IN SHARE MODE only lock
    the index records, NOT the gaps before them, and thus allow
    free inserting; each consistent read reads its own snapshot */
    READ_COMMITTED,

    /** this is the default; all consistent reads in the same trx
    read the same snapshot; full next-key locking used in locking
    reads to block insertions into gaps */
    REPEATABLE_READ,

    /** all plain SELECTs are converted to LOCK IN SHARE MODE reads */
    SERIALIZABLE
  };
}

MVCC和辅助索引

MVCC对待辅助索引和聚簇索引是不同的,聚簇索引中的记录会被适当更新,并且存在隐藏的系统列,指向undo log中的项,可以用来构建早期版本,而辅助索引不包含隐藏列,也不会被适当更新。

当一个辅助索引列被更新时,旧的辅助索引记录就会被标记为删除,新的记录会被插入,标记为删除的记录最终会被 purge。这里的更新与聚簇索引不同,是先删除再插入。被标记为删除的辅助索引记录最终会被清除(即前面提到的purge操作)

当一个辅助索引记录被标记为删除时,或者该辅助索引页被一个新事物更新,InnoDB会查找聚簇索引中的记录,在该聚集索引中会检查DB_TRX_ID字段,如果该条记录在一个读事务初始化后被修改,则从undo log中取出该记录的正确版本

如果一个辅助索引记录被标记为删除或者辅助索引页被一个新事务更新,那么覆盖索引未使用。InnoDB不是从索引结构返回值,而是在聚集索引中查找记录

幻读

https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html

幻读是指在一个事务读取了几行数据后,另一个并发事务插入了一些新行,然后第一个事务再次读取同样的范围时,会看到由于其他事务的插入操作而幻影般地出现的新行。

当同一个查询在不同的时间产生不同的结果时,事务中就会出现所谓的幻行问题。 例如一条 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是幻像行。

以前面的 account 表为例,此时想要读取并锁定表中 id 值大于 100 的所有行,以便稍后更新所选行中的某些列:SELECT * FROM account WHERE id > 100 FOR UPDATE;。这个查询从 id 大于 100 的第一条记录开始扫描索引。假设表包含 id 值为 90 和 102 的行。如果在扫描范围内的索引记录上设置的锁没有对间隙上的插入进行上锁,那么另一个会话可以将 id 为 101 的新行插入到该表中,这里间隙有3个:(-∞, 90)、(90, 102)、(102, +∞)。此时在同一个事务中执行相同的 SELECT,则会看到一个id 为 101的新行出现在查询返回的结果集中。如果将一系列行数据视为一个数据项,那么新出现的幻行将违反事务的隔离原则,即读取的数据在事务生效期间不会发生变化。

为了防止幻读,InnoDB 使用了一种称为 next-key 锁的算法,该算法将索引记录锁与间隙锁相结合。 InnoDB 搜索或扫描索引时,它会在它遇到的索引记录上设置共享或互斥锁,这个锁是行锁。 此外,索引记录上的 next-key lock 也会影响索引记录之前的间隙。 也就是说,next-key 锁是索引记录锁加上索引记录前面的间隙上的间隙锁。 如果一个会话在索引中的记录 R 上具有共享或互斥锁,则另一个会话不能在索引顺序中 R 之前的间隙中插入新的索引记录。

InnoDB 在扫描索引时可能对索引中最后一条记录之后的间隙上加锁。 在前面的例子SELECT * FROM account WHERE id > 100 FOR UPDATE;中就是这样:为了防止任何插入到 id 大于 100 的表中,InnoDB 设置的锁包括在 id 值 102 之后的间隙上的锁。

next-key 锁能够对表中不存在的东西进行加锁

使用 next-key 锁定可以在应用程序中实现唯一性检查:如果在共享模式下读取数据并且没有看到要插入的行的重复项,那么您可以安全地插入您的行并知道在读取期间在您的行的后继上设置的 next-key lock 可防止任何人同时为您的行插入重复项。

比如 account 表中数据为空,则间隙为所有 id 可能的值

可重复读

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:

  1. 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  2. 针对加锁读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

在这里插入图片描述

MVCC会出现幻读的情况

MVCC并不能完全避免幻读,某些场景下还是会出现幻读的情况,如下图所示
在这里插入图片描述
使用快照读的情况下不会加间隙锁,所以事务B的新增操作立刻就成功了,但是事务A是查不出来的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2191007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯自研Git客户端,助力每个人都可以轻松使用Git

工具介绍 UGit是一款腾讯自研的Git客户端&#xff0c;为了让每个人都可以轻松使用Git&#xff0c;从而提高开发效率和团队协作的流畅性。支持工蜂MR/CR&#xff0c;工蜂议题管理&#xff0c;另外对于Git的原生特性有着深度支持。 支持的系统 支持macOS 10.11、Apple Silicon和…

【数据结构】什么是哈希表(散列表)?

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 &#x1f4cc;哈希表的概念 &#x1f4cc;哈希函数的构造方法 &#x1f38f;直接定址法 &#x1f38f;除留余数法 &#x1f38f;平方取中法 &#x1f38f;折叠法 &#x…

自动驾驶的技术实现及原理

自动驾驶技术是现代科技领域中一项引人注目的创新&#xff0c;它具有变革运输行业并提升道路安全的潜力。随着人工智能、传感器技术以及数据处理能力的不断提升&#xff0c;自动驾驶车辆已经从实验室研究逐渐走向现实应用。 自动驾驶的技术实现及原理 1. 自动驾驶技术的核心…

【深度学习】— 多层感知机介绍、 隐藏层、从线性到非线性、线性模型的局限性

【深度学习】— 多层感知机介绍 4.1 多层感知机4.1.1 隐藏层线性模型的局限性引入隐藏层 4.2 从线性到非线性线性组合的局限性引入非线性堆叠更多隐藏层 4.1 多层感知机 在第 3 节中&#xff0c;我们介绍了 softmax 回归&#xff0c;并实现了其从零开始的实现和基于高级 API 的…

UART通信协议

什么是UART UART ( Universal Asynchronous Receiver/Transmitter&#xff0c; 通用异步收发器) 是一种常用的串行通信协议&#xff0c;用于在 计算机和外部设备之间传输数据。它是一种异步通信协议&#xff0c;也就是说数据的传输不需要事先建立好同步时钟信号。 UART&#xf…

Unity MVC框架演示 1-1 理论分析

本文仅作学习笔记分享与交流&#xff0c;不做任何商业用途&#xff0c;该课程资源来源于唐老狮 1.一般的图解MVC 什么是MVC我就不说了&#xff0c;老生常谈&#xff0c;网上有大量的介绍&#xff0c;想看看这三层都起到什么职责&#xff1f;那就直接上图吧 2.我举一个栗子 我有…

深入理解 JavaScript 事件循环机制:单线程中的异步处理核心

深入理解 JavaScript 事件循环机制&#xff1a;单线程中的异步处理核心 JavaScript 是一门单线程的编程语言&#xff0c;也就是说它在同一时间只能执行一个任务。然而&#xff0c;现代 Web 应用经常需要处理大量的异步操作&#xff0c;如用户输入、网络请求、定时器等。为了确…

Vue的基本用法及模板语法

Vue.js使用了基于 HTML 的模板语法&#xff0c;允许开发者声明式地将 DOM 绑定至底层 Vue实例的数据。所有 Vue.js的模板都是合法的 HTML&#xff0c;所以能被遵循规范的浏览器和 HTML 解析器解析。 在底层的实现上&#xff0c;Vue将模板编译成虚拟 DOM 渲染函数。结合响应系…

实现Xshell与虚拟机中Linux服务器的连接(附常见错误解决)

前言 Xshell是一个强大的安全终端模拟软件&#xff0c;它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。Xshell 通过互联网到远程主机的安全连接以及它创新性的设计和特色帮助用户在复杂的网络环境中享受他们的工作。 本文将介绍Xshell与虚拟机中Linux服务器连接…

前缀线性基——关于目前的理解以及一些样题

怎么说呢&#xff1f;在前几天我总结了了有关线性基的一篇博客&#xff0c;线性基用来去求整个区间的异或最值问题 前缀线性基——用于统计一个区间内的异或最值问题 那么我们如何去统计呢&#xff1f;那么就要去存储一个区间的异或空间线性基&#xff0c;因此我们的思路就是用…

【python】追加写入excel

输出文件运行前&#xff08;有两张表&#xff0c;“表1”和“Sheet1”&#xff09;&#xff1a; 目录 一&#xff1a;写入单表&#xff08;删除所有旧工作表&#xff0c;写入新表&#xff09;二&#xff1a;写入多表&#xff08;删除所有旧工作表&#xff0c;写入新表&#x…

平衡二叉搜索树之 AVL 树的模拟实现【C++】

文章目录 AVL树的简单介绍全部的实现代码放在了文章末尾准备工作包含头文件类的成员变量 构造函数和拷贝构造swap和赋值运算符重载析构函数findinsert[重要]当parent的平衡因子为1/-1时&#xff0c;如何向上更新祖先节点的平衡因子呢&#xff1f;怎么旋转&#xff1f;左单旋右单…

Windows Ubuntu下搭建深度学习Pytorch训练框架与转换环境TensorRT

Windows Ubuntu下搭建深度学习Pytorch训练框架与转换环境TensorRT JetBrains2024&#xff08;IntelliJ IDEA、PhpStorm、RubyMine、Rider……&#xff09;安装包Anaconda Miniconda安装.condarc 文件配置镜像源查看conda的配置和源(channel)自定义conda虚拟环境路径conda常用命…

Chromium 中JavaScript Screen API接口c++代码实现

Screen - Web API | MDN (mozilla.org) Screen Screen 接口表示一个屏幕窗口&#xff0c;往往指的是当前正在被渲染的 window 对象&#xff0c;可以使用 window.screen 获取它。 请注意&#xff1a;由浏览器决定提供屏幕对象&#xff0c;此对象一般通过当前浏览器窗口活动状…

《python语言程序设计》2018版第8章19题几何Rectangle2D类(下)-头疼的几何和数学

希望这个下集里能有完整的代码 一、containsPoint实现 先从网上找一下Statement expected, found Py:DEDENTTAB还是空格呢??小小总结如何拆分矩形的四个点呢.我们来小小的测试一下这个函数结果出在哪里呢???修改完成variable in function should be lowercase 函数变量应该…

No.2 笔记 | 网络安全攻防:PC、CS工具与移动应用分析

引言 在当今数字化时代,网络安全已成为每个人都应该关注的重要话题。本文将总结一次关于网络安全攻防技术的学习内容,涵盖PC端和移动端的恶意程序利用,以及强大的渗透测试工具Cobalt Strike的使用。通过学习这些内容,我们不仅能够了解攻击者的手法,更能提高自身的安全意识和防…

【牛顿迭代法求极小值】

牛顿迭代法求极小值 仅供参考 作业内容与要求 作业内容 作业要求 递交报告 代码 编程实现 计算偏导数 故上述非线性方程组的根可能为 f ( x , y ) f(x, y) f(x,y)的极值点&#xff0c;至于是极小值点还是极大值点或鞍点&#xff0c;就需要使用微积分中的黑塞矩阵来判断了。…

网络基础 【HTTPS】

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux初窥门径⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a; &#x1f4bb;操作环境&#xff1a; CentOS 7.6 华为云远程服务器 &#x1f339;关注我&#x1faf5;带你学习更多Linux知识…

Linux之实战命令26:timeout应用实例(六十)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

安卓手机密码忘了怎么办?(只做科普)

注意&#xff1a;只做科普&#xff0c;拒绝利用技术做一些非法事情 科普人&#xff1a;网络安全工程师~DL 科普平台&#xff1a;快手&#xff0c;CSDN&#xff0c;微信公众号&#xff0c;小红书&#xff0c;百度&#xff0c;360 本期文章耗时比较大&#xff0c;如果喜欢&…