事务ACID特性
原子性
一个事务中的操作要么全部成功,要么全部失败。
隔离性
一个事务的修改在最终提交前,对其他事务是不可见的。
持久性
一旦事务提交,所做的修改就会永久保存到数据库中。
一致性
事务的具体实现机制
redo log
undo log
redo日志
使用redo日志的原因
如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
刷新一个完整的数据页太浪费了
随机IO刷起来比较慢
redo日志如何保证持久性
好处:
redo日志格式
space ID:表空间ID。
page number:页号。
data:该条redo日志的具体内容
简单的redo日志类型
Max Row ID属性实际占用8个字节的存储空间,所以在修改页面中的该属性时,会记录一条类型为MLOG_8BYTE的redo日志
复杂一些的redo日志类型
redo日志的写入过程
redo log block和日志缓冲区
redo日志刷盘时机
- InnoDB认为如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
- 在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘。
- 后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。
- 正常关闭服务器时等等
redo日志文件组
- innodb_log_group_home_dir 指定了redo日志文件所在的目录,默认值就是当前的数据目录。
- innodb_log_file_size 指定了每个redo日志文件的大小,默认值为48MB
- innodb_log_files_in_group 指定redo日志文件的个数,默认值为2,最大值为100
- 前2048个字节,也就是前4个block是用来存储一些管理信息的。
- 从第2048字节往后是用来存储log buffer中的block镜像的。
Log Sequence Number
buf_next_to_write
flushed_to_disk_lsn
- Log sequence number:代表系统中的lsn值,也就是当前系统已经写入的redo日志量,包括写入log buffer中的日志。
- Log flushed up to:代表flushed_to_disk_lsn的值,也就是当前系统已经写入磁盘的redo日志量。
- Pages flushed up to:代表flush链表中被最早修改的那个页面对应的oldest_modification属性值。
- Last checkpoint at:当前系统的checkpoint_lsn值。
innodb_flush_log_at_trx_commit
undo日志
事务id
只读事务
通过START TRANSACTION READ ONLY语句开启一个只读事务,不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对用户临时表做增、删、改操作。
读写事务
事务id分配方式
如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配方式如下:
- 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。
- 对某个查询语句执行EXPLAIN分析它的查询计划时,有时候在Extra列会看到Using temporary的提示,这个表明在执行该查询语句时会用到内部临时表。这个所谓的内部临时表和我们手动用CREATE TEMPORARY TABLE创建的用户临时表并不一样,在事务回滚时并不需要把执行SELECT语句过程中用到的内部临时表也回滚,在执行SELECT语句用到内部临时表时并不会为它分配事务id。
- 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。
- 有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id。
事务id生成机制
本质上就是一个数字
- 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。
- 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处,这个属性占用8个字节的存储空间。
- 当系统下一次重新启动时,会将上边提到的Max Trx ID属性加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Trx ID属性值)。
trx_id隐藏列
undo日志的格式
FIL_PAGE_UNDO_LOG
INSERT操作对应的undo日志
DELETE操作对应的undo日志
使用DELETE语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段:阶段一: 将记录的delete_mask标识位设置为1,这个阶段称之为 delete mark。正常记录链表中的 最后一条记录的delete_mask值被设置为1,但是并没有被加入到垃圾链表。也就是此时 记录处于一个中间状态。在 删除语句所在的事务提交之前, 被删除的记录一直都处于这种所谓的中间状态。主要是为了实现MVCC的功能。 版本链阶段二:当该 删除语句所在的事务提交之后,会有 专门的线程后来真正的把记录删除掉。所谓真正的删除就是把 该记录从正常记录链表中移除,并且 加入到垃圾链表的头节点中,然后还要 调整一些页面的其他信息,比如页面中的 用户记录数量PAGE_N_RECS、 上次插入记录的位置PAGE_LAST_INSERT、 垃圾链表头节点的指针PAGE_FREE、 页面中可重用的字节数量PAGE_GARBAGE、还有 页目录的一些信息等等。这个阶段称之为 purge。阶段二执行完了,这条记录就算是 真正的被删除掉了。这条已删除记录 占用的存储空间也可以被重新利用了。
版本链
同时,在对一条记录进行delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来,就是我们图中显示的old trx_id和old roll_pointer属性。这样有一个好处,那就是可以通过undo日志的old roll_pointer找到记录在修改之前对应的undo日志。比方说在一个事务中,我们先插入了一条记录,然后又执行对该记录的删除操作,这个过程的示意图就是这样:
UPDATE操作对应的undo日志
这里所说的删除并不是delete mark操作,而是 真正的删除掉,也就是把这条记录 从正常记录链表中移除并加入到垃圾链表中,并且 修改页面中相应的统计信息(比如PAGE_FREE、PAGE_GARBAGE等这些信息)。由用户线程同步执行真正的删除操作,真正删除之后紧接着就要 根据各个列更新后的值创建的新记录插入。这里如果新创建的记录占用的存储空间大小 不超过旧记录占用的空间,那么可以 直接重用被加入到垃圾链表中的 旧记录所占用的存储空间,否则的话需要在页面中 新申请一段空间以供新记录使用,如果本页面内已经 没有可用的空间的话,那就需要 进行页面分裂操作,然后 再插入新记录。
在UPDATE语句所在的 事务提交前,对旧记录 只做一个delete mark操作,在事务提交后才 由专门的线程做purge操作,把它加入到垃圾链表中。这里 一定要和我们上边所说的在不更新记录主键值时,先真正删除旧记录,再插入新记录的方式区分开!之所以只对旧记录做delete mark操作,是因为 别的事务同时也可能访问这条记录, 如果把它真正的删除加入到垃圾链表后, 别的事务就访问不到了。这个功能就是所谓的MVCC。
创建一条新记录
根据更新后各列的值 创建一条新记录,并将其 插入到聚簇索引中(需 重新定位插入的位置)。由于更新后的记录 主键值发生了改变,所以需要 重新从聚簇索引中定位这条记录所在的位置,然后把它插进去。
事务执行
MySQL的事务主要主要是通过 Redo Log和 Undo Log实现的。
MySQL在事务执行的过程中,会记录相应SQL语句的UndoLog 和 Redo Log,然后在内存中更新数据并形成数据脏页。接下来RedoLog会根据一定规则触发刷盘操作,Undo Log 和数据脏页则通过刷盘机制刷盘。事务提交时,会将当前事务相关的所有Redo Log刷盘,只有当前事务相关的所有Redo Log 刷盘成功,事务才算提交成功。
事务恢复
如果事务在执行第8步,即事务提交之前,MySQL 崩溃或者宕机,此时会先使用Redo Log恢复数据,然后使用Undo Log回滚数据
如果在执行第8步之后MySQL崩溃或者宕机,此时会使用Redo Log恢复数据,大体流程如下图所示。
恢复机制
在服务器不挂的情况下,redo日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一数据库挂了,就可以在重启时根据redo日志中的记录就可以将页面恢复到系统崩溃前的状态。
崩溃后的恢复为什么不用binlog?
- 这两者使用方式不一样:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用,用于保证在数据库崩溃时的事务持久性。
- redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这的c字段加1 ” ;
- redo log是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。
- 当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经入表(写入磁盘),哪些数据还没有。
Redo日志和Undo日志的关系
- 数据库崩溃重启后,需要先从redo log中把未落盘的脏页数据恢复回来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要把未提交的事务进行回滚操作。由于回滚操作需要undo log日志支持,undo log日志的完整性和可靠性需要redo log日志来保证,所以数据库崩溃需要先做redo log数据恢复,然后做undo log回滚。
- 在事务执行过程中,除了记录redo一些记录,还会记录undo log日志。Undo log记录了数据每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。
- 因为redo log是物理日志,记录的是数据库页的物理修改操作。所以undo log(可以看成数据库的数据)的写入也会伴随着redo log的产生,这是因为undo log也需要持久化的保护。
- 事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页。事务执行COMMIT操作时,会将本事务相关的所有redo log进行落盘,只有所有的redo log落盘成功,才算COMMIT成功。然后内存中的undo log和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用redo log恢复数据。
同时写Redo和Binlog怎么保持一致?
2) MySQL上层会将数据库、数据表和数据表中的数据的更新操作写入BinLog文件。
3) InnoDB存储引擎将事务日志写入Redo Log文件中。
MySQL8中的新增特性
索引
隐藏索引
降序索引
以往的MySQL虽然支持降序索引,但是写盘的时候依然是升序保存。MySQL8.0中则是真正的按降序保存。
不再对group by操作进行隐式排序。
索引中可以使用函数表达式
创建表时创建一个函数索引,查询的时候使用同样的函数就可以利用索引了。
默认字符集
InnoDB性能提升
行缓存
MySQL8.0的优化器可以估算将要读取的行数,因此可以提供给存储引擎一个合适大小的row buffer来存储需要的数据。大批量的连续数据扫描的性能将受益于更大的record buffer
改进扫描性能
改进成本模型
InnoDB缓冲区可以估算缓存区中的有多少表和索引,这可以让优化器选择访问方式时知道数据是否可以存储在内存中还是必须存储到磁盘上。
移除了一些功能,例如query cache