26. MVCC和数据库日志
MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制。
MVCC 通过数据行的多个版本管理来实现数据库的并发控制。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。
介绍
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读, 而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
快照读
快照读出现是出于对提高并发性能的考虑,基于MVCC实现,在很多情况下避免了加锁操作,降低了开销。
快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读。
由于是多版本控制,所以快照读可能读到的是历史版本。
如果隔离级别为串行化,则快照读会退化成当前读。
当前读
当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。
隔离级别
之前介绍到事务的四个隔离级别对应解决可能出现的并发问题:

但由于MVCC的引入,MySQL中隔离级别与并发问题的对应关系有所改变:

UndoLog版本链
回顾一下undo日志的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的- 事务id赋值给trx_id 隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
roll_pointer指向的undo日志在事务提交后,它占用的Undo Log Segment也会被系统回收。insert操作由于没有更早的记录,对应的undo日志没有
roll_pointer属性
示例:
#事务一
begin;
UPDATE student SET name="李四"WHERE id=1;
UPDATE student SET name="王五"WHERE id=1;
COMMIT;
#事务二
begin;
UPDATE student SET name="钱七"WHERE id=1;
UPDATE student SET name="宋八"WHERE id=1;
COMMIT;
每次对记录进行改动都会记录一条undo日志,每条undo日志的roll_pointer属性会将undo日志连起来形成一个链表:

该链表称之为版本链 ,版本链的头节点就是当前记录最新的值。每个版本中还包含生成该版本时对应的事务id 。
实现原理
MVCC 的实现依赖于:隐藏字段、Undo Log、Read View。
ReadView
ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(“活跃”指的就是,启动了但还没提交)。
设计思路
使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用READ COMMITTED和REPEATABLE READ隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录。
ReadView中主要包含4个比较重要的内容,分别如下:
- creator_trx_id,创建这个 Read View 的事务 ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
- 
trx_ids,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- 
up_limit_id,活跃的事务中最小的事务 ID。
- 
low_limit_id,表示生成ReadView时系统中应该分配给下一个事务的id值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
规则
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
 
整体操作流程
当查询一条记录的时候,MVCC的执行流程:
- 
首先获取事务自己的版本号,也就是事务 ID; 
- 
获取 ReadView; 
- 
查询得到的数据,然后与 ReadView 中的事务版本号进行比较; 
- 
如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照; 
- 
最后返回符合规则的数据。 
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View,此时就有可能产生不可重复读或幻读的情况。
当隔离级别为可重复读的时候,一个事务只在第一次 SELECT 的时候会获取一次 Read View,之后的查询操作都重复使用,所以避免了幻读问题。
READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同。
MySQL日志
MySQL有不同类型的日志文件,用来存储不同类型的日志,常用的4种分为二进制日志、错误日志、通用查询日志和慢查询日志。MySQL 8又新增两种:中继日志和数据定义语句日志。
- 
慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便我们对查询进行优化。 
- 
通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。 
- 
错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的状态,从而对服务器进行维护。 
- 
二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。 
- 
中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。 
- 
数据定义语句日志:记录数据定义语句执行的元数据操作。 
除二进制日志外,其他日志都是文本文件。默认情况下,所有日志创建于MySQL数据目录中。
日志虽然能让我们了解MySQL内部发生的事情,但也有一定的弊端:
- 日志功能会降低MySQL数据库的性能。
- 日志会占用大量的磁盘空间。
慢查询日志已经介绍过,不再赘述,点此复习21. 索引的设计与性能分析#慢查询日志
通用查询日志
通用查询日志(general query log)用来记录用户的所有操作,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等。当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。
#查看通用查询日志功能的状态
SHOW VARIABLES LIKE '%general%';
开启日志
方式一:永久有效
[mysqld] 
general_log=ON 
#日志文件所在目录路径,filename为日志文件名
general_log_file=[path[filename]]
方式二:临时有效
SET GLOBAL general_log=on; #开启通用查询日志
SET GLOBAL general_log_file=’path/filename’; #设置日志文件保存位置
SET GLOBAL general_log=off; #关闭通用查询日志
SHOW VARIABLES LIKE 'general_log%'; #查看设置后情况
停止日志
方式1:永久有效
[mysqld] 
general_log=OFF
#或
#general_log=ON
方式2:临时有效
SET GLOBAL general_log=off;
SHOW VARIABLES LIKE 'general_log%';
通用查询日志是以文本文件的形式存储在文件系统中的,可以使用文本编辑器直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。
删除/刷新日志:
我们可以使用直接将数据目录的日志文件删除,或使用以下命令重新生成通用查询日志:
mysqladmin -uroot -p flush-logs
错误日志
开启日志
在MySQL数据库中,错误日志功能是默认开启的。而且,错误日志无法被禁止。
[mysqld] 
log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名
查看日志:
SHOW VARIABLES LIKE 'log_err%';
删除\刷新日志:
MySQL的错误日志可以直接删除或使用以下指令初始化:
install -omysql -gmysql -m0644 /dev/null /var/log/mysqld.log
mysqladmin -uroot -p flush-logs
二进制日志
binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的 DDL 和 DML 等数据库更新事件的语句。
不包含如select、show等没有修改任何数据的语句
主要应用场景为数据恢复或数据复制。
查看二进制日志的设置:
在MySQL8中默认情况下,二进制文件是开启的。
 show variables like '%log_bin%';
日志参数设置
修改MySQL的 my.cnf 或 my.ini 文件可以设置二进制日志的相关参数:
[mysqld]
#设置bin-log日志文件名,默认在/var/lib/mysql/目录
log-bin=mylog-bin
#或log-bin="路径"
#日志过期时间默认为30天,设置为600s
binlog_expire_logs_seconds=600
#单个日志文件的大小,默认1g,最大1g
max_binlog_size=100M
bin-log日志存放目录中的文件夹必须提前创建
永久关闭:
只有一个mysql服务器,那么vim  /etc/my.cnf把 log-bin 这一行注释掉,重启mysql服务即可。
临时关闭方式:
在mysql8中只有会话级别的设置,没有global级别的设置。
SET sql_log_bin=0;
当还原数据库的时候,如果不关闭二进制日志,那么还原的过程仍然会记录在二进制日志里面,不仅浪费资源,增加了磁盘的容量,还没有必要(特别是利用二进制还原数据库的时候)所以一般还原的时候会选择关闭二进制日志。
查看日志
当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。
MySQL服务重新启动一次 ,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过了 max_binlog_size 的上限(默认是1GB),就会创建一个新的日志文件。
查看当前的二进制日志文件列表及大小:
 SHOW BINARY LOGS;
MySQL8中设置binlog_format=row,使用以下命令可以将行事件以伪SQL的形式表现出来:
mysqlbinlog -v "文件路径"
#不同时显示binlog格式的语句
mysqlbinlog -v --base64-output=DECODE-ROWS "文件路径"
关于mysqlbinlog工具的使用技巧还有很多,这里简单介绍几个常用语句:
# 可查看参数帮助
mysqlbinlog --no-defaults --help
# 查看最后100行
mysqlbinlog --no-defaults --base64-output=decode-rows -vv 文件名 |tail -100
# 根据position查找
mysqlbinlog --no-defaults --base64-output=decode-rows -vv 文件名 |grep -A20 '4939002'
上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方便的查询命令:
show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];
- IN 'log_name':指定要查询的binlog文件名(不指定就是第一个binlog文件)
- FROM pos:指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)
- LIMIT [offset]:偏移量(不指定就是0)
- row_count:查询总条数(不指定就是所有行)
show binlog events in '文件名';
以上使用的都是基于binlog的默认格式
show variables like 'binlog_format';
此外binlog还有2种格式,分别是Statement和Mixed:
- Row
5.1.5版本的MySQL才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
- Statement
每一条会修改数据的sql都会记录在binlog中。
优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
- Mixed
从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。
使用日志恢复数据
mysqlbinlog恢复数据的语法如下:
mysqlbinlog [option] filename|mysql –uuser -ppass;
- filename:是日志文件名。
- option:可选项,比较重要的两对option参数是--start-date、--stop-date 和 --start-position、-- stop-position。- --start-date 和 --stop-date:可以指定恢复数据库的起始时间点和结束时间点。
- --start-position和--stop-position:可以指定恢复数据的开始位置和结束位置。
 
注意:使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如atguigu-bin.000001必须在atguigu-bin.000002之前恢复。
删除日志
MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。PURGE MASTER LOGS只删除指定部分的二进制日志文件, RESET MASTER删除所有的二进制日志文件。具体如下:
PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’
PURGE {MASTER | BINARY} LOGS BEFORE ‘指定日期’
二进制日志可以通过数据库的 全量备份 和二进制日志中保存的增量信息,完成数据库的无损失恢复。但由于起止位置不容易选择,在数据量大的场景下数据恢复难度很大。
一个有效的解决办法是配置主从数据库服务器,甚至是一主多从的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。
写入机制
binlog的写入时机非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

write和fsync的时机,可以由参数sync_binlog控制,默认是 0。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。
为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。
在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。
binlog与redolog对比
- redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎层产生的。
- 而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。
- 虽然它们都属于持久化的保证,但是侧重点不同。
- redo log 让InnoDB存储引擎拥有了崩溃恢复能力。
- binlog保证了MySQL集群架构的数据一致性
 
两阶段提交
在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。

此时如果在binlog写入时发生异常,MySQL重启后会出现主机数据与从机数据不一致的情况(主机通过redolog恢复,从机通过binlog恢复)
为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。

这种情况下,即使是redo log设置commit阶段发生异常,也不会回滚事务

中继日志
中继日志(relay log)只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。文件名的格式是:从服务器名 -relay-bin.序号。中继日志还有一个索引文件:从服务器名 -relay-bin.index,用来定位当前正在使用的中继日志。
中继日志与二进制日志的格式相同,可以用 mysqlbinlog 工具进行查看。
中继日志里是包含从服务器名的,重装系统的从服务器无法从宕机前的中继日志里读取数据,把从服务器的名称改回之前的名称。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号