MySQL架构
- server 层 :层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等
- 存储引擎层 :负责数据的存储和提取,支持 InnoDB、MyISAM、Memory 等多个存储引擎。你执行 create table 建表的时候,如果不指定引擎类型,默认使用的就是 InnoDB
redo log (重做日志) 和 binlog(二进制归档日志) undo log(回滚日志)
mysql的 redo log (重做日志) 和 binlog(二进制归档日志) undo log(回滚日志)的区别
MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,以 InnoDB 使用另外一套日志系统------也就是 redo log 来实现 crash-safe 能力;Mysql三个核心日志分别是 binlog(二进制日志) 、redo log(重做日志)、undo log(回滚日志), 这里面binlog 是server层的日志,而redo log 和undo log都是引擎层(innodb)的日志,其他数据引擎就没有redo log和undo log,例如myisam引擎就没有这2个日志,因为它不支持事物。
-
binlog(归档二进制日志) 是 MySQL 的 Server 层实现的,所有引擎都可以使用。binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如"给 ID=2 这一行的 c 字段加 1 ";binlog 是可以追加写入的。"追加写"是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。Binlog可以作为主从复制和数据恢复使用。Binlog没有自动crash-safe;
-
redo log (重做日志模块)是 InnoDB 引擎特有的,是 MySQL 的 Server 层实现的;redo log 是物理日志,记录的是"在某个数据页上做了什么修改,记录数据页的修改状态";redo log 是循环写的,空间固定会用完;redo log 是循环写的,空间固定会用完;Redo Log作为服务器异常宕机后事务数据自动恢复使用,WAL技术也是借助于redo log 来实现的;
binlog(二进制归档日志)日志详解
binlog 是作为mysql操作记录归档的日志,这个日志记录了所有对数据库的数据、表结构、索引等等变更的操作。也就是说只要是对数据库有变更的操作都会记录到binlog里面来;mysql里我们就是通过binlog来归档、验证、恢复、同步数据。
binlog 有三种记录格式,分别是ROW、STATEMENT、MIXED。
- ROW: 基于变更的数据行进行记录,如果一个update语句修改一百行数据,那么这种模式下就会记录100行对应的记录日志。
- 优点:binlog 中可以不记录执行的 SQL 语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下每一行数据修改的细节。不会出现某些特定情况下的存储过程、或 function、或trigger的调用和触发无法被正确复制的问题。
- 缺点:可能会产生大量的日志内容。
- STATEMENT:基于SQL语句级别的记录日志,相对于ROW模式,STATEMENT模式下只会记录这个update 的语句。所以此模式下会非常节省日志空间,也避免着大量的IO操作。
- 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
- 缺点:由于记录的只是执行语句,为了这些语句能在备库上正确运行,还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在备库得到和在主库端执行时候相同的结果。
- MIXED: 混合模式,此模式是ROW模式和STATEMENT模式的混合体,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。
这三种模式需要注意的是:使用 row 格式的 binlog 时,在进行数据同步或恢复的时候不一致的问题更容易被发现,因为它是基于数据行记录的。而使用 mixed 或者 statement 格式的 binlog 时,很多事务操作都是基于SQL逻辑记录,我们都知道一个SQL在不同的时间点执行它们产生的数据变化和影响是不一样的,所以这种情况下,数据同步或恢复的时候就容易出现不一致的情况。
binlog 写入策略
在进行事务的过程中,首先会把binlog 写入到binlog cache中(因为写入到cache中会比较快,一个事务通常会有多个操作,避免每个操作都直接写磁盘导致性能降低),事务最终提交的时候再把binlog 写入到磁盘中。当然事务在最终commit的时候binlog是否马上写入到磁盘中是由参数 sync_binlog 配置来决定的。
-
sync_binlog=0 的时候,表示每次提交事务binlog不会马上写入到磁盘,而是先写到page cache,相对于磁盘写入来说写page cache要快得多,不过在Mysql 崩溃的时候会有丢失日志的风险。
-
sync_binlog=1 的时候,表示每次提交事务都会执行 fsync 写入到磁盘 ;
-
sync_binlog的值大于1 的时候,表示每次提交事务都 先写到page cach,只有等到积累了N个事务之后才fsync 写入到磁盘,同样在此设置下Mysql 崩溃的时候会有丢失N个事务日志的风险。
很显然三种模式下,sync_binlog=1 是强一致的选择,选择0或者N的情况下在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。
redolog
redolog 设计原理
redo log 是属于引擎层(innodb)的日志,它的设计目标是支持innodb的"事务"的特性,事务ACID特性分别是原子性、一致性、隔离性、持久性, 一致性是事务的最终追求的目标,隔离性、原子性、持久性是达成一致性目标的手段,根据的文章我们已经知道隔离性是通过锁机制来实现的。 而事务的原子性和持久性则是通过redo log 和undo log来保障的。
redo log 能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的一致性,这也就是事务持久性的特征,一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决异常、宕机而可能造成数据错误或丢是redo log的核心职责。
redolog 记录内容
redo log记录的是操作数据变更的日志,听起来好像和binlog有类似的地方,有时候我都会想有了binlog为什么还要redo log,当然从其它地方可以找到很多的理由,但是我认为最核心的一点就是redo log记录的数据变更粒度和binlog的数据变更粒度是不一样的,也正因为这个binlog是没有进行崩溃恢复事务数据的能力的。
以修改数据为例,binlog 是以表为记录主体,在ROW模式下,binlog保存的表的每行变更记录。
比如update tb_user set age =18 where name ='张三' ,如果这条语句修改了三条记录的话,那么binlog记录就是
java
UPDATE `db_test`.`tb_user` WHERE @1=5 @2='张三' @3=91 @4='1543571201' SET @1=5 @2='张三' @3=18 @4='1543571201'
UPDATE `db_test`.`tb_user` WHERE @1=6 @2='张三' @3=91 @4='1543571201' SET @1=5 @2='张三' @3=18 @4='1543571201'
UPDATE `db_test`.`tb_user` WHERE @1=7 @2='张三' @3=91 @4='1543571201' SET @1=5 @2='张三' @3=18 @4='1543571201'
redo log则是记录着磁盘数据的变更日志,以磁盘的最小单位"页"来进行记录。上面的修改语句,在redo log里面记录得可能就是下面的形式。
java
把表空间10、页号5、偏移量为10处的值更新为18。
把表空间11、页号1、偏移量为2处的值更新为18。
把表空间12、页号2、偏移量为9处的值更新为18。
当我们把数据从内存保存到磁盘的过程中,Mysql是以页为单位进行刷盘的,这里的页并不是磁盘的页,而是Mysql自己的单位,Mysql里的一页数据单位为16K,所以在刷盘的过程中需要把数据刷新到磁盘的多个扇区中去。 而把16K数据刷到磁盘的每个扇区里这个过程是无法保证原子性的,也就意味着Mysql把数据从内存刷到磁盘的过程中,如果数据库宕机,那么就可能会造成一步分数据成功,一部分数据失败的结果。而这个时候通过binlog这种级别的日志是无法恢复的,一个update可能更改了多个磁盘区域的数据,如果根据SQL语句回滚,那么势必会让那些已经刷盘成功的数据造成数据不一致。所以这个时候还是得需要通过redo log这种记录到磁盘数据级别的日志进行数据恢复。
redolog 写入策略
redo log占用的空间是一定的,并不会无线增大(可以通过参数设置),写入的时候是进顺序写的,所以写入的性能比较高。当redo log空间满了之后又会从头开始以循环的方式进行覆盖式的写入。
在写入redo log的时候也有一个redo log buffer,日志什么时候会刷到磁盘是通过innodb_flush_log_at_trx_commit 参数决定。
当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。
除了上面几种机制外,还有其它两种情况会把redo log buffer中的日志刷到磁盘。
- 定时处理:有线程会定时(每隔 1 秒)把redo log buffer中的数据刷盘。
- 根据空间处理:redo log buffer 占用到了一定程度( innodb_log_buffer_size 设置的值一半)占,这个时候也会把redo log buffer中的数据刷盘。
redo log包括两部分内容,分别是内存中的日志缓冲(redo log buffer)和磁盘上的日志文件(redo log file)。
MySQL 每执行一条 DML 语句,会先把记录写入 redo log buffer(用户空间) ,再保存到内核空间的缓冲区 OS-buffer 中,后续某个时间点再一次性将多个操作记录写到 redo log file(刷盘) 。这种先写日志,再写磁盘的技术,就是WAL。
undolog
undolog 设计目标
redo log 是也属于引擎层(innodb)的日志,从上面的redo log介绍中我们就已经知道了,redo log 和undo log的核心是为了保证innodb事务机制中的持久性和原子性,事务提交成功由redo log保证数据持久性,而事务可以进行回滚从而保证事务操作原子性则是通过undo log 来保证的。
要对事务数据回滚到历史的数据状态,所以我们也能猜到undo log是保存的是数据的历史版本,通过历史版本让数据在任何时候都可以回滚到某一个事务开始之前的状态。undo log除了进行事务回滚的日志外还有一个作用,就是为数据库提供MVCC多版本数据读的功能。
undolog 记录内容
在Mysql里数据每次修改前,都首先会把修改之前的数据作为历史保存一份到undo log里面的,数据里面会记录操作该数据的事务ID,然后我们可以通过事务ID来对数据进行回滚。
比如我们执行 update user_info set name ="李四"where id=1的时候。整个undo log的记录形式会如下。
1、首先准备一张原始原始数据表
2、开启一个事务A: 对user_info表执行 update user_info set name ="李四"where id=1 会进行如下流程操作
1、首先获得一个事务编号 104
2、把user_info表修改前的数据拷贝到undo log
3、修改user_info表 id=1的数据
4、把修改后的数据事务版本号改成 当前事务版本号,并把DB_ROLL_PTR 地址指向undo log数据地址。
3、最后执行完结果如图:
redo、undo、binlog的生成流程与崩溃恢复
当我们执行update user_info set name ="李四"where id=1 的时候大致流程如下:
- 从磁盘读取到id=1的记录,放到内存。
- 记录undo log 日志。
- 记录redo log (预提交状态)
- 修改内存中的记录。
- 记录binlog
- 提交事务,写入redo log (commit状态)
我们根据上面的流程来看,如果在上面的某一个阶段数据库崩溃,如何恢复数据。
1 . 在第一步、第二步、第三步执行时据库崩溃:因为这个时候数据还没有发生任何变化,所以没有任何影响,不需要做任何操作。
-
在第四步修改内存中的记录时数据库崩溃:因为此时事务没有commit,所以这里要进行数据回滚,所以这里会通过undo log进行数据回滚。
-
第五步写入binlog时数据库崩溃:这里和第四步一样的逻辑,此时事务没有commit,所以这里要进行数据回滚,会通过undo log进行数据回滚。
-
执行第六步事务提交时数据库崩溃:如果数据库在这个阶段崩溃,那其实事务还是没有提交成功,但是这里并不能像之前一样对数据进行回滚,因为在提交事务前,binlog可能成功写入磁盘了,所以这里要根据两种情况来做决定。
- 如果binlog存在事务记录:那么就"认为"事务已经提交了,这里可以根据redo log对数据进行重做。其实你应该有疑问,其实这个阶段发生崩溃了,最终的事务是没提交成功的,这里应该对数据进行回滚。 这里主要的一个考虑是因为binlog已经成功写入了,而binlog写入后,那么依赖于binlog的其它扩展业务(比如:从库已经同步了日志进行数据的变更)数据就已经产生了,如果这里进行数据回滚,那么势必就会造成主从数据的不一致。
- 另外一种情况就 是binlog不存在事务记录,那么这种情况事务还未提交成功,所以会对数据进行回滚。
如果MySQL挂了,怎么办?
例如有一个修改数据的请求到MySQL服务上,对于业务代码来说,方才执行的事务是OK的,甚至前端都接受到了请求成功的响应。那结果修改的数据没同步回磁盘,MySQL宕机了会不会导致真实数据和逻辑上的数据不一致呢?其实不会的!
MySQL使用redo log解决了这个问题,redo顾名思义:重做。
当发生事务(增、删、改)时会导致缓存页变成脏页,于此同时MySQL会将事务涉及到的:对 XXX表空间中的XXX数据页XXX偏移量的地方做了XXX更新。
所以MySQL意外宕机重启也没关系。只要在重启时解析redo log中的事务然后重放遍。将Buffer Pool中的缓存页重做成脏页。后续再在合适的时机将该脏页刷入磁盘即可。
mysql的 CrashSafe能力
CrashSafe指MySQL服务器宕机重启后,能够保证:
- 所有已经提交的事务的数据仍然存在。
- 所有没有提交的事务的数据自动回滚。
如果MySQL宕机了,重启后,就需要检查redolog 日志文件里面,系统会自动定位到上次checkpoint的位置,同时,每个数据页中也存在一个LSN,当redo log中的LSN大于数据页中的LSN时,说明重启前redo log中的数据未完全写入数据页中,那么将从数据页中记录的LSN开始,从redo log中恢复数据。
mysql的 WAL技术(预写日志技术)
WAL的全称是Write Ahead Logging, 它的关键点就是先写日志, 再写磁盘. 日志是顺序写的, 磁盘是随机写. 顺序写速度是比随机写快的. 具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log的(记录板文件)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做;InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块"粉板"总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写;如果记录满了的话,擦除记录前要把记录更新到数据文件。有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
两段提交
为了保证binlog 和 redolog两份日志最终恢复到数据库的数据是一致的,采用两阶段提交的机制。
例如现在有一个数据更新的sql
java
update T set c=c+1 where ID=2;
update 语句流程
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内 存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的 一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状 态,更新完成。
两段提交的必要性
假设redo log和binlog分别提交,可能会造成用日志恢复出来的数据和原来数据不一致的情况。
情况一: 先写redolog再写binlog
如果在一条语句redolog之后崩溃了,binlog则没有记录这条语句。系统在用binlog恢复从库时便会少了这一次的修改。恢复的从库少了这条更新。
情况二:先写binlog再写redolog
如果在一条语句binlog之后崩溃了,redolog则没有记录这条语句(可以理解为主库没有)。系统在用binlog恢复从库时便会多了这一次的修改。恢复的从库多了这条更新。
由此可见,如果不使用两阶段提交,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
两段提交崩溃恢复
采用两阶段提交,在做崩溃恢复(Crash recovery)时分为以下3种情况:
情况一: binlog无记录,redolog状态prepare
由于 binlog 还没写,redo log 处于 prepare 状态还没提交,所以崩溃恢复的时候,这个事务会回滚,此时 binlog 还没写,所以也不会传到备库。
情况二:binlog有记录,redolog状态prepare
redolog 中的日志是不完整的,处于 prepare 状态,还没有提交,那么恢复的时候,首先检查 binlog 中的事务是否存在并且完整,如果存在且完整,则直接提交事务,如果不存在或者不完整,则回滚事务。
情况三:binlog有记录,redolog状态commit
直接提交事务,不需要恢复。
mysql的事务
在 MySQL 中,事务支持是在引擎层实现的。你现在知道,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。我将会以 InnoDB 为例,剖析 MySQL 在事务支持方面的特定实现,并基于原理给出相应的实践建议,希望这些案例能加深你对 MySQL 事务原理的理解
隔离性与隔离级别
先说下事物4种特性
原子性:事物在执行过程中,要么全部执行成功,要么全部失败
一致性:事物在执行过程中,保障数据的完整性,一致性,
隔离性:同一时间,只能有一个事物进行
持久性:事物执行完后后,数据会保存在数据库中,无法回滚
事物并发的几个概念
- 脏读:比如财务给员工发工资,发了1W元,员工查工资卡一看挺高兴,后来财务发现工资发错了,立马取消,又改成8K,那么员工看到的1W就是脏数据,这就是脏读
- 不可重复读:事物1需要读2次数据a,读完第一次时,事物2把a的值修改并提交了,这是事物1进行第二次读的时候前后2次读的数据不一致
- 幻读:跟不可重复度类似,例如事物1需要查询2次本月发放工资1W-2W的人,查询完第一次有5条记录,这时事物2发现有一个人的工作发的是8K,发错了,应该发1.2W的,然后把这个人的工资改成了1.2W,这时事物1进行第二次查询的时候,就会发现有6条记录,多了一条记录,就邪门了,产生幻觉了
隔离级别
读未提交:读了一个未提交的数据,隔离级别最低 (可能会脏读)
读已提交:只能读提交过后的数据,sqlserver的默认隔离级别 (可能出现不可重复读)
可重复读:同一事物多次读取数据都一致,MySQL的默认级别(mysql 默认)
序列化读:把事物排序按照顺序执行,上锁,这样会造成锁竞争,占用资源,不建议使用
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。而当前读,总是读取已经提交完成的最新版本。
- 读已提交提交是指,一个事务提交之后,它做的变更才会被其他事务看到。对于读提交,查询只承认在语句启动前就已经提交完成的数据;
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。(事务内多次查询,每次查询的结果是一致的),可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
- 串行化,顾名思义是对于同一行记录,"写"会加"写锁","读"会加"读锁"。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。(多个事务执行的话,是线性顺序执行)(当前读)
事务使用建议
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。
事务的启动方式
其实很多时候业务开发同学并不是有意使用长事务,通常是由于误用所致。MySQL 的事务启动方式有以下几种:
-
显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着2.如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此,我会建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。但是有的开发同学会纠结"多一次交互"的问题。对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一次 "begin",减少了语句的交互次数。如果你也有这个顾虑,我建议你使用 commit work and chain 语法。在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。查询查询数据库中的长事务
sql
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60