【MySQL高阶】29.事务(4)

发布时间:2026/6/8 9:43:52

【MySQL高阶】29.事务(4) 文章目录5. 隔离性实现原理5.5 READ UNCOMMITTED - 读未提交与脏读5.5.1 实现方式5.5.2 存在问题5.5.3 问题重现5.6 READ COMMITTED - 读已提交与不可重复读5.6.1 实现方式5.6.2 存在问题5.6.3 问题重现5.7 REPEATABLE READ - 可重复读与幻读5.7.1 实现方式5.7.2 存在问题5.7.3 问题重现5.8 SERIALIZABLE - 串行化5.8.1 实现方式5.8.2 存在问题5.9 不同隔离级别的性能与安全5.10 多版本控制(MVCC)5.10.1 实现原理5.10.1.1 版本链5.10.1.2 ReadView5.10.2 MVCC是否可以解决不可重复读与幻读5. 隔离性实现原理5.5 READ UNCOMMITTED - 读未提交与脏读5.5.1 实现方式读取时不加任何锁直接读取版本链中的最新版本也就是当前读可能会出现脏读不可重复读、幻读问题更新时加共享行锁(S锁)事务结束时释放在数据修改完成之前其他事务不能修改当前数据但可以被其他事务读取。5.5.2 存在问题事务的READ UNCOMMITTED隔离级别不使用独占锁所以并发性能很高但是会出现大量的数据安全问题。比如在事务A中执行了一条INSERT语句在没有执行COMMIT的情况下会在事务B中被读取到此时如果事务A执行回滚操作那么事务B中读取到事务A写入的数据将没有意义我们把这个理象叫做 “脏读” 。5.5.3 问题重现在一个客户端A中先设置全局事务隔离级别为READ UNCOMMITTED读未提交:# 设置隔离级别为READ UNCOMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) # 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-UNCOMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)打开另一个客户端B并确认隔离级别# 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-UNCOMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B由于READ UNCOMMITTED读未提交会出现脏读现象在正常的业务中出现这种问题会产生非常危重后果所以正常情况下应该避免使用READ UNCOMMITTED读未提交这种的隔离级别。5.6 READ COMMITTED - 读已提交与不可重复读5.6.1 实现方式读取时不加锁但使用快照读即按照MVCC机制读取符合ReadView要求的版本数据每次查询都会构造一个新的ReadView可以解决脏读但无法解决不可重复读和幻读问题更新时加独占行锁(X)事务结束时释放数据在修改完毕之前其他事务不能修改也不能读取这行数据。5.6.2 存在问题为了解决脏读问题可以把事务的隔离级别设置为READ COMMITTED这时事务只能读到了其他事务提交之后的数据但会出现不可重复读的问题比如事务A先对某条数据进行了查询之后事务B对这条数据进行了修改并且提交(COMMIT)事务事务A再对这条数据进行查询时得到了事务B修改之后的结果这导致了事务A在同一个事务中以相同的条件查询得到了不同的值这个现象要不可重复读。5.6.3 问题重现在一个客户端A中先设置全局事务隔离级别为READ COMMITTED读未提交:# 设置隔离级别为READ COMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; Query OK, 0 rows affected (0.00 sec) # 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)打开另一个客户端B并确认隔离级别# 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B5.7 REPEATABLE READ - 可重复读与幻读5.7.1 实现方式读取时不加锁也使用快照读按照MVCC机制读取符合ReadView要求的版本数据但无论事务中有几次查询只会在首次查询时生成一个ReadView可以解决脏读、不可重复读配合Next-Key行锁可以解决一部分幻读问题更新时加Next-Key行锁事务结束时释放在一个范围内的数据修改完成之前其他事务不能对这个范围内的数据进行修改、插入和删除操作同时也不能被查询。5.7.2 存在问题事务的REPEATABLE READ隔离级别是会出现幻读问题的在InnoDB中使用了Next-Key行锁来解决大部分场景下的幻读问题那么在不加Next-Key行锁的情况下会出现什么问题吗我们知道Next-Key锁锁住的是当前索引记录以及索引记录前面的间隙那么在不加NextKey锁的情况下也就是只对当前修改行加了独占行锁(X)这时记录前的间隙没有被锁定其他的事务就可以向这个间隙中插入记录就会导致一个问题事务A查询了一个区间的记录得到结果集A事务B向这个区间的间隙中写入了一条记录事务A再查询这个区间的结果集时会查到事务B新写入的记录得到结果集B两次查询的结果集不一致这个现象就是幻读。5.7.3 问题重现由于REPEATABLE READ隔离级别默认使用了Next-Key锁为了重现幻读问量我们把隔离级回退到更新时只加了排他锁的READ COMMITTED.# 设置隔离级别为READ COMMITTED读未提交 mysql SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; Query OK, 0 rows affected (0.00 sec) # 查看设置是否生效 mysql SELECT GLOBAL.transaction_isolation; -------------------------------- | GLOBAL.transaction_isolation | -------------------------------- | READ-COMMITTED | # 已生效 -------------------------------- 1 row in set (0.00 sec)在不同的客户端中执行事务会话A开启事务A会话B开启事务B5.8 SERIALIZABLE - 串行化5.8.1 实现方式读取时加共享表锁读取版本链中的最新版本事务结束时释放更新时加独占表锁事务结束时释放完全串行操作可以解决所有事务问题。5.8.2 存在问题所有的更新都是串行操作效率极低。5.9 不同隔离级别的性能与安全5.10 多版本控制(MVCC)上一个小节介绍了实现事务隔离性的锁机制但是频繁加锁与释放锁会对性能产生比较大的影响为了提高性能InnoDB与锁配合同时采用另一种事务隔离性的实现机制MVCC即MultiVersionedConcurrency Control多版本并发控制用来解决脏读、不可重复读等事务之间读写问题MVCC在某些场景中替代了低效的锁在保证了隔离性的基础上提升了读取效率和并发性。5.10.1 实现原理5.10.1.1 版本链MVCC的实现是基于Undo Log版本链和ReadView来完成的Undo Log做为回滚的基础在执行Update或Delete操作时会将每次操作的上一个版本记录在Undo Log中每条Undo Log中都记录一个叫做roll_pointer的引用信息通过roll_pointer就可以将某条数据对应的Undo Log组织成一个Undo链在数据行的头部通过数据行中的roll_pointer与Undo Log中的第一条日志进行关联这样就构成一条完整的数据版本链如下图所示Undo Log的具体结构和行结构请参考InnoDB存储引擎专题每一条被修改的记录都会有一条版本链体现了这条记录的所有变更当有事务对这条数据进行修改时将修改后的数据链接到版本链接的头部如下图中UNDO35.10.1.2 ReadView每条数据的版本链都构造好之后在查询时具体选择哪个版本呢这里就需要使用ReadView结构来实现了所谓ReadView是一个内存结构顾名思义是一个视图在事务使用select查询数据时就会构造一个ReadView里面记录了该版本链的一些统计值这样在后续查询处理时就不用遍历所有版本链了这些统计值具体包括m_ids当前所有活跃事务的集合m_low_limit_id活跃事务集合中最小事务Idm_up_limit_id下一个将被分配的事务Id也就是 版本链头的事务Id 1m_creator_trx_id创建当前ReadView的事务Id对应的源码如下/***************************************************************************** /** file include/read0types.h Cursor read Created 2/16/1997 Heikki Tuuri *******************************************************/ // ... 省略 // Friend declaration class MVCC; /** Read view lists the trx ids of those transactions for which a consistent read should not see the modifications to the database. */ class ReadView { // ... 省略 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; // 创建当前ReadView的事务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 */ // 如果当前ReadView和其他ReadView不需要事务Id小于此值的Undo日志可以在purge阶段删除 trx_id_t m_low_limit_no; /** AC-NL-RO transaction view that has been closed. */ bool m_closed; // 是否关闭标识 // ... 省略 };构造好ReadView之后需要根据一定的查询规则找到唯一的可用版本这个查找规则比较简单以下图的版本链为例在m_creator_trx_id201的事务执行select时会构造一个ReadView同时对相应的变量赋值m_ids活跃事务集合为[90, 100, 200]m_up_limit_id活跃事务最小事务Id 90m_low_limit_id预分配事务ID 202最大事务Id 预分配事务ID - 1201m_creator_trx_id当前创建ReadView的事务Id 201接下来找到版本链头从链头开始遍历所有版本根据四步查找规则判断每个版本第一步判断该版本是否为当前事务创建若m_creator_trx_id等于该版本事务id意味着读取自己修改的数据可以直接访问如果不等则到第二步第二步若该版本事务Id m_up_limit_id(最小事务Id)意味着该版本在ReadView生成之前已经提交可以直接访问如果不是则到第三步第三步或该版本事务Id m_low_limit_id(最大事务Id)意味着该版本在ReadView生成之后才创建所以肯定不能被当前事务访问所以无需第四步判断直接遍历下一个版本如果不是则到第四步第四步若该版本事务Id在m_up_limit_id(最小事务Id)和m_low_limit_id(最大事务Id)之间同时该版本不在活跃事务列表中意味着创建ReadView时该版本已经提交可以直接访问如果不是则遍历并判断下一个版本这样从版本链头遍历判断到版本链尾找到首个符合要求的版本即可就可以实现查询到的结果都是已经提交事务的数据解决了脏读问题。5.10.2 MVCC是否可以解决不可重复读与幻读首先幻读无法通过MVCC单独解决对于不可重复读问题在事务中的第一个查询时创建一个ReadView后续查询都是用这个ReadView进行判断所以每次的查询结果都是一样的从而解决不可重复读问题在REPEATABLE READ可重复读隔离级别下就采用的这种方式如果事务每次查询都创建一个新的ReadView这样就会出现不可重复读问题在READ COMMITTED读已提交的隔离级别下就是这种实现方式以上就是关于MVCC的相关介绍加上锁就可以实现完整的ACID中的隔离性。

相关新闻