本文纯粹是学习总结使用,收录好的,容易理解的知识。
文章目录
前言
看看总会有收获。分享快乐。
事务的特性都有什么?
指的是ACID。
- A:原子性。事务不可分割
- C:一致性
- I:隔离性。 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
- D:持久性。数据修改是永久的。
事务的并发问题了解多少?
实际场景下,事务并不是串行的,所以会带来如下三个问题:
- 脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据。
- 不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。
- 幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
MySQL四种隔离级别会产生的并发问题
不同的隔离级别有不同的现象,并有不同的锁定/并发机制,隔离级别越高,数据库的并发性就越差。
-
READ UNCOMMITTED(未提交读):事务中的修改,即使没有提交,对其他事务也都是可见的。 这个会导致脏读。
-
READ COMMITTED(提交读):事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。 会导致不可重复读,这个隔离级别也叫不可重复读。
-
REPEATABLE READ(可重复读):一个事务按相同的查询条件读取以前检索过的数据,其他事务插入了满足其查询条件的新数据。产生幻行。 会导致幻读。
-
SERIALIZABLE(可串行化):强制事务串行执行。
事务隔离级别 脏读 不可重复读 幻读 读未提交(read-uncommitted) 是 是 是 读已提交(read-committed) 否 是 是 可重复读(repeatable-read) 否 否 是(x) 串行化(serializable) 否 否 否
MySQL 默认的事务隔离级别为可重复读RR(repeatable-read) 。
MVCC与事务看下边连接(敖丙大神讲解)
罗列出来:
MVCC 解决数据丢失
MVCC,多版本的并发控制,Multi-Version Concurrency Control。
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。 借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。
重点来了。
InnoDB的MVCC实现逻辑
InnoDB存储引擎保存的MVCC的数据
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。一个保存了行的事务ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PT)。每开始一个新的事务,都会自动递增产 生一个新的事务id。事务开始时刻的会把事务id放到当前事务影响的行事务id中,当查询时需要用当前事务id和每行记录的事务id进行比较。
下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。
SELECT
InnoDB 会根据以下两个条件检查每行记录:
- InnoDB只查找版本早于当前事务版本的数据行(也就是,行的事务编号小于或等于当前事务的事务编号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- 删除的行要事务ID判断,读取到事务开始之前状态的版本,只有符合上述两个条件的记录,才能返回作为查询结果。
INSERT
InnoDB为新插入的每一行保存当前事务编号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前事务编号作为行删除标识。
UPDATE
InnoDB会插入一行新记录,保存当前事务编号作为行版本号,同时保存当前事务编号到原来的行作为行删除标识。
保存这两个额外事务编号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在REPEATABLE READ和READ COMMITIED两个隔离级别下工作。其他两个隔离级别都和 MVCC不兼容 ,因为READ UNCOMMITIED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
MVCC 在mysql 中的实现依赖的是 undo log 与 read view 。
undo log
根据行为的不同,undo log分为两种: insert undo log 和 update undo log
- insert undo log:
insert 操作中产生的undo log,因为insert操作记录只对当前事务本身课件,对于其他事务此记录不可见,所以 insert undo log 可以在事务提交后直接删除而不需要进行purge操作。
purge的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收undo pages
数据库 Insert时的数据初始状态:
update undo log:
update 或 delete 操作中产生的 undo log。 因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。
数据第一次被修改时:
当另一个事务第二次修改当前数据:
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式。
ReadView
对于 RU(READ UNCOMMITTED) 隔离级别下,所有事务直接读取数据库的最新值即可,和 SERIALIZABLE 隔离级别,所有请求都会加锁,同步执行。所以这对这两种情况下是不需要使用到 Read View 的版本控制。
对于 RC(READ COMMITTED) 和 RR(REPEATABLE READ) 隔离级别的实现就是通过上面的版本控制来完成。两种隔离界别下的核心处理逻辑就是判断所有版本中哪个版本是当前事务可见的处理。针对这个问题InnoDB在设计上增加了ReadView的设计,ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。
对于查询时的版本链数据是否看见的判断逻辑:
- 如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
举个例子:
READ COMMITTED 隔离级别下的ReadView
每次读取数据前都生成一个ReadView (m_ids列表)
时间 | Transaction 777 | Transaction 888 | Trasaction 999 |
---|---|---|---|
T1 | begin; | ||
T2 | begin; | begin; | |
T3 | UPDATE user SET name = ‘CR7’ WHERE id = 1; | ||
T4 | … | ||
T5 | UPDATE user SET name = ‘Messi’ WHERE id = 1; | SELECT * FROM user where id = 1; | |
T6 | commit; | ||
T7 | UPDATE user SET name = ‘Neymar’ WHERE id = 1; | ||
T8 | SELECT * FROM user where id = 1; | ||
T9 | UPDATE user SET name = ‘Dybala’ WHERE id = 1; | ||
T10 | commit; | ||
T11 | SELECT * FROM user where id = 1; |
这里分析下上面的情况下的ReadView
时间点 T5 情况下的 SELECT 语句:
当前时间点的版本链:
此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777,和事务888 都未提交,所以此时的活跃事务的ReadView的列表情况 m_ids:[777, 888] ,因此查询语句会根据当前版本链中小于 m_ids 中的最大的版本数据,即查询到的是 Mbappe。
时间点 T8 情况下的 SELECT 语句:
当前时间的版本链情况:
此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777已经提交,和事务888 未提交,所以此时的活跃事务的ReadView的列表情况 m_ids:[888] ,因此查询语句会根据当前版本链中小于 m_ids 中的最大的版本数据,即查询到的是 Messi。
时间点 T11 情况下的 SELECT 语句:
当前时间点的版本链信息:
此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777和事务888 都已经提交,所以此时的活跃事务的ReadView的列表为空 ,因此查询语句会直接查询当前数据库最新数据,即查询到的是 Dybala。
总结: 使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。
REPEATABLE READ 隔离级别下的ReadView
在事务开始后第一次读取数据时生成一个ReadView(m_ids列表)
时间 | Transaction 777 | Transaction 888 | Trasaction 999 |
---|---|---|---|
T1 | begin; | ||
T2 | begin; | begin; | |
T3 | UPDATE user SET name = ‘CR7’ WHERE id = 1; | ||
T4 | … | ||
T5 | UPDATE user SET name = ‘Messi’ WHERE id = 1; | SELECT * FROM user where id = 1; | |
T6 | commit; | ||
T7 | UPDATE user SET name = ‘Neymar’ WHERE id = 1; | ||
T8 | SELECT * FROM user where id = 1; | ||
T9 | UPDATE user SET name = ‘Dybala’ WHERE id = 1; | ||
T10 | commit; | ||
T11 | SELECT * FROM user where id = 1; |
时间点 T5 情况下的 SELECT 语句:
当前版本链:
再当前执行select语句时生成一个ReadView,此时 m_ids 内容是:[777,888],所以但前根据ReadView可见版本查询到的数据为 Mbappe
时间点 T8 情况下的 SELECT 语句:
当前的版本链:
此时在当前的 Transaction 999 的事务里。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,999],所以此时查询数据依然是 Mbappe。
时间点 T11 情况下的 SELECT 语句:
当前的版本链:
此时情况跟T8完全一样。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,999],所以此时查询数据依然是 Mbappe。
MVCC总结:
所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTD 、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写
、 写-读
操作并发执行,从而提升系统性能。
在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。而 REPEATABLE READ 中,在当前事务第一次查询时生成当前的 ReadView,并且当前的 ReadView 会一直沿用到当前事务提交,以此来保证可重复读(REPEATABLE READ)。
敖丙大神还是牛X啊。看完以后豁然开朗的感觉。
还有一个总结说的很好:MySQL实战45讲 第八章说道
InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的
up_limit_id。普通查询语句是一致性读,一致性读会根据 row trx_id 和 up_limit_id 的大小决
定数据版本的可见性 。
-
对于可重复读,查询只承认在事务启动前就已经提交完成的数据
-
对于读已提交,查询只承认在语句启动前就已经提交完成的数据
而当前读,总是读取到当前提交的最新版本数据
文章里的这个 up_limit_id 变量类似于m_ids .