本笔记为以前整理的零碎的关于Mysql的知识点,有深入源码的也有浅层的八股。已经被我整理成了一个pdf。
实习岗位正好也是和数据库内核有关的,之后应该还会更新。做个整理,方便秋招的时候快速回顾吧。
链接:链接
提取码:1234
目录
- 《DBNotes:Buffer Pool刷脏页细节以及改进》
- 获取一个空闲页的源码逻辑
- Page_Cleaner_Thread
- LRU_Manager_Thread
- Hazard Pointer作为驱逐算法改进
- 参考
- 《DBNotes:Join算法的前世今生》
- NestLoopJoin算法
- Simple Nested-Loop Join
- Index Nested-Loop Join
- Block Nested-Loop Join
- Batched Key Access
- Hash Join算法
- In-Memory Join(CHJ)
- On-Disk Hash Join
- 参考链接
- 《DBNotes_ Buffer Pool对于缓冲页的链表式管理》
- Buffer Pool回顾
- Buffer Pool内部组成
- freelist
- flushlist
- LRU链表管理以及改进
- 《DBNotes_single_table访问方法、MRR多范围读取优化、索引合并》
- single_table访问方法
- const
- ref
- ref_or_null
- range
- index
- all
- MRR多范围读取优化
- 索引合并
- intersection
- union
- sort-union
- 《MySQL8.0.22:Lock(锁)知识总结以及源码分析》
- 1、关于锁的一些零碎知识,需要熟知
- 事务加锁方式:
- Innodb事务隔离
- MVCC多版本并发控制
- 常用语句 与 锁的关系
- 意向锁
- 行级锁
- 2、锁的内存结构以及一些解释
- 3、InnoDB的锁代码实现
- 锁系统结构lock_sys_t
- lock_t 、lock_rec_t 、lock_table_t
- bitmap
- 锁的基本模式的兼容关系和强弱关系
- 行锁类别代码
- 记录锁的alloc函数
- 记录锁的add函数
- 记录锁的create函数
- 4、锁的流程
- 表锁加锁流程
- 行锁加锁流程
- 插入加锁流程
- 删除加锁流程带来的死锁
- 释放锁流程
- 死锁流程
- 5、参考
- count()用法
- 《MySQL——join语句优化tips》
- 要不要用join
- Join驱动表选择
- Multi-Range Read优化
- Batched Key Access (BKA)对NLJ进行优化
- BNL算法性能问题
- BNL转BKA
- 《MySQL——redo log 与 binlog 写入机制》
- binlog写入机制
- redo log写入机制
- 组提交机制实现大量的TPS
- 理解WAL机制
- 如何提升IO性能瓶颈
- 《MySQL——备库多线程复制策略》
- 备库并行复制能力
- MySQL5.6版本 并行复制策略
- MariaDB 并行复制策略
- MySQL5.7版本 并行复制策略
- MySQL5.7.22版本 并行复制策略
- 总结
- 《MySQL——查询长时间不返回的三种原因与查询慢的原因》
- 查询长时间不返回
- 等MDL锁
- 等flush
- 等行锁
- 查询慢
- 幻读现象
- 幻读带来的问题
- 如何解决幻读
- next-key lock
- 临时表的应用
- 临时表可以重名的原因
- 临时表的主备同步
- 覆盖索引优化查询
- 思考
- 事务
- 事务的必要性
- MySQL中如何控制事务
- 手动开启事务
- 事务的四大特征
- 事务的四大特征
- 事务开启方式
- 事务手动提交与手动回滚
- 事务的隔离性
- 脏读现象
- 不可重复读现象
- 幻读现象
- 串行化
- 一些补充
- 使用长事务的弊病
- `commit work and chain`的语法是做什么用的?
- 怎么查询各个表中的长事务?
- 如何避免长事务的出现?
- 事务隔离是怎么通过read-view(读视图)实现的?
- 参考
- 索引
- 回表
- 覆盖索引
- 最左前缀原则
- 联合索引的时候,如何安排索引内的字段顺序?
- 索引下推
- 重建索引问题
- 联合主键索引和 InnoDB 索引组织表问题
- in与between的区别
- 表锁是什么?表锁有什么用?表锁怎么用?
- 行锁是什么?行锁有什么用?行锁怎么用?
- 死锁与死锁检测
- 何时会死锁检测
- 如何避免高量级的死锁检测
- 练习
- 主备一致性
- 备库为什么要设置为只读模式?
- 备库设置为只读,如何与主库保持同步更新?
- A到B的内部流程如何?
- binlog内容是什么?
- `row`格式对于恢复数据有何好处
- M-M结构的循环复制问题以及解决方案
- 关于查询能力
- 关于change buffer
- 关于写能力(基于change buffer)
- MySQL索引底层原理理解以及常见问题总结
- 二叉查找树为索引
- 红黑树为索引
- B树作为索引
- B+树作为索引
- MyISAM存储引擎索引实现
- InnoDB存储引擎索引实现
- 常见问题
- 聚集索引与非聚集索引
- InnoDB基于主键索引和普通索引的查询有什么区别?
- InnoDB主键索引为何是整型的自增主键
- 何时使用业务字段作为主键呢?
- 哈希与B树
- “N叉树”的N值在MySQL中是可以被人工调整的么?
《DBNotes:Buffer Pool刷脏页细节以及改进》
本笔记知识沿用之前DBNotes: Buffer Pool对于缓冲页的链表式管理的部分知识
获取一个空闲页的源码逻辑
任何一个读写请求都需要从Buffer pool来获取所需页面。如果需要的页面已经存在于Buffer pool,那么直接利用当前页面进行操作就行。但是如果所需页面不在Buffer pool,比如UPDATE操作,那么就需要从Buffer pool中新申请空闲页面,将需要读取的数据放到Buffer pool中进行操作。
如何从buffer pool中获取一个页面呢?这依赖于buf_LRU_get_free_block函数,该函数会循环尝试去淘汰LRU list上的页面。每次循环都会访问freelist,查看是否有足够的空闲页面,如果没有,就继续从LRUlist去淘汰。这样的循环在负载较高的时候会加剧对freelist以及LRUlist的mutex的竞争。可以设置buf_pool->try_LRU_scan是做了一个优化,如果当前用户线程扫描的时候 发现没有空闲页面,那么其他用户线程就不需要进行同样的扫描。
MySQL的free页面的获取依赖于Page_Cleaner_Thread的刷新能力,如果刷新不及时,那么系统就会使用上面所说的循环逻辑来为用户线程申请空闲页面,可以看出是十分耗时间的。而如果刷新过快,也会导致性能问题,因为刷新是需要io操作的。
所以引入独立的线程负责LRU list的刷脏。目的是为了让独立线程根据系统负载动态调整LRU的刷脏能力。由于LRU list的刷脏从page cleaner线程中脱离出来,调整LRU list的刷脏能力不再会影响到page cleaner。
同时由于单线程LRUlist刷脏存在问题,设计者进行了改进。继续将LRU list独立于page cleaner threads并将LRU list单线程刷脏增加为多线程刷脏。page cleaner只负责flush list的刷脏,lru_manager_thread只负责LRU List刷脏。这样的分离,可以使得LRU list刷脏和Flush List刷脏并行执行。
Page_Cleaner_Thread
主要负责flushlist的刷脏,避免用户线程同步刷脏页。
也是每隔一定时间刷一次脏页,sleep time是自适应的,依赖于当前的lsn,flushlist中的oldest_modification以及当前的同步刷脏点。
与LRU_Manager_Thread不同,该线程每次执行刷的脏页数量也是自适应的,依赖于当前系统中脏页的比率,日志产生的速度以及几个参数。
LRU_Manager_Thread
一个系统线程,随着InnoDB启动而work,作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Freelist中,防止用户线程去做同步刷脏影响效率。
该线程每隔一段时间就去FLUSH。先尝试从LRU中驱逐部分数据页,如果数量不够就从Flushlist中驱逐。
线程执行频率是自适应的:
设定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances。
如果Freelist中的数量小于max_free_len 的1%,则sleep time = 0,表示这时候空闲页太少了,需要一直执行buf_flush_LRU_tail操作,从而腾出空闲的数据页。
如果Free List中的数量介于max_free_len的1%-5%,则sleep time减少50ms(默认为1000ms),如果Free List中的数量介于max_free_len的5%-20%,则sleep time不变,如果Free List中的数量大于max_free_len的20%,则sleep time增加50ms,但是最大值不超过rds_cleaner_max_lru_time
。
Hazard Pointer作为驱逐算法改进
在学术上,Hazard Pointer是一个指针,如果这个指针被一个线程所占有,在它释放之前,其他线程不能对他进行修改,但是在InnoDB里面,概念刚好相反,一个线程可以随时访问Hazard Pointer,但是在访问后,他需要调整指针到一个有效的值,便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。 先来说一下这个问题的背景,我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏,例如LRU_Manager_Thread和Page_Cleaner_Thread。同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A,就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中了),重新扫描新的可刷盘的脏页。另一方面,数据页刷盘是异步操作,在刷盘的过程中,我们会把对应的数据页IO_FIX住,防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘,当前Flush List中所有页面都可以被刷盘(buf_flush_ready_for_replace
返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。 要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。 这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。
参考
MySQL · 源码分析 · Innodb缓冲池刷脏的多线程实现
MySQL · 源码分析 · InnoDB LRU List刷脏改进之路
MySQL · 引擎特性 · InnoDB Buffer Pool
《DBNotes:Join算法的前世今生》
在8.0.18之前,MySQL只支持NestLoopJoin算法,最简单的就是Simple NestLoop Join,MySQL针对这个算法做了若干优化,实现了Block NestLoop Join,Index NestLoop Join和Batched Key Access等,有了这些优化,在一定程度上能缓解对HashJoin的迫切程度。但是HashJoin的支持使得MySQL优化器有更多选择,SQL的执行路径也能做到更优,尤其是对于等值join的场景。
NestLoopJoin算法
长期以来,在MySQL中执行联接的唯一算法是嵌套循环算法的变体。
Simple Nested-Loop Join
如果我们执行这样一条等值查询语句:
select * from t1 straight_join t2 on (t1.a=t2.b);
由于表 t2 的字段 b 上没有索引,每次到 t2 去匹配的时候,就要做一次全表扫描。就相当于是双for循环。如果 t1 和 t2 都是 10 万行的表(当然了,这也还是属于小表的范围),就要扫描 100 亿行。
SimpleNestLoopJoin显然是很低效的,对内表需要进行N次全表扫描,实际复杂度是N*M,N是外表的记录数目,M是记录数,代表一次扫描内表的代价。为此,MySQL针对SimpleNestLoopJoin做了若干优化。
Index Nested-Loop Join
如果我们能对内表的join条件建立索引,那么对于外表的每条记录,无需再进行全表扫描内表,只需要一次Btree-Lookup即可,整体时间复杂度降低为N*O(logM)。
再来看看这一句
select * from t1 straight_join t2 on (t1.a=t2.a);
在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,因此这个语句的执行流程是这样的:
执行流程示意图如下:
对比HashJoin,对于外表每条记录,HashJoin是一次HashTable的search,当然HashTable也有build时间,还需要处理内存不足的情况,不一定比INLJ好。
Block Nested-Loop Join
MySQL采用了批量技术,即一次利用join_buffer_size缓存足够多的记录,每次遍历内表时,每条内表记录与这一批数据进行条件判断,这样就减少了扫描内表的次数,如果内表比较大,间接就缓解了IO的读压力。
Simple Nested-Loop Join 与 Block Nested-Loop Join从时间复杂度上来说,这两个算法是一样的。但是,Block Nested-Loop Join是内存操作,速度上会快很多,性能也更好。
示意图如下:
Batched Key Access
IndexNestLoopJoin利用join条件的索引,通过Btree-Lookup去匹配减少了遍历内表的代价。如果join条件是非主键列,那么意味着大量的回表和随机IO。BKA优化的做法是,将满足条件的一批数据按主键排序,这样回表时,从主键的角度来说就相对有序,缓解随机IO的代价。BKA实际上是利用了MRR特性(MultiRangeRead),访问数据之前,先将主键排序,然后再访问。主键排序的缓存大小通过参数read_rnd_buffer_size控制。
Hash Join算法
NestLoopJoin算法简单来说,就是双重循环,遍历外表(驱动表),对于外表的每一行记录,然后遍历内表,然后判断join条件是否符合,进而确定是否将记录吐出给上一个执行节点。从算法角度来说,这是一个M*N的复杂度。HashJoin是针对equal-join场景的优化,基本思想是,将外表数据load到内存,并建立hash表,这样只需要遍历一遍内表,就可以完成join操作,输出匹配的记录。如果数据能全部load到内存当然好,逻辑也简单,一般称这种join为CHJ(Classic Hash Join),之前MariaDB就已经实现了这种HashJoin算法。如果数据不能全部load到内存,就需要分批load进内存,然后分批join,下面具体介绍这几种join算法的实现。
In-Memory Join(CHJ)
HashJoin一般包括两个过程,创建hash表的build过程和探测hash表的probe过程。
1).build phase
遍历外表,以join条件为key,查询需要的列作为value创建hash表。这里涉及到一个选择外表的依据,主要是评估参与join的两个表(结果集)的大小来判断,谁小就选择谁,这样有限的内存更容易放下hash表。
2).probe phase
hash表build完成后,然后逐行遍历内表,对于内表的每个记录,对join条件计算hash值,并在hash表中查找,如果匹配,则输出,否则跳过。所有内表记录遍历完,则整个过程就结束了
On-Disk Hash Join
CHJ的限制条件在于,要求内存能装下整个外表。在MySQL中,Join可以使用的内存通过参数join_buffer_size控制。如果join需要的内存超出了join_buffer_size,那么CHJ将无能为力,只能对外表分成若干段,每个分段逐一进行build过程,然后遍历内表对每个分段再进行一次probe过程。假设外表分成了N片,那么将扫描内表N次。这种方式当然是比较弱的。
在MySQL8.0中,如果join需要内存超过了join_buffer_size,build阶段会首先利用hash算将外表进行分区,并产生临时分片写到磁盘上;然后在probe阶段,对于内表使用同样的hash算法进行分区。由于使用分片hash函数相同,那么key相同(join条件相同)必然在同一个分片编号中。接下来,再对外表和内表中相同分片编号的数据进行CHJ的过程,所有分片的CHJ做完,整个join过程就结束了。这种算法的代价是,对外表和内表分别进行了两次读IO,一次写IO。相对于之之前需要N次扫描内表IO,现在的处理方式更好。
顺序为:外表的分片、内表分片、哈希连接
参考链接
join语句怎么优化?
MySQL8.0 新特性 Hash Join
哈希加入MySQL 8
MySQL · 新特征 · MySQL 哈希连接实现介绍
《DBNotes_ Buffer Pool对于缓冲页的链表式管理》
Buffer Pool回顾
我们知道针对数据库的增删改删操作都是在Buffer Pool中完成的,一条sql的执行步骤可以认为是这样的:
1、innodb存储引擎首先在缓冲池中查询有没有对应的数据,有就直接返回
2、如果不存在,则去磁盘进行加载,并加入缓冲池
3、同时该记录会被加上独占锁,防止多人修改,出现数据不一致
而且我们知道,可以通过设置my.cnf
配置中的innodb_buffer_pool_size
来修改缓冲池大小,加快sql查询速度,当然也需要注意设置过大会造成系统swap空间被占用,导致系统变慢降低查询性能。
Buffer Pool内部组成
缓冲池对应一片连续内存,我们将其划分为大小为16kb的页(与innodb对应),这些页称为缓冲页。
为了很好的管理这些页,设计者为每个缓冲页都创建了一些控制信息:表空间编号、页号、缓冲页在缓冲池中的地址、链表节点信息等。将每个页对应的控制信息占用的一块内存称为一个控制块。控制块与缓冲页一一对应,都存放在缓冲池中。
在Mysql启动时,会自己完成对缓冲池的初始化:向操作系统申请内存,自己划分成若干对控制块和缓冲页。
freelist
当我们从磁盘中load一个数据页到缓冲池中,我们应该放到哪个缓冲页中呢?
很显然我们应该把数据页放到“空闲”的缓冲页中。
设计者将所有空闲的缓冲页对应的控制块作为一个节点放到一个链表中,称为freelist。每次从freelist中取出一个空闲的缓冲页中,并且将该缓冲页对应的控制块信息填上,然后将该节点移除,表示缓冲页已经被使用了
flushlist
当一个控制块节点被从freelist中移除,说明该页已经被使用了。如果这种“使用操作”是对数据进行修改的话,那么必定需要将该页数据flush到磁盘上。但是每次修改一页就将那一页flush的话,磁盘IO占用率高。所以每次修改缓冲页后,将这些脏页控制块放入一个fulshlist上。当flush时机到了,就把flushlist节点对应的缓冲页刷新搭配磁盘上。
LRU链表管理以及改进
缓冲池内存有限,当freelist中没有多余的空闲缓冲页,就需要把某些旧的缓冲页从缓冲池中移除,然后把新的数据页放进来。为了提高内存命中率,使用LRU。
但是普通的LRU不能解决下面的问题;
1、加载到缓冲池的页不一定被用到(针对于预读)
2、如果有非常多的使用频率低的页被同时加载到缓冲池中,则可能会把那些使用频率非常高的页从缓冲池中淘汰。(针对全表扫描)
关于innodb对于LRU的改进见如链接:
MySQL——Innodb改进LRU算法
当然还有进一步的优化:
对于young区域的缓冲页,每次访问一个缓冲页就要把它移动到LRU链表的头部,开销比较大。毕竟,young区域的缓冲页都是热点数据。所以我们可以这样优化:只有被访问的缓冲页位于young区域1/4的后面时,才会被移动到LRU链表头部。也就是说我们将young的前0.25部分称为very young,very young里面的数据访问不会移动到头部,因为大家访问频率都是非常高的。
提醒一下,在LRUlist的节点不是freelist节点,可能是flushlist节点。不理解的话,再去上面看看两个list定义。
然而这一切的目的只有一个:尽量高效地提高缓冲池命中率。
《DBNotes_single_table访问方法、MRR多范围读取优化、索引合并》
single_table访问方法
const
在主键列或者unique二级索引与一个常数进行等值比较时才有效。
如果主键或者unique二级索引的索引列由多个列构成,则只有在索引列中的每个列都与常数进行等值比较时,才是const访问
ref
搜索条件为二级索引(非unique)与常数进行等值比较,形成的扫描区间为单点扫描区间(即【‘abc’,‘abc’】),采用二级索引来执行查询的访问方法为ref。注意采用二级索引执行查询时,每获取到一条二级索引记录就会进行一次回表操作。
TIPS:
- 二级索引列允许存储NULL值时不限制NULL值的数量,所以执行key is NULL查询时最优只能执行ref操作
- 索引列中包含多个列的二级索引时,只要最左边连续的列是与常数进行等值比较,就可以使用ref访问。
ref_or_null
当想找出某个二级索引列的值等于某个常数的记录,并且将该列中值为NULL的记录也找出来:
select * from single_table where key1 = 'abc' or key1 is null;
若使用二级索引,此时的扫描区间为:[‘abc’,‘abc’] 以及[NULL,NULL]。
这种访问方法即为ref_or_null。
range
select * from single_table where key2 IN (1438,6328) OR (key2 >= 38 AND key2 <= 79);
使用二级索引,扫描区间为[1438,1438] 、[6328,6328]、[38,79],改扫描区间为若干个单点扫描区间或者范围扫描区间。访问方法为range。当然(-无穷,+无穷)不为range访问方法。
index
key_part1,key_part2,key_part3 为二级索引,它们三个构成了一个联合索引,并且key_table2并不是联合索引的最左列。
select key_part1,key_part2,key_part3 from single_table where key_table2 = 'abc';
此时无法形成合适的范围区间来减少扫描的记录数量。
需要注意此时的查询符合两个条件:
- 查询列表中key_part1,key_part2,key_part3,都包含在联合索引中
- 搜索条件只有key_part2,这个列也包含在联合索引中
很显然,需要扫描全部的联合索引,扫描区间为[-无穷,+无穷]。由于二级索引记录只有存放索引列和主键,也不需要回表,所以此时扫描去不的二级索引记录比直接扫描全部的聚集索引记录成本要小。这种方法称为index访问。
又如:
select * from single_table order by id;
通过全表扫描对表进行查询时有order by。此时也是使用index方法。
all
全表扫描,直接扫描全部的聚集索引记录。
MRR多范围读取优化
select * from single_table where key1 = 'abc' and key2 > 1000;
该语句的执行步骤:
1、通过key1的索引定位扫描区间[‘abc’,‘abc’];
2、根据上面得到的主键值回表,得到完整用户记录,然后检测记录是否满足key2 > 1000的条件,满足则返回
3、重复2步骤,直到不满足key1 = ‘abc’
每次从二级索引中读取到一条记录后,就会根据该记录的主键值执行回表操作。
而某个扫描区间中的二级索引记录的主键值是无序的,每次回表都会随机读取一个聚集索引页面,带来的IO开销较大。
MRR会先读取一部分二级索引记录,将它们的主键值排序后再同意执行回表操作,节省IO开销。
索引合并
intersection
使用多个索引完成一次查询的执行方法称为索引合并
select * from single_table where key1 = 'a' and key3 = 'b';
可以先搜key1的索引,然后回表,根据key3条件筛选。
也可以先搜key1的索引,然后回表,根据key1条件筛选。
当然可以同时使用key1和key2的索引。在key1索引中扫描key1值得到区间[‘a’,‘a’],在key3索引中扫描key3值得到区间[‘b’,‘b’];
然后从两者操作结果中找到id列值相同的记录。然后根据共有的id值执行回表,这样可能会省下回表操作带来的开销。
当然需要注意的是要求从不同二级索引中获取到的二级索引记录都按照主键值排好序:
- 从两个有序集合中取交集比两个从无序集合中取交集要容易
- 如果获取到的id值有序排列,则在根据这些id值执行回表操作时不再是进行单纯的随机IO,就会提高效率。
如果从扫描区间中获得的记录并不是按照主键值排序的,那么就不能使用intersection索引合并。
union
select * from single_table where key1 = 'a' or key3 = 'b';
同时使用key1和key2的索引。在key1索引中扫描key1值得到区间[‘a’,‘a’],在key3索引中扫描key3值得到区间[‘b’,‘b’];
然后对两个结果进行去重,对去重后的id值进行回表操作。
同样二级索引记录都是要按照主键值排序,如果从扫描区间中获得的记录并不是按照主键值排序的,那么就不能使用union索引合并。
sort-union
union索引合并条件苛刻,下面的查询就不能使用union索引合并
select * from single_table where key1 < 'a' or key3 > 'z';
我们可以这样操作;
1、根据key1<'a’条件从key1的二级索引中获取记录,并将获取到的记录的主键值排序
2、根据key3<'z’条件从key3的二级索引中获取记录,并将获取到的记录的主键值排序
3、按照union操作两个记录合并
sort-union 索引合并比union索引合并多了一步对二级索引记录的主键值进行排序。
《MySQL8.0.22:Lock(锁)知识总结以及源码分析》
1、关于锁的一些零碎知识,需要熟知
事务加锁方式:
两阶段锁:
整个事务分为两个阶段,前一个阶段加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但是不能解锁,直到事务释放第一个锁,就进入了解锁阶段,此阶段事务只能解锁,也可以操作数据,不能再加锁。
两阶段协议使得事务具有比较高的并发度,因为解锁不必发生在事务结尾。
不过它没有解决死锁问题,因为它在加锁阶段没有顺序要求,如果两个事务分别申请了A,B锁,接着又申请对方的锁,此时进入死锁状态。
Innodb事务隔离
在MVCC并发控制中,读操作可以分为两类:快照读和当前读。
快照读读取的是记录的可见版本(有可能是历史版本),不用加锁。
当前读,读取的是记录的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不再会并发修改这条记录。
- Read Uncommited:可以读未提交记录
- Read Committed(RC):当前读操作保证对独到的记录加锁,存在幻读现象。使用MVCC,但是读取数据时读取自身版本和最新版本,以最新为主,可以读已提交记录,存在不可重复
- Repeatable Read(RR):当前读操作保证对读到的记录加锁,同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入(间隙锁),不存在幻读现象。使用MVCC保存两个事务操作的数据互相隔离,不存在不可重复读现象。
- Serializable:MVCC并发控制退化为基于锁的并发控制。不区分快照读和当前读,所有读操作均为当前读,读加S锁,写加X锁。
MVCC多版本并发控制
MVCC是一种多版本并发控制机制。锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下替代行级锁,降低系统开销。
MVCC是通过保存数据在某个时间点的快照来实现的,典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存这个行的创建时间和删除时间,这里存储的并不是实际的时间值,而是版本号,可以理解为事务的ID。每开始一个新的事务,这个版本号就会自动递增。
对于几种的操作:
- INSERT:为新插入的每一行保存当前版本号作为版本号
- UPDATE:新插入一行记录,并且保存其创建时间为当前事务ID,同时保存当前
- DELETE:为删除的每一行保存当前版本号作为版本号
- SELECT:
- InnoDB只会查找版本号小于等于事务系统版本号
- 行的删除版本要么未定义要么大于当前事务版本号,这样可以确保事务读取的行,在事务开始删除前未被删除
事实上,在读取满足上述两个条件的行时,InnoDB还会进行二次检查。
活跃事务列表:RC隔离级别下,在语句开始时从全局事务表中获取活跃(未提交)事务构造Read View,RR隔离级别下,事务开始时从全局事务表获取活跃事务构造Read View:
1、取当前行的修改事务ID,和Read View中的事务ID做比较,若小于最小的ID或小于最大ID但不在列表中,转2步骤。若是大于最大ID,转3
2、若进入此步骤,可说明,最后更新当前行的事务,在构造Read View时已经提交,返回当前行数据
3、若进入此步骤,可说明,最后更新当前行的事务,在构造Read View时还未创建或者还未提交,取undo log中记录的事务ID,重新进入步骤1.
根据上面策略,在读取数据的时候,InnoDB几乎不用获得任何锁,每个查询都能通过版本查询,只获得自己需要的数据版本,从而大大提高了系统并发度。
缺点是:每行记录都需要额外的存储空间,更多的行检查工作,额外的维护工作。
一般我们认为MVCC有几个特点:
- 每个数据都存在一个版本,每次数据更新时都更新该版本
- 修改时copy出当前版本修改,各个事务之间没有干扰
- 保存时比较版本号,如果成功,则覆盖原记录;失败则rollback
看上去保存是根据版本号决定是否成功,有点乐观锁意味,但是Innodb实现方式是:
- 事务以排他锁的形式修改原始数据
- 把修改前的数据存放于undo log,通过回滚指针与主数据关联
- 修改成功后啥都不做,失败则恢复undo log中的数据。
innodb没有实现MVCC核心的多版本共存,undo log内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。当事务影响到多行数据,理想的MVCC无能为力。
如:事务1执行理想MVCC,修改row1成功,修改row2失败,此时需要回滚row1,但是由于row1没有被锁定,其数据可能又被事务2修改,如果此时回滚row1内容,会破坏事务2的修改结果,导致事务2违反ACID。
理想的MVCC难以实现的根本原因在于企图通过乐观锁代替二阶段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统数据并无区别,而二阶段提交是目前这种场景保证一致性的唯一手段。二阶段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾。innodb只是借了MVCC名字,提供了读的非阻塞。
采用MVCC方式,读-写操作彼此并不冲突,性能更高;如果采用加锁方式,读-写操作彼此需要排队执行,从而影响性能。一般情况下,我们更愿意使用MVCC来解决读-写操作并发执行的问题,但是在一些特殊业务场景中,要求必须采用加锁的方式执行。
常用语句 与 锁的关系
对读取的记录加S锁:
select ... lock in share mode;
对读取的记录加X锁:
select ... for update;
delete:
对一条语句执行delete,先在B+树中定位到这条记录位置,然后获取这条记录的X锁,最后执行delete mark操作。
update:
- 如果未修改该记录键值并且被更新的列所占用的存储空间在修改前后未发生变化,则现在B+树定位到这条记录的位置,然后再获取记录的X锁,最后在原记录的位置进行修改操作。
- 如果为修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改后发生变化,则先在B+树中定位到这条记录的位置,然后获取记录的X锁,然后将原记录删除,再重新插入一个新的记录。
- 如果修改了该记录的键值,则相当于在原记录上执行delete操作之后再来一次insert操作。
insert:
新插入的一条记录收到隐式锁保护,不需要在内存中为其生成对应的锁结构。
意向锁
为了允许行锁和表锁共存,实现多粒度锁机制。InnoDB还有两种内部使用的意向锁,两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁
意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁仅仅用于表锁和行锁的共存使用。它们的提出仅仅是为了在之后加表级S锁或者X锁是可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。
需要注意的三点:
1、意向锁是表级锁,但是却表示事务正在读或写某一行记录
2、意向锁之间不会冲突,因为意向锁仅仅代表对某行记录进行操作,在加行锁的时候会判断是否冲突
3、意向锁是InnoDB自动加的,不需要用户干预。
行级锁
-
Record Lock:就是普通的行锁,官方名称:
LOCK_REC_NOT_GAP
,用来锁住在聚集索引上的一条行记录 -
Gap Lock:用来在可重复读隔离级别下解决幻读现象。已知幻读还有一种方法解决:MVCC,还一种就是加锁。但是在使用加锁方案时有个问题,事务在第一次执行读取操作时,“幻影记录”还没有插入,所以我们无法给“幻影记录”加上Record Lock。InnoDB提出了Gap锁,官方名称:
LOCK_GAP
,若一条记录的numberl列为8,前一行记录number列为3,我们在这个记录上加上gap锁,意味着不允许别的事务在number值为(3,8)区间插入记录。只有gap锁的事务提交后将gap锁释放掉后,其他事务才能继续插入。注意:gap锁只是用来防止插入幻影记录的,共享gap和独占gap起到作用相同。对一条记录加了gap锁不会限制其他事务对这条记录加Record Lock或者继续加gap锁。另外对于向限制记录后面的区间的话,可以使用Supremum表示该页面中最大记录。
-
Next-Key Lock:当我们既想锁住某条记录,又想阻止其他事务在该记录前面的间隙插入新记录,使用该锁。官方名称:
LOCK_ORDINARY
,本质上就是上面两种锁的结合。 -
Insert Intention Lock:一个事务在插入一条你记录时需要判断该区间点上是否存在gap锁或Next-Key Lock,如果有的话,插入就需要阻塞。设计者规定,事务在等待时也需要在内存中生成一个锁结构,表明有个事务想在某个间隙中插入记录,但是处于等待状态。这种状态锁称为Insert Intention Lock,官方名称:
LOCK_INSERT_INTENTION
,也可以称为插入意向锁。
2、锁的内存结构以及一些解释
一个事务对多条记录加锁时不一定就要创建多个锁结构。如果符合下面条件的记录的锁可以放到一个锁结构中:
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型是一样的
- 等待状态是一样的
type_mode
是一个32位比特的数,被分为lock_mode
、lock_type
、rec_lock_type
三个部分。
低4位表示:lock_mode
,锁的模式
0:表示IS锁
1:表示IX锁
2:表示S锁
3:表示X锁
4:表示AI锁,就是auto-inc,自增锁
第5~8位表示:lock_type
,锁的类型
LOCK_TABLE:第5位为1,表示表级锁
LOCK_REC:第6位为1,表示行级锁
其余高位表示:rec_lock_type
,表示行锁的具体类型,只有lock_type
的值为LOCK_REC时,才会出现细分
LOCK_ORDINARY:为0,表示next-key锁
LOCK_GAP:为512,即当第10位设置为1时,表示gap锁
LOCK_REC_NOT_GAP:为1024,当第11位设置为1,表示正常记录锁
LOCK_INSERT_INTENTION:为2048,当第12位设置为1时,表示插入意向锁
LOCK_WAIT:为256,当第9位设置为1时,表示is_waiting为false,表明当前事务获取锁成功。
一堆比特位
其他信息:涉及了一些哈希表和链表
更加细节的结构可以看这一张图:
3、InnoDB的锁代码实现
锁系统结构lock_sys_t
详细讲解见:https://dev.mysql.com/doc/dev/mysql-server/latest/structlock__sys__t.html#details
锁系统结构,在innodb启动的时候初始化,在innodb结束时释放。保存锁的hash表,相关事务、线程的一些信息
/** The lock system struct */
struct lock_sys_t {/** The latches protecting queues of record and table locks */locksys::Latches latches;/** The hash table of the record (LOCK_REC) locks, except for predicate(LOCK_PREDICATE) and predicate page (LOCK_PRDT_PAGE) locks */hash_table_t *rec_hash;/** The hash table of predicate (LOCK_PREDICATE) locks */hash_table_t *prdt_hash;/** The hash table of the predicate page (LOCK_PRD_PAGE) locks */hash_table_t *prdt_page_hash;/** Padding to avoid false sharing of wait_mutex field */char pad2[ut::INNODB_CACHE_LINE_SIZE];/** The mutex protecting the next two fields */Lock_mutex wait_mutex;/** Array of user threads suspended while waiting for locks within InnoDB.Protected by the lock_sys->wait_mutex. */srv_slot_t *waiting_threads;/** The highest slot ever used in the waiting_threads array.Protected by lock_sys->wait_mutex. */srv_slot_t *last_slot;/** TRUE if rollback of all recovered transactions is complete.Protected by exclusive global lock_sys latch. */bool rollback_complete;/** Max lock wait time observed, for innodb_row_lock_time_max reporting. */ulint n_lock_max_wait_time;/** Set to the event that is created in the lock wait monitor thread. A valueof 0 means the thread is not active */os_event_t timeout_event;#ifdef UNIV_DEBUG/** Lock timestamp counter, used to assign lock->m_seq on creation. */std::atomic<uint64_t> m_seq;
#endif /* UNIV_DEBUG */
};
lock_t 、lock_rec_t 、lock_table_t
无论是行锁还是表锁都使用lock_t结构保存,其中用一个union来分别保存行锁和表锁不同的数据,分别为lock_table_t和lock_rec_t
/** Lock struct; protected by lock_sys latches */
struct lock_t {/** transaction owning the lock */trx_t *trx;/** list of the locks of the transaction */UT_LIST_NODE_T(lock_t) trx_locks;/** Index for a record lock */dict_index_t *index;/** Hash chain node for a record lock. The link node in a singlylinked list, used by the hash table. */lock_t *hash;union {/** Table lock */lock_table_t tab_lock;/** Record lock */lock_rec_t rec_lock;};/** Record lock for a page */
struct lock_rec_t {/** The id of the page on which records referenced by this lock's bitmap arelocated. */page_id_t page_id;/** number of bits in the lock bitmap;NOTE: the lock bitmap is placed immediately after the lock struct */uint32_t n_bits;/** Print the record lock into the given output stream@param[in,out] out the output stream@return the given output stream. */std::ostream &print(std::ostream &out) const;
};struct lock_table_t {dict_table_t *table; /*!< database table in dictionarycache */UT_LIST_NODE_T(lock_t)locks; /*!< list of locks on the sametable *//** Print the table lock into the given output stream@param[in,out] out the output stream@return the given output stream. */std::ostream &print(std::ostream &out) const;
};
bitmap
Innodb 使用位图来表示锁具体锁住了那几行,在函数 lock_rec_create 中为 lock_t 分配内存空间的时候,会在对象地址后分配一段内存空间(当前行数 + 64)用来保存位图。n_bits 表示位图大小。
锁的基本模式的兼容关系和强弱关系
/* LOCK COMPATIBILITY MATRIX* IS IX S X AI* IS + + + - +* IX + + - - +* S + - + - -* X - - - - -* AI + + - - -** Note that for rows, InnoDB only acquires S or X locks.* For tables, InnoDB normally acquires IS or IX locks.* S or X table locks are only acquired for LOCK TABLES.* Auto-increment (AI) locks are needed because of* statement-level MySQL binlog.* See also lock_mode_compatible().*/
static const byte lock_compatibility_matrix[5][5] = {/** IS IX S X AI *//* IS */ { TRUE, TRUE, TRUE, FALSE, TRUE},/* IX */ { TRUE, TRUE, FALSE, FALSE, TRUE},/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},/* X */ { FALSE, FALSE, FALSE, FALSE, FALSE},/* AI */ { TRUE, TRUE, FALSE, FALSE, FALSE}type_mode
};/* STRONGER-OR-EQUAL RELATION (mode1=row, mode2=column)* IS IX S X AI* IS + - - - -* IX + + - - -* S + - + - -* X + + + + +* AI - - - - +* See lock_mode_stronger_or_eq().*/
static const byte lock_strength_matrix[5][5] = {/** IS IX S X AI *//* IS */ { TRUE, FALSE, FALSE, FALSE, FALSE},/* IX */ { TRUE, TRUE, FALSE, FALSE, FALSE},/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},/* X */ { TRUE, TRUE, TRUE, TRUE, TRUE},/* AI */ { FALSE, FALSE, FALSE, FALSE, TRUE}
};
行锁类别代码
#define LOCK_WAIT \256 /*!< Waiting lock flag; when set, it \means that the lock has not yet been \granted, it is just waiting for its \turn in the wait queue */
/* Precise modes */
#define LOCK_ORDINARY \0 /*!< this flag denotes an ordinary \next-key lock in contrast to LOCK_GAP \or LOCK_REC_NOT_GAP */
#define LOCK_GAP \512 /*!< when this bit is set, it means that the \lock holds only on the gap before the record; \for instance, an x-lock on the gap does not \give permission to modify the record on which \the bit is set; locks of this type are created \when records are removed from the index chain \of records */
#define LOCK_REC_NOT_GAP \1024 /*!< this bit means that the lock is only on \the index record and does NOT block inserts \to the gap before the index record; this is \used in the case when we retrieve a record \with a unique key, and is also used in \locking plain SELECTs (not part of UPDATE \or DELETE) when the user has set the READ \COMMITTED isolation level */
#define LOCK_INSERT_INTENTION \2048 /*!< this bit is set when we place a waiting \gap type record lock request in order to let \an insert of an index record to wait until \there are no conflicting locks by other \transactions on the gap; note that this flag \remains set when the waiting lock is granted, \or if the lock is inherited to a neighboring \record */
#define LOCK_PREDICATE 8192 /*!< Predicate lock */
#define LOCK_PRDT_PAGE 16384 /*!< Page lock */
记录锁的alloc函数
Create the lock instance,创建一个lock实例,在create函数中被调用。主要就是分配一些内存,还有设置事务请求记录锁、锁的索引号、锁的模式、行锁的pageid、n_bits。
/**
Create the lock instance
@param[in, out] trx The transaction requesting the lock
@param[in, out] index Index on which record lock is required
@param[in] mode The lock mode desired
@param[in] rec_id The record id
@param[in] size Size of the lock + bitmap requested
@return a record lock instance */
lock_t *RecLock::lock_alloc(trx_t *trx, dict_index_t *index, ulint mode,const RecID &rec_id, ulint size) {ut_ad(locksys::owns_page_shard(rec_id.get_page_id()));/* We are about to modify structures in trx->lock which needs trx->mutex */ut_ad(trx_mutex_own(trx));lock_t *lock;if (trx->lock.rec_cached >= trx->lock.rec_pool.size() ||sizeof(*lock) + size > REC_LOCK_SIZE) {ulint n_bytes = size + sizeof(*lock);mem_heap_t *heap = trx->lock.lock_heap;lock = reinterpret_cast<lock_t *>(mem_heap_alloc(heap, n_bytes));} else {lock = trx->lock.rec_pool[trx->lock.rec_cached];++trx->lock.rec_cached;}lock->trx = trx;lock->index = index;/* Note the creation timestamp */ut_d(lock->m_seq = lock_sys->m_seq.fetch_add(1));/* Setup the lock attributes */lock->type_mode = LOCK_REC | (mode & ~LOCK_TYPE_MASK);lock_rec_t &rec_lock = lock->rec_lock;/* Predicate lock always on INFIMUM (0) */if (is_predicate_lock(mode)) {rec_lock.n_bits = 8;memset(&lock[1], 0x0, 1);} else {ut_ad(8 * size < UINT32_MAX);rec_lock.n_bits = static_cast<uint32_t>(8 * size);memset(&lock[1], 0x0, size);}rec_lock.page_id = rec_id.get_page_id();/* Set the bit corresponding to rec */lock_rec_set_nth_bit(lock, rec_id.m_heap_no);MONITOR_INC(MONITOR_NUM_RECLOCK);MONITOR_INC(MONITOR_RECLOCK_CREATED);return (lock);
}
记录锁的add函数
将锁添加到记录锁哈希和事务的锁列表中。
void RecLock::lock_add(lock_t *lock) {ut_ad((lock->type_mode | LOCK_REC) == (m_mode | LOCK_REC));ut_ad(m_rec_id.matches(lock));ut_ad(locksys::owns_page_shard(m_rec_id.get_page_id()));ut_ad(locksys::owns_page_shard(lock->rec_lock.page_id));ut_ad(trx_mutex_own(lock->trx));bool wait = m_mode & LOCK_WAIT;hash_table_t *lock_hash = lock_hash_get(m_mode);lock->index->table->n_rec_locks.fetch_add(1, std::memory_order_relaxed);if (!wait) {lock_rec_insert_to_granted(lock_hash, lock, m_rec_id);} else {lock_rec_insert_to_waiting(lock_hash, lock, m_rec_id);}#ifdef HAVE_PSI_THREAD_INTERFACE
#ifdef HAVE_PSI_DATA_LOCK_INTERFACE/* The performance schema THREAD_ID and EVENT_ID are used onlywhen DATA_LOCKS are exposed. */PSI_THREAD_CALL(get_current_thread_event_id)(&lock->m_psi_internal_thread_id, &lock->m_psi_event_id);
#endif /* HAVE_PSI_DATA_LOCK_INTERFACE */
#endif /* HAVE_PSI_THREAD_INTERFACE */locksys::add_to_trx_locks(lock);if (wait) {lock_set_lock_and_trx_wait(lock);}
}
记录锁的create函数
就是调用alloc,然后add加锁,
Create a lock for a transaction and initialise it.
@param[in, out] trx Transaction requesting the new lock
@param[in] prdt Predicate lock (optional)
@return new lock instance */
lock_t *RecLock::create(trx_t *trx, const lock_prdt_t *prdt) {ut_ad(locksys::owns_page_shard(m_rec_id.get_page_id()));/* Ensure that another transaction doesn't access the trxlock state and lock data structures while we are adding thelock and changing the transaction state to LOCK_WAIT.In particular it protects the lock_alloc which uses trx's private pool oflock structures.It might be the case that we already hold trx->mutex because we got here from:- lock_rec_convert_impl_to_expl_for_trx- add_to_waitq*/ut_ad(trx_mutex_own(trx));/* Create the explicit lock instance and initialise it. */lock_t *lock = lock_alloc(trx, m_index, m_mode, m_rec_id, m_size);#ifdef UNIV_DEBUG/* GAP lock shouldn't be taken on DD tables with some exceptions */if (m_index->table->is_dd_table &&strstr(m_index->table->name.m_name,"mysql/st_spatial_reference_systems") == nullptr &&strstr(m_index->table->name.m_name, "mysql/innodb_table_stats") ==nullptr &&strstr(m_index->table->name.m_name, "mysql/innodb_index_stats") ==nullptr &&strstr(m_index->table->name.m_name, "mysql/table_stats") == nullptr &&strstr(m_index->table->name.m_name, "mysql/index_stats") == nullptr) {ut_ad(lock_rec_get_rec_not_gap(lock));}
#endif /* UNIV_DEBUG */if (prdt != nullptr && (m_mode & LOCK_PREDICATE)) {lock_prdt_set_prdt(lock, prdt);}lock_add(lock);return (lock);
}
4、锁的流程
lock system 开始启动 申请lock_sys_t结构,初始化结构体
lock system 结束关闭 释放lock_sys_t结构的元素,释放结构体
表锁加锁流程
1、检查当前事务是否拥有更强的表锁,如果有的话直接返回成功,否则继续往下走2、遍历表的锁列表,判断是否有冲突的锁,没有转3,有转43、直接创建一个表锁,放入事务的lock list中,放入table 的lock list中,加锁成功4、创建等待的表锁,然后进行死锁检测和死锁解决,回滚当前事务或者挂起当前事务
行锁加锁流程
插入加锁流程
1、对表加IX锁2、对修改的页面加X锁3、如果需要检测唯一键冲突,尝试给需要加的唯一键加一个S | next-key lock。可能会产生锁等待4、判断是否插入意向锁冲突,冲突的话加等待的插入意向锁,不冲突直接插入数据5、释放页面锁
删除加锁流程带来的死锁
删除加锁有个问题,删除并发的时候的加锁会导致死锁。
1、事务1获取表IX锁2、事务1获取页面X锁3、事务1获取第n行的 x | not gap锁4、事务1删除第n行5、事务1释放页面X锁6、事务2获取页面X锁7、事务2尝试获取第n行的 x | not gap锁,发现冲突,等待8、事务2释放页面X锁9、事务1释放第n行的锁,提交事务10、释放第n行锁的时候,检查到事务2有一个等待锁,发现可以加锁了,唤醒事务2,成功加锁11、事务3获取页面X锁12、事务3尝试删除第n行,发现第n行已经被删除,尝试获取第n行的next-key lock,发现事务2有个 x| gap锁冲突,等待13、事务3释放页面X锁14、事务2获取页面X锁,检查页面是否改动,重新检查第n行数据,发现被删,尝试获取该行next- key lock,发现事务3在等待这个锁,事务2冲突,进入等待15、造成死锁
释放锁流程
死锁流程
构造wait-for graph
构造一个有向图,图中的节点代表一个事务,图的一个边A->B代表着A事务等待B事务的一个锁
具体实现是在死锁检测时,从当前锁的事务开始搜索,遍历当前行的所有锁,判断当前事务是否需要等待现有锁释放,是的话,代表有一条边,进行一次入栈操作
死锁检测
有向图判断环,用栈的方式,如果有依赖等待,进行入栈,如果当前事务所有依赖的事务遍历完毕,进行一次出栈
回滚事务选择
如果发现循环等待,选择当前事务和等待的事务其中权重小的一个回滚,具体的权重比较函数是 trx_weight_ge, 如果一个事务修改了不支持事务的表,那么认为它的权重较高,否则认为 undo log 数加持有的锁数之和较大的权重较高。
5、参考
1、https://segmentfault.com/a/1190000017076101?utm_source=coffeephp.com
2、Mysql 8.022源代码
3、深入浅出MySQL 8.0 lock_sys锁相关优化
count()用法
count()语义:该函数为一个聚合函数,对于返回的结果集一行行地判断,如果count函数地参数不是NULL,累计值就加1,否则不加。最后返回累计值。
所以count(*),count(主键id)和count(1)都表示返回满足条件地结果集地总行数;
而count(字段)则表示返回满足条件地数据行里面,参数“字段”不为NULL的总个数。
count(主键id):
InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。
sever层拿到id后,判断id是不可能为空的,就按行累加
count(1):
InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加
count(字段):
1、如果这个字段定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加
2、如果这个字段允许为null,那么在执行的时候,要判断字段是否为null,不是null才累加
count(*):
不会把全部字段取出来,而是专门做了优化,不取值。并且count(*)肯定不是null,按行累加。
所以按照效率排序的话: count(字段) < count(主键id) < count(1) 约等于 count(*)
InnoDB是支持事务的,MyISAM不支持事务。
InnoDB每一行记录都要判断自己是否对这个会话是否可见,所以对于count(*)请求来说,InnoDB只好把数据一行一行地读出依次判断,可见地行才能够用于计算“基于这个查询”地表地总行数。
《MySQL——join语句优化tips》
要不要用join
1、如果使用的是Index Nested-Loop Join
算法,即可以用上被驱动表的索引,可以用
2、如果使用的是Block Nested-Loop Join
算法。扫描行数过多,尤其是大表join会导致扫描多次被驱动表,会占用大量系统资源,这种Join尽量不要用
Join驱动表选择
1、如果是Index Nested-Loop Join
算法,使用小表做驱动表
2、如果是Block Nested-Loop Join
算法,在 join_buffer_size 足够大,大表小表一样,当 join_buffer_size 不够大时,选择小表做驱动表
注意,在决定哪个表做驱动表时,应该是两个表按照各自条件过滤完成之后,计算参与join的各个字段的总数据量,数据量小的表,那就是小表。
Multi-Range Read优化
若有这样查询语句:
select * from t1 where a>=1 and a<=100;
a值是递增的,但是回表后的id并非如此,而是随机的,会带来性能损失。
大多数数据按照主键递增顺序插入得到,所以我们可以认为如果按照主键的递增顺序查找的话,对磁盘的读比较接近顺序读,从而可以提升读性能。
1、根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中;
2、将read_rnd_buffer中的id进行递增排序;
3、排序后的id数组,依次到主键id索引中查找记录,并作为结果返回
总的来说就是:**先将索引数据缓存,查到id之后,排序之后再回表 **
用法:
设置:
set optimizer_switch="mrr_cost_based=off
现在的优化器在判断消耗时,更倾向于不使用MRR,所以需要设置为off后,就会固定使用MRR
Batched Key Access (BKA)对NLJ进行优化
Index Nested-Loop Join
执行逻辑是:从驱动表t1,一行行取出a值,再到驱动表t2去做join。对于表t2来说,每次都是匹配一个值,MMR优势用不上。
既然这样,将表t1的数据取出来一部分,先放到一个临时内存里:join_buffer.
然后在此基础上复用MRR即可。
使用方法:
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
BNL算法性能问题
之前提到过InnoDB的LRU优化:第一次从磁盘读入内存的数据页,会先放到old区域,如果1s后这个数据页不再被访问,就不会移动到LRU链表头部,这样对Buffer Pool命中率影响就不大了。
如果使用了BNL的join语句,多次扫描一个冷表,并且这个语句执行时间超过1s,就会在再次扫描冷表时,把冷表的数据页移动到LRU链表头部。
如果冷表数据很大, 会一直占据old区,正常页无法进入,无法更新young区
tips: 冷表,指表中数据还没有加载到bufferpool中,需要先从盘里读出来的表
又因为优化机制,一个正常访问的数据页要进入young区域,需要隔1s再次被访问到。由于join’语句在循环都磁盘和淘汰内存页,进入old区域的数据页很可能在1s之内就被淘汰了。
大表join后对于Buffer Pool的影响是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。
总结,BNL对于系统的影响:
1、可能多次扫描被驱动表,占用磁盘IO资源
2、判断join条件执行M * N次,占用CPU资源
3、可能导致Buffer Pool的热数据被淘汰,影响内存命中率
所以我们需要优化BNL,通过给驱动表的join字段加索引的方式,将BNL转换为BKA
BNL转BKA
对于一些不常执行大表join的sql,不在被驱动表上创建索引的情况,可以创建一个临时表 create templete table在这个临时表上创建索引,然后让驱动表与临时表做join操作。 为什么不在被驱动表上创建索引,是因为这块sql功能不常用,创建索引浪费空间,并且可能触发这块的join sql 也不经常调用。
创建临时表以及join语句示例如下:
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
《MySQL——redo log 与 binlog 写入机制》
WAL机制告诉我们:只要redo log与binlog保证持久化到磁盘里,就能确保MySQL异常重启后,数据可以恢复。
下面主要记录一下MySQL写入binlog和redo log的流程。
binlog写入机制
1、事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
2、binlog cache,系统为每个线程分配了一片binlog cache内存,参数binlog_cache_size控制单个线程内binlog cache大小。如果超过了这个大小就要暂存磁盘
3、事务提交的时候,执行器把binlog cache里完整的事务写入binlog中。并清空binlog cache
4、每个线程都有自己的binlog cache,共用一份binlog文件
5、write,是把日志写入到文件系统的page cache,内存中,没有持久化到磁盘,所以速度比较快,图中的fsync是将数据持久化到磁盘,占用磁盘的IOPS
关于何时write、fsync是由参数sync_binlog控制的:
1、sync_binlog = 0时,每次提交事务都只write,不fsync;
2、sync_binlog = 1时,每次提交事务都会执行fsync;
3、sync_binlog = N(N>1)时,表示每次提交事务都write,但累积N个事务后才fsync。
sync_binlog控制binlog真正刷盘的频率,对于一个IO非常大的情景,这个数字调大可以提高性能,但是如果容错率非常低的情况下,必须设为1.(sync_binlog设置为N对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志)
redo log写入机制
事务在执行过程中,生成的redo log是要先写到redo log buffer的。
redo log buffer里面的内容并不需要每次生成后都要持久化到磁盘中。
如果事务执行期间MySQL发生异常重启,那么这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。
事务没提交的时候,redo log buffer部分日志也是有可能被持久化到磁盘中的。
上面三个颜色表征了redo log可能的三种状态:
1、存在redo log buffer中,物理上是在MySQL进程内存中,即红色部分;
2、写到磁盘(write),但是没有持久化(fsync),物理上实在文件系统的page cache里面,即黄色部分;
3、持久化到磁盘,对应的是hard disk,也就是图中的绿色部分;
前两步是写内存,最后一步是磁盘IO,所以要在page cache够大且不影响写入page cache前将redo log 持久化到磁盘 。
为了控制redo log 的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,他有三种可能取值:
1、设置为0,每次事务提交的时候都只是把redo log留在redo log buffer中;
2、设置为1,每次事务提交的时候都只是把redo log直接持久化到磁盘;
3、设置为2,每次事务提交时都只是把redo log写到page cache;
与binlog不同,binlog是每个线程都有一个binlog cache,而redo log是多个线程共用一个redo log buffer。
InnoDB有一个后台线程,每隔1s,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘,事务执行过程中的redo log也是直接写在redo log buffer上的,所以,未提交的事务的redolog也可能被持久化到磁盘。
还有两种场景也会导致没有提交的事务的redo log写入到磁盘中:
情形1:
redo log buffer占用的空间即将达到innodb_log_buffer_size一半的时候,后台线程会主动写盘。
(这里只是write,没有fsync)
情形2:
并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。
(事务A执行一半,部分redo log到buffer中;事务B提交,且 innodb_flush_log_at_trx_commit ,会把redo log buffer里的log全部持久化到磁盘中)
补充说明
两阶段提交在时序上redo log先prepare 再写binlog,最后再把redo log commit;
innodb_flush_log_at_trx_commit 设置成 1,prepare阶段redo log就已经落盘。所以redo log再commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了。
sync_binlog 和 innodb_flush_log_at_trx_commit都设置为1,即一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare阶段),一次是binlog。
组提交机制实现大量的TPS
首先介绍日志逻辑序列号(log sequence number,LSN)的概念。LSN是单调递增的,每次写入长度length的redo log,LSN的值就会加上length。
三个并发事务(trx1,trx2,trx3)在prepare阶段,都写完redo buffer,并持久化到磁盘。
对应的LSN为50、120、160.
对应流程:
1、trx1第一个到达,被选为这组的leader;
2、等trx1要开始写盘的时候,这个组里面已经有三个事务,这时候LSN也变成了160;
3、trx1去写盘的时候,LSN=160;trx1返回时,所有LSN<= 160的redo log都被持久化到磁盘中;
4、trx2与trx3直接返回。
总结:
一次组提交中,组员越多,节约磁盘IOPS的效果越好。如果是单线程,就只能一个事务对应一次持久化操作
这样保证binlog也可以组提交了。由于step3速度快,所以集合到一起的binlog比较少,所以binlog的组提交效果不如redo log组提交。
提升binlog效果:
--1.binlog_group_commit_sync_delay :b表示延迟多少微秒后才调用fsync;
--2.binlog_group_commit_sync_no_delay_count :表示累积多少次以后才调用fsync;
理解WAL机制
WAL机制是减少磁盘写,可是每次提交事务都要写redo log和binlog ,磁盘读写次数没有变少。
所以WAL机制主要得益于两个方面:
--1、redo log和binlog都是顺序写,磁盘的顺序写比随机写速度要快
--2、组提交机制,可以降低磁盘IOPS消耗
如何提升IO性能瓶颈
1、设置binlog_group_commit_sync_delay
和binlog_group_commit_sync_no_delay_count
参数,减少binlog的写盘次数。这个方法是基于“额外故意等待”来实现的,可能会增加语句的响应时间,但是不会丢失数据
2、将sync_binlog
设置为大于1的值(100~1000)。不过会有主机掉电时丢binlog日志的风险
3、将 innodb_flush_log_at_trx_commit 设置为2。会有主机掉电丢数据的风险
《MySQL——备库多线程复制策略》
备库并行复制能力
主要涉及两个方面的并行度:
1、客户端写入主库的能力
2、备库上sql_thread
执行中转日志relay log
1的并行能力比2强。
主库上由于InnoDB支持行锁,对业务并行度的支持比较友好。
备库上如果用单线程,会导致备库应用日志不够快,造成主备延迟。
现在MySQL使用的是多线程复制
coordinator 就是原来的sql_thread,不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,是worker线程。线程个数由slave_parallel_workers
决定,一般设置为8~16。
coordinator在分发事务的时候,要遵循两个要求:
- 不能造成更新覆盖。也就是说更新同一行的两个事务必须被分发到同一个worker中。
- 同一个事务不能被拆开,必须放到同一个worker中。
MySQL5.6版本 并行复制策略
支持粒度:库
用于决定分发策略的hash表key值:数据库名
优势:
1、构造hash值快;一个实例上的DB数目不会很多。
2、不要求binlog格式。row和statement格式的binlog都可以拿到库名。
缺点:
1、主库表在同一个DB中,策略失效
2、不同DB热点不同,起不到并行效果
MariaDB 并行复制策略
策略:
1、能够在同一组里提交的事务,一定不会修改同一行
2、主库上可以并行执行的事务,备库上一定是可以并行执行的
为了实现该策略,MariaDB实现方法为:
1、在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1
2、commit_id直接写到binlog里
3、传到备库应用的时候,相同commit_id的事务分发到多个worker执行
4、一组全部执行完后,coordinator再去取下一批
这个策略目标就是备库模拟主库的并行模式。
不过主库再一组事务commit的时候,下一组事务实际上是处于"执行中"状态的。
而按照MariaDB策略,在备库上执行的时候,要等一组事务完全执行完,下一组事务才能开始执行,这样系统的吞吐量就不够。
这个策略,对于长事务来说不友好。如果一组里有一个超大事务线程,该组其他线程执行完后要等待这个线程执行完,之后才能切换到下一组。这段时间,只有一个线程进行工作,浪费了资源。
MySQL5.7版本 并行复制策略
策略思想:
1、同时处于prepare状态的事务,在备库执行时是可以并行的
2、处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的
通过调节binlog_group_commit_sync_delay
和binlog_group_commit_sync_no_delay_count
参数
来来拉长binlog从write到fsync的时间,以此减少binlog’的写盘次数。同时在并行复制策略里,可以用来制造更多“同时处于prepare阶段的事务”。这样就能增加备库复制的并行度。
通俗来讲,这两个参数,既可以让主库提交慢一点,又可以让备库执行快一点。在MySQL5.7处理备库延迟时,可以调节这两个参数,达到提升备库复制并行度的目的。
MySQL5.7.22版本 并行复制策略
新增了一个参数binlog-transaction-dependency-tracking
,用来控制是否启用这个新策略。
可选值:
1、COMMIT_ORDER
,表示根据同时进入prepare和commit来判断是否可以并行
2、WRITESET
,表示对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,即writeset没有交集,就可以并行。
3、WRITESET_SESSION
,在WRITESET
基础上多了一个约束:在主库上同一线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序
为了唯一标识,hash通过"库名+表名+索引名+值"计算。如果表上除了主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增加一个hash值。
这个版本的好处在于:
--1、writeset是在主库生成后直接写入到binlog里的,在备库执行的时候,不需要解析binlog内容,节省了备库计算量
--2、不需要把整个事务的binlog都扫一边才能决定分发到哪个worker,更加节省内存
--3、备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的
对于表上没有主键和外键约束的场景,WRITSET策略也没有办法并行,会暂时退化为单线程模型。 所以,表是否有主键,也是影响主备同步延迟原因之一。
总结
单线程复制能力低于多线程复制,对于更新压力较大的主库,备库可能一直追不上主库。
MySQL备库并行策略,修改了binlog的内容,也就是说不是向上兼容的,所以需要注意。
《MySQL——查询长时间不返回的三种原因与查询慢的原因》
构造一张表,表有两个字段id和c,再里面插入了10万行记录
create table 't' ('id' int(11) not null,'c' int(11) default null,primary key ('id')
) engine = InnoDB;delimiter ;;
create procedure idata()
begindeclare i int;set i = 1;while( i <= 100000) doinsert into t values(i,i);set i = i+1;end while;
end;;
delimiter ;call idata();
查询长时间不返回
在表t执行:
select * from t where id = 1;
查询结果长时间不返回。
等MDL锁
大概率是表t被锁住了,接下来分析原因:一般都是首先执行show processlist命令,看看当前语句处于什么状态。
表示现在有个线程正在表t上请求或者持有MDL写锁,把select语句阻塞了:
session A通过lock table命令持有表t的MDL写锁,而sessionB 的查询需要获取MDL读锁,所以session B 进入等待状态。
处理方式:找到谁持有MDL写锁,然后把它kill掉。
通过
select blocking_pid from sys.schema_table_lock_waits;
得到blocking_pid = 4;
然后用kill命令断开即可。
等flush
在表t上执行下面语句:
select * from information_schema.processlist where id=1;
可以查看出该线程的状态是Waiting for table flush;
表示现在有一个线程正要对表t做flush操作。
flush tables t with read lock; --只关闭表t
--or
flush tables with read lock; --关闭MySQL里面所有打开的表
正常来说,这两个语句执行起来都很快,除非它们也被别的线程堵住了。
所以可能是:有一个flush tables
命令被别的语句堵住了,然后它又堵住了我们的select语句。
下图是执行结果:
等行锁
select * from t where id = 1 lock in share mode;
由于访问id = 1这个记录时要加读锁,如果这时候已经有一个事务在这行记录上持有一个写锁,我们的select语句就会被堵住,如下:
session A启动事务,占用写锁,但是不提交,导致session B被堵住。
可以通过:
mysql> select * from t sys.innodb_lock_waits where locked_table='`test`.`t`'\G
进行查询,查出是谁占着这个写锁
发现是4号线程,然后我们kill 4
查询慢
select * from t where c = 50000 limit 1;
由于字段c上没有索引,所以这个语句只能走id主键顺序扫描,因此需要扫描5万行。
扫描行数多,所以执行慢,这个很好理解。
而下面的这条语句扫描行数为1,但是执行时间取却较长
select * from t where id = 1;
在这个场景下,session A先启动了一个事务,之后session B才开始执行update语句。
session B 更新完 100 万次,生成了 100 万个回滚日志 (undo log)
带lock in share mode 的sql是当前读,因此会直接读到 1000001 ,所以速度很快。
select * from t where id = 1语句是一致性读,因此需要从 1000001 开始,依次执行undo log,执行100万次后,才将1返回
**回滚日志过大引起的一致性读慢,当前读快 **
#《MySQL——幻读与next-key lock与间隙锁带来的死锁》
create table 't' ('id' int(11) not null,'c' int(11) default null,'d' int(11) default null,primary key ('id'),key 'c' ('c')
) engine = InnoDB;
insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
该表除了主键id,还有索引c。问下面的语句序列,是怎么加锁的,加的锁又是什么时候释放的呢?```sql
begin;
select * from t where d = 5 for update;
commit;
这条语句会命中d=5这一行,对应主键id=5,因此在select语句执行完成后,id=5这一行会加一个写锁,并且由于两阶段锁协议,这个写锁会在执行commit语句的时候释放。
由于字段d上没有索引,因此这条查询语句会做全表扫描,那么,其他被扫描的不满足的行记录会不会被加锁?
幻读现象
如果旨在id=5这一行加锁,而其他行不加锁,在下面这个情况下:
session A执行了三次当前读,并且加上了写锁。
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
幻读与不可重复读的区别
在同一个事务中,两次读取到的数据不一致的情况称为幻读和不可重复读。幻读是针对insert导致的数据不一致,不可重复读是针对 delete、update导致的数据不一致。
1、在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。
2、session B的修改结果被session A之后的select语句用当前读看到,不能称为幻读。幻读仅仅指"新插入的行"
幻读带来的问题
1、破坏语义。
session A在T1就说了,把d=5的行锁住,不准别的事务进行读写,此时被破坏。
因为如果我们这时插入d=5的数据,这条新的数据不在锁的保护范围之内。
2、数据一致性问题
锁的设计是为了保证数据的一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。
即使给所有行加上了锁,也避免不了幻读,这是因为给行加锁的时候,这条记录还不存在,没法加锁 。
也就是说即使把所有的记录都上锁了,还是阻止不了新插入的记录
如何解决幻读
产生的幻读的原因是:行锁只能锁住行
为了解决幻读问题,InnoDB引入新的锁:间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的空隙,比如在表t,初始化插入了6个记录,就产生了7个间隙:
执行:
select * from t where d = 5 for update
6个记录加上了行锁,同时加上了7个间隙锁。
间隙锁与行锁有点不一样
行锁可以分为读锁与写锁
与行锁有冲突关系的是另外一个行锁。
间隙锁不一样,间隙锁之间不存在冲突关系。
与间隙锁存在冲突关系的,是"向间隙中插入一个记录"这个操作。
举例:
由于表t中并没有c=7这个记录,所以session A加的是间隙锁(5,10)。而session B也是在这个间隙加的间隙锁,它们的目标都是保护这个间隙,不允许插入值,所以两者不冲突。
next-key lock
间隙锁与行锁合称next-key lock,每个lock都是前开后闭区间。间隙锁是开区间。
如上面我们插入数据,使用:
select * from t for update
形成了7个next-key lock,分别是:
(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]
supremum是一个不存在的最大值。
next-key lock 的引入解决了幻读问题,但是也带来了新的问题。
如,现在有这样一个业务逻辑:
任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据。
begin;
select * from t where id = N for update;
--如果行不存在
insert into t values(N,N,N);
--如果行存在
update t set d = N set id = N;commit;
现在出现这个现象:这个逻辑一旦有并发,就会碰到死锁。
死锁的产生:两个间隙锁不冲突,相互等待行锁
执行流程:
1、session A执行select…for update语句,由于id=9这一行不存在,因此会加上间隙锁(5,10)
2、session B执行select…for update语句,同样会加上间隙锁(5,10)
3、session B插入(9,9,9),被session A的间隙锁锁住,进入等待
4、session A擦汗如·插入(9,9,9),被session B的间隙锁锁住。
InnoDB死锁检测发现了这对死锁关系,然后报错返回了。
所以说间隙锁的引入可能会导致相同的语句锁住更大的范围,从而影响并发度。
间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。 但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。
#《MySQL——临时表》
## 内存表与临时表区别
临时表,一般是人手动创建。 内存表,是mysql自动创建和销毁的。
内存表,指的是使用Memory引擎的表,建表语法:create table ... engine = memeory
表的数据存在内存里,系统重启后会被清空,但是表的结构还在。
临时表,可以使用各种引擎类型。如果使用的是InnoDB或者MyISAM引擎,写数据是写在磁盘上的。当然临时表也可以使用Memory引擎。
临时表特性
1、一个临时表只能被创建它的session访问,对于其他线程不可见,当此session结束时,会自动删除临时表
2、临时表可以与普通表同名。如果同一个session里有同名的临时表和普通表,使用show create语句以及增删改查语句,访问的是临时表
3、show tables命令不显示临时表
临时表的应用
由于不用担心线程之间的重名冲突,临时表经常被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。
查询语句到所有的分库中查找满足条件的行,然后统一做order by操作。
可以把各个分库拿到的数据汇总到一个MySQL实例的一个表中,然后在这个汇总实例上做逻辑操作。如下:
至于临时表的存储位置,可以放在分库中的某一个。
另外一个使用场景就是使用union(如果使用的是union all就不需要用了)。系统会先创建一个内部临时表,执行第一个子查询的结果放到临时表中,执行第二个子查询的结果先看看插入是否成功,成功则插入。最后从临时表中按行取数据,然后返回结果,删除临时表。
临时表可以重名的原因
无论是普通表还是临时表,一个表都会对应一个table_def_def
- 一个普通表的
table_def_def
的值由"库名+表名"得到。所以在同一个库下创建两个同名的普通表,会由重复性错误。 - 对于临时表,
table_def_def
在“库名+表名”的基础上还加上了“server_id + thread_id”
在实现上,每个线程都维护了自己的临时表链表,每次session内操作表的时候,先遍历链表,检查是否有这个名字的的临时表,有就优先操作,否则再操作普通表。
session结束时,对链表中的每个临时表,执行drop操作。这个操作也会被写道binlog里用于主备复制。
临时表的主备同步
row格式的binlog不会记录临时表相关语句,只有statement或者mixed格式才会记录。
创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出的时候会自动删除临时表,但是备库同步线程还是在运行的,所以主库还需要写个DROP TEMPORARY TABLE
传给备库。
当主库上两个session创建了同名临时表t1,这两个语句被传给备库上。
主库执行语句的线程id会被写道binlog中,备库可以用线程id构造临时表的table_def_key
:
备库名 + t1 + “主库的serverid” + “session的thread_id”
,所以两个表在备库的应用线程不会冲突。
#《MySQL——使用联合索引、覆盖索引,避免临时表的排序操作》
## 联合索引避免临时表排序
在上一篇笔记(MySQL——order by逻辑(全字段排序与rowid排序))中,讲到查询语句查询多个字段的时候使用order by语句实现返回值是有序的,而order by是使用到了临时表的,会带来时间和空间损失。
其实使用联合索引,就可以避免临时表的排序操作。
只要保证city这个索引上取出来的行天然就是按照name递增排序的话,就可以不用再排序了。
alter table t add index city_user(city,name);
在这个索引里面,通过树搜索的方式定位到第一个满足city = '杭州’的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要city值是杭州,name值一定有序。
查询流程变为:
1、从索引(city,name)找到第一个满足city = '杭州’条件的主键id;
2、到主键id索引取出整行,取name、city、age三个字段值,作为结果集的一部分直接返回
3、从索引(city,name)取下一个记录主键id;
4、重复step2、3直到查到第1000条记录,或者不满足city = '杭州’条件时循环结束。
覆盖索引优化查询
可以使用覆盖索引继续优化查询的执行流程:
覆盖索引指,索引上的信息足够满足查询请求,不需要再回到主键索引上取数据。
针对select city,name,age from t
这个查询,可以创建一个city、name和age的联合索引,对应语句为:
alter table t add index city_user_age(city,name,age);
这时,对于city字段的值相同的行来说,还是按照name字段的值递增排序。查询语句的执行流程变为:
1、从索引(city,name,age)找到第一个满足city = '杭州’条件的记录,取出其中的city、name和age三个字段值,作为结果集的一部分直接返回
2、从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回
3、重复步骤2,直到查到第1000条记录,或者是不满足city = '杭州’条件时循环结束。
当然,并不是说每个查询能用上覆盖索引,就要把语句中涉及的字段都建上联合索引。因为索引有维护代价。
思考
假设表里面已经有了city_name(city,name)联合索引。你需要查询杭州和苏州两个城市中所有市民的名字,并且按名字排序,显示前100条记录。
select * from t where city in('杭州','苏州') order by name limit 100;
这个语句会有排序。因为条件是苏州或杭州。如果只有一个条件如只有杭州,那么就不需要排序操作。
如果我们需要实现一个在数据库端不需要排序的方案,可以这么实现:
把这一条语句拆成两条语句,流程如下:
1、执行select * from t where city = '杭州' order by name limit 100;
(这个语句不需要排序,客户端用一个长度为100的内存数组A保存结果)
2、执行select * from where city = '苏州' order by name limit 100;
(相同的方法,结果被存入内存数组B)
3、对AB两个有序数组采用归并排序,得到name最小的前100值,这就是我们需要的结果了。
事务
事务的必要性
mysql中,事务是一个最小的不可分割的工作单元。事务能够保证一个业务的完整性。
比如我们的银行转账:
-- a -> -100
UPDATE user set money = money - 100 WHERE name = 'a';-- b -> +100
UPDATE user set money = money + 100 WHERE name = 'b';
如果程序中,只有一条语句执行成功了,而另外一条没有执行成功,就会出现前后不一致。就会有人白嫖。
因此,在执行多条有关联 SQL 语句时,事务可能会要求这些 SQL 语句要么同时执行成功,要么就都执行失败。
也就是说事务具有原子性。
MySQL中如何控制事务
1、mysql是默认开启事务的(自动提交)
默认事务开启的作用:
-- 查询事务的自动提交状态
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
| 1 |
+--------------+
当我们执行一个sql语句时候,效果会立即体现出来,且不能回滚。
回滚举例
CREATE DATABASE bank;USE bank;CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20),money INT
);INSERT INTO user VALUES (1, 'a', 1000);SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+
执行插入语句后数据立刻生效,原因是 MySQL 中的事务自动将它提交到了数据库中。那么所谓回滚的意思就是,撤销执行过的所有 SQL 语句,使其回滚到最后一次提交数据时的状态。
在 MySQL 中使用 ROLLBACK 执行回滚:
由于所有执行过的 SQL 语句都已经被提交过了,所以数据并没有发生回滚。
-- 回滚到最后一次提交
ROLLBACK;SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+
将自动提交关闭后,可以数据回滚:
-- 关闭自动提交
SET AUTOCOMMIT = 0;-- 查询自动提交状态
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
| 0 |
+--------------+
现在我们测试一下:
INSERT INTO user VALUES (2, 'b', 1000);-- 关闭 AUTOCOMMIT 后,数据的变化是在一张虚拟的临时数据表中展示,
-- 发生变化的数据并没有真正插入到数据表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+-- 数据表中的真实数据其实还是:
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+-- 由于数据还没有真正提交,可以使用回滚
ROLLBACK;-- 再次查询
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+
可以使用COMMIT
将虚拟的数据真正提交到数据库中:
INSERT INTO user VALUES (2, 'b', 1000);
-- 手动提交数据(持久性),
-- 将数据真正提交到数据库中,执行后不能再回滚提交过的数据。
COMMIT;-- 提交后测试回滚
ROLLBACK;-- 再次查询(回滚无效了)
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+
总结
1、查看自动提交状态:
select @@AUTOCOMMIT;
2、设置自动提交状态:set AUTOCOMMIT = 0;
3、手动提交: 在 @@AUTOCOMMIT = 0 时,可以使用commit
命令提交事务
4、事务回滚: 在 @@AUTOCOMMIT = 0 时,可以使用rollback
命令回滚事务
事务给我们提供了一个可以反悔的机会,假设在转账时发生了意外,就可以使用 ROLLBACK
回滚到最后一次提交的状态。假设数据没有发生意外,这时可以手动将数据COMMIT
到数据表中。
手动开启事务
可以使用BEGIN
或者 START TRANSACTION
手动开启一个事务。
-- 使用 BEGIN 或者 START TRANSACTION 手动开启一个事务
-- START TRANSACTION;
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';-- 由于手动开启的事务没有开启自动提交,
-- 此时发生变化的数据仍然是被保存在一张临时表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+-- 测试回滚
ROLLBACK;SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+
当然事务开启之后,使用commit提交后就不能回滚了。
事务的四大特征
事务的四大特征
A 原子性:事务是最小的单位,不可以分割
C 一致性:事务要求同一事务中的sql语句,必须要保证同时成功或者同时失败
I 隔离性:事务1 和事务2 之间是具有隔离性的
D 持久性:事务一旦结束(commit or rollback),就不可以返回
事务开启方式
1、修改默认提交 set autocommit = 0;
2、begin
3、start transaction
事务手动提交与手动回滚
手动提交:commit
手动回滚:rollback
事务的隔离性
事务的隔离性:
1、read uncommitted; 读未提交的
2、read committed; 读已经提交的
3、repeatable read; 可以重复读
4、serializable; 串行化
脏读现象
在read uncommitted
的隔离级别下:
脏读:一个事务读到了另外有一个事务没有提交的数据
实际开发不允许脏读出现。
如果有两个事务 a、b
a事务对数据进行操作,在操作的过程中,事务并没有被提交,但是b可以看见a操作的结果。b看到转账到了,然后就不管了。后面a进行rollback操作,钱又回去了,完成白嫖。
不可重复读现象
在read committed
的隔离级别下:
小王一开始开启了一个事务,然后提交了几个数据,然后出去抽烟。
在他抽烟的时候,小明在其他电脑上开启了一个事务,然后对那个表提交了一个数据。
小王烟抽完了,然后统计表中数据,发现不对劲。前后不一致了。
幻读现象
在repeatable read;
的隔离级别下:
事务a和事务b同时操作一张表,事务a提交的数据也不能被事务b读到,就可以造成幻读。
可以观察如下步骤:
小明 在杭州 开启一个事务;
小王 在北京 开启一个事务;
小明 对table进行插入数据操作,然后commit;然后查看表,发现操作成功
小王在对table进行插入之前也查看表,然而并没有小明插入的数据,于是乎他插入了同样的一条数据,数据库报错。
小王很是疑惑,这就是幻读现象。
串行化
在serializable
的隔离级别下:
当user表被事务a操作的时候,事务b里面的写操作是不可以进行的,会进入排队状态(串行化)。
“读-读”在串行化隔离级别允许并发。
直到事务a结束之后,事务b的写入操作才会执行。
串行化的问题是性能特差。
一般来说,隔离级别越高,性能越差。
MySQL默认隔离级别是:repeatable read;
一些补充
使用长事务的弊病
从存储空间上来说:
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
长事务还占用锁资源,也可能拖垮整个库。
commit work and chain
的语法是做什么用的?
提交上一个事务,并且再开启一个新的事务。它的功能等效于:commit + begin
。
怎么查询各个表中的长事务?
这个表中记录了所有正在运行的事务信息,里面有事务的开始时间。可以从这里看出哪些事务运行的时间比较长。
select * from information_schema.innodb_trx;
如何避免长事务的出现?
从数据库方面:
a.设置autocommit=1,不要设置为0。 b.写脚本监控information_schemal.innodb_trx表中数据内容,发现长事务,kill掉它。 c.配置SQL语句所能执行的最大运行时间,如果查过最大运行时间后,中断这个事务 从**SQL语句**方面: 设置回滚表空单独存放,便于回收表空间
从业务代码方面:
1、检查业务逻辑代码,能拆分为小事务的不要用大事务。
2、检查代码,把没有必要的select语句被事务包裹的情况去掉
事务隔离是怎么通过read-view(读视图)实现的?
每一行数有多个版本,当我们要去读取数据的时候,要判断这个数据的版本号,对当前事务而言,是否可见,如果不可见,则要根据回滚日志计算得到上一个版本。如果上一个版本也不符合要求,则要找到再上一个版本,
直到找到对应正确的数据版本。
参考
一天学会MySQL
https://time.geekbang.org/column/article/68963
索引
回表
回到主键索引树搜索的过程,我们称为回表。
覆盖索引
覆盖索引就是在这次的查询中,所要的数据已经在这棵索引树的叶子结点上了。
select ID from T where k between 3 and 5
ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表.
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
覆盖索引的第二个使用:在联合索引上使用,也可以避免回表。
如果现在有一个高频请求,要根据市民的身份证号查询他的姓名。我们可以建立一个(身份证号、姓名)的联合索引。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。
最左前缀原则
联合索引先根据第一个字段排序,如果第一个字段有相同的,就按照第二个字段排序。
只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
联合索引的时候,如何安排索引内的字段顺序?
第一原则:
如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
如果我们有个频繁的要求:根据姓名找到该人身份证,那么应该建立联合索引:(name,ID)
反之,如果我们有个频繁的要求:根据该人身份证找到该人姓名,那么应该建立联合索引:(ID,name)
索引下推
索引覆盖是你要查的信息在二级索引中已经有了,就不需要回表。索引下推是你的过滤条件有一部分符合了最左前缀,那么会用上索引,如果此时不符合最左前缀的部分刚好有联合索引中的字段,那么在利用最左前缀进行索引查询的同时,会根据这些字段多做一步过滤,减少索引查询出来的条数,这样就减少了回表次数。
如:
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
重建索引问题
假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引。
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;
如果你要重建索引 k:
alter table T drop index k;
alter table T add index(k);
如果你要重建主键索引:
alter table T drop primary key;
alter table T add primary key(id);
上面这两个重建索引的作法对此有什么理解?
为什么要重建索引?
索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。
理解
不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。
推荐使用:
alter table T engine=InnoDB
联合主键索引和 InnoDB 索引组织表问题
有这么一个表:
CREATE TABLE `geek` (`a` int(11) NOT NULL,`b` int(11) NOT NULL,`c` int(11) NOT NULL,`d` int(11) NOT NULL,PRIMARY KEY (`a`,`b`),KEY `c` (`c`),KEY `ca` (`c`,`a`),KEY `cb` (`c`,`b`)
) ENGINE=InnoDB;
既然主键包含了 a、b 这两个字段,那意味着单独在字段 c 上创建一个索引,就已经包含了三个字段了呀,为什么要创建“ca”“cb”这两个索引?同事告诉他,是因为他们的业务里面有这样的两种语句:
select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;
为了这两个查询模式,这两个索引是否都是必须的?为什么呢?
表记录:
主键 a,b 的聚簇索引组织顺序相当于 order by a,b ,也就是先按 a 排序,再按 b 排序,c 无序。
a | b | c | d |
---|---|---|---|
1 | 2 | 3 | d |
1 | 3 | 2 | d |
1 | 4 | 3 | d |
2 | 1 | 3 | d |
2 | 2 | 2 | d |
2 | 3 | 4 | d |
索引 ca 的组织是先按 c 排序,再按 a 排序,同时记录主键:
这个跟索引 c 的数据是一模一样的。
c | a | b |
---|---|---|
2 | 1 | 3 |
2 | 2 | 2 |
3 | 1 | 2 |
3 | 1 | 4 |
3 | 2 | 1 |
4 | 2 | 3 |
索引 cb 的组织是先按 c 排序,再按 b 排序,同时记录主键:
c | b | a |
---|---|---|
2 | 2 | 2 |
2 | 3 | 1 |
3 | 1 | 2 |
3 | 2 | 1 |
3 | 4 | 1 |
4 | 3 | 2 |
所以,结论是 ca 可以去掉,cb 需要保留。
in与between的区别
--1.
select * from T where k in(1,2,3,4,5)
--2.
select * from T where k between 1 and 5
1.in 内部的数字是未知的,不知道是否有序,是否连续等,所以你只能一个一个去看。
2.一个已知的升序、范围查询,只需定位第一个值,后面遍历就行了。
## 全局锁是什么?全局锁有什么用?全局锁怎么用?
全局锁主要用在逻辑备份过程中,对于InnoDB 引擎的库,使用–single-transaction
;
MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL),让整个库处于只读状态。
表锁是什么?表锁有什么用?表锁怎么用?
表锁一般是在数据库引擎不支持行锁的时候才会被用到的.
表锁的语法是 lock tables … read/write;
加上读锁,不会限制别的线程读,但会限制别的线程写。加上写锁,会限制别的线程读写。
行锁是什么?行锁有什么用?行锁怎么用?
行锁就是针对数据表中行记录的锁。
比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
事务 B 的 update 语句会被阻塞,直到事务 A 执行 commit 之后,事务 B 才能继续执行。
一定知道了事务 A 持有的两个记录的行锁,都是在 commit 的时候才释放的。若行锁不是在 commit 之后被释放,而是在该语句执行完就被释放,则不会出现事务 B 被锁住。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
调整语句顺序并不能完全避免死锁。
死锁与死锁检测
并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
为了避免这个操作,常用死锁检测。
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参innodb_deadlock_detect
设置为 on
,表示开启这个逻辑。
死锁检测算法复杂度很高 N个进程,遍历N遍,M个资源,每个资源操作一次。则复杂度 O(M*N^2)。
假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。最终检测的结果可能是没有死锁,但是这期间要消耗大量的 CPU 资源。
何时会死锁检测
每条事务执行前都会进行检测吗?
并不是,如果他要加锁访问的行上有锁,他才要检测。
一致性读不会加锁,就不需要做死锁检测;
并不是每次死锁检测都都要扫所有事务。比如某个时刻,事务等待状态是这样的:
B在等A,
D在等C,
现在来了一个E,发现E需要等D,那么E就判断跟D、C是否会形成死锁,这个检测不用管B和A。
死锁检测其实就是算法,环的检测,不必每次遍历一遍当前事务,只需要判断事务链表中,每加入一个新事物后是否有环的生成,有就形成死锁。这个方法和leetcode的链表中的环检测应该是一个道理。
如何避免高量级的死锁检测
为了避免这个问题,一般来说有两种方法:
1、果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。一旦发生死锁现象,则会出现超时(50s)
2、控制并发度:
1、对于相同行的更新,在进入引擎之前排队。
2、减少行更新锁冲突的方法:将单行拆成逻辑上的多行
练习
如果你要删除一个表里面的前 10000 行数据,有以下三种方法可以做到:
第一种,直接执行 delete from T limit 10000;
第二种,在一个连接中循环执行 20 次 delete from T limit 500;
第三种,在 20 个连接中同时执行 delete from T limit 500。
方案一,事务相对较长,则占用锁的时间较长,会导致其他客户端等待资源时间较长。
方案二,串行化执行,将相对长的事务分成多次相对短的事务,则每次事务占用锁的时间相对较短,其他客户端在等待相应资源的时间也较短。这样的操作,同时也意味着将资源分片使用(每次执行使用不同片段的资源),可以提高并发性。
方案三,人为自己制造锁竞争,加剧并发量。
主备一致性
备库为什么要设置为只读模式?
有这样几点考虑:
1、有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作
2、防止切换逻辑有bug,比如切换过程中出现双写( 同时写两个库(A、B )),造成主备不一致
3、可以用 readonly
状态,来判断节点的角色
备库设置为只读,如何与主库保持同步更新?
readonly
的设置对于super权限用户是无效的。用于同步的线程,就拥有super权限。
A到B的内部流程如何?
主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写binlog;
备库B与主库A之间维持一个长连接。主库内部有一个线程,专门用于服务备库B这个长连接。
一个事务日志同步的完整过程:
1、备库B通过change master
命令,设置主库A的IP、端口、用户名、密码,以及请求binlog的起始位置(文件名+日志偏移量)
2、备库B执行start slave
命令,备库启动两个线程io_thread
、sql_thread
。io_thread
负责与主库建立连接
3、主库A校验完用户名、密码后,按照备库B传过来的起始位置,读取本地的binlog然后发给备库B
4、备库B拿到binlog后,写到本地文件,称为中转日志(relay log)
5、sql_thread
读取中转日志relay log ,解析日志里的命令,并执行
binlog内容是什么?
在解释内容之前,需要知道binlog的格式。
binlog有三种格式:statement
、row
、mixed
statement
binlog_format=statement
时,binlog 里面记录的就是 SQL 语句的原文
statement格式的binlog的缺陷有个缺陷:
主备使用的索引可能是不一致的,最终导致执行删除时删除的数据不一致。
**row **
row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event: Table_map
和Delete_rows
.
1、 Table_map
, 用于说明操作的表是test库的表t
2、Delete_rows
, 用于定义删除的行为
当binlog_format = row
,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,肯定不会出现主备删除不同行的问题
mixed
mixed格式用于哪些场景呢?
statement
格式可能会导致主备不一致,所以要使用row
格式
row
格式比较占空间,同时也更要耗费IO资源,影响执行速度
所以采用这种方案,采用mixed
格式,MySQL自己会判断这条SQL语句是否可能引起主备不一致,如果可能,使用row
格式,否则使用statement
格式。
row
格式对于恢复数据有何好处
现在,越来越多场景要求使用row
格式的binlog,可以从delete、insert、update三种sql语句角度看待这个问题。
使用delete语句,row
格式会把被删除的行的整行信息保存。所以删错之后,只需要把binlog记录的delete语句转成insert就能恢复了。
使用insert语句,row
格式会记录所有的字段信息。所以插入错误的时候,只需要把binlog记录的insert语句转成delete语句就能恢复了。
使用update语句,binlog会记录修改前整行的数据和修改后的整行数据。所以如果update误执行,只需要把event前后的两行信息对调,再去数据库执行,就能恢复数据了。
M-M结构的循环复制问题以及解决方案
图1是M-S结构,但是现在常用的是M-M结构,M-M结构区别在于:节点A与节点B总是互为主备关系,所以在切换的时候就不用修改主备关系了。
M-M存在循环复制问题:
在节点A更新一个语句,把生成的binlog发给节点B。
节点B执行完更新语句后也会生成binlog。
如果A同时为B的备库,A会把节点B新生成的binlog拿过去执行。节点A和B之间会不断循环执行这个更新语句。
解决方案:
已知MySQL在binlog中记录了命令第一次执行所在实例的server id。
1、规定两个库的server id 必须不同。若相同,则不能设定为主备关系
2、备库接到binlog,生成与原binlog的server id相同的新的binlog
3、每个库在收到从自己的主库发过来的日志后,先判断server id,如果和自己的相同,表示这个日志是自己生成的,丢弃这个日志。
所以使用M-M结构的日志执行流程如下:
1、从节点A更新的事务,binlog里记录的都是A的server id
2、传到节点B执行一次后,节点B生成的binlog的server id 也是A的server id
3、再传给节点A,A判断这个server id与自己的相同,不处理这个日志
> 如果能够保证业务代码不会写入重复数据,就可以继续往下看。 如果业务不能保证,那么必须创建唯一索引。
关于查询能力
普通索引和唯一索引在查询能力上是没有很大差别的。
如:select id from T where k=5
1、普通索引查找到满足条件的第一个记录(5,500)后需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
2、对于唯一索引,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止搜索。
InnoDB的数据按照数据页来读写,每一个数据页大小默认为16KB.
对于普通索引来说,查找k=5的记录,该记录所在的数据页都在内存里,无非就是多做一次
查找与判断下一条记录的操作。
当然,如果刚好k=5这个记录在数据页的最后一行,那么就得读取下一个数据页,这个会稍微复杂一点。
关于change buffer
需要更新一个数据页时,如果数据页在内存中就直接更新。
如果这个数据页在磁盘中,InnoDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读这个数据页了。
在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中的关于这个页的操作。
change buffer 优点:
将更新操作先记录到change buffer ,减少读磁盘,语句执行速度会提升。
数据读入内存会占用buffer pool,使用change buffer可以避免占用内存,提高内存利用率
change buffer 缺点:
1、唯一索引的更新不能使用change buffer
2、change buffer的主要目的就是将记录变更动作缓存下来,在一个数据页merge之前,change buffer上记录越多,收益越大
如果一个业务的更新模式是写入后马上做查询,这样不会减少IO访问,反而增加了change buffer的维护代价。
关于写能力(基于change buffer)
普通索引在不需要立即读时候可以很好的应用change buffer,所以大部分场合建议使用普通索引。
如果在更新之后,马上伴随这个记录拆线呢,那么建议关闭change buffer。
redo log 主要节省的是随机写磁盘的IO消耗,change buffer 主要节省的则是随机读磁盘的IO消耗。
MySQL索引底层原理理解以及常见问题总结
二叉查找树为索引
二叉树的key为col2,value为索引所在行的磁盘地址。
但如果拿col1来作为key的话,会发现二叉搜索树退化成链表。
红黑树为索引
仍然以col1作为索引key,发现找6只需要查找3次。比二叉查找树更加合适一点
当表中有1百万行数据时,这棵树的高度会越来越大。如果我们查找的元素在叶子节点,查找次数会非常多。
B树作为索引
可以在树的横向上做文章,每个节点原本只存储一行数据的地址,现在可以修改为存储多行数据。因为树的高度越多说明IO操作越多,导致与磁盘的交互越多。
B树:
叶节点具有相同的深度,叶节点的指针为空。
所有的索引元素不重复
节点中数据索引从左到右递增排列
B+树作为索引
B+树
非叶子节点不存储data,只存储索引,这样可以放更多索引
叶子节点包含所有索引字段。
叶子节点用指针连接,提高区间访问性能。
也就是说在叶子节点存储了完整的元素,然后把一些处于中间位置的索引元素提取出来,作为非叶子节点。
MySQL设置默认节点大小为16kb,一个bigint为8byte,一个指针为6byte。所以一个节点最多能存16kb/14b = 1170。
再假设叶子节点一个元素占空间大小为1kb。
如果全部节点存储了满了,h = 3的时候一共能够存储1170 * 1170 * 16 = 21902400;这样可以存两千多万个数据了。
以下面为例:
注意,整个树都放在磁盘中,每次load一个节点进入内存。一般来说,先从根节点开始load。
我们现在要找6。比对根节点的3,6大于3,向右比较,发现6大于5,于是从5右边的指针找到下面一层的节点.
然后把这一层的节点从磁盘里面load到内存中。
我们还可以看到最底层的节点之间会有链表相连。
MyISAM存储引擎索引实现
注意,存储引擎是用来形容数据库中的表的。
MyISAM索引文件和数据文件是分离的。
我们使用查询语句:
select * from ... where Col1 = 49;
首先查找是否是索引字段,如果是就从MYI文件中的B+树里面去定位到这个元素。key存储的是索引元素,data存储的是索引元素所在的那一行的磁盘地址指针。拿到指针后去MYD文件定位。
InnoDB存储引擎索引实现
索引和数据放到了同一个文件中:.ibd文件。
叶节点包含了完整的数据记录,而不只是一个地址指针。
常见问题
聚集索引与非聚集索引
InnoDB就是聚集索引,索引和数据文件合在一起。
MyISAM是非聚集索引,索引和数据文件分离。
非聚集索引要查找两次,一次找到指针地址,一次根据指针地址找具体数据。
聚集索引只需要查找一次,直接找到具体数据,所以效率要更高。
InnoDB基于主键索引和普通索引的查询有什么区别?
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
InnoDB主键索引为何是整型的自增主键
自增主键的使用,关于存储和性能
InnoDB必须要有主键,而且推荐使用的是整型的自增主键。
因为数字好建立索引,方便比较,而且相比较于字符串类型,占用的空间更小。
关于自增:由于底层叶子节点是递增排列的,如果此时主键是递增的,那么新插入的元素就显然在叶子节点的最右边。
如果主键不是递增的,插入一个新的元素可能就会在叶子节点链表中间某处。B+树的结构调整就十分巨大了,可能上层的非叶子节点的索引值要修改。
例如这里我们插入8
树的结构发生了很大变化,直接裂开。
自增主键的插入数据模式,每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
何时使用业务字段作为主键呢?
只有唯一的索引,而且该索引为唯一索引。由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
哈希与B树
哈希查找某个key很快,但是不支持范围查找。
B树用到范围查找就很方便了。叶子节点从左到右是一个递增的趋势。并且叶子节点之间通过指针相连,所以不需要再返回到上层索引中寻找。如果我们要找大于20的元素,那么只要在最底层,20元素的右边进行遍历即可。
如果是小于某个元素的情况,就是从底层叶子节点的左边开始,一直包含到边界即可。
“N叉树”的N值在MySQL中是可以被人工调整的么?
1, 通过改变key值来调整
N叉树中非叶子节点存放的是索引信息,索引包含Key和Point指针。Point指针固定为6个字节,假如Key为10个字节,那么单个索引就是16个字节。如果B+树中页大小为16K,那么一个页就可以存储1024个索引,此时N就等于1024。我们通过改变Key的大小,就可以改变N的值
2, 改变页的大小
页越大,一页存放的索引就越多,N就越大。
数据页调整后,如果数据页太小层数会太深,数据页太大,加载到内存的时间和单个数据页查询时间会提高,需要达到平衡才行。