读未提交(Read UnCommitted/RU)
⼜称为脏读,⼀个事务可以读取到另⼀个事务未提交的数据。这种隔离级别岁最不安全的⼀种,因为未提交的事务是存在回滚的情况。
读已提交(Read Committed/RC)
⼜称为不可重复读,⼀个事务因为读取到另⼀个事务已提交的修改数据,导致在当前事务的不同时间读取同⼀条数据获取的结果不致。
举个例⼦,在下⾯的例⼦中就会发现SessionA在⼀个事务期间两次查询的数据不⼀样。原因就是在于当前隔离级别为 RC,SessionA的事务可以读取到SessionB提交的最新数据。
可重复读(Repeatable Read/RR)
⼜称为幻读,⼀个事物读可以读取到其他事务提交的数据,但是在RR隔离级别下,当前读取此条数据只可读取⼀次,在当前事务中,不论读取多少次,数据任然是第⼀次读取的值,不会因为在第⼀次读取之后,其他事务再修改提交此数据⽽产⽣改变。因此也成为幻读,因为读出来的数据并不⼀定就是最新的数据。
举个例⼦:在SessionA中第⼀次读取数据时,后续其他事务修改提交数据,不会再影响到SessionA读取的数据值。此为可重复读。
串⾏化(Serializable)
所有的数据库的读或者写操作都为串⾏执⾏,当前隔离级别下只⽀持单个请求同时执⾏,所有的操作都需要队列执⾏。所以种隔离级别下所有的数据是最稳定的,但是性能也是最差的。数据库的锁实现就是这种隔离级别的更⼩粒度版本。
不同事务同时操作同⼀条数据产⽣的问题
上⾯的两种情况就是对于⼀条数据,多个事务同时操作可能会产⽣的问题,会出现某个事务的操作被覆盖⽽导致数据丢失。
LBCC,基于锁的并发控制,Lock Based Concurrency Control。
使⽤锁的机制,在当前事务需要对数据修改时,将当前事务加上锁,同⼀个时间只允许⼀条事务修改当前数据,其他事务必须等待锁释放之后才可以操作。
MVCC,多版本的并发控制,Multi-Version Concurrency Control。
使⽤版本来控制并发情况下的数据问题,在B事务开始修改账户且事务未提交时,当A事务需要读取账户余额时,此时会读取到B事务修改操作之前的账户余额的副本数据,但是如果A事务需要修改账户余额数据就必须要等待B事务提交事务。
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提⾼了数据库的并发处理能⼒。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,⽤户可以查看当前数据的前⼀个或者前⼏个历史版本,保证了ACID中的I特性(隔离性)。
InnoDB存储引擎保存的MVCC的数据
InnoDB的MVCC是通过在每⾏记录后⾯保存两个隐藏的列来实现的。⼀个保存了⾏的事务 ID(DB_TRX_ID),⼀个保存了⾏的回滚指针(DB_ROLL_PT)。每开始⼀个新的事务,都会⾃动递增产 ⽣⼀个新的事务id。事务开始时刻的会把事务id放到当前事务影响的⾏事务id中,当查询时需要⽤当前事务id和每⾏记录的事务id进⾏⽐较。下⾯看⼀下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。
SELECT
InnoDB 会根据以下两个条件检查每⾏记录:
undo log
根据⾏为的不同,undo log分为两种:insert undo log 和 update undo log
insert 操作中产⽣的undo log,因为insert操作记录只对当前事务本身课件,对于其他事务此记录不可⻅,所以 insert undo log 可以在事务提交后直接删除⽽不需要进⾏purge操作。
purge的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收undo pages
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。
举个例⼦:
READ COMMITTED 隔离级别下的ReadView 每次读取数据前都⽣成⼀个ReadView (m_ids列表)
这⾥分析下上⾯的情况下的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列表)
所谓的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)。