MySQL
MySQL简言
SQL执行顺序
1 |
|
储存引擎
MySQl5.5后默认使用InnoDB引擎,与此前的MyISAM相比,InnoDB支持事务,行锁,间隙锁,MVCC,外键;索引也采用聚簇索引
BufferPool
为提升读写性能,InnoDB不会直接读写磁盘而是引入BufferPool。
BufferPool基本思路
- 减少磁盘IO次数:读先读缓存,写先写redo log再批量刷脏
- IO合并为顺序IO:多次写操作合并后再顺序执行,将随机IO变为顺序IO,这能减少磁盘寻址的时间
CheckPoint
checkpoint是在redo log中指示脏数据(留在bufferpool未刷入盘的数据)位置的指针,根据种类,checkpoint会按照指定策略刷脏。
改进型LRU
实际上,MySQL 确实没有直接使用 LRU 算法,而是在 LRU 算法上进行了优化。这是因为全表扫描和预读机制中,MySQL的一次读取都可能带着很多其他数据页,这就可能把不久前刚读出来的数据页挤占出去。
改进型LRU则相当于冷热分离,其相比于LRU,新元素不从头部插入,而是从中间位插入。该中间位将LRU队列分为了old区和young区。新数据如果在1s(由innodb_old_blocks_time
控制)内被访问就会晋升到young区,如果需要淘汰数据页也只会从old区淘汰。
日志
bin-log | redo-log | undo-log | |
---|---|---|---|
生成 | MySQL Server | InnoDB | InnoDB |
作用 | 主从复制/数据恢复 | 崩溃恢复 | MVCC/事务回滚 |
类型 | 逻辑日志 | 物理日志 | 逻辑日志 |
主从复制
主节点的数据变更先记录在bin-log上,从节点的IO进程会主动向主节点的Log-Dump线程拉取bin-log的更新内容,拉取后从节点记录到自己的relay log中。从节点还有SQL进程,用于读取relay log并执行语句,进而实现主从同步。
主从复制有三种模式:
- 异步模式:master提交事务后立刻返回结果给客户端,不关心slave是否已经接收到binlog并处理
- 半同步模式:master提交事务后需等待至少一个slave接收到bin log并写到relay log才返回成功
- 全同步模式:master提交事务后必须等待所有的slave都接收到bin log并写道relay log后才能返回成功
索引
B+树
InnoDB的索引结构为B+树,相比于B树有以下优势:
- B+树只有叶子节点储存数据:B+树的非叶子节点体积要更小,而树的每一层占用一页,这导致一页能储存更多的节点,B+树整体会更为矮壮。那么高度越低,检索的时间复杂度也越低。
- B+树叶子节点以双向链表连接:叶子节点可互相访问,这样范围查询的效率更高
聚簇索引
聚簇索引是一颗B+树,叶子节点储存索引列的数据。InnoDB会根据每张表的主键构造主键索引,索引的叶子节点存放的是完整的数据行。非主键上构建的索引称为辅助索引,辅助索引的叶子节点就只储存了数据行在索引列上的数据,以及数据行在主键索引的位置。
而非聚簇索引的主键索引和辅助索引结构一致,也都是B+树。叶子节点不储存数据,数据储存在独立的地方,而叶子节点储存一个指向真正数据的地址。
索引覆盖与回表
InnoDB的辅助索引的叶子节点只有索引列的数据,如果查询的SELECT列包含在了索引列中,本次查询就只需要经历一次辅助索引,称之为索引覆盖。否则,SELECT中额外的列需要回到主键索引中继续查询,该过程称为回表。
索引下推
在遍历索引的时候,先查询索引包含的字段,过滤掉不符合条件的记录,减少回表次数。
页
MySQL与磁盘交互的最小单位为页,页默认为16KB,页内储存着不定量的数据行。页内的数据行是根据主键升序排列的,因此如果插入了一个随机主键的数据,为保证有序性,页内数据就会迁移到合适位置,进而可能导致页分裂。其中涉及到的数据重排、新页申请都会影响插入效率。
MySQL查询主要流程:
- 查询数据时,先通过索引定位到数据所在的页,
- 然后将页内整体加载到缓存中,
- 通过二分法在页中检索数据,定位数据到某两个slot之间
- 最后从start slot 指向的记录向后查找,直到查到数据或者end slot的最后一条记录。
最左匹配原则
对于联合索引(a,b,c),MySQL会从左至右依次匹配查询列,直至遇到范围查询。对于WHERE a=1 and b>2 and c=3,ab会利用上索引,c则不会。
索引失效场景
除了不符合最左匹配原则,有些场景会直接导致索引失效。
- WHERE a+1=2 | WHERE a=‘1’ | WHERE a != 1 | WHERE a is not null | WHERE a like ‘%a’
- or查询中,有一个条件的索引失效都会导致整个查询的索引失效
高效利用索引
- 高频查询的,内容简短的、区分度高的字段适合建立索引,更新频率高的字段不适合做索引
- WHERE语句遵循最左匹配原则,SELECT要尽量是索引覆盖,覆盖不了也要保证索引下推
- 避免索引失效场景
事务
ACID
事务的原子性是通过 undo log 来实现的
事务的持久性是通过 redo log 来实现
事务的隔离性是通过 (读写锁+MVCC)来实现,读写锁解决写写冲突,MVCC解决读写冲突
事务的一致性是通过 原子性、持久性和隔离性 来实现,总的来说,一致性是事务完成后的结果要与预期一致
持久性
MySQL的数据修改会先记录在redo log上,redo log再根据刷盘策略持久化到磁盘上。此外,InnoDB存储引擎有一个后台线程,每隔1
秒,就会把redo log buffer中的内容写到文件系统缓存(page cache),然后调用fsync刷盘。
日志文件组
硬盘上存储的redo log
日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo
日志文件大小都是一样的。组件组将多个redo log环形排列,write pos标记当前记录位置,记录新数据则后移;checkpoint标记脏数据的开始位置,刷脏时后移
当write pos追上checkpoint就必须刷脏了。
原子性
当事务对数据进行修改时,undo log会记录修改的反向操作。当事务需要回滚时便可以利用undo log恢复到原状态。
隔离性
并发事务会存在脏读、不可重复读、幻读的问题,MySQL提供的读已提交、可重复读、可串行化隔离级别能够依次解决以上问题。
锁实现
读已提交:读操作不添加锁,写操作要求排它锁X
可重复读:读操作要求共享锁,读完就释放,写操作要求排它锁
可串行化:读操作要求共享锁,事务结束才释放,写操作要求排它锁
间隙锁
排它锁和共享锁并不能完全解决幻读问题,如果读操作是范围查询,就需要间隙锁将整个范围都锁住,共享锁只能锁住一行。
间隙锁有Gap Lock和Next-key Lock,Next-key Lock比起Gap Lock多锁住了记录行。
MVCC实现
MVCC通过版本控制来实现不加锁而达到读写分离的效果。InnoDB为每一行记录都创建了两个隐藏列,分别记录了行的创建版本和行的删除版本。Undo log中记录了数据行的版本变更,这样每一行都会有历史可追溯。
此外Read View用来屏蔽高于Read View版本的数据变更,这样如果要解决脏读,读事务会在每次读操作前更新Read View,保证每次读的都是最新版本的数据。如果要解决不可重复读,读事务只会在事务开始时生成Read View,保证读到的都是同一版本。
分库分表
数据库瓶颈
IO瓶颈
磁盘IO: 如果热点数据过多,数据库缓存被占满,每次查询会产生大量IO进而降低查询速度。此时可采用分库或垂直分表
网络IO:请求数据过多,网络带宽不足。此时可采用分库
CPU瓶颈
SQL:SQL查询包含了耗时操作如join、group by、索引不当等会增加CPU运算压力。此时多为优化表结构、SQL语句或是业务简化
单表数据过大:查询时扫描的行过多。此时可采用水平分表
分库分表策略
水平分库
以字段为依据,按照一定策略(Hash、Range、Tag等)将数据引流到不同库中。分库的库结构一样、库中数据不重复、所有库的数据的并集是全量数据。
水平分库在面对高并发量的场景能很好缓解IO和CPU压力
水平分表
以字段为依据,按照一定策略将数据引流到不同表中。
相比于水平分库,水平分表适用于并发量不高但单表数据过于庞大的情况。
垂直分库
以表为依据,按照业务归属不同,将数据引流至对应的库中。
垂直分库是以服务化的思想来看待,库与库之间的数据不具有相关性,每个库应该服务于单个或多个强相关的服务
垂直分表
以字段为依据,按照字段的活跃程度分离出热表(主表)和冷表(扩展表)。一般来说,冷热表应至少有一列交集,用于关联数据。
垂直分表适用于并发量不高,但表字段很多,且有明显的热点数据和非热点数据区别,单行数据的储存空间较大。
非Sharding Key查询
分片依据(Sharding Key)要求选择查询频率高,长度简短的列。当查询时不带Sharding Key,则需要想办法引入Sharding Key用于寻找表的位置。
映射法
另建查询列和Sharding Key的映射表,查询前先找到Sharding Key再定位到具体表
基因法
查询列经过转化生成一串基因,取出后x位(x取决于分布式数据库个数),将其代替掉分布式ID的后x位,得到的就是Sharding Key.
分库分表问题
- 事务一致性问题,解决方案有:
• 分布式事务 - 跨库join查询,解决方案有:
• 表冗余,全局表
• 字段冗余
• ER分片,有关联关系的表放同一个分片上,能有效避免跨分片join的问题
• 业务层分两次查询然后组装(常用) - 跨节点分页、排序、函数问题,解决方案有:
• 将不同分片的结果集进行汇总和再排序 - 全局主键避重问题,解决方案有:
• 分布式id,比如雪花算法、基于数据库实现的号段模式等。 - 数据迁移、扩容问题,解决方案有:
• 迁移历史数据、容量规划
• Range + Hash取模 分组法
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!