Mysql学习之路(三)索引、存储引擎、锁

文章目录

  本系列文章:
    Mysql学习之路(一)数据类型、关联查询、常用函数、事务、视图
    Mysql学习之路(二)Mysql SQL语句练习题
    Mysql学习之路(三)索引、存储引擎、锁
    Mysql学习之路(四)Mysql架构、Mysql数据库优化
    Mysql学习之路(五)SQL语句优化、慢查询、执行计划

一、索引

  索引的简单理解:

  • 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针
  • 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树
  • 通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间(存储在磁盘上)的

1.1 索引概述

  每种存储引擎(如 MyISAM、InnoDB、BDB、MEMORY 等)对每个表至少支持16个索引,总索引长度至少为256字节。
  MyISAM和InnoDB存储引擎的表默认创建的都是B+树索引。
  MySQL支持前缀索引。
  MySQL还支持全文本(FULLTEXT)索引,该索引可以用于全文搜索。只限于CHAR、VARCHAR 和 TEXT列。
  默认情况下,MEMORY存储引擎使用HASH索引,但也支持B树索引。
  用CREATE关键字创建索引的语法:

	CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
		[USING index_type]
		ON tbl_name (index_col_name,...)
	index_col_name:
		col_name [(length)] [ASC | DESC]

  示例:

	create index cityname on city (city(10));

  删除索引的语法:

	DROP INDEX index_name ON tbl_name

  删除索引示例:

	drop index cityname on city;

  索引设计的一般步骤:

  • 1、整理表上的所有SQL,重点包括select、update、delete操作的where条件所用到的列的组合、关联查询的关联条件等。
  • 2、整理所有查询SQL的预期执行频率。
  • 3、整理所涉及列的选择度。
  • 4、选择合适的主键。没有特别合适的列时,建议使用自增列作为主键。
  • 5、优先给那些执行频率最高的SQL创建索引。
  • 6、按执行频率排序,依次检查是否需要为每个SQL创建索引。
  • 7、索引合并,利用复合索引来降低索引的总数,充分利用最左前缀的原则,让索引尽可能多地复用,同时在保证复用率的情况下,把选择度更高的列放在索引的最左侧。
  • 8、上线后,通过慢查询分析、执行计划分析、索引使用统计,来确定索引的实际使用情况,根据情况做出调整。

1.2 索引的优缺点

  • 索引的优点
      可以大大加快数据的检索速度,提高系统的性能,这也是创建索引的最主要的原因。
  • 索引的缺点
  1. 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
  2. 空间方面:索引需要占物理空间

1.3 可以使用多少列创建索引

  任何标准表最多可以创建 16 个索引列

1.4 索引分类

  在 InnoDB 里面,索引类型有三种,普通索引、唯一索引(主键索引是特殊的唯一索引)、全文索引。
  Mysql索引的5种类型:主键索引、唯一索引、普通索引和全文索引、组合索引。这5种类型索引的特点:

 普通索引:仅加速查询;
 唯一索引:加速查询 + 列值唯一(可以有null)
 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个
 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并;
 全文索引:对文本的内容进行分词,进行搜索。

  • 1、主键索引
      主键是一种唯一性索引,但它必须指定为PRIMARY KEY,每个表只能有一个主键。主键一般情况可以设置为自增,主键自增时,插入数据时都是在最后追加,不会在表中间插入数据,索引方便维护。
      创建主键索引的两种方式示例:
	-- 在创建表的时候,指定主键索引
	create table table_primarykey
	(   id int primary key auto_increment  ,
	    name varchar(20)
	);

	-- 先创建表,再增加主键索引
	create table table_primarykey
	(   id int  auto_increment  ,
	    name varchar(20)
	);
	 
	alter table table_primarykey
	add primary key (id);
  • 2、唯一索引
      索引列的所有值都只能出现一次,即必须唯一,值可以为空。一个表允许多个列创建唯一索引。创建唯一索引示例:
	create unique index 索引名 on 表名(列名1,列名2……)

	ALTER TABLE `table_name` ADD UNIQUE (`column`)
  • 3、普通索引
      基本的索引类型,值可以为空,没有唯一性的限制。示例:
	 create table table_index
	(   id int primary key auto_increment  ,
		-- 在创建表的时候,指定普通索引
	    name varchar(20) ,
		index index_name (name)
	);

	create table table_index
	(   id int primary key auto_increment  ,
	    name varchar(20) 
	);
	-- 建表后,添加普通索引
	alter table table_index
	add index index_name (name);
  • 4、全文索引
      全文索引的索引类型为FULLTEXT。全文索引可以在varchar、char、text类型的列上创建。
      MyISAM支持,Innodb在Mysql5.6之后支持。

  用 like + % 就可以实现模糊匹配了,为什么还要全文索引?like + % 在文本比较少时是合适的,但是对于大量的文本数据检索,是不可想象的。全文索引在大量的数据面前,能比 like + % 快 N 倍,速度不是一个数量级,但是全文索引可能存在精度问题。

  全文索引创建示例:

	--创建article表
	CREATE TABLE article (    
		id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,    
		title VARCHAR(200),    
		content TEXT,    
		--在title和content列上创建全文索引
		FULLTEXT (title, content) 
	);

	--给现有的article表的title和content字段创建全文索引
	--索引名称为fulltext_article
	ALTER TABLE articleADD FULLTEXT INDEX fulltext_article (title, content)
  • 5、组合索引
      多列值组成一个索引,专门用于组合搜索
      如果对多列进行索引(组合索引),列的顺序非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。例如:
      假设存在组合索引(c1,c2),查询语句select * from t1 where c1=1 and c2=2能够使用该索引。查询语句select * from t1 where c1=1也能够使用该索引。但是,查询语句select * from t1 where c2=2不能够使用该索引,因为没有组合索引的引导列,即要想使用c2列进行查找,必需出现c1等于某值。
      这就是最左匹配原则。简单来说,在两个列上的组合索引,有个前后顺序(c1,c2),在查询c1时可以使用该组合索引,在同时查询c1、c2时也可以使用该索引,但只查询c2时不能使用该索引。
      组合索引创建示例:
	CREATE TABLE `left_test` (
		`id` int(11) NOT NULL,
		`a` int(11) DEFAULT NULL,
		`b` int(11) DEFAULT NULL,
		`c` int(11) DEFAULT NULL,
		`d` int(11) DEFAULT NULL,
		`e` int(11) DEFAULT NULL,
		PRIMARY KEY (`id`),
		KEY `m_index` (`a`,`b`,`c`)
	) ENGINE=InnoDB DEFAULT CHARSET=utf8

1.5 前缀索引

  有时候需要索引很长的字符列,这会让索引变得大且慢。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。对于BLOB,TEXT,或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。
  使用前缀索引时,难度在于:如何定义前缀截取的长度
  其中一种确定前缀长度的做法是:计算完整列的选择性,并使其前缀的选择性接近于完整列的选择性
  计算完整列的选择性:

	mysql> select count(distinct city) / count(*) from city_demo;
	+---------------------------------+
	| count(distinct city) / count(*) |
	+---------------------------------+
	|                          0.4283 |
	+---------------------------------+
	row in set (0.05 sec)

  可以在一个查询中针对不同前缀长度的选择性进行计算,这对于大表非常有用,在同一个查询中计算不同前缀长度的选择性示例:

	mysql> select count(distinct left(city,3))/count(*) as sel3,
	    -> count(distinct left(city,4))/count(*) as sel4,
	    -> count(distinct left(city,5))/count(*) as sel5, 
	    -> count(distinct left(city,6))/count(*) as sel6 
	    -> from city_demo;
	+--------+--------+--------+--------+
	| sel3   | sel4   | sel5   | sel6   |
	+--------+--------+--------+--------+
	| 0.3367 | 0.4075 | 0.4208 | 0.4267 |
	+--------+--------+--------+--------+
	1 row in set (0.01 sec)

  可以看见当索引前缀为6时的基数是0.4267,已经接近完整列选择性0.4283。因此在这个例子中,合适的前缀长度是6。创建索引示例:

	alter table city_demo add key (city(6));

1.6 设计索引的原则

  • 1、最适合索引的列是出现在WHERE子句中的列,或连接子句中指定的列,而不是出现在SELECT关键字后的选择列表中的列。即:为经常需要排序、分组和联合操作的字段建立索引;为常用作为查询条件的字段建立索引。
  • 2、尽量使用高选择度索引。索引列的基数越大,索引的效果越好。
  • 3、使用短索引。如果对字符串列进行索引,应该指定一个前缀长度,只要有可能就应该这样做。
  • 4、利用最左前缀。即利用最左匹配原则。
  • 5、 如果索引字段的值很长,那么查询的速度会受到影响,最好使用值的前缀来索引
  • 6、对于InnoDB存储引擎的表,尽量手工指定主键

  对于 InnoDB 存储引擎的表,数据默认会按照一定的顺序保存,如果有明确定义的主键,则按照主键顺序保存;
  如果没有主键,但是有唯一索引,那就是按照唯一索引的顺序保存;
  如果既没有主键又没有唯一索引,那么表中会自动生成一个内部列,按照这个列的顺序保存。
  按照主键或者内部列进行的访问是最快的,所以 InnoDB 表尽量自己指定主键,当表中同时有几个列都是唯一的,都可以作为主键的时候,要选择最常作为访问条件的列作为主键,提高查询的效率。

  • 6、定义有外键的数据列一定要建立索引
  • 7、不是所有的表都需要键索引。常见的代码表、配置表等数据量很小的表,除了主键之外,再创建索引没有太大的意义。索引扫描和全表扫描相比,并不会带来性能的大幅提升。大表的查询、更新、删除操作要尽可能通过索引进行。对于达标来说,全表扫描速度较慢。
  • 8、不要过度索引。因为额外的索引要占磁盘空间,并降低写操作的性能。因此,只需保持所需的索引有利于查询优化。
  • 9、谨慎选择低选择度索引。对于选择性低并且数据分布均衡的列,因为过滤的结果集大,创建索引的效果通常不好;如果列的选择性低并且数据缝补不均衡,比如男女比例为99:1,那么此时创建对于查询条件为‘女’的过滤结果集就比较小,所以的效率较高。
  • 10、谨慎使用唯一索引。使用唯一索引,在特定查询的速度上会有提升,同时能满足业务上对唯一性约束的需求。但在Mysql5.6之后,InnoDB引擎新增了Change Buffer特性来提升写入性能,除了主键之外的唯一索引会导致ChangeBuffer无法被使用,对写入性能影响较大。
  • 11、尽量的扩展索引(组合索引),不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  • 12、使用联合索引时,取值离散大的字段(变量各个取值之间的差异程度)的列放到联合索引的前面
  • 13、更新频繁字段不适合创建索引
  • 14、对于定义为text、image和bit的数据类型的列不要建立索引。

1.7 什么是最左匹配原则

  假设存在组合索引(c1,c2),查询语句select * from t1 where c1=1 and c2=2能够使用该索引。查询语句select * from t1 where c1=1也能够使用该索引。但是,查询语句select * from t1 where c2=2不能够使用该索引,因为没有组合索引的引导列,即要想使用c2列进行查找,必需出现c1等于某值。这就是最左匹配原则。简单来说,在两个列上的组合索引,有个前后顺序(c1,c2),在查询c1时可以使用该组合索引,在同时查询c1、c2时也可以使用该索引,但只查询c2时不能使用该索引。
  在最左前缀匹配原则中,Mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整

  例如组合索引(a,b,c),组合索引的生效原则是:从前往后依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面
  比如:

  1. where a=3 and b=45 and c=5 … 这种三个索引顺序使用中间没有断点,全部发挥作用;
  2. where a=3 and c=5… 这种情况下b就是断点,a发挥了效果,c没有效果
  3. where b=3 and c=4… 这种情况下a就是断点,在a后面的索引都没有发挥作用,这种写法联合索引没有发挥任何效果;
  4. where b=45 and a=3 and c=5 … 这个跟第一个一样,全部发挥作用,abc只要用上了就行,跟写的顺序无关。

1.8 创建索引的三种方式

  • 1、在执行CREATE TABLE时创建索引
	CREATE TABLE user_index2 (
		id INT auto_increment PRIMARY KEY,
		first_name VARCHAR (16),
		last_name VARCHAR (16),
		id_card VARCHAR (18),
		information text,
		KEY name (first_name, last_name),
		FULLTEXT KEY (information),
		UNIQUE KEY (id_card)
	);
  • 2、使用ALTER TABLE命令去增加索引
	ALTER TABLE table_name ADD INDEX index_name (column_list);
	#添加主键
	ALTER TABLE table_name ADD PRIMARY KEY (id);
	ALTER TABLE table_name ADD CONSTRAINT pk_n PRIMARY KEY (id);
	#添加外键
	ALTER TABLE m ADD CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES n(id);  

  ALTER TABLE用来创建普通索引、UNIQUE索引或主键索引。
  其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
  索引名index_name可自己命名。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

  • 3、使用CREATE INDEX命令创建
	CREATE INDEX index_name ON table_name (column_list);

  CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)。

  • 4、删除索引
	#根据索引名删除普通索引、唯一索引、全文索引
	ALTER TABLE table_name DROP KEY index_name 
	#删除主键索引
	ALTER TABLE table_name DROP PRIMARY KEY 
	#删除外键
	ALTER TABLE m DROP FOREIGN KEY 'fk_id';

1.9 索引的基本原理

  索引用来快速地寻找那些具有特定值的数据。如果没有索引,一般来说执行查询时遍历整张表。
  索引的原理很简单,就是把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序;
  2. 对排序结果生成倒排表;
  3. 在倒排表内容上拼上数据地址链;
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据。

1.10 B+树索引和Hash索引

1.10.1 B+树索引和Hash索引的特点

  索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快

  • 1、B+树索引

      B+tree性质:
  1. n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
  2. 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  3. 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
  4. B+ 树中,数据对象的插入和删除仅在叶节点上进行
  5. B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。
  • 2、哈希索引
      简要说下,类似于数据结构中简单实现的Hash表(散列表)一样,当我们在Mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。示例:

1.10.2 B+树索引和Hash索引的适用条件

  索引算法有B+树算法和Hash算法。

  • 1、B+树算法
      B+树是最常用的Mysql数据库索引算法,也是Mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量,示例(以下查询可以用到索引):
	select * from user where name like 'jack%'; 

    B+树索引:如果以通配符开头,或者没有使用常量,则不会使用索引,例如:

	select * from user where name like '%jack'; 
  • 2、哈希算法
      Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像B+树索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于B+树索引。

1.10.3 Hash索引和B+树索引的比较

  Hash索引底层就是Hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。
  那么可以看出他们有以下的不同:

  • Hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询
      因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围。
  • Hash索引不支持使用索引进行排序,原理同上。
  • Hash索引不支持模糊查询以及多列索引的最左前缀匹配。原理也是因为hash函数的不可预测。AAAA和AAAAB的索引没有相关性。
  • Hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询。
  • Hash索引虽然在等值查询上较快,但是不稳定。性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差。而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低

  因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度

1.11 B树索引和B+树索引

  • 在B树中,将键和值存放在非叶子节点和叶子节点在B+树中,非叶子节点都是键,没有值,叶子节点同时存放键和值
  • B树的叶子节点各自独立;B+树的叶子节点有一条链相连

1.11.1 B树和B+树的好处

  • 1、B树的好处
      B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效
  • 2、B+树的好处
      由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间。

1.11.2 数据库为什么使用B+树而不是B树

  • B树只适合随机检索,而B+树同时支持随机检索和顺序检索
  • B+树空间利用率更高,可减少I/O次数,磁盘读写代价更低。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素;
  • B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
  • 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。

1.12 百万级别或以上的数据如何删除

  关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
  因此,要删除有很多索引的表中数据的建议:

  1. 所以想要删除百万数据的时候可以先删除索引
  2. 然后删除其中无用数据
  3. 删除完成后重新创建索引

  与直接删除相比,绝对是要快速很多;并且万一删除中断,一切删除会回滚。

1.13 索引生效和失效条件

1.13.1 索引生效条件

  以下是几个几个能够使用索引的典型场景。

  • 1、匹配全指
      对索引中所有列都指定具体值,即对索引中的所有列都有等值匹配的条件,示例:
	explain select * from rental 
	where rental_date = '2021-05-25 17:12:23' 
	and inventory_id = 373
	and customer_id = 343;
  • 2、匹配值的范围查询
      对索引的值能够进行范围查询,示例:
	explain select * from tental 
	where customer_id >= 373 and customer_id < 400;
  • 3、匹配最左前缀
      仅仅使用索引中的最左列进行查找。
  • 4、仅仅对索引进行查询
      当查询的列都在索引的字段中时,查询的效率更高。
  • 5、匹配列前缀
      仅仅使用索引中的第一列,并且只包含索引第一列的开头一部分进行查找。示例,查询标题title是以AFRICAN开头的电影信息:
	create index idx_title_desc_part on file_text (title(10),description(20));
	explain select title from film_text where title like 'AFRICAN%';
  • 6、如果列名是索引,那么使用column_name is null就会使用到索引

  总的来说,MySQL只对以下操作符才使用索引:

  <
  <=
  =
  >
  >=
  between
  in
  以及某些时候的like(不以通配符%或_开头的情形)

1.13.2 索引失效条件

  1. 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。
  2. 对于多列索引,不是使用的第一部分,则不会使用索引。
  3. like查询是以%开头。
  4. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则会出现隐式转换,从而不使用索引。
  5. 不满足最左原则,则不会使用复合索引。
  6. 如果Mysql估计使用全表扫描要比使用索引快,则不使用索引。
  7. 索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式、计算(+ - * /)

1.14 聚簇索引与非聚簇索引

1.14.1 聚簇索引与非聚簇索引

  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据。
  • 非聚簇索引:将数据存储和索引分开结构,索引结构的叶子节点指向了数据的对应行,

  Myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
  Innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值。
  何时使用聚簇索引与非聚簇索引:

行为 是否应该使用聚簇索引 是否应该使用非聚簇索引
列经常被分组排序
返回某范围内的数据
一个或极少不同值
小数目的不同值
大数目的不同值
频繁更新的列
外键列
主键列
频繁修改索引列

1.14.2 两者的区别

  根本区别:聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致
  聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值记录,其余就连续性的记录在物理也一样连续存放。聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候,会对数据页重新排序。
  非聚集索引制定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用B+树结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式。非聚集索引层次多,不会造成数据重排。

1.14.3 非聚簇索引一定会回表查询吗

  此处要先提一句什么是回表:

  非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表

  不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询
  举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

1.14.4 Innodb中的主键索引和辅助索引

&emsp 聚集索引:索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(比如字典的目录是按拼音排序的,内容也是按拼音排序的,按拼音排序的这种目录就叫聚集索引)。
&emsp 在 InnoDB 里面,它组织数据的方式叫做叫做(聚集)索引组织表,主键索引是聚集索引,非主键都是非聚集索引。
&emsp InnoDB 中,主键索引和辅助索引是有一个主次之分的。辅助索引存储的是辅助索引和主键值。如果使用辅助索引查询,会根据主键值在主键索引中查询,最终取得数据。

  如果一张表没有主键怎么办?

  1. 如果定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
  2. 如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。
  3. 如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行数据的写入而主键递增。

1.15 索引条件下推

  索引条件下推(Index Condition Pushdown),简称ICP。MySQL5.6新添加,用于优化数据的查询。
  索引条件下推优化(Index Condition Pushdown (ICP) )是MySQL5.6添加的,用于优化数据查询。不使用索引条件下推优化时存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件。
  当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
  开启 ICP:

	set optimizer_switch='index_condition_pushdown=on';

1.16 覆盖索引

  先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表
  例如:select * from user_innodb where name = 'qingshan';
  在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表。

1.17 索引的数据结构

  索引的文件存储形式与存储引擎有关。

  常见的存储引擎主要有:MyIsam、InnoDB、Memory等。
   索引文件的结构:hash、二叉树、B树、B+树。

1.17.1 InnoDB 逻辑存储结构

  MySQL 的存储结构分为 5 级:表空间、段、簇、页、行。

  • 1、表空间(Table Space)
      表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。分为:系统表空间、独占表空间、通用表空间、临时表空间、Undo 表空间。
  • 2、段(Segment)
      表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念。一个 ibd 文件(独立表空间文件)里面会由很多个段组成。创建一个索引会创建两个段,一个是索引段:leaf node segment,一个是数据段:non-leaf node segment。
      索引段管理非叶子节点的数据。数据段管理叶子节点的数据。也就是说,一个表的段数,就是索引的个数乘以 2。
  • 3、簇(Extent)
      一个段(Segment)又由很多的簇(也可以叫区)组成,每个区的大小是 1MB(64个连续的页)。
      每一个段至少会有一个簇,一个段所管理的空间大小是无限的,可以一直扩展下去,但是扩展的最小单位就是簇。
  • 4、页(Page)
      为了高效管理物理空间,对簇进一步细分,就得到了页。簇是由连续的页(Page)组成的空间,一个簇中有 64 个连续的页。(1MB/16KB=64)。这些页面在物理上和逻辑上都是连续的。
      InnoDB 也有页的概念(也可以称为块),每个页默认 16KB。页是 InnoDB 存储引擎磁盘管理的最小单位,通过 innodb_page_size 设置。

  一个表空间最多拥有 232 个页,默认情况下一个页的大小为 16KB,也就是说一个表空间最多存储 64TB 的数据。

  操作系统和内存打交道,最小的单位是页 Page。文件系统的内存页通常是 4K。

  • 5、行(Row)
      InnoDB 存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。

1.17.2 哈希表

  本质上是个数组,往里面存元素时,先通过哈希函数求得一个int值,再想该int值转换为数组中对应的位置,然后将元素插入。哈希表的数组中其实都是存的k-v键值对,key是真正存放的数据,value一般是某个固定值。
  哈希表可以完成索引的存储,每次在添加索引的时候需要计算指定列的哈希值,取模运算后计算出下标,将元素插入下标位置。

  哈希索引的特点:

  1. 它的时间复杂度是 O(1),查询速度比较快。因为哈希索引里面的数据不是按顺序存储的,所以不能用于排序。
  2. 我们在查询数据的时候要根据键值计算哈希码,所以它只能支持等值查询(= IN),不支持范围查询(> < >= <= between and)。

  如果字段重复值很多的时候,会出现大量的哈希冲突(采用拉链法解决),效率会降低。
  哈希表在使用的时候,需要将全部的数据加载到内存,比较耗费内存空间,不是很合适。

  • InnoDB 内部使用哈希索引来实现自适应哈希索引特性
      InnoDB 只支持显式创建 B+Tree 索引,对于一些热点数据页,InnoDB 会自动建立自适应 Hash 索引,也就是在 B+Tree 索引基础上建立 Hash 索引,这个过程对于客户端是不可控制的,隐式的。

1.17.3 B树

  平衡二叉搜索树又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。当插入数据次数过多时,旋转次数较多,影响性能
  红黑树是基于AVL树的一个升级,损失了部分查询性能,来提升插入的性能,在红黑树中最低子树和最高子树之差小于2倍即可,在插入的时候,不需要进行N多次的旋转操作,而且还加入了变色的性能,来满足插入和查询性能的平衡。
  索引的数据,是放在硬盘上的。当我们用树的结构来存储索引的时候,访问一个节点就要跟磁盘之间发生一次 IO。InnoDB 操作磁盘的最小的单位是一页,大小是 16K。那么,一个树的节点就是 16K 的大小。
  如果我们一个节点只存一个键值+数据+引用,例如整形的字段,可能只用了十几个或者几十个字节,它远远达不到 16K 的容量,所以访问一个树节点,进行一次 IO 的时候,浪费了大量的空间。所以如果每个节点存储的数据太少,从索引中找到我们需要的数据,就要访问更多的节点,意味着跟磁盘交互次数就会过多。如果是机械硬盘时代,每次从磁盘读取数据需要 10ms 左右的寻址时间,交互次数越多,消耗的时间就越多。

 解决方案是什么呢?

  第一个就是让每个节点存储更多的数据。第二个,节点上的关键字的数量越多,我们的指针数也越多,也就是意味着可以有更多的分叉(我们把它叫做“路数”)。因为分叉数越多,树的深度就会减少(根节点是 0)。
  前面的结论就是:二叉树以及N多的变种都不能支撑索引,原因是树的深度过深,导致IO次数变多,影响数据读取的效率。所以此时需要用多叉树(B树)来实现索引。

  • B树示例:

      B树的特点:
  1. 所有键值分布在整棵树中
  2. 所有有可能在非叶子节点结束在关键字全集内做一次查找,性能逼近二分查找
  3. 每个节点最多拥有m个子树
  4. 根节点至少有2个子树
  5. 分支节点至少拥有m/2颗子树(除根节点也叶子节点之外都是分支节点)
  6. 所有叶子节点都在同一层、每个节点最多可以拥有m-1个key,并且以升序排列。

  在上面的例子中,假设每个节点占用一个磁盘块,一个节点上有两个升序排列的关键字和是哪个指向子树根节点的指针,指针存储的是子节点所在磁盘块的两个地址。两个关键词划分成的三个范围域对应的是三个指针指向的子树的数据的范围域。以根节点为例,关键字为16和34,P1指针指向的子树的数据范围为小于16,P2指针指向的子树的数据范围为16-34,P3指针指向的子树的数据范围为大于34。
  查找关键字过程:

  1. 根据根节点找到磁盘块1,读入内存(磁盘IO第一次)
  2. 比较关键字28在区间(16,34),找到磁盘块1的指针P2
  3. 根据P2指针找到磁盘块3,读入内存(磁盘IO第二次)
  4. 比较关键字28在区间(15,31),找到磁盘块3的指针P2
  5. 根据P2指针找到磁盘块8,读入内存(磁盘IO第三次)
  6. 在磁盘块8中的关键字列表中找到关键字28

  B树的缺点:

  1. 每个节点都有key和data,而每个页存储空间是有限的,如果data比较大的话会导致每个节点存储的key数量变小;
  2. 当存储的数据量很大的时候会导致深度较大,增大查询时磁盘IO次数,进而影响查询性能。

1.17.4 B+树

  • B+树示例:
    InnoDB 底层存储结构为B+树, B树的每个节点对应innodb的一个page, page大小是固定的,一般设为 16k。其中非叶子节点只有键值,叶子节点包含完成数据。
     B+树是在B树的基础上做的一种优化,变化如下:

1、B+树每个节点可以包含更多的节点,这样做的原因有两个:一是为了降低树的高度,二是将数据范围变成多个区间,区间越多,数据检索越快
2、非叶子节点存储key,叶子节点存储key和数据
3、叶子节点两两指针相互连接(符合磁盘的预读特性),顺序查询性能更高


在B+树上有两个头指针,一个指向根节点,一个另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+树进行两种查找:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进随机查找。

 Innodb,B+树叶子节点直接放置数据的示例:

 Innodb是通过B+树结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键。
如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后再通过主键索引来找到对应的记录,叫做回表
 MyISAM中的B+树和Inondb相比,差异之处就在于索引和表数据是分开存储的,看个例子:

 InnoDB索引适用场景:

  1. 经常更新的表,适合处理多重并发的更新请求
  2. 支持事务
  3. 可以从灾难中恢复(通过 bin-log 日志等)。
  4. 外键约束。只有他支持外键。
  5. 支持自动增加列属性 auto_increment。

  MySQL 中的 B+Tree 有几个特点:

  1. 它的关键字的数量是跟路数(子树数量)相等的
  2. B+Tree 的根节点和枝节点中都不会存储数据,只有叶子节点才存储数据。搜索到关键字不会直接返回,会到最后一层的叶子节点。比如我们搜索 id=28,虽然在第一层直接命中了,但是全部的数据在叶子节点上面,所以我还要继续往下搜索,一直到叶子节点。
  3. 在 InnoDB 中 B+ 树深度一般为 1-3 层,它就能满足千万级的数据存储
  4. B+Tree 的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构。
  5. 它是根据左闭右开的区间 [ )来检索数据。

1.18 查看索引使用情况

  如果索引正在工作,Handler_read_key 的值将很高,这个值代表了一个行被索引值读的次数,很低的值表明增加索引得到的性能改善不高,因为索引并不经常使用。
  Handler_read_rnd_next 的值高则意味着查询运行低效,并且应该建立索引补救。这个值的含义是在数据文件中读下一行的请求数。如果正进行大量的表扫描,Handler_read_rnd_next 的值较高,则通常说明表索引不正确或写入的查询没有利用索引。
  命令示例:

	show status like 'Handler_read%';

  结果示例:

二、存储引擎

2.1 MyISAM与InnoDB区别

  存储引擎:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。常用的存储引擎有:

  • 1、Innodb引擎
      Innodb引擎提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
  • 2、MyIASM引擎
      不提供事务的支持,也不支持行级锁和外键
  • 3、MEMORY引擎
      所有的数据都在内存中,数据的处理速度快,但是安全性不高。

  MyISAM与InnoDB区别:

MyISAM Innodb
存储结构 每张表被存放在三个文件
frm:表结构
MYD:数据文件
MYI:索引文件
frm:表定义文件
ibd :数据文件
InnoDB表的大小只受限于操作系统文件的大小,一般为2GB
存储空间 MyISAM可被压缩,存储空间较小 InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引
可移植性、备份及恢复 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了
记录存储顺序 按记录插入顺序保存 按主键大小有序插入
外键 不支持 支持
事务 不支持 支持
锁支持 支持表级锁 支持行级锁、表级锁
SELECT MyISAM更优
INSERT、UPDATE、DELETE InnoDB更优
select count(*) myisam更快,因为myisam内部维护了一个计数器,可以直接调取。
索引的实现方式 B+树索引,myisam 是堆表 B+树索引,Innodb 是索引组织表
哈希索引 不支持 支持
全文索引 支持 不支持(在5.6之后支持)
索引类型 非聚簇索引 聚簇索引
适合操作类型 大量select 大量insert、delete、update

  Mysql默认引擎:5.1版本之前默认引擎是MyISAM,之后是InnoDB。

2.2 MyISAM索引与InnoDB索引的区别

  • 1、InnoDB索引是聚簇索引(表数据和文件放在一起),MyISAM索引是非聚簇索引
      聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引
      非聚簇索引:表数据和索引单独一个文件保存(MyISAM)。其中.frm文件存放表结构,.MYI文件存放索引数据,.MYD存放实际数据。
  • 2、InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • 3、MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据
  • 4、InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效

2.3 存储引擎选择

  如果没有特别的需求,使用默认的Innodb即可。
  MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
  Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。

2.4 查看Mysql提供的所有存储引擎

	show engines;

  示例:

2.5 存储引擎优化

  不同的业务表,应该选择不同的存储引擎,例如:查询插入操作多的业务表,用 MyISAM。临时数据用 Memery。常规的并发大更新多的表用 InnoDB
  字段定义原则:使用可以正确存储数据的最小数据类型

  • 1、整数类型
      INT 有 8 种类型(tinyint、smallint、mediumint、int、integer、bigint、bit),不同的类型的最大存储范围是不一样的。比如性别,可以用 TINYINT。
  • 2、字符类型
      变长情况下,varchar更节省空间,但是对于varchar字段,需要一个字节来记录长度。因此固定长度的用char,不要用varchar。
  • 3、非空
      非空字段尽量定义成 NOT NULL,提供默认值,或者使用特殊值、空串代替 null。NULL 类型的存储、优化、使用都会存在问题
  • 4、不要用外键、 触发器、 视图
      降低了可读性;影响数据库性能,应该把把计算的事情交给程序,数据库专心做存储;数据的完整性应该在程序中检查。
  • 5、大文件存储
      不要用数据库存储图片或者大文件;把文件放在 NAS(网络存储器) 上,数据库只需要存储 URI(相对路径),在应用中配置 NAS 服务器地址。
  • 6、表拆分
      将不常用的字段拆分出去,避免列数过多和数据量过大。

2.6 一张表,里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记录的 ID 是 18 还是 15

  1)如果表的存储引擎是 MyISAM,那么是 18。因为 MyISAM 表会把自增主键的最大 ID 记录到数据文件里,重启 MySQL 自增主键的最大ID 也不会丢失
  2)如果表的存储引擎是 InnoDB,那么是 15。InnoDB 表只是把自增主键的最大 ID 记录到内存中,所以重启数据库或者是对表进行OPTIMIZE 操作,都会导致最大 ID 丢失。

三、锁

  锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的 计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一 个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
  相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是:不同的存储引擎支持不同的锁机制:

  1. MyISAM和MEMORY存储引擎采用的是表级锁
  2. InnoDB存储引擎既支持行级锁,也支持表级锁,但默认情况下是采用行级锁。

3.1 表级锁和行级锁

  • 1、表级锁
      表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
      该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。表锁被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁。
      MyISAM只支持表锁,因此性能相对Innodb来说相对降低,而Innodb支持表锁和行锁。
      表级锁的特点:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
  • 2、行级锁
      行锁的是mysql锁中粒度最小的一种锁,因为锁的粒度很小,所以发生资源争抢的概率也最小,并发性能最大,但是也会造成死锁,每次加锁和释放锁的开销也会变大。
      主要是Innodb使用行锁,Innodb也是Mysql在5.5.5版本之后默认使用的存储引擎。
      行级锁的特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
  • 3、表级锁和行级锁的对比
      仅从锁的角度来说:
  1. 表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;
  2. 行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

  OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易;
  OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。

  1. OLTP 系统强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作;
  2. OLAP 系统则强调数据分析,强调SQL执行市场,强调磁盘I/O,强调分区等。

3.2 共享锁和排他锁

  • 1、共享锁
      共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
      示例:若事务A对数据对象1加上S锁,则事务A可以读数据对象1但不能修改,其他事务只能再对数据对象1加S锁,而不能加X锁,直到事务A释放数据对象1上的S锁。这保证了其他事务可以读数据对象1,但在事务A释放数据对象1上的S锁之前不能对数据对象1做任何修改。
  • 2、排他锁
      排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁。
      示例:若事务A对数据对象1加上X锁,事务A可以读数据对象1也可以修改数据对象1,其他事务不能再对数据对象1加任何锁,直到事务A释放数据对象1上的锁。这保证了其他事务在事务A释放数据对象1上的锁之前不能再读取和修改数据对象1。

3.3 意向共享锁和意向排它锁

  意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
  意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
  意向共享锁和意向排它锁总称为意向锁。意向锁的出现是为了支持Innodb支持多粒度锁。
  意向锁是表级别锁。

当需要给一个加表锁的时候,需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则直接判断一次就知道表中是否有数据行被锁定了。所以说将意向锁设置成表级别的锁的性能比行锁高的多。

  有了意向锁之后,前面例子中的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
  意向锁的作用就是:当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。

3.4 乐观锁和悲观锁

  • 1、乐观锁
      乐观锁实现方式:一般会使用版本号机制或CAS算法实现
      乐观锁不是数据库自带的,需要我们自己去实现,示例:
  1. SELECT data AS old_data, version AS old_version FROM …;
  2. 根据获取的数据进行业务操作,得到new_data和new_version
  3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
    if (updated row > 0) {
     // 乐观锁获取成功,操作完成
    } else {
     // 乐观锁获取失败,回滚并重试
    }

  乐观锁的优点:乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。
  乐观锁的缺点:乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
  总结:读用乐观锁,写用悲观锁

  • 2、悲观锁
      悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制(select ... for update)
      当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用
      MySQL中,select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描
      实现悲观锁时,必须先使用set autocommit=0;关闭Mysql的autoCommit属性,因为查询出数据之后就要将该数据锁定。
      悲观锁的使用示例:
  1. 开始事务
    begin; 或者 start transaction;
  2. 查询出商品信息,然后通过for update锁定数据防止其他事务修改
    select status from t_goods where id=1 for update;
  3. 根据商品信息生成订单
    insert into t_orders (id,goods_id) values (null,1);
  4. 修改商品status为2
    update t_goods set status=2;
  5. 提交事务
    commit; --执行完毕,提交事务

  在第2步我们将数据查询出来后直接加上排它锁(X)锁,防止别的事务来修改事务1,直到我们commit后,才释放了排它锁。
  悲观锁的优点:保证了数据处理时的安全性。
  悲观锁的缺点:加锁造成了开销增加,并且增加了死锁的机会。降低了并发性。

  • 3、乐观苏和悲观锁的适用场景
      乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
      如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
      悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,例子在select ... for update前加个事务就可以防止更新丢失。悲观锁和乐观锁大部分场景下差异不大,一些独特场景下有一些差别,一般可以从如下几个方面来判断。
  1. 响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。
  2. 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大。
  3. 重试代价:如果重试代价大,建议采用悲观锁。

3.5 间隙锁、记录锁、临键锁

  接下来三种锁(间隙锁、记录锁、临键锁)都是Innodb的行锁,前面我们说过行锁是基于索引实现的,一旦加锁操作没有操作在索引上,就会退化成表锁。
  间隙锁:作用于非唯一索引上,主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
  如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
  记录锁:它封锁索引记录,作用于唯一索引上。
  临键锁:作用于非唯一索引上,是记录锁与间隙锁的组合。

3.6 死锁

  死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象。

  如何解决死锁?

  1. 等待事务超时,主动回滚。
  2. 进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。
  3. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
  4. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
  5. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;如果业务处理不好可以用分布式事务锁或者使用乐观锁。

3.7 MyISAM锁

  MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)表独占写锁(Table Write Lock)
  对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的。
  假设有以下数据:

	CREATE TABLE `mylock` (
	  `id` int(11) NOT NULL AUTO_INCREMENT,
	  `NAME` varchar(20) DEFAULT NULL,
	  PRIMARY KEY (`id`)
	) ENGINE=MyISAM DEFAULT CHARSET=utf8;

	INSERT INTO `mylock` (`id`, `NAME`) VALUES ('1', 'a');
	INSERT INTO `mylock` (`id`, `NAME`) VALUES ('2', 'b');
	INSERT INTO `mylock` (`id`, `NAME`) VALUES ('3', 'c');
	INSERT INTO `mylock` (`id`, `NAME`) VALUES ('4', 'd');
  • 1、写锁阻塞读
     ​ ​当一个线程获得对一个表的写锁之后,只有持有锁的线程可以对表进行更新操作。其他线程的读写操作都会等待,直到锁释放为止。
     ​ ​在命令行可以这样给表加上写锁:lock table mylock write。加上该写锁之后,该session可以正常操作,别的session对该表的查询会阻塞。只有释放该锁:unlock tables后,别的session查询才能正常进行。
  • 2、读锁阻塞写
     ​ ​一个session给表加读锁,这个session可以锁定表中的记录,但更新和访问其他表都会提示错误,同时,另一个session可以查询表中的记录,但更新就会出现锁等待。
     ​ ​当session1给表加上读锁:lock table mylock read后,session1可以查询该表记录,session1不能查询没有锁定的表,session1插入或者更新表会提示错误。
     ​ ​session2可以查询该表记录,可以查询或者更新未锁定的表,插入数据会等待获得锁。当session1释放锁unlock tables后,session2获得锁,更新成功。

 ​ ​MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁

  • 3、MyISAM的并发插入问题
     ​ ​MyISAM表的读和写是串行的,这是就总体而言的,在一定条件下,MyISAM也支持查询和插入操作的并发执行。
     ​ ​假如session1可以通过lock table mylock read local获取表的read local锁定,session1不能对表进行更新或者插入操作,session1不能查询没有锁定的表,session1不能访问其他session插入的记录。此时session2可以查询该表的记录,session2可以进行插入操作,但是更新会阻塞,。
     ​ ​然后session1通过unlock tables释放锁资源,session1可以查看session2插入的记录,session2获取锁,更新操作完成。

 ​ ​可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺。

mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 352   |
| Table_locks_waited    | 2     |
+-----------------------+-------+

 ​ ​如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

3.8 InnoDB锁

 ​ 可以通过检查innodb_row_lock状态变量来分析行锁的争夺情况:

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 18702 |
| Innodb_row_lock_time_avg      | 18702 |
| Innodb_row_lock_time_max      | 18702 |
| Innodb_row_lock_waits         | 1     |
+-------------------------------+-------+

  如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高。

  InnoDB的行锁模式及加锁方法

  • 1、共享锁
      又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 2、排他锁
      又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

  Mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
  如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。
  所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

  InnoDB行锁是通过给索引上的索引项加锁来实现的
  InnoDB这种行锁实现特点意味着:InnoDB只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁

  • 1、在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁
      比如创建一张表:
	create table tab_no_index(id int,name varchar(10)) engine=innodb;
	insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');

  session1可以通过select * from tab_no_index where id = 1 for update命令,只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁。

  • 2、创建带索引的表进行条件查询,innodb使用的是行锁
      比如创建一张表:
	create table tab_with_index(id int,name varchar(10)) engine=innodb;
	alter table tab_with_index add index id(id);
	insert into tab_with_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');

  此时当一个session对一个行加锁时,不影响另一个session对别的行的操作。

3.9 Mysql中的锁总结

  • 1、MyISAM的锁总结
  1. 共享读锁之间是兼容的,但共享读锁与排他写锁之间,以及排他写锁之间是互斥的,也就是说读和写是串行的。
  2. 在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
  3. MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
  4. 由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。
  • 2、InnoDB锁总结
       InnoDB是基于索引来完成行锁。示例 select * from tab_with_index where id = 1 for update;
      for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁。

  在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。
  在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:

  1. 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
  2. 选择合理的事务大小,小事务发生锁冲突的几率也更小;给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
  3. 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
  4. 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
  5. 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

3.10 隔离级别与锁的关系

  在Read Uncommitted(读未提交)级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突。
  在Read Committed(读已提交)级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁
  在Repeatable Read(可重复读)级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁
  SERIALIZABL(串行化) 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/121308952