MySQL高级(MVCC实现原理,日志系统)篇

x33g5p2x  于2021-12-16 转载在 Mysql  
字(5.0k)|赞(0)|评价(0)|浏览(537)

MySQL日志系统

前面我们系统了解了一个查询语句的执行流程,并介绍了执行过程中涉及的处理模块。相信你还记得,一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎,但是我们的update语句,涉及到数据的持久化,我们又是怎么保证,更新操作的进行的呢?

与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。如果接触 MySQL,那这两个词肯定是绕不过的,我后面的内容里也会不断地和你强调。不过话说回来,redo log 和 binlog 在设计上有很多有意思的地方,这些设计思路也可以用到你自己的程序里。

1、redo log

在 MySQL 里存在这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,就采用了 MySQL 里经常说到的 WAL 技术(Write-Ahead Logging)也就是预处理日志!

具体来说,

  • 当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了,
  • 同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,

但是InnoDB的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为==crash-safe==。

CrashSafe指MySQL服务器宕机重启后,能够保证:

  • 所有已经提交的事务的数据仍然存在。
  • 所有没有提交的事务的数据自动回滚。

例如:如果Mysql 进程异常重启了,系统会自动去检查redo log,将未写入到Mysql的数据从redo log恢复到Mysql中去。

2、binlog

前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

这两种日志有以下三点不同:

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
  4. MySQL崩溃的话,redo log会保证数据不会丢失,而binlog只是记录了执行的sql语句 ;

有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程

mysql> update T set c=c+1 where ID=2;
  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

3、两阶段提交

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们

看看这两种方式会有什么问题?仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过

程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

  • 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
  • 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

4、日志的开启

  • redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
  • sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

六、MySQL事务

  • 读未提交(READ UNCOMMITTED):一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交(READ COMMITTED):一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(REPEATABLE READ):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化(SERIALIZABLE):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

以后我们会把读提交简称为RC,可重复读简称为RR

一般我们面临的并发场景就是如下三种

  • 读读并发 :不会存在任何线程安全问题,也不需要并发控制
  • 读写并发 :会有线程安全问题,因此需要加,但是加锁效率太低,故引入MVCC(锁)进行并发控制
  • 写写并发 :会有线程安全问题,更新丢失问题

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制,解决并发读写问题

1、MVCC实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的

1、快照读和当前读

当前读 :读取的是MySQL对应数据的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

select lock in share mode(共享锁); select for update ; update ; insert ;delete(排他锁) /*这些操作都是一种当前读

快照读 :读取的是MySQL对应数据的历史版本

select   /*仅这一个操作时快照读

MVCC多版本控制指的是维持一个数据的多个版本,使得读写操作没有冲突,快照读是MySQL为了实现MVCC的一个非阻塞读功能

实现原理

2、隐藏字段

隐藏字段是不可见的,除非翻阅源码 ;

这里的undo log就是上述日志中InnoDB自带的另一种日志,保存的是历史版本的数据,用以回滚

我们发现,unlog当中旧的版本数据形成了一个链表,链表首部存储的是最新的旧纪录,链表尾部存放的是最旧的旧纪录

undolog不会无限膨胀下去,会存在一个后台线程,purge线程,当发现当前记录不需要回滚且不需要参与MVCC的时候,就会把数据清

理掉

3、readview

事务在进行快照读的时候,会生成一个读视图来进行可见性判断,可见性判断是由可见性算法来确定(详聊可见性算法)

我们可以看到,事务2修改并提交的数据事务1中是可以看到的!

当我们再次进行如图测试,却的查不到我们的事务2修改并提交的数据,这是为什么?

我们看到第二次操作只是比第一次操作多执行了1次select操作,这是为什么呢?

让我们再次重温这句话:当事务在进行快照读的时候,会生成一个读视图来进行可见性判断,可见性判断是由可见性算法来确定 !

而图中我们的select就恰恰就是快照读的唯一操作,所以会生成一致性读视图即consistent read view,进行可见性判,来判断事务1能否

看到事务2修改,且已经提交的内容!

1、首先我们可见性视图有如下几个字段,所以生成可见性视图会对如下字段进行填充

2、根据第一次操作表进行填充

  • DB_TRX_ID:是我们上述表中的隐藏字段,表示的是最近修改的事务id
  • 右侧就是我们的可见性

有了上述的内容,我们通过可见性算法对致性视图进行判断,得出结论:这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。

因此,我们的操作1(图1)的操作得以解释!

3、根据第二次的操作,对表进行填充

我们的操作2进行了2次select操作,第一次select操作进行快照读的时候生成了一个一致性视图如下:

此时还未进行update操作,所以是读不到的,(注意:DB_TRX_ID是不属于ReadView的字段的,属于隐藏字段!)

接下来当我们执行完update,并且提交的时候进行select操作,会生成一个新一致性视图,但是从3行开始的操作都是一致的,生成的一致性视图也应该是与操作2的一致的呀,按理说我们应该也会生成如下的可见性视图呀!

但是我们如果是如下视图,经过可见性算法,我们应该可以看到数据的呀,所以得出结论:第二次select并未创建新的视图

总结 : 能否看到修改的数据取决于可见性算法,可见性算法比较的时候取决于readview中的结果值

因为不同隔离级别生成readView的时机是不同的

  • RC隔离级别:每次进行快照读都会生成新的ReadView ;
  • RR隔离级别:只有在事务进行第一次快照读的时候生成ReadView,之后的快照读都用之前的ReadView ;

如果当前的所有操作都是当前读,那么是不会产生幻读问题,只有当前读和快照读一起使用的时候才会产生幻读问题

最后,我们加锁解决问题 ;

for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句“,

MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。

相关文章

最新文章

更多