楚雄网站建设,fusion app,广州seo网站推广技巧,广州百度网站排名优化文章目录 InnoDB 事务模型事务ACID特性事务隔离级别 事务操作事务并发问题事务数据读写类型Consistent Nonlocking Reads 快照读Locking Reads 加锁读 MVCC 并发控制实现原理InnoDB 隐藏列Read ViewUndo log实现过程 MVCC与隔离级别MVCC和辅助索引 幻读可重复读MVCC会出现幻读的… 文章目录 InnoDB 事务模型事务ACID特性事务隔离级别 事务操作事务并发问题事务数据读写类型Consistent Nonlocking Reads 快照读Locking Reads 加锁读 MVCC 并发控制实现原理InnoDB 隐藏列Read ViewUndo log实现过程 MVCC与隔离级别MVCC和辅助索引 幻读可重复读MVCC会出现幻读的情况 InnoDB 事务模型
事务ACID特性
事务有以下4个特点
原子性Atomicity指事务是一个不可分割的工作单位事务中的操作要么都发生要么都不发生。一致性Consistency事务必须使数据库从一个一致性状态变换到另外一个一致性状态数据还是准确可靠的多条DML语句同时成功同时失败隔离性Isolation事务的隔离性是指一个事务的执行不能被其他事务干扰即一个事务内部的操作及使用的数据对并发的其他事务是隔离的并发执行的各个事务之间不能互相干扰。持久性Durability持久性是指一个事务一旦被提交它对数据库中数据的改变就是永久性的数据持久到硬盘文件事务才算结束接下来的其他操作和数据库故障不应该对其有任何影响
事务隔离级别
SQL标准提出了四种隔离级别来规避这些现象隔离级别越高性能效率就越低这四个隔离级别如下
读未提交read uncommitted指一个事务还没提交时它做的变更就能被其他事务看到读提交read committed指一个事务提交之后它做的变更才能被其他事务看到可重复读repeatable read指一个事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的MySQL InnoDB 引擎的默认隔离级别串行化serializable 会对记录加上读写锁在多个事务对这条记录进行读写操作时如果发生了读写冲突的时候后访问的事务必须等前一个事务执行完成才能继续执行
针对不同的隔离级别并发事务时可能发生的现象也会不同。也就是说
在读未提交隔离级别下可能发生脏读、不可重复读和幻读现象在读提交隔离级别下可能发生不可重复读和幻读现象但是不可能发生脏读现象在可重复读隔离级别下少数情况会发生幻读现象但是不会发生脏读和不可重复读现象在串行化隔离级别下脏读、不可重复读和幻读现象都不可能会发生。
不同的数据库厂商对 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锁。
事务并发问题
下面介绍几种常见事务并发问题 脏读指在一个事务过程里读取了另一个未提交的事务中的数据。比如事务 T1将某一值修改然后事务T2读取该值此后T1因为某种原因撤销对该值的修改这就导致了T2所读取到的数据是无效的即脏数据 不可重复读指对于数据库中的某条数据一个事务范围内多次查询返回不同的数据值这里不同是指某一条或多条数据的内容前后不一致但数据条数相同。出现此问题的原因可能是由于在查询间隔该事物需要用到的数据被另一个事务修改并提交了。不可重复读和脏读的区别是脏读是某一事务读取了另一个事务未提交的脏数据而不可重复读则是读取了其他事务提交的数据如果没有其他事务提交那么还是相同的值。需要注意的是在某些情况下不可重复读并不是问题
不可重复读可以这样理解有一个大事务要执行很长时间另外有一堆小事务很快就执行完成。大事务来回查一个数据。一堆小事务串行改这个数据改完之后提交成功。大事务虽然读到的也是小事务提交之后的内容但是这个数据经常被小事务改来改去。
幻读例如事务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 COMMITTED和REPEATABLE READ这两个隔离级别下处理SELECT语句的默认模式也就是说普通的SELECT语句就会执行快照读操作。不同的隔离级别快照的生成时间不同。
如果使用REPEATABLE READ隔离级别快照基于执行第一次读取操作的时间。使用 READ COMMITTED 隔离级别将快照重置为每个一致性读取操作的时间。其他隔离级别不需使用快照读
本次查询会看到在该时间点之前提交的事务所做的更改以及稍后或未提交的事务所做的更改。此规则的例外是查询会看到同一事务中先前语句所做的更改。 如果另一个事务在已经分配给你的时间点后删除了一行并提交你不会看到该行已被删除。 插入和更新的处理方式类似。
此例外会导致以下异常如果您更新表中的某些行则 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 autocommit0;SET autocommit0;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 语句 DROP TABLEMySQL 无法使用已删除的表并且 InnoDB 会破坏该表。ALTER TABLE 操作会形成原始表的临时副本并在构建临时副本时删除原始表
当您在事务中重新发出一致读取时新表中的行不可见因为在获取事务的快照时这些行不存在。在这种情况下事务返回错误ER_TABLE_DEF_CHANGED“表定义已更改请重试事务”。
对于未指定 FOR UPDATE 或 LOCK IN SHARE MODE 的子句如 INSERT INTO ... SELECT、UPDATE ... (SELECT) 和 CREATE TABLE ... SELECT中的select读取的类型会有所不同
默认情况下InnoDB 在这些语句中使用更强的锁并且 SELECT 部分的行为类似于READ COMMITTED即每个一致性读操作即使在同一个事务中都设置和读取自己的新快照。要在这种情况下执行非锁定读取需要启用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 UPDATE、SELECT ... FOR SHARESELECT ... LOCK IN SHARE MODE 都是加锁读。MySQL 8 中SELECT ... FOR SHARE代替了SELECT ... LOCK IN SHARE MODE但为了向后兼容LOCK IN SHARE MODE仍可用两者是等价的。区别在于FOR SHARE 支持 OF table_nameNOWAIT和SKIP LOCKED等选项后面会展开解释。 为什么读操作需要加锁 如果在同一事务中执行了普通 SELECT 的同时又执行了 INSERT 或 UPDATE那么普通的 SELECT 语句无法提供足够的保护其他的事务可以更新或删除当前事务查询的某个结果。因此需要通过加锁来保证数据一致性 为什么UPDATE要加锁 比如要update一条记录但是在另一个事务中已经delete掉这条数据并且commit了如果update就会产生冲突所以在update的时候需要知道最新的数据。并且 update 时需要加锁 下面介绍几种常见的加锁读操作
SELECT … LOCK IN SHARE MODE 在读取的每一行上设置共享锁其他会话可以读取这些行但在持有锁的事务提交之前不能修改它们。 如果其中任何行被另一个尚未提交的事务更改则查询将等待该事务结束然后使用最新值。SELECT … FOR UPDATE 对查询的每一行以及查询使用到的所有的索引项进行加锁效果等同于更新这些行的UPDATE 语句。其他事务如果想要更新这些被加锁的行或者执行SELECT … LOCK IN SHARE MODE以及在某些隔离级别下想要读取这些数据都会被阻塞。加锁读会忽略Read View中存在的记录上设置的任何锁不能锁定旧版本的记录它们是通过undo log中保存的记录在内存中的副本上来重建的。
当事务提交或回滚时所有由 LOCK IN SHARE MODE 和 FOR UPDATE 查询设置的锁都会被释放。 注意事项 加锁读可能造成死锁具体取决于事务的隔离级别加锁读操作不允许用于只读事务中的全局表只有在禁用自动提交时才能执行加锁读操作使用START TRANSACTION开始事务或者手动关闭自动提交set auto commit设置为 0。外部语句中的加锁读子句不会锁定嵌套子查询中表的行除非子查询中还指定了锁定读取子句。 例如以下语句不会锁定表 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使用这些信息来完成在事务回滚时需要进行的一些恢复操作还可使用这些信息构建数据行的早期版本以实现快照读。
数据库并发的场景大概分为三种
只有读操作这种情况比较少只有读操作就不用加锁了不会有并发问题有读有写最常见的场景有并发问题可能遇到脏读幻读不可重复读只有写操作有线程安全问题可能会存在更新丢失问题比如一个事务覆盖了之前事务的更新结果
现在有下面这一张表
CREATE TABLE account (id int NOT NULL AUTO_INCREMENT COMMENT 主键,name varchar(64) DEFAULT NULL COMMENT 用户名,money decimal(10, 5) COMMENT 金额,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb3 COMMENT账户;实现原理
InnoDB 通过以下方式实现了MVCC
在每行记录后面保存两个隐藏的列一个是行的事务IDDB_TRX_ID用于记录创建或修改该行的事务ID另一个是回滚指针DB_ROLL_PTR指向该行可能存在的一个旧版本通过 undo 日志记录数据的旧版本信息以便在需要时能够回滚到旧版本。在执行查询时根据当前事务的ID和每行记录的事务ID以及回滚指针确定哪些版本的数据对当前事务是可见的。
下面分别介绍这三部分
InnoDB 隐藏列
InnoDB内部会向数据库中存储的每一行记录添加三个字段
6字节的DB_TRX_ID字段此字段表示最后插入或者更新该行的事务ID删除delete在InnoDB内部会被视为update被删除的行数据中会有一个特殊的bit位来标记该行被删除7字节的DB_ROLL_PTR字段称为回滚指针该指针指向写到回滚段的一条undo log记录如果该行被update那么该undo log记录将包含更新之前的该行的数据可用于重建该行数据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 Tableparam[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 dont 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_limitsuint64_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 结构主要包含以下几个字段
m_idsRead View生成时系统正活跃的事务ID列表包含了在ReadView生成时系统中所有未提交的事务ID。用于判断某个版本的数据是否对当前事务可见。如果某个数据版本的事务ID在m_ids中且不等于当前事务IDm_creator_trx_id则该版本对当前事务不可见。m_up_limit_id记录 m_ids 列表中事务ID最小的ID用于快速判断一个事务是否在ReadView生成前已经提交。如果某个数据版本的事务ID小于min_trx_id则表明生成该版本的事务在生成ReadView前已经提交因此该版本对当前事务可见。m_low_limit_idReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值1。由于事务ID是自增的这个值用于判断某个数据版本是否在ReadView生成后产生。如果某个数据版本的事务ID大于等于max_trx_id则表明生成该版本的事务在生成ReadView后才生成因此该版本对当前事务不可见。m_creator_trx_id创建ReadView的事务ID。即当前执行快照读的事务ID。 在判断数据版本可见性时如果数据版本的事务ID等于m_creator_trx_id即使该事务在m_ids中即未提交也认为该版本对当前事务可见因为是自己生成的数据。 Read View 和隔离级别的关系 使用Read View的隔离级别是 REPEATABLE READ、READ COMMITTED 和 READ UNCOMMITTED
ReadView的生成时机和复用策略与事务的隔离级别有关
读已提交RC在每一次执行快照读时都会生成新的ReadView。可重复读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
insert undo log仅在事务回滚中需要并且在事务提交后会被立即丢弃。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 checkparam[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_IDparam[in] name table namereturn 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 所在的旧记录就是当前事务能看见的最新的老版本。一般流程如下
首先比较 DB_TRX_ID up_limit_id, 如果小于则当前事务能看到DB_TRX_ID 所在的记录如果大于等于进入下一个判断接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的那对当前事务肯定不可见如果小于则进入下一个判断判断DB_TRX_ID 是否在活跃事务列表之中如果在则代表Read View生成时刻最后操作该记录的事务还没有提交Read View是看不见这行数据的如果不在则说明最后操作该记录的事务在Read View生成之前就已经提交了因此 Read View 是可以看见这行数据的
MVCC与隔离级别
在MySQL中不同的事务隔离级别对数据的可见性和一致性有不同的要求而MVCC是实现这些隔离级别的一种重要手段。MVCC主要属于读已提交和可重复读这两个隔离级别下具体来说
READ UNCOMMITTED 在这个隔离级别下可能出现脏读事务可以读取到其他事务未提交的数据。无需锁无需mvcc因为修改数据直接改源数据 会出现脏读。由于MVCC要求事务只能读取到已提交的数据版本因此在读未提交隔离级别下MVCC不生效。READ COMMITTED 在这个隔离级别下事务只能读取到其他事务已提交的数据。每次查询都会创建ReadView读取数据。MySQL通过MVCC实现了读已提交隔离级别确保每个事务读取到的数据都是最新的已提交数据。REPEATABLE READ MySQL的默认隔离级别在这个隔离级别下同样的查询只有第一次会创建ReadView读取数据事务在整个执行过程中看到的数据是一致的即使其他事务提交了更新当前事务也不会看到这些更新。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
幻读是指在一个事务读取了几行数据后另一个并发事务插入了一些新行然后第一个事务再次读取同样的范围时会看到由于其他事务的插入操作而幻影般地出现的新行。
当同一个查询在不同的时间产生不同的结果时事务中就会出现所谓的幻行问题。 例如一条 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 可防止任何人同时为您的行插入重复项。 可重复读
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」但是它很大程度上避免幻读现象并不是完全解决了解决的方案有两种
针对快照读普通 select 语句是通过 MVCC 方式解决了幻读因为可重复读隔离级别下事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的即使中途有其他事务插入了一条数据是查询不出来这条数据的所以就很好了避免幻读问题。针对加锁读select … for update 等语句是通过 next-key lock记录锁间隙锁方式解决了幻读因为当执行 select … for update 语句的时候会加上 next-key lock如果有其他事务在 next-key lock 锁范围内插入了一条记录那么这个插入语句就会被阻塞无法成功插入所以就很好了避免幻读问题。 MVCC会出现幻读的情况
MVCC并不能完全避免幻读某些场景下还是会出现幻读的情况如下图所示 使用快照读的情况下不会加间隙锁所以事务B的新增操作立刻就成功了但是事务A是查不出来的