MySQL
回来吧我的MySQL!
持久化:把数据保存到可掉电式存储设备中,以供之后使用
不同类型的数据存储到不同介质中
图片、视频、代码、二进制等存到文件中,复杂类型数据存储到数据库中
为什么数据存在数据库里而不是文件里?
例如上亿条数据如果存在txt、excel文件里,找寻一条需要O(n)遍历,如果在数据库的一张表里,有很多优化策略提高查询速度
同时数据库存储类型更丰富
数据库与数据库管理系统
数据库管理系统DBMS管理多个数据库,一般开发人员会针对每一个应用创建一个数据库
真实的数据库就是一个文件夹下的n多张表(文件)
MySQL的身份是DBMS,负责打开与操作这些文件
navicat是一个客户端的可视化工具,代替命令行执行用户和MySQL的交互。
RDBMS与非RDBMS
RDBMS:
DataBase :以行和列的形式存储数据,形成一张表,多张表构成一个库
把现实世界的信息抽象成二维数据表示
非RDBMS:
非关系型数据库舍弃了关系型数据库中的结构化存储+结构化查询+数据之间的关系;(不需要经过SQL层的解析)获得更高的性能
类别:
键值型数据库 redis
文档型数据库 MangoDB,通过k-v形式存储XML,JSON格式文档
搜索引擎数据库 ElasticSearch 核心原理是:倒排索引
列式数据库 HBase 行式是Z,列式是N 可用大量降低系统IO,适合分布式文件系统
图数据库 Neo4j
MySQL体系结构
连接层:
对接客户端的连接服务
完成:
连接处理
授权认证
连接池管理
…
服务层:
完成大多数核心服务功能
完成:
提供SQL接口
维护缓存
SQL解析与优化
内置函数的调用
完成所有跨存储引擎的操作
…
引擎层:
负责数据的存储和提取
完成:
对上层提供存储API
维护索引机制
可靠存储、回滚、查询、一致性等
…
存储层:
将数据存储在文件系统之上
完成:
维护数据文件
维护日志文件
…
存储引擎
MySQL是怎样运行的
MySQL提供接口的策略:
在linux下成为守护进程(daemon)、在windows下注册为服务(随机自启)
提供多层包装的启动模式,连环嵌套:mysqld -> mysqld_safe -> mysql.server
mysqld是二进制启动程序,后面的都是shell脚本,用来在前者基础上增加功能
关于网络:
IP地址分类:
公网IP:指在因特网上直接可达的地址,通过公网IP可以访问别人,也可以被人访问
分类:
1
2
3
4
5
6
7
8
9A类: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使用的地址,目的是作为私有地址,供家庭、企业、学习等内部组网使用
这些地址只会出现在私网中,通常根据容纳的主机数选择使用哪一段
正常家庭用C类
学习 / 大型企业可能用B / A类,比如手机在大型商场上网时获取的是10开头的内网地址
私网设备连接公网——NAT地址转换
思路:端口映射——将内网众多IP+端口的设备挤到外网一个IP+65535个端口
MySQL服务器过程简述:
关于查询缓存:
mysql 5.7开始不推荐使用缓存,MySQL 8删除缓存
why 缓存?
缓存中维护者MySQL服务器刚刚执行完的结果,维护一个缓存可以避免重复的查询,并且如果缓存是线程共享的,且服务确实存在热点SQL,那么添加缓存直接避免后续的 语法解析、查询优化、存储引擎...
why not 缓存?
使用场景有点少,意外情况有点多
- 配合MySQL事务。较难实现
- SQL中包含系统函数时,无法使用缓存
- 为了确保缓存命中,需要对具体的SQL语句处理(而不是直接比较字符串是否出现,这样的话当多个空格,where顺序不同…都会导致无法命中)
语法解析:
对客户端发来的文本进行解析,包括:词法解析、语法解析、语义分析… 对应到具体的数据结构上
本质就是编译此SQL
查询优化:
语法解析后虽然知道要查的内容,但SQL写法多种多样,传入的并非最快的
需要经过如:外连接转内连接、表达式简化、子查询转连接…
最终生成 执行计划
进入存储引擎
设置的设置:
命令行启动时 作为参数设置
配置文件设置
配置文件有默认存在的位置、默认名称、多个文件直接默认优先级…
配置文件形式:
1
2
3
4
5
6
7
8
9
10
11
12cat 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
系统变量:
1
show variables like 'default';
系统变量分GLOBAL(全局范围)和SESSION(会话范围)
系统启动时,将一些GLOBAL的变量设置默认值,新的会话中如果需要更改同样的变量,设置属于自身的SESSION级别的变量
编码解码:
MySQL通过网络连接客户端与服务器,网络就意味着编码,最终都是字节数组
确保客户端与服务器的编码解码字符集相同【一般在客户端的请求中携带了编码方式,由此动态设置SESSION级别的属性】
服务器端解码的字符集和用于处理的字符集可能不一致:
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 |
|
MySQL字符集设置的偏好:
优先和操作系统一致——也就是和本地文件系统、命令行工具、其他IO设备协调一致
但存在特殊场景:当操作系统字符集太低级【ascii】,而MySQL最低是Latin1,要么就是utf8,则此时必须按照MySQL;或者显示的设置了default-character-set
InnoDB
InnoDB页:
完成的功能是:实现对磁盘上数据的读写 –> 一次读写的粒度决定一批操作的效率:InnoDB决定一页设置成16KB,将数据划分为若干页。
行格式
真实存储空间如何表示抽象行数据的格式
- 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
7MySQL索引页的管理方式:
- 物理存储是连续的,但通过头部信息构建逻辑存储
- 通过链表链接每个记录,并且是按照主键ID顺次链接
- 头节点和尾节点特殊处理,作为两条特殊的记录
- 删除一条记录只是逻辑上标识被删,并由链表绕开此记录,但物理上仍然存在
并且删除后再添加记录时有可能复用此空间 -- 通过一个垃圾链表
当主键就是原本的主键时甚至会回到原本链表的位置记录的真实信息
各个属性的值
除了真实值外,MySQL还会添加额外的真实数据
row_id:
当整张表没有确认主键、也没用not null 且 UNIQUE的属性被委派为主键,MySQL主动添加一个隐藏列作为主键
trx_id:
事务ID
roll_pointer:
回滚指针
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)
但维护槽同样具有成本,增删记录时要保证每个槽的数据量大致相同,就要求更新槽的信息;
InnoDB的B+树索引
B+树:
从优化的角度看B+树的发明:
为了在不同索引页中找到目标记录,需要给各个索引页添加目录,由目录构成一级索引
这些索引的管理收到数据项crud的影响也不简单,决定复用对数据项的管理方式,即:类似COMPACT行格式,类似槽的页面
通过索引可以查到数据项所在页面,但索引本身越来越长,一旦图片16KB,最开始的二分查找就不行了 – 再建第二层索引
索引分为两类:
聚簇索引
特点:
叶子节点存储的是完整的用户记录(包括隐藏列)
使用记录的主键值大小进行排序
在叶子节点和内节点中都按照主键排成单向链表
叶子节点根据主键值排成双线链表
同一层的内节点中,目录项也是根据主键排序的双向链表
二级索引
叶子节点存储的是索引列和主键的匹配信息
使用相应的索引列进程排序
联合索引
当使用C2,C3两列建立联合索引时,目录页中每项记录的是C2,C3,子页的信息,按照先C2,后C3的顺序顺次排列;叶子节点记录的是C2,C3,主键的信息,按照先C2,后C3的顺序顺次排列;
一些结论
InnoDB存储引擎会自动为主键建立聚簇索引(如果没有显式知道主键,并且没有声明不允许为null的UNIQUE键,则会自动添加主键)
索引的代价:
空间的代价:
一个页面是16KB,一颗B+树由多少页组成…
时间的代价:
- 维护的成本:
每当对一张表crud时,很容易破坏原本数据项的顺序,而B+树对顺序要求十分苛刻,每一层都是有序的双向链表,所以可能导致一次增删改操作引发页面分裂,页面回收…
- 执行查询语句前会计算使用各个索引的成本,如果索引太多会有很多无用功
如何确定选择哪个索引
由where限制条件确定是否缩小了扫描区间的范围
原本我对限制条件的认识是:条件越多找起来越麻烦,但很多场景下未必;有些限制条件和索引契合度高,可以把扫描区间限制的很小(尤其在联合索引中);有的限制条件和索引契合度巨低,甚至用or的限制条件可能会破坏好索引带来的简便
根据可能使用的索引,分别判断扫描区间的大小,即可判断该不该用
另外索引也可以用于排序
当有10000条数据,要按照某列排序后返回前十个
没有索引的场景下需要全部加载到内存里排序
但有索引时就很简单
另外索引也可以用于分组
当有10000条数据,要按照某列分组后统计
没有索引的场景下,在扫描过程中不清楚还有没有同组的,需要建临时表来存储统计信息
但有索引就天然分组
走索引的问题
回表可能不如全表扫描
假如按照索引列得到很多不连续的主键值,需要一个一个回标查询全部信息,但是对聚簇索引的访问太过随机,就不如连续的访问(因为页都是存储在磁盘的,需要时才IO进内存,太随机容易有颠簸)
到底是全表还是回表,由查询优化器实现
但是可以通过限制条件限制回表的代价:limit – 别回表太多次
如何更好的使用索引
只为用于搜索、排序、分组的列创建索引 – 减少使用成本
考虑索引列中,不重复值的个数:
当重复值太多,即使是where condition = value的条件也会有很多回表查询,回表太多就不如全表扫描
索引列的类型尽量小(指能用tinyInt就别用Int)
因为类型越小,16KB能存下就越多,树的页越少,高度越低
所以主键也能小就小
覆盖索引
为了避免回表的成本,可以在查询列表中只包含索引列和主键
避免在搜索条件中出现索引列的计算式
mySQL直接不认识
新增记录尽量主键自增,而不是中间插入主键
因为自增的主键即使分页也只需要把新值插入新页,但中间的主键可能会导致分页+半数数据的复制
mysql数据目录
InnoDB表空间
症结1:
当一张表的数据量太大,常规的B+树会有巨多的叶子节点和非叶子节点,但无法保证这些逻辑上相关的节点在物理上存储的接近
因为在磁头移动时,越接近的数据带来的成本越低
所以尝试给一张表更大的空间(区),相关的空间都先从一个区中申请
症结2:
区的粒度有点大,会产生不少碎片
所以有碎片区的概念,进而区分出:空闲区、有剩余空间的碎片区、没有剩余空间的碎片区….
为了管理这些区,出现了经典操作:使用SDES Entry记录每个区的元数据,用各种链表串联各个Entry(很想PCB)
Free链表,在需要申请空闲区时访问
Free_Frag链表…
单表访问方法
几个量级:
const:指通过主键值在聚簇索引中直接定位,或者通过(唯一)二级索引找到一条后进行回表
ref:通过二级索引单点扫描找到挺多条记录,每条都要回表
range:在二级索引中就有较大的扫描区间,区间内每条记录分别回表
index:遍历二级索引
all:全表扫描
索引合并:
一般情况下,InnoDB只会为单个索引生成扫描区间,但是某些场景下如果两个索引都能生成区间,并且区间的交集能减少扫描成本,则会有 索引合并
表连接的原理
连接的本质:将各个表中的记录,一次进行匹配,匹配后的组合返回客户端;如果没有任何过滤条件,则结果就是笛卡尔积
分为内连接和外连接
区别在于:外连接会把不符合on自居的记录保留,加入到结果集中
连接的原理:
嵌套循环连接算法
对于内连接(谁是驱动表无所谓)
选择驱动表,先执行对驱动表的单表查询
对步骤1中的查询结果集中的每条记录,都分别到驱动表中查找匹配的记录
基于成本的优化
MySQL在执行一个查询时,可能有不同的执行方案,如何选择成本最低的?
成本分为IO成本和CPU成本,规定读取页面的成本为1,检测一条记录是否符合搜索条件的成本为0.2
计算成本步骤:
根据搜索条件,找出所有可能使用的索引
计算全表扫描的代价
计算使用不同所以执行查询的代价
对比各种执行方案,找出最小值
InnoDB统计数据如何收集
基于规则的优化
MySQL的设计人员对于任何糟糕的SQL输入,都会将其转化成高级的SQL
重写操作有:
条件化简:
表达式的计算
移除不必要的括号、没用的条件…
常量替换
外连接消除
尝试吧外连接转化成内连接,内连接则可以优化连接顺序降低成本
子查询优化
in的优化
优先转化为半连接
无法转化,则尝试用物化表方式
explain
Explain用来查看执行计划
各个列:
table:Explain的每条记录对应一个单表访问,table标识访问哪张表
id:查询语句中有几个Select,每个Select会被分配一个唯一ID,尤其在子查询,union中(但子查询有时候会转化成连接查询,导致id唯一)
但并不是查几张表就有几个ID,比如连接查询就是两个Select,一个id
select_type:每个小的select在整个大查询中可能扮演不同的角色
type:单表访问方法
除了const,ref,range,index,all,还有其他方法
例如:system,fulltext…
possible_key和key
可能用到的索引和真实使用的索引
key_len…
explain format=json select…用来通过JSON查看执行计划
optimizer trace
用来查看优化器生成执行计划的整个过程,用来获悉为什么不用别的查询方案
开启后,在某个数据库中生成相应文件
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,导致原本正常在缓冲区的页面被淘汰)
- 在预读场景下更加明显
预读:mysql出于局部性原理做的优化
当对某个区中页面访问多次时,会将整个区的页面全都加载进Buffer Pool,其中又分连续读取56个和随机读取13个
预读加载页进Buffer Pool时异步的
- 在全表扫描场景下也很明显
假如是一个很大的表,不得不全表扫描,这就相当于将整个BufferPool换一次血,扫描完后还要重新加载上次没搞完的任务
解决方案:冷热分离 / 老年代|新生代
MySQL默认老年代占37%
Java老年代与新生代的比例默认2:1,而MySQL恰好反过来,主要原因是:
JVM需要避免Full GC,才将老年代容量加大,并且还限制了对象进入老年代,而MySQL并没有GC的限制,只需要单纯从缓存的角度考虑分区大小即可——防止老年代的数据被顶出去,所以选择加大新生代
另外,感觉Java中某些对象生命周期巨长,无论是架构产生的对象还是业务产生的对象,所以老年代需要更大的空间存放它们,但是MySQL在查询时并没有绝对热点的页,所以老年代太大也没必要
策略:
从磁盘加载进来的页面优先进入新生代,第一次访问时还在新生代,第二次访问时尝试加载到老年代
似乎有点刻意,这样做难道不会导致所有页面都简单过一遍新生代立马到老年代吗?
后续不访问的就只会在老年代消逝(预读)
很难确定一个时间点将新生代的升级到老年代
因为MySQL页和对象有所不同,一页有多条记录,访问一次页面可能是访问了它其中一条,也可能是一百条,于是很难确定是否是热点页面
MySQL根据访问时间间距判断是否为热点(排除全表扫描的情况):在第一次访问处于新生代的页面时标记时间戳,第二次访问时当时间间距较短,表示可能时全表扫描,不升级
对于LRU链表的更新似乎太过频繁 尤其对于经常访问的老年代,每次访问一个页面都要移动到头部,维护成本比较高
提出划分热点数据与更热的数据
将老年代前四分之一定义为更热,访问此区域不会移动链表…
刷新脏页到磁盘:
两种策略:
沿着flush链表刷新
遇到一个刷一个,但可能刷到热点页面,刷进去立马加载回来
感觉这个才是main
沿着LRU新生代刷新
遇到一个先判断是否是更新过的,是则刷进去
Buffer Pool的集群
当Buffer Pool特别大的时候,可能承受不住太高的并发,会将其分成多个小Buffer Pool,彼此独立,类似集群
而如何分配这些小Pool的大小,为了能够动态分配,MySQL提出了比一整个Pool更小的概念 —— chunk(小号Pool,里面也是控制块和缓冲页)
- 之后便以chunk为单位进行增加或删除内存空间
怎么知道哪个页在Buffer Pool?哈希值+哈希表
页的哈希值如何刻画?表空间+页号
事务:
- ACID:
原子性:
如果在执行操作的过程中发生错误,能够把已经执行的操作恢复到执行前
错误来源多种多样:主要原因是:一条指令在数据库层面,操作系统层面都是多条指令,任何一个位置都未必保证原子性,出问题都要整个事务一起承担
隔离性:
任何两个事务操作应该互不影响
典型场景:事务B读取到事务A未提交的脏数据(多线程竟态问题)
一致性:
MySQL内部数据需要符合一定规约,比如not null,自增这些强行符合的;同时也有些数据库无法保证的一致性,例如age > 0,转账后收支平衡…这些理论上可以由MySQL的check实现一部分,或者由触发器实现,但更多场景下交由开发者在上层完成校验
持久性:
数据库承诺的操作一定要落盘
redo
redo log重做日志
为了保证事务的持久性,应该在任何一个事务提交后就将其落盘,但是一个事务有多条指令,每条指令可能更改了许多不同的页,不同的页还随机分布在磁盘各个区域,一次刷盘IO带来的成本有点高
让步:落盘并非全落,而是只将必要信息持久化,假如MySQL宕机导致事务commit却未落盘,redo必要信息即可复原
redo log顺序写入,并且逻辑接近普通指令 - 数据量不大
redo 具体粒度如何? 到底写了什么?
假如记录的粒度太小:即每条redo log记录某个页面某个偏移量具体改几个字节,那么一个insert语句会对应巨多log,不光把真实数据写入聚簇索引,还要更新所有二级索引、更新槽信息、更新上下记录的链、更新page header,更新统计信息…
假如记录的粒度太大:直接把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 刷盘时机:
空间不足时
事务提交时:可以不刷buffer Pool,但必须刷log buffer
将某个脏页刷新到磁盘前,要保证redo log刷新到磁盘
后台线程以一定频率刷盘
正常关闭服务器时
checkpoint时
redo管理刷盘方式:
维护了两个指针
lsn(log sequence number):从MySQL启动开始,每新增一条redo log,lsn自增log大小的数量
即:lsn指向log buffer最末尾
flushed_lsn:每当将一个block刷入磁盘,flushed_lsn自增对应大小
即:flushed_lsn指向log buffer最开始
redo管理空间覆盖的方式:
维护了一个指针
- 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便于对同一张表进行修改
undo
某些场景下,事务只执行了一半就由于不可抗力无法执行(可能由于断电、OS异常,也可能是认为回滚),undo日志的作用就是将和之后没能执行属于同一事务的操作回滚,从而实现原子性
Question:事务不是在提交后才刷盘吗?那在未提交过程中就算中断也不会影响磁盘数据,为什么还需要undo?
Ans:提交后才刷buffer pool,但是未必提交后才刷log buffer;
- 假如在还没提交前刷了很多log buffer,但是宕机了,如果没有undo log,重启后加载redo就相当于之前已经将脏页落盘了
所以新的问题是:根据flush链表落盘时,怎么知道一页上的事务已经提交了呢?
MVCC
redo,undo解决MySQL的原子性,而一致性的保障策略很多:
not null,唯一索引,自增主键…
开发者自定义限制条件实现一致性
防止多个事务并发执行时对同一份数据的并发改操作
并发改破坏一致性:是因为一个线程读后被另一个线程改了,原本线程还按照不存在的数据执行操作,导致数据紊乱
同样的问题有三种类型:
脏写(dirty write)
指:一个事务修改了另一个未提交事务修改过的数据
异常场景
AB两事务先后修改X的值,B提交后A宕机(尝试回滚),但B已提交要保证持久性,但A没有提交要保证原子性…
破环一致性的例子:
如果原本一致性要求X == Y,但A改了X,B改了X和Y,A再改Y就引发数据不一致
脏读(dirty read)
指:一个事务读到了另一个未提交事务修改过的数据,而如果这个数据最后没有提交,则读到的就是不存在的数据
不可重复读(Non-repeatable Read)
指:一个事务修改了另一个未提交事务读取的数据,导致再读发现数据不同
幻读(Phantom)
指:一个事务先读取符合条件P的记录,后来另一个事务写入了符合P的数据,之后第一个事务再读就出现两次读取不一致
承受一部分问题,换来一部分性能
前面四种并发问题的危害性有所不同
脏写 > 脏读 > 不可重复读 > 幻读
设立多种隔离级别,就是对上述问题进行妥协,换来性能提升
【SQL规范规定的四种隔离级别】
读未提交(READ UNCOMMITTED)
读已提交(READ COMMITTED)
可重复读(REPEATABLE READ)
可串行化(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是否已经提交,主要包括以下内容
m_ids:当前系统活跃的读写事务的事务id列表
min_trx_id:当前系统活跃的事务id的最小值
max_trx_id:当前系统应该分配给下一个事务的事务id
creator_trx_id:生成改readView的事务id
具体流程:
假设被访问的版本的事务id是A
如果A == 自身id,则标明是自己修改的记录,可见
如果A < min_id,则标明A事务已经被提交了,可见
如果A > max_id,则标明A事务是在生成read view之后才开启并添加undo,不可见
如果A在min和max之间,只能说明曾经执行过这个事务
如果A在活跃事务id列表中,则表示仍未提交,不可见
如果A不在,则说明已经提交了,可见
注意的点:
只有事务尝试修改内容时(insert,update,delete),才会获取到事务id,否则一直trx_id = 0;
readView上记录的只是各种事务标记,通过事务标记 匹配 版本链上版本
read commited & repeated read
read committed:
在每次查询时,创建当时的readView
repeated read:
一个事务多次查询,使用第一次申请的read View
锁
并发访问情况三种:
读读
读写
写写
写写锁:
所有隔离级别都必须满足,实现方式就是必须串行
事务在尝试对记录进行修改时,会首先查看记录是否关联一把锁,如果事务关联,表明已被加锁,如果没,则自身尝试加锁
自身加锁:在内存中创建一个锁结构,关键属性有trx_id,is_waiting表示这把锁是否被阻塞
创建好后和记录关联,有访问权的事务is_waiting=false
发现有锁:自身再在内存中创建一个锁结构,iswaiting = true,也和记录进行关联
当第一个事务提交后释放锁,寻找到下一个阻塞的事务,更改iswaiting信息
读写锁:
MySQL控制读写合理进行的方式有:
MVCC
加读写锁(读的事务将其他读/写的事务全都串行)
使用MVCC可以实现读操作和写操作并发执行互不影响,因为读是针对旧版本的数据(在undo中),写是针对新版本的数据(在B+树中)
使用读写锁性能较差,但某些场景下是必须的
例如:银行取钱时,读后不允许其他事务写…
读写锁:
读写锁怎么能够实现防止读写、写写、却不妨读读呢?
设计人员设计了两把相关锁:共享锁 / 独占锁
共享锁:share锁 S锁
独占锁:Exclusive锁 X锁
两个事务都想获取一条记录的S锁,方行;一个事务拥有X锁,另一个事务尝试获取S锁 / X锁,阻塞;
以上读写锁都是行级锁
行级锁粒度小更加灵活,但是维护成本高;表级锁粒度大,简单直接,维护成本也小
以下读写锁都是表级锁
表级锁也区分S锁和X锁,运行逻辑和行级锁类似,不过要兼容行级锁
兼容:
当表加了表级S锁,所有行级和表级的X锁都要阻塞;当表加了表记X锁,所有…
问题:
表级锁在加锁前需要判断表上是否有锁,加S和加X都要判断
但是不光要判断整个表级别有无表级锁,还要判断每行有没有行级锁
为了不遍历,提出 意向锁
意向锁:
意向共享锁 IS锁:加在表级别,表示事务想要读
意向独占锁 IX锁:加在表级别,表示事务想要写
一个事务想要读取某条记录时,先尝试在表级别上加IS锁,但如果已经有别的事务加了表级X锁,则阻塞;如果没有,加IS锁表明自身意向后,再对具体行加行级读锁
所以当有事务尝试加表级锁X时,需要等待所有的IS锁,IX锁都解开;
尝试加表级锁S时,需要等待所有IX锁都解开
多种行级锁:
Record Lock(正经记录锁)
以上谈论的都是Record Lock,用于在行记录上加锁,分S锁和X锁,用于局部互斥访问
Gap Lock(间隙锁)
间隙锁是为了实现在Repeatable Read级别下解决幻读问题
实际解决方案有两种:MVCC和Gap Lock
幻读场景下,由于没法给不存在的记录加锁,Gap Lock选择给某条记录的周围加锁(实际上是给此记录之前到上一条记录的中间部分不允许插入)
被锁阻塞的事务会加一个人插入意向锁(Insert interntion Lock)挂载到记录上,当gap锁解除时触发插入意向锁相关的事务
一行对应一个行级锁,但一万行可能只对应几个行级锁
MySQL为了避免由于访问行数太多,导致维护过多行级锁,提出将有关系的行共同使用同一把行级锁
关系:
操作是在同一个事务中
记录是在同一个页面中
加锁的类型相同
等待状态一致
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传入何值,都必正确