索引
在无索引情况下,就需要从第一行开始扫描,一直扫描到最后一行,我们称之为 全表扫描,性能很低。
如果我们针对于这张表建立了索引,假设索引结构就是二叉树,那么也就意味着,会对age这个字段建立一个二叉树的索引结构。
优势:
1.提高数据查询的效率,降低数据库的IO成本。(数据库的数据是存在磁盘的,你要查询就要操作磁盘就会有IO)
2.通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。
劣势:
1.索引列也是要占用空间的。
2.降低更新表的速度,对表进行DML时,效率降低。
以下为MYSQL支持的所有索引数据结构和相应的支持引擎。
索引结构
B-tree
B树的定义
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
-
1.树中每个结点至多有m棵子树,即至多含有m−1个关键字。
-
2.若根结点不是终端结点,则至少有两棵子树。
-
3.除根结点外的所有非叶结点至少有⌈m/2⌉棵子树,即至少含有⌈m / 2 ⌉−1个关键字。
-
4.所有非叶结点的结构如下:
其中,n为元素个数,K代表节点的关键字,P代表指针。满足K1<K2<…<Kn;Pi指针指向子树的根节点,Pi-1所指的子树中所有节点关键字小于Ki,Pi所指子树中所有节点关键字大于Ki。
节点中关键字个数有限制为:n(⌈m/2⌉-1<= n <= m-1) -
5.所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key,5
个指针:
B树的插入
-
1.定位。找出插入该关键字的最低层中的某个非叶结点(在B树中查找key时,会找到表示查找失败的叶结点,这样就确定了最底层非叶结点的插入位置。注意:插入位置一定是最低层中的某个非叶结点)。
-
2.插入。每个非失败结点的关键字个数都在区间[ [⌈m/2⌉−1,m−1]内。插入后的结点关键字个数小于m,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于m−1时,必须对结点进行分裂。
-
3.分裂:取一个新结点,在插入key后的原结点,从中间位置⌈m/2⌉将其中的关键字分为三部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置⌈m/2⌉的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度+1
B树的查找
在B树上查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息到所指的子树中去查找。
B树的查找包含两个基本操作:①在B树中找结点;②在结点内找关键字。
由于B树常存储在磁盘上,因此前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的,即在找到目标结点后,先将结点信息读入内存,然后在结点内采用顺序查找法或折半查找法。
例如,在上图中查找关键字42,首先从根结点开始,根结点只有一个关键字,且42>22,若存在,必在关键字22的右边子树上,右孩子结点有两个关键字,而36<42<45,则若存在,必在36和45中间的子树上,在该子结点中查到关键字42,查找成功。若查找到叶结点时(对应指针为空指针),则说明树中没有对应的关键字,查找失败。
B+ 树
B+树是应文件系统(比如数据库)所需而出现的一种B树的变形树。
m阶的B+树与m阶的B树的主要差异如下:
- 1.有n棵子树的结点中包含有n个关键字;
- 2.所有的数据都在叶子节点。叶子结点本身依关键字的大小自小而大顺序链接;
- 3.所有分支(非叶子)结点可以看成是索引,不含具体数据,结点中仅含有其子树中的最大(或最小)关键字。
- 4.在B+树中,每个结点(非根内部结点)的关键字个数n的范围是⌈m/2⌉≤n≤m(根结点:1≤n≤m);在B树中,每个结点(非根内部结点)的关键字个数n范围是⌈m/2⌉−1≤n≤m−1 (根结点: 1≤n≤m−1)。
在mysql中对B+tree进行了一定优化。:在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。
Hash
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
特点:
- A. Hash索引只能用于对等比较(=,in),不支持范围查询(between,>,< ,…)
- B. 无法利用索引完成排序操作
- C. 查询效率高,通常(不存在hash冲突的情况)只需要一次检索就可以了,数据较少时效率通常要高于B+tree索引。
索引分类
在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。
而在Innodb引擎中,又可以分为:聚集索引和二级索引。
聚集索引(Clustered Index):将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。必须有,而且只有一个。
- 如果存在主键,主键索引就是聚集索引。
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
- 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。
二级索引:将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键。可以存在多个。
- 聚集索引的叶子节点下挂的是这一行的数据 。
- 二级索引的叶子节点下挂的是该字段值对应的主键值。
查找举例:
1.根据name字段进行查询,根据name='Arm’到name字段的二级索引中进行匹配查找。但是在二级索引中只能查找到 Arm 对应的主键值 10。
2.由于查询返回的数据是*,所以此时,还需要根据主键值10,到聚集索引中查找10对应的记录,最终找到10对应的行row。
3.最终拿到这一行的数据,直接返回即可。
索引语法
创建索引
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name ( index_col_name,… ) ;
name字段为姓名字段,该字段的值可能会重复,为该字段创建索引。
CREATE INDEX idx_user_name ON tb_user(name);
为profession、age、status创建联合索引。
CREATE INDEX idx_user_pro_age_sta ON tb_user(profession,age,status);
phone手机号字段的值,是非空,且唯一的,为该字段创建唯一索引。
CREATE UNIQUE INDEX idx_user_phone ON tb_user(phone);
查看索引
SHOW INDEX FROM table_name ;
删除索引
DROP INDEX index_name ON table_name ;
索引使用
索引失效
最左前缀法则
如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。
在 tb_user 表中,有一个联合索引,这个联合索引涉及到三个字段,顺序分别为:profession,age,status。
对于最左前缀法则指的是,查询时,最左的列,也就是profession必须生效,否则索引全部失效。
PS:必须存在,但是顺序无所谓。索引无效不代表无法查询,只是用于辅助查询的索引失效了。
范围查询
联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。
例如
select * from tb_user where profession = ‘软件工程’ and age > 30 and status= ‘0’;
这里的status字段的索引无效。
运算引起失效
不要在索引列上进行运算操作, 索引将失效。
select * from tb_user where substring(phone,10,2) = ‘15’;
这样会使得phone失效。
字符串不加引号失效
字符串类型字段使用时,不加引号,索引将失效。
这样写没问题
explain select * from tb_user where profession = ‘软件工程’ and age = 31 and status = ‘0’;
但是由于数据库中status是字符串类型,这样不加引号会导致数据库做一次隐式转换,从而又导致了运算,引起失效。
explain select * from tb_user where profession = ‘软件工程’ and age = 31 and status = 0;
模糊查询
在左侧添加模糊查询会使得索引失效。
生效
explain select * from tb_user where profession like ‘软件%’;
失效
explain select * from tb_user where profession like ‘%工程’; explain select * from tb_user where profession like ‘%工%’;
or连接
当or连接的条件,左右两侧字段都有索引时,索引才会生效。
如果age没有索引,但是id和phone有索引那么。
explain select * from tb_user where id = 10 or age = 23;
explain select * from tb_user where age = 23 or phone = ‘17799990017’;
数据分布影响
MySQL在查询时,会评估使用索引的效率与走全表扫描的效率,如果走全表扫描更快,则放弃索引,走全表扫描。
因为索引是用来索引少量数据的,如果通过索引查询返回大批量的数据,则还不如走全表扫描来的快,此时索引就会失效。
SQL提示
use index : 建议MySQL使用哪一个索引完成此次查询
例如
explain select * from tb_user use index(idx_user_pro) where profession = ‘软件工程’;
ignore index : 忽略指定的索引。
例如
explain select * from tb_user ignore index(idx_user_pro) where
profession = ‘软件工程’;
force index : 强制使用索引。
例如
explain select * from tb_user force index(idx_user_pro) where
profession = ‘软件工程’;
索引使用性能优化
覆盖索引
尽量使用覆盖索引,减少select *。
我们在创建索引的时候就已经把表中的id和很多字段给放到索引中了,但是如果你查找的数据不再索引中,就还是会造成回表查询,增加io次数。我们就是要尽量避免这种情况。
先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取数据的方式,就称之为回表查询。
explain中会有extra字段。
前缀索引
有些字段的长度很长,查询的时候进行比对就会造成大量的IO,影响查询的效率,我们可以对这种字符串建立索引,从而提高索引效率。
例如:
create index idx_email_5 on tb_user(email(5));
联合索引
针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
单列索引:即一个索引只包含单个列。
联合索引:即一个索引包含了多个列。
假如我们频繁查询的数据是phone和name两个字段,我们可以选择建立两个单列索引和一个联合索引。
但是如果我们的select语句中同时用phone和name作为判断变量,那么如果选择单列索引,还是会造成回表查询。
联合索引的索引示意。
索引的简历原则
-
- 针对于数据量较大,且查询比较频繁的表建立索引。
-
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索
引。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索
-
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
-
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
-
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
-
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
-
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
SQL优化
insert优化
如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。
批量插入数据
Insert into tb_test values(1,‘Tom’),(2,‘Cat’),(3,‘Jerry’);
手动控制事务
start transaction; insert into tb_test
values(1,‘Tom’),(2,‘Cat’),(3,‘Jerry’); insert into tb_test
values(4,‘Tom’),(5,‘Cat’),(6,‘Jerry’); insert into tb_test
values(7,‘Tom’),(8,‘Cat’),(9,‘Jerry’);
commit;
插入大批量数据,用MySQL数据库提供的load指令进行插入
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n' ;
主键优化
在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的。
页分裂
B+树将聚集索引存储在叶子节点,一个叶子节点可以看做一个页面,如果插入的数据不断增加,会进行“页分裂”。
顺序插入主键
我们顺序插入主键时,当一个页的空间不够了会进行页分裂然后插入到下一个页中。
乱序插入主键
但是当我们乱序插入时,就会造成频繁的页分裂,还会造成频繁的存储修改和指针修改。
所以对于主键的设计有如下原则。
1.尽量降低主键长度。
2.尽量选择自增主键,循序插入。
3.尽量不要用无规则主键。
4.尽量避免对主键的修改。
页合并
如果数据删除,则会在页面空间小于一定阈值时进行“页合并”。
order by优化
B+树自身有一定顺序,所以在排序时尽量使用覆盖索引。
比如:
create index idx_user_age_phone_aa on tb_user(age,phone);
创建索引后,根据age, phone进行升序排序
select id,age,phone from tb_user order by age;
需要注意的是,order by的多个字段的顺序,必须和索引相应字段的顺序都一致或都相反,会无法完成覆盖索引。
例如
如下sql语句的两个字段age和phone和索引中的是相反顺序,但是由于都是相反的,就仍然可以完成覆盖索引。
select id,age,phone from tb_user order by age desc , phone desc ;
但是一个升序,一个降序,就无法完成覆盖索引。
explain select id,age,phone from tb_user order by age asc , phone desc ;
group by优化
分组操作优化也是尽量使用覆盖索引,避免回表查询。
注意:gropu by后跟的关键字也是要符合最左原则的。
比如,如下sql可以完成覆盖索引。
select profession ,count(*) from tb_user group by profession , age;
但是这样就不行了。
select profession ,count(*) from tb_user group by age;
limit优化
通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。
select * from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id;
count优化
- MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高; 但是如果是带条件的count,MyISAM也慢。
- InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
效率方面:
count(字段) < count(主键 id) < count(1) ≈ count(*)。
所以尽量使用 count(*)。
update优化
当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。
update course set name = ‘javaEE’ where id = 1 ;
但是当我们在执行如下SQL时。
update course set name = ‘SpringBoot’ where name = ‘PHP’ ;
当我们开启多个事务,在执行上述的SQL时,我们发现行锁升级为了表锁。 导致该update语句的性能大大降低。
存储过程
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
从思想来看就是sql语句的封装和复用。
但是,,个人觉得这个东西其实都是可以在jvm里实现的。
基本语法
创建
CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
-- SQL语句
END ;
调用
CALL 名称 ([ 参数 ]);
查看
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询指定数据库的存储过程及状态信息
HOW CREATE PROCEDURE 存储过程名称 ; -- 查询某个存储过程的定义
删除
DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;
例如
create procedure p1()
begin
select count(*) from student;
end;
-- 调用
call p1();
-- 查看
select * from information_schema.ROUTINES where ROUTINE_SCHEMA ='itcast';
show create procedure p1;
-- 删除
drop procedure if exists p1;
参数
- IN 该类参数作为输入,也就是需要调用时传入值 。是默认参数类型
- OUT 该类参数作为输出,也就是该参数可以作为返回值。
- INOUT 既可以作为输入参数,也可以作为输出参数。
CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
-- SQL语句
END ;
变量
系统变量
查看
SHOW [ SESSION | GLOBAL ] VARIABLES ; – 查看所有系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES LIKE ‘…’; – 可以通过LIKE模糊匹配方式查找变量
SELECT @@[SESSION | GLOBAL] 系统变量名;
赋值
SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;
用户变量
创建与赋值
SET @变量名 = 值 [, @变量名 = 值] ;
SELECT 字段名 INTO @变量名 FROM 表名;
使用
SELECT @var_name ;
局部变量
创建
DECLARE 变量名 变量类型 [DEFAULT 值] ;
赋值
SET 变量名 = 值 ;
SELECT 字段名 INTO 变量名 FROM 表名;
例如
create procedure p2()
begin
declare stu_count int default 0;
select count(*) into stu_count from student;
select stu_count;
end;
call p2();
if-Then
IF 条件1 THEN
.....
ELSEIF 条件2 THEN -- 可选
.....
ELSE -- 可选
.....
END IF;
例如
create procedure p3()
begin
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 p3();
case
创建
语法1
CASE case_value
WHEN when_value1 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;
语法2
CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
例如
create procedure p6(in month int)
begin
declare result varchar(10);
case
when month >= 1 and month <= 3 then
set result := '第一季度';
when month >= 4 and month <= 6 then
set result := '第二季度';
when month >= 7 and month <= 9 then
set result := '第三季度';
when month >= 10 and month <= 12 then
set result := '第四季度';
else
set result := '非法参数';
end case ;
select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);
end;
call p6(16);
while
与while类似的还有repeat和loop,功能上来讲都可以用while替代,感兴趣可以自己去看一下。
WHILE 条件 DO
SQL逻辑...
END WHILE;
例如
create procedure p7(in n int)
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
select total;
end;
call p7(100);
游标
用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进
行循环的处理。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下。
通俗来讲,就是一个存放SQL——结果集的变量,打开就读了sql,然后通过fetch就可以读结果集。
创建游标
DECLARE 游标名称 CURSOR FOR 查询语句 ;
打开游标
OPEN 游标名称 ;
获取游标记录
FETCH 游标名称 INTO 变量 [, 变量 ] ;
关闭游标
CLOSE 游标名称 ;
例如
create procedure p11(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 p11(30);
Handler
用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。
基本语法:
DECLARE handler_action HANDLER FOR condition_value [, condition_value]... statement ;
handler_action 的取值:
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition_value 的取值:
SQLSTATE sqlstate_value: 状态码,如 02000
SQLWARNING: 所有以01开头的SQLSTATE代码的简写
NOT FOUND: 所有以02开头的SQLSTATE代码的简写
SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
例如
create procedure p11(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;
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;
call p11(30);
上面的
declare exit handler for SQLSTATE ‘02000’ close u_cursor;
可以改为
declare exit handler for not found close u_cursor;
函数
这个我觉的就更没必要在sql里写了
存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。具体语法如下:
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
-- SQL语句
RETURN ...;
END ;
对于characteristic
- DETERMINISTIC:相同的输入参数总是产生相同的结
- NO SQL :不包含 SQL 语句。
- READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句。
create function fun1(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 fun1(50);
锁
在多线程并发访问数据库时,锁用于确保数据的一致性和有效性。
按照锁的粒度讲MySQL的锁分为三类
- 全局锁:锁定数据库中的所有表。
- 表级锁:每次操作锁住整张表。
- 行级锁:每次操作锁住对应的行数据。
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
基本语法
加全局锁
flush tables with read lock ;
数据备份
mysqldump -uroot –p1234 itcast > itcast.sql
释放锁
unlock tables ;
表级锁
锁定粒度大,发生锁冲突的概率高,并发度低。应用在MyISAM、InnoDB、BDB等存储引擎中。
表锁
加锁
lock tables 表名… read/write。
释放锁
客户端断开连接默认释放锁
unlock tables
共享锁(read lock):对于所有线程写操作全部拒绝,但是允许所有线程的读操作。
排他锁(write lock):允许持锁线程读写,拒绝其他线程一切读写
元数据锁
meta data lock , 元数据锁,简写MDL。
MDL加锁过程由系统自动控制,无需显式使用,访问表时自动获取。
MDL作用是维护表元数据(表明,字段名,字段类型)的数据一致性,表上有活动事务时,不可以对表元数据进行写入操作。避免DML与DDL和DQL冲突,保证读写的正确性。
也就是说,会给DML和DDL语句执行时加MDL。DQL会获取SHARED_READ,DML会获取SHARED_WRITE,而DDL会获取EXCLUSIVE。EXCLUSIVE和另外两个都是互斥的。
意向锁
在InnoDB中使用意向锁来减少表锁的检查,使表锁无需检查每行数据是否加锁。
意向锁的作用是避免DML或DQL在执行时,行锁与表锁的冲突。
原理:对涉及到的行加锁同时也对整个表加意向锁,其他线程想要加表锁时不必逐行判断行锁,而是通过判断想要添加的表锁和意向锁是否冲突,从而判断能否成功添加表锁。
意向共享锁:与 表锁共享锁(read)兼容,与表锁排他锁(write)互斥。
select … lock in share mode
意向排他锁:与表锁共享锁(read) 及 排他锁(write)都互斥。
由DML自动添加。
行级锁
每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。
- InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。
- 对于无索引字段不会添加行锁,会直接添加表锁。
行锁(Record Lock)
锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
共享锁:允许一个事务去读一行,对于此行,阻塞想要获取排他锁的进程。不同线程对对同一行数据获取共享锁是允许的。
添加和意向锁的操作一致。
select … lock in share mode
排他锁:允许获取排他锁的事物对数据更新,阻塞想获取改行任何锁的进程。
由DML自动添加
间隙锁(gap lock)
锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别支持。
临键锁(Next-Key Lock)
行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。
间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 临键锁进行搜索和索引扫描,以防止幻读。
- 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
- 索引上的等值查询(非唯一普通索引),向右遍历到最后一个不满足查询需求的值时,临键锁退化为间隙锁。
- 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。
Innodb
InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,那么InnoDB存储引擎是比较合适的选择。
表空间
表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。
段
段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点, 索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。
区
区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。
页
页,是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。
行
InnoDB 存储引擎数据是按行进行存放的。
基本架构
内存架构
Buffer Pool
缓冲池。是主存中的一个区域,用于缓冲磁盘和内存之前的访问速率差值,将经常访问的数据加载到缓冲池,在执行DML和DQL时先操作缓冲池中的数据,之后以一定频率刷新到磁盘,减少了磁盘IO。在专用服务器上通常将80%内存分配给缓冲池。
缓冲池中包含:索引页,数据页,undo页,插入缓存,自适应哈希索引,锁信息。
缓冲池中的页分为三种。
- free page:空闲page,未被使用。
- clean page:被使用page,数据没有被修改过。
- dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。
Change Buffer
更改缓冲区。当DML操作的数据不在缓冲池时,先将数据变更存入更改缓冲区,直到数据被读取时,将该数据合并入缓冲池中。避免了DML操作对索引的影响从而产生大量磁盘IO。
Adaptive Hash Index
自适应哈希索引。Innodb会在某种特定条件下使用hash索引,这种索引无需人工调控。
Log Buffer
日志缓冲区。默认大小16MB,存储需要存入磁盘的log文件,定期刷新到磁盘。
磁盘结构
System Tablespace
系统表空间。是上述内存架构的更改缓冲区存储区域。也可能会有表的数据和索引。
File-Per-Table Tablespaces
文件表空间。每创建一个表都产生一个对应存储表数据和索引的表空间。
General Tablespaces
通用表空间。用户主动创建的表空间。可在创建表时指定该空间。
创建表空间
CREATE TABLESPACE ts_name ADD DATAFILE ‘file_name’ ENGINE =engine_name;
指定该空间
CREATE TABLE xxx … TABLESPACE ts_name;
Undo Tablespaces
撤销表空间。用于存储undo log日志。
Temporary Tablespaces
临时表空间。用于存储用户创建的会话临时表或全局临时表。
Doublewrite Buffer Files
双写缓冲区。缓冲池将数据刷新到磁盘前,先将数据写入双写缓冲区,便于恢复数据。
Redo Log
重做日志。分为重做日志缓冲和重做日志文件。前者存于内存后者存于磁盘。事物提交后将修改信息存入重做日志文件,用于数据恢复。
后台线程
Master Thread
核心后台线程。负责调度其他线程以及各种操作。
IO Thread
要负责IO请求的回调。
有四种:
Read thread 4个 负责读操作。
Write thread 4个 负责写操作。
Log thread 1个 负责将日志缓冲区刷新到磁盘。
Insert buffer thread 1个 负责将写缓冲区内容刷新到磁盘。
Purge Thread
用于回收事务提交后不可用的undo log。
Page Cleaner Thread
协助 Master Thread 刷新脏页到磁盘的线程
事物支持
谈到事物支持,就是谈如何满足事物的四个特性:原子,一致,持久,隔离。
Innodb有两份日志,锁和mvcc模式来保证事物实现这些特性。
MVCC(弱一致性,多版本并发控制)
MVCC(Multi-Version Concurrency Control),多版本并发控制。维护一个数据的多个版本,实现读写无冲突,通过快照读实现非阻塞读功能。具体的实现依赖于隐藏字段,undo log链,ReadView
当前读:读取记录的最新版本,读取时需要加锁。可避免脏读。
快照读:读取当前可见版本有可能是历史数据,无需加锁。通过undo log版本链实现。
- Read Committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
普通的select是快照读,而在默认的RR隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。
- Serializable:快照读会退化为当前读。
隐藏字段和undo log链
- DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
- DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
- DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
回滚指针和最近修改事物ID
如下是一条insert的原始数据。可见最近修改事物ID是1,因为新插入所以回滚指针是null
而后有如下四个事物并发访问。
当执行完事物2之后。
当执行完事物3之后
readview
读视图。作为MVCC为快照读SQL提供数据时的依据,记录当前系统活跃事物的id。
- m_ids 当前活跃的事务ID集合
- min_trx_id 最小活跃事务ID
- max_trx_id 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
- creator_trx_id ReadView创建者的事务ID
trx_id 代表当前undo log版本链对应事务ID。
- READ COMMITTED :在事务每一次执行快照读都生成ReadView。
- REPEATABLE READ:在事务第一次快照读时生成ReadView,后续复用该ReadView。
举例分析
上述事物5在读取时就会分两次产生两个readview。
以第一个readview为例。在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:
其实看到这里也就明白了MVCC实际上是就是通过“维护”数据版本来实现的非阻塞读,提高了并发性,这也是弱一致性的体现。
由此可见,高并发和高一致是互斥的概念,并非一致性弱就不好,也并非高一致性就好,具体业务需要具体权衡。