undo
事务id:只有在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id
。
- INSERT:插入类型的undo日志主要记录主键信息,对应的删除该主键记录即可
- DELETE:如果是删除记录会分为两阶段(根据事务的进度)
- 事务未提交,语句已执行,会把记录的delete_mask改为1,中间状态,此时还在正常记录的链表中
- 事务提交后,把delete_mask标记为1的记录从正常记录链表中移除,加入到已删除链表的头部
- 而事务提交后,不需要用到undo日志,所以其实只要保存第一阶段的undo日志即可。
- UPDATE:
- 不更新主键
- 就地更新:如果被更新的每个列,前后占用的存储空间一样大,可以直接在原纪录的基础上修改值
- 如果空间不一样:先删旧记录,再插新记录:真正的删除记录,移到垃圾链表。如果新的记录占用的空间大小不超过旧的空间,则可以直接复用原来的空间,否则要在页面中新申请一段空间。如果页面没有新空间,需要进行页分裂。
- 更新主键
- 旧记录进行delete mark,因为如果这个事务没有提交,记录却删除了,则其他事务就无法根据主键找到该记录了。等到事务提交后再一到垃圾链表中,进行delete mark操作前会有一条undo日志
- 重新插入新纪录。并产生一条undo日志
- 不更新主键
事务隔离级别
- 脏写:一个事务修改了另一个未提交事务修改过的数据(这里的未提交指的是该事务进行修改的时候,读到的数据还是未提交时候的数据而不是已提交完后的数据)
- 脏读:一个事务读到了另一个未提交事务修改过的数据,如果修改后回滚,读到了一个不存在的数据
- 不可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,简单来说就是一个事务内同一个查询可能返回不同的结果
- 幻读:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了
幻读
。幻读与不可重复读的区别重点是幻读强调了读取到之前没有读取到的记录。
严重程度:脏写 > 脏读 > 不可重复读 > 幻读
隔离级别:
隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
READ UNCOMMITTED |
读未提交 | Possible | Possible | Possible |
READ COMMITTED |
读已提交 | Not Possible | Possible | Possible |
REPEATABLE READ |
可重复读 | Not Possible | Not Possible | Possible |
SERIALIZABLE |
可串行化 | Not Possible | Not Possible | Not Possible |
也就是说:
-
READ UNCOMMITTED
隔离级别下,可能发生脏读
、不可重复读
和幻读
问题。 -
READ COMMITTED
隔离级别下,可能发生不可重复读
和幻读
问题,但是不可以发生脏读
问题。 -
REPEATABLE READ
隔离级别下,可能发生幻读
问题,但是不可以发生脏读
和不可重复读
的问题。 -
SERIALIZABLE
隔离级别下,各种问题都不可以发生。 - 每种隔离级别都不允许脏写
MySQL默认是RR,但是互联网项目推荐RC。为什么默认是RR,因为在mysql5.0前,主从复制的binlog的格式为statement。顺序是先插后删,会造成先删后插的事务在master和slave中不一致,也就是主从不一致。所以需要靠RR来保持一致。
在RR级别下,会存在间隙锁,出现死锁的概率高,且条件列未命中索引会锁表。而RC只锁行。
在RC级别下的主从复制binlog要用row格式
MVCC(Multi-Version Concurrency Control)
版本链
- 每次对记录进行改动都会记录一条undo日志,每条undo日志会有一个roll_pointer属性(insert的没有,因为没有更早版本),可以将这些undo日志都连起来,串成一个链表,如:
- 记录的每次更新都会把旧值放到一条undo日志中,随着更新次数增多,会形成一个链表,最新的会在链头。且会记录事务id
ReadView
核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。需要ReadView
m_ids
:表示在生成ReadView
时当前系统中活跃的读写事务的事务id
列表。min_trx_id
:表示在生成ReadView
时当前系统中活跃的读写事务中最小的事务id
,也就是m_ids
中的最小值。-
max_trx_id
:表示生成ReadView
时系统中应该分配给下一个事务的id
值。小贴士: 注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
-
creator_trx_id
:表示生成该ReadView
的事务的事务id
。小贴士: 我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
有了ReadView,就可以根据以下步骤判断记录的某个版本是否可见:
- 如果被访问版本的
trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值小于ReadView
中的min_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
前已经提交,所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值大于或等于ReadView
中的max_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
后才开启(对RC来说且肯定还未提交,否则此时生成的ReadView的max_trx_id应该会大于当前访问版本,对RR来说,在他之后开启的事务是不允许被读到的),所以该版本不可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见,就顺着版本链找到下一个版本的数据。
READ COMMITTED —— 每次读取数据前都生成一个ReadView
所以每一次查询之间如果有新的事务提交,那根据上面的查找流程,每次都会找到最新一次提交的事务的记录
REPEATABLE READ —— 在第一次读取数据时生成一个ReadView
所以只有第一次查询的时候生成ReadView,之后每次查询不管是否有新的事务提交,都会查到相同的结果(第一次查询时候的结果)。
具体流程参考:https://juejin.im/book/6844733769996304392/section/6844733770071801870
MVCC小结
从上边的描述中我们可以看出来,所谓的MVCC
(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SELECT
操作时访问记录的版本链的过程,这样子可以使不同事务的读-写
、写-读
操作并发执行,从而提升系统性能。READ COMMITTD
、REPEATABLE READ
这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
小贴士: 我们之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的。不然如果更新主键的记录直接删了,就没办法出现在版本链中,就没办法通过MVCC来找到对应的记录。