一、MySQL索引
1.1.索引简介
索引是一种数据库中的数据对象,它能够提高数据库中的数据检索速度.MySQL支持多种类型的索引,每种类型的索引有其特定的用途和性能特点.
MySQL中的索引种类如下:
- B-Tree索引
- 数据结构B-Tree
- 根据叶子结点的存储数据的种类不同分为:聚簇索引(主键索引)和非聚簇索引
- Hash索引
- 适合等值查询
- R-Tree索引(空间索引)
- 全文索引
索引的代价:
- 空间上的代价
- 每建立一个索引都要为它建立一颗B+树,每一颗B+树的每一个节点都是一个数据页,一个页默认会占用16KB(即4个物理页的大小),一个B+树如果很大,则会占用很大的空间
- 时间上的代价
- 每次对表的增删改操作时,都需要去修改各个B+树.在修改B+树时可能会造成页面分裂、页面回收等操作,增加耗时.
1.2.索引的原理
1.2.1.索引结构
1.2.2.聚簇索引、二级索引、联合索引的特点
聚簇索引,具有以下两个特点:
- 使用记录主键值的大小进行记录和页的排序,这主要包括三个方面
- 页内的记录是按照主键的大小顺序排成一个单项链表
- 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表
- 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表.
- B+树的叶子节点存储的是完整的用户记录
- 所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)
二级索引,当搜索条件为非主键列时的B+树,例如以c2列建B+树,对应主键的聚簇索引有以下不同
- 使用记录c2列的大小进行记录和页的排序
- 页内记录是按照c2列的大小顺序排成一个单向列表
- 各个存放用户记录的页也是根据页中记录的c2列大小顺序排成一个双向链表
- 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的c2列大小顺序排成一个双向链表。
- B+树的叶子节点存储的并不是完整的用户记录,而是c2列+主键的搭配
- 目录项记录中不再是主键+页号的搭配,而变成c2列+页号的搭配
联合索引 使用多列进行排序,类似二级索引,假如索引列为(c2,d3),则会为c2,d3建立一个联合索引的B+树,特点如下
- 每条目录项记录都由c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。
- B+树叶子节点处的用户记录由c2、c3和主键c1列组成。
1.3.索引的应用
1.3.1.如何挑选索引
创建索引时,可以从以下几个方面进行考虑:
- 只为用于搜索、排序或者分组的列创建索引
- 只为where子句中的列、order by、group by 后的列创建索引
- 列的基数(区分度),
- 在行数一定的情况下,基数越大,该列的值越分散,最好为基数大的列建立索引.
- 索引列的类型尽量小
- 索引字符串的前缀
- 为字符串建立索引的问题
- B+树索引中的记录需要把该列的完整字符串存储起来,而且字符串越长,在索引中占用的存储空间越大。
- 如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。
- 索引列前缀对排序的影响
- 无法支持,只能文件排序
- 为字符串建立索引的问题
- 主键尽量是递增的
1.3.2.如何应用索引
create table t ( id int primary key, a int not null default 0, b varchar(16) not null default '', c int not null default 0 ) engine = InnoDB
数据
(1,1,'a',10),
(2,2,'b',11),
(3,3,'c',12),
(4,4,'d',13)
覆盖索引
假如对表t加索引 ac(a,c),查询sql
select * from t where a between 2,5 ;
MySQL innodb会先根据a 去索引ac所在的B+树进行搜索,搜索到符合的记录后,再返回主键索引去查询完整记录.
先进行联合索引查询,再去主键索引查询的过程,称为回表.
查询时,为了避免回表,如果查询的列都在联合索引里面时,sql可以变成
select a ,c from t where a between 3,5;
即索引ac已经满足了我们的查询需求,我们称则称该索引为覆盖索引.
最左前缀原则
假如对表t建立索引 t_all(a,b,c) 索引使用情况如下
## 全值匹配
select * from t where a = 1 and b = 'a' and c= 10; 无论a,b,c顺序如何,都会使用到t_all索引
## 最左列时
select * from t where a = 1 ; //使用t_all索引, 索引长度(key_len)为a的大小
select * from t where a = 1 and b = 'a' ;//使用t_all索引, 索引长度(key_len)为a,b的大小
select * from t where b = 'a' // 会使用到t_all索引,但是扫描类型为index,即扫描整个索引树. index与all不同时,all是扫描整个磁盘数据,
进行全表扫描.
## 匹配列前缀
select * from t where a = 1 and b like 'A%'; 会使用
select * from t where a = 1 and b like '%A%'; 不会
select * from t where a = 1 and b like '%A'; 不会
## 匹配范围值 在匹配的过程中遇到<>=号,就会停止匹配,
select * from t where a>1 and c>1 and c<10 ,key_len是a的长度 ,扫描类型是index
select * from t where a<2 and c>1 and c<10 如果a匹配的数量很少,如1条,key_len是a的长度 ,扫描类型是range,
select * from t where c>1 and c<10 扫描类型为index
## 精确匹配第一列并范围匹配其它列
select * from t where a = 1 and c<100 type 扫描类型为ref,使用索引为t_all
1.3.3.Explain命令
mysql> EXPLAIN SELECT 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
每一列的含义:
- id: 在一个大的查询语句中每个select关键字都对应一个唯一的id
- select_type: select的关键字对应的查询类型
- table:表名
- partitions: 匹配的分区信息
- type : 单表访问方法
- possible_keys : 可能用到的索引
- key : 实际用到的索引
- key_len : 实际使用到的索引长度
- ref : 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
- rows : 扫描的行数
- filtered : 过滤后剩余条数占扫描条数的百分比
- extra : 额外信息
type:值类型
- system
- const
- eq_ref : 通过主键的等值查询
- ref : 二级索引的等值查询
- fulltext
- ref_or_null
- index_merge
- unique_subquery
- index_subquery
- range : 索引的范围查询
- index : 扫描全部索引
- all
二、MVCC 和 MySQL锁
2.1.MVCC和锁的关系
在MySQL中是如何处理并发读写问题?
假如现在有两个事务A\B分别对id=1这条数据进行操作,会出现哪些问题?
问题 | 描述 |
脏写 |
如果先执行语句2,然后执行语句5,如果未提交事务的写操作可以互相影响,那么会造成脏写 即:一个事务修改了另一个未提交事务修改过的数据就是脏写 |
脏读 |
在事务A中,如果在事务B执行完语句5之后为提交,语句1,1-1读出的id=1数据的name为jason_2 则说明发生了脏读. 即读取到了一个未提交事务修改后的值. |
不可重复读 |
如果事务中可以读取到其它事务已经提交的值,则会发生不可重复读. 不可重复读和脏读的区别是,不可能重复读是读取的已提交事务的值. |
幻读 |
一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的 记录也读出来 . 如果读出来的记录,被删除,再次读取,算不算幻读? 不算,幻读指的是增加. |
事务并发执行可能带来各种问题,事务并发访问相同记录的情况主要分为以下三种:
- 读-读
- 无影响
- 写-写
- 脏写的问题,在任何隔离级别里面都不允许发生,当存在并发写时,需要通过锁来进行数据修改的排队.
- 读-写或写-读
- 会有脏读、不可重复读、幻读问题
SQL标准规定的不同隔离级别,解决问题的力度如下:
- READ UNCOMMITTED (读未提交): 可能发生脏读、不可重复读、幻读
- READ COMMITTED (读已提交) : 可能发放不可重复读、幻读
- REPEATABLE READ(可重复读) : 可能发放幻读
- SERIALIZABLE (串行读) : 所有问题都不可能发生
对于事务并发访问相同记录,所带来的三种情况:
- 读-读: 允许发生
- 写-写:不允许脏写发生,需要进行排队处理,有MySQL的锁进行负责.
- 读-写,写-读:针对脏读、不可重复读、幻读问题,可以有两种解决方案:
- 方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
- 读写操作都采用加锁的方式
所以对于读来说有两种方式的读,一种是MVCC读取数据,称为快照读,也称为一致性读.另一种是当前读,即加锁读,加锁的方式有两种:共享锁(S锁)和排他锁(独占锁,X锁).
- 所有普通的Select语句在RC、RR隔离级别下都算是一致性读.
- 加锁读
- 共享锁(S锁) : select * from t where xxx lock in share mode
- 独占锁(X锁) : select * from t where xxx for update
- 写操作
- Delete :
- 对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取一下这条记录的X锁,然后再执行delete mark操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。
- update,在对一条记录做UPDATE操作时分为三种情况:
- 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。其实 我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读。
- 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录 彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护。
- 如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。
- Delete :
S、X兼容性:
兼容性 | S | X |
S | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 |
2.2.MVCC
MVCC: Muti Version Concurrency Controll 多版本并发控制
MVCC 是通过版本链+ReadView来实现的.
在RC隔离级别里,每次进行快照读操作的时候都会重新生成新的ReadView,所以每次可以查询到最新的结果在RR隔离级别里,只有当事务在第一次进行快照读的时候才会生成ReadView,之后进行的快照读操作都会沿用之前的.
2.3.锁
2.3.1.Innodb中锁介绍
在MySQL的Innodb中即支持表锁,也支持行锁.
表级别锁介绍:
1.执行DDL操作时,会对表进行加锁,但是加的是元数据锁,这时会阻塞select、insert、update、delete操作
2.表锁,分为S锁和X锁, S锁: Lock tables 表名 read , X锁: Lock tables 表名 write
3.IS\IX锁,当我们在对使用InnoDB存储引擎的表的某些记录加S锁之前,那就需要先在表级别加一个IS锁,当我们在对使用InnoDB存储引擎的表的某些记录加X锁之前,
那就需要先在表级别加一个IX锁。IS锁和IX 锁的使命只是为了后续在加表级别的S锁和X锁时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。
4.表级别的AUTO-INC锁
4.1.采用AUTO-INC锁,也就是在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,
在该语句执行结束后,再把AUTO-INC锁释放掉。 这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
4.2.采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,
就把该轻量级锁释放掉,并 不需要等到整个插入语句执行完才释放锁。
在MySQL中通过,innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值,当 innodb_autoinc_lock_mode值为0时,一律采用AUTO-INC锁;当innodb_autoinc_lock_mode值为2时,一律采用轻量级锁;当innodb_autoinc_lock_mode值为1时,两种方式混着来(也就是在插入 记录数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。不过当innodb_autoinc_lock_mode值为2时,可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交 叉的,在有主从复制的场景中是不安全的。
从不同角度来看Mysql的锁:
2.3.2.Innodb中的行锁
行锁实现方式:
无索引行锁会升级为表锁(RR级别会升级为表锁,RC级别不会升级为表锁)
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁。
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。
间隙锁:
间隙锁,锁的就是两个值之间的空隙。间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
Mysql默认级别是repeatable-read,有办法解决幻读问题吗?间隙锁在某些情况下可以解决幻读问题。
假设account表里数据如下:
select * from account;id name balance 1 刘备 300 2 张飞 100 3 关羽 300 10 黄忠 1000 20 诸葛亮 100
那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,
在Session_1下面执行 update account set name = '张三' where id > 8 and id <18;,则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
间隙锁是在可重复读隔离级别下才会生效。
临键锁(Next-key Locks)
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间,是一个左开右闭区间。临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。
记录锁
记录锁也叫行锁,例如:
select * from emp where empno = 1 for update;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。