MySql进阶篇---007:锁:全局锁、表级锁、行级锁,InnoDB引擎详解:逻辑存储结构、 架构、事务原理、MVCC多版本并发控制,MySQL管理:4个系统数据库、常用工具

5.锁

5.1 概述

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、 RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

MySQL中的锁,按照锁的粒度分,分为以下三类:

  • 全局锁:锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据。

5.2 全局锁

5.2.1 介绍

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML(数据操作语言)的写语句,DDL(数据定义语言)语句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

数据备份:指的就是将我们数据库当中的数据来备份成一个sql文件,备份到磁盘当中。

为什么全库逻辑备份,就需要加全就锁呢?

A.我们一起先来分析一下不加全局锁,可能存在的问题。

假设在数据库中存在这样三张表: tb_stock 库存表,tb_order 订单表,tb_orderlog 订单日志表。

在这里插入图片描述

  • 在进行数据备份时,先备份了tb_stock库存表。
  • 然后接下来,在业务系统中,执行了下单操作,扣减库存,生成订单(更新tb_stock表,插入tb_order表)。
  • 然后再执行备份 tb_order表的逻辑。
  • 业务中执行插入订单日志操作。
  • 最后,又备份了tb_orderlog表。

此时备份出来的数据,是存在问题的。因为备份出来的数据,tb_stock表与tb_order表的数据不一 致(有最新操作的订单信息,但是库存数没减)。

那如何来规避这种问题呢? 此时就可以借助于MySQL的全局锁来解决。

B.再来分析一下加了全局锁后的情况

在这里插入图片描述

对数据库进行进行逻辑备份之前,先对整个数据库加上全局锁,一旦加了全局锁之后,其他的DDL(定义语言)、 DML(增删改)全部都处于阻塞状态,但是可以执行DQL(查询)语句,也就是处于只读状态,而数据备份就是查询操作。 那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这样就保证了数据的一致性 和完整性。

加锁之后只能读不能写。

5.2.2 语法

1).加全局锁

#对当前数据库实例加上全局锁
#登录Mysql之后执行
flush tables with read lock ;

2).数据备份

#借助mysql当中提供的一个工具mysqldump 指定备份时访问数据库的用户名和密码
#itcast :你要备份的哪个数据库
#itcast.sql:把备份的数据存到哪一个sql文件当中
#此命令不能登录mysql之后执行,只能是在外面没有登录的窗口格式中执行
mysqldump -uroot –p1234 itcast > itcast.sql

数据备份的相关指令, 在后面MySQL管理章节, 还会详细讲解.

3). 释放全局锁

#登录Mysql之后执行
unlock tables ;

5.2.3 演示:通过全局锁实现数据备份

使用sqlyog打开本地安装在windows系统上的数据库
在这里插入图片描述

准本数据库

在这里插入图片描述

准备数据:学生表 课程表 多对多关系所以还有一个中间表学生课程表

create table student(
    id   int auto_increment comment '主键ID' primary key,
    name varchar(10) null comment '姓名',
    no   varchar(10) null comment '学号'
)comment '学生表';

INSERT INTO student (name, no) VALUES ('黛绮丝', '2000100101');
INSERT INTO student (name, no) VALUES ('谢逊', '2000100102');
INSERT INTO student (name, no) VALUES ('殷天正', '2000100103');
INSERT INTO student (name, no) VALUES ('韦一笑', '2000100104');



create table course(
    id int auto_increment comment '主键ID' primary key,
    name varchar(10) null comment '课程名称'
)comment '课程表';


INSERT INTO course (name) VALUES ('Java');
INSERT INTO course (name) VALUES ('PHP');
INSERT INTO course (name) VALUES ('MySQL');
INSERT INTO course (name) VALUES ('Hadoop');




create table student_course(
    id int auto_increment comment '主键' primary key,
    studentid int not null comment '学生ID',
    courseid  int not null comment '课程ID',
    constraint fk_courseid foreign key (courseid) references course (id),
    constraint fk_studentid foreign key (studentid) references student (id)
)comment '学生课程中间表';

INSERT INTO student_course (studentid, courseid) VALUES (1, 1);
INSERT INTO student_course (studentid, courseid) VALUES (1, 2);
INSERT INTO student_course (studentid, courseid) VALUES (1, 3);
INSERT INTO student_course (studentid, courseid) VALUES (2, 2);
INSERT INTO student_course (studentid, courseid) VALUES (2, 3);
INSERT INTO student_course (studentid, courseid) VALUES (3, 4);

在这里插入图片描述

打开3个Dos窗口:模拟3个会话
第一个窗口:在备份之前加上全局锁

#先登录上mysql
mysql -uroot -proot

#加上全局锁
flush tables with read lock ;

在这里插入图片描述
第二个窗口:加了全局锁之后在其他的客户端,只能读不能写

mysql -uroot -proot

use db01;

#可以查询
select * from student;

#不可以更新:此时光标一直处于阻塞状态
update student set name ='A' where id = 2;

在这里插入图片描述
第三个窗口:数据备份

#mysqldump是mysql提供的一个工具不是sql语句,所以不能再mysql的命令行中执行,需要在windows命令行中执行。
#如果访问的不是本机数据库 还要加上-h指定主机地址
mysqldump -uroot –proot db01> D:/db01.sql

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在第一个窗口中释放锁
在这里插入图片描述

unlock tables ;

在第二个窗口,此时再次执行更新语句就可以更新了

update student set name ='A' where id = 2;

在这里插入图片描述

5.2.4 特点

数据库中加全局锁,是一个比较重的操作,存在以下问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
  • 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。
    • 如果业务当中的数据库是主从结构并且做了读写分离,这个时候的写入操作是不会进行阻塞的,因为写入主库我们可以从 从库当中进行备份,但是从从库当中进行备份又会存在一个新的问题:在备份期间从库是不能够执行从主库同步过来的二进制日志的,这将会导致主从延迟。

在InnoDB引擎中,我们可以在备份时加上参数--single-transaction参数来完成不加锁,来保证数据的一致性备份。

含义:加上这个参数实际上就是在Mysql InnoDB引擎的底层,它实际上是通过快照读来进行实现的。

#在备份的时候加上--single-transaction这个参数
mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql

注意:这种方式不用加全局锁

5.2.5 演示:通过快照备份数据

第一步:准备数据, 还是用之前的db01数据库。

第二步:打开一个新的dos窗口,输入命令

mysqldump --single-transaction -uroot –proot db01> D:/db02.sql

在这里插入图片描述

在这里插入图片描述

5.3 表级锁

5.3.1 介绍

表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。

对于表级锁,主要分为以下三类:

  • 表锁
  • 元数据锁(meta data lock,MDL)
  • 意向锁

5.3.2 表锁

对于表锁,分为两类:

  • 表共享读锁(read lock) :读锁
  • 表独占写锁(write lock):写锁

语法:

  • 加锁:lock tables 表名… read/write
    • 一次性可以锁定多张表
    • 加的是读锁还是写锁
  • 释放锁:unlock tables / 客户端断开连接 。

特点:

A.读锁
在这里插入图片描述
流程:客户端1对这张表加了读锁,此时客户端1可以读取这张表的数据,客户端1不能对这张表写的操作。客户端2是可以读取这张表数据的,但是他也不能够写。当客户端1把这一块的业务逻辑执行完之后,通过unlock tables语句释放表锁。

左侧为客户端一,对指定表加了读锁,只能够读不能写,不会影响右侧客户端二的读,但是会阻塞右侧客户端的写。

测试:

student表结构:
在这里插入图片描述

打开2个会话窗口,都已经登录上了mysql并且都切换到了db01这个数据库。

第一个会话窗口

mysql -uroot -proot

use db01;

#加上读锁
lock tables student read;

#读取是没有问题的
select * from student ;

#写操作不能够执行
update student set name = "bb" where id = 2;

#释放锁
unlock tables;

在这里插入图片描述

第二个会话窗口

mysql -uroot -proot

use db01;

#读取是没有问题的
select * from student ;

#写操一直作处于阻塞状态,一旦第一个会话窗口释放了锁就可以执行成功了
update student set name = "bb" where id = 2;

在这里插入图片描述

B.写锁

在这里插入图片描述

流程:客户端1对这张表加了写锁,客户端1既可以读也可以写,而客户端2不能读也不能写,之后通过unlock tables释放锁。

左侧为客户端一,对指定表加了写锁 既能读也能写,会阻塞右侧客户端二的读和写。

测试:

第一个窗口

mysql -uroot -proot

use db01;

#加上写锁
lock tables student write;

# 可以读
select * from student ;

#可以写
update student set name = "小米" where id = 4;

#释放锁
unlock tables;

在这里插入图片描述

第二个窗口

mysql -uroot -proot

use db01;


# 不可以读
select * from student ;

#不可以写,一直处于阻塞状态,一旦客户端1的窗口释放了锁就可以执行了。
update student set name = "小米" where id = 4;

在这里插入图片描述

结论: 读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。

5.3.3 元数据锁

meta data lock , 元数据锁,简写MDL。

MDL加锁过程是系统自动控制,无需显式使用lock这样的关键字手动加锁,在访问一张表的时候会自动加上元数据数锁。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML(crud)与DDL(数据定义语言:创建表)冲突,保证读写的正确性

这里的元数据,大家可以简单理解为就是一张表的表结构。 也就是说,某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的。

在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变 更操作的时候,加MDL写锁(排他)。

  • 读锁之间是可以兼容的,但是写锁之间、写锁和读锁之间是互斥的。

常见的SQL操作时,所添加的元数据锁:

对应SQL 锁类型 说明
lock tables xxx read / write (对某一张表加上读锁或写锁) SHARED_READ_ONLY / SHARED_NO_READ_WRITE(它会加上对应的元数据锁)
select 、select … lock in share mode (执行查询或者加上一把共享锁) SHARED_READ(它会自动的加上元数据锁中的读锁/共享锁) 与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥
insert 、update、delete、select … for update SHARED_WRITE (它会自动的加上元数据锁中的写锁,实际上它也是元数据锁中的读锁/共享锁) 与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥
alter table … (意味着要修改表结构了) EXCLUSIVE(它会自动的加上元数据锁中的写锁/排他锁) 与其他的MDL都互斥
  • SHARED_READ和SHARED_WRITE共享锁之间相互兼容
  • SHARED_READ、SHARED_WRITE和EXCLUSIVE排他锁之间是互斥的

演示:

当执行SELECT、INSERT、UPDATE、DELETE等语句时,添加的是元数据共享锁(SHARED_READ / SHARED_WRITE),共享锁之间是兼容的,所以2个客户端怎么执行都可以。

即:客户端1此时是共享锁,客户端2此时也是共享锁,共享锁之间是兼容的,所以2个客户端怎么执行都可以。

客户端1:

mysql -uroot -proot

use db01;

#开启事务
begin;

#查询操作
select * from student;

#提交事务
commit;

客户端2:

mysql -uroot -proot

use db01;

#开启事务
begin;

#查询操作
select * from student;

#提交事务
commit;

在这里插入图片描述

当执行SELECT语句时,添加的是元数据共享锁(SHARED_READ),会阻塞元数据排他锁(EXCLUSIVE),之间是互斥的。

即:客户端1此时是共享锁,客户端2此时是排他锁,共享锁和排他锁之间是互斥的,所以客户端2处于阻塞状态。直到客户端1提交事务此时元数据锁就释放了,这样客户端2就可以更新表结构了。

客户端1

#开启事务
begin;

#查询操作
select * from student;

#提交事务
commit;

在这里插入图片描述

客户端2

#开启事务
begin;

#修改表结构:往表中新添加一个字段,处于阻塞状态,直到客户端1提交事务此时元数据锁就释放了,
#          这样客户端2就可以更新表结构了。
alter table student add column java int;

在这里插入图片描述

我们可以通过下面的SQL,来查看数据库中的元数据锁的情况:
注意:

  • 适用于mysql8以上的版本,mysql5的版本没有这个metadata_locks这张表
  • 如果提交事务就没有元数据锁了,没有提交事务查询时才可以看到元数据锁。
#查看当前数据库表当中,所涉及到的元数据锁。
#这条sql语句实际上查询的是我们系统表当中的metadata_locks这张表,在这张表当中
#    就记录了我们当前数据库实例当中的元数据锁。
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks ;

我们在操作过程中,可以通过上述的SQL语句,来查看元数据锁的加锁情况。

在这里插入图片描述

5.3.4 意向锁

1)介绍

为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查,从而提高性能

假如没有意向锁,客户端一对表加了行锁后,客户端二如何给表加表锁呢,来通过示意图简单分析一 下:

首先客户端一,开启一个事务,然后执行DML操作,在执行DML语句时,会对涉及到的行加行锁

在这里插入图片描述

当客户端二,执行并发操作想对这张表加表锁时,不能对这张表直接加表锁,因为已经对表中的一行加上了行锁此时在对表加上表锁时,会造成行锁和表锁冲突了。所以客户端2在加表锁时,首先会检查当前表是否有对应的行锁,此时就会从第一行数据,检查到最后一行数据有没有行锁,效率较低,如果没有,则添加表锁。

为了解决在执行DML语句时,行锁与表锁的冲突,在InnoDB中引入了意向锁,这样表锁在检查的时候不用去检查每一行数据是否加锁,使用意向锁来减少表锁的检查,从而提高性能。

在这里插入图片描述

有了意向锁之后 :

客户端一,在执行DML操作时,会对涉及的行加行锁,同时也会对该表加上意向锁

在这里插入图片描述

而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁(如果这张表的意向锁和当前所加的这个表锁是兼容的,那么直接加锁。如果这张表的意向锁和当前所加的这个表锁是不兼容的,那么就会一直处于阻塞状态,直到客户端1进行了事务提交把行锁释放、意向锁释放之后,那么它就会解除阻塞状态拿到这张表的表锁),这样就不用逐行判断行锁情况了。

在这里插入图片描述

2)分类
  • 意向共享锁(IS):执行select … lock in share mode语句时自动添加 。
    • 与表锁共享锁(read)兼容,与表锁排他锁(write)互斥。
  • 意向排他锁(IX):执行insert、update、delete、select…for update语句时自动添加 。
    • 与表锁共享锁(read)及排他锁(write)都互斥,意向锁之间不会互斥。

一旦事务提交了,意向共享锁、意向排他锁,都会自动释放。

可以通过以下SQL,查看意向锁及行锁的加锁情况:

#操作的是data_locks表
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
3)演示:

A.意向共享锁与表锁的兼容情况

客户端1:

begin;

#在select 语句后面加上lock in share mode表示,它会加上这一行行锁的共享锁,
#  同时为student 这张表加上意向共享锁。

select * from student where id=1 lock in share mode;

commit;

在这里插入图片描述

客户端2:

#查看这张表的行锁和意向锁是否加上
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

#为student这张表加上读锁
# 成功:因为意向共享锁与读锁兼容
lock tables student read;


#为student这张表加上写锁
# 阻塞状态:因为意向共享锁与读锁互斥,如果客户端1提交事务就会释放行锁和表锁,这样表锁的写锁也就添加成功了
lock tables student write;

在这里插入图片描述

B.意向排他锁与表读锁、写锁都是互斥的

客户端1:

begin;

#在执行update语句时会自动为这一行加上行锁,与此同时为这张表加上意向排他锁
update student set name="ccc" where id =2 ;

commit;

客户端2:

#查看这张表的行锁和意向锁是否加上
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

#为student这张表加上读锁
# 阻塞:因为意向排他锁与读锁互斥,如果客户端1提交事务就会释放行锁和表锁,这样表锁的读锁也就添加成功了
lock tables student read;

在这里插入图片描述

5.4 行级锁

5.4.1 介绍

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。

InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:

InnoDB的数据是基于索引组织的:索引的结果b+tree结构,InnoDB存储引擎的分类 聚集索引和二级索引,聚集索引的叶子节点挂的是行数据,二级索引的叶子节点挂的是主键,我们的行数据是基于聚集索引来存储的,所以叫做InnoDB的数据是基于索引组织的。

  • 行锁 /记录锁(Record Lock)锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC(Read committed:不支持脏读 、支持不可重复读、 支持幻读)、RR(Repeatable Read:不支持脏读 、不支持不可重复读、 支持幻读)隔离级别下都支持。
    在这里插入图片描述

  • 间隙锁(Gap Lock)锁定索引记录的间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。间隙:2个记录之间的范围,6到12之间的间隙,16到18之间的间隙。
    在这里插入图片描述

  • 临键锁(Next-Key Lock)行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。eg:在34处加上临键锁,它会锁住34这行记录并且锁住34记录前的这个间隙
    在这里插入图片描述

5.4.2 行锁

1).介绍

InnoDB实现了以下两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
    • 即:共享锁和共享锁主键是兼容的,共享锁和排它锁之间是互斥的。
  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
    • 即:如果这个事物他获取到了这一行数据的排他锁,那么此时可以更新数据。假如第一个事物获取到了某一行数据的排他锁,那么其他的事物就不能再获取这一行数据的共享锁和排他锁。

两种行锁的兼容情况如下:

  • 共享锁和共享锁之间:兼容
    • 事务a获取了一个共享锁,事务b也可以继续获取这行数据的共享锁。
  • 共享锁和排他锁之间:冲突
    • 事务a获取到了这行数据的共享锁,那么事务b是获取不到这行数据的排他锁。
  • 排他锁和排他锁之间:冲突

在这里插入图片描述

常见的SQL语句,在执行时,所加的行锁如下:

SQL 行锁类型 说明
INSERT … 排他锁 自动加锁
UPDATE … 排他锁 自动加锁
DELETE … 排他锁 自动加锁
SELECT(正常) 不加任何锁
SELECT … LOCK IN SHARE MODE 共享锁 需要手动在SELECT之后加LOCK IN SHARE MODE
SELECT … FOR UPDATE 排他锁 需要手动在SELECT之后加FOR UPDATE

2).演示
默认情况下,InnoDB在 REPEATABLE READ默认事务隔离级别下运行,InnoDB使用 next-key临键锁进行搜索和索引扫描,以防止幻读。

  • 情况1:针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会把临键锁自动优化为行锁。
    • 也就是说:SELECT语句要满足唯一索引、等值匹配,并且加上LOCK IN SHARE MODE、FOR UPDATE才会准华为对应的行级共享锁、行级排他锁。
    • 如果SELECT 语句只有LOCK IN SHARE MODE、FOR UPDATE这2个关键词而没有满足唯一索引、等值匹配时,他也不是行锁。
  • 情况2:InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁(也就是表锁),此时就会升级为表锁

可以通过以下SQL,查看意向锁及行锁的加锁情况:

select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;

示例演示

数据准备:这张表只有一个主键索引

CREATE TABLE `stu` (
	`id` int NOT NULL PRIMARY KEY AUTO_INCREMENT,
	`name` varchar(255) DEFAULT NULL,
	`age` int NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;


INSERT INTO `stu` VALUES (1, 'tom', 1);
INSERT INTO `stu` VALUES (3, 'cat', 3);
INSERT INTO `stu` VALUES (8, 'rose', 8);
INSERT INTO `stu` VALUES (11, 'jetty', 11);
INSERT INTO `stu` VALUES (19, 'lily', 19);
INSERT INTO `stu` VALUES (25, 'luci', 25);

在这里插入图片描述

演示行锁的时候,我们就通过上面这张表来演示一下。

A.普通的select语句,执行时,不会加锁。

客户端1:
mysql -uroot -p1234

use db01;

begin;

#简单地select语句不会加任何行锁
select * from stu where id = 1;



客户端2#通过以下SQL,查看意向锁及行锁的加锁情况
#查询结果为空,说明当前没有加锁
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

#既然都没有加锁,此时在开启一个事务去查询相同的stu表当中id为1的数据
#可以查询,因为这条语句不会加锁
begin;

#简单地select语句不会加任何行锁
select * from stu where id = 1;

在这里插入图片描述

B.select…lock in share mode,加共享锁,共享锁与共享锁之间兼容。

客户端1#对id为1的这条记录,加上共享锁
select * from stu where id = 1 LOCK IN SHARE MODE;



客户端2#此时会有2个锁,不看这个TABLE IS意向共享锁,看的是下面这个行锁共享锁 没有间隙
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;


#再次执行相同的sql,发现执行成功:共享锁和共享锁之间是兼容的
select * from stu where id = 1 LOCK IN SHARE MODE;

#再次查询锁情况发现:id为1的这条记录加了2个共享锁
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;


客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述

共享锁与排他锁之间互斥。

客户端2commit;

begin;

#提交事务就会释放掉客户端2的共享锁,此时开启一个新的事物查看就只剩下客户端1的共享锁了(id为1的数据)
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

#共享锁与排他锁之间互斥
#给id=3的记录添加排它锁,成功,因为客户端1是给id=1的记录添加的共享锁
update stu set name = "java" where id = 3;

#给id=1的记录添加排他锁,处于阻塞状态
update stu set name = "java" where id = 1;


最后:演示完成后把客户端1和客户端2的事务都进行提交,此时id=1的这条记录的共享锁和排它锁都释放了
commit;

在这里插入图片描述

客户端一获取的是id为1这行的共享锁,客户端二是可以获取id为3这行的排它锁的,因为不是同一行 数据。 而如果客户端二想获取id为1这行的排他锁,会处于阻塞状态,以为共享锁与排他锁之间互斥。

C.排它锁与排他锁之间互斥

客户端1begin;

#添加排它锁
update stu set name = "web" where id = 1;


客户端2begin;

#添加排它锁排它锁和排它锁之间互斥,所以处于阻塞状态,直到客户端1提交了事务释放了这行记录的排他锁
#  此时客户端2的这个语句就可以执行了。
update stu set name = "web" where id = 1;

客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述

当客户端一,执行update语句,会为id为1的记录加排他锁; 客户端二,如果也执行update语句更新id为1的数据,也要为id为1的数据加排他锁,但是客户端二会处于阻塞状态,因为排他锁之间是互斥的。 直到客户端一,把事务提交了,才会把这一行的行锁释放,此时客户端二,解除阻塞。

D.无索引行锁升级为表锁

stu表中数据如下:

select * from stu;

在这里插入图片描述

我们在两个客户端中执行如下操作:

客户端1begin;

#更新正常情况下添加的是行锁排它锁,但是name字段没有索引所以会升级为表锁
update stu set name = "xiaohei" where name= "lily";


客户端2begin;

#按理说此时操作不同的记录添加行锁排它锁是成功的,但是由于升级为表锁所以处于阻塞状态。
#  直到客户端1提交事务释放表锁,此时就可以执行成功了。
update stu set name = "xiaohei" where id= 1;

客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述

在客户端一中,开启事务,并执行update语句,更新name为Lily的数据,也就是id为19的记录 。然后在客户端二中更新id为3的记录,却不能直接执行,会处于阻塞状态,为什么呢?

原因就是因为此时,客户端一,根据name字段进行更新时,name字段是没有索引的,如果没有索引, 此时行锁会升级为表锁(因为行锁是对索引项加的锁,而name没有索引)。

接下来,我们再针对name字段建立索引,索引建立之后,再次做一个测试:

客户端1#给name字段创建索引
CREATE INDEX idx_stu ON stu(name);

begin;

#更新:添加的是行锁排它锁
update stu set name = "xiaohei" where name= "lily";


客户端2begin;

#操作不同的记录添加行锁排它锁是成功的
update stu set name = "xiaohei" where id= 1;

客户端1:
在这里插入图片描述

客户端2:
在这里插入图片描述

此时我们可以看到,客户端一,开启事务,然后依然是根据name进行更新。而客户端二,在更新id为3 的数据时,更新成功,并未进入阻塞状态。 这样就说明,我们根据索引字段进行更新操作,就可以避免行锁升级为表锁的情况。

5.4.3 间隙锁&临键锁

默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key临键锁进行搜索和索引扫描,以防止幻读

  • 索引上的等值查询(并且这个索引是:唯一索引),给不存在的记录加锁时,优化为间隙锁 。
    • 即:要在唯一索引上进行等值查询,主键索引就是唯一索引。
  • 索引上的等值查询(并且这个索引是:非唯一普通索引,即二级索引),向右遍历时最后一个值不满足查询需求时,next-key lock 临键锁 退化为间隙锁。
  • 索引上的范围查询(并且这个索引是:唯一索引)----会访问到不满足条件的第一个值为止,会加上临键锁。

注意:

  • 间隙锁唯一目的是防止其他事务插入间隙,造成幻读现象。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
  • 对于间隙锁和临键锁也不需要区记,只需要针对于某一条sql可以分析出他为什么要加这个锁就可以了。

示例演示

当前表的记录:

在这里插入图片描述

A.索引上的等值查询(并且这个索引是唯一索引),给不存在的记录加锁时,优化为间隙锁 。

客户端1begin;

#正常情况是行级排他锁
#id是主键即唯一索引、做等值查询、id为5的这条记录不存在,所以此时在2和8之间添加的是间隙锁
update stu set age= 10 where id= 5;


客户端2#RECORD:行级锁  x:排它锁  GAP:间隙锁  8:代表锁的是8之前的间隙,不包含3和8这2条记录,锁的只是3和8之间的间隙
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

begin;

#插入id为7的记录,处于阻塞状态:因为间隙锁锁住了3和8之间的间隙,直到客户端1提交了事务释放了间隙锁,此时
#    插入操作的行级排他锁就可以执行成功了。
insert into stu values(7,"xiaogang",7);

客户端1:
在这里插入图片描述

客户端2:
在这里插入图片描述

B.索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 临键锁在这里插入代码片退化为间隙锁。

介绍分析一下:

我们知道InnoDB的B+树索引,叶子节点是有序的双向链表。 假如,我们要根据这个二级索引查询值为18的数据,并加上共享锁,我们是只锁定18这一行就可以了吗?

并不是,18之前其它的事务有可能在插入一个字段值为18的记录,18之后的其他事物有可能在插入一个字段值为18的记录,因为是非唯一索引,这个结构中可能有多个18的存在,所以,在加锁时会继续往后找,找到一个不满足条件的值(当前案例中也就是29)。此时会对18加临键锁(锁住18记录,以及18之前的间隙),并对29之前的间隙加锁(锁住29之前的间隙)。

在这里插入图片描述

客户端1:
#给stu表的age字段加上二级索引
CREATE INDEX idx_stu_age ON stu(age);

begin;

#加上行级共享锁:满足非唯一索引、等值查询---》共享锁会转化为间隙锁
select * from stu where age =3 LOCK IN SHARE MODE;


客户端2#3:对现有的这条记录加上行锁
#3,3:临建锁,把3这条记录以及3之前的间隙锁住
#7,7:把3和7之间的间隙锁住
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

客户端1:
在这里插入图片描述
客户端2:

在这里插入图片描述

C.索引上的范围查询(唯一索引)----会访问到不满足条件的第一个值为止,会加上临键锁。

客户端1:
begin;

#加上行级共享锁:满足唯一索引、范围查询---》共享锁会转化为临建锁
select * from stu where id>=19 LOCK IN SHARE MODE;


客户端2#19:对19记录加上行锁
#25:临键锁,锁的是25以及25之间的间隙
#supremum pseu:正无穷,在25到正无穷之间加了个临键锁
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;

在这里插入图片描述

查询的条件为id>=19,并添加共享锁。 此时我们可以根据数据库表中现有的数据,将数据分为三个部分:

  • [19]

  • (19,25]

  • (25,+∞]

所以数据库数据在加锁是,就是将19加了行锁,25的临键锁(包含25及25之前的间隙),正无穷的临键锁(正无穷及之前的间隙)。

5.5 总结

在这里插入图片描述

6.InnoDB引擎

6.1 逻辑存储结构

InnoDB的逻辑存储结构如下图所示:
在这里插入图片描述

1).表空间

表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。

2).段

段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点, 索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。

3).区

区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。

4).页

页,是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性, InnoDB 存储引擎每次从磁盘申请 4-5 个区。

5).行

行,InnoDB 存储引擎数据是按行进行存放的。

在行中,默认有两个隐藏字段:

  • Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
  • Roll_pointer:每次对某条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个 隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

6.2 架构

6.2.1 概述

MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

在这里插入图片描述

6.2.2 内存结构

在这里插入图片描述

在左侧的内存结构中,主要分为这么四大块儿: Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer。 接下来介绍一下这四个部分。

1).Buffer Pool

InnoDB存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能 弥补这两者之间的I/O效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁 盘I/O。

在InnoDB的缓冲池中不仅缓存了索引页和数据页,还包含了undo页、插入缓存、自适应哈希索引以及InnoDB的锁信息等等。

缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频 率刷新到磁盘,从而减少磁盘IO,加快处理速度。

缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:

  • free page:空闲page,未被使用。
  • clean page:被使用page,数据没有被修改过。
  • dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。

在专用服务器上,通常将多达80%的物理内存分配给缓冲池 ,以此来提高mysql的执行效率 。
参数设置: show variables like ‘innodb_buffer_pool_size’;

在这里插入图片描述

2).Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。Change Buffer的意义是什么呢?

先来看一幅图,这个是二级索引的结构图:

在这里插入图片描述

与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新 可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

3).Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在 进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需 要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。

InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度, 则建立hash索引,称之为自适应hash索引。

自适应哈希索引,无需人工干预,是系统根据情况自动完成。

参数: adaptive_hash_index,默认是开启的on
在这里插入图片描述

4).Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O。

参数:
innodb_log_buffer_size:缓冲区大小(默认16777216)
在这里插入图片描述

innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个: (默认是1)

  • 1: 日志在每次事务提交时写入并刷新到磁盘,默认值。
  • 0: 每秒将日志写入并刷新到磁盘一次。
  • 2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。

在这里插入图片描述

6.2.3 磁盘结构

接下来,再来看看InnoDB体系结构的右边部分,也就是磁盘结构:
在这里插入图片描述

1).System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建 的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)

参数:innodb_data_file_path
在这里插入图片描述

系统表空间,默认的文件名叫 ibdata1。

2).File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。

开关参数:innodb_file_per_table ,该参数默认开启。

在这里插入图片描述

那也就是说,我们每创建一个表,都会产生一个表空间文件,如图:

在这里插入图片描述

3).General Tablespaces

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。(没有创建默认是没有的)

A.创建表空间

#指定表空间的名字  指定表空间关联的表空间文件  指定存储引擎
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;

在这里插入图片描述

B.创建表时指定表空间

# 需要先选择数据库,才能执行这条语句
CREATE TABLE xxx ... TABLESPACE ts_name;

在这里插入图片描述

4).Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。

5).Temporary Tablespaces

InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

6).Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

在这里插入图片描述

7).Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。

以循环方式写入重做日志文件,涉及两个文件:

在这里插入图片描述

前面我们介绍了InnoDB的内存结构,以及磁盘结构,那么内存中我们所更新的数据,又是如何刷新到磁盘中的呢? 此时,就涉及到一组后台线程,接下来,就来介绍一些InnoDB中涉及到的后台线程。

在这里插入图片描述

6.2.4 后台线程

在这里插入图片描述

在InnoDB的后台线程中,分为4类,分别是:Master Thread 、IO Thread、Purge Thread、Page Cleaner Thread。

1).Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性, 还包括脏页的刷新、合并插入缓存、undo页的回收 。

2).IO Thread

在InnoDB存储引擎中大量使用了AIO(异步非阻塞)来处理IO请求, 这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调。

线程类型 默认个数 职责
Read thread 4 负责读操作
Write thread 4 负责写操作
Log thread 1 负责将日志缓冲区刷新到磁盘
Insert buffer thread 1 负责将写缓冲区内容刷新到磁盘

我们可以通过以下的这条指令,查看到InnoDB的状态信息,其中就包含IO Thread信息。

#登录mysql后执行
show engine innodb status;

在这里插入图片描述

3).Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收。

4).Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞。

6.3 事务原理(重点)

6.3.1 事务基础

1).事务

事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

2).特性

  • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态(约束前后也要保证一致)。
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环 境下运行。
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
    • 只要事务提交了,数据就必须要持久化的保存到磁盘当中。

那实际上,我们研究事务的原理,就是研究MySQL的InnoDB引擎是如何保证事务的这四大特性的。
在这里插入图片描述

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由InnoDB中的两份日志来保证的,一份是redo log日志,一份是undo log日志。 而隔离性是通过数据库的锁, 加上MVCC多版本并发控制来保证的。
在这里插入图片描述

我们在讲解事务原理的时候,主要就是来研究一下redolog,undolog以及MVCC。

锁已经学过了。

6.3.2 redo log 重做日志

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用,从而保证持久性。

如果没有redolog,可能会存在什么问题的? 我们一起来分析一下。

我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中 的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却 没有持久化下来,这就出现问题了,没有保证事务的持久性。

在这里插入图片描述
那么,如何解决上述的问题呢? 在InnoDB中提供了一份日志 redo log,接下来我们再来分析一下,通过redolog如何解决这个问题。

在这里插入图片描述

有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘或者涉及到的数据已经落盘,此时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。

那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新到磁盘呢 ?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这种先写日志的方式,称之为 WAL(Write-Ahead Logging)。

6.3.3 undo log 回滚日志

回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和MVCC(多版本并发控制) 。

undo log和redo log记录物理日志(主要记录数据页里面的内容是什么样的)不一样,它是逻辑日志(主要记录是每一步执行什么样的操作)。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录(执行update之前这一行数据长什么样子)。当执行rollback时,就可以从undo log中的逻辑记录读取到变更之前的样子,之后把数据回滚到变更之前的样子,这样就保证了事务的原子性。

Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。

Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment回滚段中,内部包含1024个undo log segment。

6.4 MVCC多版本并发控制(重点)

6.4.1 基本概念

1)当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(行级共享锁),select … for update、update、insert、delete(行级排他锁)都是一种当前读。

测试:

客户端1begin;

select * from stu;

#没有任何锁
#客户端2执行更新语句并且提交事务后,客户端1也不能查询到最新的数据(不能读取客户端2更新的数据)
#因为当前默认的隔离级别是可重复读读:在这个事物持续期间可以重复多次读取此字段,不允许其它事务更新此字段。
#当前select 语句不是当前读
select * from stu;

#加上行级共享锁,此时就可以读取到最新的数据(可以读取到客户端2更新的数据)
#因为客户端2提交了事务所以不存在共享锁和排他锁阻塞现象
#这就是当前读
select * from stu lock in share mode;

客户端2begin;

# 添加行级排他锁
update stu set name = 'Jsp' where id = 1;

commit;

在这里插入图片描述

在测试中我们可以看到,即使是在默认的RR隔离级别下,事务A中依然可以读取到事务B最新提交的内 容,因为在查询语句后面加上了 lock in share mode 共享锁,此时是当前读操作。当然,当我们加排他锁的时候,也是当前读操作。

2)快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据, 不加锁,是非阻塞读。

  • Read Committed读已提交数据:每次select,都生成一个快照读。
  • Repeatable Read可重复读(默认):开启事务后第一个select语句才是快照读的地方。后续执行的select语句实际上直接查的就是前面产生的这个快照数据
  • Serializable串行化:快照读会退化为当前读,每一次读取操作都会加锁。

测试:

客户端1begin;

select * from stu;

#没有任何锁
#客户端2执行更新语句并且提交事务后,客户端1也不能查询到最新的数据(不能读取客户端2更新的数据)
#因为当前默认的隔离级别是可重复读读:在这个事物持续期间可以重复多次读取此字段,不允许其它事务更新此字段。
#当前select 语句不是当前读,是快照读:即使客户端2提交了事务,客户端1也读取不到最新的数据,读取的是历史版本
select * from stu;


客户端2begin;

# 添加行级排他锁
update stu set name = 'Jsp' where id = 1;

commit;

在这里插入图片描述

在测试中,我们看到即使事务B提交了数据,事务A中也查询不到。 原因就是因为普通的select是快照读,而在当前默认的RR隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同 的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。

3)MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本, 使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需 要依赖于数据库记录中的三个隐式字段、undo log日志、readView

接下来,我们再来介绍一下InnoDB引擎的表中涉及到的隐藏字段 、undolog 以及 readview,从而来介绍一下MVCC的原理。

6.4.2 隐藏字段

6.4.2.1 介绍

在这里插入图片描述

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

隐藏字段 含义
DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

解释:

  • DB_TRX_ID:最近修改这一行记录它的事务id,当我们进行记录的插入或者最后一次修改该的时候,那么存储引擎会自动的为 DB_TRX_ID去赋值。
  • DB_ROW_ID:隐藏主键,这个字段并不是每一张表都会生成,当某一张表没有主键时会自动的生成这个隐藏字段作为隐藏主键,如果这张表有主键那么这个隐藏字段不会出现。

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键, 如果有主键,则不会添加该隐藏字段。

6.4.2.2 测试

1).查看有主键的表 stu

进入服务器中的 /var/lib/mysql/itcast/ , 查看stu的表结构信息, 通过如下指令:

#mysql的数据存储在此目录下
cd  /var/lib/mysql/

#查看的是itcast数据库下的表,所以就切换到itcast,里面是一个个的xxx.ibd独立表空间文件
cd /itcast/ 

#ibd文件是二进制文件不能直接打开,借助此命令进行打开
#stu.ibd:想要打开的ibd文件名
ibd2sdi stu.ibd

查看到的表结构信息中,有一栏 columns,在其中我们会看到处理我们建表时指定的字段以外,还有额外的两个字段 分别是:DB_TRX_ID 、 DB_ROLL_PTR ,因为该表有主键,所以没有DB_ROW_ID 隐藏字段。

在这里插入图片描述
在这里插入图片描述

2).查看没有主键的表 employee

建表语句:

#这个表是在itcast数据库下创建的,登录mysql后执行
create table employee (id int , name varchar(10));

此时,我们再通过以下指令来查看表结构及其其中的字段信息:

#mysql的数据存储在此目录下
cd  /var/lib/mysql/


#查看的是itcast数据库下的表,所以就切换到itcast,里面是一个个的xxx.ibd独立表空间文件
cd /itcast/ 

#ibd文件是二进制文件不能直接打开,借助此命令进行打开
#employee.ibd:想要打开的ibd文件名
ibd2sdi employee.ibd

查看到的表结构信息中,有一栏 columns,在其中我们会看到处理我们建表时指定的字段以外,还有额外的三个字段 分别是:DB_TRX_ID 、 DB_ROLL_PTR 、DB_ROW_ID,因为employee表是没有指定主键的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.4.3 undo log 回滚日志

6.4.3.1 介绍

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。

而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

6.4.3.2 版本链

有一张表原始数据为:
在这里插入图片描述

  • DB_TRX_ID : 代表最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID,是自增的。
  • DB_ROLL_PTR : 由于这条数据是才插入的,没有被更新过,所以该字段值为null。

然后,有四个并发事务同时在访问这张表。

A.第一步

在这里插入图片描述

当事务2执行第一条修改语句时,会记录undo log日志,记录数据变更之前的样子; 然后更新记录, 并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

在这里插入图片描述

B.第二步

在这里插入图片描述

当事务3执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

在这里插入图片描述

C.第三步
在这里插入图片描述

当事务4执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

在这里插入图片描述

  • 最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
  • 在undolog版本链中所记录的就是,当前这一条记录它的所有历史记录版本。
  • 那么我们在查询的时候到底返回的是那个版本呢?????
    • 这个实际上不是由undolog版本链来控制的,而是有mvcc的第三个组价readview来控制的。

6.4.4 readview

ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

ReadView中包含了四个核心字段:

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的,下一个要分配的事务id是几)
creator_trx_id ReadView创建者的事务ID

而在readview中就规定了版本链数据的访问规则:

trx_id 代表当前undolog版本链对应事务ID(当前事务的id)。

在获取历史版本数据的时候到底获取的是哪个版本,就是那当前事务的id和上面ReadView的4个属性进行对比。

条件 是否可以访问 说明
trx_id == creator_trx_id 可以访问该版本 成立,说明数据是当前这个事务更改的。那我刚刚更新的这个记录 那我肯定是可以读取出来的
trx_id < min_trx_id 可以访问该版本 成立,说明数据已经提交了。
trx_id > max_trx_id 不可以访问该版本 成立,说明该事务是在ReadView生成后才开启。
min_trx_id <= trx_id<= max_trx_id 如果trx_id不在m_ids中, 是可以访问该版本的 成立,说明数据已经提交。

不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED (读已提交):在事务中每一次执行快照读时生成ReadView。
  • REPEATABLE READ(可重复读):仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。

6.4.5 原理分析

6.4.5.1 RC隔离级别(读已提交)

RC隔离级别下,在事务中每一次执行快照读时生成ReadView。

我们就来分析事务5中,两次快照读读取数据,是如何获取数据的?

在事务5中,查询了两次id为30的记录,由于隔离级别为Read Committed,所以每一次进行快照读都会生成一个ReadView,那么两次生成的ReadView如下。

在这里插入图片描述
解释:

  • 这个图代表的是多个事务并发操作,从上往下看
  • 第一个ReadView:
    • m_ids:{3,4,5},执行当前快照读的时候当前活跃的事务ID集合。在执行第一个查询id为30的记录这条sql时,事务2不是活跃的id因为他已经提交了,事务3、事务4和事务5此时都还没有提交事务,所以是活跃的事务ID。
    • min_trx_id:3,所以此时最小的活动事务id就是3.
    • max_trx_id:6,预分配的下一个事务id,就是当前最大活跃事务id+1,所以为6。
    • creator_trx_id:5,执行这条更新sql,也就是快照读的时候,会生成ReadView,那么它的创建事务的id就是5.
  • 第二个ReadView:同理
    • m_ids:{4,5}
    • min_trx_id:4
    • max_trx_id:6
    • creator_trx_id:5

那么这两次快照读在获取数据时,就需要根据所生成的ReadView以及ReadView的版本链访问规则, 到undolog版本链中匹配数据,最终决定此次快照读返回的数据。

A.先来看第一次快照读具体的读取过程:
在这里插入图片描述
在这里插入图片描述

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  • 先匹配这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 , 都不满足,则继续匹配undo log版本链的下一条。
    在这里插入图片描述

  • 再匹配第二条 ,这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配undo log版本链的下一条。
    在这里插入图片描述

  • 再匹配第三条 ,这条记录对应的trx_id为2,也就是将2带入右侧的匹配规则中。①不满足 ②满足 终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。
    在这里插入图片描述

B.再来看第二次快照读具体的读取过程:
在这里插入图片描述
在这里插入图片描述

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  • 先匹配 这条记录,这条记录对应的trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 , 都不满足,则继续匹配undo log版本链的下一条。
    在这里插入图片描述

  • 再匹配第二条 ,这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②满足 。终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。
    在这里插入图片描述

6.4.5.2 RR隔离级别(可重复读)

RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的

那MySQL是如何做到可重复读的呢? 我们简单分析一下就知道了

在这里插入图片描述

我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返回的结果也是一样的

所以呢,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。

解释一致性则是由redolog 与 undolog共同保证:数据在执行之前和执行之后是一致的,也就是说这个事务执行失败我要全部回滚,保证数据前后一致。数据一旦提交一定要保证数据是更新过来了。

在这里插入图片描述

6.5 总结

在这里插入图片描述

7.MySQL管理

7.1 系统数据库

Mysql数据库安装完成后,自带了以下四个数据库,具体作用如下:

数据库 含义
mysql 存储MySQL服务器正常运行所需要的各种信息 (时区、主从、用户、权限等)
information_schema 提供了访问数据库元数据的各种表和视图,包含数据库、表、字段类型及访问权限等
performance_schema 为MySQL服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数
sys 包含了一系列方便 DBA 和开发人员利用 performance_schema性能数据库进行性能调优和诊断的视图

在这里插入图片描述

7.2 常用工具

7.2.1 mysql

该mysql不是指mysql服务,而是指mysql的客户端工具。

作用:登录mysql数据库系统

语法 :
    #mysql 选项   数据库名
	mysql [options] [database]
	
选项 :
	-u, --user=name 		 #指定用户名
	
	-p, --password[=name]	 #指定密码
	
	-h, --host=name 		 #指定服务器IP或域名
	
	-P, --port=port 		 #指定连接端口 (注意P是大写)
	
	-e, --execute=name 		 #执行SQL语句并退出

-e选项可以在Mysql客户端执行SQL语句,而不用登录进MySQL数据库再执行,对于一些批处理脚本, 这种方式尤其方便。

示例:

#连接的本机并且端口号是3306,那么-h -p可以省略。
#需要指定连接的数据库,表明是那个数据库执行这个sql
mysql -uroot -p1234 db01 -e "select * from stu";

在这里插入图片描述

7.2.2 mysqladmin

mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。

通过帮助文档查看后面可以跟什么选项,每个选项的作用是什么:

	mysqladmin --help

在这里插入图片描述

语法:
    #options:可以跟上选项
    #command:可以写指令,比如创建数据库、删除数据库、刷新权限等等
	mysqladmin [options] command ...
	
选项:
	-u, --user=name 		#指定用户名
	-p, --password[=name]   #指定密码
	-h, --host=name 		#指定服务器IP或域名
	-P, --port=port 		#指定连接端口

示例:

#查看当前数据库的版本(因为要管理数据库所以要先连接数据库)
mysqladmin -uroot -p1234 version;

#创建数据库
mysqladmin -uroot -p1234 create db02;

#删除数据库
mysqladmin -uroot -p1234 drop db02;

在这里插入图片描述

7.2.3 mysqlbinlog

由于服务器生成的二进制日志文件以二进制格式保存,如果想要检查这些文本的文本格式就很不方便,因为他不是具体的文本文件,此时就可以使用到mysqlbinlog 日志管理工具查看这个二进制日志文件。

这个指令和二进制日志文件有关,二进制日志文件在mysql运维篇中详细讲解。

语法 :
    #            选项      日志文件名1  日志文件名2
	mysqlbinlog [options] log-files1 log-files2 ...
	
选项 :
	-d, --database=name 		指定数据库名称,只列出指定的数据库相关操作。
	-o, --offset=# 				忽略掉日志中的前n行命令。
	-r,--result-file=name 		将输出的文本格式日志输出到指定文件。
	-s, --short-form 			显示简单格式, 省略掉一些信息。
	--start-datatime=date1 --stop-datetime=date2 			指定日期间隔内的所有日志。
	--start-position=pos1  --stop-position=pos2 			指定位置间隔内的所有日志。

示例:
A. 查看 binlog.000008这个二进制文件中的数据信息

#找到二进制日志存放的位置
cd /var/lib/mysql

#如果是文本文件直接可以使用cat指令查看,但现在是二进制文件所以会显示乱码
cat binlog.000001

#解决:使用mysqlbinlog指令查看
mysqlbinlog binlog.000001

在这里插入图片描述

上述查看到的二进制日志文件数据信息量太多了,不方便查询。 我们可以加上一个参数 -s 来显示简单格式。


mysqlbinlog -s binlog.000001

在这里插入图片描述

7.2.4 mysqlshow

mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。

语法 :
    #         选项       哪一个数据库 哪一张表  哪一个字段
	mysqlshow [options] [db_name [table_name [col_name]]]
	
选项 :
	--count 	#显示数据库及表的统计信息(数据库,表 均可以不指定)
	-i 			#显示指定数据库或者指定表的状态信息
	
示例:

	#查询test库中每个表中的字段数,及行数(同样需要先连接上mysql才能进行操作)
	mysqlshow -uroot -p2143   --count test

	#查询test库中book表的详细情况
	mysqlshow -uroot -p2143   --count test book

示例:

A.查询每个数据库的表的数量及表中记录的数量

#没有写数据库名,统计的是所有数据库
mysqlshow -uroot -p1234 --count

在这里插入图片描述

B.查看数据库db01的统计信息

#统计指定数据库的信息
mysqlshow -uroot -p1234 --count   db01

在这里插入图片描述

C.查看数据库db01中的course表的信息

#统计指定数据库下的具体表的信息
mysqlshow -uroot -p1234 --count  db01 course 

在这里插入图片描述

D.查看数据库db01中的course表的id字段的信息

#统计指定数据库下的具体表下的具体字段的信息
mysqlshow -uroot -p1234 --count  db01 course id

在这里插入图片描述

7.2.5 mysqldump

mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。

语法 :
    #options选项:分为连接选项和输出输出选项
    #                   备份指定的数据库下的指定表
	mysqldump [options] db_name [tables]
	#               通过--database或者-B指定备份哪几个数据库   
	mysqldump [options] --database/-B db1 [db2 db3...]
	#               备份所有的数据库使用--all-databases或者-A
	mysqldump [options] --all-databases/-A
	
连接选项 :
	-u, --user=name 		指定用户名
	-p, --password[=name] 	指定密码
	-h, --host=name 		指定服务器ip或域名
	-P, --port=# 			指定连接端口
	
输出选项:
	--add-drop-database 	在每个数据库创建语句前加上drop database语句
	--add-drop-table 		在每个表创建语句前加上drop table语句,默认开启;不开启(--skip-add-drop-table)
	-n, --no-create-db 		不包含数据库的创建语句
	-t, --no-create-info 	不包含数据表的创建语句
	-d --no-data 			不包含数据
	-T, --tab=name 			自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件

示例:

A.备份db01数据库

#备份db01数据库,并且把备份出的数据放到db01.sql文件中
#执行完毕后就会在当前目录下出现db01.sql文件
mysqldump -uroot -p1234 db01 > db01.sql

#查看目录结构:
ll

在这里插入图片描述

可以直接打开db01.sql,来查看备份出来的数据到底什么样。

#查看此文件内容
cat db01.sql

在这里插入图片描述

备份出来的数据包含:

  • 删除表的语句
  • 创建表的语句
  • 数据插入语句

如果我们在数据备份时,不需要创建表,或者不需要备份数据,只需要备份表结构,都可以通过对应的参数来实现。

B.备份db01数据库中的表数据,不备份表结构(-t)

#不包含建表语句
mysqldump -uroot -p1234 -t db01 > db02.sql

ll

在这里插入图片描述

打开 db02.sql ,来查看备份的数据,只有insert语句,没有备份表结构。

cat db02.sql

在这里插入图片描述

C.将db01数据库的表的表结构与数据分开备份(-T)

#自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件
#/root:表示把这2个文件存在root目录下
#db01 stu:备份的是哪一个数据库下的哪一张表
# 发现报错:
mysqldump -uroot -p1234 -T /root db01 stu

cd /root

#发现只生成了一个.sql文件
ll

在这里插入图片描述

执行上述指令,会出错,数据不能完成备份(只生成了一个.sql文件),原因是因为我们所指定的数据存放目录/root,MySQL认 为是不安全的,需要存储在MySQL信任的目录下。那么,哪个目录才是MySQL信任的目录呢,可以查看 一下系统变量 secure_file_priv 。执行结果如下:

mysql -uroot -p1234

#登录进mysql才能执行这个命令
#结果为:/var/lib/mysql-files/ 
show variables like '%secure_file_priv%';

#往mysql信任的目录存放文件
#打开另一个会话窗口,这是没有登录mysql时执行的命令
mysqldump -uroot -p1234 -T /var/lib/mysql-files/ db01 stu

cd /var/lib/mysql-files/

#发现此时就有了2个文件
ll

在这里插入图片描述

在这里插入图片描述

上述的两个文件 score.sql 中记录的就是表结构文件,而 score.txt 就是表数据文件,但是需要注意表数据文件,并不是记录一条条的insert语句,而是按照一定的格式记录表结构中的数据。如 下:

cat stu.sql

cat stu.txt

在这里插入图片描述

在这里插入图片描述

7.2.6 mysqlimport / source

1)mysqlimport

mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。

也就是说7.2.5使用的mysqldump -uroot -p1234 -T /var/lib/mysql-files/ db01 stu,把db01数据库的表的表结构与数据分开备份(-T)到2个文件,stu.sql、stu.txt。此时使用以下命令把这txt文件导回到数据库表结构中。

语法 :
    #             选项   数据库的名字  导回的txt文件
	mysqlimport [options] db_name textfile1 [textfile2...]
	
示例 :
	mysqlimport -uroot -p2143 test /tmp/city.txt

测试:

首先删除stu表的数据

#清空表,表示清空表中的所有数据,但是表结构保留。
TRUNCATE TABLE stu;

SELECT * FROM stu;

在这里插入图片描述

ll

pwd

#会报错:
#把txt文件中的数据导入到表结构当中,把数据恢复出来
#直接写txt文件仍然会报错:原因是直接写stu.txt文件表示当前目录,需要使用mysql信任的目录进行导入
#   即便我们已经进入到这个/var/lib/mysql-files mysql所信任的目录下但是他不识别,必须在执行命令的时候
#   加上此mysql所信任的目录
mysqlimport -uroot -p1234 db01 stu.txt

#解决:加上此mysql所信任的目录
mysqlimport -uroot -p1234 db01 /var/lib/mysql-files/stu.txt

在这里插入图片描述

再次查询stu表就有数据了

在这里插入图片描述

2)source

如果需要导入sql文件,可以使用mysql中的source 指令 :

语法 :
	#需要先登录mysql后才能执行此命令
	#导入什么目录下的sql文件
	source /root/xxxxx.sql

测试:

首先删除db01数据库下的所有表:

在这里插入图片描述

之前生成的db01.sql文件
在这里插入图片描述

其次使用命令导入sql文件

mysql -uroot -p1234

use db01;

#之前学习7.2.5把db01数据库备份到了/var/lib/mysql/目录下,生成了db01.sql文件
source /var/lib/mysql/db01.sql

在这里插入图片描述

刷新数据库可以看到:数据恢复了

在这里插入图片描述

7.3 总结

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/aa35434/article/details/132899675