导图社区 mysql
索引是帮助存储引擎快速获取数据的一种数据结构,一起来了解MySQL 是怎么加锁的吧。
编辑于2023-03-31 09:12:18 黑龙江省数据库
主题
索引
什么是索引
帮助存储引擎快速获取数据的一种数据结构
什么是存储引擎
如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法
按照数据结构区分
B+tree索引、Hash索引、Full-text索引·
为什么要尽可能的减少IO访问的次数
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作
B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
回表
如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据。
索引覆盖
当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据。
为什么MYsqlInnode选择B+树作为索引的数据结构
B+树与B树
B+树是在叶子节点上存储数据,而B树的非叶子节点也要存储数据,所以B+树的非叶子节点能存储的索引值更多,在相同的IO操作下,就能查询更多的节点
B+树与二叉树
即使数据达到千万级别,B+tree的高度依然维持在3~4层,也就是说只需要进行3~4层的磁盘的IO操作就能查询到目标数据,但是二叉树的每个节点最多只能有看i两个儿子节点,要比B+树高上不少,所以二叉树检索到目标数据时,要经历的磁盘I/O次数要更多
B+树与哈希表
Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因
按照物理存储分类
聚簇索引(主键索引)
对主键字段建立的索引叫做主键索引,聚簇索引的B+树的叶子节点上存放的是实际数据
辅助索引
对普通字段建立的索引叫做二级索引,存放的是主键值,而不是具体的数据
按照字段的个数划分
单列索引
联合索引
多个字段普通字段组成一个索引
最左匹配原则
也就是会按照最左优先的索引进行匹配,在进行联合索引的时候,如果不按照最左优先原则会导致索引失效,因为有些索引是全局无序的,但是在局部是有序的,而且利用索引的前提就是索引里边的Key是有序的,
范围查询
在遇到返回查询如(<或者>)的时候就会停止匹配,也就是在范围查询的字段可以用到联合索引,但是在范围查询字段的后边的字段无法用到联合索引,,但是对于>=,<=,between,like前缀匹配的范围查询,就不会停止匹配
什么时候需要创建索引/不需要创建索引
索引的优点:提高查询速度
索引的缺点:1:占用物理空间,数量越大占用的空间越大,2:创建索引和维护索引要耗费时间,这种时间随着数据量的增大而增大 3:会降低表的增删改的效率,因为每次增删改索引,B+树为了维护索引有序性,都需要进行动态的维护
什么时候使用索引
字段有唯一性限制
经常用于where查询条件的字段,这样就能提高整个表的查询速度,查询条件不是一个字段,可以建立联合索引
经常用于group by或者order by的字段,这样在查询的时候就不需要再去做一次排序了
什么时候不需要创建索引
表的数据太少的时候,不需要创建索引
字段中有大量重复的数据,不需要创建索引
字段需要进行经常的更新不需要创建索引
where groupby orderby里边用不到的数据
索引的优化
innoDB是如何存储数据的
InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。
数据页的结构
为什么要有页目录
数据页中的记录按照主键顺序组成单向链表,如果没有页目录,就需要从头遍历链表
页目录与记录的关系
页中的记录划分为组,页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量称之为槽,每个槽相当于指针指向了不同组的最后一个记录,槽里边有曹号,通过槽号进行二分查找
页目录就是由多个槽组成的,槽相当于分组记录索引,然后,因为记录是按照快速定位要查询的记录在哪个槽,定位到槽后,在遍历槽内所有的记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表
槽内的记录有限制
第一个分组中的记录只能有 1 条记录; 最后一个分组中的记录条数范围只能在 1-8 条之间; 剩下的分组中记录条数范围只能在 4-8 条之间。
B+树是如何进行查询的
B+树的查询记录
只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他上层节)仅用来存放目录项作为索引。 非叶子节点分为不同层次,通过分层来降低每一层的搜索量; 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询;
聚簇索引和二级索引
区分
叶子节点存放的是什么数据
聚簇索引的叶子节点存放的是实际数据,所有完整的数据存放在叶子节点中
二级索引的叶子节点存放在的是主键值
为什么选取B+树作为索引
索引的数据结构应该满足的条件
尽可能减少磁盘的IO访问次数
能高效的查询每一个数据的同时又能进行范围查询
到B+树演变过程
数组
缺点
插入新元素的时候性能太低,插入一个元素需要将,这个元素的之后的所有元素后移一位
二叉搜索树
缺点
虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是当插入的节点都是二叉树中最大的元素,二叉查找树就会退化成一条链表。查找数据的时间复杂度变成了O(n)
树存储在磁盘中,访问每个节点,都对应一次磁盘I/O操作,也就是说树的高度就等于每次查询数据时磁盘的IO操作的次数,所有树越高,就会影响查询性能
二叉平衡树
缺点
虽然解决了二叉树的退化问题,但是二叉树有一个致命的弊端,就是每个节点只有两个孩子,所以只要数据量一大,那么树的高度就会很高,会影响整体数据查询的效率
多路平衡二叉树(B,B+树)
两者的性能对比
单点查询
B树进行单个查询的时候,最快可以在O(1)的时间代价就查询到,从平均时间上来看,会比B+树稍快一些,但是查询的波动较大,没有B+树稳定,B+树的非叶子节点不存放数据,只存放索引,所以当数据量相同的情况下,要比既存放数据有存放索引的B树要更加扁平,所以,B+树可以比B树更矮胖,
插入和删除效率
B+树的非叶子节点中有大量的冗余节点,所以在添加,或者删除节点的时候,不会发生复杂的树的变化
范围查询
B+树的叶子节点之间还有一个链表进行连接
适用场合
存在大量的范围检索的场景,适合使用B+树
对于大量的单个查询的场景可以考虑B树
索引失效
对索引使用左或者左右模糊匹配(当使用左或者是左右模糊匹配的时候,也就是like%xx或者like%xx%这两种方式都会造成索引失效)
因为索引B+树是按照索引值进行有序排列存储的,只能按照前缀进行比较,如果使用name like '%林',方式进行查询,因为查询的结果可能是陈林,张林,周林,之类的,所以不知道从哪个索引值开始比较,于是就采用全表查询的方法
对索引使用函数
索引保存的是索引字段的原始值,而不是经过函数计算之后的值,就没办法进行索引
对索引进行表达式计算
索引保存的是索引字段的原始值,而不是表达式之后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的是全表扫描
对索引隐式类型转换
如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描。
MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。
select * from t_user where phone = 1300000001;->select * from t_user where CAST(phone AS signed int) = 1300000001;
对索引使用了函数
联合索引非最左匹配
索引下推
可以在存储引擎层进行索引遍历过程中,对索引中包含的字段先做判断,直接滤掉不满足条件的数据,在返回给server层,从而减少回表次数
原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
WHERE 子句中的 OR
因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描
事务
事务有哪些特性
原子性
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样
通过undo log完成的
一致性
是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
通过持久性+原子性+隔离性来保证的
隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,每个事务同时处理相同的数据的时候,不会相互干扰,每个事务都有一个完整的数据空间,对其他的并发事务都是隔离的。
通过MVCC多版本并发控制器或者锁机制来保证的
持久性
事务结束后,对数据的修改是永久性的,即使系统故障也不会丢失
通过redo log重做事务来保证的
并发事务会引起什么问题
脏读
如果一个事务读到了另一个事务没有提交的数据
不可重复读
在一个事务多次读取同一个数据,如果出现两次读到的数据不一样的情况就会发生不可重复读
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
不可重复读和幻读的区别
不可重复读是针对行级别的,幻读是针对范围级别的
四种隔离等级
读未提交
指一个事务还没提交,他做的变更就能被其他的事务看到
会发生的问题
脏读,不可重复读,幻读
读提交
指一个事务提交之后,他做的变更才能被其他的时候看到
会发生的问题
不可重复读,幻读
可重复读
指一个事务执行过程中看到的数据,一直跟着这个事务启动时看到的数据是一致的,时默认的隔离等级
可能发生的问题
幻读
解决的办法
针对快照读,是是通过MVCC的方式解决了幻读
针对当前读,是通过next-key-lock方式解决了幻读,当执行select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入
update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。
Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。
事务 A 执行了这面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。 然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。
串行化
对记录加上读写锁,在多个事务对这条记录进行读写的时候,如果发生了读写冲突,后访问的事务必须等到前一个事务执行完成
不会使用串行化,会影响性能
隔离等级实现的方式
串行读
通过加读写锁的方式来避免并行访问的
读提交
可重复读
Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
开始事务<>启动事务
第一种:begin/start transaction 命令;
只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
第二种:start transaction with consistent snapshot 命令
马上启动事务。
Read Viem在MVCC里如何工作
trx_id:当一个事务对某条聚簇索引记录进行改动时,就会该事务的事务ID记录在trx_id的隐藏列中
roll_pointer:每次对某条聚簇索引记录进行改动的时候,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是个指针,指向每一个旧版本的记录,于是可以通过他找到修改前的记录
min_trx_id<m_ids<"""""""<max_trx_id
max_trx_id:是全局事务的最大值
修改后的记录可不可见,主要是查看当前记录的trx_id与是否在m_id队列中
可见
当前记录的trx_id小于read view中的min_trx_id值,表示这个版本的记录是在创建read view 前已经提交的事务生成的
如果记录的 trx_id 不在 m_ids列表中且trx_id在max_trx_id和min_trx_id之间,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见
不可见
当前记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
这种通过版本链来控制并发事务访问同一记录时的行为就叫MVCC(多版本并发控制)
为什么叫做通过版本链来控制并发事务访问
当前事务通过updata语句修改了该条记录,那么在当前记录的就会生成响应的trx_Id和undo log 并以链表的方式串联起来,形成版本链
这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。
可重复读是如何工作过的
是在启动一个事务的时候,创建了一个快照,而且在整个事务期间也会一直使用这个快照,不会因为其中一个事务提交了就把该事务从m_id队列中移除
事务A读取数据B:100,事务B修改数据B为200(一旦修改记录就会当前记录生成一个新的记录版本并且事务ID为当前ID,指针指向上一个版本,方便进行回溯),但是未提交,事务A读取数据B:100(当前修改数据的事务ID在mid中,不可见),事务B提交之后,仍不可见,因为快照还是最开始的那个,m_id每个发生改变
读提交是如何工作过的
在每次读取数据的时候都会生成一个新的快照
事务A读取数据B:100,事务B修改数据B为200(一旦修改记录就会当前记录生成一个新的记录版本并且事务ID为当前ID,指针指向上一个版本,方便进行回溯),但是未提交,事务A读取数据B:100(当前修改数据的事务ID在mid中,不可见),事务B提交之后,事务A读取数据B,创建一个新的快照,当前事务trx_id小于max_trx_Id,但是不在m_id中,所以更新数据可见
产生幻读的场景
当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。-------快照读
对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
避免幻读的场景
尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
锁
全局锁
执行之后,整个数据库就处于只读的状态,如对数据的增删改操作(insert delete update ),对表结构的更改操作(alter table,drop table)
主要作用
全库数据备份
缺点
如果对全局锁,意味着整个数据库都是只读状态,会造成业务停滞
在全局数据备份的时候,使用全局锁,会影响业务的处理,那么有什么其他的方式可以进行避免
如果数据的引擎支持可重复读的隔离级别,那么在进行备份数据库的时候可以先开启一个read view,然后在整个事务执行期间都使用这个read view进行备份,所以有了MVCC的帮助,在备份期间仍然可仍然可以进行业务的处理
表级锁
表锁
元数据库(MDL)
我们不需要显示的去指定MDL,因为我们对数据库进行操作的时候,会自动的给这个表加上MDL
MDL是为了保证当用户对表执行CRUD操作,防止其他线程对这个表的结构进行变更
当一个线程执行select语句(加上MDL读锁)如果有其他线程要更改该表的结构的时候(申请MDL写锁),将会被阻塞,直到MDL读锁被释放
MDL的释放时机
在当前事务进行释放的时候,我们才会进行释放MDL
当数据库里有长事务的时候,再对表结构进行变更操作的时候,会发生一些问题
当前事务A执行一条select语句(MDL读锁),但是一直没有释放,所以就会一直占用,这时候来了一个事务B去也执行select,可以顺利的执行,但是如果事务C想要去修改表的结构(MDL写锁)就会发生阻塞,但是又因为事务A是个长事务,所以说,事务C的业务就会一直卡顿,如果后续来了大量的select请求就会无法执行,大量的线程被阻塞住,这时候数据库的线程就会爆满
在申请MDL锁的时候有一个队列,在队列中写锁的优先级高于读锁,所以一旦出现MDL写锁阻塞就会导致所有的写锁阻塞
所以为了能安全的对表的结构进行更改,先要查看表中是否有长事务,如果有先kill这个长事务
意向锁
目的是为了快速的判断表里是否有记录被加锁
在使用InnoDB引擎的表里对某些记录加上共享锁之前,现在表级别前加上一个意向共享锁
在使用InnoDB引擎的表里对某些记录加上独占锁之前,现在表级别前加上一个意向独占锁
select对记录加共享锁和独占锁(锁定读)
select。。。lock in share mode
select。。 。for update
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁冲突,只会和共享表锁(lock tables ...read )和独占表锁(lock tables... write)发生冲突
冲突的形式:读写互斥,读读共享,写写互斥
AUTO-INC锁
作用
将表里的主键设置成自增的
特殊
不是一个事务提交后才进行释放,而是执行完插入语句就会释放
行级锁
Record Lock,记录锁,也就是仅仅把一条记录锁上
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)
其他事务就无法对该记录进行修改了
Gap Lock,间隙锁,锁定一个范围,但是不包含记录()
只存在可重复读隔离等级级别,目的是为了解决幻读现象
使用了间隙锁之后,当前范围内无法插入
间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的
Next-Key Lock(临键锁):Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身
next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中
与间隙锁的不同点
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
插入意向锁
表明有事务想在某个区间插入新记录,但是现在处于等待状态。
MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁
但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁,间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点
尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
MySQL 是怎么加锁的
什么 SQL 语句会加行级锁?
一般的select不会加锁,使用快照读,一般是无锁,使用MVCC多版本并发控制
select 。。。for update
select。。。lock in share mode
update 和 delete 操作都会加行级锁,且锁的类型都是独占锁(X型锁)。
MySQL 是怎么加行级锁的
加锁的对象是索引,加锁的基本单位是next-key lock,加锁的区间(],间隙锁是区间(),但是在一定的情况下 next-key lock 会退化为记录锁和间隙锁
总的来说,如果只使用记录锁或者间隙锁就能解决幻读的问题,就会进行退化
幻读:当一个事务前后两次查询的数据集,不相同的时候,就会发生幻读
解决幻读的关键
避免结果集某一条记录被其他事务删除,或者有其他事务插入了一条新记录
唯一索引等值查询
存在
退化为记录锁
原因
无法增加
主键具有唯一性,如何其他事务插入相同的主键时,会发生主键冲突,索引无法插入
无法删除
由于对当前记录加入了记录锁,所以其他的事务无法对当前记录进行删除
不存在
退化为间隙锁(最近的两者之间)
原因
如果是 next-key lock,就意味着其他事务无法删除 id = 5 这条记录,但是这次的案例是查询 id = 2 的记录,只要保证前后两次查询 id = 2 的结果集相同,就能避免幻读的问题了,所以即使 id =5 被删除,也不会有什么影响,那就没必须加 next-key lock,因此只需要在 id = 5 加间隙锁,避免其他事务插入 id = 2 的新记录就行了。
唯一索引范围查询
当唯一索引进行范围查询时,会对每一个扫描到的索引加 next-key 锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁: 情况一:针对「大于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。 情况二:针对「小于或者小于等于」的范围查询,要看条件值的记录是否存在于表中: 当条件值的记录不在表中,那么不管是「小于」还是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。 当条件值的记录在表中,如果是「小于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;如果「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
非唯一索引等值查询
非唯一索引范围查询
没有加索引的查询