InnoDB 事务模型

文章目录

  • 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: 以上隐藏字段的值都是编的

/** 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 toconsider 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 systemcolumns.Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned offfor 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 tothe 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 dropin replacement. It is specific to ReadView. */class ids_t {...}private:/** The read should not see any transaction with trx id >= thisvalue. 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 strictlysmaller (<) than this value.  In other words, this is thelow 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 transactionswhose 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 canbe modified by this method if an older versionneeds 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 bebuilt 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: 当前事务// assertut_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, Serializableif (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 wasdeleted 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 wedo not look at a possible earlier version of a record; thusthey are not 'consistent' reads under this isolation level;otherwise like level 2 */READ_UNCOMMITTED,/** somewhat Oracle-like isolation, except that in range UPDATEand DELETE we must block phantom rows with next-key locks;SELECT ... FOR UPDATE and ...  LOCK IN SHARE MODE only lockthe index records, NOT the gaps before them, and thus allowfree inserting; each consistent read reads its own snapshot */READ_COMMITTED,/** this is the default; all consistent reads in the same trxread the same snapshot; full next-key locking used in lockingreads 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

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

幻行问题

https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html
当同一个查询在不同的时间产生不同的结果时,事务中就会出现所谓的幻行问题。 例如一条 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是幻像行。

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

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

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

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

可重复读

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

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

在MySQL中,关于Repeatable Read隔离级别是否会有幻读(Phantom Reads)的问题,需要具体看MySQL的实现和使用的存储引擎。

关于MySQL的可重复读(REPEATABLE READ)隔离级别:

  1. InnoDB的实现:在可重复读隔离级别下,InnoDB使用了MVCC技术来避免幻读。具体来说,它会为每个事务提供一个一致性的数据快照,这样事务在开始时看到的数据集在整个事务的生命周期内都会保持不变,即使其他事务在此期间进行了修改。
  2. 幻读与间隙锁(Gap Lock):尽管InnoDB通过MVCC避免了传统的幻读,但它仍然可能遇到一种特定类型的幻读,这涉及到间隙锁(Gap Lock)。间隙锁是InnoDB用来锁定一个范围,但不包括记录本身的锁。当事务试图锁定一个范围以防止其他事务插入新的记录时,它实际上是在锁定这个范围中的间隙。如果另一个事务在这个间隙中插入了新的记录,那么第一个事务在后续尝试读取或锁定这个范围时可能会看到这个“幻影般地出现”的新行。但请注意,这不是通过MVCC看到的幻读,而是由于间隙锁的使用和并发插入操作导致的。
  3. SELECT … FOR UPDATE 和 SELECT … LOCK IN SHARE MODE:当使用这些语句时,InnoDB可能会使用间隙锁来锁定一个范围,从而可能导致上述类型的幻读。
  4. NEXT-KEY LOCK:实际上,InnoDB在可重复读隔离级别下使用NEXT-KEY LOCK,这是一种结合了记录锁和间隙锁的锁策略。它锁定一个记录以及它之前的间隙,从而确保在读取范围内不会插入新的记录。
    综上所述,在MySQL的InnoDB存储引擎和可重复读隔离级别下,传统的幻读(即读取到新的行)通过MVCC被避免了。但是,由于间隙锁和NEXT-KEY LOCK的使用,仍然存在一种特定类型的幻读风险,涉及到在锁定的间隙中插入新的记录。

MVCC会出现幻读的情况

MVCC并不能完全避免幻读,某些场景下还是会出现幻读的情况

  1. a事务先select,b事务insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放,释放后a事务可以随意操作
  2. a事务再select出来的结果在MVCC下还和第一次select一样,
  3. 接着a事务不加条件地update,这个update会作用在所有行上(包括b事务新加的),
  4. a事务再次select就会出现b事务中的新行,并且这个新行已经被update修改了.

上面这样,事务2提交之后,事务1再次执行update,因为这个是当前读,他会读取最新的数据,包括别的事务已经提交的,所以就会导致此时前后读取的数据不一致,出现幻读。

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

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

相关文章

HTB:Synced[WriteUP]

目录 连接至HTB服务器并启动靶机 1.What is the default port for rsync? 2.How many TCP ports are open on the remote host? 3.What is the protocol version used by rsync on the remote machine? 4.What is the most common command name on Linux to interact w…

一些关于上传数据-p7zip-full-压缩包的经验

目录 前言 7z 简介 Windows如何压缩tar.gz格式 一、下载7-ZIP 二、tar文件进一步压缩 说明&#xff1a; 前言 本人每次在linux服务器上执行apt-get install p7zip-full命令&#xff0c;都会有复杂依赖报错&#xff08;因为实验过程中用到的依赖包太多了&#xff09;&…

[Python学习日记-39] 闭包是个什么东西?

[Python学习日记-39] 闭包是个什么东西&#xff1f; 简介 闭包现象 闭包意义与作用 简介 在前面讲函数和作用域的时候应该提到过&#xff0c;当函数运行结束后会由 Python 解释器自带的垃圾回收机制回收函数内作用域已经废弃掉的变量&#xff0c;但是在 Python 当中还有一种…

Linux中的多线程

Linux线程概念 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序 列” 进程是系统分配资源的基本实体 线程是CPU调度的基本单位 POSIX线程库 创建线程 功能&#xff1a;创建一个新的线程 原…

Tkinter打包成EXE安装文件

打包成 .exe可执行文件 1. 安装PyInstaller&#xff0c;命令如下&#xff1a; pip install pyinstaller2. 编写你的Tkinter应用程序&#xff1a; 创建一个Python文件&#xff0c;例如app.py&#xff0c;并写入你的Tkinter代码。 3. 在 app.py 文件所在的目录使用PyInstaller…

从零开始讲PCIe(5)——66MHZ的PCI总线与其限制

一、前言 在之前的内容中&#xff0c;我们已经基本了解了PCI总线的设计思路和传输机制&#xff0c;之前的内容我们都是基于33MHZ版本的PCI总线进行学习的&#xff0c;为了支持更到的带宽&#xff0c;PCI协议还有一种66MHZ的版本。 二、高带宽PCI&#xff08;66MHZ&#xff09;…

UML类图全解析

1.UML的基本介绍 1.1什么是UML 1.UML > 统一建模语言&#xff0c;是一种用于软件系统分析和设计的语言工具&#xff0c;它用于帮助软件开发人员进行思考和记录思路的结果。 2.UML本身是一套符号的规定&#xff0c;就像数学符号和化学符号一样&#xff0c;这样符号用于描述软…

dll动态库加载失败导致程序启动报错以及dll库加载失败的常见原因分析与总结

目录 1、问题说明 2、dll库的隐式加载与动态加载 2.1、dll库的隐式加载 2.2、dll库的显式加载 3、使用Process Explorer查看进程加载的dll库信息以及动态加载的dll库有没有加载成功 3.1、使用Process Explorer查看进程加载的dll库信息 3.2、使用Process Explorer查看动态…

交叠型双重差分法

交叠型双重差分法&#xff08;Staggered Difference-in-Differences, Staggered DiD&#xff09;是一种扩展的双重差分&#xff08;Difference-in-Differences, DiD&#xff09;方法&#xff0c;用于处理多个时间点的政策干预或处理组&#xff08;treatment group&#xff09;并…

JavaWeb的小结02

第2章-第2节 一、知识点 HttpServletRequest请求对象、HttpServletResponse响应对象、响应内容类型Content-Type、请求转发、重定向、ServletContext对象。 二、目标 深刻理解HttpServletRequest对象的作用。 深刻理解HttpServletResponse对象的作用。 掌握HttpServletRequ…

企业必备:搭建大模型应用平台实操教程

最近AI智能体很火&#xff0c;AI智能体平台化产品肯定属于大公司的。但在一些场景下&#xff0c;尤其是对业务数据要求很高的公司&#xff0c;那就只能用私有大模型。不一定完全是为了对外提供服务&#xff0c;对内改造工作流也是需要的。所以 我感觉未来大部分企业都会搞一个…

普渡PUDU MT1:AI赋能,破解大面积场景清洁新挑战

普渡AI智能扫地机器人PUDU MT1:破解大面积场景清洁难题的新利器 在仓储物流、工业车间、交通枢纽、大型商场等大面积场景中,清洁难题一直是管理者们头疼的问题。这些区域面积广阔,清洁任务繁重,传统清洁方式难以胜任。然而,普渡机器人最新推出的AI智能扫地机器人PUDU MT1…

什么是 HTTP Get + Preflight 请求

当在 Chrome 开发者工具的 Network 面板中看到 GET Preflight 的 HTTP 请求方法时&#xff0c;意味着该请求涉及跨域资源共享 (CORS)&#xff0c;并且该请求被预检了。理解这种请求的背景&#xff0c;主要在于 CORS 的工作机制和现代浏览器对安全性的管理。 下面是在 Chrome …

ConcurrentHashMap在JDK1.7和1.8的区别,详解

目录 1.了解HashMap底层插入原理 2.ConcurrentHashMap 是什么&#xff1f; HashTable的实现 3.ConcurrentHashMap 1.7和1.8的区别 4、JDK1.7 中的ConcurrentHashMap实现原理 6、JDK1.8中的ConcurrentHashMap 7.链表转红黑树条件 1.8 put方法 8.并发扩容 9.总结 首先呢…

Origin正态分布检验

在spass中用Shapiro-Wilk检验--正态分布检测 Shapiro-Wilk检验--正态分布检测_spss shapiro-wilk检验-CSDN博客

数据服务-实时同步(sersync)

1. 概述 1.之前我们通过rsync定时任务实现定时备份/同步 2. 对于NFS我们需要进行实时同步 2. Sersync原理 3. 上手指南 环境主机web0110.0.0.7(nfs客户端)nfs0110.0.0.31(rsync客户端) (nfs服务端)backup10.0.0.41(rsync服务端) 3.1 rsync服务端准备 参考: 数据服务-备份服务…

好用便宜的头戴式耳机哪款好?强推四款高分爆单耳机精品!

音质&#xff0c;是耳机的灵魂。头戴式降噪耳机&#xff0c;以其卓越的音质表现&#xff0c;为您演绎音乐的真谛。无论是细腻的情感表达&#xff0c;还是震撼的音效体验&#xff0c;它都能让您感受到音乐的魅力所在。那好用便宜的头戴式耳机哪款好&#xff1f;&#xff0c;这里…

为什么芯片有多个不同的供电电压?

一、为什么芯片有多个不同的供电电压&#xff1f; 优化性能与功耗&#xff1a;芯片的核心部分&#xff08;Core&#xff09;和输入输出部分&#xff08;IO&#xff09;可能采用不同的电压。核心电压通常较低&#xff0c;以减少功耗和发热&#xff0c;提高能效&#xff1b;而IO电…

Linux驱动开发常用调试方法汇总

引言&#xff1a;在 Linux 驱动开发中&#xff0c;调试是一个至关重要的环节。开发者需要了解多种调试方法&#xff0c;以便能够快速定位和解决问题。 1.利用printk 描述&#xff1a; printk 是 Linux 内核中的一个调试输出函数&#xff0c;类似于用户空间中的 printf。它用于…

CSID-GAN:基于生成对抗网络的定制风格室内平面设计框架论文阅读

CSID-GAN: A Customized Style Interior Floor Plan Design Framework Based on Generative Adversarial Network 摘要前言II. CSID-GAN METHODA. Overall FrameworkB. Algorithm and Loss Function III. DATASETS AND EVALUATION METRICSA. DatasetsB. Evaluation Metrics IV.…