还不懂mysql的undo log和mvcc?算我输!
undo log有两个作用:提供回滚和MVCC。
undo log是逻辑日志。
undo log存在于一个特殊的段中,存在于表空间中,和主键id组织的数据存在一个文件中,毕竟每行数据都有个指向undo log的指针。
当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时
它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
undo log的存储方式
innodb存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。
在以前老版本,只支持1个rollback segment,MySQL5.5可以支持128个rollback segment,即支持128*1024个undo操作,还可以通过变量 innodb_undo_logs自定义多少个rollback segment,默认值为128。
undo log默认存放在共享表空间中。
默认rollback segment全部写在一个文件中,但可以通过设置变量 innodb_undo_tablespaces 平均分配到多少个文件中。
delete/update操作的内部机制
当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。
但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。
并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。
通过undo log记录delete和update操作的结果发现:
delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
update分为两种情况:update的列是否是主键列。
- 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
- 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
insert undo log和update undo log为啥要分开,为啥提交之后insert undo log可以直接删除了,update undo log需要等待purge?
如果某个事务ID=100新增了一条记录,那么在这个事务版本之前这个记录是不存在的
- 这条数据要么是事务100提交的,然后就存在这条数据了
- 事务100没有提交,这条数据是nul
这条数据本身就是一个版本,要么是不存在,读取不到,要么就是存在,可以读取
数据是否存在,在RC和RR级别看事务有没有提交。
undo log 分成两种格式
一种给insert操作。记录中不含回滚指针,不含旧值。insert之前是没有的。
一种给update/delete操作。有回滚指针,有旧值。
对于INSERT_UNDO,调用函数trx_undo_page_report_insert进行插入,记录格式大致如下图所示:
对于UPDATE_UNDO,调用函数trx_undo_page_report_modify
进行插入,UPDATE UNDO的记录格式大概如下图所示:
什么是MVCC
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,
做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
当前读和快照读
当前读
像select lock in share mode(共享锁
), select for update ; update, insert ,delete(排他锁
)这些操作都是一种当前读
它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
提高并发性能,快照读是基于多版本并发控制,即MVCC,在很多情况下,避免了加锁操作,降低了开销;
既然是基于多版本,即快照读读到的不一定是数据的最新版本,而有可能是之前的历史版本
MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读
, 而非当前读
当前读实际上是一种加锁的操作,是悲观锁的实现
当前读,快照读和MVCC的关系
(1)MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念,
而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它
(2)快照读就是MySQL实现MVCC的其中一个具体非阻塞读功能。
(3)当前读就是悲观锁的具体功能实现
要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段
,undo日志
,Read View
等去完成的,具体可以看下面的MVCC实现原理
MVCC能解决什么问题
数据库并发场景有三种,分别为:
读-读
:不存在任何问题,也不需要并发控制读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC带来的好处
用来解决读-写冲突
的无锁并发控制,为每个修改保存一个版本,读操作只读事务开始前的的快照。
(1)在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
(2)还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出了MVCC,所以我们可以形成两个组合:
MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
MVCC的实现原理
MVCC的实现依赖记录中的 3个隐式字段
,undo日志
,Read View
。
取出DB_TRX_ID
(即当前事务ID),与系统当前其他活跃事务的ID去对比(由Read View维护),
不符合可见性,那就通过DB_ROLL_PTR
回滚指针去取出Undo Log
中的DB_TRX_ID
再比较,直到找到满足特定条件的DB_TRX_ID
,
那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新版本
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,
DB_ROLL_PTR,
DB_ROW_ID
等字段
DB_TRX_ID
6byte,记录创建这条记录 或者 最后一次修改该记录的事务ID
DB_ROLL_PTR
7byte,回滚指针,用于配合undo日志,指向这条记录的上一个版本(存储于rollback segment里)用于配合undo日志
DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID
产生一个聚簇索引
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
undo日志
undo log主要分为两种:
insert undo log
代表事务在insert
新记录时产生的undo log
, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log
事务在进行update
或delete
时产生的undo log
; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge
线程统一清除
purge
从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
Read View(读视图)
事务进行快照读的读视图(Read View),用来做可见性判断。
(1)当执行快照读的时候,对该记录创建一个Read View
读视图,记录当前活跃事务的ID。
(2)用来判断当前事务能够看到哪个版本的数据,可能是当前最新的数据,也可能是该行记录的undo log
里面的数据。
实现
当每个事务开启时,都会被分配一个ID, 这个ID是递增的,最新的事务,ID值越大
trx_list:列表,记录Read View生成时刻系统正活跃的事务ID
up_limit_id:记录trx_list列表中事务ID最小的ID
low_limit_id:系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
(1)首先判断 DB_TRX_ID < up_limit_id,
如果小于,则当前事务能看到DB_TRX_ID
所在的记录,如果大于等于进入下一个判断
(2)然后判断 DB_TRX_ID >= low_limit_id
,如果大于等于,则DB_TRX_ID
所在的记录在Read View
生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
(3)判断DB_TRX_ID
是否在活跃事务之中,trx_list.contains(DB_TRX_ID)
- 如果在,则代表
Read View
生成时刻,这个事务还在活跃,还没有Commit,修改的数据对当前事务不可见;
- 如果不在,这个事务在
Read View
生成之前就已经Commit了,修改的结果对当前事务可见
select count(*) 总行数
在 MyISAM 存储引擎中,把表的总行数存储在磁盘上,直接返回总数据。
在 InnoDB 存储引擎中,没有将总行数存储在磁盘上,会先把数据读出来,一行一行的累加,最后返回总数量。
在默认隔离级别可重复读的情况下,通过多版本并发控制(MVCC)来实现,每一行记录都需要判断自己是否对这个会话可见,因此在统
计总数量时,InnoDB 只好把数据一行一行的读取出来判断,只有当前会话可见的才纳入统计中。
所以同一时刻不同会话查询到的数量就不一样。