目录
1 InnoDB引擎
1.1 索引
1.2 事务
1.3 事务隔离级别
1.4 MVCC原理
2 回表
3 覆盖索引
4 最左匹配原则
5 主键生成问题
6 Mysql的行锁和表锁
使用索引加快查询速度,其实就是将无序的数据变成有序,在InnoDB引擎中索引的底层数据结构是B+树。
那么为什么不适用红黑树和B树呢?
Mysql的数据时存储在硬盘的,在查询时一般不能一次性把全部数据加载到内存中。
红黑树是二叉查找树的变种,一个Node节点只能存储一个Key和一个Value。
B和B+树与红黑树不一样,它们算是多路搜索树,相较于二叉搜索树而言,多路搜索树的高度会较低。所以很容易发现,在数据不能一次加载至内存的场景下,数据需要被检索出来,选择B或B+树的理由就很充分了(一个Node节点存储信息更多),树的高度更低,而这会影响检索的速度。
B+树相对于B树而言,有两个特性:
在Mysql InnoDB引擎下,每创建一个索引,相当于生成了一颗B+树,如果这个索引是聚集索引,那当前B+树的叶子节点存储着主键和当前行的数据;如果索引是非聚集索引,那当前B+树的叶子结点存储着主键和当前索引散列值。
比如:select * from user where id>=10;
执行时只要定位到id为10的记录,然后在叶子节点之间通过遍历链表(叶子节点组成的链表),就可以找到往后的记录了。
而由于B树是会在非叶子结点也存储数据,要遍历的时候可能得跨层检索,相对麻烦些,所以基于树的层级以及业务使用场景特性,Mysql选择B+树作为索引的底层数据结构。
其实InnoDB引擎是自适应哈希索引的,哈希索引的创建由InnoDB存储引擎自动优化创建无法干预。
事务可以使一组操作全成功或者全失败,事务目的是为了保证数据最终的一致性。
其几大特性主要分为原子性、一致性、隔离性、持久性。
1) read uncommit。
简单来说就是事务B读取了事务A还没提交的数据,也就是脏读。
事实上如果读加锁的话,更新数据时就无法取数据了,极大地影响数据库性能。
所以在Mysql InnoDB引擎层面,有了新的解决方案Multi-version concurrency control。在MVCC下就可以做到读写不阻塞,且避免了类似脏读的问题。
MVCC通过生产数据快照并用这个快照来提供一定级别(语句级或事务级)的一致性读取。回到事务隔离级别下,针对于read commit隔离级别,它生成的就是语句级快照,而针对repeatable read,它生成的就是事务级的快照。
read uncommit解决脏读的思路,就是在读取的时候生成一个版本号,等到其他事务commit了之后才会读取最新已commit的版本号数据。比如事务A读取了记录生成了版本号,事务B修改了记录,事务A再读取的时候,是根据最新的版本号来读取的,如果事务B还没commit,那么事务A还是读取之前版本号的数据。
2) read commit。
read commit解决了脏读但也会有不可重复读的并发问题。
比如A查询数据时,由于B修改了数据库导致A每次都查询到不同的结果。
3) repeatable read。
repeatable read隔离级别是事务级别的快照,每次读取的都是当前事务的版本,即使当前数据被其他事物修改了,也只会读取当前版本的数据。
所以在InnoDB引擎下的repeatable read隔离级别下,在MVCC下已经解决了幻读的问题,因为它读取的是历史版本的数据。而如果是当前读则需要配合间隙锁来解决幻读的问题。
4) serializable。
serializable隔离级别,是最高的隔离级别,相当于不允许事务并发,效率最低但是最安全嘚
主要是通过read view和undo log来实现的。
简单来说就是比对版本来实现读写不阻塞,而版本的数据存在于undo log中,而针对于不同的隔离级别,无非就是read commit隔离级别下,每次获取一个新的read view、repeatable read隔离级别则每次事务只获取一个read view。
其实所谓的回表,就是当我们使用非聚集索引查询数据时,检索的数据可能包含其它列,但走的索引树叶子节点只能查到当前列值以及主键ID,所以需要根据主键ID再去去查一遍数据,得到SQL所需的列。
select orderId, orderName from orderDetail where orderId=123
比如orderId是索引,索引树的叶子结点只有orderId和Id,而我们还想检索出orderName,所以Mysql会用Id再去查出orderName返回给我们,这过程就叫做回表。
所以想要避免回表,需要使用覆盖索引。
实际上就是将想要查询的列刚好在叶子结点上都存在,比如创建了orderId和orderName联合索引,刚好需要查询的也是这两个字段,这些数据都存在索引树的叶子节点上就不需要回表操作了。
简单来说,有个索引(a, b, c, d),查询条件a=1 and b=2 and c>3 and d=4,那么每个节点依次命中a, b, c 而没有d,因为先匹配最左边的,索引只能用于查询key是否相等,遇到范围查询(>, <, like, between)就会退化为线性查找了。
首先主键要保证唯一性和空间尽可能短,而由于索引的特性(有序),如果生成像uuid那种主键,插入的性能会比自增的差,因为uuid在插入时可能需要移动磁盘块,比如块内存的空间在当前时刻已经存储满了但新生成的uuid需要插入已满的块内,就需要移动块的数据。
行锁实际上作用于索引上,当我们SQL命中了索引,那锁住的就是命中条件内的索引节点(行锁),如果没命中索引,那锁的就是整个索引树(表锁)。
行锁又可以简单分为读锁(共享锁和死锁)和写锁(排它锁)。