MySQl大多数事务性存储引擎实现的都不是简单的行级锁。基于高性能考虑,他们一般都同时是想了多版本并发控制器(MVCC)。不仅仅MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自实现机制不尽相同,因为MVCC没有一个统一的实现标准。MVCC可以说是行级锁的一个变种,但是他在多数情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC只存在于MySQL的Read Commit和Read Repeatable的隔离级别下。
事务日志
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序 I/O,而不像随机 I/O需要在次盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
redo log 是InnoDB存储引擎层的日志,主要用于记录事务的日志信息,开启一个事务时,会记录一个日志序列号,当事务执行时会向日志缓冲(redo buffer)插入事务日志,并且在事务提交前会把redo buffer中的日志信息记录到磁盘中。如数据库掉电,InnoDB存储引擎会使用redo log恢复到掉电前的时刻,以此来保证数据的完整性。
undo log 是InnoDB存储引擎层的日志,主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。
undo log用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
InnoDB
是一个多版本的存储引擎:它保存有关已更改行的旧版本的信息,以支持并发和回滚等事务功能 。此信息存储在表空间中称为回滚段的数据结构中(在Oracle中的类似数据结构之后)。InnoDB
使用回滚段中的信息来执行事务回滚中所需的撤消操作。它还使用该信息构建行的早期版本以进行一致读取。
在内部,InnoDB
为数据库中存储的每一行添加三个字段。6字节DB_TRX_ID
字段指示插入或更新行的最后一个事务的事务标识符。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已删除。每行还包含一个DB_ROLL_PTR
称为滚动指针的7字节 字段。roll指针指向写入回滚段的undo log记录。如果更新了行,则undo log记录包含在更新行之前重建行内容所需的信息。一个6字节的DB_ROW_ID
字段包含一个行ID,当插入新行时,该行ID会单调增加。如果 InnoDB
自动生成聚簇索引,索引包含行ID值。否则,该 DB_ROW_ID
列不会出现在任何索引中。
撤消段中的undo log分为插入和更新undo log。只在事务回滚中才需要插入undo log,并且可以在事务提交后立即丢弃。更新undo log也用于一致性读取,但只有在没有事务InnoDB
已分配快照的情况下才能丢弃它们 ,在一致读取中可能需要更新undo log中的信息来构建数据库的早期版本行。 在InnoDB
多版本控制方案中,使用SQL语句删除行时,不会立即从数据库中物理删除该行。InnoDB
只有在丢弃为删除写入的更新undo log记录时,才会物理删除相应的行及其索引记录。此删除操作称为purge,并且速度非常快,通常与执行删除的SQL语句的时间顺序相同。
DB_TRX_ID
:记录操作该数据事务的事务ID;DB_ROLL_PTR
:指向上一个版本数据在undo log 里的位置指针;DB_ROW_ID
: 隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引;
read view
read view 其实就是一个保存事务ID的list列表。记录的是本事务执行时,MySQL还有哪些事务在执行。
Read Repeatable 对应的是在每个事务启动的时候创建一个read view。
Read Commit 对应的是每次执行SQL statement时候创建一个read view。
Read View结构
struct read_view_t{ // 由于是逆序排列,所以low/up有所颠倒 // 能看到当前行版本的高水位标识,> low_limit_id皆不能看见 trx_id_t low_limit_id; // 能看到当前行版本的低水位标识,< up_limit_id皆能看见 trx_id_t up_limit_id; // 当前活跃事务(即未提交的事务)的数量 ulint n_trx_ids; // 以逆序排列的当前获取活跃事务id的数组 // 其up_limit_id<tx_id<low_limit_id trx_id_t* trx_ids;
// 创建当前视图的事务id trx_id_t creator_trx_id; // 事务系统中的一致性视图链表 UT_LIST_NODE_T(read_view_t) view_list; };版本可见性
read view其实保存的是当前活跃事务的所有事务id,如果当前行版本对应修改的事务id不在当前活跃事务里面的话,表示当前版本可见,否则就是不可见。也就是看不到read view创建以后启动的事务,看不到read view创建时活跃的事务。Read View不可见的话,就从undo log中读取。
只有在非锁select下才会创建read view。
当前读和快照读
- 当前读
当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的 (Update、 delete、 insert、 select ....lock in share mode、 select for update 为当前读)
- 快照读
快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本读取的一个快照信息(mysql读取undo log历史版本) ;
快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题:
- 解决了因加锁导致的修改数据时无法对数据读取问题;
- 解决了因加锁导致读取数据时无法对数据进行修改的问题;