为什么会造成深分页?

假如有张百万数据的表,你想通过分页的方式来展示这些数据。当用户请求第1w页数据时,假设

pagesize=10,那么最终是9万9千,10。数据库必须先扫描前9万9千条数据,才能返回第1w页的数据,

如果你查询返回的字段不在索引中,需要回表很多次,性能明显很低

怎么优化?

假设:SELECT c1, c2, cn… FROM table WHERE name = “Hollis” LIMIT 1000000,10

1.子查询+jion优化

1
2
3
4
5
6
7
8
9
10
SELECT c1, c2, cn...
FROM table
INNER JOIN (
SELECT id
FROM table
WHERE name = "Hollis"
ORDER BY id
LIMIT 1000000, 10
) AS subquery ON table.id = subquery.id

这样的话就不需要每条记录都回表

2.子查询+ID过滤

1
2
3
4
5
6
7
SELECT c1, c2, cn...
FROM table
WHERE name = "Hollis"
AND id >= (SELECT id FROM table WHERE name = "Hollis" ORDER BY id LIMIT 1000000, 1)
ORDER BY id
LIMIT 10

缺点就是ID一定得是自增的

3.记录上一个ID,即游标字段

4.如果是基于文本的搜索,使用ES

快速根据 key 找到 value 的“键值对集合” ,查询和插入平均时间复杂度都是o(1)

底层数据结构

1.7为数组+链表

1.8为数组+红黑树/链表

为什么要用红黑树,为何不一上来就树化?

链表太长,会影响查询性能,出现树化意味着被DDoS攻击了,即 被人恶意哈希冲突攻击(构造大量相同

的hashkey),树化可以缓解这种情况。

而平衡二叉树在极端情况下也是有这个问题,相当于链表了,红黑树比二叉树好一点,因为红黑树追求

大致平衡,自平衡效率高。

树化应该是一种偶然现象,链表短的情况下没必要,查询性能很高了,而且链表的占用内存要比树

要少

何时会树化,树化阈值为什么是8?

数组长度>=64 且链表长度>=8 就会进行扩容

jdk开发者他们基于大量的实验验证出来的,什么泊松分布,在负载因子为0.75的情况下,链表长度为8

的概率为0.00006,即选择8是让树化的概率最小

红黑树何时退化

- 扩容拆分树时,发现树的元素小于6,也会链表化
- remove树节点时,发现root,root.left,root.rigth,root.left.left为null(移除前检查),也会链表化

索引计算?为什么数组容量要为2的n次幂

idnex=hash&(size-1),相当于hash%size,但前提是size为2的n次幂

这个是为了底层能通过位运算来提高性能,还有一个就是扩容的时候如果index=oldCap&(size-1)==0,

说明还在原位,如果不是newIndex=oldIndex+oldSize。综上所叙,size为2的幂次方的好处挺多的

数组容量可以不为2的n次幂吗

可以的。size为2的n次幂虽然性能比较高,但如果hash全是偶数,运算出来的index就都是偶数,这

就导致hash分布不均

若追求更好的hash分布,可以用质数作为容量,这样还不用二次hash

例如hashTable它的扩容策略就是 oldCap*2+1

hashmap的容量如何设置?

hashCode都有了,为什么还要调用HashMap的hash()方法对hashCode进行二次哈希?为什么用异或?

jdk1.8中是拿着hashcode()的高16位和低16位进行异或

jdk1.7中是拿hashcode()多次移位,异或

用异或是为了扰乱hash,让hash分布更加均匀,只要32位中的一位不同,hash()返回的结果就不同

put方法流程?1.8和1.7的区别

1.HashMap为懒加载,即初始化只会初始化负载因子,而不是初始化容量

2.计算索引(桶下标)

3.判断索引位置是否为null,为null就创建Node节点插入

4.不为null,则判断是链表还是树,创建对应的节点走相应的添加or更新。如果是链表达到了树化的

逻辑,就走树化的逻辑(链表长度>=8且数组长度>=64)

5.添加后,判断是否需要扩容

1.7和1.8的扩容也是有所不同

1.7扩容时链表节点移动为头插入法,而1.8为尾插法

1.7是大于阈值且插入的桶没空位才会扩容(懒扩容),1.8是大于阈值就会扩容(主动扩容)

1.8扩容时计算Node的索引时,会有优化(位置不变+oldCap)。1.7是重新hash

get()方法流程

1.计算hash值

2.定位桶索引

3.通过hash()和equals方法和第一个桶节点比较是否相等,若相等就返回,不相等就进行链表or红黑树的

查询逻辑

加载因子为什么为0.75

在空间和时间查询性能之间能取得较好平衡

大了,链表长度容易长

小了,就经常扩容,影响性能

但其实根据二项分布,0.693是最好的选择,但考虑到size为2的n次幂,故取到了0.75,其实0.695….其

实也可以,只不过从中位数的角度,0.75是最好的

多线程下出现的问题

1.7的时候扩容时为头插入法,就会出现死循环

丢数据

两个线程同时判断一个桶位置可以插入位置,就插入了,然后就造成数据被覆盖了,而不会走解决

hash冲突的逻辑

1.7为什么要采用头插入法

首先HashMap本身就不是为多线程设计的,头插入法在线程安全的情况下完全是合适的,而且实现起来

也很简单

还有就是jdk开发者认为后插入的元素更加热点,放在头节点比较合适

jdk8为什么采用了尾插入法

1.jdk8引入的红黑树,那么可以顺便遍历一下链表的元素,统计个数,判断是否需要树化

2.避免多线程下扩容可能产生的死循环

key能否为null?key对象有什么要求?

HashMap的key可以为null。对象必须实现hashcode()和equals方法,hashcode()是定位,equals是比较

两者是否相等,并且key的内容必须不能被修改

hash冲突解决方法

拉链法:hash冲突时,同一槽位的拉成一条链表,hashmap采用的

开放寻址法:发生hash冲突时,寻找其他位置,这就保证了一个位置只有一个元素

线性探测:hash冲突时,往后找,直到找到第一个为null的位置,ThreadLocalMap采用的

二次探测:跟线性探测类似,只不过往后探测的步长是有规律的,2,4,8,16

二次hash

只有bin log开启的情况下才存在,默认情况下是不存在两阶段提交的

默认情况下的流程

把事务更改的数据记录到redo log中,当事务提交时,进入commit阶段,先把redo log的状态变为

commit。然后根据innodb_flush_log_at_trx_commit有不同的行为

如果宕机了要回滚,就看看redo log状态是否为commit,如果是就数据恢复。如果是prepare,就根据

redo log的完整性,看看是否要回滚或者数据恢复

在开启了redo log和bin log的情况下就需要考虑到两个的一致性了

1.主节点宕机了,redo写了,但bin log没来得及写入,那么主节点重启数据恢复后,数据就会出现主从

不一致

2.主节点宕机,redo没写,但bin log已经写了,那么也会出现主从不一致的情况

两阶段提交就是为了解决redo log和bin log不一致的情况

它其实就是分布式事务协议,保证多个逻辑要么都成功,要么都失败

它把单个事务的提交拆成了两个阶段,一个是 prepare阶段,一个是commit阶段

prepare

    * 将XID(内部事务XA的ID)写入redo log,并将redo log的事务状态置为prepare。然后将redo log持久化到磁盘(InnoDB_flush_log_trx_id==1)

commit

    * <font style="color:rgb(44, 62, 80);">把 XID 写入到 binlog</font>,并将其持久化到磁盘 (sync==1)
    * <font style="color:rgb(44, 62, 80);">接着调用引擎层的接口,把redo log对应的事务状态置为commit,这时候不需要持久化,只需要进行write就可以了即存到os的page  cache。因为只有bin log写入成功,</font>**<font style="color:rgb(44, 62, 80);">那么不管redo log的状态是不是commit</font>**<font style="color:rgb(44, 62, 80);">,都是会认为事务已经是执行成功了的</font>

出现的异常情况

不管是时刻a or b,此时的redo log的状态都为prepare

MySQL重启后,如果发现redo log的状态为prepare,那么就会拿着redo log的XID去bin log里面寻

找,是否存在。如果存在,表示bin log已经写入成功,即事务已经执行成功了,那么就做数据恢复

如果没有,即事务没执行成功,就会做一个事务回滚操作

ps:事务回滚时redo log会怎么变化?事务回滚redo log也会产生相应数据变更记录

所以说两阶段提交是以 bin log写入成功为事务提交成功的标识

两阶段提交有什么缺点吗?

1.磁盘IO高,会刷两次盘,一次是redo,一次是binlog

2.锁竞争激烈,早期MySQL,事务只有获取锁,才能进入prepare状态,一直到commit状态才会释放

虽然加锁完美解决了一致性的问题,但在高并发的情况下性能不太行

这时候组提交就出现了

两者的不同

适用对象

写入方式

日志格式

用途

为什么有了 binlog, 还要有 redo log?

历史遗留问题,早期mysql官方只有binlog日志,且不支持掉电数据恢复。而redo log是InnoDB引擎特有

的,是为了解决buffer pool脏页丢失的问题,只依靠bin log是不能实现crash safe

能不能只用redo,不用binlog

可以的,不考虑主从复制or数据备份的话(即从权限上解决删库跑路的问题),而且bin log默认就是关

闭的

为什么binlogcache是线程私有的,而redo log buffer的就是全局共享的呢?

这是因为一个事务的bin log要求不可以被拆分的,且我们都知道的bin log是用来主从同步的。一个线程

同时只能执行一个事务,基于这个特性,每个线程就得有一个binlogcache。如果不是的话,到时候在从

库执行binlog时,每个事务的命令就会交叉在一起,那这样就会破坏原子性了,这显然是不行的。

而redo log显然就没这个要求

MySQL引入bin log组提交是为了提升两阶段提交过程中,磁盘IO高的问题。它会把多个bin log刷盘合并成一

个刷盘

先写undo log,修改buffer pool的同时,先记录变更数据到redo log中,然后再写bin log。那么这时候事务

已经算提交了,不管脏页有没有被刷到磁盘

用于数据备份和主从复制,默认下是关闭的

bin log有三种格式

statement:记录的是sql原句

row:记录的是哪里变更了xx

row和statement混合

怎么刷盘的?

mysql会先给每个线程分配一块binlog cache,且每个binlog cache都有固定大小binlog_cache_size,如

超过了这个大小,就会先暂时存到磁盘。虽然每个线程都有,但最终都还是写到同一个bin log里

什么时候binlog cache里的内容会写到磁盘?

记录的是事务的更改数据 即某个页的修改,用于保证事务的持久性,就是如果数据库宕机后,可以用于数据

恢复

采用了WAL(写前日志,先写日志再写数据),将随机写变成顺序写

记录格式为物理日志,即记录的是某某地方发生了xx变化

为什么事务提交后不直接把 Buffer pool 的数据同步到磁盘

首先你如果直接用bufferpool,可能你就修改一个页中的某条数据,然后就把整个页的数据刷到磁盘里去

这显然是不合理的,因为数据库内存和磁盘交换的基本单位是页(16kb),所以你每次刷只能刷一整

页。且这个行为是随机写,效率偏低。而写redo log,一行记录可能就占10多byte,内容少,再加上这

哥顺序写,性能要比直接刷盘要快得多

redo log的三种状态

1.在redo log buffer里

2.在os的page cache里

3.在磁盘里

redo的流程

整体流程就是

1.先看看bufferpool里面有没有数据,没有就去磁盘拉取

2.这时候先把修改的内容记录到redo log buffer里

3.在bufferpool里修改页数据

4.事务提交时,将redo log里的内容追加到redo file里面去

**磁盘表数据的最终变更是靠 Buffer Pool 中的脏页刷盘实现的,不是 redo log 直接修改的;redo log 的 **

作用是在崩溃后辅助恢复这些变更

ps:Redis的AOF是写后日志,先执行Redis的命令,再写日志。为什么?

这可能跟AOF记录的内容有关,AOF记录的是执行的Redis命令,先执行可以避免记录一些错误的命

那么为什么redo log要为写前日志了呢

redo log buffer刷盘时机参数:<font style="color:rgb(194, 24, 91);background-color:rgb(255, 244, 244);">innodb_flush_log_at_trx_commit</font> 的三种状态

InnoDB_flush_log_at_trx_commit==1:每次事务提交时都刷盘,会先把pool里的内容刷到操作系统的文件缓存

系统page cache里去,再刷到磁盘里,这个是默认设置。好处就是能满足ACID的D,因为只有你的redo

log落入磁盘才算事务提交成功,但就是效率低

InnoDB_flush_log_at_trx_commit==2:先刷到page cache,再由OS后台线程定期 fsync。操作系统宕机就会

丢失这个定期的间隔的数据,但效率要比上一种高一点

innodb_flush_log_at_trx_commit==0:不刷到page cache,直接由后台线程定期刷到磁盘

怎么选择?从安全性和性能两个方面出发

redo log写满了怎么办?

我们都知道redo log是为了解决buffer pool脏页数据丢失的问题(脏页:内存里有,磁盘没有的数据页);

因为redo log写日志,它是通过循环写的方式,磁盘里有两文件,什么0和1,一开始从0开始写,写完就到1

然后再从头开始写覆盖原先数据,前提是原先数据已经全部落入磁盘。那么写满的意思就是redo log记录的变更都还没持久化到磁盘的数据库表(即write pos追上check point了)。

那么这时候MySQL就会被阻塞,停下来把buffer pool的脏页数据刷到磁盘,并把旧的redo log擦除掉,腾空间出来,mysql才会恢复。(所以说在高并发系统中,适当设置redo log的大小很重要的)

事务还没提交,redo会被持久化到磁盘吗?

是有可能的。

一般来说有三种情况。

1.你采用的刷盘策略为0和2,因为它们的刷盘策略是由操作系统后台线程来控制,而不是说你执行完commit

后才刷盘,那么就存在这种commit前,redo log就被持久化到磁盘

ps:事务怎么提交??? 就是手动命令哦

1
2
3
begin;

commit;//执行到这行,就是提交了

2.采用的刷盘策略为1,也有可能。另一个事务提交时顺便把其他事务在redo log buffer里的redo log也给刷

到磁盘里

3.redo log pool满了,达到我们设置的上限的一半了,就会先把里面的内容刷到page cache,但不会落盘,

还是在磁盘里

保证原子性,用于事务回滚

实现MVCC

记录的是事务修改前的数据

它的持久化是靠redolog