目录
一、索引算法
这块分为B+树索引,全文索引,哈希索引。
B+树索引并不能找到一个给定键值的具体行,B+树索引能找到的只是被查找数据行所在的页,然后数据库通过把页读入到内存,再在内存中进行查找,最后得到要查找的数据。
1、B+树
B+树是为磁盘或其它直接存储辅助设备设计的一种平衡查找树。在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上。
1.1B+树的插入
叶子节点page未满,索引page未满:
(1)直接插入记录到叶子节点。
叶子节点page满了,索引page未满:
(1)拆分叶子节点page
(2)将中间的节点放入到索引page中
(3)原page数据按大小放入两个叶子节点page,并插入元素
叶子节点page满了,索引page满了:
(1)拆分叶子节点page
(2)拆分索引节点page
(3)将各中间节点放入上层page
旋转操作:
当要插入的元素所要在的页满了,但是其左兄弟页没有满,那么此时并不会进行拆页,而是会将当前满了的页的一些数据移到左兄弟页,然后再将元素插入当前页。
其实这里介绍的分裂是最简单的情况,数据库中会有所不同,一是分裂不一定是从中间分裂的,二是涉及到并发。
1.2B+树的删除
叶子节点page大于填充因子,索引节点page大于填充因子:
- 直接删除节点。
叶子节点page小于填充因子,索引节点page大于填充因子:
- 合并叶子节点和它的兄弟节点,更新索引节点page。
叶子节点page小于填充因子,索引节点page小于填充因子:
- 合并叶子节点和它的兄弟节点,更新索引节点page,合并索引节点和它的兄弟节点。
2、B+树索引
2.1聚集索引
Innodb存储引擎表是索引组织表,即表中数据按主键顺序存放;而MyISAM存储引擎表是堆表,即数据按插入顺序存储。而聚集索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录信息,也将聚集索引的叶子节点称为数据页。
聚集索引的存储并不是物理上连续的,而是逻辑上连续的。这里有两点:一是前面说过的页通过双向链表链接,页按照主键的顺序排序;二是每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。
2.2辅助索引
辅助索引的叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签,用于找到与索引对应的行数据。
2.3创建索引
Mysql5.5之前添加删除索引都需要对表加排他锁,新建临时表,然后把原表中的数据导入到临时表中,接着删除原表。
个人对这种做法是很不解的,之后他们就出来了一种称为FIC(Fast Index Creation)的技术,简单来说就是聚集索引修改和原来一样,辅助索引只加S锁,不加X锁了,并且也不拷贝一份数据了。
2.4Online DDL
FIC让索引创建时的读操作不再阻塞,但是会阻塞DML操作和DDL操作,所以mysql5.6之后开始支持Online DDL,它允许辅助索引创建的时候,INSERT,UPDATE等DML操作都能执行,部分如列的重命名等DDL操作也能执行,原理就是辅助索引创建删除的同时,将INSERT这类操作日志写到一个缓存中,完成索引创建后再将这些操作重做到表上,这个做法的味道感觉就像redis中的AOF重写过程中存在一个AOF重写缓冲区,记录这期间的命令一样。
2.5Cardinality
Cardinality是判断是否作为索引的一个指标,当一个字段的重复率很高如性别,地区等,它是不适合作为索引的,因为这种情况很有可能还是要扫描大部分的页,效率并没有提升,还增加了索引维护的成本。
那么怎么知道一个字段的不重复率是不是很高呢,可以看cardinality,它表示的是字段不重复记录数量的预估值,cardinality/n_rows_in_table就是不重复率了,不重复率高则该字段是适合作为索引的。
Innodb中cardinality的计算是随机采样B+树中的八个叶子节点页,统计每个页不同记录的个数,然后等比例扩大到整个表的页数即为预估的不重复记录数量。这个数会在表中1/16的数据发生过变化或超过20亿条数据发生了变化后重新计算。
2.6MRR
MRR(Multi-Range Read):简单来说就是对于范围查询会先将读到的索引键值对按页的id在缓存中排好序,然后按顺序读磁盘中数据。这样做可以将随机访问转化为较为顺序的数据访问,性能提升极大。
3、哈希索引
根据行的查询情况会自适应地建立哈希键值对,用于等值查询。
4、全文索引
通过数值比较、范围过滤等就可以完成绝大多数我们需要的查询,但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较。全文索引就是为这种场景设计的。
InnoDB存储从1.2版本开始支持全文索引的技术,其采用full inverted index的技术。在InnoDB存储引擎中将(documentid, position)视为一个“ilist”。因此在全文索引的表中,有两个列,一个是word字段,另一个是ilist字段,并且在word字段上设有索引。
二、锁
Innodb中,锁分为latch和lock,latch指的是互斥量读写锁这些,lock指的是对事物的锁,锁的是表,页,行。
1、行级锁
行级锁分为:共享(S)锁和排他(X)锁。
排他锁的算法又有三种:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:前两者加起来,锁定一个范围,并锁定记录本身
这里有以下几点需要理解:
- 所谓锁算法,锁的都是B+树中的一段物理范围,而不是抽象的数值范围。
- record lock锁的是一个记录实例,而不是索引等于某个值的所有记录。对应(1)中的描述,它锁的就是某条具体的记录在B+树中的一段物理数据。
- gap lock锁定一个范围,它锁的并不是索引在一个数值上的范围,而是当前这个数值范围在B+树中的那段物理数据。
理解了上面几点,下面两个机制就很好懂了:
- Next-Key Lock能够防止幻读,比如1(gap 3 gap 3 gap)6这块数据,括号内是被锁住的,这时想要插入3是插不进去的,这就防止了3的数量增加从而防止了幻读,但是同时2,4,5也插不进去了,算是一点牺牲把,如果它们能插进去,也就是锁的1 gap(3 gap 3)gap 6,那么3也是有可能能够插进去的。
- 当索引唯一时,Next-Key Lock降级为Record Lock,因为该索引已经唯一了所以没必要对两边加锁了。
这样子看来,有了Next-Key Lock,RR隔离级别下就不存在幻读了?不然,如果不显示地使用FOR UPDATE语句进行锁定读取,还是会出现幻读,可以参考:https://www.cnblogs.com/13579net/p/11429923.html 和 https://www.jianshu.com/p/cef49aeff36b 。
2、乐观锁
所谓乐观锁,也叫乐观并发控制,本质上就是CAS,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么当前正在提交的事务会进行回滚。
乐观锁一般依靠的是记录数据版本来实现,即通过在表中添加版本号字段来作为是否可以成功提交的关键因素。
3、意向锁
意向锁分为:意向共享(IS)锁和意向排他(IX)锁。意向锁指事务希望在更细粒度上进行加锁,要对某行加排他锁,首先需要对表加意向排他锁,然后对页加意向排他锁,然后对行加排他锁,这样做目的是将锁定的对象分为多个层次,提高效率。
试想一下如果没有意向锁,那此时有一个事务想对表加共享锁,它需要对整个表每行都判断一下有没有被加排他锁,这样效率极低。
兼容性:
要注意的是,这里的兼容性都是表锁级别的,意向锁并不会与行级共享/排他锁互斥。
4、一致性非锁定读
一致性非锁定读是指innodb通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果此时读取的行正在执行delete或update操作,并不会等待行锁的释放,而是会读取行的一个快照数据。
快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。
在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
5、锁问题
- 脏读:当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED。
- 死锁
三、事务
数据库事务是指单个工作逻辑的一系列操作,其要么完全成功执行,要么完全没执行,满足ACID属性。
ACID是以下四点的缩写:
- 原子性(Atomicity):事务的一系列操作要么全部成功,要么失败就全部回滚。
- 一致性(Consistency):事务执行前后是从一个正确的状态转移到另一个正确的状态,也叫内部一致性,与CAP中的一致性不同。
- 隔离性(Isolation):一个事务在对某一数据操作时,其它事务不能对该数据进行操作。
- 持久性(Durability):事务一旦提交,对数据库的修改就是永久性的了,任一时刻即使数据库崩溃,数据也不丢失。
1、原子性
Innodb的原子性是通过undo实现的,undo存放在数据库内部的一个特殊段中,位于共享表空间内。如果用户执行的事务或语句由于某种原因失败了,就可以利用这些undo信息将数据回滚到修改之前的样子。
当回滚时,innodb实际上做的是与先前相反的工作。对于INSERT,回滚时执行一个DELETE;对于UPDATE,执行一个相反的UPDATE;所以并不会让表空间的大小收缩。
当然,undo log的操作也会产生redo log,做持久性的保护。
当事务提交后并不能马上删除undo log,因为可能有其它事务需要通过undo log得到记录之前的版本,如RR隔离级别下的重复读。具体删除undo log的操作是交给purge线程,purge线程做的事情还有真正地更新记录行。对于一个delete操作,innodb只是将那行记录的delete flag设为1,记录并没有删除,还存在于B+树中,甚至没有产生undo log,真正的delete操作是在该行记录已不被任何其它事务引用时才进行的。
2、一致性
innodb的锁机制,redo日志,undo日志共同保证了事务的一致性。锁机制和undo日志在前面已经有讲到了,在持久性里面会着重介绍。
3、隔离性
隔离性是通过锁来实现的,Innodb的锁家族很丰富,第二章已经详细介绍了,这里不再赘述。
4、持久性
事务的持久性是用重做日志保证的,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,事务的commit操作才算完成。
之前我就在想一个问题,这个redo日志到底有什么用呢,我每次对实际更新或者操作的数据做持久化不是也一样就够了吗?实际仔细想想就能知道,这样做主要是因为redo日志是顺序写,效率更高,每次直接对数据做持久化是随机写,效率会很低,所以实际数据的持久化都是待缓存积攒到一定量再一次写进磁盘以提高效率。
5、事务的隔离级别
在说隔离级别之前,先理解两个概念:不可重复读和幻读。Innodb技术内幕中写到,mysql官方文档将不可重复读定义为幻象问题。但是我并没有检索到这条定义,所以这里说一下我自己的理解。不可重复读指的是事务A两次读取同一行记录,因为事务B的UPDATE操作导致其两次读取的数据不一样。幻读指的是事务A两次读取一定范围的记录,由于事务B的INSERT操作导致其两次读取的数据不一样。当然,这两个问题都可以通过前面介绍的显示添加Next-Key Lock解决。
有了这两个概念,从mysql官网整理以下四个事务的隔离级别:
- READ UNCOMMITTED:以非锁定方式读,并可能产生脏读。
- READ COMMITED:同一个事务,每次读都读取最新快照;对于锁定读,只锁定记录,即只使用Record Lock,所以可能发生幻读。
- REPEATABLE READ:非锁定读则读取第一次读取建立的快照,这样仿佛非锁定读可以解决幻读问题,但实际上并没有完全解决,例子参考行级锁小节给出的例子;对于锁定读,每次锁定一个范围,即使用Next-Key Lock,可以解决不可重复读和幻读问题。
- SERIALIZABLE:类似于REPEATABLE READ,如果没有启用autocommit则会对非锁定读隐式转换为SELECT ... FROM FOR SHARE;
6、分布式事务
关于分布式事务事务,在网上看到一个很好的总结:“分布式事务本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。”
InnoDB提供对XA事务的支持,实现两阶段提交。
XA事务由一或多个RM(Resource Managers,一般为数据库),一个TM(Transaction Manager,协调全局事务的各个事务,mysql中为连接mysql的客户端)以及一个AP(Application Program,定义事务边界,指定全局事务中的操作)组成。
在第一阶段,所有节点开始准备(PREPARE),告诉事务管理器它们准备好提交了。第二阶段,事务管理器告诉资源管理器执行ROLLBAKC还是COMMIT。
四、InnoDB的一些特性
1、后台线程
- Master Thread:将缓存池中的数据异步刷新到磁盘。
- IO Thread:四个read thread,四个write thread,一个insert buffer thread,一个log thread。
- Purge Thread:回收已经使用并分配的undo页。
2、LRU
缓存池通过LRU算法管理,但不同的是,新访问的页并不是直接放到首部,而是放到midpoint位置,因为数据库经常会全表扫描。
3、Insert Buffer
对于非聚集索引的插入或者更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓存池中,若在,则直接插入;若不在,则先放入一个Insert Buffer对象中,然后再以一定的频率和情况进行InsertBuffer和辅助索引叶子节点的merge操作,这样做的目的主要是减少离散写,可以将多个写操作合并到一次。
但是Insert Buffer的使用需要同时满足两个条件:
- 索引是辅助索引。
- 索引不是唯一的。
如果索引是唯一的,那每次插入或更新都要去查找索引页来判断记录的唯一性,这样肯定又会有离散读的情况发生,让Insert Buffer失去意义。同样这也是索引必须是辅助索引的原因。
Insert Buffer的数据结构是一棵B+树,全局只有一棵Insert Buffer B+树,负责所有的表的辅助索引。
参考:
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
https://www.cnblogs.com/cxxjohnson/p/9145548.html
《MySQL技术内幕--InnoDB存储引擎》
https://blog.csdn.net/mrzhouxiaofei/article/details/79940958