MySQL

回来吧我的MySQL!

  1. 为什么要使用数据库

  • 持久化:把数据保存到可掉电式存储设备中,以供之后使用

    不同类型的数据存储到不同介质中

    图片、视频、代码、二进制等存到文件中,复杂类型数据存储到数据库中

    为什么数据存在数据库里而不是文件里?

    例如上亿条数据如果存在txt、excel文件里,找寻一条需要O(n)遍历,如果在数据库的一张表里,有很多优化策略提高查询速度

    同时数据库存储类型更丰富

  1. 数据库与数据库管理系统

    数据库管理系统DBMS管理多个数据库,一般开发人员会针对每一个应用创建一个数据库

    真实的数据库就是一个文件夹下的n多张表(文件)

    MySQL的身份是DBMS,负责打开与操作这些文件

    navicat是一个客户端的可视化工具,代替命令行执行用户和MySQL的交互。

  2. RDBMS与非RDBMS

    RDBMS:

    • DataBase :以行和列的形式存储数据,形成一张表,多张表构成一个库

      把现实世界的信息抽象成二维数据表示

非RDBMS:

  • 非关系型数据库舍弃了关系型数据库中的结构化存储+结构化查询+数据之间的关系;(不需要经过SQL层的解析)获得更高的性能

  • 类别:

    • 键值型数据库 redis

    • 文档型数据库 MangoDB,通过k-v形式存储XML,JSON格式文档

    • 搜索引擎数据库 ElasticSearch 核心原理是:倒排索引

    • 列式数据库 HBase 行式是Z,列式是N 可用大量降低系统IO,适合分布式文件系统

    • 图数据库 Neo4j

  1. MySQL体系结构

    • 连接层:

      对接客户端的连接服务

      • 完成:

        连接处理

        授权认证

        连接池管理

    • 服务层:

      完成大多数核心服务功能

      • 完成:

        提供SQL接口

        维护缓存

        SQL解析与优化

        内置函数的调用

        完成所有跨存储引擎的操作

    • 引擎层:

      负责数据的存储和提取

      • 完成:

        对上层提供存储API

        维护索引机制

        可靠存储、回滚、查询、一致性等

    • 存储层:

      将数据存储在文件系统之上

      • 完成:

        维护数据文件

        维护日志文件

  2. 存储引擎


MySQL是怎样运行的

  1. MySQL提供接口的策略:

    1. 在linux下成为守护进程(daemon)、在windows下注册为服务(随机自启)

    2. 提供多层包装的启动模式,连环嵌套:mysqld -> mysqld_safe -> mysql.server

      mysqld是二进制启动程序,后面的都是shell脚本,用来在前者基础上增加功能

  2. 关于网络:

    1. IP地址分类:

      公网IP:指在因特网上直接可达的地址,通过公网IP可以访问别人,也可以被人访问

      分类:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      A类:10.0.0.0到127.255.255.255主要分配给大量主机而局域网网络数量较少的大型网络

      B类:128.0.0.0到191.255.255.255一般用于国际性大公司和政府机构

      C类:192.0.0.0到223.255.255.255用于一般小公司校园网研究机构等

      D类:224.0.0.0到239.255.255.255用于特殊用途,又称作广播地址

      E类:240.0.0.0到255.255.255.255暂时保留

      私有地址:

      在上述地址中,预留了一部分不会被公网IP使用的地址,目的是作为私有地址,供家庭、企业、学习等内部组网使用

      这些地址只会出现在私网中,通常根据容纳的主机数选择使用哪一段

      img

      正常家庭用C类

      学习 / 大型企业可能用B / A类,比如手机在大型商场上网时获取的是10开头的内网地址

      私网设备连接公网——NAT地址转换

      思路:端口映射——将内网众多IP+端口的设备挤到外网一个IP+65535个端口

  3. MySQL服务器过程简述:

    1. 关于查询缓存:

      mysql 5.7开始不推荐使用缓存,MySQL 8删除缓存

      why 缓存?

      缓存中维护者MySQL服务器刚刚执行完的结果,维护一个缓存可以避免重复的查询,并且如果缓存是线程共享的,且服务确实存在热点SQL,那么添加缓存直接避免后续的 语法解析、查询优化、存储引擎...

      why not 缓存?

      使用场景有点少,意外情况有点多

      1. 配合MySQL事务。较难实现
      2. SQL中包含系统函数时,无法使用缓存
      3. 为了确保缓存命中,需要对具体的SQL语句处理(而不是直接比较字符串是否出现,这样的话当多个空格,where顺序不同…都会导致无法命中)
    2. 语法解析:

      对客户端发来的文本进行解析,包括:词法解析、语法解析、语义分析… 对应到具体的数据结构上

      本质就是编译此SQL

    3. 查询优化:

      语法解析后虽然知道要查的内容,但SQL写法多种多样,传入的并非最快的

      需要经过如:外连接转内连接、表达式简化、子查询转连接…

      最终生成 执行计划

    4. 进入存储引擎

  4. 设置的设置:

    1. 命令行启动时 作为参数设置

    2. 配置文件设置

      配置文件有默认存在的位置、默认名称、多个文件直接默认优先级…

      配置文件形式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      cat my.cnf
      [client]
      default-character-set=utf8
      [mysql]
      default-character-set=utf8
      [mysqld]
      init_connect='SET collation_connection = utf8_unicode_ci'
      init_connect='SET NAMES utf8'
      character-set-server=utf8
      collation-server=utf8_unicode_ci
      skip-character-set-client-handshake
      skip-name-resolve
  5. 系统变量:

    1
    show variables like 'default';

    系统变量分GLOBAL(全局范围)和SESSION(会话范围)

    系统启动时,将一些GLOBAL的变量设置默认值,新的会话中如果需要更改同样的变量,设置属于自身的SESSION级别的变量

  6. 编码解码:

    MySQL通过网络连接客户端与服务器,网络就意味着编码,最终都是字节数组

    1. 确保客户端与服务器的编码解码字符集相同【一般在客户端的请求中携带了编码方式,由此动态设置SESSION级别的属性】

    2. 服务器端解码的字符集和用于处理的字符集可能不一致:

      character_set_client决定服务器如何解析客户端的字节数组,正确解析后但可能还无法满足需求——有时想要用另一个字符集进行比较

      MySQL很关注字符集之间的比较:比大小、是否相同...SHOW COLLATION

      例如:

      1
      select 'a' == 'A';

      语句被服务区正确解析,但输出是true or false取决于用于比较的字符集

      例如:

      1
      select user where name = "吴松林";

      加入原本user表默认使用A字符集,name列中存着的0101就通过A字符将吴松林编码成0101,而此时整句话来自客户端,如果此时整句话不是由A字符集编码的,就无法得到对应的0101,导致匹配失败。

即:MySQL处理编码解码问题:将整个逻辑分层:下层解决通信中的编码,上层解决业务中的编码(保证语句的字符集和原本表的默认字符集一致,且使用合适的collation),上下层通过字符集转化来切换

另外,服务器按什么字符集返回也需要设置

三个SESSION级别的变量

1
2
3
character_set_client
character_set_connection
character_set_results
  1. MySQL字符集设置的偏好:

    优先和操作系统一致——也就是和本地文件系统、命令行工具、其他IO设备协调一致

    但存在特殊场景:当操作系统字符集太低级【ascii】,而MySQL最低是Latin1,要么就是utf8,则此时必须按照MySQL;或者显示的设置了default-character-set

  2. InnoDB

    1. InnoDB页:

      完成的功能是:实现对磁盘上数据的读写 –> 一次读写的粒度决定一批操作的效率:InnoDB决定一页设置成16KB,将数据划分为若干页。

    2. 行格式

      真实存储空间如何表示抽象行数据的格式

      1. COMPACT行格式

      内容:

      • 记录的额外信息

      • 变长字段长度列表

        为了让真实存储的信息可被区分(且可以快速的随机访问),头部记录了类似信息坐标偏移量的信息;针对varchar这种变长属性,需要明确其具体长度

        varchar(M)中,M最大值为255,表示最多可以存储M个字符

        • 理想情况是:存储的字段都不长,最长只有200个字符,且使用ascii编码,那么表示此字段长度只需要0-255 即1字节
        • 但如果字段比较长,或者使用类似utf-8mb4编码,长度突破255十分常见

        方案:提前计算--能用一字节就别用两字节

        策略:短路计算--能尽早结束就早结束

        当字段的最大字符数M × 编码字符集的最大字节数 N < 255 – 一字节

        当 > 255,计算实际的字段占用字节数L,当L大于127时,使用两字节

        Question:如何在一堆字节中区分一个字节属于单字节字符 or 双字节字符?

        限制:

        当属于单字节字符时,第一个bit是0,双的话第一个bit是1

        • 第一位是0,天生适合ascii,因为本来字符就不多

        • 第一位是1,只是对这种字符集对应方案进行了很弱的限制

          【字符集只需要关注如何将字符和某个数对应,并不要求数连续】

          GBK的解决方案是:根据第一个字节将字符分区,再根据第二个字节决定是分区中的哪一个

      • NULL值列表

        用于记录一条记录中哪些是null值,由于直接将null标记在真实数据中很占空间,于是将这些null提取出来,统一用0/1表示是否为null,通过优化策略,节约空间

      • 记录头信息

        由固定5bytes组成,用于描述记录的一些属性

        例如:

        • deleted_flag:标记该记录是否被删除
        • min_rec_flag:表示是否是B+树中非叶子节点中最小目录项记录
        • n_owned:
        • heap_no:当前记录在堆中的相对位置
        • next_record:逻辑上下一条记录距离当前的距离
        1
        2
        3
        4
        5
        6
        7
        MySQL索引页的管理方式:
        - 物理存储是连续的,但通过头部信息构建逻辑存储
        - 通过链表链接每个记录,并且是按照主键ID顺次链接
        - 头节点和尾节点特殊处理,作为两条特殊的记录
        - 删除一条记录只是逻辑上标识被删,并由链表绕开此记录,但物理上仍然存在
        并且删除后再添加记录时有可能复用此空间 -- 通过一个垃圾链表
        当主键就是原本的主键时甚至会回到原本链表的位置
      • 记录的真实信息

      各个属性的值

      除了真实值外,MySQL还会添加额外的真实数据

      • row_id:

        当整张表没有确认主键、也没用not null 且 UNIQUE的属性被委派为主键,MySQL主动添加一个隐藏列作为主键

      • trx_id:

        事务ID

      • roll_pointer:

        回滚指针

1
2
3
4
5
6
7
8
9
10
真实数据的记录就是:将每个不为null的属性依次排开

- 注意:

当编码字符集使用变长的,在 变长字段长度列表 中需要记录定长字段的真实字节数【感觉就是将这个char退化成varchar,由于此char的不确定性也很高】

当编码字符集使用定长的,几乎没有不确定性


...
  1. InnoDB索引页

    页是InnoDB管理存储空间的基本单位,InnoDB为了不同目的设计了不同类型的页:

    例如:存放表空间头部信息的页,存放Change Buffer信息的页,存放INODE信息的页,存放undo日志信息的页……以及存放记录的页 – 索引页

    一个索引页分为七个部分:

    • File Header:存储页的通用信息;Page Header:存储页的专有信息

      File Header:页号、上一页的地址、下一页的地址、页的类型

      Page Header:槽的数量、free space大小、页中垃圾链表的头节点、当前页在B+树中的层

    • User Records:存储真实记录,大小不固定

    • Free Space:尚未使用的部分

    • Page Directory:本质就是稀疏索引(槽),将几个连续的数据分成一组,通过先确定组(二分查找),再确定数据的方式加快查找速度

      槽 - 好处是查找的成本降低了,基本上从O(N)降低到O(logN)

      但维护槽同样具有成本,增删记录时要保证每个槽的数据量大致相同,就要求更新槽的信息;

  2. InnoDB的B+树索引

    B+树:

    从优化的角度看B+树的发明:

    1. 为了在不同索引页中找到目标记录,需要给各个索引页添加目录,由目录构成一级索引

    2. 这些索引的管理收到数据项crud的影响也不简单,决定复用对数据项的管理方式,即:类似COMPACT行格式,类似槽的页面

    3. 通过索引可以查到数据项所在页面,但索引本身越来越长,一旦图片16KB,最开始的二分查找就不行了 – 再建第二层索引

    索引分为两类:

    1. 聚簇索引

      特点:

      1. 叶子节点存储的是完整的用户记录(包括隐藏列)

      2. 使用记录的主键值大小进行排序

    1. 在叶子节点和内节点中都按照主键排成单向链表

    2. 叶子节点根据主键值排成双线链表

    3. 同一层的内节点中,目录项也是根据主键排序的双向链表

  3. 二级索引

    1. 叶子节点存储的是索引列和主键的匹配信息

    2. 使用相应的索引列进程排序

  4. 联合索引

    当使用C2,C3两列建立联合索引时,目录页中每项记录的是C2,C3,子页的信息,按照先C2,后C3的顺序顺次排列;叶子节点记录的是C2,C3,主键的信息,按照先C2,后C3的顺序顺次排列;

一些结论

  1. InnoDB存储引擎会自动为主键建立聚簇索引(如果没有显式知道主键,并且没有声明不允许为null的UNIQUE键,则会自动添加主键)

  2. 索引的代价:

    1. 空间的代价:

      一个页面是16KB,一颗B+树由多少页组成…

    2. 时间的代价:

      1. 维护的成本:

      每当对一张表crud时,很容易破坏原本数据项的顺序,而B+树对顺序要求十分苛刻,每一层都是有序的双向链表,所以可能导致一次增删改操作引发页面分裂,页面回收…

      1. 执行查询语句前会计算使用各个索引的成本,如果索引太多会有很多无用功

如何确定选择哪个索引

由where限制条件确定是否缩小了扫描区间的范围

原本我对限制条件的认识是:条件越多找起来越麻烦,但很多场景下未必;有些限制条件和索引契合度高,可以把扫描区间限制的很小(尤其在联合索引中);有的限制条件和索引契合度巨低,甚至用or的限制条件可能会破坏好索引带来的简便

根据可能使用的索引,分别判断扫描区间的大小,即可判断该不该用

另外索引也可以用于排序

当有10000条数据,要按照某列排序后返回前十个

没有索引的场景下需要全部加载到内存里排序

但有索引时就很简单

另外索引也可以用于分组

当有10000条数据,要按照某列分组后统计

没有索引的场景下,在扫描过程中不清楚还有没有同组的,需要建临时表来存储统计信息

但有索引就天然分组

走索引的问题

回表可能不如全表扫描

假如按照索引列得到很多不连续的主键值,需要一个一个回标查询全部信息,但是对聚簇索引的访问太过随机,就不如连续的访问(因为页都是存储在磁盘的,需要时才IO进内存,太随机容易有颠簸)

到底是全表还是回表,由查询优化器实现

但是可以通过限制条件限制回表的代价:limit – 别回表太多次

如何更好的使用索引

  1. 只为用于搜索、排序、分组的列创建索引 – 减少使用成本

  2. 考虑索引列中,不重复值的个数:

    当重复值太多,即使是where condition = value的条件也会有很多回表查询,回表太多就不如全表扫描

  3. 索引列的类型尽量小(指能用tinyInt就别用Int)

    因为类型越小,16KB能存下就越多,树的页越少,高度越低

    所以主键也能小就小

  4. 覆盖索引

    为了避免回表的成本,可以在查询列表中只包含索引列和主键

  5. 避免在搜索条件中出现索引列的计算式

    mySQL直接不认识

  6. 新增记录尽量主键自增,而不是中间插入主键

    因为自增的主键即使分页也只需要把新值插入新页,但中间的主键可能会导致分页+半数数据的复制

  1. mysql数据目录

  2. InnoDB表空间

    • 症结1:

      当一张表的数据量太大,常规的B+树会有巨多的叶子节点和非叶子节点,但无法保证这些逻辑上相关的节点在物理上存储的接近

      因为在磁头移动时,越接近的数据带来的成本越低

      所以尝试给一张表更大的空间(区),相关的空间都先从一个区中申请

    • 症结2:

      区的粒度有点大,会产生不少碎片

      所以有碎片区的概念,进而区分出:空闲区、有剩余空间的碎片区、没有剩余空间的碎片区….

      为了管理这些区,出现了经典操作:使用SDES Entry记录每个区的元数据,用各种链表串联各个Entry(很想PCB)

      • Free链表,在需要申请空闲区时访问

      • Free_Frag链表…

  3. 单表访问方法

    几个量级:

    1. const:指通过主键值在聚簇索引中直接定位,或者通过(唯一)二级索引找到一条后进行回表

    2. ref:通过二级索引单点扫描找到挺多条记录,每条都要回表

    3. range:在二级索引中就有较大的扫描区间,区间内每条记录分别回表

    4. index:遍历二级索引

    5. all:全表扫描

    索引合并:

    一般情况下,InnoDB只会为单个索引生成扫描区间,但是某些场景下如果两个索引都能生成区间,并且区间的交集能减少扫描成本,则会有 索引合并

  4. 表连接的原理

    连接的本质:将各个表中的记录,一次进行匹配,匹配后的组合返回客户端;如果没有任何过滤条件,则结果就是笛卡尔积

    分为内连接外连接

    区别在于:外连接会把不符合on自居的记录保留,加入到结果集中

    连接的原理:

    嵌套循环连接算法

    对于内连接(谁是驱动表无所谓)

    1. 选择驱动表,先执行对驱动表的单表查询

    2. 对步骤1中的查询结果集中的每条记录,都分别到驱动表中查找匹配的记录

  5. 基于成本的优化

    MySQL在执行一个查询时,可能有不同的执行方案,如何选择成本最低的?

    成本分为IO成本和CPU成本,规定读取页面的成本为1,检测一条记录是否符合搜索条件的成本为0.2

    计算成本步骤:

    1. 根据搜索条件,找出所有可能使用的索引

    2. 计算全表扫描的代价

    3. 计算使用不同所以执行查询的代价

    4. 对比各种执行方案,找出最小值

  6. InnoDB统计数据如何收集

  7. 基于规则的优化

    MySQL的设计人员对于任何糟糕的SQL输入,都会将其转化成高级的SQL

    重写操作有:

    1. 条件化简:

      表达式的计算

      移除不必要的括号、没用的条件…

      常量替换

    2. 外连接消除

      尝试吧外连接转化成内连接,内连接则可以优化连接顺序降低成本

    3. 子查询优化

    4. in的优化

      • 优先转化为半连接

      • 无法转化,则尝试用物化表方式

  8. explain

    Explain用来查看执行计划

    各个列:

    1. table:Explain的每条记录对应一个单表访问,table标识访问哪张表

    2. id:查询语句中有几个Select,每个Select会被分配一个唯一ID,尤其在子查询,union中(但子查询有时候会转化成连接查询,导致id唯一)

      但并不是查几张表就有几个ID,比如连接查询就是两个Select,一个id

    3. select_type:每个小的select在整个大查询中可能扮演不同的角色

    4. type:单表访问方法

      除了const,ref,range,index,all,还有其他方法

      例如:system,fulltext…

    5. possible_key和key

      可能用到的索引和真实使用的索引

    6. key_len…

    explain format=json select…用来通过JSON查看执行计划

  9. optimizer trace

    用来查看优化器生成执行计划的整个过程,用来获悉为什么不用别的查询方案

    开启后,在某个数据库中生成相应文件

  10. InnoDB的Buffer Pool

    作用:缓存 - 调节磁盘和CPU的矛盾

    Buffer Pool

    • 为了缓存索引页,MySQL向操作系统申请的一篇连续的内存

    • 内部划分为若干个控制块和缓冲页,缓冲页默认和索引页大小一致16KB

    • 每个缓冲页对应一个控制块(所属表空间、页号、缓冲页在buffer pool的地址、链表节点信息…)

    • 链表们

      • 维护Free链表,用来感知哪些缓冲页可用

        Free链表连接BCB(缓冲控制块)

        想要维护一个Free链表,只需要在上层固定一个基节点(头节点)

      • 维护Flush链表,用来感知哪些页是脏页

      • 维护LRU链表

        LRU(Least Recently Used 最近最少使用)

        思想:当Buffer Pool空间不足时,尝试将最近最不常使用的页淘汰

        策略:将每个加载进Buffer Pool的页面挂载到LRU链上,新加载到的挂载到头节点后,通过缓存命中一次的更新到头节点上;如此在链表最后的页面就是最近使用最少的

        问题:

        • 容易被顶(某些不常用的页被大量加载到Buffer Pool,导致原本正常在缓冲区的页面被淘汰)
        1. 在预读场景下更加明显

            预读:mysql出于局部性原理做的优化   

        当对某个区中页面访问多次时,会将整个区的页面全都加载进Buffer Pool,其中又分连续读取56个和随机读取13个

        预读加载页进Buffer Pool时异步的

        1. 在全表扫描场景下也很明显

            假如是一个很大的表,不得不全表扫描,这就相当于将整个BufferPool换一次血,扫描完后还要重新加载上次没搞完的任务

        解决方案:冷热分离 / 老年代|新生代

        MySQL默认老年代占37%

        Java老年代与新生代的比例默认2:1,而MySQL恰好反过来,主要原因是:

        • JVM需要避免Full GC,才将老年代容量加大,并且还限制了对象进入老年代,而MySQL并没有GC的限制,只需要单纯从缓存的角度考虑分区大小即可——防止老年代的数据被顶出去,所以选择加大新生代

        • 另外,感觉Java中某些对象生命周期巨长,无论是架构产生的对象还是业务产生的对象,所以老年代需要更大的空间存放它们,但是MySQL在查询时并没有绝对热点的页,所以老年代太大也没必要

        策略:

        1. 从磁盘加载进来的页面优先进入新生代,第一次访问时还在新生代,第二次访问时尝试加载到老年代

          似乎有点刻意,这样做难道不会导致所有页面都简单过一遍新生代立马到老年代吗?

          1. 后续不访问的就只会在老年代消逝(预读)

          2. 很难确定一个时间点将新生代的升级到老年代

            因为MySQL页和对象有所不同,一页有多条记录,访问一次页面可能是访问了它其中一条,也可能是一百条,于是很难确定是否是热点页面

          3. MySQL根据访问时间间距判断是否为热点(排除全表扫描的情况):在第一次访问处于新生代的页面时标记时间戳,第二次访问时当时间间距较短,表示可能时全表扫描,不升级

        2. 对于LRU链表的更新似乎太过频繁 尤其对于经常访问的老年代,每次访问一个页面都要移动到头部,维护成本比较高

          • 提出划分热点数据与更热的数据

            将老年代前四分之一定义为更热,访问此区域不会移动链表…

    • 刷新脏页到磁盘:

      两种策略:

      1. 沿着flush链表刷新

        遇到一个刷一个,但可能刷到热点页面,刷进去立马加载回来

        感觉这个才是main

      2. 沿着LRU新生代刷新

        遇到一个先判断是否是更新过的,是则刷进去

    • Buffer Pool的集群

      当Buffer Pool特别大的时候,可能承受不住太高的并发,会将其分成多个小Buffer Pool,彼此独立,类似集群

      而如何分配这些小Pool的大小,为了能够动态分配,MySQL提出了比一整个Pool更小的概念 —— chunk(小号Pool,里面也是控制块和缓冲页)

      • 之后便以chunk为单位进行增加或删除内存空间

    • 怎么知道哪个页在Buffer Pool?哈希值+哈希表

      页的哈希值如何刻画?表空间+页号

  11. 事务:

    1. ACID:
  • 原子性:

    如果在执行操作的过程中发生错误,能够把已经执行的操作恢复到执行前

    错误来源多种多样:主要原因是:一条指令在数据库层面,操作系统层面都是多条指令,任何一个位置都未必保证原子性,出问题都要整个事务一起承担

  • 隔离性:

    任何两个事务操作应该互不影响

    典型场景:事务B读取到事务A未提交的脏数据(多线程竟态问题)

  • 一致性:

    MySQL内部数据需要符合一定规约,比如not null,自增这些强行符合的;同时也有些数据库无法保证的一致性,例如age > 0,转账后收支平衡…这些理论上可以由MySQL的check实现一部分,或者由触发器实现,但更多场景下交由开发者在上层完成校验

  • 持久性:

    数据库承诺的操作一定要落盘

  1. redo

    redo log重做日志

    • 为了保证事务的持久性,应该在任何一个事务提交后就将其落盘,但是一个事务有多条指令,每条指令可能更改了许多不同的页,不同的页还随机分布在磁盘各个区域,一次刷盘IO带来的成本有点高

    • 让步:落盘并非全落,而是只将必要信息持久化,假如MySQL宕机导致事务commit却未落盘,redo必要信息即可复原

    • redo log顺序写入,并且逻辑接近普通指令 - 数据量不大

    redo 具体粒度如何? 到底写了什么?

    1. 假如记录的粒度太小:即每条redo log记录某个页面某个偏移量具体改几个字节,那么一个insert语句会对应巨多log,不光把真实数据写入聚簇索引,还要更新所有二级索引、更新槽信息、更新上下记录的链、更新page header,更新统计信息…

    2. 假如记录的粒度太大:直接把insert的SQL记录到redo中:为什么不可以?

      reod记录的是计算结果,直接记录insert会导致重复计算

    总体而言:一条redo的粒度要比纯粹记录修改某个位置要大,也比将整个操作记录成redo要小

    设计者通过抽象出各种操作类型,将几个物理位置的修改聚合到一个抽象的方法中,这个方法的粒度局限在更新一个页的内容(当insert时,包括插入数据,更新链表上下节点、更新槽、更新page header…)

    相关概念:mini-transaction

    最大的事务概念中包括多条语句,每条语句可能操作多个页面,每个页面操作可能修改多个位置,其中每一步都可能出错。一种思路是:将大问题分解成小问题,大事务分解成小事务

    mysql提出mini-transaction,用来刻画对页面的一次原子性访问

    • mini-transaction通过整体写入log buffer中来保证原子性执行

    • 多个mini-transaction通过在最后声明结束标识来确保原子性

    所以在log buffer中,同一事务的不同mini-transaction并不连续存储,而是在并发的进程中顺序存储,但同一mini-transaction中的redo log是连续存储的

    redo log 刷盘时机:

    1. 空间不足时

    2. 事务提交时:可以不刷buffer Pool,但必须刷log buffer

    3. 将某个脏页刷新到磁盘前,要保证redo log刷新到磁盘

    4. 后台线程以一定频率刷盘

    5. 正常关闭服务器时

    6. checkpoint时

    redo管理刷盘方式:

    • 维护了两个指针

      1. lsn(log sequence number):从MySQL启动开始,每新增一条redo log,lsn自增log大小的数量

        即:lsn指向log buffer最末尾

      2. flushed_lsn:每当将一个block刷入磁盘,flushed_lsn自增对应大小

        即:flushed_lsn指向log buffer最开始

    redo管理空间覆盖的方式:

    • 维护了一个指针

      1. checkpoint_lsn:表示在log buffer中,此指针之前的redo log对应的buffer pool中的内容,都被刷新到磁盘

      每当进行checkpoint操作,就会计算系统中可以被覆盖的log日志有哪些(更新checkpoint_lsn)

      怎么计算?看flush链表头信息

      flush链表表示还没刷入磁盘的脏页,而flush尾节点就是上次刷盘的下一个页面,通过此节点即可知上次刷盘刷到哪里

    1
    2
    3
    4
    5
    6
    整体流程:
    - 在Buffer Pool中,一般由异步线程从LRU链表和Flush链表上挑出脏页刷入磁盘
    - 每个事务的每个mini事务都会先写在redo log buffer中,通过lsn记录增长
    - 当程序执行任务负荷较重,lsn增长太快;此时在buffer pool异步刷盘不过来
    - 导致redo log buffer早期的日志无法被覆盖,新的redo log无处可写
    - 此时只能让用户线程来帮忙刷盘

    崩溃后页面修复小技巧:

    将不同的redo log通过其表空间和页号计算出哈希值,根据哈希值挂载到不同位置,如此一来挂载同一个桶中的redo便于对同一张表进行修改

  2. undo

    某些场景下,事务只执行了一半就由于不可抗力无法执行(可能由于断电、OS异常,也可能是认为回滚),undo日志的作用就是将和之后没能执行属于同一事务的操作回滚,从而实现原子性

    Question:事务不是在提交后才刷盘吗?那在未提交过程中就算中断也不会影响磁盘数据,为什么还需要undo?

    Ans:提交后才刷buffer pool,但是未必提交后才刷log buffer;

    • 假如在还没提交前刷了很多log buffer,但是宕机了,如果没有undo log,重启后加载redo就相当于之前已经将脏页落盘了

    所以新的问题是:根据flush链表落盘时,怎么知道一页上的事务已经提交了呢?

  3. MVCC

    redo,undo解决MySQL的原子性,而一致性的保障策略很多:

    1. not null,唯一索引,自增主键…

    2. 开发者自定义限制条件实现一致性

    3. 防止多个事务并发执行时对同一份数据的并发改操作

      并发改破坏一致性:是因为一个线程读后被另一个线程改了,原本线程还按照不存在的数据执行操作,导致数据紊乱

      同样的问题有三种类型:

      1. 脏写(dirty write)

        指:一个事务修改了另一个未提交事务修改过的数据

        异常场景

        • AB两事务先后修改X的值,B提交后A宕机(尝试回滚),但B已提交要保证持久性,但A没有提交要保证原子性…

        • 破环一致性的例子:

          如果原本一致性要求X == Y,但A改了X,B改了X和Y,A再改Y就引发数据不一致

      2. 脏读(dirty read)

        指:一个事务读到了另一个未提交事务修改过的数据,而如果这个数据最后没有提交,则读到的就是不存在的数据

      3. 不可重复读(Non-repeatable Read)

        指:一个事务修改了另一个未提交事务读取的数据,导致再读发现数据不同

      4. 幻读(Phantom)

        指:一个事务先读取符合条件P的记录,后来另一个事务写入了符合P的数据,之后第一个事务再读就出现两次读取不一致

    4. 承受一部分问题,换来一部分性能

      前面四种并发问题的危害性有所不同

      脏写 > 脏读 > 不可重复读 > 幻读

      设立多种隔离级别,就是对上述问题进行妥协,换来性能提升

      【SQL规范规定的四种隔离级别】

      1. 读未提交(READ UNCOMMITTED)

      2. 读已提交(READ COMMITTED)

      3. 可重复读(REPEATABLE READ)

      4. 可串行化(SERIALIZABLE)

    其中,脏写是最严重的问题,所有隔离级别都舍弃

    【MySQL支持的四种隔离级别】

    MySQL在 可重复读 级别下,很大程度上禁止了幻读的发生

    默认级别就是 可重复读


    MVCC

    undo 日志不仅在事务回滚中应用,在MVCC中也由undo记录历史版本

    undo日志中有两条undo 日志链,insert undo 和 update undo;

    insert undo 指不需要为MVCC服务的undo日志,即其他事务对此操作不需要可见

    update undo 指需要为MVCC服务的undo日志,即其他并发事务由于要访问历史版本,而update undo完全保存了(一般由update,delete操作触发)

    为什么需要delete mark?

    delete操作,修改主键的update操作,原本都需要将旧记录完全删除掉,但是MVCC需要保证其他并发执行的事务依然能够读到之前的旧记录——能够找到undo版本链,只有通过B+树上旧记录作为入口,否则完全删除的话即使有undo,也找不到


    每对记录进行一次改动,都会记录一条undo log;

    undo log 中,同一记录的多个版本通过 roll_pointer属性串联成链表,在最新版本(聚簇索引)中保存的roll_pointer指向对应undo中的旧版本

    —— 版本链

    每个版本中包括将其修改的事务id,通过保存旧版本即对应事务id来控制并发事务访问相同记录的行为,就是多版本并发控制

    —— MVCC


    对于满足 读已提交 的隔离级别,需要保证事务能够识别哪个undo版本是已经提交的;例如:如果某个事务修改了数据但没做提交,另一事务不应该读取到最新版本

    MySQL通过对比访问事务自身的事务id和访问目标版本的事务id,以及当前活跃的事务id,来判断是否可见

    • ReadView

      由某个事务生成,用来形容当下事务活跃情况;让创建者明白某个事务id是否已经提交,主要包括以下内容

      1. m_ids:当前系统活跃的读写事务的事务id列表

      2. min_trx_id:当前系统活跃的事务id的最小值

      3. max_trx_id:当前系统应该分配给下一个事务的事务id

      4. creator_trx_id:生成改readView的事务id

    具体流程:

    假设被访问的版本的事务id是A

    1. 如果A == 自身id,则标明是自己修改的记录,可见

    2. 如果A < min_id,则标明A事务已经被提交了,可见

    3. 如果A > max_id,则标明A事务是在生成read view之后才开启并添加undo,不可见

    4. 如果A在min和max之间,只能说明曾经执行过这个事务

      1. 如果A在活跃事务id列表中,则表示仍未提交,不可见

      2. 如果A不在,则说明已经提交了,可见

    注意的点:

    1. 只有事务尝试修改内容时(insert,update,delete),才会获取到事务id,否则一直trx_id = 0;

    2. readView上记录的只是各种事务标记,通过事务标记 匹配 版本链上版本

    read commited & repeated read

    • read committed:

      在每次查询时,创建当时的readView

    • repeated read:

      一个事务多次查询,使用第一次申请的read View

  4. 并发访问情况三种:

    1. 读读

    2. 读写

    3. 写写


    • 写写锁:

      所有隔离级别都必须满足,实现方式就是必须串行

      事务在尝试对记录进行修改时,会首先查看记录是否关联一把锁,如果事务关联,表明已被加锁,如果没,则自身尝试加锁

      • 自身加锁:在内存中创建一个锁结构,关键属性有trx_id,is_waiting表示这把锁是否被阻塞

        创建好后和记录关联,有访问权的事务is_waiting=false

      • 发现有锁:自身再在内存中创建一个锁结构,iswaiting = true,也和记录进行关联

    当第一个事务提交后释放锁,寻找到下一个阻塞的事务,更改iswaiting信息

    • 读写锁:

      MySQL控制读写合理进行的方式有:

      1. MVCC

      2. 加读写锁(读的事务将其他读/写的事务全都串行)

      使用MVCC可以实现读操作和写操作并发执行互不影响,因为读是针对旧版本的数据(在undo中),写是针对新版本的数据(在B+树中)

      使用读写锁性能较差,但某些场景下是必须的

      例如:银行取钱时,读后不允许其他事务写…


      读写锁:

      读写锁怎么能够实现防止读写、写写、却不妨读读呢?

      设计人员设计了两把相关锁:共享锁 / 独占锁

      • 共享锁:share锁 S锁

      • 独占锁:Exclusive锁 X锁

      两个事务都想获取一条记录的S锁,方行;一个事务拥有X锁,另一个事务尝试获取S锁 / X锁,阻塞;

    • 以上读写锁都是行级锁

      行级锁粒度小更加灵活,但是维护成本高;表级锁粒度大,简单直接,维护成本也小

      以下读写锁都是表级锁

      表级锁也区分S锁和X锁,运行逻辑和行级锁类似,不过要兼容行级锁

      兼容:

      当表加了表级S锁,所有行级和表级的X锁都要阻塞;当表加了表记X锁,所有…

      问题:

      表级锁在加锁前需要判断表上是否有锁,加S和加X都要判断

      但是不光要判断整个表级别有无表级锁,还要判断每行有没有行级锁

      为了不遍历,提出 意向锁

      意向锁:

      1. 意向共享锁 IS锁:加在表级别,表示事务想要读

      2. 意向独占锁 IX锁:加在表级别,表示事务想要写

    一个事务想要读取某条记录时,先尝试在表级别上加IS锁,但如果已经有别的事务加了表级X锁,则阻塞;如果没有,加IS锁表明自身意向后,再对具体行加行级读锁

    所以当有事务尝试加表级锁X时,需要等待所有的IS锁,IX锁都解开;

    尝试加表级锁S时,需要等待所有IX锁都解开

    多种行级锁:

    1. Record Lock(正经记录锁)

      以上谈论的都是Record Lock,用于在行记录上加锁,分S锁和X锁,用于局部互斥访问

    2. Gap Lock(间隙锁)

      间隙锁是为了实现在Repeatable Read级别下解决幻读问题

      实际解决方案有两种:MVCC和Gap Lock

      幻读场景下,由于没法给不存在的记录加锁,Gap Lock选择给某条记录的周围加锁(实际上是给此记录之前到上一条记录的中间部分不允许插入)

      被锁阻塞的事务会加一个人插入意向锁(Insert interntion Lock)挂载到记录上,当gap锁解除时触发插入意向锁相关的事务


    一行对应一个行级锁,但一万行可能只对应几个行级锁

    MySQL为了避免由于访问行数太多,导致维护过多行级锁,提出将有关系的行共同使用同一把行级锁

    关系:

    1. 操作是在同一个事务中

    2. 记录是在同一个页面中

    3. 加锁的类型相同

    4. 等待状态一致


SQL注入

  • 手法1:

    通过联合查询,使得回显的数据中显示自定义信息 / 其他信息

  • 手法2:

    通过order 5 / 4 / 3 / 2 是否正常执行,判断一共有几列

  • 手法3:

    通过联合查询,尝试查到数据库关键统计信息

    在数据库infomation_schema内部存储的系统信息表

    核心表:

    SCHEMATA表

    提供了当前mysql实例中所有数据库的信息。是show databases的结果取之此表。
    TABLES表

    提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。是show tables from schemaname的结果取之此表。
    COLUMNS表

    提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是show columns from schemaname.tablename的结果取之此表。
    STATISTICS表

    提供了关于表索引的信息。是show index from schemaname.tablename的结果取之此表。
    USER_PRIVILEGES(用户权限)表

    给出了关于全程权限的信息。该信息源自mysql.user授权表。是非标准表。

  • 手法4:报错注入

  • 手法5:布尔注入

  • 手法6:注释注入:

    原本要根据username = ‘’ and password = ‘’ 判断有无

    如果username传入的值中有# 或者 – 可以将后面的注释掉,结果无论password传入何值,都必正确


MySQL
https://13038032626.github.io/2024/05/17/MySQL/
Author
Ha_Ha_Wu
Posted on
May 17, 2024
Licensed under