目录
- 事务
- 事务的四大特性ACID
- 事务相关SQL语句
- 事务原理
- 事务如何解决隔离性
- 隔离性总结
- 事务如何解决原子性、一致性、持久性
- redo log 重做日志
- CheckPoint 检查点机制
- Double Writer 双写磁盘
- undo log 回滚日志
- 锁
- 表级锁
- 表读锁、表写锁
- 元数据锁MDL
- 意向锁
- 行级锁
- 行读锁,行写锁
- 间隙锁
- 临键锁
- MVCC
- 实现原理
- 读已提交 RC 详细原理
- 可重复读 RR 详细原理
事务
事务在MySQL中是InnoDB引擎独有的,将一组SQL语句看做一个不可分割整体,整体中所有SQL语句的执行要么全部成功,要么全部失败。
事务的四大特性ACID
原子性 Atomicity
:原子是不可分割的做小单位。在一个事务中将的所有SQL看是一个整体,要么全部成功,要么全部失败,是不可分割的最小单位。一致性 Consistency
:事务的执行前和执行后数据都要保持一致状态,数据总量不变。例:A有10元,B有10元,此时A要给B转10元。此时就要开启事务,A减去10元,B加上10元,事务提交。提交后,A有0元,B有20元,钱的总数保持一致,没有凭空消失。
那么如果开启事务,将A的账户直接减去10元不就达不到一致性了嘛?我的理解:A直接操作一条SQL,没有利用上事务隔离性 Isolation
:事务内不受并发线程影响,每一个线程保持独立环境运行。例:A开启事务将id为2的数据改为0,在未提交事务之前,其他线程不能够操作id为2的数据,使得即使在多个线程下,依然能够有独立环境运行。持久性 Durability
:事务一但提交或回滚,就是永久性不可更改。例如:A开启事务,给B转了10元,一但提交事务,这此次操作的10元就再也不会回来;一但回滚事务,这此次操作的10元就不会再转给B。
事务相关SQL语句
事务有开启就一定会有回滚或提交,即使没有手动操作,等到超时后也会自动回滚。开启与提交``、回滚是一组操作。
- 事务开启
begin;
或START TRANSACTION;
- 事务提交
commitl
- 事务回滚
rollback;
事务是默认自动提交的,即每条SQL都是一个独立的事务,要想使得事务包含一组SQL,就要开启手动提交事务
- 查看当前会话事务是否自动提交
SELECT @@autocommit
结果为 1自动提交 0 手动提交 - 将当前事务设置为手动提交
SET @@autocommit = 0 ;
事务原理
事务如何解决隔离性
如果事务只有ACD,没有隔离性I,那么就会面临线程安全问题。
一个数据库服务器可供多次连接同时使用,每一个连接在服务器中都会为其创建一个线程,多个线程间共同工作。那么就面临一个问题:多个线程间操作一个数据怎么办?例如:id=1的金额初始为0元,A线程,开启事务,给id=1的金额加10元;这时B线程,开启事务将id=1的金额减去20元,A提交事务,B回滚事务。理想中应该id=1的金额应该为10元,但实际为-10元,就是多个线程间操作相同数据带来线程安全问题
。这个例子就是众多安全问题中最严中一类,为脏写
。
多线程带来的问题: 按照严重程度从低到高
线程间可能对同一个数据发生写写
,读写
,读读
。读读不会有任何安全问题,但写写与读写就分被对应脏写,脏读问题
脏写
:上面例子已经介绍,只要是想要进行多线程操作,就一定要解决脏写问题。脏读
:一个线程读取了另一个线程未提交的数据。例如 id=1的金额为0元,A用户开启事务向其转10元,这时B线程查询id=1的有10元,进行相应的操作,然后A竟然回滚了,id=1的线程又回到0元,那么B线程的一顿操作是不是就有误了呢?
事务使用锁来解锁多线程安全问题
(具体后面介绍)
脏写解决方法:
:A线程在写入时加入线程锁,其他线程在操作此数据前会检测是否有锁,若没有自己加上锁后再操作,若有,任何写
操作都会等待,一直等到A线程释放锁。这样在A线程写
操作时,其他线程都不能参与,只能进行读取
,为A线程形成了一个独立的操作空间,但有脏读问题。脏读解决方法:
:在上一个的基础上,当其他线程判断要操作的数据有锁时,任何读
,写
都会等待。这样A线程对此数据的任何操作在事务提交前,既不会受到其他线程写干扰,由不会被其他线程看见。
那读读怎么判断?怎么区分线程的读写?因此,锁就被分为了读写锁,写锁是排斥锁,读锁是共享锁。在写时上写锁,使得其他线程上读锁和写锁都失败,在读时上读锁,使得其他线程上读锁成功,上写锁失败
。即写时不能读,读时不能写,写时不能写,读时可以读。
只使用锁带来的问题?,如果在一个线程写入时,其他线程既不能读又不能写,只能等待上一个锁释放,这就成单线程操作了,影响线程的并发性
多版本并发控制MVCC的使用
:(后面会具体介绍)MVCC只能提高读线程的效率
每一条数据修改后都会有对应的历史版本,在其他线程写时,既然不能够读取当前不能够写的数据,那可以读取此数据当前未提交事务前的历史版本,这样就减少了读的线程等待,提高了并发。
但这样又带来3个问题:
不可重复读:
如果在第一次读取数据与第二次读取数据期间,有线程将修改此数据的事务提交,导致读取数据的版本不一致,数据前后就不一致。有A,B同时操作一个有1000元的账户。A开启事务查询账户余额为1000元,这是B开启事务向账户充值了1000元,这时A再次查询账户余额仍为1000元,当B提交事务后,A第三次查询账户发现余额变为了2000。同一个事务,查询同一个数据,结果不一致。解决此问题很简,一个事务中只读取一个版本数据即可
,但会带来下面这个问题幻读(只有插入数据时遇到)
:虽然没有读取到,但数据是真实存在的。因为读取的可能是历史版本,为了解决不可重复读,使得即使当前操作数据的线程提交事务,依然不能读取到。但如果成功获取到锁,插入数据就是要插入最新数据,最新数据可能与历史版本不一致,使其感觉到读取的不真实,仿佛出现幻觉。 例如:A线程开启事务将id=1的用户修改下姓名,这是B线程开启事务读取id为1的用户,读取的是A事务提交前的数据版本,A修改完姓名后,又插入一条id为2的数据,然后提交。B线程由于读的是历史版本并未查询到id为2的数据,但当其插入时,却发现id为2的数据已经存在。问题:在B线程查询时,上的是什么级别锁?
解决方法
:利用间隙锁或临建锁,使得查询的区间不予许插入数据,既然数据无法插入,就解决了幻读问题。MySQL在可重复读阶段就通过锁的方式解决了幻读
但是如果在readview时给读加锁,万一这是主线程已经完成添加数据怎么办?如果在主线程加锁,主线程可以业务就出现问题。
隔离性总结
隔离性面临的问题:线程并发和MVCC带来的问题
-
脏写
:必须解决 -
脏读
-
不可重复读
-
幻读
InnoDB引擎为了提高并发效率,在隔离性进行了取舍,隔离级别越高,系统并发量越低。按照取舍程度不同,又分为4种,按照安全性由低到高排序为 -
读未提交 RU
:解决了脏写问题,在一个线程DML操作某条数据时,对该条数据进行加锁,使其不能修改,不让其他线程DML操作,但可以进行读取。即脏读
-
读已提交 RC
:解决了脏读,读未提交问题。在一个线程DML操作某条数据时,对该数据进行加锁,使其不能修改也不能读取当前数据,只能读取MVCC的ReadView -
可重复读 (默认)RR
:只读取一个版本的ReadView,解决读取不同版本导致数据不一致问题 -
序列化
:在写操作时,其他线程任何读取只能等待。
事务如何解决原子性、一致性、持久性
数据的写操作如果直接操作磁盘可能会发生随机IO,会有很大的性能问题,所以是先从内存中获取,操作内存中的数据,最终由其他线程批量写入磁盘。
持久性:
为了确保内存中脏页最终能够写入磁盘,就要预防可能突发的情况(宕机,断电等),需要redolog日志进行记录。redolog
日志先写在内存中,待事务提交后再写入磁盘中(频率可设定),其顺序IO速度要大于数据的随机IO速度。如果在提交事务后,还没有刷入磁盘,这是发生宕机。待重新启动后,会读取redolog中的内容重新进行写入,确保数据的持久性。如果在事务开启还为提交时发生宕机虽然内存数据消失,redolog内存记录消失,但会提示事务提交失败,磁盘数据不变,依然能够确保持久性。当事务成功提交后,redolog中关于此事务的记录就失去了作用,会被新的记录覆盖。
原子性:当事务成功提交或在事务过程中内存数据消失,会发生上面的情况。如果在代码层面发生意外导致事务回滚或者手动回滚。这是就要使得此事务开启前到回滚时对内存数据的所有操作都要进行回溯。而redolog日志只记录着磁盘哪里修改为什么数据,而没有内存中数据之前是什么样子。这就需要undolog
日志,每执行一条SQL内存中的数据发生变化,该日志都会记录着一条与之相反的SQL,用来恢复数据的变化,达到事务如果成功提交后就一定会存放在磁盘,如果事务中回滚,就一定能回到之前的数据,即原子性。
一致性:有了原子性事务中所有SQL要么全部成功,要么全部失败,有了持久性,所有成功提交的事务都会被刷新到磁盘,二者组合也就达到了一致性。
redo log 重做日志
redolog 的作用就是确保内存中的已提交事务中修改的数据即脏页
能够全部刷新到磁盘的日志备份。
redolog分为两部分,一部分是在内存作为缓存 redo log buffer
默认16M大小,以一部分是磁盘文件redo logo file
,有两个文件 ib_logfile0
和 ib_logfile1
每个48M,可以循环着写
因为要想修改数据,首先到将数据加载到内存中即InnoDB引擎的Buffer Pool
中进行修改,因为磁盘IO读写速度太慢。但在事务提交后就将内存中的数据全部刷新到磁盘,也可能发生每次微小的修改就要进行一次随机IO,效率低下;如果不这样做,内存中的数据是不安全的,就可能发生最坏的情况,当事务开启,完成在内存的修改后,成功提交事务,但提交后突然就宕机了,导致内存的脏页没有刷新到磁盘,造成数据的丢失,违背了持久性。
InnoDB的解决方案是:在修改内存中的数据时,同时将数据修改情况写入redo log Buffer缓存区中,在事务提交后,内存中的脏页不立即进行刷新,交给checkpoint
机制保证数据的最终落盘,redo log Buffer中的内容写入磁盘,因为redo log磁盘文件的写入是顺序写入
且内容少,这就要比直接将数据刷新到磁盘快得多。待redo完成磁盘写入后事务才算提交成功,如果在期间发生意外,事务直接会提价失败,发生回滚。确保在事务提交后,一定有对应的redo日志备份。
当脏页数据在刷新磁盘过程中发生意外丢失时,待服务重启后,可以通过磁盘文件redo log进行恢复。因为redo log记载了磁盘的哪个位置的数据是什么,且是持久性的文件。这样就通过些redo日志解决了内存数据不安全问题。这种技术也叫WAL (Write-Ahead Log)
,当redolog中记录对应的脏页已经成功刷新到了磁盘则此记录也没有用了,会被新记录覆盖
在此图中共有两次刷盘:
- redo 将redo Buffer中的记录在事务提交后刷新到磁盘
- 内存中的脏页定时刷新到磁盘
redo log也并不是一定要在事务提交后进行刷盘,是可以配置刷盘频率的
-
事务提交后不刷盘,由master线程每隔一秒将redo log Buffer中的内容刷新到磁盘,这种存在数据丢失可能,但IO次数少,效率更高
-
事务提交时进行刷盘(默认),每隔1s也刷盘。所以说,redo的刷盘不一定是要在事务提交时的,在事务过程中有1s间隔刷盘,当内存占用超过一半也会刷盘。这种方式最安全
-
事务提交时,只把 redo log buffer 内容写入 page cache,不进行同步。由系统自己决定什么时候同步到磁盘文件。这种方式虽然性能更高,在SQL服务宕机后,依然数据不会消失。但系统宕机,或者断电等依然会使得数据消失,依然不十分可靠。
redo log中记载的是物理日志:即记录的是磁盘具体某个位置的修改情况,所以在数据恢复时,较命令那种因为免去执行,速度更快。
redo log优点
- 减少磁盘的刷新频率,如果没有它那么每次事务提交后都要将脏页写入磁盘
- 顺序写入磁盘,刷盘速度更快
- 分为内存和磁盘两部分,内存部分使得能够连续不断的记录事务执行,磁盘最终进行保存
那既然数据在内存修改后,使用redo可以不立即刷新到磁盘,但redo的大小是有限的,一但超过备份会被覆盖;内存大小是有限的。什么时候要将内存中的数据刷新到磁盘呢?
CheckPoint 检查点机制
它的目标就是以一定时机将内存的脏页刷新到磁盘。两次checkpoint之间的数据一定要全部在redo中记录,否则数据备份不完整,意外发生时,无法完全恢复
checkPoint 共有两种
Sharp checkpoint
:在数据库正常关闭时,将内存中全部
刷新到磁盘Fuzzy checkpoint
:在数据库运行时,在不同的时机将部分
脏页刷新到磁盘,因为全部刷新会有性能问题
那么在运行时,刷新时机是什么时候呢?共有四种触发时机:
Master Thread Checkpoint
:定时刷新,由Master Thread以指定间隔时间,异步
刷新到磁盘FLUSH_LRU_LIST Checkpoint
:利用LUR页面置换算法,在当Buffer Pool内存不足
的时候,page cleaner
将最近最少使用的页刷新到磁盘Async/Sync Flush Checkpoint
:在redo 日志空间不足
的时候,由page cleaner
将一部分脏页刷新到磁盘,这样redo记载的相关脏页记录就能够释放,记录新的数据。但对于是同步和异步呢?取决于redo log剩余的空间Dirty Page too much
:当内存中脏页占比过多时,由Master Thread将脏页刷新到磁盘。保证Buffer Pool有足够的空间
在数据安全的情况下,肯定是越晚刷新到磁盘性能越高,从这方面进行SQL服务性能优化,就让checkpoint尽量少达到触发机制。
就要增大Buffer Pool内存空间,增大redo log的空间。
但将内存中脏页刷新到磁盘时,是直接操作数据对应的磁盘进行随机IO吗?并不是,还有营一个中间缓冲
Double Writer 双写磁盘
为什么称之为双写呢? 因为在内存中一份数据先是顺序
写入磁盘中双写缓冲区,确定写入成功后,在将内存中同样的数据离散
写入对应表空间中。
为什么要有双写机制呢?
先来说 双写机制带来的坏处:将同样的数据写入磁盘两次,多进行了一次IO,虽然是顺序写入,但多了一次IO,使得整体性能下降了5~10%。
再来说为什么引入了双写机制?根本原因就是解决内存数据在刷新磁盘过程中的页损坏问题,也就是部分写问题
页损坏问题:InnoDB引擎是以页为单位的,每页大小为16K,操作系统(Linux)也是以页为单位的,但它的大小是4K,也就是说当内存中的数据要想刷新到磁盘,要先交给操作系统页,由操作系统页去刷新到磁盘。这就代表内存数据每刷新一页,就要分4次写入操作系统。但如果在这4次中发生宕机等意外,导致内存中有一个不完整的页写入了磁盘,导致页损坏(部分写问题
)。这时,也不能违背持久性原则,那因为宕机等没有正确写入磁盘的数据怎样进行恢复呢?
首先,我想到的就是确保内存数据写入磁盘的还有一个redo日志。但是redo log记载的是什么?是物理记录,即xxx页偏移量为xxx的地址数据为xxxx,但没有写完整导致宕机记载的页已经损坏!
,页损坏了,相当于对应页地址的数据记录也无效了。因此redo无法使用
binlog呢?binlog的功能时数据备份和主从复制,它根本不关注内存与磁盘数据之间即它无法区分哪些是已写入磁盘,哪些是在脏页。
要是进行全量恢复,貌似还可以。但代价也太大了,是不现实的事。
这时就引入了双写机制,即不先操作redo log记载的页,这样就能防止在写入过程中发生意外后,导致redo 记载的页发生损坏使得无效。
那么先把数据写入哪里呢?写入的地方一定要是磁盘中,否则没有意义。因为做的的就是内存与磁盘间交互备份。
InnoDB引擎给出的是顺序先写入系统表空间中,成功后离散写入对应独立表空间。InnoDB还为double write的写入速度做了优化,因为这样写入是顺序写入,多了一个IO操作,也尽量减少其带来的性能损耗。
以上就是为什么要引入双写机制
Double write文件
有了这个机制肯定有对应的存储文件。
双写缓存去共有部分,一部分在内存,一部分在磁盘。每部分都固定为2M
双写缓冲区是磁盘表空间上的128个页,即两个区每个区1M,共2M。当Buffer Pool数据进行复制时,要先将数据复制到双写缓存的内存部分,再由内存部分分两次,每次以1M大小先顺序写入磁盘部分的双写文件,写入成功后,再将同样的数据离散写入对应的独立表空间中。
当内存触发了checkponit机制后,开始将部分页写磁盘。具体流程如下:
- 将要写入的脏页进行在内存中进行复制,复制到
double write
内存部分 - double write 内存部分每次写入1个区即1M,分两次顺序写入系统表空间部分
- 确定写入成功后, double write内存部分再将同样的数据离散写入对应的独立表空间
如果在双写过程发生意外呢
第一写发生意外:第一次是将内存数据(double write 内存缓存区)写入系统表文件中进行备份
如果此过程发生意外,但redo记载的页没有发生损坏
,可以使用redo直接进行恢复
第二次写发生意外:第二次写入是将内存数据(double write 内存缓存区)写入对应的独立表空间中
写入磁盘发生意外会导致页损坏,这时redo记载的页也无效。但有了系统表空间的文件,系统在重新启动,会检查过程页是否完好,发现页损坏会从系统表空间中进行恢复
redo页是不可能发生意外的, 因为只有redo页写入成功后,事务才算完成。
undo log 回滚日志
redo页只能处理事务正常提交的情况,而如果在事务期间发生异常导致回滚或者进行手动回滚,都不能恢复事务开启前的数据。因为内存中的数据在执行过SQL后已经发生改变,redo记录的都是磁盘数据的修改。这时,就需要一个undo log
作为回滚日志,当执行一条SQL后,在回滚日志中就要记录对应恢复原数据的指令。例如 insert id=2的数据,在undo log中就要记录delete id=2。但并不是完整的SQL,而是像伪SQL。当执行回滚后,在通过执行此事务的undo log中的反向操作,实现恢复。确保消息的原子性和一致性。
undo log记载的是逻辑日志,是对已经实实在在发生了变化的数据进行一个恢复,实际的位置已经发生了改变
那为什么要采用逻辑日志呢? 对于回滚日志,如果数据是增加,而原数据都没有此数据,就无法记载之前的情况。此外,记录逻辑日志在MVCC的ReadView时可能更为方便。
undo log 日志作用
- undo log可以回滚数据,能够通过执行反向SQL,回到之前的数据状态
- undo log可以用来记录某条数据的历史版本,即MVCC,但这是针对与修改和删除操作的,对于记录的写操作,当事务提交后,就失去了作用,会被删除。
undo的存储
- undo log采用段的方式进行管理,也就是
回滚段
。每个回滚段有1024个回滚页, - 每一个事务只会采用一个回滚段,但一个回滚段可能同时服务多个事务
- 每个事务开启都会制定一个回滚段
当事务提交时,会根据undo记录是否可用于MVCC,如果不是会放入purge进行回收,并判断所在的页是否可供下一个事务使用
锁
锁是用来当有多个线程争抢有限资源时进行一种分配控制。在数据库时为了防止线程安全问题,让各个线程有序获取资源,就需要锁。
是一种标记,不同锁采用不同的标记,代码针对不同的标记对应不同的处理方式。
InnoDB引擎是行级锁,粒度更细,使得在操作数据时锁的范围更小,并发量更高。 但如果不能够通过索引定位到对应的行数据,就会形成表级锁。一个线程操作一行数据,整张表都要被锁入,大大的降低了系统的并发性。
按照功能可以将锁分为读锁
与写锁
。 数据库中所有的线程功能都可以分为读
和写
,多个线程之间一起读一个数据没有任何问题,但如果读一个正在写的数据或者说写一个正在读的数据就会发生 脏读
,最为严重的是写一个正在写的数据就会发生脏写,导致数据错乱。因此在读时加上用于读的锁,在上读锁时,要确保读取的数据没有写锁,该锁用来防止写,但可以读,每一个线程读完释放掉读的锁。在写时加上一个写锁,在上锁时要确保该数据已经没有任何读锁和写锁,该锁用来防止在写时防止其他线程写或读, 确保上写锁时,是由此线程进行操作,当操作完毕释放掉该锁。
即读时可以读,写时不可写,读时不可写,写时不可以读,
通过一个写锁与读锁的配合,就能解决脏写,脏读(在读时要加锁,在加锁前后不能有写锁,在加锁后,无法上写锁)。
既然知道锁的两大类,那么按照作用范围又可以分为 三大类,页级锁,表级锁,行级锁。在这个类按照具体的功能又可以继续细分。 在InnoDB引擎,显然行级锁才是最为常用的。
锁其实还可以分为单纯的读写锁(只专注于表中数据的并发安全性,忽略表结构等 如表读/写级锁,行级读/写锁)和不单纯的锁。
表级锁
表读锁、表写锁
用于在多线程读取数据和写入数据时,如果引擎采用的是锁住整张表,那么在读时就会开启整张表的读锁,使得整张表每行数据都只能读,但不能写。在插入数据时,整张表都不能有其他线程写,也不能有其他线程进行读。
上锁:
- 表
写锁
:lock tables 表名 write
:整张表在锁释放之前,任意线程只能读不能写(包括自己)。若上锁失败,可能此表存在写锁 - 表
读锁
:lock tables 表名 read
:整张表只有此线程能够写和读,其他线程既不能写,也不能读。若上锁失败,可能此表存在读锁,写锁。
事务的隔离性是由锁自动控制的,在自己手动上锁和释放锁时,与事务无关。
解锁有两种方式,输入命令的方式unlock tables;
或者直接断开上锁方的连接
元数据锁MDL
元数据锁是引擎自动加上的,确保在对表中数据读取和写入时,不能对表结构进行修改。避免DDL语句与DML和DQL冲突。
表级的读锁,写锁和行级的读锁、写锁开启都会自动为所在的表加上元数据锁,元数据锁是给DDL语句看的,有MDL锁就不能执行任何DDL操作,但DML锁对数据操作的读写锁是透明的。DML也是一个互斥锁(写锁),一个表中存在此锁就不能再加。
MDL上锁方一个是DML和DQL,另一个是DDL。是他俩通过MDL锁来形成互斥关系。
意向锁
意向锁更像是一种标记,用来处理表级锁与行级锁的冲突。当此表要上写锁时,要确定此表没有表级的写锁,读锁,还要确保表中的每一条行数据没有读写锁,这就比较困难了。因为要想知道每一行啥情况需要将每一行进行遍历检查,这就会影响上锁的效率。如果在每一行在上锁时,都自动为其所在的表上一个意向锁,标记此行中有锁,这样当表进行DDL操作或上写锁时,就直接判断是否有意向锁即可。
同样,当我们想为表上读锁时,就允许表中含有行级别的读锁,不允许有写锁,当为表上写锁时,就不允许任何一行含有读锁和写锁。
因此,意向锁也分为共享锁(读锁)和排他锁(写锁)。行数据中只有行级别读锁,意向锁就为共享锁,就与表级别的写锁互斥;行数据中只要含有写锁此表的意向锁就为排他锁,与表级别的读锁,写锁都互斥。
表意向锁是代表行级锁与对应的表级锁进行互斥,免去在上表锁时逐个检查行锁的上锁情况,提升上锁效率。
行级锁
行读锁,行写锁
与表锁的作用相同,只是范围由整张表缩小到了表中的某行数据,这样在操作数据时,为锁住的部分就能够由其它线程进行操作,整体并发量更高,但一定要同过索引来检索数据
间隙锁
临键锁
MVCC
多版本并发控制
(Multi-Version Concurrency Control 简称MVCC),维护着一个数据的多个版本。这种机制可以获取当前数据的每个事务前的具体数据,即历史版本。例如:表中新插入一条id=2,name=‘张三’的数据,然后,当将id=2的数据修改其name=‘李四’,原name=‘zs’的数据会被覆盖,但在undo log中在记录着当前数据怎样去恢复到name=‘张三’。此时,如果将name修改为‘李四’的事务并没有提交,在事务中,在读已提交或者更高的隔离级别中是不能够读取name为‘李四’这条数据的,即不能实现当前读
,为了减少线程间的读
等待,提高读
并发,可以读取此事务前的最新数据,即name为‘张三’的数据。这种方式为快照读
。快照读解决了当前操作线程写数据加锁带来的其他线程阻塞读,但写操作依然需要等待
。如果是在可重复读隔离级别下,读取的数据如果可能不是最新提交的。
实现原理
MVCC 的实现离不开 表中隐藏的两个字段
,undo log
,ReadView
隐藏字段
当我们创建表时,除了我们显示设计的字段外,InnoDB引擎还会为我们添加2个或3个隐藏字段,它们只在系统内部使用。那为什么个数还不一定,因为其中有一个就是常说的rowId
,如果我们指明主键,系统就不会创建此字段,此时就只有两个隐藏字段了。
下面详细介绍一下这三个隐藏字段,这些隐藏字段可以在表文件中查看到
DB_TRX_ID
:最新修改的事务Id,记录插入这条记录或最后一次修改该记录的事务ID。DB_ROLL_PTR
:回滚指针,配合undo log指向这条数据的上一个版本,在上一个版本中的此字段再次指向上上一个版本,形成版本链。DB_ROW_ID
:如果表没有主键,为了生成主键索引,系统会为我们自动创建字段,每条数据生成一个唯一且非空的值作为索引的key。如果指明主键了,则不会创建该字段。
根据隐藏字段,我们得知每条数据都会记录着当前最新的事务Id,和其版本链
因为undo log做数据恢复,里面记载了每条数据如果恢复到上一个已提交事务后的逻辑记录,同样上一个版本也同样记载了如何恢复到上上一个版本… 这样就在逻辑上仿佛记载了每条数据的版本链,每一个数据指向自己的上一个版本。由隐藏字段DB_ROLL_PTR
存放版本链第一条地址,这就将版本链与当前数据创建联系。由当前数据的DB_ROLL_PTR
字段能够顺着指针移动一直找到数据的各个事务版本。
只有其他线程对当前数据刚开启一个事务时,之前的数据才会形成一个历史版本,由新数据(尽管未提交)指向其在undo中的地址,新的数据中事务Id字段记录的就是当前开启的事务id。undo log并不关注事务的提交,只是关注事务的开启和回滚。
演示:
- 插入一条新数据
- 新增数据提交后,其他线程开启事务,修改此条数据,将age修改为3,但未提交时或者已提交时
- 此条数据又经过不断修改后,形成更多的历史版本信息
undo log中记录此条数据的历史版本,每个版本中都有各自版本数据的事务Id,回滚指针。历史版本中的数据只是逻辑上的,实际记载的是如果恢复到这样的数据
每个字段都有了最新操作此数据的事务Id(可能提交可能未提交)和其历史版本,但怎样判断其他事务要读取此数据的哪个版本的数据呢?
ReadView
readView即读视图
,是某个事务执行MVCC的依据,由它去判断应该读取哪一个版本的数据
ReadView中包含4个核心字段:备注:活跃事务:已开启但未提交的事务;事务Id是自增的
m_ids
:当前活跃的事务Id们集合min_trx_id
:最小活跃事务idmin_trx_id
:当前最大事务id+1,并不存在,只是下一个将要开启事务的事务idcreator_trx_id
:来读取MVCC的事务id,一切都是围绕这个id的事务要读取数据
有了表的事务id统计,有了历史版本链,有了当前事务id,那么剩下的就是读取的规则,即哪个事务能够读取哪个版本数据的规则
当前来读取的事务id会按照此规则逐个查找满足的选项,若都不满足,和一个版本同样去逐一比对
判断该数据是否可读用到的字段 m_ids
,min_trx_id
,max_trx_id
,creator_trx_id
,DB_TRX_ID
条件 | 是否可读 | 说明 |
---|---|---|
行中对应事务id==来读取的事务id | 是 | 此条数据就是id修改的,可以读 |
行中对应事务id<最小事务id | 是 | 操作此条数据的事务已经提交,可以读取 |
行中对应事务id>=最大事务id+1 | 否 | 每个线程读取时都会生成此时的ReadView,所以ReadView统计的数据在可能不及时,但行数据中的事务id是最新的。如果行事务id大于统计的事务最大值,也一定大于此线程事务。即在此线程读之前,又有其他线程后开启但已操作此数据,因此也不可读。这也存在一个问题,如果此事务已提交但依然不可读,但概率极小 |
最小事务id <=行中对应事务id<=最大事务id+1 | 不一定 | 如果id不在ids中 虽然该事务已经晚开启,但已提交,但如果在ids中,此表中的事务正开启,不能访问 |
每一行中的数据都要进行逐一进行比较,如果都不满足再去比较下一行
当前行数据事务id>ReadView记载的最大事务id
读已提交 RC 详细原理
读已提交,当一个线程操作时,会上写锁,其他线程只能执行MVCC,带来的问题就是可能不可重复读。因为每次读取都是生成一个ReadView,在两次读取时有时间间隔,这就会使得ReadView中的数据会发生变化。例 原正在操作的数据 满足 行事务id>max_trx_id,此线程不能读取这个版本的数据,但一次读取时,同样的id满足了 id<min_trx_id,这样这个版本的数据就变的可读了。这就使得两次读取的不一致,带来不可重复问题。
可重复读 RR 详细原理
不可重复读是因为每次读取生成的ReadView不同,如果每一次读取时,都使用第一次读取时的ReadView,那么每一次读取的规则都一样,读取的数据就相同。
虽然使得一个事务中每次读取都是相同的数据,但牺牲了读取最新已提交的数据,最新已提交的数据是真实存在的,而读取却还在过去,在写操作时不能使用MVCC,就又回到了现实,就可能发生幻读 。上面已经介绍,这里不加赘述。
不可重复读和幻读是因为使用MVCC机制产生的,而不可重复读是在隔离级别为读已提交阶段,幻读是在隔离级别为可重复读阶段,因此只有在读已提交阶段
(RC),读未提交阶段
(RR)才会使用到MVCC
MVCC给我的感觉像是一个平行空间,我们只能在平行空间里体验,却不能进行任何修改,如果修改那么当前空间就会发生变化。所以要修改只能回到当前空间。例如:我在历史版本中将id为2的数据给删除,在当前事务要修改id为2的数据时却发现他已经消失了。
类似于这种,一个方块代表一行数据,每一层代表其每一个版本再根据ReadView确定要读取哪个版本。