151 lines
12 KiB
Markdown
151 lines
12 KiB
Markdown
- [mysql mvcc](#mysql-mvcc)
|
||
- [Internal Impl](#internal-impl)
|
||
- [undo log](#undo-log)
|
||
- [insert undo log](#insert-undo-log)
|
||
- [update undo log](#update-undo-log)
|
||
- [commit regularly](#commit-regularly)
|
||
- [purge](#purge)
|
||
- [insert and delete rows in smallish batches](#insert-and-delete-rows-in-smallish-batches)
|
||
- [purge lag](#purge-lag)
|
||
- [innodb\_purge\_threads](#innodb_purge_threads)
|
||
- [innodb\_max\_purge\_lag](#innodb_max_purge_lag)
|
||
- [Multi-Versioning and Secondary Indexes](#multi-versioning-and-secondary-indexes)
|
||
- [mvcc mechanism](#mvcc-mechanism)
|
||
- [read view](#read-view)
|
||
- [read view for read committed isolation level](#read-view-for-read-committed-isolation-level)
|
||
- [read view for repeatable read isolation level](#read-view-for-repeatable-read-isolation-level)
|
||
- [read view impl](#read-view-impl)
|
||
- [innodb clustered index hidden columns](#innodb-clustered-index-hidden-columns)
|
||
- [implementation principle](#implementation-principle)
|
||
|
||
|
||
# mysql mvcc
|
||
innodb是一个`多版本`的存储引擎,对于被修改的records,其保存了records旧版本的信息,从而支持事务的`并发`和`回滚`等特性。
|
||
|
||
innodb使用了rollback segment中存储的信息来进行`undo操作`,从而`支持事务在需要时进行回滚`;`并且支持通过undo信息构建record的早期版本,从而实现一致性读`。
|
||
|
||
## Internal Impl
|
||
在innodb的内部实现中,其为数据库中存储的每行数据都额外添加了3个字段:
|
||
- `DB_TRX_ID`:长度为6字节,用于标识`最后对改行数据进行insert/update操作的事务`
|
||
- 其中,delete操作也被看做是`update`,因为innodb在对数据执行删除操作时,并不会立马对数据进行物理删除,而是会先标记该行数据的delete mark,数据实际被物理删除发生在purge阶段
|
||
- `DB_ROLL_PTR`:长度为7字节,被称为`roll pointer`。该指针指向undo record,通过undo record中的内容可以构建该行数据被更新前的版本
|
||
- `DB_ROW_ID`:长度为6字节,该字段包含一个row id,该row id在数据被插入时会单调的增长。
|
||
- 如果表使用的是innodb自动生成的聚簇索引(未显式指定主键),那么自动生成的聚簇索引将会包含该row id的值
|
||
- 如果表显式指定了聚簇索引,那么`DB_ROW_ID`并不会出现在任何索引中
|
||
|
||
## undo log
|
||
在rollback segment中,undo log可以分为两种类型:
|
||
- insert undo log
|
||
- update undo log
|
||
|
||
### insert undo log
|
||
insert undo log只会在事务回滚时被用到,在事务提交之后,insert undo log的内容可以被立马删除。
|
||
|
||
### update undo log
|
||
update undo log除了用于事务回滚外,还用于一致性读(MVCC)。对于update undo log,其只在满足如下条件时可以被删除
|
||
- 当`需要通过该update undo log来构建数据先前版本`的事务都不存在时,update undo log才可以被删除
|
||
|
||
> innodb会为每个事务分配一个快照,事务在进行一致性读时,实际读取的是快照中的旧版本数据。
|
||
>
|
||
> 快照需要通过update undo log来还原行数据的旧版本。如果没有事务再需要通过该update undo log来还原旧数据,代表该update undo log可以被删除。
|
||
|
||
### commit regularly
|
||
在编写和数据库交互的代码时,推荐周期性的提交事务,`包括只会进行一致性读的读事务`。如果事务A的运行时间较长,且一直未提交,那么在事务A运行时,其他事务对数据库的更新操作,其生成的update undo log在事务A运行期间都无法被丢弃(因为有可能A会读取被修改数据之前的版本,此时需要通过undo update log来进行旧版本数据的还原)。
|
||
|
||
故而,如果存在一直不提交的事务,那么可能会造成update undo log无法丢弃,那么rollback segment占用的空间大小会不断增加,填满其所位于的undo tablespace。
|
||
|
||
### purge
|
||
在innodb的mvcc方案中,在通过delete sql删除行数据后,行数据并不会立马就从该数据库中被移除(不会立马被物理删除)。
|
||
|
||
innodb直到`删除update undo log record时`才`删除undo log record关联的行数据/index records`。该删除操作被称为`purge`。
|
||
|
||
purge的操作很快,并且purge的顺序通常和执行delete sql的时间顺序相同。
|
||
|
||
### insert and delete rows in smallish batches
|
||
如果针对表同时执行insert和delete的小批量操作,且insert和delete的速率相同,那么purge现成的回收速率可能会小于数据的插入速率。这样会导致`dead rows`(标记为逻辑删除但未实际purge的数据行)堆积,表占用空间越来越大,并造成disk相关操作变慢。
|
||
|
||
在上述场景下,应当限制新操作,并且通过调整innodb_max_purge_lag向purge thread分配更多资源。
|
||
|
||
#### purge lag
|
||
##### innodb_purge_threads
|
||
purge操作在后台由一个或多个purge threads执行。purge threads的数量由`innodb_purge_threads`来进行控制,默认值的取值逻辑如下:
|
||
- 如果可获取的逻辑核数小于等于16,则默认值为1
|
||
- 如果logic processors大于16,那么默认值为4
|
||
|
||
当dml操作集中在一张表上,那么该表的purge操作由一个现成来执行,这样可能会造成purge操作变慢,增加purge lag。
|
||
|
||
##### innodb_max_purge_lag
|
||
如果purge lag超过`innodb_max_purge_lag`时,purge工作会自动在多个purge threads之间进行重新分配。
|
||
|
||
当purge threads设置过大时,可能会造成与user threads的争用,故而相当适当的管理purge threads大小。
|
||
|
||
`innodb_max_purge_lag`的默认值为0,代表默认不存在max purge lag。
|
||
|
||
|
||
## Multi-Versioning and Secondary Indexes
|
||
在mvcc中,对待聚簇索引和辅助索引的方式不同。在聚簇索引中,record中的列是`update in-place`,并且其hidden system columns指向undo log records,通过undo log records可以还原数据的先前版本。
|
||
|
||
但是,辅助索引不包含hidden system columns,并且对于辅助索引的更新也不是update in-place。
|
||
|
||
当辅助索引的column被更新时,旧的辅助索引记录将会被标记为delete marked,并且插入新的辅助索引记录。`delete marked index records`最终会被purge。
|
||
|
||
当辅助索引中的index record被`delete marked`或`辅助索引页被newer transaction(更新的事务)更新时,innodb将会在聚簇索引中查询记录`。在聚簇索引中,该record的DB_TRX_ID将会被检查,查看并且会通过undo log构建出当前线程可见的数据版本。
|
||
|
||
> 行数据的版本记录存储在聚簇索引中。如果innodb在查找记录索引时发现该辅助index record被delete marked,此时并不确定delete marked的操作是否对查询事务可见,需要在聚簇索引中查找记录并构建历史版本。
|
||
>
|
||
> 当辅助索引页被newer transaction更新时,也无法确定该辅助索引页中的内容是否对当前事务可见,同样需要查询聚簇索引来构建先前的历史版本。
|
||
|
||
> 如果当前事务初始化后,record才被其他事务修改,那么根据事务的一致性读原则,record的修改不应对当前事务可见,需要通过record对应的undo log内容还原到数据的旧版本。
|
||
|
||
> 如果`当前辅助索引中的数据被delete marked`或`当前辅助索引页被newer transaction所更新`,那么`covering index`技术将不会被使用。故而,并不会直接从辅助索引中返回值,innodb而是会再次从clustered index中查询记录。
|
||
|
||
## mvcc mechanism
|
||
mvcc机制主要用于处理多事务之间对数据的并发访问,并且通过`版本快照`来实现事务之间的数据隔离。
|
||
|
||
事务A可见的数据版本为`事务A开启时的数据版本`,即使在事务A执行的过程中,事务B对数据进行了修改,事务A后续读取时仍然读取的是修改之前的版本。
|
||
|
||
综上所属,根据mvcc机制可以实现可重复读的隔离级别。
|
||
|
||
### read view
|
||
在mvcc中,存在`read view`这一概念,其原理类似于如下描述:
|
||
- 在事务开启时,为所有数据创建一个快照,后续该事务在执行过程中都会从快照中读取数据,从而可以消除其他事务修改数据所造成的影响
|
||
|
||
#### read view for read committed isolation level
|
||
对于`read committed`隔离级别的事务,其等价于`generate a read view before each select statement is executed`
|
||
|
||
#### read view for repeatable read isolation level
|
||
对于`repeatable read`隔离级别的事务,其等价于`generate a read view before transaction executes the first select statement`,并且在后续整个事务的执行过程中都使用该read view。
|
||
|
||
#### read view impl
|
||
read view实现并非是简单的为所有数据创建一个备份,其原理如下。
|
||
|
||
read view在被创建时,会记录如下信息:
|
||
- `m_ids`:当read view被创建时,read view会记录当前数据库中所有活跃事务id的集合
|
||
- `min_trx_id`: 该field代表当read view被创建时,数据库中所有活跃事务id中的最小值,相当于`m_ids`集合中的最小值
|
||
- `max_trx_id`: 该值代表当`read view被创建时`,`数据库将要授予下一个新创建事务的事务id`,即`当前全局最大的事务id + 1`
|
||
- `creator_trx_id`:代表创建该read view的事务id
|
||
|
||
#### innodb clustered index hidden columns
|
||
除了read view外,innodb mvcc中还包含另外的部分:`hidden_columns`。
|
||
|
||
innodb的clustered index中会包含如下hidden columns:
|
||
- `trx_id`: 代表最后对该clustered index record进行修改的事务id
|
||
- `roll_pointer`: 每当clustered index record被修改时,指向`记录数据旧版本undo log`的指针会被写入到该column中。故而,该`roll_pointer`包含了一个单向链表,`包含clustered index record所有的旧版本`
|
||
|
||
#### implementation principle
|
||
通过上述记录的read view信息和clustered index hidden columns信息,可以决定数据的历史版本对事务是否可见:(`每个历史版本中都包含trx_id信息,用于代表该版本的最后修改事务id`)
|
||
- `trx_id = creator_trx_id`: 如果`数据最后被修改的事务id`和`创建read view的事务id`相同,那么该行数据版本对当前事务可见
|
||
- `trx_id < min_trx_id`:如果`行数据的最后被修改事务id`小于`read view中最小的活跃事务id`,代表read view创建时,trx_id对应的事务已经被提交,此时该行数据版本对当前事务可见
|
||
- `trx_id >= max_trx_id`:如果`行数据最后被修改的事务id`大于或等于`read view被创建时的全局最大事务id + 1`,read view创建时`该数据版本对应的修改事务id还不存在`,即read view创建时该修改还未发生,故而该行数据版本对当前事务不可见
|
||
- `min_trx_id <= trx_id < max_trx_id`:
|
||
- `trx_id位于m_ids中`:如果trx位于m_ids中,代表read view在创建时,该数据版本对应的事务还未提交,即该数据版本对事务不可见
|
||
- `trx_id未位于m_ids中`: 代表在read view创建时,该数据版本对应的事务已经提交,故而该行数据版本可见
|
||
|
||
> read view通过记录创建时的`活跃事务id列表`和`全局最大事务id + 1`,可以明确的区分`数据版本的最后修改事务id造成的修改`是否对readview可见,其判断逻辑如下:
|
||
> - 对于id大于等于`全局最大事务id + 1`的事务,在read view创建时都没有被创建,故而未来事务造成的修改对read view不可见
|
||
> - 对于id小于`最小活跃事务id`的事务,代表在read view创建时已提交,read view创建之前提交的事务对read view是可见的
|
||
> - 对于id位于`[min_trx_id, max_trx_id)`之间的事务,其是否可见取决于其是否位于`活跃事务id列表`中来进行判断
|
||
> - 如果位于`活跃事务id列表`中,代表read view创建时该历史版本对应修改还未提交,故而对read view不可见
|
||
> - 如果不位于`活跃事务id列表`中,代表read view创建时该历史版本对应修改已经提交,已提交事务的修改对read view可见
|
||
|