WANG LH , Research & Development

MySQL原理解析

2021.05.31 22:05

存储引擎

MyISAM

  1. 不支持事务、只支持表锁设计,默认fulltext 全文索引
  2. 索引文件和数据文件分开存储,存储引擎表由MYD和MYI组成,MYD用来存放数据文件,MYI用来存放索引文件
  3. 是MySQL的默认存储引擎
  4. 它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。

INNODB

  1. 支持事务,支持行级锁,支持外键
  2. 采用了聚集的方式,数据和索引一起存放,每张表都是按主键的顺序进行存放,如果没有显式的指定主键,它会为表的每一行生成一个6字节的ROWID作为主键

B+树

非叶子节点不存储数据,子节点冗余存储父节点索引,叶子节点存储数据,叶子节点存储全部索引并形成一条有序的链表。
每个磁盘块大概能存16kb的数据,所以非叶子节点的磁盘块能存1170个索引地址,叶子节点因为节点要存数据,应该能存16个节点,那么一个高度为三的B+树,就能存储上千万的数据了,查询是只需要三次IO,查询性能极大提升。

如图所示,如果要查找数据项18,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定18在15和56之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P1指针的磁盘地址把磁盘块2由磁盘加载到内存,发生第二次IO,18在15和20之间,锁定磁盘块2的P2指针,通过指针加载磁盘块c到内存,发生第三次IO,同时内存中做二分查找找到18,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上千万的数据,如果上千万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要千万次的IO,显然成本非常非常高。

根据上述原理解释为什么不用其他索引:

  1. 不用hash,hash虽快无法支持范围查询
  2. 不用二叉树,比如自增主键,二叉树会退化成链表,显然不行
  3. 不用红黑树,红黑树虽然能解决二叉树退化链表的问题,但是上千万的数据,红黑树的高度太高,查询性能不好
  4. 不用B树,B树是索引和数据存一起,需要多次磁盘IO,影响性能

根据B+树的原理就可以理解一些建索引的原则了

  1. 为什么不能建太多索引,因为索引的维护成本太高,同时也会占用大量空间,索引经常更新的字段也不适合建索引,维护更新成本比较高。
  2. 为什么联合索引要符合最左原则,假设联合索引a,b,c,索引的顺序是根据a顺序确定,然后才能确定b的顺序,最后才能确定c的顺序。如果如果用b,c去查,没有a的前提,b的顺序是乱的,c也同样是乱,他们只能在符合最左的原则才能确定顺序。
  3. 为什么索引列选择离散值大的列,如果索引的值只有1和2两个值,要查等于1的列,也遍历了半个表的数据,并没有提升什么性能。

一些小技巧
有时会面临更新大表的一些数据,如果有索引,更新会很慢。这个时候不妨按照下面步骤处理。

  1. 删除索引
  2. 更新数据
  3. 重建索引

事务的四大特性ACID

原子性(Atomicity)

事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

一致性(Consistency)

事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

隔离性(Isolation)

同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

持久性(Durability)

事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事务的并发问题

脏读

事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

幻读

系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读

事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除,幻读侧重于一批数据。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

事务的隔离级别

读未提交(最低级别,任何情况都无法保证)

读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

读已提交(可避免脏读的发生)

读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

可重复读(可避免脏读、不可重复读的发生)

MySQL默认存储引擎
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。
可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。

串行化(可避免脏读、不可重复读、幻读的发生)

Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。MySQL的默认隔离级别就是Repeatable read。

MySQL查询优化

悲观锁和乐观锁

悲观锁

悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统的数据访问层中实现了加锁机制,也无法保证外部系统不会修改数据。

乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

  1. 使用版本号

使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

  1. 使用时间戳

乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。