MySQL系列结束篇
SQL 优化
插入数据
1.insert
如果我们要一次性向同一张数据库表中插入多条数据,我们有三种优化方案
# 基础版本
insert into tb_text values(1, 'zhangsan')
insert into tb_text values(2, 'lisi')
insert into tb_text values(3, 'wangwu')
-- 优化方案一:批量插入数据
insert into tb_test values(1, 'zhangsan'), (2, 'lisi'), (3, 'wangwu')
-- 优化方案二:手动控制事务
start transaction;
insert into tb_text values(1, 'zhangsan')
insert into tb_text values(2, 'lisi')
insert into tb_text values(3, 'wangwu')
commit;
-- 优化方案三:主键顺序插入性能高于乱序插入
insert into tb_test values(1, 'zhangsan'), (2, 'lisi'), (3, 'wangwu')
insert into tb_test values(3, 'wangwu'), (1, 'zhangsan'), (2, 'lisi')
如果一次要插入大批量数据(几百万条记录),使用 insert 性能较低,可以使用 load 指令插入【100w数据 17s,仅供参考】
-- 客户端连接服务端时,加上参数,--local-infile
mysql --local-infile -u root -p
-- 设置全局参数 local_infile 为 1,目的是开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行 load 指令,将准备好的数据加载到表结构中
load data local infile '/文件路径' into table tb_user fields terminated by ',' lines terminated by '\n';
主键优化
我们将从多个方面介绍如何设计主键,以及证明为什么主键顺序插入的性能要高于乱序插入的
(1)数据组织方式
在 InnoDB 存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表成为索引组织表
行数据,都是存储在聚集索引的叶子结点上的,我们再看一下 InnoDB 的逻辑结构图
数据行记录在逻辑结构的页中,页的大小固定为 16KB,如果插入的数据行在该页存储不下,将会存储到下一个页中(页的地址空间是连续的),页与页之间会通过指针连接
(2)页分裂
-
页分裂就是由于主键不按顺序插入,导致的需要重新设置链表指针(页合页之间的指针)的现象
-
页可以为空,可以部分填充,也可以完全填充,每个页包含了 2-N 行数据,根据主键排序
-
如果一行数据过大,产生移除后会将元素插入到新的页中
主键顺序插入 >> 当前页存满后,会顺序插入到下一个页
aa 从磁盘申请页,主键顺序插入
bb 第一个页没有存满,还可以继续插入数据
cc 当第一个页写满之后,再写入第二个页,页与页之间通过指针连接
dd 当第二页写满后,将元素写入第三个页即可
主键乱序插入 >> 链表指针可能需要重新排序
aa 假设前两页都写满了,数据情况如下
bb 此时在插入主键值为 50 的记录,会发生什么现象呢
因为索引结构的叶子结点是有序的,所以当前要插入的记录不会直接插入第三个页,理论上应该插入第一个页的末尾
第一个页已经存储不下了,那么会怎么办呢?
上面由于主键乱序插入出现的这种现象我们成为 "页分裂",是比较耗费性能的操作
(3)页合并
页合并的这种现象一般出现在删除记录数达到页空间的百分之五十,页会尝试和相邻页的数据进行合并,提高页空间利用率
当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记为 (flaged),并且它的空间允许被使用 【通过覆盖的方式重新利用空间】
当页中删除的记录达到 MERGE_THRESHOLD (默认为页的 50%),InnoDB 会开始宣召最靠近的页(前、后),查看是否可以将两个页合并以优化空间使用
删除数据,并将页合并之后,再次插入新的数据 21,则直接插入 3# 页
我们称上面的这个现象为页合并,合并页的阈值可以自定义,在创建表或创建索引时指定
PS:
A.MySQL 发生页合并现象后,插入新数据,是直接开一个新页插入数据,还是会插入到合并页的剩余空间处?
如果在数据页合并后插入新数据,MySQL 会将新数据插入到新分配的数据页中,而不是插入到合并前的两个数据页的剩余空间中。原因是,在数据页合并后,这两个数据页已经被标记为“空闲页”,它们的数据已经被移动到合并后的新数据页中,如果继续向这两个数据页中插入新数据,可能会破坏数据页的一致性。
B.具体如何设置合并页的阈值?
可以参考这篇文章:blog.csdn.net/wanbin64703…
(4)索引设计原则
- 满足业务需求的情况下,尽量降低主键的长度
- 插入数据时,尽量选择顺序插入,选择使用 AUTO_INCREMENT 自增主键
- 尽量不要使用 UUID 做主键或其他自然主键(身份证号)等
- 业务操作时,避免对主键的修改
order by 优化
- 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则
- 尽量使用覆盖索引
- 多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC / DESC)
- 如果不可避免的出现 filesort,大数据量排序时,可以适当增大排序缓冲区的大小 sort_buffer_size (默认为 256K)
MySQL 的排序,有两种方式:
- Using filesort: 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区 sortbuffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序 【性能低,尽量要优化为 Using index】
- Using index: 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index, 不需要额外排序,操作效率高 【性能高】
-- 清理部分索引,以免对测试产生印象
show index from tb_user;
-- 执行排序 SQL
explain select id, age, phone from tb_user order by age;
explain select id,age,phone from tb_user order by age,phone;
由于 age、phone 都没有索引,所以此时排序时,出现 Using filesort,排序性能较低
-- 创建索引
create index idx_user_age_phone on tb_user(age, phone);
-- 再次测试
explain select id, age, phone from tb_user order by age;
explain select id,age,phone from tb_user order by age,phone;
建立索引之后,再进行排序查询,就由原来的 Using filesort 变成了 Using index,性能就是比较高的了
-- 创建索引后,根据 age, phone 进行降序排序
explain select id,age,phone from tb_user order by age desc, phone desc;
我们可以看到出现了 Using index,但是此时 Extra 中出现了 Backward index scan,这个代表反向扫描索引,因为在 MySQL 中我们创建的索引,默认索引的叶子结点都是从小到大排序的,而此时我们查询排序时,是从大到小,所以在扫描时,反向扫描就会出现 Backward index scan。在 MySQL8 版本中,支持降序索引,我们也可以创建降序索引
-- 根据 phone,age 进行升序排序, phone 在前,age 在后
explain select id,age,phone from tb_user order by phone, age;
我们发现 Extra 中出现了 Using filesort,因为排序时也要满足最左前缀法则
- 创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时 就会出现Using filesort
- 可以在创建联合索引时指定哪个字段升序、哪个字段降序
create index idx on tb_user(age asc, phone desc)
###group by 优化
- 在分组操作时,可以通过索引提升效率
- 在分组操作时,索引的使用也是满足最左前缀法则的
-- 删除除了主键索引以外的所有索引,以防对测试产生影响
drop index pas on tb_user;
drop index email_index on tb_user;
drop index idx_user_age_phone on tb_user;
-- 查看所有索引
show index from tb_user;
-- 执行分组相关的 SQL,查看测试计划
explain select profession, count(*) from tb_user group by profession;
-- 为 profession、age、status 创建联合索引
create index pas on tb_user(profession, age, status);
-- 再次测试查看执行计划
explain select profession, count(*) from tb_user group by profession;
-- 按照两个字段分组查询
explain select profession, count(*) from tb_user group by profession, age;
-- 不满足最左前缀法则查询
explain select profession, count(*) from tb_user group by age;
Using temporary
表示查询使用了临时表。这通常出现在有一些排序操作或者分组操作的查询中。MySQL 会将查询结果先存储到一个临时表中,然后再进行排序或者分组。使用临时表会增加查询的开销,因此应该尽可能避免出现Using temporary
。Using index
表示查询使用了索引覆盖。这通常出现在只查询表中某些列,而这些列都有索引的情况下。MySQL 可以直接利用索引中的数据返回查询结果,而不需要再访问数据表,这样可以大大提升查询效率。使用覆盖索引可以减少查询所需要的 IO 操作,因此在需要高效查询部分字段的情况下,可以采用覆盖索引来提高查询性能。
###limit 优化
- 在数据量比较大时,采用 limit 分页查询,越往后,分页查询效率越低
- 一般分页查询时,通过创建覆盖索引能比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
-- 当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记
录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大
select * from tb_sku limit 1000000, 10;
-- 覆盖索引 + 子查询优化
explain select * from tb_sku t, (select id from tb_sku order by id limit 2000000, 10) a where t.id = a.id;
###count 优化
在数据量很大的情况下,执行 count 操作是非常耗时的 select count(*) from tb_user;
- MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个 数,效率很高; 但是如果是带条件的count,MyISAM也慢
- InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出 来,然后累积计数
优化策略:自己计数(例如使用 Redis,但是对于有条件的 count 仍然很麻烦)
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加,最后返回累计值
效率排序:count(字段) < count(主键 id) < count(1) 约等 count(*) 【推荐尽量使用 count(*)
】
count(1) 和 count(*) 有什么区别呢?
- count(*) 代表 count(0)
- 0 和 1 都不为空,所以统计的都是表中的总行数
###update 优化
InnoDB 的行锁是针对索引假的锁,不是对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁
-- 因为有主键索引,所以是行锁
explain update tb_user set name = '界吕布' where id = 1;
-- 因为 name 字段没有索引,所以执行 SQL 时行锁升级为表锁,导致 update 语句的性能降低
explain update tb_user set name = '界吕布' where status = 6;
视图/存储过程/触发器
视图
- 什么是视图?
- 一种虚拟存在的表,视图中的数据在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的
- 视图只保存了查询的SQL逻辑,不保存查询的结果
- 视图相关的语法:
-- 创建视图
create [or replace] view 视图名称[(列名列表)] as select语句 [with [cascaded | local] check option]
-- 查看创建视图语句
show create view 视图名称;
-- 查看视图数据
select * from 视图名称 ...;
-- 采用 create 覆盖已经创建的视图
create [or replace] view 视图名称[(列名列表)] as select语句 [with [cascaded | local] check option]
-- 采用 alter 修改视图
alter view 视图名称[(列名列表)] as select语句 [with [cascaded | local] check option]
-- 删除视图
drop view [if exists] 视图名称 [,视图名称] ...
通过案例来演示
-- 创建视图
create or replace view user_v_1 as select id,name from tb_user where id <= 10;
-- 查询视图
show create view user_v_1;
select * from user_v_1;
select * from user_v_1 where id < 3;
-- 修改视图
create or replace view user_v_1 as select id,name,phone from tb_user where id <= 10;
alter view user_v_1 as select id,name from tb_user where id <= 10;
-- 删除视图
drop view if exists user_v_1;
我们是否可以通过视图来插入、修改表中数据呢?
-- 通过视图更新表中数据
create or replace view t as select * from test where id <= 3;
select * from t;
insert into t values(1, '界吕布');
insert into t values(4, '神吕布');
为什么会出现上面的这种现象呢?
- 因为我们在创建视图的时候指定了 id <= 3,插入 id 为 4 的数据可以插入成功,但是不符合视图要求,所以不在视图中进行显示
- 在定义视图时指定了条件,在我们插入、修改、删除操作时只需要借助视图的检查选项就可以成功操作
- 检查选项
-
当使用 with check option 子句创建视图时,MySQL 会通过视图检查正在更改的每个行,使其符合视图的定义
-
MySQL 允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性
-
为了确定检查的范围,MySQL 提供了两个选项:视图B是基于视图A创建的
-
视图A指定了检查选项为 cascaded(级联,默认),那么执行检查视图B时,还会检查关联视图A
-
视图A指定了检查选项为 local(本地),那么执行检查视图B时,不会检查关联视图A
-
- 视图的更新
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一关系,视图包含以下情况之一,则视图不可更新(代表不能更新数据库表中的数据)
- 聚合函数或窗口函数(sum()、min()、max()、count()等)
- distinct
- group by
- having
- union 或 union all
-- 创建一个不可更新的视图
create or REPLACE view t as select count(*) from test;
-- 尝试更新视图
insert into t values(1, 'test');
- 视图的作用
(1)简单
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那么被经常使用的查询可以被定义为视图,从而使用户不必为以后的操作每次指定全部的条件
(2)安全
数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据
(3)数据独立
视图可帮助用户屏蔽真实表结构变化带来的影响
- 应用案例演示
- 视图可以用于隐藏敏感字段,让用户只能看到基本字段
-- 创建视图
create view tb_user_view as select id,name,profession,age,gender,status,createtim from tb_user;
-- 查看视图
select * from tb_user_view;
- 视图可以用来简化操作,比如多张表联合查询定义为视图
-- 定义视图
create view tb_stu_course_view as select s.name studnet_name, s.no student_no, c.name course_name from student s, student_course sc, course c where s.id = sc.studentid and sc.courseid = c.id;
-- 查看视图
select * from tb_stu_course_view;
存储过程
- 存储过程指的是什么?
先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化开发,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的
- 封装、复用:可以把某一业务 SQL 封装在存储过程中,需要用到的时候直接调用即可
- 接受参数、返回数据:再存储过程中,可以传递参数,也可以接受返回值
- 减少网络交互、效率提升:如果涉及到多条 SQL,没执行一次都是一次网络传输。而如果封装在存储过程中,我们只需要网络交互一次可能就可以了
- 与存储过程相关的语法
-- 创建存储过程
create procedure 存储过程名称 ([参数列表])
begin
-- SQL 语句
end;
-- 调用存储过程
call 名称 ([参数]);
-- 查询指定数据库的存储过程和状态信息
select * from information_schema.routines where routine_schema = 'xxx';
-- 查询某个存储过程的定义
show create procedure 存储过程名称;
-- 删除某个存储过程
drop procedure [if exists] 存储过程名称;
在命令行中,执行创建存储过程的 SQL 时,需要通过关键字 delimiter 指定 SQL 语句的结束符
接下来通过案例演示来说明如何使用存储过程
-- 定义存储过程
create procedure fun()
begin
select count(*) from tb_user;
end;
-- 调用存储过程
call fun();
-- 查询指定数据库的存储过程和状态信息
select * from information_schema.routines where routine_schema = 'zwh';
-- 查看存储过程的定义
show create procedure fun;
-- 删除存储过程
drop procedure if exists fun;
- 介绍存储过程中使用的几种变量
在 MySQL 中变量分为三种类型:系统变量、用户定义变量、局部变量
(1)系统变量
系统变量是 MySQL 服务器提供的,属于服务器从面,又分为 全局变量(global)、会话变量(session) 两种
-- 查看所有系统变量
show [session | global] variables;
-- 通过模糊匹配查找变量
show [session | global] variables like '...';
-- 查看指定变量的值
select @@[session | global] 系统变量名;
-- 案例演示 >> session 代表当前回话级别的变量值、global 代表全局级别的变量值
-- 查看所有系统变量
show global variables;
-- 通过模糊匹配查找变量
show global variables like 'admin%';
-- 查看指定变量的值
select @@admin_port;
-- 设置系统变量
set [session | global] 系统变量名 = 值;
set @@[session | global]系统变量名 = 值;
如果没有指定 session/global,默认是 session, 会话变量
- mysql 服务重新启动后,所设置的全局参数会失效,可以在 /etc/my.cnf 中配置,让mysql重启全局参数仍然生效
- 全局变量(global): 全局变量针对于所有的会话
- 会话变脸(session): 会话变量针对于单个会话,在另外一个会话窗口就不生效了
-- 查看系统变量
show session variables;
show session variables like 'auto%';
show global variables like 'auto%';
select @@global.autocommit;
select @@session.autocommit;
-- 设置系统变量
set session autocommit = 1;
insert into course(id, name) values (6, 'ES');
set global autocommit = 0;
select @@global.autocommit;
(2)用户定义变量
使用户根据需要自己定义的变量,用户变量不需要提前声明,在用的时候直接用 "@@变量名" 使用就可以,其作用域为当前连接
-- 使用 set 赋值时,可以使用 = 或 :=
set @var_name = expr [, @var_name = expr] ...;
set @var_name := expr [, @var_name := expr] ...;
-- 使用 select 也可以完成赋值
select @var_name := expr [, @var_name := expr] ...;
select 字段名 info @var_name from 表名;
-- 使用用户自定义变量,用户自定义变量无需对其进行声明和初始化,只不过获取到的值为 NULL
select @var_name;
案例演示如何定义、使用用户定义变量
-- 赋值
set @myname = 'lihua';
set @myage := 18;
set @mygender := 'woman', @myhobby := 'English';
select @mycolor : = 'red';
select count(*) into @mycount from tb_user;
-- 使用
select @myname, @myage, @mygender, @myhobby;
select @mycolor, @mycount;
select @var_null;
(3)局部变量
是根据需要定义的局部生效的变量,访问之前没需要 declare 声明。可以用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的 begin ... end 块
-- 声明局部变量(数据库字段类型) int、bigint、char、varchar、date、time 等
declare 变量名 变量类型 [default ...];
-- 赋值
set 变量名 = 值;
set 变量名 := 值;
select 字段名 into 变量名 from 表名 ...;
接下来通过案例演示如何定义和使用局部变量
-- 定义存储过程
create procedure fun()
begin
-- 局部变量定义在构造过程内部
declare stu_count int default 0;
-- 为局部变量赋值
select count(*) into stu_count from tb_user;
-- 调用局部变量
select stu_count;
end;
-- 执行构造过程
call fun();
-- 删除存储过程
drop procedure if exists fun;
- if 关键字的使用
if 关键字用于条件判读,语法结构如下,对于 elseif 可以有零个或多个, else 可以有一个或没有
if 条件1 then
......
elseif 条件2 then
......
else
......
end if;
通过案例来设计一个存储过程:
/*
根据定义的分数score变量,判定当前分数对应的分数等级。
score >= 85分,等级为优秀。
score >= 60分 且 score < 85分,等级为及格。
score < 60分,等级为不及格
*/
create procedure fun()
begin
-- 直接将 score 参数值写死了,应该通过存储过程的方法传递进去
declare score int default 58;
declare result varchar(10);
if score >= 85 then
set result := '优秀';
elseif score >= 60 then
set result := '及格';
else
set result := '不及格';
end if;
select result;
end;
call fun();
- 参数
参数主要分为以下三种: in、out、inout
create procedure 存储过程名称 ([IN/OUT/INOUT 参数名 参数类型])
BEGIN
-- SQL 语句
END;
下面通过两个案例来进行演示:
(1) 案例一:将传入的200分制的分数,进行换算,换算成百分制,然后返回
create PROCEDURE castScore(inout score double)
begin
set score := score * 0.5;
end;
-- 自定义变量初始化
set @score = 198;
-- 调用存储过程,修改用户定义变量
call castScore(@score);
-- 查看用户定义变量的值
select @score;
(2)案例二:根据传入参数score,判定当前分数对应的分数等级,并返回
- score >= 85分,等级为优秀
- score >= 60分 且 score < 85分,等级为及格
- score < 60分,等级为不及格
create procedure grade(in score double)
begin
declare result varchar(10) default '';
if score >= 85 then
set result = '优秀';
elseif score >= 60 then
set result = '及格';
else
set result = '不及格';
end if;
select result;
end;
call grade(88);
- case 关键字的用法
case 结构及作用,和流程控制函数很类似,有两种语法格式:
-- 语法一: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,执行statement_list2, 否则就执行 statement_list
CASE case_value
WHEN when_value1 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;
-- 语法二:当条件search_condition1成立时,执行statement_list1,当条件search_condition2成立时,执行statement_list2, 否则就执行 statement_list
CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
案例实现:根据传入的月份,判断月份所属的季节
- 1-3月份,为第一季度
- 4-6月份,为第二季度
- 7-9月份,为第三季度
- 10-12月份,为第四季度
create procedure month(in month int)
begin
declare result varchar(10);
case
-- 条件可以替换为 month >= 1 and month <= 3
when month in (1, 2, 3) then set result := '第一季度';
when month in (4, 5, 6) then set result := '第二季度';
when month in (7, 8, 9) then set result := '第三季度';
when month in (10, 11, 12) then set result := '第四季度';
else
set result := '非法参数';
end case;
select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);
end;
call month(6);
- while 关键字的用法
有条件的循环控制语句,条件满足后,再执行循环体中的 SQL 语句,具体语法如下:
-- 先判断条件,条件为 true 才执行逻辑,否则不执行逻辑
while 条件 do
-- SQL 逻辑
end while;
案例分析:计算从1累加到n的值,n为传入的参数值
create procedure fun(in num int)
begin
declare sum int default 0;
declare i int default 1;
while i <= num do
set sum = sum + i;
set i = i + 1;
end while;
select concat('从1到' , num, '的元素和为: ' , sum);
end;
call fun(3);
- repeat 关键字用法
repeat 是有条件的循环控制语句,当满足 until 声明的条件时,退出循环
-- 限制性一次逻辑,然后判断 until 条件是否满足,如果满足则退出,不满足继续下一次循环 [类似 Java do while 结构]
repeat
-- SQL 逻辑
until 条件
end repeat;
案例分析:计算从1累加到n的值,n为传入的参数值。(使用repeat实现)
create procedure fun(in n int)
begin
declare sum int default 0;
repeat
set sum = sum + n;
set n = n - 1;
-- until 满足条件则退出循环
until n <= 0
end repeat;
select sum;
end;
call fun(3);
- loop 的用法
loop 实现简单的循环,如果不在 SQL 逻辑中增加退出循环的条件,可以用其来实现简单的死循环
- leave:配合循环使用,退出死循环
- iterate:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环
[begin_label:] loop
-- SQL 逻辑
end loop [end_label];
-- 退出指定标记的循环体
leave label;
-- 直接进入下一次循环
iterate label;
案例一:计算从1累加到n的值,n为传入的参数值
create procedure fun(in n int)
begin
declare sum int default 0;
sum:loop
if n <= 0 then
leave sum;
end if;
set sum = sum + n;
set n = n - 1;
end loop sum;
select sum;
end;
call fun(3);
案例二:计算从1到n之间的偶数累加的值,n为传入的参数值
create procedure fun(in n int)
begin
declare sum int default 0;
i:loop
if n <= 0 then
leave i;
end if;
if n % 2 = 1 then
set n = n - 1;
iterate i;
end if;
-- 偶数
set sum = sum + n;
set n = n - 2;
end loop i;
select sum;
end;
call fun(3);
- 游标
游标 是用来存储查询结果集的数据类型,在存储过程和函数中可以使用游标对结果集进行循环的处理
-- 声明游标
declare 游标名称 cursor for 查询语句;
-- 打开游标
open 游标名称;
-- 获取游标记录
fetch 游标名称 into 变量 [, 变量];
-- 关闭游标
close 游标名称;
案例演示:根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名 (name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表 (id,name,profession)中
-- 删除原来的存储过程
drop procedure fun;
-- 定义存储过程
create procedure fun(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
-- 声明游标,存储查询结果集
declare u_cursor cursor for select name,profession from tb_user where age <= uage;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
-- 开启游标
open u_cursor;
while true do
-- 获取游标中的记录
fetch u_cursor into uname, upro;
-- 将数据插入到我们准备的新表中
insert into tb_user_pro values (null, uname, upro);
end while;
-- 关闭游标
close u_cursor;
end;
call fun(30);
因为 while 循环没有设置结束条件,所以最后运行 call fun(30)
会报错,但是我们发现结果集已经存储到了 tb_user_pro
表中
上面的这个问题可以通过 MySQL 为我们提供的条件处理程序 Handler 来解决
- 条件处理程序
可以用来定义在流程控制过程中遇到问题时相应的处理步骤,语法如下:
declare handler_action handler for condition_value [, condition_value] ... statement;
handler_action 的取值:
continue: 继续执行当前的程序
exit: 终止执行当前的程序
condition_value 的取值:
SQLSTATE sqlstate_value: 状态码(0200等)
SQLWARNING: 所有以 01 开头的 SQLSTATE 代码的简写
NOT FOUND: 所有以 02 开头的 SQLSTATE 代码的简写
SQLEXCEPTION: 所有没有被 SQLWARNING 或 NOT FOUND 捕获的 SQLSTATE 代码的简写
案例分析:根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名 (name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表 (id,name,profession)中
A. 通过 SQLSTATE 指定具体的状态码
create procedure fun(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age <= uage;
-- 声明条件处理程序:当 SQL 语句执行抛出的状态码为 02000 时,将关闭游标并退出
declare exit handler for SQLSTATE '02000' close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
fun(30);
B.通过 SQLSTATE 的代码简写方式 NOT FOUND,上面的代码替换一条语句
declare exit handler for not found close u_cursor;
具体的错误状态码,可以参考官方文档:
存储函数
存储函数是有返回值的存储过程,存储函数的参数只能是 IN 类型的,语法如下:
create function 存储函数名称([参数列表])
returns type [characteristic ...]
begin
-- SQL 语句
Return ...;
end;
characteristic 说明:
- deterministic: 相同的输入参数总是产生相同的结果
- no sql:不包含 SQL 语句
- reads sql data:包含读取数据的语句,但不包含写入数据的语句
-- 经典案例:计算从1累加到n的值,n为传入的参数值
create function fun(n int)
returns int deterministic
begin
declare total int default 0;
while n > 0 do
set total = total + n;
set n = n - 1;
end while;
return total;
end
select fun(3);
在mysql8.0版本中binlog默认是开启的,一旦开启了,mysql就要求在定义存储过程时,需要指定 characteristic特性,否则就会报如下错误:
触发器
触发器是与表有关的数据库对象,指在 insert / update /delete 之前或之后,触发并执行触发器中定义的 SQL 语句集合
- 触发器的这种特性可以协助应用在数据库端确保数据的完整性、日志记录、数据校验等操作
- 使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他数据库是相似的,现在触发器还只支持行级触发,不支持语句级触发
-- 创建触发器
create trigger trigger_name;
before/after insert/update/delete
-- 行级触发器
on tbl_name for each row
begin
trigger_stmt;
end;
-- 查看触发器
show triggers;
-- 删除触发器 schema_name 用来指定删除哪个数据库中的触发器,默认为当前数据库
drop trigger [schema_name, ] trigger_name;
案例分析:通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表 user_logs 中,包含增加、修改、删除
(1)添加数据触发器
-- 创建日志表 user_logs
create table user_logs(
id int(11) not null auto_increment,
operation varchar(20) not null comment '操作类型, insert/update/delete',
operate_time datetime not null comment '操作时间',
operate_id int(11) not null comment '操作的ID',
operate_params varchar(500) comment '操作参数',
primary key(`id`)
)engine=innodb default charset=utf8;
-- 创建插入数据触发器
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values
(null, 'insert', now(), new.id, concat('插入的数据内容为:
id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ',
profession=', NEW.profession));
end;
-- 查看触发器
show triggers;
-- 插入数据
insert into tb_user(id, name, phone, email, profession, age, gender, status,
createtime) VALUES (26,'三皇子','18809091212','erhuangzi@163.com','软件工
程',23,'1','1',now());
我们可以看到日志表中的数据可以正常插入,以及插入数据的正确性
(2)修改数据触发器
create trigger tb_user_update_trigger;
after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values
(null, 'update', now(), new.id,
concat('更新之前的数据: id=',old.id,',name=',old.name, ', phone=',
old.phone, ', email=', old.email, ', profession=', old.profession,
' | 更新之后的数据: id=',new.id,',name=',new.name, ', phone=',
NEW.phone, ', email=', NEW.email, ', profession=', NEW.profession));
end;
-- 查看触发器
show triggers;
-- 更新
update tb_user set profession = '会计' where id = 23;
update tb_user set profession = '会计' where id <= 5;
(3)删除数据触发器
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
values
(null, 'delete', now(), old.id,
concat('删除之前的数据: id=',old.id,',name=',old.name, ', phone=',
old.phone, ', email=', old.email, ', profession=', old.profession));
end;
-- 查看触发器
show triggers;
-- 删除数据
delete from tb_user where id = 26;
锁
锁是计算机协调多个进程或线程并发访问某一资源的机制,MySQL 中的锁按照粒度分为三类:
- 全局锁:锁定数据库中的所有表
- 表级锁:每次操作锁住整张表
- 行级锁:每次操作锁住对应的行数据
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的 DML、DDL、已经更新操作的事务提交语句都将被阻塞。
- 典型的应用场景就是做全表的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性
- 为什么全库逻辑备份就要加全局锁呢?
(1)分析不加全局锁可能出现的问题
假设在数据库中存在这样的三张表: tb_stock(库存表)、tb_order(订单表)、tb_orderlog(订单日志表)
- 在进行数据备份时,先备份了 tb_stock 库存表
- 然后接下来,在业务系统中,执行了下单操作,扣减库存,生成订单(更新tb_stock表、插入 tb_order表)
- 然后再执行备份 tb_order 表的逻辑
- 业务中执行插入订单日志操作
- 最后,又备份了 tb_orderlog 表
此时备份出来的数据,是存在问题的。因为备份出来的数据,tb_stock表与tb_order表的数据不一 致(有最新操作的订单信息,但是库存数没减)
(2)再分析加了全局锁后的情况
对数据库进行进行逻辑备份之前,先对整个数据库加上全局锁,一旦加了全局锁之后,其他的DDL、 DML全部都处于阻塞状态,但是可以执行DQL语句,也就是处于只读状态,而数据备份就是查询操作。 那么数据在进行逻辑备份的过程中,数据库中的数据就是不会发生变化的,这样就保证了数据的一致性 和完整性
-- 加全局锁
flush tables with read lock;
-- 数据备份
mysqldump -u root -p 111111 zwh > zwh.sql
-- 释放锁
unlock tables;
数据库中加全局锁,是一个比较重的操作,存在以下问题:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本停滞(可以DQL)
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟
在 InnoDB 引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份
mysqldump --single-transaction -u root -p 111111 zwh > zwh.sql
表级锁
表级锁每次操作锁住整张表,锁定粒度大,发生锁冲突的概率最高,并发度最低,主要应用在 MyISAM、InnoDB、BDB等存储引擎中。
对于表级锁,主要分为以下三类:
- 表锁、元数据锁(meta data lock, MDL)、意向锁
- 率先出场的是我们的表锁
对于表锁又分为两类:表共享读锁(read lock)、表独占写锁(write lock)
-- 加锁
lock tables 表名 ... read / write;
-- 释放锁
unlock tables / 客户端断开连接
接下来通过图来进一步说明读锁和写锁的使用情况:
(1)读锁
左侧为客户端一,对指定表加了读锁,不会影响客户端二的度,但是会阻塞右侧客户端的写
(2)写锁
左侧客户端为表添加了独占锁(写锁),左侧客户端读写都能执行,但是右侧客户端不仅不能写,也不能读了
读锁不会阻塞其他客户端的读,但是会阻塞写。写锁既会阻塞其他客户端的读,又会阻塞 其他客户端的写
- 第二位出场的是元数据锁(DML)
MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。
MDL 锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。
简单来说:==元数据可以理解为表结构,一张表涉及未提交的事务时,是不能修改这张表的表结构的==
在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变 更操作的时候,加MDL写锁(排他)
(1)当执行 select、insert、delete 等语句时,添加的是元数据共享锁 (shared_read / shared_write)
(2)当执行 select 语句时,添加的是元数据共享锁 (shared_read),会阻塞元数据排他锁(exclusive),之间是互斥的
-- 查看数据库中的元数据锁的情况:
select object_type, obejct_schema,object_name, lock_type, lock_duration from performance_schema.metadata_locks;
- 最后出场的是意向锁
为了避免 DML 在执行时,加的行锁与表锁的冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查
(1)假如没有意向锁,客户端一对表加了行锁后,客户端二如何给表加表锁呢,来通过示意图简单分析一 下
首先客户端一,开启一个事务,然后执行DML操作,在执行DML语句时,会对涉及到的行加行锁
当客户端二,想对这张表加表锁时,会检查当前表是否有对应的行锁,如果没有,则添加表锁,此时就 会从第一行数据,检查到最后一行数据,效率较低
(2)使用了意向锁,整个流程就改变了
客户端一,在执行DML操作时,会对涉及的行加行锁,同时也会对该表加上意向锁
而其他客户端,在对这张表加表锁的时候,会根据该表上所加的意向锁来判定是否可以成功加表锁,而 不用逐行判断行锁情况了
意向锁又分为了两种:
-
意向共享锁(IS): 由语句 select ... lock in share mode 添加,兼容表锁共享锁(read)、与表锁排他锁(write) 互斥
-
意向排他锁(IX):由语句 insert、update、delete、select ... for update 添加,与表锁共享锁、表锁排他锁都互斥,意向锁之间不会互斥
-
事务一旦提交了,意向共享锁、意向排他锁都会自动释放
-- 查看意向锁及行锁的加锁情况
select obejcy_schema, object_name, index_name, lock_type, lock_mode, lock_data from performance_schema.data_locks;
(1)演示意向锁与表锁共享锁是兼容的
(2)演示意向排他锁与表锁共享锁、表锁排他锁都是互斥的
行级锁
行级锁每次操作都锁住对应的数据行,锁的粒度最小,发生锁冲突的概率最低,并发度最高,应用在 InnoDB 存储引擎中。
InnoDB 的数据都是基于索引组织的,所以行锁时通过对索引上的索引项加锁来实现的,而不是对记录加锁。
行级锁分为以下三类:
- 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行 update 或 delete。在 RC、RR 隔离级别都支持
- 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert,产生幻读。在 RR 隔离级别下都支持
- 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙 Gap。在 RR 隔离级别下支持
- 行锁
InnoDB 实现类以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁
- 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获取相同数据集的共享锁
常见的 SQL 语句,在执行时,所加的行锁如下:
默认情况下, InnoDB 在 repeatable read 事务隔离级别运行, InnoDB 使用 next-key 锁进行搜索和索引扫描,以防止出现幻读
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁
- InnoDB 的行锁时针对于索引加锁,不通过索引条件检索数据,那么 InnoDB 将对表中的记录加锁,此时就会升级为表锁
-- 查看意向锁及行锁的加锁情况:
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)
(1)普通的 select 语句执行时不会加锁
(2)select ... lock in share mode,加共享锁,共享锁与共享锁之间兼容
共享锁和排他锁之间互斥,客户端1为 id 为1的记录设置共享锁,客户端2为id为2设置排他锁,当客户端2为id为1申请排他锁时,就会处于阻塞状态
(3)排他锁与排他锁互斥
当客户端一,执行update语句,会为id为1的记录加排他锁; 客户端二,如果也执行update语句更 新id为1的数据,也要为id为1的数据加排他锁,但是客户端二会处于阻塞状态,因为排他锁之间是互 斥的。 直到客户端一,把事务提交了,才会把这一行的行锁释放,此时客户端二,解除阻塞。
(4)无索引行锁升级为表锁
在客户端一中,开启事务,并执行update语句,更新name为Lily的数据,也就是id为19的记录 。 然后在客户端二中更新id为3的记录,却不能直接执行,会处于阻塞状态,为什么呢? 原因就是因为
此时,客户端一,根据name字段进行更新时,name字段是没有索引的,如果没有索引, 此时行锁会升级为表锁(因为行锁是对索引项加的锁,而name没有索引)。
为了验证上面说法的正确性,我们为 name 字段建立索引重新测试:
此时我们可以看到,客户端一,开启事务,然后依然是根据name进行更新。而客户端二,在更新id为1 的数据时,更新成功,并未进入阻塞状态。 这样就说明,我们根据索引字段进行更新操作,就可以避 免行锁升级为表锁的情况
- 间隙锁 & 临键锁
默认情况下,InnoDB 在 repeatable read 事务隔离级别运行, InnoDB 使用 next-key 锁进行搜索和索引扫描,以防止幻读。
- 索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁
- 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
- 索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止
那么间隙锁到底是做什么用的呢?
- 当使用等值查询对一个唯一索引上的不存在的记录进行加锁操作时,MySQL 为了防止其他并发事务在此时插入相同的索引值,会对查询结果的左右间隔空间加锁,从而形成间隙锁,避免其他事务中插入相同索引值的数据,以此来保证数据的一致性和完整性
- 间隙锁可以共存,一个事务的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁
案例演示环节:
(1)索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁
换句话理解:就是一个位置查询数据为空的事务没提交前,不能对这个位置插入数据
我们可以看到 id = 5 的数据不存在,它的左右 id 为3、8,所以 id 3-8 之间的间隙就会被锁住,利用第二个客户端插入id 为 4、7 的数据都失败了,插入id 为 2 的数据成功了
(2)索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
我们知道 InnoDB 的 B+树索引叶子结点是有序的双向链表,因为不是唯一索引,所以我们根据二级索引查询值为18的数据就可能存在多个,但是因为是有序的,所以只需要找到第一个不满足条件的值。所以会对 18 加临键锁,对29之前加间隙锁
(3)索引上的范围查询(唯一索引),访问到第一个不满足条件的值为止
查询的条件为id>=19,并添加共享锁。 此时我们可以根据数据库表中现有的数据,将数据分为三个部 分:
[19] (19,25] (25,+∞]
所以数据库数据在加锁是,就是将19加了行锁,25的临键锁(包含25及25之前的间隙),正无穷的临 键锁(正无穷及之前的间隙)
InnoDB 引擎
逻辑存储结构
(1)表空间
InnoDB 存储引擎逻辑结构的最高层,如果用户启用了参数 innodb_file_per_table ,则每张表都会有一个表空间 (xxx.idb),一个 mysql 实例可以对应多个表空间,用于存储记录、索引等数据
- MySQL 实例是指在一个操作系统进程中运行的一个 MySQL 数据库服务,并且可以独立地处理客户端的连接和请求
- 在MySQL 8.0 中自动开启,那么如何手动开启这个配置呢?
-- 查看系统变量参数值
show variables like 'innodb_file_per_table';
-- 开启功能.
set GLOBAL innodb_file_per_table = 'ON';
(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 日志中,然后这个隐藏列就相当于一个指针,通过它可以找到记录修改前的信息
架构
MySQL 5.5 版本开始,默认使用 InnoDB 存储引擎,擅长事务处理、具有崩溃恢复特性。架构图如下(左内存结构、右磁盘结构)
- 内存结构
从图中我们可以得知,内存结构主要分为四个板块:Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer
(1)Buffer Pool
InnoDB 存储引擎基于磁盘文件存储,访问磁盘硬盘和在内存中访问速度相差较大,为了平衡两者之间的 IO 效率,就设计了一个数据缓冲池,避免每次访问都进行磁盘 IO
在 InnoDB 的缓冲池中不仅缓存了索引页、数据页,还有 undo 页、插入缓存、自适应哈希索引以及 InnoDB 的锁信息等等。
缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增 删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频 率刷新到磁盘,从而减少磁盘IO,加快处理速度
缓冲池以 Page 页为单位,底层采用链表数据结构管理 Page,根据状态 Page 分为三种类型:
- free page:空闲 page,没有被使用
- clean page:被使用 page,但是数据没有被修改过
- dirty page:脏页,被使用 page,数据被修改过,缓冲区中的数据与磁盘数据产生了不一致
-- 查看存储引擎缓冲池大小 value的单位是字节,大约 128M
show variables like 'innodb_buffer_pool_size';
(2)Change Buffer
针对非唯一二级索引页的更改缓冲区,在执行 DML(增删改)语句时,如果这些数据 Page 没有在 Buffer Pool 中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer,在未来数据被读取时,再将数据合并恢复到 Buffer Pool 中,再将合并后的数据刷新到磁盘中
通过二级索引的结构图,我们来简单分析一下 Change Buffer 的意义是什么?
与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新 可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO
(3)Adaptive Hash Index
自适应哈希索引,无需人工干预,是系统根据情况自动完成
- 用于优化对 Buffer Pool 数据的查询
- InnoDB 存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下 hash 索引可以提升速度,则建立 hash 索引
(4)Log Buffer
日志缓冲区,用来保存要写入到磁盘中的 log 日志数据(redo log、undo log),默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务。增加日志缓冲区的大小可以节省磁盘 I/O
- innodb_log_buffer_size :缓冲区大小参数
- innodb_flush_log_at_trx_commit:日志刷新到磁盘实际,主要的取值包含以下三个
- 1:日志在每次事务提交时写入并刷新到磁盘,默认值
- 0:每秒将日志写入并刷新到磁盘一次
- 2:日志在每次事务提交后写入,并每秒刷新到磁盘一次
2.磁盘结构
(1)System Tablespace
系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个标文件或通用表空间中创建的,它可能包含表和索引数据,系统表空间默认文件名为 idbatal
show variables like 'innodb_data_file_path';
(2)File-Per-Table Tablespaces
这个已经说过很多遍了,开启后每个表都会对应一个表空间文件,具体的文件格式根据创建表时选择的存储引擎有关系
-- mysql8 默认开启
show variables like 'innodb_file_per_table';
(3)General Tablespaces
通用表空间,可以在创建表时,指定表空间
-- 创建表空间
create tablespace ts_name add datefile 'file_name' engine = engine_name;
-- 创建表时指定表空间
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),存储在磁盘中
- 当事务提交之后会把所有修改信息都存到该日志中,用于刷新脏页到磁盘时、发生错误时进行数据恢复
- 以循环方式写入重做日志文件,涉及两个文件: ib_logfile0、ib_logfile1
- 后台线程
在 InnoDB 的后台线程中分为四类:Master Thread、IO Thread、Purge Thread、Page Cleaner Thread
(1)Master Thread
核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收
(2)IO Thread
在 InnoDB 存储引擎中大量使用了 AIO 来处理 IO 请求,这样可以极大地提高数据库的性能,我们的 IO Thread 主要负责这些 IO 请求的回调
-- 查看 InnoDB 的状态信息,包含 IO Thread 信息
show engine innodb status \G;
(3)Purge Thread
主要用于回收事务已经提交了的 undo log,在事务提交之后, undo log 可能不用了,用它来回收
(4)Page Cleaner Thread
协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master 的工作压力,减少阻塞
事务原理
- 事务基础
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
这就不得不再次提及事务的四大特性了:ACID
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败
- 一致性(Consistency):事务完成时,必须使所有的数据保持一致状态
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
在这里我们主要研究 MySQL 的 InnoDB 引擎是如何保证事务的四大特性的
- 原子性、一致性、持久化由 InnoDB 中 redo log、undo log 日志保证的
- 隔离性是通过数据库的锁 + MVCC 保证的
- redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用 于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用
(1)简单分析没有 redo log 可能会出现什么问题?
当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果 缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中 的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘 中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后 将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却 没有持久化下来,这就出现问题了,没有保证事务的持久性
那么 redo log 是如何解决上面的这个问题的呢?
当对缓冲区的数据进行增删改之后,首先将操作的数据页的变化记录在 redo log buffer 中,在提交事务时,将 redo log buffer 中的数据刷新到 redo log 磁盘文件中。
过一段时间之后,如果刷新缓冲区的脏页到磁盘时发生错误,就可以借助于 redo log 进行数据恢复,这样保证了事务的持久性。但是如果脏页成功刷新到磁盘 或 涉及到的数据已经落盘,此时 redo log 就没有作用了,可以删除掉,这就是为什么两个 redo log 文件是循环写的。
为什么每次提交事务,都要刷新 redo log 到磁盘中,而不是直接将 bufer pool 中的脏页刷新到磁盘呢?
- 因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。
- 而 redo log 在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的,顺序写的效率远大于随机写的效率,这种先写日志的方式称为 WAL(Write-Ahead Logging)
- undo log
回滚日志用于记录数据被修改前的信息,作用包含两个:提供回滚(保证事务的原子性)和 MVCC(多版本并发控制)
undo log 和 redo log不同,前者是逻辑日志、后者是物理日志
- 日志里存储的是当前执行命令的反命令,比如执行 delete 命令,日志会记录一条相反的 insert 命令
- 可以这样理解:回滚不是撤销我们刚刚执行的命令,而是通过执行对应相反的命令来实现回滚
我们需要知道以下两个概念:
- undo log 销毁: undo log 在事务执行时产生,事务提交时并不会立即删除 undo log,因为这些日志还可能用于 MVCC
- undo log 存储:undo log 采用段的方式进行管理和记录,存放在前面介绍的 rollback segment 回滚段中,内部包含 1024 个 undo log segment
MVCC
- 介绍几个相关的基本概念
(1)当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
在测试中我们可以看到,即使是在默认的RR隔离级别下,事务A中仍然可以读取到事务B最新提交的内容,就是因为我们在查询语句后面加上了 lock in share mode
共享锁,此时是读操作,当我们加排他锁时也是读操作
(2)快照读
简单的 select(不加锁)就是快照读,读取的是记录数据的可见版本,可能是历史数据,不加锁的度是非阻塞读
- Read Committed:每次 select 都会产生一个快照读
- Repeatable Read:开启事务后第一个 select 语句才是快照读的地方
- Serializable:快照读会退化为当前读
快照读就是不加锁的 select,另一个客户端提交了更新事务,左侧查询到的还是原来的结果
在测试中,我们看到即使事务B提交了数据,事务A中也查询不到。 原因就是因为普通的select是快照 读,而在当前默认的RR隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同 的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。
(3)MVCC
多版本并发控制(Multi-Version Concurrency Control),指维护一个数据的多个版本,使得读写操作没有冲突,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log 日志、readView
- 隐藏字段
如果为表指定了主键,那么就不会生成隐藏主键
-- 查看我们 stu 表的 ibd 信息 >> 寻找隐藏字段的信息
cd /var/lib/mysql/zwh
-- 查看表结构信息
idb2sdi stu.ibd
- undolog
在 insert、update、delete 的时候会产生回滚日志,就是为了便于数据回滚
- 在 insert 的时候产生的 undo log 日志只在回滚时需要,在事务提交后,可以被立即删除
- 在 update、delete 的时候产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不能被删除
我们来看一张表原始数据
DB_TRX_ID 代表最近修改事务 ID,记录插入这条记录或最后一次修改记录的事务ID,是自增的
DB_ROLL_PTR: 由于这是一条新鲜的数据,没有被更新过,所以这个字段值为 null
假设现在有四个并发事务同时访问这张表:
当事务2执行第一条修改语句时,会记录 undo log 日志,记录数据改变之前的样子,然后更新记录,并且记录本次操作的事务ID,回滚指针用来指定如果发生回滚,那么应该回滚到哪个版本
当事务3执行第一条修改语句时,也是记录 undo log 日志,记录数据改变前的样子,然后更新记录,并且记录本次操作的事务ID,回滚指针记录前一个版本
事务4也有 update 语句根他们的流程差不多
不同事务或相同事务对同一条记录进行修改,会导致该记录的 undo log 生成一条记录版本的链表,链表的头部是最新的旧记录,链表的尾部是最早的旧记录
- readView
读视图时快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务id(未提交)
在 readview 中规定了版本链数据的访问规则:trx_id 代表当前 undo log版本连对应事务 ID
不同的隔离级别,生成 ReadView 的时机不同:
- read commited:在事务中每一次执行快照读时生成 ReadView
- repeatable read:仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView
- 原理分析
(1)RC 隔离级别
RC 隔离级别下,在事务中每一次执行快照读时生成 ReadView
我们来分析下图中事务5两次快照读读取数据,到底是如何获取数据的?
在事务5中,查询了两次 id 为 30的记录,由于隔离级别为 Read Committed,所以每一次进行快照读都会产生一个 ReadView
A.先看第一次快照读的读取过程:
在进行匹配时,会从 undo log 的版本链从上到下挨个匹配:
- 先匹配最上面的记录,对应的 trx_id 为4,然后将 4 带入右侧的匹配规则,发现四条规则都不满足,则继续匹配 undo log 版本链的下一条
- 再匹配第二条,其 trx_id 为 3,带入规则,也是都不满足,继续向下匹配
- 再匹配第三条,其 trx_id 为 2,第一条规则不满足,第二条规则满足 >> 终止匹配,返回版本链中记录的这条数据
B.查看第二次快照读取的过程:
(2)RR隔离级别
-
RR隔离级别下,仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView
-
RR是可重复读,在一个事务中,执行两次相同的 select 语句查询到的结果是一样的
MySQL 是如何实现可重复读的呢?
我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的
总结: MVCC 的实现原理就是通过 InnoDB 表的隐藏字段、UndoLog 版本链、ReadView 来实现的。而 MVCC + 锁,则实现了事务的隔离性,而一致性则是由 redolog 与 undolog 保证
MySQL 管理
在安装完 MySQL 数据库后,我们发现自带了四个数据库
常用工具
1.mysql 客户端工具
-- 语法
mysql [options] [database]
-- 选项
-u 指定用户名、-p 指定密码、-h 指定服务器IP或域名、-P 指定连接端口、-e 执行SQL语句并退出
-- 案例演示,查询 stu 表的所有信息
mysql -uroot -p111111 -e "select * from stu";
-- 如果是 docker 创建的 mysql,可以通过通过下面这个指令进入容器内部,然后再进入 mysql 数据库
docker exec -it mysql /bin/bash
2.mysqladmin
一个执行管理操作的客户端程序,可以用它来检查服务器的配置和当前的状态、创建并删除数据库等
-- 语法
mysqladmin [options] command ...
-- 选项
-u 用户名、-p 指定密码、-h 指定服务器IP或域名、-P 指定连接端口
-- 使用 MySQL 管理命令删除 test 数据库
mysqladmin -uroot -p111111 drop 'test';
-- 查看管理操作客户端程序的版本
mysqladmin -uroot -p111111 version;
3.mysqlbinlog
如果想要检查服务器生成的以二进制格式保存的二进制日志文件,就要用到我们的 mysqlbinlog 日志管理工具
-- 语法
mysqlbinlog [options] log-files log-files2 ...
-- 选项
-d 指定数据库名称,只列出指定的数据库相关操作
-o 忽略掉日志中的前 n 行命令
-r 将输出的文本格式日志输出到指定文件
-s 显示简单格式,省略掉一些信息
--start-datatime=data1 --stop-datatime=data2 指定日期间隔内的所有日志
--start-position=pos1 --stop-position=pos2 指定位置间隔内的所有日志
-- 查看 binlog.000008 这个二进制文件中的数据信息
mysqlbinlog -s binlog.000001
4.mysqlshow
是一个客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引
-- 语法
mysqlshow [options] [db_name [table_name [col_name]]]
-- 选项
--count 显示数据库及表的统计信息(可以不指定数据库、表)
-i 显示指定数据库或者指定表的状态信息
-- 查询 zwh 库中每个小中的字段数及行数
mysqlshow -uroot -p111111 zwh --count
-- 查询 zwh 库 stu 表的详细情况
mysqlshow -uroot -p111111 zwh stu --count
-- 查询 zwh 库 stu 表 id 字段的详细情况
mysqlshow -uroot -p111111 zwh stu id --count
5.mysqldump
mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包括创建表、插入表的 SQL 语句
-- 语法
mysqldump [options] db_name [tables]
mysqldump [options] --database / -B db1 [db2 db3 ...]
mysqldump [options] --all-databases/ -A
-- 连接选项
-u 用户名、-p 密码、-h 服务器IP或域名、-P 连接端口
-- 输出选项
--add-drop-database 在每个数据库创建语句前加上 drop database 语句
--add-drop-table 在每个表创建语句前加上 drop table 语句,默认开启,不开启 (--skip-add-drop-table)
-n 不包含数据库的创建语句
-t 不包含数据表的创建语句
-d 不包含数据库
-T 指定生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件
(1)备份数据库 zwh
-- 通过指定一些参数可以实现定制化备份
mysqldump -uroot -p111111 zwh > zwh.sql
-- 查看备份文件中的内容
cat zwh.sql
备份出来的数据包括:删除表的语句、创建表的语句、数据插入语句
(2)参数使用需要注意 -T
参数会将表结构和数据生成两个文件,数据存放的目录必须是 MySQL 信任的目录
-- 查看 mysql 信任的目录
show variables like 'secure_file_priv';
-- 如果信任目录为空,我们可以自己指定以下
set @@global secure_file_priv = '/var/lib/mysql-files';
-- 执行定制化备份
mysqldump -uroot -p111111 -T /var/lib/mysql-files/ zwh stu
6.mysqlimport/source
(1)mysqlimport
客户端数据导入工具,用来导入 mysqldump 加 -T 参数后导出的文本文件
-- 语法
mysqlimport [options] db_name textfile1 [textfile2]
-- 案例
mysqlimport -uroot -p111111 zwh /tmp/city.txt
(2)source
如果需要导入 sql 文件,可以使用 mysql 中 source 指令
-- 语法
source /root/xxx.sql
日志
错误日志
错误日志是 MySQL 最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,建议首先查看此日志。
该日志是默认开启的,默认存放目录 /var/log,默认的日志文件名为 mysqld.log
-- 查看日志位置
show variables like '%log_error%';
-- 我们可以将错误日志输出到指定的文件中
# 将日志文件输出到 /var/log/mysql/error.log
[mysqld]
log_error=/var/log/mysql/error.log
# 命令行的方式指定输出路径
$ mysqld --log-error=/var/log/mysql/error.log
二进制日志
二进制日志(binlog) 记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但不包括数据查询(select show)语句。主要的作用如下:
- 容灾时的数据恢复
- MySQL 的主从赋值,在 MySQL8 中,二进制日志是默认开启的
show variables like '%log_bin%';
-
log_bin_basename: 当前数据库服务器的 binlog 日志的基础名称(前缀),需要在后面拼接上编号(从000001开始)
-
log_bin_index: binlog 的索引文件,里面记录了当前服务器关联的 binlog 文件有哪些。
MySQL 服务器中提供了多种格式来记录二进制日志,具体格式及特点如下:
show variables like '%binlog_format%'
如果我们需要配置二进制日志的格式,需要在 /etc/my.cnf 中配置 binlog_format 参数即可
二进制日志文件是以二进制方式存储的,不能直接读取,我们可以使用 mysqlbinlog 日志查询工具来查看
-- 语法
mysqlbinlog [参数选项] logfilename
-- 参数选项
-d 数据库名、-o 忽略日志中前n行命令、-v 将行事件(数据变更)重构为 SQL 语句
-vv 将事件(数据变更)重构为 SQL 语句,并输出注释信息
清理日志的方法:
也可以在mysql的配置文件中配置二进制日志的过期时间,设置了之后,二进制日志过期会自动删除
show variables like '%binlog_expire_logs_seconds%';
查询日志
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句。默认情况下,查询日志是未开启的
可以通过修改配置文件 /etc/my.cnf 文件来开启查询日志
-- 0 代表关闭,1 代表开启
general_log = 1
-- 设置日志的文件名,默认文件名为 host_name.log
general_log_file = mysql_query.log
开启了查询日志之后,在 MySQL 的数据存放目录下会出现对应的日志文件,之后所有客户端的增删改查操作都会记录在该日志文件中,长时间运行后,该日志文件将会非常大
慢查询日志
慢查询日志记录了所有执行时间超过参数 log_query_time(默认10s,最小为0s,可以精确到微秒) 设置值并且扫描记录数不小于 min_examined_row_limit 的所有 SQL 语句日志,默认未开启
-- 修改 MySQL 配置文件 /etc/my.cnf
# 开启慢查询日志
slow_query_log = 1
# 执行时间参数
long_query_time = 2
# 记录执行较慢的管理语句
log_slow_admin_statements = 1
# 记录执行较慢的未使用索引的语句
log_queries_not_using_indexed = 1
-- 重启 MySQL 服务生效
主从复制
概述
主从复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行,从而使得从库和主库的数据保持同步。
MySQL 支持一台主库同时向多台从库进行复制,从库同时也是可以作为其他从服务器的主库,实现链式复制。
MySQL 复制的优点主要包含以下三个方面:
- 主库出现问题,可以快速切换到从库提供服务
- 实现读写分离,降低主库的访问压力
- 可以在从库中执行备份,以避免备份期间影响主库服务
原理
MySQL 主从复制的核心就是 二进制日志,具体过程如下:
- Master 主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中
- 从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log
- slave 重做中继日志中的时间,将改变反映成从库自己的数据
搭建
1.准备工作
准备好两台服务器之后,在上述的两台服务器中分别安装好 MySQL,并完成基础的初始化准备(安装、密码配置登操作)工作。其中:
- 192.168.200.200 主服务器 master
- 192.168.200.201 从服务器 slave
2.修改主库配置
(1)修改配置文件 /etc/my.cnf
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 – 232-1,默认为1
server-id=1
#是否只读,1 代表只读, 0 代表读写
read-only=0
#忽略的数据, 指不需要同步的数据库
#binlog-ignore-db=mysql
#指定同步的数据库
#binlog-do-db=db01
(2)重启 MySQL 服务器
systemctl restart mysqld
-- 重启 docker mysql 容器
docker restart 容器ID
(3)登录 mysql,创建远程连接的账号,并授予主从复制权限
#创建 test 用户,并设置密码,该用户可在任意主机连接该 MySQL 服务
create user 'zwh'@'%' identified with mysql_native_password by 'root@123456';
# 为用户分配主从复制权限
grant replication slave on *.* to 'zwh'@'%';
(4)通过指令查看二进制日志坐标
show master status;
- file 从哪个日志文件开始推送日志文件
- position:从哪个位置开始推送日志
- binlog_ignore_db: 指定不需要同步的数据库
3.修改从库配置
(1)修改配置文件 /etc/my.cnf
#mysql 服务ID,保证整个集群环境中唯一,取值范围:1 – 2^32-1,和主库不一样即可
server-id=2
#是否只读,1 代表只读, 0 代表读写
read-only=1
(2)重新启动MySQL服务
systemctl restart mysqld
(3) 登录mysql,设置主库配置
CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.200.200', SOURCE_USER='itcast',
SOURCE_PASSWORD='Root@123456', SOURCE_LOG_FILE='binlog.000004',
SOURCE_LOG_POS=663;
上述是8.0.23中的语法。如果mysql是 8.0.23 之前的版本,执行如下SQL:
CHANGE MASTER TO MASTER_HOST='192.168.200.200', MASTER_USER='itcast',
MASTER_PASSWORD='Root@123456', MASTER_LOG_FILE='binlog.000004',
MASTER_LOG_POS=663;
(4)开启同步操作
start replica ; #8.0.22之后
start slave ; #8.0.22之前
(5)查看主从同步状态
show replica status ; #8.0.22之后
show slave status ; #8.0.22之前
转载自:https://juejin.cn/post/7235493849073631290