一个最基本的数据库,应当可以做到以下几点
-
数据持久化,可以将数据保存到磁盘,服务重启数据依然存在。
-
可以按照某种关系存储数据,如果你用过IO流,那么你会发现整理数据也是一件复杂的事情。我是该追加写呢还是找到某条数据位置再进行写呢?这是个很复杂的问题。
-
快速查找。你想想自己如果将数据写入txt,那又如何高效的去找到某条数据?支持随机查找吗?
-
故障恢复与数据回滚,倘若你的服务断电了,如何确保数据一定是写入到文件的?若是误删或误改了某条数据,你又如何进行恢复?
MySQL的架构
关于MySQL的简单架构图。
MySQL大致可以分为服务层与存储引擎层。在单独抽离了存储引擎层后,你可以选择合适的引擎,例如InnoDb,MyIsam,Memory等等。
关于不同的存储引擎,使用的方式可能不同。我主要想讲的是InnoDb引擎,MySQL 5.5 版本后默认的存储引擎。
MySQL的日志系统
MySQL有三大日志,分别是重做日志(redo log),二进制日志(bin log),以及回滚日志(undo log)。这三个日志非常重要,学习MySQL数据库一定免不了要和他们打交道。
bin log
bin log是Server层的日志,无论使用的是什么引擎,都可以使用这种日志。这个日志记录的是逻辑日志,就是SQL语句。例如insert into table set xx = xx
在bin log中记录的也是这样的一条SQL。而且bin log 采用的是追加写的形式,也即是说在写完一个bin log文件之后,不会覆盖前面的,而是新开一个文件继续追加写。
redo log
redo log 是存储引擎InnoDB所提供的日志模块。个日志记录的是,物理日志。记录的是当前SQL在哪一个数据页上将什么数据修改为了什么数据。
关于redo log,我很喜欢林晓斌老师在《MySQL实战45讲》中讲的例子,酒馆的账本与黑板的例子。在古时候的酒馆中,老板会有一本账本,以及身后的一块黑板。倘若今天有人去喝酒,赊账。在很忙的时候,老板会将这条记录写在黑板上,后续等到酒馆打烊了,不忙的时候,才将这个记录写进自己的账本中。
事实上,在MySQL也是这么做的,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。
而黑板和账本配合的整个过程,其实就是 MySQL中常说到的 WAL (Write-Ahead Logging)技术,WAL 的全称是 ,它的关键点就是先写日志,再写磁盘,也就是先写黑板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(黑板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像酒馆打烊之后老板做的事。
如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,黑板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。
与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“黑板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos 是当前记录的位置,一边写一边后移。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是“黑板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 check point,表示“黑板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
undo log
undo log 记录的是与执行SQL相反的SQL。例如,在user表,id为1的用户age为32,那么执行update table user set age = 45 where id = 1
,那么undo log中则会记录update table user set age = 32 where id = 1
,如果执行的是delete语句,那么相应的,它会记录一条insert语句。
undo log是MySQL用于事务模块的重要日志,其中的MVCC(多版本并发控制技术)就与undo log版本链强相关。这篇文章重点不在此,因此不再多说。
MySQL如何做数据恢复
假如在今天的12点钟,你误删了一个表。这种情况下该怎么恢复数据?首先,在使用MySQL时,通常会对其进行全量备份。一般是一天、三天或每周一次。
-
那么此时应当找到最近的一次全量备份放入临时库中。
-
找到从全量备份的那一刻开始,将bin log重放到误操作今天的12点钟。
-
如此你便拿到了误操作之前的数据,此时你可以将临时库中的数据按需要恢复回去。
MySQL如何做到故障恢复?(Crash-Safe的能力)
在InnoDB引擎下,MySQL支持事务。因此故障恢复还需要考虑到已提交的数据与未提交的数据。单独靠bin log 或 redo log 是无法保证crash-safe的。
两阶段提交
一条update语句的简单执行过程
我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
-
执行器先找向存储引擎找到 id = 1 这一行。id 作为主键,存储引擎直接用B+树搜索找到这一行。如果id=1 这行所在的数据页已经在内存中,就直接返回给执行器;否则就先从磁盘读入内存中,再返回。
-
执行器拿到存储引擎给的行数据,把这个值加上 1,比如原来是 n,现在为 n+1,得到了一行新的数据,再调用存储引擎的接口写入这一行新的数据。
-
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。
-
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
-
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交commit状态。
最后三步看起来有点复杂,InnoDB将 redo log 的写入分为了两个步骤:prepare阶段和commit阶段,这就是两阶段提交。
图中白色框表示是在 InnoDB引擎内部执行的,绿色框表示的是在执行器中执行的。
为什么日志需要“两阶段提交”。
由于 redo log 与 bin log 是两个层单独的日志,如果不采用两阶段提交的方式,要么是先写 redo log 再写 bin log,或采用反的顺序。
下面看看这两种方式会出现什么问题。
仍然使用用前面的 update 语句来做例子。假设当前 id=1 的行,字段 a 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
-
先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 a 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 a 的值就是 0,与原库的值不同。
-
先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 a 的值是 0。但是 binlog 里面已经记录了 “把 a 从 0 改成 1” 这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 a 的值就是 1,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
总结
学习了挺久的MySQL,突然又对其的数据恢复和故障恢复起了兴趣,往深入了解又发现了之前一些之前无法理解的问题突然迎刃而解了。
-
MySQL的数据恢复与故障恢复依赖着几个日志,bin log 与 redo log。bin log 是逻辑日志,记录的是原始SQL语句,redo log 是InnoDB引擎支持的,是物理日志,记录了在哪个数据页修改了哪些数据,并且redo log 是循环写日志。
-
MySQL需要按照一定时间进行全量备份,这样我们可以依靠最近一次全量备份点,以及从该点开始记录的bin log进行数据重放恢复
-
MySQL在使用了InnoDB引擎后,支持了事务,因此故障恢复需要确保可以区分已提交事务与未提交事务。这个依赖于redo log 的二阶段提交。