放一张官方架构图:
参考文章:
- 一文带你了解MySQL之InnoDB_Buffer_Pool-阿里云开发者社区这一篇buffer pool讲解的很好
- 【动画演示:MySQL的BufferPool和ChangeBuffer是如何加快数据读写速度的】https://www.bilibili.com/video/BV1Uz4y1K7ab?vd_source=92b159c136b59d13f9d27d523edfb396(多视频讲解,讲的也很好全是干货不拖拉)
- 一文带你了解MySQL之Log Buffer-阿里云开发者社区
- MVCC详解,深入浅出简单易懂-CSDN博客
- MySQL的并发 | 记录每个瞬间
1、事务ACID
ACID指的是:
- 原子性(Atomicity)
- 要么事务中的所有操作都执行成功,要么都不执行。
- 若在执行事务的过程中发生了错误,那么事务会进行回滚,撤销之前做过的操作,就像事务从未发生一样。
- 一致性(Consistency)
- 当事务完成的时候,要求数据库的所有数据是处于一种语义上正确的状态(事务将数据库从一种状态转变为下一种一致的状态,这里的一致状态是指:数据库事务中的所有数据都是事务提交完成后的状态。),侧重点是对数据可见性的约束。,举个例子,例如张三向李四要转一百块,从最终结果来说,张三会-100,李四会+100,这个是原子性。而在中间过程中,可能会出现张三-100而李四还未收到,这个是中间态,它不能对外可见,只有最终状态可以被感知。(中间态是不能被感知到的)(当隔离性不同会发生变化)
- 隔离性(Isolation)
- 每个事务都是隔离开的,互不干扰。通过加锁(当前读)和MVCC(快照读)来实现。
- 持久性(Durability)
- 事务执行完成后,一旦提交,就是永久有效的。不会因为宕机等故障导致数据丢失,以保证数据库的高可靠性(High Reliability)(不是高可用性)。
2、事务的四种隔离级别
InnoDB引擎中,定义了四种事务的隔离级别,级别越高事务的隔离性越好,但是响应的性能就越低。事务的四种隔离级别是由mysql的各种锁和MVCC机制来实现的。
- Read uncommitted(读未提交):脏读
- Read committed(读已提交):不可重复读(大多数)
- repeatable read(可重复读):脏写(默认)
- serializable(串行)
详细解释:
Read uncommitted
假设当前有两个事务
set tx_isolation = 'read-uncommitted'
begin;
update account set balance = balance + 500 where id = 1;
set tx_isolation = 'read-uncommitted'
begin;
select * from account where id = 1;
在这里,因为事务A和B都是读未提交,因此就算事务A未提交,事务B也会读到中间的结果(中间态暴露,发生了脏读)。
假设,事务A发生了错误,需要rollback,但是事务B已经读到了500,就会出现逻辑上的错误了。
Read committed
为什么又叫不可重复读?
假设事务A是读已提交级别的事务,然后在事务内进行了两次读取相同值的操作。读取第一次的时候,是500,接着在第二次读取之前,数据被其他事务修改并且提交,第二次就读取到了1000。这就是为什么不可以重复读,会导致不知道哪个数据是正确的。(如何解决?脏读解决)
Reaptable read
相对于不可重复读,当事务A读了一次数据之后,后面的数据都是相同的,即使中间实际数据库发生了改变,也不会变化(效果是这样)。并且,只要当第一次读之后,后面不管读谁的数据,都是第一次的时候固定的数据。(why?实现原理?)
可能会出现的事故:假设事务A读取数据之后,真实数据发生了改变,然后事务A要对它进行update,就会发生数据不一致。(脏写)(如何解决?)
Serializable
可以解决所有并发的问题,但是性能下降。当一个没有提交,另一个事务会被阻塞,不允许并发。
3、隔离的底层实现原理
Mysql主要有两种锁:
-
读锁(共享锁、S锁):执行select会用,读锁是读操作共享的,阻塞写操作。
select .... lock in share mode
-
写锁(拍它锁,X锁):delete、update、insert会加,排它。
当事务的隔离级别是串行的时候,会加两种锁。
当处在读未提交(不可重复读)时,在读操作时,添加上lock in share mode
读操作就会被写操作堵塞,实现了串行。也就说,串行的实现,在读未提交的基础上,加上一个lock in share mod
4、Buffer Pool
Buffer pool,顾名思义,缓冲池,就是缓冲一些热点数据,使得不必每次操作都要进行磁盘IO。buffer pool是mysql最重要的内存组件,介于外部系统和磁盘之间,设计的完善可以极大的提高数据库的整体性能。
在缓冲池中,不仅存储了索引页和数据页,还有undo页、插入缓存、自适应哈希索引和InnoDB的锁信息。
4.1、chunk
这里快速过一遍Buffer Pool中的chunk对数据的管理。
为了更好的管理缓存页,在初始化的时候,会为每个缓存页构建一个相对的控制块。
为了快速的区分哪个缓存页是空缓存页,引入了Free链表。
free链表存储的是指向空闲缓存页的控制块的指针。这样就能快速定位空缓存页了。
接下来引入脏页:系统在每次对数据库进行一次操作的时候,并不会直接更改位于磁盘上的数据,这样子就效率太低了,实际会先更改存储在内存中Buffer Pool中的缓存页,然后就返回。这时候就会出现,内存中的缓冲页和数据库的实际数据不一致的关系,这里不一致的页就叫做脏页。
脏页需要被系统定时的落盘(调用write+fsync),就需要mysql知道哪些缓存页是脏页,于是有了flush链表。结构是和free链表一样的。一般来说,mysql会每隔1s将脏页写入进磁盘中,或者当访问到了脏页的数据的时候,也会进行一次落盘。
除此之外,能存储在内存中的数据是有限的,内存是宝贵的,需要引入一个算法来提高缓存效率,mysql使用的是因地制宜改良的LRU(Least Recently Use)。
先看一般的LRU,使用一个LRU链表来记录存储在内存中的页,若需要载入一个新页并且空间不够了,就淘汰掉在链表中最久未使用的页。
但是,使用这样子的结构不能解决两个问题:
- 预读失效
- BufferPool污染
4.1.1、预读失效:
先来说说预读:根据局部性原理,靠近当前访问数据的数据,在接下来很有可能会被访问,因此在每次磁盘IO的时候,都会提前也将下一页的数据缓存到内存中。
在Innodb里面,有两种预读机制
- 线性预读:Innodb将物理文件按照extent(每个extent有64个页,每页16KB)进行划分,若在同一extnet中,顺序访问的页数超过了参数
innodb_read_ahead_threshold
%(默认56),就会直接把下一个extent整体异步加载进buffer pool。 - 随机预读:默认关闭的选项,若读取了连续的13个页(与是否顺序读取无关),就默认把当前extend剩余的页加载进来。
但是,假如这些数据一直没用过直到被淘汰了,它不仅将之前可能的热点数据给淘汰掉,还占用了好几轮的位置,不就白白浪费性能了吗,这就是预读失效。
预读失效主要存在的问题是,预读的数据虽然没被访问,但是处在LRU链表中太久了,迟迟不被淘汰,为了解决这个问题,mysql对LRU链表进行了分区,分成了两段,一段叫“young”,一段叫“old”。
在新生代的部分,存储的是实际被访问过的数据,在旧生代的部分,存储的是预读但未被访问的部分。这样就可以保证预读数据不过久停留,让更多的空间给真正的热点数据了。
当有新的预读页进来,就淘汰掉之前的预读页。当预读页被访问,就加入到yound链表的头部。这个占比是可以改的,默认是63:77
4.1.2、Buffer Pool污染
在改进过LRU链表解决掉预读失效问题后,又会有着新的问题:
假如当要执行一次大范围数据查询,或者全盘扫描(例如模糊查询)的时候,就会不得不将所有的数据都放到缓存中,之前的热点数据被挤下去,不仅如此,因为是全盘扫描,所以处在old的数据会被快速扫描一次,就会晋升为young,但事实上这些数据只需要被查看一次,这就导致了又堆积了大量的冷数据在young中,这就是Buffer Pool污染
针对这个问题,mysql引入了对old块的阻塞时间阈值Innodb_old_blocks_time(默认1s),处在old区域的页若第一次被访问的时间间隔<1s>
4.2、buffer pool的chunk结构
在实际生产中,一个bufferpool肯定是不够用的,每次并发操作都要申请独立的锁去操作单一bufferpool,性能会收到比较大的影响。实际上,一个buffer pool(宏观整体)会被拆分成多个小的buffer_pool_instance
,每个instance又被拆分成多个chunk,每个instance有自己独立的flush链表、free链表、LRU链表继续管理。
4.3、Change Buffer
Change Buffer是位于Buffer Pool中的一块空间(默认占用25%),它的作用是:
当进行一次操作的时候,若是变更非唯一索引的记录,这时候就不会先将对应的数据页加载到内存中,而是只将这次更新记录存放在change buffer中(同时会同步到redo log)。若后面,当前要更新的数据被加载到内存了,Change buffer就会对对应的数据进行修正(merge),使得客户端看到的是对的数据,这样就不必每次修改都进行一次磁盘IO,等到后面需要读的时候再修改就好了(类似于线段树的懒惰更新?)。
注意,必须是非唯一索引才进行这部分操作。因为唯一索引要进行唯一性校验,得加载数据到内存。
4.4、Double Write Buffer
我们现在知道了,InnoDB默认的页大小是16kb,而操作系统文件的页大小是4kb。也就是说,在一起刷盘的过程中,一个InnoDB页要刷进4个操作系统文件页,这就可能会存在这样一个场景:刷到一半,宕机了怎么办?(Redo log无法修复页数据损坏的情况)
为此,引入了DWB。DWB一部分存在于Buffer Pool中,一部分存在于磁盘中。当有页数据要刷盘的时候,会遵循一下流程:
- 1:现将buffer pool中的页数据拷贝到DWB中
- 2:DWB中的页数据落盘到磁盘中的Double Write Buffer files中
- 3:DWB内存中的数据页,再刷到数据磁盘存储.ibd文件中
现在再假设,在1或者2中断了,redo log可以帮助我们恢复数据(磁盘中的数据是完整为改动的),3中断了,可以靠磁盘上的DWB文件来恢复,就解决了数据刷一半的问题了。
5、B+树
B+树从二叉树->平衡二叉树BBT->B树BT演进过来,相对于B树的改进,在InnoDB中,B+树叶子节点不在存储数据而是只存储键值,因为Innodb在磁盘中是按页存储的,一页16KB(可以改,innodb_page_size),如果一页能存储尽可能多的键值,就能让整个树显得矮胖。假如根节点(一个页)能存储1000个主键(1000阶),那么3层B+树就能存储1000*1000*1000=10亿个数据,根节点常驻内存,顶多只需要两次磁盘IO就能找到数据。
InnoDB是把数据放在B+树上的,B+树的键值就是主键,以主键作为B+树索引的键值而构建的B+树索引,称之为聚集索引,而对于用户定义其他列作为键值构建的B+树索引称之为非聚集索引。在非聚集索引中,叶子节点存储的数据为(索引列的值-主键值)。因此对于非聚集索引,是要先查找对应的主键,在到聚集索引查找到对应的数据。
知道数据的存储,就知道了InnoDB里面,“索引即数据,数据即索引”的含义。
假如对于一个用户表,要查询age=20的user,如果有1000个相同的age,从非聚集索引B+树就查到了1000个主键,就要在聚簇索引B+树里面查找1000次,代价是比较高的,对应会有一些优化手段:
- 页预读(Read-Ahead):顺序读子页的时候,提前加载下一块子页。
- 缓存命中:热点数据存储在内存,先查内存。
5.1、AHI
另外,Innodb在自己的buffer pool中还建立了自适应哈希索引(Adaptive Hash Index)AHI,像索引的索引,存储热点索引的具体数据的索引。(一般连续查询超过3次的数据就被放入到这里面)
AHI注意事项:
- 只支持等值查询(=、IN、AND等),不支持模糊查询或者范围查询、排序。
- 基于主键的查询,大部分都是Hash Search
- 不是主键的查询,基本都不是Hash Search
- 无序、非持久化
- 当业务运用大量模糊查询,建议关闭,set global innodb_adaptive_hash_index=off
5.2、插入和页分裂
B+树的插入,就是根据主键进行一次搜索,找到对应的位置,调整一下链表结构就好了。
假如插入后,页大小>16KB,会发生页分裂,一般来说,流程是开辟新的一页,然后原页按键排序,半以后的数据被放置在新页,其他的保留在旧的页中。然后从old page和new page选出分割键k_split
,一般是新页的第一个键值,然后上推给父节点记录。
若父节点页因为新添加一个索引键溢出了,就会拆分成两部分,直到找到一个没满的父节点,或最后分裂到 根节点。
如果根也满了,分裂后会生成两个子页,并 新建一个根节点,把这两个页的分割键和指针放进去,树的 高度 +1。
6、一阶段提交:Redo Log
什么是Redo Log?
Redo Log即重做日志,根据mysql的官方解释:重做日志是一种基于磁盘的数据结构,存储着在崩溃恢复期间,需要纠正的不完整事务写入的数据。
Redo Log是物理日志,记录的是“在某个数据页上做了什么改动”,用它来保证事务的ACID特性。
在每次的事务操作中,不可能每次操作都将对应的变更写进RedoLog在返回成功给客户端,这样每次都要进行磁盘IO就效率太低了。所以在内存充,Innodb还会开辟一段内存空间,叫做Log Buffer(日志缓冲),就用来存储一次次的变更日志。
Redo Log是基于WAL策略的,是WAL的一种实现方式,WAL即Write-Ahead-Logging(日志先行)策略。当客户端完成一次事务进行commit的时候,会立刻将对应的变更写入进Redo Log中,当完整的写入Redo Log后完成了二阶段提交的第一阶段(还有写binlog)。
从宏观上来看,每次commit的流程如下:
涉及到的两个系统调用:
write:刷盘 fsync:持久化到磁盘 write(刷盘)指的是MySQL从buffer pool中将内容写到系统的page cache中,并没有持久化到系统磁盘上。这个速度其实是很快的。(内存到内存)buffer pool由innodb管理,page cache由操作系统管理 fsync指的是从系统的cache中将数据持久化到系统磁盘上。这个速度可以认为比较慢,而且也是IOPS升高的真正原因。
从结果来说,只有数据真正的被记录在Redo Log中,才可以保证这次的事务是顺利完成了,就算服务器宕机了重启也能成功恢复数据。但是在实际,不可能每次commit就立刻写入到磁盘中,必须要做一个批量写优化,就是存储在Log Buffer中,在找时间一次性都写进磁盘里面。
所以就会有,什么时候write,什么时候fsync的问题,mysql给出了三种策略,根据innodb_flush_log_at_trix_commit
参数来选择,定义如下:
- 若值为1,开启最佳性能模式:每次commit后,只需将数据写进Log Buffer就可以返回,然后每隔一秒钟执行write和fsync。(若刷盘前,服务器宕机,那么数据就会丢失。)
- 若值为2,开启强一致模式:每次commit,需要等待数据写入磁盘再返回。(安全性保障,效率偏低)
- 若值为3,折中方案,兼顾性能和安全:每次commit需要write成功(写进了操作系统的page cache)才能返回,每隔一秒都会执行一次fsync,若整个系统宕机,最多只丢失1s的数据。
在实际的开发中,更推荐是选择折中方案,一般来说,用户态的进程崩溃的概率是要比操作系统宕机的概率要大的,而相对1方案,只需要多执行一个write,数据都是在内存中转移,性能影响不大。
6.1、Redo Log的结构
mysql有两个设计Redo Log存储的参数:
Innodb_log_file_size
:定义每个redo_log文件的大小Innodb_log_files_in_group
:定义redo_log文件的数量
这里引用一下参考文章的图:
write pos
记录当前写的位置,边写边向后移动,Free空间就是还未被写入的空间,可以继续覆盖;若Write Pos触碰到了Checkpoint,就表示Redo Log满了,需要停下来推进一下Check Point。(老日志会被覆盖,所以不适合长期保存)
最后说下来,Redo log就是保证Innodb崩溃能恢复的“预写物理日志”。
7、二阶段提交:Bin Log
BinLog(二进制日志)是介于客户端和Mysql的Innodb存储引擎之间的server层中,它的结构和redo log相似,同样拥有一块缓存(binlog cache),和处在磁盘上的持久化Binlog Files。
BinLog的作用是记录每一次对数据库执行修改的“事件(events)”,这样做是为了当个目的:
- 支持数据库复制:从节点获取到Bin log,就可以根据Bin Log的内容执行相同顺序的事件,达到复制的目的;
- 恢复到某个时间点的状态
从这里来看,Bin Log貌似和Redo Log也太相似了?那能否保留一个就好了呢?
答案是不行的,Bin Log和Redo Log的共同存在才能保证数据库的crash-safe能力。
从定位来看,Redo Log属于“物理日志”,记录的是数据库在哪些页中做了byte-level的修改,保证“多页修改”的原子性和持久化,但是并不包含具体的处理逻辑。就算服务器崩溃后,能从Redo Log恢复到上一次事务的数据,也无法重放诸如DDL、触发器、非Innodb引擎下的操作或全局元数据修改(权限变更等)。
而Bin Log属于“逻辑日志”,记录的是SQL语句或者行级变更的逻辑事件,例如一次update、一次delete操作,但是不记录底层存储引擎对各个页的具体修改。
假设一笔事务涉及到了多条索引、多个页的修改,binlog写入后若崩溃,重放这些事务逻辑,虽然能重建数据,但是在崩溃现场,原来各页的部分写入可能已经破坏了页的原有结构,这时候就必须由redo log来保证crash后能执行数据的回滚。
总结一下,它们的职责定位如下:
- redo log:是Innodb存储引擎的物理预写日志,具有幂等性,负责崩溃时保证事务的页级一致性。
- bin log:是Mysql服务层的逻辑变更日志,不具有幂等性,负责复制于时间点恢复。
所以职责上,它们负责的是不一样的东西,各司其职才能做到真正的crash-safe。
7.1写入机制
只要执行了一次DML操作,Mysql就会立刻将对应的操作写进redo log buffer和bin cache中,然后根据参数的不同,在执行事务的提交后来决定要不要输入磁盘。
主要设计的参数是sync_binlog
:
- 若为0,表示最优性能模式,每次提交了事务只保证写进了cache,不主动fsync。
- 若为1,表示强一致模式,每次提交了事务都保证要数据落盘。
- 若为N(N>1),为折中模式,每次提交都只写进了cache,当积攒了N个事务后就执行fsync。
当完成了redo log和bin log的提交,这个事务的commit就算成功了。
8、Undo Log
Undo Log是Mysql三大日志的最后一个日志结构,它的职责是负责语句级别的回滚操作(roll back)。每当要执行一条记录的更新的时候(update、delete、insert),会将具体的要更改的旧数据存储在undo log中,以便后面可以进行数据的回滚操作。
拿一段例子来说明,假如有变量A=5,在执行update A = 10的时候:
1.将A=5记录在Undo log
2.将A=10记录在Redo log
3.更新A为10
4.commit();(刷Redo log、bin log、标记提交成功)
5.后台异步刷盘
8.1、MVCC
如何具体的实现Undo Log的功能,就要说到MVCC了(Multi-Version Concurrency Control)多版本并发控制。MVCC是为了在读取数据时不加锁来提高读取效率和并发性的一种手段。
回顾事务的四个特性,其中的隔离性就需要通过MVCC来实现。
隔离有四个等级:RU、RC、RR、S。在读已提交RC和可重复读RR隔离级别下的快照读,都是基于MVCC实现的。
MVCC的实现基于undolog、版本链、readview,引用一张博客看到的图:
对于一条数据,除了我们显示定义的字段外,mysql还会隐式添加一些字段:
- trix_id:每进行一次事务操作,就会增加一次事务ID
- roll_pointer:回滚找到上一次事务版本的数据
什么是readview呢?
当使用select读取一条数据的时候,可能会发现这条数据目前处在多个版本里面,那么如何决定要读取哪一条呢?就需要依赖readview来帮我们维护一些信息。
在readview快照中主要有以下的字段:
字段 | 含义 |
---|---|
m_ids | 表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表 |
min_trx_id | 表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是m_ids 中的最小值 |
max_trx_id | 表示生成 ReadView 时系统中应该分配给下一个事务的 id 值 |
creator_trx_id | 表示生成该 ReadView 的事务的 事务id |
如何使用这些字段?规则如下:
trx_id == creator_trx_id
:可以访问这个版本,说明是当前事务创建的记录。trx_id < min>:可以访问这个版本,说明要读取的事务已经提交。(基本不会进入这里,trx_id小说明事务早就结束了)
trx_id > max_trx_id
:不可以访问这个版本,若当前事务的ID更大,那么说明已经当前事务已经不存在这个快照读版本链中了。min_trx_id <= trx_id <=max_trx_id
:即当前的事务仍在活跃事务的列表,就表示当前事务尚未提交(否则就>max_trix_id了),那么不可见。反之,说明已经提交了事务,可见。
现在,再回头说一下RR隔离级别和RC隔离级别是怎么实现的:
- 对于RR,repeatable read来说,基于的是事务级快照,在每个事务执行它的第一条一致性读(快照读)的时候,就会基于当前系统状态创建一个快照,此后其他事务不会看到当前事务进行的变更。(相当于要动手之前,先拍了个照,只对这个照片继续改动,其他人看不到)。
- 对于RC,read committed来说,基于的是语句级级快照,每要执行一次一致性读的时候,都会重新创建一个新的read view。(不可重复读因为每次读取可能不一样)
9、宏观更新流程
个人理解的整体流程大致如下,屏蔽掉了一些缓存的细节:
SOME QUESTION
在回答任何问题的时候,都要记得性能问题是一个综合性问题,不止有单个因素导致的,从数据分布、建表规范、查询负载、硬件性能等多个方面都会有影响,不要“一刀砍死”。
1、B+树为什么同级节点是双向链表,单向不行吗?
相对于单向,双向就支持降序了。总之需要用到相邻节点的时候,好处是很多的。
还例如:节点重平衡便利:删除时借用与合并操作,能 O(1) 定位左右兄弟;并发与恢复:减少锁争用、简化 crash recovery 中的指针修复;小成本大收益:相对叶页大小,额外指针开销微乎其微,带来显著性能与可维护性提升。
2、为什么Innodb选择了B+,不是红黑树、或者类似redis的跳表?
没有最好的,只有合适的,就从有什么红黑树做不到、redis做不到的地方来看就好了。
mysql提供范围查找,从这个方面红黑树就out了,红黑树范围查找要中序遍历,无法借助页级别的顺序读也没有链表指针,效率就低下了很多。也从这个方面说说跳表,跳表的节点是分散在堆内存中的,无法一次顺序读入页,效率也没有B+高。顺序读的性能相比于随机读是高了非常多的,也没有办法和系统page cache对齐。
增加、删除记录,维护开销大:红黑树要频繁的旋转调整层高、跳表一下要更新多行数据。
3、那么什么地方适合红黑树和跳表呢?
适合纯内存、指针级的场景。Why?
充分运用了指针小、定位精确的优势。在内存中,随机读写延迟在纳秒量级,运用指针又能精确定位到数据,指针跳转带来的开销就很小了。另外在空间上,B+树每次访问一个节点就是访问一个页,对内存来说存储的应该是频繁访问的精确数据,在这方面B+树就不合适了,涉及到页分裂那些也会消耗更多的空间。
4、在存储数据的时候,推荐使用自增主键吗?
我认为要结合场景来分析。在小项目,非分布式的时候,使用自增主键是可以的,自增主键有以下的好处:
- 数据集中分布在连续的内存块中,管理更加容易,插入效率高,写入延迟更低(每次都是在B+树的末端添加数据)
- 可读性好,便于人工排查问题所在
那么相对的,在面对不同的开发环境的时候,我认为会有以下的缺点:
- 分布式环境中,数据会写入多个节点中,每个节点的自增是单独的,当需要合并会引发冲突。每次插入数据,都会占用自增锁和插入锁。
- 另外,对于安全性来讲,使用自增主键可能会暴露业务量,若系统在隐私方面有要求,那么就尽量不要使用自增ID。(预测性高)
代替的方案,使用雪花算法。
结论:对于单机或读写不太极端的场景,自增主键是最高效、最简单的选择。当系统需要水平扩展(分布式)、多活或对ID有安全性要求的时候,应该考虑雪花算法。(无论哪种方案,都要结合查询模式(点查 vs. 范围扫)、并发量、可维护性来选型,避免“一刀切”——这才是一名合格的后端工程师在面试中会给出的回答)
5、适合使用UUID做主键吗?存在哪些问题
我们知道,UUID的全局唯一性做的很好,由32个十六进制字符和4个连续字符组成,它能做到完全随机,这是它带来的优点,同时也是缺点所在。为什么是缺点呢?就要谈到B+树的存储原理了。正因为它生成的ID是完全随机,就可能会导致插入当前数据,会导致大量的数据移动,因为B+树的叶节点存储的数据的主键要求是有序的。
6、描述一下雪花算法的原理,以及有哪些优势和不足,如何解决不足。
雪花算法生成的ID组成:第一位不用,后41位用来存储UNIX时间戳(可以存储2^41毫秒,大概69年),接下来的10位用来标识机器码(5位标识数据中心+5位标识机器ID,可以有1024个生成节点),接下来12位为序列号,存储同一毫秒内,若有多个ID请求,就保证自增。最多可以支持同一个机器同一时刻生成2^12=4096个ID。若也不够,就会自旋等待下一毫秒生成。
优点就体现在它无中心化,各节点无需继续协调独立生成,低延迟、有序、全局唯一、空间紧凑。
缺点:依赖于服务器的时钟,若服务器出现了时钟回调,就可能出现一模一样的ID;以及粒度只能控制在毫秒级别,若同一毫秒同一机器并发生成4096以上的ID,就需要等待下一毫秒导致性能下降。
为了解决这些缺点,可以使用雪花算法的改进版,我的项目使用的是sonyflake进行ID生成。
7、描述一下sonyflake?
sonyflake对雪花算法的位重新分配:
- 第一位保留位0
- 39位表示时间戳,虽然位少了,但是它的时间戳单位位10ms,每10ms更新一次,就能表示大约174年的时间了。
- 8位表示序列号,同一10ms支持256个ID
- 16位表示机器/节点ID,支持65536个生成节点
先说服务器时钟的问题,sonyflake不再依赖系统时间,而是使用了Go的单调时钟
now := time.Since(startTime).Nanoseconds() / sonyflakeTimeUnit
这样就不会时钟回拨了。另外,16bit标识机器也能容纳更多的扩容空间。
虽然每毫秒最多只支持同一机器生成256个ID了,但是鉴于实际开发中,大型项目会采用分布式多节点的形式,256也是够用的。(节点数*256为实际1ms支持的请求量)
8、什么是最左匹配原则?
在B+树中,若是联合索引树(索引是有多个键组成,例如(age,name)),那么它的排序规则是按照第一项进行排序的,例如按照age升序,若age相同,再按照name排序。因此在B+树的叶节点中,可以看到所有的记录都是先按照age升序,再按照name升序,因此直接来看name是无序的,最左匹配就要求在查找联合索引记录的时候,要按照左往右的键顺序查找,若先按照name查,就失去B+树存在的意义了。
9、在隔离级别RR和RC中,都会存在什么问题?如何解决
都会存在Lost update的问题。解决方案是人为乐观锁,为所有的记录添加有个version字段,每次要更新都必须让version自增,这样子当更新的时候,就可以使用CAS检查version是否改变来决定是否更新了。注意,UPDATE/DELETE本身并不是一致性读,都是锁定读(总是读到最新的数据),因此在RR级别也可以使用乐观锁了。
10、Redo Log是记录对数据的更新的,有这功夫我直接去更新磁盘文件不就好了吗?何必多此一举?
记录redo log和直接修改磁盘文件还是不太一样的,首先,假若在更新磁盘数据的时候,服务器就宕机了,这时候真实的数据都只被更新了一半,是不完整的数据,数据库的持久性就丢失掉了并且无法恢复。其次,刷盘redo log是顺序写,性能是直接写真实磁盘数据的随机写的几十倍,这样子即能保证数据的持久性,又能最大保证效率。
11、MySQL 跨表事务是如何实现的?
对于单一存储引擎内的跨表事务,当开始执行事务的时候,都会获取一个全局的事务id:trx_id,并在整个事务生命周期内保持不变。无论操作多少张表,所有对InnoDB表的DML都打上这个相同的事务号。
当事务执行DML/DDL时,Server会先对涉及的表申请一个metadata lock,防止表结构变更;然后对行级进行修改的时候,会申请一个行级锁,保证并发时写操作的隔离性。更改完成后,就是redo log、undo log的流程了。
12、MySQL (a, b, c) 联合索引, WHERE a < ? AND b > ? AND c < ?
可以走哪些索引?
对于这条记录而已,mysql进行索引查询,只会用到左前缀,即a<?
的部分,这是因为二级索引B+树的结构来确定的。(不赘述)myslq会在非聚簇索引B+树中,找到所有满足a<?的数据,然后剩下的两个条件都是交给过滤器来做的,即一一筛选出满足b和c的数据,再进行回表找到对应的元数据。
对于一个联合索引,若where的条件中,只要出现了范围操作(<,>,BETWEEN,LIKE),在该操作后面的所有列都会失去索引推进能力。
若也想对b和c进行索引加速,可以把它们拆分成单列索引,然后使用mysql的Index Merge(交集)来合并;(就是每个二级索引树都查一次,找到主键相同的部分)