文章目录

  • 1. 锁机制
  • 1.1 InnoDB的锁类型
  • 1.1.1 读锁(S锁)
  • 1.1.2 写锁(X锁)
  • 1.1.3 记录锁(Record Lock)
  • 1.1.4 间隙锁(GAP Lock)
  • 1.1.5 记录锁和间隙锁的组合(next-key Lock)
  • 1.1.6 MDL锁
  • 1.1.7 页锁
  • 1.1.8 意向锁
  • 1.1.9 全局锁
  • 1.1.10 死锁问题
  • 1.2 表锁
  • 1.3 从另一个角度区分锁的分类
  • 1.3.1 乐观锁
  • 1.3.2 悲观锁
  • 2. 日志系统
  • 2.1 bin log日志
  • 2.1 概述
  • 2.2 数据恢复
  • 2.3 格式分类
  • 2.4 日志格式
  • 2.5 binlog刷盘
  • 2.6 binlog实现主从同步
  • 2.2 其他日志
  • 2.2.1 通用查询日志,默认关闭
  • 2.2.2 慢查询日志
  • 2.2.3 错误日志
  • 2.3 redo log日志
  • 2.3.1 MTR
  • 2.3.2 log buffer
  • 2.3.3 checkpoint
  • 2.3.4 一个事务的执行流程
  • 2.3.5 系统崩溃的影响
  • 2.4 undo log日志
  • 2.4.1 概述
  • 2.4.2 事务id(DB_TRX_ID)
  • 2.4.3 DB_ROLL_PTR
  • 2.4.4 分类
  • 2.4.5 物理存储结构
  • 2.4.6 记录流程
  • 2.4.7 回滚过程
  • 2.5 ACID靠什么保证的?
  • 2.6 InnoDB 是如何实现事务的
  • 3. 隔离级别和MVCC
  • 3.1 Read View(读视图)
  • 3.1.1 实现原理
  • 3.1.2 readView的结构
  • 3.2 快照读原理解析
  • 3.3 解决脏读和不可重复读
  • 3.3.1 解决脏读
  • 3.3.1 解决不可重复读
  • 3.3.3 解决幻读
  • 4. InnoDB 四大特性
  • 4.1 插入缓冲(insert buffer)
  • 4.2 二次写(double write)
  • 4.3 自适应哈希索引(adaptive hash index)
  • 4.4 预读(read ahead)
  • 线性预读(linear read-ahead)
  • 随机预读(Random read-ahead)


1. 锁机制

锁是为了保证数据库中数据一致性,使各种 共享资源在被访问时变得有序而设计的一种规则。

MySQL中不同的存储引擎支持不同的锁机制,InnoDB支持行锁,有时也会升级为表锁Myisam只支持表锁

  • 表锁:表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表时,必须等前一个事务释放了锁才能进行对表进行访问。

表锁的特点:
开销小,加锁快,不会出现死锁。锁粒度大,发生锁冲突的概率小,并发度相对低

  • 行锁:行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只要被锁住的记录不能访问,其他的记录可正常访问。

行锁的特点:
开销大,加锁慢,会出现死锁。锁粒度小,发生锁冲突的概率大,并发度高。

今天我们讲锁主要从InnoDB引擎来讲,因为它既支持行锁、也支持表锁。

1.1 InnoDB的锁类型

基于锁的属性分类:

  • 共享锁
  • 排他锁

基于锁的粒度分类:

  • 行级锁(InnoDB)
  • 表级锁(InnoDB和Myisam)
  • 页级锁(BDB引擎)
  • 记录锁
  • 间隙锁
  • 临键锁
  • MDL锁
  • 全局锁

基于锁的状态分类:

  • 意向共享锁
  • 意向排它锁

1.1.1 读锁(S锁)

读锁(共享锁,shared lock)简称S锁,一个事务获取了一个数据行的读锁,其他事务也能获得该行对应的读锁,但不能获得写锁即一个事务在读取一个数据行时,其他事务也可以读,但是不能对该数据行进行增删改得操作。

注意:
读锁是共享锁,多个事务可以同时持有,当有一个或者多个事务持有共享锁时,被锁得数据就不能修改。

简而言之:就是可以多个事务读,但只能一个事务写。

读锁是通过select.... lock in share mode;语句给被读取得行记录或行记录得范围加一个读锁,让其他事务可以读,但是要想申请加写锁,那就会被阻塞。

事务一:

begin;
SELECT * FROM demo_info where id=1 lock in share mode;

事务二:

begin;
UPDATE demo_info set key1='aa' where id=1 ;

卡住了,说明程序被阻塞,确实加了锁。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java

s锁是可以被多个事务同时获取的,我们在两个不同的事务中分别对同一行数据加上s锁,结果都可以成功,如下图:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_02

1.1.2 写锁(X锁)

写锁(排它锁或者独占锁,exclusive)简称X锁,一个事务获取了一个数据行的写锁,即可以读该行的记录,又可以修改该行的记录。但其他事务就不能在获取该行的其他任何锁。包括S锁,直到当前事务将锁释放。这保证了其他事务在当前事务释放锁之前不能再修改数据。

注意:
写锁是独占锁,只有一个事务可以持有,当这个事务持有写锁时,被锁的数据就不能被其他事务修改。

  1. 一些DML语句的操作都会对行记录加写锁。

事务一:

begin;
UPDATE demo_info set key1='aa' where id=1 ;

事务二:

begin;
UPDATE demo_info set key1='bb' where id=1 ;

卡住了,说明程序在加锁,确实加了锁,但是我们发现,其他事务还能读,有点不符合逻辑,这是因为MySQL实现了MVCC模型,后边会详细介绍。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java_03

  1. 比较特殊的就是 select ... for update;它会对读取的行记录加一个写锁,那么其他任何事务就不能对被锁定的行上加任何锁,要不然会被阻塞。

事务一:

begin;
SELECT * FROM demo_info where id=1 for update;

事务二:

begin;
UPDATE demo_info set key1='bb' where id=1 ;

卡住了,说明加了锁了。

  1. X锁是只能被一个事务获取,我们在两个的不同事务中分别对同一行数据加上X锁,发现后者会被阻塞,如下图。

1.1.3 记录锁(Record Lock)

记录锁也属于行锁的一种,只有InnoDB才支持,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。

精准条件命中,并且命中的条件字段是唯一索引。

加了记录锁之后,数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

我们使用以下四个案例来验证记录锁的存在:

  1. 两个事务修改同一行记录,该场景下,where条件中的列不加索引。

事务一:

begin;
UPDATE demo_info set key1='aa' where common_field='1' ;

事务二:

begin;
UPDATE demo_info set key1='bb' where common_field='1' ;

发现事务二卡住了,只有事务一提交了,事务二才能继续执行,很明显,这一行数据被住了。

  1. 两个事务修改同表不同行记录,此时where条件也不加索引。

事务一:

begin;
UPDATE demo_info set key1='aa' where common_field='1' ;

事务二:

begin;
UPDATE demo_info set key1='bb' where common_field='2' ;

发现事务二卡住了,只有事务一提交了,事务二才能继续执行,很明显。表被住了。

  1. 两个事务修改同一行记录,where条件加上索引

事务一:

begin;
UPDATE demo_info set key1='aa' where common_field='1' ;

事务二:

begin;
UPDATE demo_info set key1='bb' where common_field='1' ;

发现事务二卡住了,只有事务一提交,事务二才能继续执行。很明显,这一行数据被住了。

  1. 两个事务修改同表不同行记录,此时where条件加索。

事务一:

begin;
UPDATE demo_info set key1='aa' where common_field='1' ;

事务二:

begin;
UPDATE demo_info set key1='bb' where common_field='2' ;

发现都可以顺利修改,说明锁的的确是行。

综上所述:

  • 行锁是加在索引上的,这是标准的行级锁。
  • 如果where条件没有索引,那么行锁会升级为表锁。

1.1.4 间隙锁(GAP Lock)

间隙锁属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙,则会形成一个区间,遵循左开有闭原则。

范围查询并且查询未命中记录,查询条件必须命中索引,间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。

间隙锁帮我们解决了mysqlRR级别下的一部分幻读问题,间隙锁锁定的是记录范围,不包含记录本身,也就是不允许在某个范围内插入或者删除数据。

间隙锁生成的条件:

  1. A事务使用where进行范围检索时未提交事务,此时B事务向A满足检索条件的范围插入或者删除数据。
  2. where条件必须有索引。

事务一:

begin;
select * from student where id between 7 and 15 lock in share mode;

事务二:

insert into student values (12,'tom',66,'d');

发现卡住了,第一个事务会将id715之间的数据全部锁定,不允许在缝隙间插入。

事务三:

insert into student values (18,'tom',66,'d');

插入一个id18的数据,竟然成功了,因为18不在事务一的检索的范围

1.1.5 记录锁和间隙锁的组合(next-key Lock)

临键锁也属于行锁的一种,并且它是InnoDB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合

临键锁会把查询出来的数据锁住,同时也会把该范围内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。

它的封锁范围,既包含索引记录,又包含索引区间

触发条件:范围查询并命中,查询命中了索引。

结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读,重复读,幻读问题,加了临键锁之后,在范围区间内数据不允许被修改和插入。

注意:

临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

1.1.6 MDL锁

MySQL 5.5引入了meta data lock,简称MDL锁,用于保证表中元数据的信息。在会话A中,表开启了查询事务后,会自动获得一个MDL锁,会话B就不可以执行任何DDL语句,不能执行为表中添加字段的操作,会用MDL锁来保证数据之间的一致性。

元数据就是描述数据的数据,也就是你的表结构。意识是在你开启了事务之后获得了意向锁,其他事务就不能更改你的表结构。MDL锁都是为了防止在事务进行中,执行DDL语句导致数据不一致。

1.1.7 页锁

页级锁是MySQL中锁粒度介于行级锁和表级锁中间的一种锁,表级锁速度快,但冲突多,行级锁冲突少,但是速度慢,所以取了折中的页级锁,一次锁定相邻的一组记录,

页级锁的特点:
开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

1.1.8 意向锁

如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁,你们不能对整个表加共享锁或者排他锁了,那么后面需要对整个表加锁的人只需要获取这个状态,就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。

意向共享锁:
当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。

意向排他锁:
当一个事务试图对整个表进行加排他锁之前,首先需要获得这个表的意向排他锁。

1.1.9 全局锁

全局锁:Flush tables with read lock,加锁之后整个数据库实例都处于只读状态,所有的数据变更操作都会被挂起。一般用于全库逻辑备份。

1.1.10 死锁问题

发生死锁的必要条件4个,分别为互斥条件不可剥夺条件请求与保持条件循环等待条件

  • 互斥条件,在一段时间内,计算机中的某个资源只能被一个进程占用。此时,如果其他进程请求该资源,则只能等待。
  • 不可剥夺条件,某个进程获得的资源在使用完毕之前,不能被其他进程强行夺走,只能由获得资源的进程主动释放。
  • 请求与保持条件,进程已经获得了至少一个资源,又要请求其他资源,但请求的资源已经被其他进程占有,此时请求的进程就会被阻塞,并且不会释放自己已获得的资源。
  • 循环等待条件,系统中的进程之间相互等待,同时各自占用的资源又会被下一个进程所请求。例如有进程A、进程B和进程C三个进程,进程A请求的资源被进程B占用,进程B请求的资源被进程C占用,进程C请求的资源被进程A占用,于是形成了循环等待条件,如图1-7所示。

我模拟了一个死锁场景,如下:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_04


InnoDB使用的是行级锁,在某种情况下会产生死锁问题,所以InnoDB存储引擎采用了一种叫作等待图(wait-for graph)的方法来自动检测死锁,如果发现死锁,就会自动回滚一个事务。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_05

在MySQL中,通常通过以下几种方式来避免死锁。

  • 尽量让数据表中的数据检索都通过索引来完成,避免无效索引导致行锁升级为表锁。
  • 合理设计索引,尽量缩小锁的范围。
  • 尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围。
  • 尽量控制事务的大小,减少一次事务锁定的资源数量,缩短锁定资源的时间。如果一条SQL语句涉及事务加锁操作,则尽量将其放在整个事务的最后执行。

如何查看死锁

  • 使用命令show engine innodb status 查看最近的一次死锁。
  • InnoDB Lock Monitor打开锁监控,每15s输出一次日志,使用完毕后建议关闭,否则会影响数据库性能。

对待死锁常见的两种策略

  • 通过innodblockwait_timeout来设置超时时间,一直等待直到超时。
  • 发起死锁检测,发现死锁之后,主动回滚死锁中的某一个事务,让其他事务继续执行。

1.2 表锁

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁

  • 第一种情况是:事务需要更新大部分或者全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
  • 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚,这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁,减少数据库因为事务回滚带来的开销。

InnoDB下 ,主动上表锁的方式如下:

lock tables teacher write,student read;
select * from teacher;
commit;
unlock table

使用时有几点需要额外注意:

  • 使用lock tables 虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server层负责的,仅当autocommit=0(手动提交事务),innodb_table_lock=1(默认设置)时,InnoDB层才能感知MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁,否则,InnoDB将无法自动检测并处理这种死锁。
  • 在使用lock tables 对InnoDB加锁时要注意,事务结束前,不要用unlock tables释放表锁,因为unlock tables会隐含地提交事务,commit和rollback不能释放用lock tables 加的表级锁,必须用unlock tables释放表锁,正确的方式见如下语句。
SET AUTOCOMMIT=0;

LOCAK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and here];

COMMIT;
UNLOCK TABLES;
  • 表锁的力度很大,慎用。

1.3 从另一个角度区分锁的分类

1.3.1 乐观锁

乐观锁大多是基于数据版本记录机制实现,一般是给数据库表增加一个version字段。

  • 读取数据时,将此版本号一同读出。
  • 更新时,对此版本号加1,此时将提交数据的版本号数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据表当前版本号,则予以更新,否则认为是过期数据。

事务一:

select * from ydl_student where id = 1;

事务二:

select * from ydl_student where id = 1;
update ydl_student set score = 99,version = version + 1 where id = 1 and version = 1;
commit;

事务一:

update ydl_student set score = 100,version = version + 1 where id = 1 and version = 1;
commit;

发现更新失败,应为版本号被事务二、提前修改了,这使用了不加锁的方式,实现了一个事务修改期间,禁止其他事务修改的能力。

1.3.2 悲观锁

总有刁民想害朕!!!

悲观锁依靠数据库提供的锁机制实现,MySQL中的共享锁排它锁都是悲观锁,数据库的增删改操作默认都会加排它锁,而查询不会加任何锁,此处不赘述。

2. 日志系统

mysql给我们提供了很多有用的日志,这是mysql服务层给我们提供的:

日志类型

写入日志的信息

二进制日志

记录了对MySQL数据库执行更改的所有操作

慢查询日志

记录了所有执行时间超过long_query_time 秒的所有查询或不使用索引的查询

错误日志

记录了在启动,运行或停止mysqlId时遇到的问题

通用查询日志

记录了建立的客户端连接和执行的语句

中继日志

记录了从复制主服务器接受的数据更改

2.1 bin log日志

2.1 概述

二进制日志(binnary log)是以事件形式记录了对MySQL数据执行修改的所有操作。

binlog记录了所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(例如INSERT、UPDATE、DELETE…)的二进制日志。不会记录SELECTSHOW这类操作,因为这类操作对数据本身并没有修改,但可以通过查询通用日志来查看MySQL执行过的
所有语句。

binlogMySQL Server层维护的,跟采用何种存储引擎没有关系,记录的是所有的更新操作的日志记录。binlog是在事务最终commit前写入的。我们执行SELECT不涉及数据更新的语句是不会记binlog的,而涉及到数据更新则会记录。需要注意的是,对支持事务的引擎比如InnoDB来说,必须要提交了事务才会记录binlog

binlog文件写满以后,会自动切换到下一个日志文件继续写,而不会覆盖以前的日志,这个也区别redo log,undo log循环写入的,即后面写入的可能会覆盖前面写入的。

binlog有两个常用的使用场景:

  • 主从复制:后面会专门有一个章节代领大家搭建一个主从同步的两台mysql服务。
  • 数据恢复:通过mysqlbinlog工具来恢复数据。

mysql8中的binLog默认是开启的,5.7默认是关闭的,可以通过参数log_bin控制:

2.2 数据恢复

  1. 确认binlog是否开启,log_bin变量的值为ON代表binlog日志是开启状态。
show variables like 'log_bin';

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_06


如果没有开启,可以在配置文件 [mysqld] 下写入如下内容并重启mysql服务:

# 开启 Binlog 并写明存放日志的位置
log_bin = /usr/local/mysql/log/bin-log
  1. 为了防止干扰,我们flush刷新log日志,自此刻会产生一个新编号的binlog日志文件:
flush logs;
  1. 查看所有binlog日志列表
show master logs;

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_07

  1. 查看master状态,即最后(最新)一个binlog日志的编号名称,及其最后一个操作事件pos结束点(Position)值,这一步可有可无:
show master status;

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_08

  1. 执行sql

先创建表,并插入一些数据:

DROP TABLE IF EXISTS ydl_student;
CREATE TABLE `ydl_student` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `score` int(255) DEFAULT NULL,
  `grade` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (1, 'lucy', 80, 'a');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (2, 'lily', 90, 'a');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (3, 'jack', 60, 'c');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (4, 'hellen', 40, 'd');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (5, 'tom', 60, 'c');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (6, 'jerry', 10, 'd');
INSERT INTO `ydl_student`(`id`, `name`, `score`, `grade`) VALUES (7, 'sily', 20, 'd');

执行删除操作,假装误删除,直接全部删除也可以,把表删了都行,一样的道理:

delete from ydl_student where id in (3,5);
  1. 查看binlog日志,我们因为刷新了日志,所以本次操作都会在最新的日志文件上:

因为binlog的日志文件是二进制文件,不能用文本编辑器直接打开,需要用特定的工具来打开,MySQL提供了mysqlbinlog来帮助我们查看日志文件内容。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java_09

# 查看全部的日志信息
mysqlbinlog -v C:\Users\DXH\Desktop\bin-log.000002

文件内容

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_10


真实的情况下,我们的日志文件比较复杂,内容比较多使用时间范围查询后任然可能需要花费时间去排查问题,这里我们找到了误删除的位置:

# 指定位置范围
mysqlbinlog -v C:\Users\DXH\Desktop\bin-log.000002 --start-position=0 --stop-position=986

# 指定时间范围
mysqlbinlog -v C:\Users\DXH\Desktop\bin-log.000002 --start-datetime="2022-07-08 15:18:00" --stop-datetime="2022-07-08 18:18:00"

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_11

截取其中的一段进行分析:

# at 2324
#220708 18:00:46 server id 1  end_log_pos 2430  Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1657274446/*!*/;
delete from ydl_student where id in (3,5)

上面输出包括信息:

  • position:位于文件中的位置,即第一行的(# at 2324),说明该事件记录从文件的第2324个字节开始。
  • timestamp:事件发生的时间戳,即第二行的(#220708 18:00:46
  • server id :服务器标识。
  • end_log_pos:表示下一个事件的结束位置(即当前事件的结束位置+1)。
  • thread_id:执行该事件的线程idthread_id=1
  • exec_time: 事件执行的花费时间。
  • error_code: 错误码,0意味着没有发生错误。
  • type:事件类型Query
  1. 执行恢复,通过上一步的操作,我们找到了删除的位置2254(即第一个红框),执行下面的语句:
mysqlbinlog -v C:\Users\DXH\Desktop\bin-log.000002 --stop-position=2254 -v | mysql -uroot -p密码
  1. 至此,数据已完全恢复了:

binlog的数据恢复的本质,就是将之前执行过的sql,从开始到指定位置全部执行一遍,如果报错【当前表已经存在】,就将数据库的表删除,重新恢复。

2.3 格式分类

binlog有三种格式,使用变量binlog_format查看当前使用的是哪一种。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_12

  • StatementStatement-Based Replication,SBR:每一条会修改数据的SQL都会记录在binlog中。
  • RowRow-Based Replication,RBR:不记录SQL语句的上下文信息,仅保存那条记录被修改。
  • MixedMixed-Based Replication,MBRStatementRow 的混合体,当前默认的选项,5.7中默认row

我们举一个例子来说明rowstatement的区别,在下面的插入语句中我们有一个函数uuid(),如果日志文件仅仅保存sql语句,下一次执行的结果可能不一致,所以Row格式的文件,他保存的是具体哪一行,修改成了什么数据,记录的是数据的变化,不是简单的sql

insert into ydl_student values (8,UUID(),45,'d');

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_13


Statement和row的优劣

  • Statement模式只记录执行的SQL,不需要记录每一行数据的变化,因此极大的减少了binlog的日志量,避免了大量的I/O操作。提升了系统的性能。
  • 由于 Statement 模式只记录 SQL,而如果一些 SQL 中 包含了函数,那么可能会出现执行结果不一致的情况。比如说 uuid() 函数,每次执行的时候都会生成一个随机字符串,在 master 中记录了 uuid,当同步到 slave 之后,再次执行,就得到另外一个结果了。所以使用 Statement 格式会出现一些数据一致性问题。
  • MySQL5.1.5 版本开始,binlog 引入了 Row 格式,Row 格式不记录 SQL 语句上下文相关信息,仅仅只需要记录某一条记录被修改成什么样子了。
  • 不过 Row 格式也有一个很大的问题,那就是日志量太大了,特别是批量 update、整表 deletealter 表等操作,由于要记录每一行数据的变化,此时会产生大量的日志,大量的日志也会带来 IO 性能问题。

2.4 日志格式

  • binlog文件以一个值为0Xfe62696e的魔数开头,这个魔数对应0xfebin
  • binlog由一系列的binlog event构成。每个binlog event包含headerdata两部分。
  • header部分提供的是event的公共的类型信息,包括event创建时间服务器等等。
  • data部分提供的是针对该event的具体信息,如具体数据的修改。

常见的事件类型有:

  • FORMAT_DESCRIPTION_EVENT:该部分位于整个文件的头部,每个binlog文件都必定会有唯一一个该event
  • WRITE_ROW_EVENT:插入操作。
  • DELETE_ROW_EVENT:删除操作。
  • UPDATE_ROW_EVENT:更新操作。记载的是一条记录的完整的变化情况,即从前量变为后量的过程。
  • ROTATE_EVENTBinlog结束时的事件,用于说明下一个binlog文件。

一个event的结构如下,我们在恢复数据的时候已经看到了:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_14

  • 每个日志的最后都包含一个rotate event用于说明下一个binlog文件。
  • binlog索引文件是一个文本文件,其中内容为当前的binlog文件列表,比如下面就是一个mysql-bin.index文件的内容。

2.5 binlog刷盘

二进制日志文件并不是每次写的时候同步到磁盘,因此当数据库所在的操作系统发生宕机时,可能会有最后一部分数据没有写入二进制日志文件中。这给恢复和复制带了问题,参数sync_binlog=[N]表示每写多少次就同步到磁盘,如果将N设为1,即sync_binlog=1表示采用同步写磁盘的方式来写二进制日志,这是写操作不使用操作系统的缓冲来写二进制日志,备注:该值默认为0,采用操作系统机制进行缓冲数据同步)。

2.6 binlog实现主从同步

数据库单点部署的问题

  • 服务器宕机,会导致业务停顿,影响客户体验。
  • 服务器损坏,数据丢失,不能及时备份,造成巨大损失。
  • 读写操作都在同一台服务器,在并发量大的情况下性能存在瓶颈。

那么我们就可以使用mysqlbinlog搭建一个一主多从mysql集群服务。这样的服务可以帮助我们异地备份数据、进行读写分离,提高系统的可用性。

(1) 主从复制工作原理剖析

MySQL的主从复制中主要有三个线程,Master(binlog dump thread)Slave(I/O thread,SQL thread)Master一条线程和Salve中的两条线程。

  1. 主节点binlog主从复制的基础是主库记录数据库的所有变更记录到binlog中,binlog是数据库服务器启动的那一刻起,保存所有修改数据库结构和内容的一个文件。
  2. Master数据库将操作语句记录到binlog日志中,然后授予Slave远程连接的权限(Master一定要开启binlog二进制日志功能,通常为了数据安全考虑, Slave也开启binlog功能)。
  3. Slave数据库开启两个线程:I/O线程和SQL线程,其中I/O线程负责读取Masterbinlog内容到中继日志relay log里。SQL线程负责从Relay log日志里读出binlog内容,并更新到Slave数据库里,这样就能保证 Slave数据和Master数据保持一致了。最终保证主从数据库的一致性。
  4. MySQL主从复制至少需要两个MySQL的服务,当然MySQL服务可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。
  5. MySQL主从复制最好确保masterslave服务器上的MySQL版本相同(如果不能满足版本一致,那么要保证master主节点的版本低于slave从节点的版本)。
  6. masterslave两节点间时间需同步。

注意:
主从节点使用binlog文件+position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置发起同步。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_15


由于MySQL默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了,由此产生两个概念。

  • 全同步复制:主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。
  • 半同步复制:和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回 ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成

(2)怎么配置mysql主从复制

环境准备
安装两个mysql,使用vmvare安装两个linux系统就可以:

mysql1(master): 42.192.181.133:3306
mysql2(slave):  124.220.197.17:3306

mysql 配置文件配置

mysql1(master): 配置文件设置,开启bin_log(已经开启的可以忽略)且需要配置一个server-id

#mysql master1 config 
[mysqld]
server-id = 1            # 节点ID,确保唯一

# log config
log-bin = master-bin     #开启mysql的binlog日志功能

mysql2(slave): 需要开启中继日志

[mysqld]
server-id=2
relay-log=mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=sys.%
replicate-wild-ignore-table=information_schema.%
replicate-wild-ignore-table=performance_schema.%

重启两个mysql,让配置生效。

第三步 在master数据库创建复制用户并授权

  1. 进入master的数据库,为master创建复制用户
CREATE USER 'repl'@'124.220.197.17' IDENTIFIED BY 'Root12345_';
  1. 赋予该用户复制的权利
grant replication slave on *.* to 'repl'@'124.220.197.17' 
FLUSH PRIVILEGES;
  1. 查看master的状态
show master status;
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000005      120|              | mysql            |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
  1. 配置从库
CHANGE MASTER TO 
MASTER_HOST = '42.192.181.133',  
MASTER_USER = 'repl', 
MASTER_PASSWORD = 'Root12345_',
MASTER_PORT = 3306,
MASTER_LOG_FILE='mysql-bin.000020',
MASTER_LOG_POS=2735,
MASTER_HEARTBEAT_PERIOD = 10000; 

# MASTER_LOG_FILE与主库File 保持一致
# MASTER_LOG_POS=120 , #与主库Position 保持一致

解释:MASTER_HEARTBEAT_PERIOD表示心跳的周期。当MASTER_HEARTBEAT_PERIOD时间之内,master没有binlog event发送给slave的时候,就会发送心跳数据给slave

  1. 启动从库slave进程
mysql> start slave;
Query OK, 0 rows affected (0.04 sec)
  1. 查看是否配置成功
show slave status \G;
  • Slave_IO_Running:从库的I/O线程,用来接受Master发送的binlog,并将其写入到中继日志relay log
  • Slave_SQL_Running:从库的SQL线程,用来执行从relay log读取并执行binlog
  • Slave_IO_RunningSlave_SQL_Running:这两个进程的状态需全部为 YES,只要有一个为 NO,则复制就会停止。
  • Master_Log_File:要同步的主库的binlog文件名。
  • Read_Master_Log_Pos:已同步的位置,即同步的 binlog 文件内的字节偏移量,该值会随着主从同步的进行而不断地增长。
  • Relay_Log_File:从库的中继日志文件,对接收到的主库的 binlog 进行缓冲。从库的SQL线程不断地从 relay log 中读取 binlog 并执行。
  • Relay_Log_Posrelay log 中已读取的位置偏移量。
  • Seconds_Behind_Master: 主从同步延时, 值为 0 为正常情况,正值表示已经出现延迟,数字越大从库落后主库越多。
  1. 在主库创建一个数据库、创建一张表,执行一些sql语句进行测试。

(3)可能遇到的问题

在配置mysql主从复制的时候可能出现一下错误:

Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.

原因

如果你使用了两台虚拟机,一主一从,从库的mysql是直接克隆的。在mysql 5.6的复制引入了uuid的概念,各个复制结构中的server_uuid得保证不一样,但是查看到直接克隆data文件夹后server_uuid是相同的。

解决

找到data文件夹下的auto.cnf文件,修改里面的server_uuid值,保证各个dbserver_uuid不一样,重启db即可。

cd /www/server/data

修改server_uuid的值

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_16


使用

select uuid();

生成一个uuid即可,重启数据库。

2.2 其他日志

2.2.1 通用查询日志,默认关闭

MySQL通用查询日志,它是记录建立的客户端连接和执行的所有DDLDML语句(不管是成功语句还是执行有错误的语句),默认情况下,它是不开启的。请注意,它也是一个文本文件。

可以通过以下的sql查看查询日志的状态:

show variables like 'general_log';

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_17


使用以下命令开启通用查询日志,一般不开启,这是为了测试,当然也可以修改配置文件,重启服务:

# 在全局模式下,开启通用查询日志,1表示开启,0表示关闭
SET global general_log=1;

开启后,我们随便执行sql语句之后,你会发现data目录多了以下文件:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_18


使用more命令查看该文件:

more VM-12-17-centos.log

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_19

2.2.2 慢查询日志

当前版本慢查询日志默认是开启的,有的版本是关闭的,使用如下命令查看慢查询日志的状态:

show variables like 'slow_query_log';

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_20


那么,何为慢?mysql通过一个变量‘long_query_time’来确定sql慢不慢,执行时间大于该值就会被记录在慢查询日志中,默认是10s

show variables like 'long_query_time';

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_21


以下是【慢查询日志】的记录文本:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java_22

详细的分析慢SQL可以查看我的另一篇博文——数据库优化——慢查询MySQL定位优化流程

2.2.3 错误日志

错误日志(Error Log)主要记录 MySQL 服务器启动和停止过程中的信息、服务器在运行过程中发生的故障和异常情况等。一旦发生mysql服务无法启动、程序崩溃一定要记得去查询错误日志:

show variables like 'log_error';

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_23


我们随便人为一个错误导致他无法启动,重新启动mysql命令如下:

service mysqld restart
systemctl mysqld restart

我们将inndb的系统表空间文件重命名,重新启动mysql服务,发生问题:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java_24


查询错误日志,寻找蛛丝马迹:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_25


修改回正确的名字,重新启动成功:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_26

2.3 redo log日志

接下来的两个日志,是InnoDB解决不同问题而引出的两类日志文件。

redo log(重做日志)的设计主要是为了防止因系统崩溃而导致的数据丢失,其实解决因系统崩溃而导致数据丢失的思路如下。

  1. 每次提交事务之前,必须将所有和当前事务相关的 Buffer Pool中的脏页 刷入磁盘,但是这个效率比较低,可能会影响主线程的效率,产生用户等待,降低响应速度,因为刷盘是I/O操作,同时一个事务的读写操作也不是顺序读写。
  2. 把当前事务中修改的数据内容在日志中记录下来,日志是顺序写,性能很高,其实mysql就是这么做的,这个日志被称为redo log。执行事务中,每执行一条语句,就可能有若干redo日志,并按产生的顺序写入磁盘,redo日志占用的空间非常小,当redo log空间满了以后又会从头开始以循环的方式进行覆盖式的写入。

redo log的格式比较简单,包含一下几个部分:

  1. type:该日志的类型,在5.7版本中,大概有53种不同类型的redo log,占用一个字节。
  2. space id:表空间id
  3. page number:页号。
  4. data:日志数据。

2.3.1 MTR

innodb执行任务时,有很多操作,必须具有原子性,我们把这一类操作称之为MIni Transaction,我们以下边的例子为例:

在我们向B+树中插入一条记录的时候,需要定位这条数据将要插入的数据页,因为插入的位置不同,可能会有以下情况:

  1. 待插入的页拥有充足的剩余空间,足以容纳这条数据,那就直接插入就好了,这种情况需要记录一条 MLOG_COMP_REC_INSERT类型redo日志就好了,这种情况成为乐观插入。
  2. mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_27

  3. 待插入的页剩余空间不足以容纳该条记录,这样就比较麻烦了,必须进行页分裂了。必须新建一个页面,将原始页面的数据拷贝一部分到新页面,然后插入数据。这其中对应了好几个操作,必须记录多条rede log,包括申请新的数据页、修改段、区的信息、修改各种链表信息等操作,需要记录的redo log可能就有二三十条,但是本次操作必须是一个【原子性操作】,在记录的过程中,要全部记录,要么全部失败,这种情况就被称之为一个MIni Transaction(最小事务)。
  4. mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_28

(1)MTR的按组写入

对于一个MTR操作必须是原子的,为了保证原子性,innodb使用了组的形式来记录redo 日志,在恢复时,要么这一组的的日志全部恢复,要么一条也不恢复。innodb使用一条类型为MLO_MULTI_REC_END类型的redo log作为组的结尾标志,在系统崩溃恢复时只有解析到该项日志,才认为解析到了一组完整的redo log,否则直接放弃前边解析的日志。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_29


(2)单条redolog的标识方法

有些操作只会产生一条redo loginnodb是通过【类型标识】的第一个字符来判断,这个日志是单一日志还是组日志,如下图:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_30


(3)事务、sql、MTR、redolog的关系如下

  • 一个事务包含一条或多条sql
  • 一条sql包含一个或多个MTR
  • 一个MTR包含一个或多个redo log

2.3.2 log buffer

任何可能产生大量I/O的操作,一般情况下都会设计【缓冲层】,mysql启动时也会向操作系统申请一片空间作为redo log的【缓冲区】,innodb使用一个变量buf_free来标记下一条redo log的插入位置(标记偏移量),log buffer会在合适的时机进行刷盘:

  • log buffer空间不足。logbuffer的容量由innodb_log_buffer_size指定,当写入log buffer的日志大于容量的50%,就会进行刷盘。
  • 提交事务时,如果需要实现崩溃恢复,保证数据的持久性,提交事务时必须提交redo log,当然你也可以为了效率不去提交,可以通过修改配置文件设置该项目。
  • 后台有独立线程大约每隔一秒会刷新盘一次。
  • 正常关闭服务器。
  • checkpoint时,后边会讲。

有缓冲就可能存在数据不一致,咱们接着往下看。

2.3.3 checkpoint

redolog日志文件容量是有限的,需要循环使用,redo log的作用仅仅是为了在崩溃时恢复脏页数据使用的,如果脏页已经刷到磁盘上,其对应的redo log也就没用了,他也就可以被重复利用了。checkpoint的作用就是用来标记哪些旧的redo log可以被覆盖。

我们已经知道,判断redo log占用的磁盘空间是否可以被重新利用的标志就是,对应的脏页有没有被刷新到磁盘。为了实现这个目的,我们需要了解一下下边几个记录值的作用:

(1)lsn
lsnlog sequence number)是一个全局变量。mysql在运行期间,会不断的产生redo log,日志的量会不断增加,innodb使用lsn来记录当前总计写入的日志量,lsn的初始值不是0,而是8704,原因未知。系统在记录lsn时是按照【偏移量】不断累加的。lsn的值越小说明redo log产生的越早。

每一组redo log都有一个唯一的lsn值和他对应,你可以理解为lsnredo log的年龄。

(2)flush_to_disk_lsn

flush_to_disk_lsn也是一个全局变量,表示已经刷入磁盘的redo log的量,他小于等于lsn,举个例子:

  1. redo log写入log bufferlsn增加,假如:8704+1024 = 9728,此时flush_to_disk_lsn不变。
  2. 刷如512字节到磁盘,此时flush_to_disk_lsn=8704+512=9256

如果两者数据相同,说明已经全部刷盘。

(3)flush链中的lsn

其实要保证数据不丢失,核心的工作是要将buffer pool中的脏页进行刷盘,但是刷盘工作比较损耗性能,需要独立的线程在后台静默操作。

回顾一下flush链,当第一次修改某个已经加载到buffer pool中的页面时,他会变成【脏页】,会把他放置在flush链表的头部flush链表是按照第一次修改的时间排序的。再第一次修改缓冲页时,会在【缓冲页对应的控制块】中,记录以下两个属性:

  • oldest_modification第一次修改缓冲页时,就将【修改该页面的第一组redo log的lsn值】记录在对应的控制块。
  • newest_modification每一次修改缓冲页时,就将【修改该页面的最后组redo loglsn】记录在对应的控制块。

既然flush链表是按照修改日期排序的,那么也就意味着,oldest_modification的值也是有序的。

(4)checkpoint过程

执行一个check point可以分为两个步骤

第一步:计算当前redo log文件中可以被覆盖的redo日志对应的lsn的值是多少:

  1. flush链是按照第一次修改的时间排序的,当然控制块内的【oldest_modification】记录的lsn值也是有序的。
  2. 我们找到flush链表的头节点上的【oldest_modification】所记录的lsn值,也就找到了一个可以刷盘的最大的lsn值,小于这个值的脏页,肯定已经刷入磁盘。
  3. 所有小于这个lsn值的redo log,都可以被覆盖重用。
  4. 将这个lsn值赋值给一个全局变量checkpoint_lsn,他代表可以被覆盖的量。

第二步:将checkpoint_lsn与对应的redo log日志文件组偏移量以及此次checkpoint的编号(checkpoint_no也是一个变量,记录了checkpoint的次数)全部记录在日志文件的管理信息内。

2.3.4 一个事务的执行流程

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_31


主线程

  1. 客户端访问mysql服务,在buffer pool中进行操作(如果目标页不在缓冲区,需要加载进入缓冲区),此时会形成脏页。
  2. 记录redo log,可能产生很多组日志,redo log优先记录在缓冲区,会在提交事务前刷盘。
  3. 刷盘时根据checkpoint的结果,选择可以使用的日志空间进行记录。
  4. 成功后即可返回,此时数据不会落盘,这个过程很多操作只在内存进行,只需要记录redo log(顺序写),所以速度很快。

线程1:
不断的对flush链表的脏页进行刷盘,对响应时间没有过高要求。

线程2:
不断的进行checkpoin操作,保证redo log可以及时写入。

2.3.5 系统崩溃的影响

  • log buffer中的日志丢失log buffer中的日志会在每次事务前进行刷盘,如果在事务进行中崩溃,事务本来就需要回滚。
  • buffer pool中的脏页丢失,崩溃后可以通过redo log恢复,通过checkpoint操作,我们可以确保,内存中脏页对应的记录都会在redo log日志中存在。

redo log保证了崩溃后,数据不丢失,但是一个事务进行中,如果一部分redo log已经刷盘,那么系统会将本应回滚的数据同样恢复,为了解决回滚的问题,innodb提出了undo log

2.4 undo log日志

2.4.1 概述

undo log(也叫撤销日志或者回滚日志),他的主要作用是为了实现回滚操作。同时,他是MVCC多版本控制的核心模块。undo log保存在共享表空间【ibdata1文件】中。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_32


注意8.0以后undolog有了独立的表空间:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_java_33


在讲undo log之前需要先了解行数据中的两个隐藏列:

2.4.2 事务id(DB_TRX_ID)

我们已经讲过,在innodb的行数据中,会自动添加两个隐藏列,一个是【DB_TRX_ID】,一个是【DB_ROLL_PTR】,本章节会详细介绍这两列的具体作用,如果该表中没有定义主键,也没有定义【非空唯一】列,则还会生成一个隐藏列【DB_ROW_ID】,这个我们之间也讲过,是为了生成聚簇索引使用的。

事务id是一个自增的全局变量,如果一个【事务】对任意表做了【增删改】的操作,那么innodb就会给他分配一个独一无二的事务id

DB_TRX_ID代表最近修改事务id记录创建当前记录或者最后一个修改的事务id

冷知识:

  • 事务id保存在一个全局变量【MAX_TRX_ID】上,每次事务需要分配事务id,就会从这个全局变量中获取,然后自增1
  • 该变量每次自增到256的倍数会进行一个落盘(保存在表空间页号为5的页面中),发生服务停止或者系统崩溃后,再起启动服务,会读取这个数字,然后再加256。这样做既保证不会有太多I/O操作,还能保证id的有序增长。比如:读到256进行落盘,后来有涨到302,突然崩溃了,下次启动后,第一个事务的id就是256+256=512,保证新的事务id一定大。

2.4.3 DB_ROLL_PTR

undo log在记录日志时是这样记录的,每次修改数据,都会将修改的数据标记一个【新的版本】,同时,这个版本的数据的地址会保存在修改之前的数据的roll_pointer列中,如下:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_34

可以理解为回滚指针,指向这条记录的上一个版本。

2.4.4 分类

当我们对数据库的数据进行一个操作时必须记录之前的信息,将来才能【悔棋】,如下:

  • 插入一条数据时,至少要把这条数据的主键记录下来,以后不想要了直接根据主键删除。
  • 删除一条数据时,至少要把这个数据所有的内容全部记录下来,以后才能全量恢复。但事实上不需要,每行数据都有一个delete_flag,事务中将其置1,记录id,如需要回滚根据id复原即可,提交事务后又purge线程处理垃圾。
  • 修改一条数据时,至少要将修改前后的数据都保存下来。

innodbundo log分为两类:

  • 一类日志只记录插入类型的操作(insert
  • 一类日志只记录修改类型的操作(deleteupdate

为什么分为这两类呢?

  1. 插入型的记录不需要记录版本,事务提交以后这一片空间就可以重复利用了。
  2. 修改型的必须将每次修改作为一个版本记录下来,即使当前事务已经提交,也不一定能回收空间,应为其他事务可能在用。

2.4.5 物理存储结构

undo同样是以页的形式进行存储的,多个页是使用链表的形式进行管理,针对【普通表临时表】,【插入型修改型】的数据,一个事务可能会产生以下四种链表:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_35


这是物理存储模型,分成四种类型,是为了更好的管理。

2.4.6 记录流程

  1. 开启事务,执行【增删改】时获得【事务id】。
  2. 在系统表空间中第5号页中,分配一个回滚段,回滚段是轮动分配的,比如,当前事务使用第5个回滚段,下个事务就使用第6个。

【回滚段】是一个【数据页】,里边划分了1024undo slot,用来存储日志链表的头节点地址。

  1. 在当前回滚段的cached链表(回收可复用的)和空闲solt中,找到一个可用的slot,找不到就报错。
  2. 创建或复用一个undo log页,作为first undo page,并把他的地址写入undo solt中。

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_36

2.4.7 回滚过程

  1. 服务再次启动时,通过表空间5号页面定位到128个回滚段的位置,
  2. 遍历所有的slot,找到所有状态不为空闲的slot,并且通过undolog的标记为找到现在活跃(未提交)的所有的事务id
  3. 根据undo log的记录,将数据全部回滚。

2.5 ACID靠什么保证的?

  • A原子性undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的SQL
  • C一致性由其他三大特性保证,程序代码要保证业务上的一致性。
  • I隔离性MVCC和锁来保证。
  • D持久性内存redo log来保证,mysql修改数据的同时在内存redo log记录这次操作,宕机的时候可以从redo log恢复。

InnoDB redo log写盘,InnoDB事务进入prepare状态。
如果前面prepare成功,binlog写盘,再继续将事务日志持久化到binlog,如果持久化成功,那么InnoDB事务则进入commit状态(在redo log里面写一个commit记录)。

redo log的刷盘会在系统空闲的时候进行。

2.6 InnoDB 是如何实现事务的

InnoDB通过Buffer PoolLogBufferRedo logUndo log来实现事务,以一个update语句为例:

  1. InnoDB在收到一个update语句后,会先根据条件找到数据所在的页,并把该页缓存在Buffer Pool中。
  2. 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据。
  3. 针对update语句生成一个RedoLog对象,并存入LogBuffer中。
  4. 针对update语句生成undo log日志,用于事务回滚。
  5. 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中。
  6. 如果事务回滚,则利用undo log日志进行回滚。

数据更新的流程

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据库_37


执行流程:

  1. 执行器先从引擎中找到数据,如果在内存中直接返回,如果不在内存中,查询后返回。
  2. 执行器拿到数据之后会先修改数据,然后调用引擎接口重新写入数据。
  3. 引擎将数据更新到内存,同时写数据到redo中,此时处于prepare阶段,并通知执行器执行完成,随时可以操作,
  4. 执行器生成这个操作的binlog
  5. 执行器调用引擎的事务提交接口,引擎把刚刚写完的redo改成commit状态,更新完成。

3. 隔离级别和MVCC

MVCC】,全称Multi-Version Concurrency Control,即【多版本并发控制】。MVCCMySQL InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式去处理【读-写冲突】,做到即使有【读写冲突】时,也能做到不加锁,非阻塞并发读,学习mvcc之前我们需要学习一些新的概念。

注意:
MVCC只在READ COMMITTEDREPEATABLE READ两个隔离级别下工作,其他两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE则会对所有读取的行都加锁。

3.1 Read View(读视图)

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的【当前读】和【快照读】,我们都知道undo log会记录一个事务对一条数据的所有修改,并形成版本链:

  • 当前读:像select lock in share mode(共享锁),select for updateinsertupdatedelete(排它锁),这些操作都是【当前读】,他读取的是记录的【最新版本】。读取时还要保证其他【并发事务】不能修改当前记录,会对读取的记录进行加锁。
  • 快照读:像不加锁的select操作就是快照读,即不加锁的【非阻塞读】,快照读的前提是【隔离级别不是串行级别】。串行级别下的快照读会【退化成当前读】。顾名思义,快照读读取的是【快照】,它是通过readView实现的。

3.1.1 实现原理

Read View就是事务进行【快照读】的时候产生的【读视图】(Read View),在该事务【执行快照读】的那一刻,会生成数据库系统当前的一个快照。

注意:
快照】不是说将数据库复制一份,【Read View】的主要作用是做【可见性判断】。快照的实现逻辑是通过undo的【版本链】,配合一些【参数】,比如事务id,来确定当前事务可以读取的版本。

3.1.2 readView的结构

举一个列子,当前有事务id1213141620的五个事务,他们在同时修改一条数据,此时,事务13发生读取行为,在【事务13】读取之前【事务14】已经提交,当前场景下,将产生一个readview如下:

一个readView就是一个【结构体】,你甚至可以理解成为java里的实例(readview)和属性,包含属性如下:

  • m_ids:生成该readView时,当前系统中【活跃的事务id列表,对于当前案例,因为14已经提交,就不活跃了,所以该变量的值为[12,13,16,20]
  • min_trx_id:当前系统【活跃事务】中最小的【事务id】,它也是m_ids的最小值,当前案例的值就是12
  • max_trx_id:当前系统中计划分配给下一个事务的id,他可能是m_ids的最大值+1,也可能比他大。当前案例值假设为22
  • creator_trx_id:生成这个readView的事务id,当前案例的值为12

以上readview配合undo log就可以形成一个【快照】,那他是怎么读取的呢?

3.2 快照读原理解析

在一个事务读取数据时,会根据当前数据形成一个readView,读取时会按照以下逻辑进行读取:

  • 如果【被访问数据的事务trx_id】和readView中的【creator_trx_id值】相同,意味着自己在访问自己修改过的记录,当然可以访问。
  • 如果【被访问数据的事务trx_id】小于readView中的【min_trx_id】值,说明生成这个版本数据的事务,在生成readView前已经提交,这样的数据也可以访问。

通俗一点,这个数据之前被其他的事务修改过,但是事务已经提交,所以这个版本的数据是可以使用的,这样不会产生脏读。

  • 如果【被访问数据的事务trx_id】大于readView中的【max_trx_id】值,说明生成这个版本数据的事务,是在生成readView后开启,这样的数据不应该访问。

通俗一点:你读取数据之后,有人修改了当前数据,那人家后边修改的数据,你也不能读。

  • 如果【被访问数据的事务trx_id】如果在min_trx_idmax_trx_id范围内,则需要判断是不是在【m_ids】中(目的是判断这个数据是不是已经提交),如果在,说明生成这个版本的事务还是活跃的,没有提交的事务产生的数据当然不能读,如果不在,说明事务已经提交,该数据可以被访问。

通俗一点:这个数据被现在活跃的其他事务正在修改中,读取时要看此时这个事务是不是已经提交,目的也是为了不要读取别人未提交的事务。

我们用下边的案例来看一下这个过程:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql_38

3.3 解决脏读和不可重复读

对于RU(读未提交)隔离级别的事务来说,由于可以读取到未提交的事务,所有直接读取【最新的记录】(当前读)就可以,对于serializable的事务来说,必须使用加锁的方式来访问。

3.3.1 解决脏读

先思考一个问题,脏读指的是当前事务中读取到了其他事务未提交的数据,那解决的思路是什么:

(1)没有undo+mvcc
一个事务读取了数据之后,立马给这个数据加写锁,不允许其他事务进行修改,这是加锁解决脏读。

(2)使用undo+mvcc
所有事务对数据的修改,记录成版本链,使用readview进行版本选择,每个事务只能读取满足条件的数据,这个过程不需要加锁。

使用mvcc很好的解决了【读写操作】的并发执行,而且采用了无锁机制。

3.3.1 解决不可重复读

RCRR两个隔离级别解决不可重复读是通过生成readview时间不同

(1)RC隔离级别,同一个事务中【每次读取数据时都生成一个新的ReadView】,两次读取时,如果中间有其他事务进行提交,可能会生成两个不同的readview,导致当前事务中,两次读取的数据不一致,这就是不可重复读。具体的执行流程如下:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_mysql一个数据库能建多少个表_39


(2)RR隔离级别,同一个事务中【只在第一次读取数据时生成一个ReadView】,以后这个事务中一直使用这个readview,那么同一个事务中就能保证多次读取的数据是一致的,具体的执行流程如下:

mysql一个数据库能建多少个表 mysql可以建多少个数据库_数据_40

3.3.3 解决幻读

他是通过间隙锁实现的,一旦锁定某一个范围的数据,就会对这个范围的数据加锁,间隙锁保证我们不能在这个范围内插入新的数据。

4. InnoDB 四大特性

InnoDB 四大特性分别是:

  1. 插入缓冲(insert buffer
  2. 二次写(double write
  3. 自适应哈希索引(adaptive hash index
  4. 预读(read ahead

4.1 插入缓冲(insert buffer)

索引是存储在磁盘上的,所以对于索引的操作需要涉及磁盘操作,如果我们使用自增主键,那么在插入主键索引(聚簇索引)时,只需不断追加即可,不需要磁盘的随机IO,但是如果我们使用的是普通索引,大概率是无序的,此时就涉及到磁盘的随机IO,而随机I/O的性能是比较差的(Kafka 官方数据:磁盘顺序I/O的性能是磁盘随机I/O4000~5000)。

因此,InnoDB存储引擎设计了Insert Buffer,对应非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池(Buffer pool)中,若在,则直接插入。若不在,则先放入到一个Insert Buffer对象中,然后再以一定的频率和情况进行Insert Buffer和辅助索引叶子节点的 merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

插入缓冲的使用需要满足以下两个条件:

  1. 索引是辅助索引。
  2. 索引不是唯一的。

因为在插入缓冲时,数据库不会去查找索引页来判断插入的记录的唯一性,如果去查找肯定又会有随机读取的情况发生,从而导致 Insert Buffer 失去了意义。

4.2 二次写(double write)

脏页刷盘风险InnoDBpage size一般是16kb,操作系统写文件是以4kb作为单位,那么每写一个InnoDBpage到磁盘,操作系统需要写4个块,于是可能出现16k的数据,写入4k时,发生了系统断电或系统崩溃,只要一部分写是成功的,这就是partial page write(部分页写入)问题。这时会出现数据不完整的问题。

这时是无法通过redo log恢复的,因为redo log记录的是对页的物理修改,如果页本身已经损坏,重做日志也无能为力。

double write就是用来解决该问题的,double write由两部分组成,一部分为内存中的double write buffer,其大小为2MB,另一部分是磁盘上共享表空间中连续的128个页,即2个区(extent),大小也是2M

为了解决partial page write 问题,当MySQL将脏数据刷新到磁盘的时候,会进行以下操作:

  1. 先将脏数据复制到内存中的double write buffer
  2. 之后通过double write buffer再分2次,每次1MB写入到共享表空间的磁盘上(顺序写,性能很高)。
  3. 完成第二步之后,马上调用fsync 函数,将double write buffer中的脏页数据写入实际的各个表空间文件(离散写)。

如果操作系统在将页写入磁盘的过程中发生崩溃,InnoDB再次启动后,发现了一个page数据已经损坏,InnoDB存储引擎可以从共享表空间的double write中找到该页的一个最近的副本,用于进行数据恢复了。

4.3 自适应哈希索引(adaptive hash index)

哈希(hash)是一种非常快的查找方法,一般情况下查找的时间复杂度为 O(1)。但是由于不支持范围查询等条件的限制,InnoDB 并没有采用 hash 索引,但是如果能在一些特殊场景下使用 hash 索引,则可能是一个不错的补充,而 InnoDB 正是这么做的。

具体的,InnoDB 会监控对表上索引的查找,如果观察到某些索引被频繁访问,索引成为热数据,建立哈希索引可以带来速度的提升,则建立哈希索引,所以称之为自适应(adaptive)的。自适应哈希索引通过缓冲池的 B+ 树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB 会自动根据访问的频率和模式来为某些页建立哈希索引。

4.4 预读(read ahead)

InnoDBI/O的优化上有个比较重要的特征为预读,当InnoDB预计某些page可能很快就会需要用到时,它会异步地将这些page提前读取到缓冲池(buffer pool)中,这其实有点像空间局部性的概念。

空间局部性(spatial locality):如果一个数据项被访问,那么与他地址相邻的数据项也可能很快被访问。

InnoDB使用两种预读算法来提高I/O性能:

  1. 线性预读(linear read-ahead
  2. 随机预读(randomread-ahead

其中,线性预读以 extent(块,1extent 等于64page)为单位,而随机预读放到以 extent 中的 page 为单位。线性预读着眼于将下一个extent 提前读取到 buffer pool 中,而随机预读着眼于将当前 extent 中的剩余的 page 提前读取到 buffer pool

线性预读(linear read-ahead)

线性预读(Linear read-ahead:线性预读方式有一个很重要的变量 innodb_read_ahead_threshold,可以控制 Innodb 执行预读操作的触发阈值。如果一个 extent 中的被顺序读取的 page 超过或者等于该参数变量时,Innodb将会异步的将下一个 extent 读取到 buffer pool中,innodb_read_ahead_threshold 可以设置为0-64(一个 extent 上限就是64页)的任何值,默认值为56,值越高,访问模式检查越严格。

随机预读(Random read-ahead)

随机预读(Random read-ahead: 随机预读方式则是表示当同一个 extent 中的一些 pagebuffer pool 中发现时,Innodb 会将该 extent 中的剩余 page 一并读到 buffer pool中,由于随机预读方式给 Innodb code 带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃。要启用此功能,请将配置变量设置 innodb_random_read_aheadON