注:这里主要以mysql5.7,对于最新mysql6,8有了更多的变化,暂无研究,等研究后再次更新
mysql的数据存储在磁盘与内存(内存中数据也是来源与磁盘,放在内存中为了加快查询效率)中,这些数据被mysql使用了不同的技术形成了不同的数据文本结构放入了磁盘中,在我们使用时这些技术通过不同的索引技巧,不同的锁定水平使得我们可以放心,便捷快速的使用这些数据。这些对应的技术就是数据引擎,数据引擎在mysql是一个核心组建,在每一张表的创建时,都会指定先对应的数据引擎,最终根据用户的最终选择,构建相对应的数据结构。
mysql不同的版本对应的数据引擎的种类也不同,在5.7中主要可以分为: 1. MyIsam , 2. Mrg_Myisam, 3. Memory, 4. Blackhole, 5. CSV, 6. Performance_Schema, 7. Archive, 8 InnoDB这几种,而我们使用最多的最频繁的就两种:1InnoDB与Mylsam。所以这里只介绍InnoDB与Mylsam
InnoDB是我们现实中用的最多的一种引擎,它使用针对于事务的存储引擎。InnoDB引擎提供了对ACID(A:原子性;C:一致性;I:隔离型;D:一致性)事务的支持,同时实现了sql标准的4大隔离机制(1:读未提交;2:读已提交;3:可重复读;4:序列化)。该引擎提供了两种锁级别,行级锁和表级锁。MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。InnoDB创建一张表时会创建3个文件,其中db.opt存放了数据库的配置信息,比如数据库的字符集还有编码格式。student.frm是表结构文件,仅存储了表的结构、元数据(meta),包括表结构定义信息等。不论是哪个表引擎都会有一个frm文件。student.ibd是表索引文件,包括了单独一个表的数据及索引内容。InnoDB的特点可以归纳以下几点:
InonDB中存在两种类型索引:主索引和辅助索引。
InnoDB引擎创建的索引其数据底层为B+树,B+树中叶子节点中存储的为实际的数据(为表中的行数据),这么看来InnoDB的索引本身就是数据文件,所以对该索引又称为聚簇索引(也可以称为聚集索引)(聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的),索引对应的key一般为该表的主健(若当前表中无主键时,InnoDB会选择一个非空的唯一索引来创建聚簇索引;如果这也没有,InnoDB会隐式的创建一个自增的列来作为聚簇索引。其实这么看来也可以理解为,InnoDB的表肯定包含主健,也为何我们一般建议表的主健为自增的,因为自增的数字在构建B+树,极大避免了为了维持B+Tree的特性而频繁的分裂调整的消耗,同时避免使用长随机字符串—例如UUID,理由在于:第一随机字符串增加了频繁的分裂调整的概率,第二:过长的字符串存储对应节点上导致该节点可存储的值变少,那么在相同数据下该健越大B+树越瘦高)。上文说到为了保证查询效率,mysql将B+树中的节点与磁盘的块(或者称为)结构结合起来,使得我们访问该节点只花费一次io操作。而因为数据对应的块只能和B+树的一个节点进行绑定,那么就表明了该索引只会存在这一个,所以我们也可以叫它主索引。如下图所示为一个主索引结构:
右侧为一个表其中col1为主健,那么主健15对应行数据在叶子节点中存储。这时候就会产生一个疑问?如果我对于Col3列构建一个索引那么,InnoDB该如何处理呢?这里就引出了第二个类型的索引:辅助索引(从索引和数据是否在一起的角度也可以称为非聚齐性索引。这里为何叫它辅助索引,后续介绍)
上文中说到因为每个数据块只能对应主索引的节点,那么col3创建的索引对应的B+树的结构是怎么样的呢?如下图所示:
这里以英文字符的ASCII码作为比较准则。该树与主索引树中最大的区别在在于叶子节点存储为数据对应的主健(这里也是为何主健不易过大的原因之一,过大的主键会导致辅助索引的树体积过大),从这点可以看出辅助索引搜索数据可能需要检索两遍索引:第一遍通过辅助索引查到对应数据若如果所需的信息不够那么此时再通过主索引查询真正的数据。所以这里也是何称为辅助索引的原因,因为它是辅助于主索引的存在,最终都要走主索引这一条路。这里我们可以想象主索引的角色类似于公司中CTO,而辅助角色类似于架构师,多个架构师收集方法,最终都是需要CTO进行审核确定,那么架构师的作用就是辅助于CTO(该CTO也是可以收集方案)作出决断。
可参考大神文章很详细
InnoDB中锁可以分为两种:表锁和行锁,两者的粒度不一样,实现细节不一样,各有自己的优点和缺点,
注:InnoDB中行锁是和索引有关,也就是说只有用到了索引才能使用行锁,否则都只使用表锁
表锁一般分为两种:表读锁(Table Read Lock)和表写锁(Table Write Lock)
其中在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞即
读读不阻塞:若存在用户在读数据,其他的用户也在读数据,不会加锁
读写阻塞:若存在用户在读数据,那么其他用户不能修改当前用户读的数据,会加锁
写写阻塞:若存在用户在写数据,那么其他用户不能修改当前用户写的数据,会加锁
读锁和写锁是互斥的,读写操作是串行。
共享锁用法:
LOCK TABLE table_name [ AS alias_name ] READ
排它锁用法:
LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE
解锁用法:
unlock tables;
行锁必须要用到索引的锁
InnoDB中行锁可以实现方式分为Record Lock、Gap Lock、Next-key Lock这三种
Next-key Lock有以下注意情况:
如下所示(其中id为主键,userId为普通索引):
1:对主键或唯一索引,使用的是=查询并且命中,这种情况只会加行记录锁,不会存在间隙锁,如果当前查询条件未命中那么存在间隙锁并且它锁定的区域为查询值所在的间隙区域的一个开区间。
2:对于非主键索引或者非唯一索引时,也分为以下情况
3:对于非索引列查询,当前读操作时,会加全表gap锁
InnoDB中行锁按照使用方式可以分为两种:共享锁和排他锁
共享锁(又称S锁 读锁):若当前A事务对某数据加了共享锁,此时该事务只能对该数据进行读不能进行修改操作。此时其他事务只能对该数据再加S锁,而不能加X锁(排它锁),只能等A事务将锁释放时,其他事务才能对该数据进行修改操作。(在java中也有对于共享锁的具体实现,例如CountDownLatch,ReadWriteLock中的读锁都是共享锁。具体可参考AQS它是java中除Synchronized其它锁的本质)
#使用方式
select ..... from table lock in share mode;
排他锁(又称X锁 写锁):若当前A事务对某事务加了排他锁,此时该事务对该数据可以进行读写操作,其他事务不能再对该数据加任何锁,直到A释放了锁。这保证了其他事务在A释放锁之前不能再读取和修改A。(在java中也有对于排他锁的具体实现,例如Synchronized,ReentrantLock都是排他锁)
##使用方式
select ... from table for update;
从上文我们可以知道,如果要用到行锁肯定会使用到索引的
MVCC-多版本并发控制,它可以说mysql中对乐观锁(可以看作是一个携带版本控制的CAS算法)的一个具体实现,
对于一个事务中的查询主要分为两种,当前读与快照读
当前读:读取的是数据库中最新版本的数据的数据,对要读取的数据加锁(),保证无其他事务对其进行修改导致数据安全,锁机制一般通过Next-key Lock(Record Lock+Gap Lock)来实现了。主要类似与以下语句为当前读:
快照读:快照读主要通过MVCC机制实现,使用快照读,读取数据时不需要对数据进行加锁,且快照读不会被其他事物阻塞。所以它的性能非常高,但会存在一个问题即获取到的数据不一定是最新实时的(和事务的隔离级别)。
InnoDB引擎中提供了4种事务隔离级别:
读提交(READ COMMITTED):
不可重复读(REPEATABLE READ):
为何行锁中会出现死锁(死锁出现的原因在于互斥、持有、不可剥夺、环形等待)的情况,其实和索引也有着一定的关系
可以分为以下几种情况:
第一种: 当两个事务同时执行,t1语句锁住了A数据主索引,t2语句等待了B数据的索引锁释放。另一个事务中t1语句锁定了B数据索引,t2语句A数据等待主索引的锁释放,在高并发的情况下就会导致了死锁。例如:
--A事务
select name from user where id =3 for update;
update t1 set name ='aa' user where id =1;
--B事务
select name from user where id =1 for update;
update t1 set name ='bb' user where id =3;
--table数据 id为主键
#####id#########name#########
#####1##########张#########
#####3##########李#########
第二种:从上述描述中我们知道InnoDB中索引分为两种聚集性或者非聚集性 ,非聚集性索引中叶节点存储的是主键,所以如果一个查询使用的非聚集性查询时不当的写法+高并发(并不是说在非并发下就不产生,而是指的概率)时就会产生死锁。如下所示:
--主索引字段为id 辅助索引为name age
--事务1
select * from user where name ='张三' for update;
--事务2
select * from user where old >25 for update;
--表中数据为
######id#######name######age#####
######1########张三#######26#####
######2########李四#######21#####
######3########王五#######23#####
######4########张三#######29#####
######5########李四#######22#####
为何出现死锁的原因在于:事务1的查询会根据辅助索引name查询到两条记录,并且存储是按照先小后大的顺序进行排序的,即加锁的顺序在于[1,张三,26]后[4,张三,29],事务2的查询会根据辅助索引age也查询到两条记录,但是此时的查询获取的顺序为先大后小,即加锁顺序为[4,张三,29],后[1,张三,26],那么此时在高并发的情况下,就大概率出现死锁了,事务1于事务二都在等对方的锁释放。
那么对应我们要在写应用中注意避免出现死锁:
若出现死锁了,有两种解决方案:
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以执行,将参数innodb_deadlock_detect设置为on,表示开启这个逻辑
InnoDB锁中对应的特性:
Mylsam引擎在创建表的同时对应会创建3个文件,第一个用于存储表的定义—.frm文件;第二个用于存储表数据信息—.MYD;第三个用于存储索引信息—.MYI,操作系统对大文件的操作是比较慢的,这样将表分为三个文件,那么.MYD这个文件单独来存放数据自然可以优化数据库的查询等操作。而Mylsam引擎独立与操作系统(即Linux与Winodws都可以使用),所以这些数据可以通过文件进行平台迁移。
MyLsam有着自己的特性:
Mylsam引擎创建的索引也可以分为两种类型:主索引与辅助索引
如下所示是一个主索引结构它的数据底层也为B+树,其中叶子节点中存放的是实际数据地址,如下图所示为Mylsam中的索引结构:
假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。
如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:
同样也是一棵B+树,data域保存也是数据记录的地址。其实在Mylsam中,主索引要求key是唯一的(Mylsam创建的表不一定要求设定主健),而辅助索引的key可以重复,除此之外Mylsam中主索引和辅助索引(Secondary key)在结构上没有任何区别。
MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取数据文件中相应数据记录。从上述描述可以看出,Mylsam引擎创建的索引数据与数据是分离的所以对比与InnoDB中索引将索引也称为非聚簇索引(也可以称为非聚集索引)(叶级页指向表中的记录,记录的物理顺序与逻辑顺序没有必然的联系。非聚簇索引则更像书的标准索引表,索引表中的顺序通常与实际的页码顺序是不一致的)。
可参考大神文章很详细
组合索引也可以称为联合索引,从字面上就可以理解,相对于一般索引的单字段构成,联合索引可以为多个字段创建一个索引。例如当我们为字段(a,b,c)3个字段创建一个联合索引,那么对于索引存储是按照第一个字段a进行排序,若a相同时根据B进行排序,若B相同时再根据C进行排序。这样的索引的方式我们可以类比于小时候我们通过新华字典学习拼音查询一样,相同的前缀单词肯定是排在一起的,总体上是根据首字母排序。如下所示:
a b c
1 1 6
1 2 1
1 2 3
1 5 1
2 3 6
2 4 1
2 4 2
2 6 1
联合索引也是由B+树实现了,不通于单索引中每个节点存储的是当字段数据,联合索引中节点会包含对应的所有字段,如下图所示:
第二例中第一个字段都是存储的Akroyd,那么此时就会按照第二个字段进行排序可以看到的确如此(Christian-Debble-Kristen).
从上述可以看出要想该索引有效,必须要求查询条件中必须包含第一个字段,下列查询都会使用到该联合索引,因为三个查询按照 (a ), (a,b ),(a,b,c )的顺序都可以利用到索引,这就是最左前缀匹配。
select * from table where a=1;
select * from table where a=1 and b=2;
select * from table where a=1 and b=2 and c=3;
下列查询语句就不会使用到索引
select * from table where b=2;
select * from table where c=3;
select * from table where b=2 and c=3;
下列查询语句就只会用到索引a
select * from table where a=1 and c=3;
下列查询语句也会用到索引,虽然查询位置没有遵循最左在前,原因在于mysql查询优化器会判断纠正这条sql语句该以什么样的顺序执行效率最高,最后才生成真正的执行计划。建议还是索引顺序来查询,这样查询优化器就不用重新编译了。
select * from table where b=2 and a=1;
select * from table where b=2 and a=1 and c=3;
除了联合索引之外,对mysql来说其实还有一种前缀索引。前缀索引就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。
一般来说以下情况可以使用前缀索引:
一些文章中也提到:
MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度。但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index)。