• MySQL笔记


    一、MySQL组成

    1. 连接池

    2. SQL接口

    3. SQL解析器

    4. 查询优化器

    5. 执行器

    6. 存储引擎

    二、SQL执行过程

    1. 连接池获得外部请求

    2. 分配给工作线程

    3. 调用SQL接口

    4. SQL解析器解析SQL语句

    5. 查询优化器生成最优执行计划

    6. 执行器根据执行计划调用存储引擎的接口

    7. 从磁盘中加载数据到缓冲池

    三、物理存储结构

    1. 表空间

    2. 数据区

    3. 数据页,默认16kb

    4. 数据行

    四、Buffer Pool缓冲池

    1. 内存中的一块空间,默认128M

    2. 先把数据从磁盘加载到缓冲池中

    3. 一个数据页对应一个缓存页

    4. 直接更新缓冲池的数据,变成脏页

    5. 后台IO线程刷入磁盘中

    6. 数据结构:描述数据块+缓存页

    7. 当Buffer Pool总内存比较大(大于1G),会自动划分出多个buffer pool实例,每个实例内有独立的free、frush、lru链表。而每个实例内又通过多个chunk块动态扩展内存,这多个chunk块共用三大链表。

    8. innodb_buffer_pool_size:缓冲池总大小,一般设置为机器内存的50%~60%。

    9. innodb_buffer_pool_instances:缓冲池实例数,高并发下很影响性能。

    10. innodb_buffer_pool_chunk_size:chunk块大小,单位是M。

    11. buffer pool总大小 = (chunk大小 × buffer pool数量)的2倍数。

    磁盘是随机读取的,效率很低,IOPS,每秒io数。

    五、undo.log

    1. 回滚日志

    2. 更新前保存数据旧值

    3. 用于事务失败回滚

    六、redo.log

    1. 重做日志

    2. 记录更新的内容

    3. 事务提交时必须刷入磁盘中

    4. 因为更新的是缓冲池(内存)中的数据,脏页还没刷入磁盘,此时宕机了可以用redo.log进行重做

    5. 写日志是顺序写,速度和写内存相当

    redo.log是InnoDB特有的日志

    七、binlog

    1. 二进制日志或归档日志

    2. MySQL 通用日志

    3. 主从复制时使用

    4. 事务提交时必须刷入磁盘

    八、三个双向链表

    1. free链表记录空闲缓存页

    2. flush链表记录脏页

    3. LRU链表记录最近最少使用的缓存页,当没有空闲缓存页时,会先将这部分缓存页进行刷盘

    九、两种数据读写机制

    1. 磁盘随机读写(性能低,IOPS,响应延迟等参数,SSD固态硬盘好过机械硬盘)

    2. 磁盘顺序读写(性能高,redolog,binlog等日志,os cache机制)

    十、事务的四种问题

    1. 脏写:回滚了别人更新的数据。

    2. 脏读:读到了别人未提交的数据。

    3. 不可重复读:事务执行期间,读到别人已提交的数据,多次读到的值不同。

    4. 幻读:事务执行期间进行范围查询,这时别人新增或删除了数据,每次查到的数据量都不一样。

    前面三种问题主要针对同一条数据的修改,最后一种幻读问题针对的是表的新增或删除

    十一、事务的四种特性

    1. 原子性:参与事务的所有SQL是最小的分割单元,要么一起执行成功,要么一起执行失败。

    2. 一致性:事务执行前后数据都是正确的,不存在矛盾。

    3. 隔离性:事务之间相互隔离,不干扰。

    4. 持久性:事务提交成功后,数据变化是永久性的。

    十二、事务的四种隔离级别

    1. 读未提交:简称RU级别,可以读到别人未提交的数据,任何一种问题都不能防止。

    2. 读已提交:简称RC级别,只能读到别人已提交的数据,可防止脏读,不能防止不可重复读、幻读,Oracle默认。

    3. 可重复读:简称RR级别,在事务执行期间多次查询,无论别人怎么修改提交,都是查到一样的值,可防止脏写、脏读、不可重复读,不能防止幻读,MySQL默认,且它和SQL标准不同,可以避免幻读(MVCC机制)。

    4. 串行化:事务没有并发执行,所有问题都可以避免。

    十三、并发控制方案

    针对同一条数据存在两种并发情况:

    1. 读&写并发:通过MVCC机制实现RC、RR等隔离等级。

    2. 读&读并发:通过互斥锁实现。

    十四、undolog日志版本链

    每条数据和undolog都包含两个隐藏字段:

    trx_id
    roll_pointer

    trx_id是最近一次更新这条数据的事务id,是一个递增的序列。roll_pointer是回滚指针,指向上一条undolog。

    十五、ReadView

    执行一个事务期间会生成一个或多个ReadView(一次性视图),主要包含四个关键元素:

    1. m_ids:生成当前ReadView时还活跃的没提交的事务id列表。

    2. min_trx_id:当前m_ids列表里最小的那个。

    3. max_trx_id:是指下一个要生成的事务 id。下一个要生成的事务 id 肯定比现在所有事务的 id 都大。

    4. creator_trx_id:每开启一个事务都会生成一个过多个ReadView,而creator_trx_id 就是这个开启的事务的 id。

    事务60开启生成ReadView时,还有一个事务70未提交,那么m_ids就是[60,70],mix_trx_id就是60,max_trx_id就是71,creator_trx_id就是60。

    十六、版本比较规则

    一个事务是否可读当前版本数据,要拿数据行中的trx_id和该事务ReadView进行比较:

    1. 当trx_id = creator_trx_id:说明是自己修改的,可见。

    2. 当trx_id < min_trx_id:说明是生成当前ReadView前别人提交的,可见。

    3. 当trx_id >= max_trx_id:说明是生成当前ReadView后又有人更新了数据,不可见。

    4. 当min_trx_id <= trx_id < max_trx_id且在m_ids列表中:说明是生成当前ReadView时,修改了数据的事务是未提交的活跃事务,不可见。

    5. 当min_trx_id <= trx_id < max_trx_id且不在在m_ids列表中:说明事务在生成ReadView之前已经已经提交了,可见。

    当比较得出的结果是对当前版本的数据不可见时,会顺着undolog版本链往下,拿undolog中的trx_id和当前事务ReadView进行比较,符合以上规则就直接读取undolog中的数据,这也叫快照读。

    十七、MVCC机制(多版本并发控制)

    MVCC机制就是MySQL通过undolog日志版本链+ReadView机制实现RC、RR隔离等级的一套无锁机制,极大地提升了读&写并发性能。

    上面说了一个事务执行期间会生成一个或多个ReadView。

    1. RC级别:事务执行期间每一次select都会生成一个ReadView,这样已提交的事务就不会在m_ids列表里面,但是符合min_trx_id <= trx_id <= max_trx_id,所以可见,具体参照上面「版本比较规则」第5点。

    2. RR级别:事务执行期间无论发生多少次select都只会生成一个ReadView,所以期间有其他事务提交了,也还会在m_ids列表里,会被当前事务认为是未提交的,所以不可见,具体参照上面「版本比较规则」第4点。

    3. 解决幻读:事务执行期间突然别人新增数据进来,但是别人的事务id是大于等于当前事务ReadView中的max_trx_id的,所以不可见,具体参照上面「版本比较规则」第3点。

    十八、锁的思想

    1. 悲观锁:假设总会被别人先更新,因此需要先抢占互斥锁,适合写多读少的场景,MySQL中的独占锁、共享锁都是悲观锁的实现。

    2. 乐观锁:假设总不会被别人更新,因此在更新前不抢占互斥锁,而是在更新时比较是否有人更新,适合写少读多的场景,可以通过添加version等字段来实现。

    十九、MySQL两种锁的概念

    1. 独占锁:简称X锁,对谁都互斥。

    2. 共享锁:简称S锁,只有共享锁对共享锁不互斥,其他对谁都互斥。

    二十、锁的范围

    1. 行锁、页锁、表锁。

    2. InnoDB支持行锁、表锁。

    3. MyISAM只支持表锁。

    二十一、四种事务问题的解决方案

    1. 脏写:属于读&读并发问题,通过锁解决。

    2. 脏读、不可重复读、幻读:属于读&写并发问题,通过MVCC机制解决。

    二十二、加锁大致流程

    1. 事务1把某行数据从磁盘加载到Buffer Pool缓冲页。

    2. 检查到未被别人加锁则直接加锁(生成锁数据结构,等待状态=false,并和数据关联起来,进行更新)。

    3. 事务2检查到数据已被别人加锁,此时也会对数据加锁,但会排队等待(生成锁数据结构,等待状态=true,并和数据关联起来,线程阻塞等待)。

    4. 事务1更新完后提交,释放它自己的锁,并找到和这条数据关联的事务2的锁,修改它的等待状态=false,并唤醒事务2继续执行。

    5. 事务2执行直至提交后释放锁。

    二十三、查询显式加锁

    1. 加共享锁:select * from table_name where id = xxx lock in share mode。

    2. 加独占锁:select * from table_name where id = xxx for update。

    查询显式加锁即表示接下来可能进行更新,其他事务就不能更新。如果加的是共享锁,则跟其他事务查询加的共享锁是不互斥的。

    二十四:隐式加锁

    正常我们在进行update table_name set column_name = xxx where id = xxx的时候,会隐式地加上独占锁,这就是行锁,独占锁都是互斥的,所以不可能发生「脏写」问题,一个事务提交了才会释放自己的独占锁,唤醒下一个事务执行。

    二十五、开发习惯

    一般不建议通过显式查询加锁,在数据库层面实现负载的业务锁机制,而是通过redis/zookeeper等分布式锁实现。

    二十六、磁盘数据页存储结构

    1. 数据页与数据页之间组成双向链表。

    2. 数据页内数据行之间组成单向链表。

    3. 每个数据页都有一个页目录,记录:主键→槽位,数据就放在槽位里。

    二十七、页分裂

    为了方便二分法查找,数据页内的数据行要按照主键id有小到大排列,且后一个数据页里的主键id值都要比前一个数据页的大,若主键id是自增的就不用挪动,若是uuid等类型的随机字符串id,则会进行比较id大小,进行数据行挪动,这就是页分裂。

    二十八、全表扫描

    1. 假如查找条件:where name like '%张三',此时不走索引。

    2. 从第一个数据页开始加载到内存,查询出所有数据行一条条进行比对,依次循环,直至扫描完所有数据页。

    二十九、索引数据结构

    1. 索引也是以数据页的形式存储在磁盘中,叫做「索引页」。

    2. 以主键索引为例,索引页保存内容是最小主键值和页号对应关系,如:

    最小id=1 → 页号=2
    最小id=4 → 页号=8

    1. 等到索引数据越来越多时,会再往上分裂出一个父级索引页,并指向下面的两个子页,如:

    最小id=1 → 索引页号=20
    最小id=58 → 索引页号=28

    1. 依次类推,形成一个B+树,同一层级的索引页组成「双向链表」,和数据页的双向链表是一样的。

    2. 根据id进行查找的时候,从B+树的最顶级索引页开始,使用二分法查找,一层一层往下,根据最小id值,找出属于哪个数据页。

    3. 最后在数据页里面的页目录里面,再次使用二分法查找出属于哪个槽位,然后把数据读取出来。

    一个索引就对应一棵独立的B+树,基于主键的聚簇索引的索引页里面仅包含了主键值+页号。而我们自己建立的索引不管是单列索引还是复合索引,它的索引页里面仅包含索引字段的值+主键值。

    三十、聚簇索引

    1. 以InnoDB存储引擎的主键索引为例。

    2. 最下层的索引页实际上是直接指向了数据页的,可以说是索引页+数据页共同组成了一颗大的B+树,「叶子节点就是数据页本身」,这就是聚簇索引。

    3. 而MyISAM存储引擎,主键索引树的叶子节点并不是数据页,这也是它和InnoDB引擎的最大区别。

    三十一、普通索引(二级索引)

    1. 以name字段索引为例。

    2. 索引页里面存储的是:

    最小的name值 → 索引页号

    (实际上还有主键值,name值相同的情况下,比较主键值,把数据放到主键值大的数据页里面去。)

    1. 索引树的最下面一层存储的是主键id和name值。

    2. 如果是select * from table where name = '张三'的查询步骤是:先在name索引树里面往下找到id,然后再通过主键聚簇索引查出整行数据,这就叫「回表查询」。

    3. 多字段复合索引也是一样的,索引页里面存储的是复合字段的最小值+索引页号。

    聚簇索引和普通索引的区别是:

    1. 聚簇索引最下面一层叶子节点就是数据页,存放的是完整的一行一行的数据。
    2. 普通索引最下面叶子节点也是页,当存放的仅仅是,索引列的值和主键值。如复合索引(x1,x2,x3)的索引树,存的就是x1、x2、x3的值,以及主键id。

    三十二、回表

    若查询的字段多于复合索引树中维护的字段,那得先通过该索引查出主键值,然后拿主键值通过聚簇索引,找到整行数据,最后查出相应的字段,这个过程就叫做「回表」。

    三十三、索引维护过程

    InnoDB存储引擎默认会给我们维护一套主键的索引结构,即「聚簇索引」,而且表里的数据是直接放在索引树的最下层,作为叶子节点。聚簇索引的维护过程:

    1. 表创建好之后就是一个空的数据页,这个数据页作为聚簇索引的一部分,叫做「根页」。

    2. 新增数据时直接插入根页中。

    3. 当根页满了,新增两个新的数据页,把根页中的数据移到两个新的页中,并按照主键id值大小进行排序,第二个数据页的主键值都大于第一个数据页的主键值。

    4. 根页升级为「索引页」,保存的是最小主键值+页号,并分别指向下面两个数据页。

    5. 当数据越来越多,一个索引页放不下所有索引了,就会依次按照同样的方式分裂出更多的数据页和索引页。

    三十四、复合索引原理

    1. 班级、姓名、科目建立复合索引。

    2. 索引树叶子节点上包含三个索引字段的值,以及主键id值,并按顺序排列。

    3. 排列顺序:首先按照班级字段的值来排序,如果一样则按照姓名字段来排序,如果一样再按照科目名称来排序。

    4. 父级索引页存放的是每个子页里面班级+姓名+科目组合最小的值,以及对应的主键值。

    5. 索引页内部记录组成单向链表,同级的索引页之间组成双向链表。

    6. 查询是从最左侧的索引字段开始,用二分法进行查找,即先找出班级,再找出姓名,然后再找出科目。所以where条件一定得包含最左侧的索引字段,不然不走索引。

    三十五、索引设计原则

    1. 一个表索引数量不要太多,一般设计两三个,尽量包含where、order by、group by的字段,避免影响增删改的效率以及过多占用磁盘空间。

    2. 索引应建立在基数比大的字段,避免建立在值比较少,如只有0、1两个值这样的字段,避免无法发挥二分法查找的优势。

    3. 避免在索引字段上使用函数或者计算,那将导致不会使用索引。

    4. 从「二十九、索引数据结构」中知道,索引页里面仅仅包含索引字段的值+主键值。若遇上select * from table order by xxx这种查询,即使order by的字段设计了索引,此时也有可能因为大量的回表到聚簇索引查询,而让MySQL优化直接放弃索引,干脆走全表扫描。针对这种情况要加limit、where限制。

    5. 指定查询字段代替用select *,甚至最好就是需要的字段直接能包含在索引字段里面,这边就避免了二次回表查询。

    6. 尽量针对字段类型比较小的字段设计索引,比如tinyint,这样占用磁盘空间小。如果不得不对varchar (255)长文本字段设计索引的话,可以考虑前缀索引,即只把前面20个字做索引,但这样的话order by、group by都无法使用这个字段的索引了。

    7. 主键尽量用自增序列,尽量不用uuid之类的随机序列,这样起码可以避免主键的聚簇索引频繁地页分裂。

    8. 等值匹配原则:当where条件里面的字段和复合索引的字段完全一样时,一定会用上索引,即使顺序不一样,MySQL也会自动优化成按复合索引的顺序去查询。

    9. 最左侧列匹配原则:从「三十四、复合索引原理」可知,复合索引是从最左侧的字段开始查询,所以需要保证每个where、order by、group by后期面跟的字段,都是复合索引最左侧开始的部分字段,这样索引才会有用。

    10. 最左前缀匹配原则:like 'xxx%'可以走索引,而like '%xxx'不会走索引。

    11. 范围查找匹配原则:如果where条件中第一个就是范围查询,那么只有对复合索引最左侧那个字段的范围查询才会走索引。(首先顺序要遵守第9点最左侧开始原则)

    12. 等值匹配+范围查找匹配原则:如果where条件中第一个是等值匹配,后面是一个或多个范围匹配,那只有等值匹配和第一个范围匹配可以用上索引,后面的范围匹配用不上。(首先顺序要遵守第9点最左侧开始原则)

    三十六、执行计划

    每次提交一个SQL给到MySQL,它里面的查询优化器,都会计算出一个最优的执行计划,包括:

    1. 怎么查各个表

    2. 使用哪些索引

    3. 怎么做排序和分组

    三十七、查看执行计划

    在查询语句在开头加explain命令,如:
    explain select * from table where id = xxx

    三十八、执行计划名词解释

    1. id:一个复杂SQL可能包含多条执行计划,这个就是执行计划的id。

    2. select_type:查询类型。

    3. table:查询的表名。

    4. partitions:表分区。

    5. type:查询方式(关键)。

    6. possible_keys:可供选择的索引(关键)。

    7. key:实际使用的索引名称。

    8. rows:预估会读取多少条数据。

    9. filtered:经过搜索条件过滤之后剩余数据的百分比。

    三十九、执行计划type

    1. const:主键聚簇索引,或者普通唯一索引查询。

    2. ref:普通索引查询。

    3. range:使用普通索引进行范围帅选。

    4. index:查询的字段包含在复合索引列内,只需要遍历复合索引树,不需要回表查询。

    5. all:全表扫描。

    以上方式查询速度由高到低。

    四十、MySQL压测关键性能指标

    1. QPS:每秒处理多少个请求。

    2. TPS:每秒处理多少个事务。

    3. IOPS:每秒执行多少个随机IO读写。

    4. 吞吐量:磁盘存储每秒可以读写多少字节的数据量。

    5. latency:往磁盘里写入一条数据的延迟。

    6. CPU负载:CPU处理能力。

    7. 网络负载:网路带宽每秒中传输数据量。

    8. 内存负载:内存耗费。

    四十一、MySQL调优的思路

    数据库调优简单来说,其实就是控制好表中的数据量,维护好索引,一般不会有太大的问题。

    1. 调整buffer pool的总大小,实例数,chunk块大小等参数,减少因为缓存页满了而大量刷盘的次数。

    2. 建立索引。

    3. 主从复制,读写分离。

    4. 分库分表。

    5. 避免在高峰期执行批量数据处理等长事务。

    6. 强制使用索引语法,避免因查询结果为空而让MySQL进行全表扫描。

    7. 不要使用not in等方式会导致全表扫描。

    8. 定期进行历史表转存,减少主表中的数据量。

    四十二、主从复制&读写分离原理

    1. 主库写,从库读。

    2. 主库增删改的时候把二进制日志binlog写入磁盘。

    3. 从库发送请求到主库拉取binlog。

    4. 从库执行binlog的操作,还原出一样的数据。

    四十三、主动复制的问题

    1. 主库运行了一段时间后,后面中途要搭从库,要怎么做?——在凌晨的时候,让业务系统对外不可用,不让数据写进来,然后在主库上手动备份数据,那到从库上还原,之后再开始主从复制。

    2. 异步复制的时候,从库还没拉取到binlog日志,主库就挂了,这时候切换到从库了但数据没同步过来怎么办?——采用半同步的方式进行复制,也就是等从库成功拉取到binlog日志,并给与反馈到主库了,主库才提交事务。

    四十四、主从复制架构搭建方式

    1. MySQL传统复制方式。

    2. GTID复制方式。

    3. Mycat等中间件搭建。

    四十五、单表数据量建议

    不要超过1000万,较好是在500万以内,最佳选择是100万以内,建立好索引,问题不会太大。

    四十六、数据量大小估算

    一般1亿行数据,大概在1GB到几GB,几千万用户,顶多就几GB大小。

    四十七、分库数量考量

    1. 数据量有多大,参考「四十六」。

    2. 看并发压力有多高,去用户中心,虽然数据量可能有几千万,但并发压力并不高,两台服务器足够了。

    四十八、分表数量考量

    按数据行数参考要分到每个表的行数有多少,参考「四十五」。

    四十九、分库分表拆分方式

    1. 垂直拆分:不同业务的表分到不同的数据库,分担读写压力。

    2. 水平拆分,一张表中的大量数据分到不同库不同表。

    五十、分库分表技术

    1. Mycat(需要额外维护服务)
    2. Sharding-JDBC(直接内嵌到应用服务中)

    五十一、入库策略

    将主键id对表数量进行取模运算,然后根据余数路由到不同的库表。

    五十二、订单表分库分表策略

    1. 根据订单id进行拆分,能均匀分散落到各个表中。(若根据用户id拆分,活跃用户的订单多,僵尸用户订单少,会导致分散不均匀)

    2. 针对用户端,要查询当前用户的订单列表,可以再做一个(userId,orderId)索引映射表,然后再对这个表按用户id进行拆分,这样就会有两次查询,先根据用户id查出他的订单id列表,然后用订单id去查询补全剩下的数据。

    3. 针对运营端,要根据多个条件进行复杂检索,可以做binlog监听,把要搜索的字段同步到ES,由ES进行复杂条件检索出一批订单id后,在根据订单id查询补全剩下的数据。

  • 相关阅读:
    SAP VA02R批量修改销售订单拒绝原因的BAPI:BAPI_SALESORDER_CHANGE
    SSM酒店管理系统的设计与实现毕业设计源码260839
    如何自己开传奇单机架设超详细图文教程
    网络安全实战:记一次比较完整的靶机渗透
    关于Netty的一些问题
    在 .NET 7上使用 WASM 和 WASI
    【月报】Aavegotchi 7 月更新请查收!
    Java应用打包成Docker镜像
    2022年0701-Com.Java.Basis第五课《针对流程控制的语句练习题》
    【Eureka详细讲解】
  • 原文地址:https://blog.csdn.net/weixin_44360895/article/details/126819419