一、前置知识
-
初识
- Mysql的客户端/服务端架构
- Mysql的安装启动
- 启动服务器
- unix系统:
- mysqld
- mysql_safe(间接调用mysqld)
- mysql.server(间接调用mysql_safe)
- mysql_mutil(启动多个服务器实例)
- windows系统
- 手动启动
- 以服务的方式运行服务程序(“完整可执行文件路径” --install[-manual][服务名])
- unix系统:
- 启动客户端:mysql -h主机名 -u用户名 -p密码
- 启动服务器
- 客户端和服务端连接的过程
- TCP/IP
- 命名管道和共享内存(C/S均位于同一台Windows主机)
- Unix域套接字文(C/S均位于同一台Unix主机)
- 服务器处理客户端请求
- 一条sql的大概过程
- 连接管理
- 缓存通信线程-连接池
- 不同计算机之间进行通信还可以采用传输层安全性TLS协议对连接进行加密
- 解析与优化
- 查询缓存-问题:每次查询、更新缓存都会造成一定开销,Mysql8.0删除
- 语法解析:对接收到的文本请求进行分析,判断语法是否真确,提取要查询的表、各种查询条件等信息,本质上是一个信息编译的过程
- 查询优化:对执行语句优化生成一个最终的执行计划,比如外连接转内连接、表达式简化、子查询转连接等
- 存储引擎
- 封装了数据的提取、存储等操作
- Mysql处理请求的过程简单划分为Server层和存储引擎层
- Server:连接管理、查询缓存、语法解析、查询优化
- 存储引擎层:真实存取数据相关功能
- 常用执行引擎
-
启动选项和系统变量
- 启动选项和系统变量
- 举例
- 相同点
- 大部分的系统变量可以被当做启动参数
- 设置方式
- 命令行:mysqld --default-storage-engine=InnoDB
- 配置文件:[server] default-storage-engine=InnoDB
- 用来调整程序启动后的行为
- 不同点
- 启动选项
- 有些启动选项不是系统变量:defaults-file(配置文件默认路径)
- 系统变量
- 程序运行过程中设定
- 有些是程序运行过程中自动生成的不可以当启动选项,如auto_increment_offset、character_set_client
- 查看系统变量:SHOW VARIABLES LIKE 'max_connections';
- 启动选项
- 状态变量
- 用来显示服务器程序运行状态
- 查看命令:SHOW [CLOBAL|SESSION] STATUS [LIKE匹配的模式]
- Mysql服务监控通过类似方式抓取相关运行数据
- 启动选项和系统变量
-
字符集合和比较规则
- 简介
- 字符集:指某个字符范围的编码规则(包含哪些字符、字节和字符的映射)
- 常见字符集
- 比较规则:指对某个字符集中的字符比较大小的一种规则
- mysql支持的字符集和比较规则
- 字符集和比较规则的应用
- 乱码的由来-编码和解码使用了不同的字符集
- mysql中字符集的转换
- 简介
二、底层存储结构和索引
-
InnoDB记录结构
- 回顾
- 存储引擎:负责对表中的数据进行读取和写入;不同存储引擎有不同特性
- Mysql默认存储引擎:5.5.5开始InnoDB
- InnoDB页简介
- 页是什么?
- 磁盘和内存交互的基本单位
- 管理存储空间的基本单位
- 默认大小16KB
- 为什么有页?
- 磁盘读写效率低,将数据划分为页,以页作为内存和磁盘交互的基本单位
- 页是什么?
- InnoDB行格式
- 行格式
- 每条记录在磁盘上的存储形式
- 4种行格式
- compact-5.0引入,作为默认行格式为了更高效存储数据记录
- redundant-5.0之前
- dynamic-5.7默认行格式
- compress
- 指定行格式:
- COMPACT
- 记录额外信息+真实数据
- 额外信息:用来管理而添加的额外信息
- 变长字段长度列表
- 存储Mysql变长数据字段(VARCHAR、TEXT)的数据占有字节数,逆序存放
- NULL值列表
- 统一管理值为NULL的列
- 为什么?避免NULL值冗余到真实数据中占用额外空间
- 处理过程
- 统计表中允许存储NULL的列
- 将每个允许存储NULL的列对应一个二进制位,按照逆序排列;1为NULL,0不为NULL
- NULL值列表必须用整数个字节的位表示,如不是整数个字节,高位补0
- 示例
- 记录头信息
- 5个字节,40个二进制位
- 变长字段长度列表
- 记录真实的数据
- Mysql为每条记录添加隐藏列
- 主键生成策略
- 优先使用自定义主键
- 选取一个Unique列为主键
- 给表默认添加一个名为ROW_ID的隐藏列为主键
- 示例
- REDUNDANT
- DYNAMIC
- COMPRESS-采用压缩算法对页面进行压缩
- 列溢出
- 记录的数据太多产生溢出
- COMPACR&REDUNDANT
- 在记录的真实数据处只会存储改列的一部分数据(768字节),把剩余的数据分散存储在几个其它页中,然后记录的真实数据处用20个字节存储指向这些页的地址
- DYNAMIC&COMPRESS
- 把所有字节都存储到其它页面中,只在记录的真实数据处存储其它页面的地址
- mysql一行能存储多少字符?
- VARCHAR(M),最多可以存储65535个字节/不同字符集下一个字符所占的字节数(还有存储额外数据)
- 行格式
- 总结
- 页:磁盘和内存交互的基本单位、管理存储空间的基本单位,默认16KB
- InnoDB的四种行格式
- COMPACT、REDUNDANT、DYNAMIC、COMPRESS
- 行格式的存储结构:额外信息、真实数据
- 行溢出:记录的数据过长,当前页放不下的时候会把多余数据存储到其他页中
- 回顾
-
InnoDB数据页结构
- 不同类型的页
- 数据页结构
- 记录在页中的存储
- 插入数据的空间分配
- 记录在页中的存储
- 记录头信息回顾
- 记录存储示意图
- delete_mask:是否被删除,0未删除,1删除
- heap_no:当前记录在本页中的位置
- record_type:
- next_record
- 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量(字节)
- 链表:可以通过一条记录找到它的下一条记录(按照主键值从小到大顺序的下一条)
- 示意图
- 删除一条记录
- 页目录
- 如何根据主键查找数据?分类+目录
- 示意图(分组规定:对于最小记录所在的分组只能拥有一条记录,最大记录所在的分组只能有1~8条,剩下的分组范围只能是4~8条之间)
- 将所有正常的记录(包括最大和最小记录,不包括被删除的记录)划分为几个组
- 每个组的最后一条记录(组内最大的记录)的头信息中的n_owned属性表示该组有几条记录。
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页尾部的地方-Page Directory。页目录中的地址偏移量被称为槽SLOT,即页目录由槽组成。
- 根据主键查找
- 示意图
- 查找过程
- 通过二分法确定记录所在的槽,找到槽中主键最小的记录
- 通过记录的next_record属性遍历该槽所在组的各个记录
- 文件头
- 各种类型的页都通用,都以File Header作为第一个组成部分,存储通用的信息
- CHKSUM:通过某种算法,将很长的字符串加密成短字符串
- 双向链表结构(并不是所有类型的页都有上一个和下一个属性),物理空间并非连续
- 页头
- 针对数据页记录,存储记录的状态信息
- 文件尾
- 作用:为了检测一个页是否完整
- 两个部分
- 前4字节:页的校验和-同文件头校验和相对
- 后4字节:页面被最后修改时对应的日志序列位置LSN
- 举例:页中的数据在内存中被修改后同步至磁盘,中途断电
- 页文件头、文件尾校验和不一致,表明是一个损坏的页;
- File Header先被同步进磁盘,等整个页同步完成时,校验和也会被写入页尾同步到磁盘,完全同步成功,则收尾校验和一致。
- 总结
- InnoDB为了不同的目的而设计了不同类型的页,把存放数据的页叫数据页
- 数据页的7个组成部分
- 页中的记录通过记录头信息中的next_record属性串联成一个单向链表
- 页记录被划分为多个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory
- 数据页之间通过File Header中上/下页编号形成一个双向链表
-
B+树索引
- 数据页回顾
- 没有索引如何进行查找
- 在一个页中查找
- 以主键为条件:二分法定位对应的槽,再遍历该槽对应分组的记录
- 以其他列为条件:从最小记录开始依次遍历查找
- 在多个页中查找
- 定位记录所在的页(只能从第一页沿着双向链表一直往下找)
- 从所在页中查找对应记录
- 在一个页中查找
- 索引
- 一个简单索引方案,为快速定位记录所在的数据页也建立一个目录
- 规定下一页用户记录的主见比必须大于上一页用户记录的主键值
- 插入三条数据后
- 再插入一条(页分裂)
- 给页建立一个目录项
- 插入多条数据
- 建立目录项
- 这几个目录项放在物理存储器上连续存储
- 这个目录项别名-索引
- 规定下一页用户记录的主见比必须大于上一页用户记录的主键值
- InnoDB中的索引方案
- 上述简易方案假设目录项存储在连续物理空间内
- 数据页最多保证16KB的连续空间,数据增多就没有足够的物理空间
- 对记录进行增删操作,将对目录项大面积移动
- InnoDB解决方案
- 复用存储用户的数据页来存储目录项,目录项记录(只有主键和页编号)
- 区分:记录头信息的record_type:1为目录项记录
- 例子
- 目录项越来越多-多级目录项页
- 结构简化-B+树
- B+树结构能存储多少数据
- 每叶子结点数据页存100条数据,每目录项数据页存100条记录
- 二层100*100,三层100*100*100,以此类推
- 一般三层
- 聚簇索引
- 两个特性
- 使用记录主键值的大小进行记录和页的排序
- 页内记录按主键大小排成一个单向链表
- 存放用户记录的页根据主键大小排成一个双向链表
- 存放目录项记录的页在同一层中也是根据目录项记录的主键大小排成一个双向链表
- B+树叶子节点存储完整的用户记录
- 使用记录主键值的大小进行记录和页的排序
- InnoDB存储引擎会自动创建聚簇索引
- 聚簇索引就是数据的存储方式,即:索引即数据,数据即索引
- 两个特性
- 二级索引-非聚簇索引
- 想用别的列作为搜索条件?
- 为别的列建索引
- 与聚簇索引不同点
- 使用记录c列的大小进行记录和页的排序
- B+树叶子结点存储的并不是完整的用户记录,而只是c列+主键这两个列的值
- 目录项:c列+主键值+页号
- 查找步骤(回表)
- 确定目录项记录页
- 确定真实数据页
- 确定具体的记录
- 确定对应主键值在聚簇索引中的具体用户记录
- 为保证目录项的唯一性,如果c2相同那么就可以对比主键来进行查询
- 想用别的列作为搜索条件?
- 联合索引:按照c2和c3作为排序规则,先按c2排序,相同再按c3排序,左匹配原则
- 例子
- 注
- 每条记录项都是由c2、c3和页号组成
- B+树叶子节点的用户记录由c2、c3和主键值组成
- B+树的生成
- 每个表默认生成B+树索引,一开始根节点没有记录
- 加入记录现存根节点
- 根节点用完,就会把记录复制到新的页,然后进行页分裂
- 根节点是不会移动的
- 上述简易方案假设目录项存储在连续物理空间内
- MySIAM索引
- 索引和数据分开,与innodb不同
- 表记录存于数据文件,但是不会分页,可以通过行号查找数据
- 没有对主键进行排序
- MyISAM把索引存入文件,但是叶子节点是主键值+行号,先通过索引找到行号然后再找记录(都是二级索引)
- 总结
- InnoDB存储引擎的索引是B+树,完整的用户记录存储在叶子节点,其他节点存储目录项记录
- InnoDB索引分两种
- 聚簇索引:以主键值的大小作为页和记录的排序规则,在叶子节点出存储的记录出包含表中所有的列
- 非聚簇索引:以索引列的大小作为页和记录的排序规则,在叶子节点存储的记录内容是索引列+主键。记录项记录由索引列的值、主键值和页号组成。
- B+树根节点不会移动
- MyIASM存储引擎的数据和索引分开存储,在叶子节点处存储的是列+行号
- 一个简单索引方案,为快速定位记录所在的数据页也建立一个目录
-
B+树索引的使用
- 回顾
-
每个索引都对应一棵 B+ 树, 分为多层,最底层是叶子节点,其余为内节点。所有用户记录都存储在 B+ 树的叶子节点,所有 目录项记录 都存储在内节点;
-
InnoDB存储引擎会自动为主键(如果没有它会自动帮我们添加)建立 聚簇索引,聚簇索引的叶子节点包含完整的用户记录;
-
可以为其他列建立 二级索引 ,其 叶子节点包含的用户记录由 索引列 + 主键 组成,若想通过二级索引 查找完整的用户记录,需通过 回表 操作,即在通过 二级索引找到主键值之后再到聚簇索引 中查找完整的用户记录;
-
B+树中每层节点都是按照索引列值从小到大排序而组成双向链表,且每个页内的记录(不论是用户记录还是目录项记录)都是按照索引列的值从小到大而形成单链表;
-
联合索引:页面和记录先按照联合索引前边的列排序,若该列值相同,再按照 联合索引 后边的列排;
-
通过索引查找记录是从 B+ 树的根节点开始,一层一层向下搜索。每个页面都按照索引列的值建立了Page Directory (页目录),所以在这些页面中查找非常快。
-
- 索引的代价
- 空间代价:一颗大B+树由许多数据页组成,将占据很大一片存储空间;
- 时间代价:增删改操作可能会对节点和记录的排序造成破坏,存储引擎需要额外的时间进行页分裂、页回收等操作,以维护节点和记录的排序。另外,执行查询语句前,会生成执行计划,分析不同索引的成本。若索引太多,可能会导致成本分析过程耗时长。
- B+树索引的使用
- 全值匹配
- 最左匹配
- 匹配列前缀
- 匹配范围值
- 用于排序(索引列)
- 用于分组(索引列)
- 回表的代价
- 从二级索引获取主键值
- 根据主键值在聚簇索引查询(随机I/O,性能低)
-
需要回表的记录越多,使用二级索引的性能就越低 ,甚至低于全表扫描
-
覆盖索引:
-
查询列表里只包含索引列,避免回表
-
- 更好使用索引
- 只为用于搜索、排序或分组的列创建索引
- 考虑列的基数(不重复数据的个数)
-
在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。
-
最好为基数大的列建立索引,为基数太小列的建立索引效果可能不好
-
- 索引列的类型尽量小
-
在定义表结构的时候要显式指定列的类型,以整数类型为例,有 TINYINT 、 MEDIUMINT 、 INT 、 BIGINT,占用的存储空间依次递增, 类型大小 指的就是 该类型表示的数据范围的大小 。能表示的整数范围也是依次递增,若建立整数列索引, 在表示的整数范围允许的情况下,尽量让索引列使用较小的类型 ,比如我们能使用 INT 就不要使用 BIGINT
-
数据类型越小,在查询时进行的比较操作越快(CPU层面);
-
数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘 I/O 带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
-
-
对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键适用更小的数据类型,也就意味着节省更多的存储空间和更高效的 I/O 。
-
- 索引字符串值的前缀:只对字符串的前几个字符进行索引
-
让索引列在比较表达式中单独出现
- 若索引列在比较表达式中不是以单独列的形式出现,而是以某个表达式,或者函数调用形式出现的话,是用不到索引的。
- 主键插入顺序
-
对于使用 InnoDB 存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在 聚簇索引 的叶子节点的。而记录又是存储在数据页中的,数据页和记录又是按照记录主键值从小到大进行排序,若插入的记录的主键值是依次增大的话,那每插满一个数据页就换到下一个数据页继续插,而若主键值忽大忽小的话,则会产生较多的页分裂操作-性能损耗
-
建议: 让主键具有 AUTO_INCREMENT, 让存储引擎自己为表生成主键而不是手动插入
-
- 避免冗余和重复索引
- 总结
-
B+ 树索引在空间和时间上都有代价,谨慎使用。
-
B+ 树索引适用于下边这些情况:全值匹配、匹配左边的列、匹配范围值、精确匹配某一列并范围匹配另外一列、用于排序、用于分组
-
在使用索引时需要注意下边这些事项:
- 只为用于搜索、排序或分组的列创建索引
- 为列的基数大的列创建索引
- 索引列的类型尽量小
- 可以只对字符串值的前缀建立索引
- 只有索引列在比较表达式中单独出现才可以适用索引
- 为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有 AUTO_INCREMENT 属性。
- 定位并删除表中的重复和冗余索引
- 尽量使用覆盖索引进行查询,避免回表带来的性能损耗。
-
- 回顾
-
数据目录
- 文件系统
-
InnoDB存储引擎把表存储在磁盘上的,而OS通过文件系统管理磁盘。即: 像 InnoDB 、 MyISAM 这样的存储引擎都是把表存储在文件系统上 。读取数据,存储引擎会从文件系统中把数据读出来并返回。写入数据的,存储引擎会把这些数据又写回文件系统。
-
- mysql数据目录:MySQL服务器程序在启动时会到文件系统的某个目录下加载文件,在运行过程中产生的数据也都会存储到这个目录下的某些文件中,这目录就称为数据目录
- 数据目录与安装目录的区别:数据目录是存储 MySQL 在运行过程中产生的数据
- 查看:SHOW VARIABLES LIKE 'datadir';
- 数据目录的结构
- 数据库在文件系统中的表示
- 在数据目录下创建一个和数据库名同名的子目录
- 在该子目录下创建一个名为 db.opt 的文件,包含该数据库的各种属性,比方该数据库的字符集和比较规则
- 表在文件系统中的表示
- 表结构:数据库子目录下创建了一个描述表结构的文件——表名.frm
- 表中数据:存放在表空间下的页
- 数据库在文件系统中的表示
- 表空间
- 系统表空间
- 独立表空间:使用 独立表空间 来存储表数据,数据库子目录下创建——表名.ibd
- 文件系统
-
InnoDB表空间
- 回顾
- 各种类型的页:InnoDB以页为单位管理存储空间
-
任何类型的页都会包含两个部分
-
File Header:记录页面的一些通用信息
-
File Trailer:校验页是否完整,保证从内存到磁盘刷新时内容的一致性。
-
- 独立表空间
- 区extend:页太多,对于16KB的页来说,物理上连续的64个页就是一个区,一个区默认占用1MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。
- 每个区的组成
-
extend0 最开始的 3 个页面的类型是固定的 分别是:
-
FSP_HDR 类型:这个类型的页面是用来登记整个表空间的一些整体属性以及本组所有的 区 ,也就是extent 0 ~ extent 255 这 256个区的属性,整个表空间只有一个 FSP_HDR 类型的页面。
-
IBUF_BITMAP 类型:这个类型的页面是存储本组所有区的所有页面关于 INSERT BUFFER 的信息。
-
INODE 类型:这个类型的页面存储了许多称为 INODE 的数据结构。
-
-
其余各组最开始的2个页面的类型是固定的,也就是说 extent 256 、 extent 512 这些区最开始的2个页面的类型是固定的,分别是:
-
XDES 类型:全称是 extent descriptor ,用来登记本组256个区的属性,也就是说对于在 extent 256 区中的该类型页面存储的就是 extent 256 ~ extent 511 这些区的属性,上边介绍的 FSP_HDR 类型的页面其实和 XDES 类型的页面的作用类似,只不过 FSP_HDR 类型的页面还会额外存储一些表空间的属性。
- IBUF_BITMAP 类型
-
- 表空间被划分为许多连续的区 ,每个区默认由64个页组成,每256个区划分为一组,每个组的最开始的几个页面类型是固定的。
- 区的分类
- 空闲的区FREE:现在还没有用到这个区中的任何页面。
- 有剩余空间的碎片区FREE_FRAG:表示碎片区中还有可用的页面。
- 没有剩余空间的碎片区FULL_FRAG:表示碎片区中的所有页面都被使用,没有空闲页面。
- 附属于某个段的区FSEG:每一个索引都可以分为叶子节点段和非叶子节点段,除此之外InnoDB还会另外定义一些特殊作用的段,在这些段中的数据量很大时将使用区来作为基本的分配单位。
-
为了方便管理这些区,设计了一个称为 XDES Entry 的结构(全称就是Extent Descriptor Entry),每一个区都对应着一个 XDES Entry 结构
- Segment ID:每一个段都有唯一ID,Segment ID表示该区所在的段。
- List Node:这个部分可以将若干个 XDES Entry 结构串联成一个链表
- State:表明区的状态。FREE 、 FREE_FRAG 、 FULL_FRAG和 FSEG
- Page State BitMap
-
XDES Entry 链表
- 段中插入数据,当段中数据较少时,先查看表空间中是否有状态为 FREE_FRAG 的区,若有那么从该区中取一些零碎的页把数据插进去;否则到表空间下申请一个状态为 FREE 的区,把该区的状态变为 FREE_FRAG ,然后从该新申请的区中取一些零碎的页把数据插进去。之后不同的段使用零碎页的时候都会从该区中取,直到该区中没有空闲空间,然后该区的状态就变成了FULL_FRAG 。
- 如何知道表空间哪些区是FREE、FREE_FRAG、FULL_FRAG的
- 把状态为 FREE 的区对应的 XDES Entry 结构通过 List Node 连接成一个链表,称之为 FREE 链表。
- 把状态为 FREE_FRAG 的区对应的 XDES Entry 结构通过 List Node 连接成一个链表,称之为 FREE_FRAG 链表。
- 把状态为 FULL_FRAG 的区对应的 XDES Entry 结构通过 List Node 连接成一个链表,称之为 FULL_FRAG 链表
- 段中
-
FREE 链表:同一个段中,所有页面都是空闲的区对应的 XDES Entry 结构会被加入到这个链表。注意和直属于表空间的 FREE 链表区别开了,此处的 FREE 链表是附属于某个段的。
-
NOT_FULL 链表:同一个段中,仍有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。
-
FULL 链表:同一个段中,已经没有空闲空间的区对应的 XDES Entry 结构会被加入到这个链表。
-
注:每一个索引都对应两个段,每个段都会维护上述3个链表!
-
- 段:存放叶子节点的区的集合就算是一个 段 ( segment ),存放非叶子节点的区的集合也算是一个 段 。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。
- 段分配空间策略
-
在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的。
-
当某个段已经占用了 32 个碎片区页面之后,就会以完整的区为单位来分配存储空间。
-
- 段分配空间策略
- 区extend:页太多,对于16KB的页来说,物理上连续的64个页就是一个区,一个区默认占用1MB空间大小。不论是系统表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组。
- 系统表空间
- 总结
- 回顾
三、数据访问、决策和优化
-
单表访问
- 访问方法:MySQL 执行查询语句的方式
- const:通过主键列来定位一条记录,SELECT * FROM single_table WHERE id = 1438;MySQL 会直接利用主键值在聚簇索引中定位对应的用户记录
- ref:对某个普通的二级索引列与常数进行等值比较,如:SELECT * FROM single_table WHERE key1 = 'abc';
- ref_or_null:查询二级索引为null的记录
- range:利用索引(聚簇、二级)进行范围匹配
- index:采用遍历二级索引记录
- all:全表扫描
- 索引合并:一个查询中使用到多个二级索引
- Intersection合并,取交集
-
Union 合并,取并集
- Sort-Union合并:按照二级索引记录的主键值进行排序,之后按照 Union 索引合并方式执行的方式称之为 Sort-Union 索引合并
- 总结
- 访问方法:MySQL 执行查询语句的方式
-
连接原理JOIN
- 过程
-
首先确定第一个需要查询的表,这个表称之为驱动表 。选取代价最小的那种访问方法去执行单表查询语句;
-
针对1中从驱动表产生的结果集中的每一条记录,分别需要到被驱动 表中查找匹配的记录,所谓匹配的记录,指的是符合过滤条件的记录。
-
- 内连接ui外连接
- 内连接:对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集
- 外连接:驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集
- 左连接:选取左侧的表为驱动表
- 右连接:选取右侧的表为驱动表
-
WHERE: 不论是内连接还是外连接,凡是不符合 WHERE 子句中的过滤条件的记录都不会被加入最后的结果集。
-
ON:对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配 ON 子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用 NULL 值填充。内连接中, MySQL 会把它和 WHERE 子句一样对待,也就是说: 内连接中的 WHERE 子句和 ON 子句是等价的 。
- 连接原理
- 嵌套循环连接:驱动表只访问一次,但被驱动表却可能被多次访问,访问次数取决于对驱动表执行单表查询后的结果集中的记录条数
- 总结
- 过程
-
基于成本的优化
- 成本:I/O成本;CPU成本
- 基于成本优化步骤
-
根据搜索条件,找出所有可能使用的索引
-
计算全表扫描的代价
-
计算使用不同索引执行查询的代价
-
对比各种执行方案的代价,找出成本最低的那一个
-
- 连接查询成本
-
单次查询驱动表的成本
-
多次查询被驱动表的成本
-
-
InnoDB统计数据
- 方式(InnoDB 默认是以表为单位来收集和存储统计数据)
- 永久性:这种统计数据存储在磁盘上,也就是服务器重启之后这些统计数据还在
- 非永久性:这种统计数据存储在内存中
- 总结
-
InnoDB 以表为单位来收集统计数据,这些统计数据可以是基于磁盘的永久性统计数据,也可以是基于内存 的非永久性统计数据。
-
innodb_stats_persistent 控制着使用永久性统计数据还是非永久性统计数据
-
innodb_stats_persistent_sample_pages 控制着永久性统计数据的采样页面数量;
-
innodb_stats_transient_sample_pages 控制着非永久性统计数据的采样页面数量;
-
- 方式(InnoDB 默认是以表为单位来收集和存储统计数据)
-
Mysql基于规则的优化
- 查询重写
- 条件简化
- 外连接消除
- 子查询优化
- 总结
- 查询重写
-
Explain
- Exlpain:一条查询语句在经过 MySQL 查询优化器的基于成本和规则的优化会后生成一个执行计划 ,展示了接下来具体执行查询的方式
- 使用:Explain + 查询语句
- 各类详解
- 输出列
- table:涉及的表名
- id
- 简单查询:每出现一个 SELECT 关键字就会为它分配一个唯一的 id 值
- 连接查询:连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的,出现在前边的表表示驱动表,出现在后边的表表示被驱动表
- 子查询:每个 SELECT 关键字都会对应一个唯一的 id 值;但查询优化器可能对查询语句进行重写,从而转换为连接查询-id相同
- UNION:会创建临时表,对结果去重。多一条执行计划,id 为 NULL 表明这个临时表是为了合并两个查询的结果集而创建的。UNION ALL不去重,就不涉及临时表。
- select_type
- partitions:分区
- type:表明访问方法
- system:当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory
- const:根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是 const
- eq_ref:在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问
- ref:当通过普通的二级索引列与常量进行等值匹配时来查询某个表
- fulltext:全文索引
- ref_or_null:当对普通二级索引进行等值匹配查询,该索引列的值也可以是 NULL 值
- index_merge:索引合并
- unique_subquery:针对在一些包含 IN 子查询的查询语 句中,如果查询优化器决定将 IN 子查询转换为 EXISTS 子查询,而且子查询可以使用到主键进行等值匹配
- index_subquery:index_subquery 与 unique_subquery 类似,只不过访问子查询中的表时使用的是普通的索引
- range:如果使用索引获取某些 范围区间 的记录,那么就可能使用到 range 访问方法
- index:可以使用索引覆盖,但需要扫描全部的索引记录时
- ALL:全表扫描
- possible_keys:表示在查询语句中,执行单表查询时可能用到的索引
-
在使用 index 访问方法来查询某个表时, possible_keys 列是空的
-
possible_keys列中的值并不是越多越好,可能使用的索引越多,查询优化器计算查询成本时就得花费更长时间,所以如果可以的话,尽量删除那些用不到的索引
-
- key:表示实际用到的索引
- key_len:表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度
- ref:当使用索引列等值匹配的条件去执行查询时,展示与索引列作等值匹配的是什么,比如只是一个常数或者是某个列
- rows
-
若查询优化器使用全表扫描的方式对某个表执行查询时, rows列代表预计需要扫描的行数
-
若使用索引来执行查询时, rows 列就代表预计扫描的索引记录行数
-
- filtered
- Extra:额外信息
- Using Index
- Using temporary
- Json格式的Explain:查看执行计划花费的成本
- 使用:Explain FORMAT=JSON 具体SQL
- Extended Explain:用 EXPLAIN 语句查看执行计划后,可使用 SHOW WARNINGS 语句查看有关的一些扩展信息:如查询优化器重写的语句(参考)
- Exlpain:一条查询语句在经过 MySQL 查询优化器的基于成本和规则的优化会后生成一个执行计划 ,展示了接下来具体执行查询的方式
-
optimizer trace
- 简介:查看优化器生成执行计划的整个过程
- 过程
-
Buffer Pool
- 缓存的重要性:访问一个页的一条记录,需要先把整个页的数据加载到内存中。然后进行读写访问,之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,若再有请求访问该页面时,可省去磁盘 IO 的开销。
- Buffer Pool
- 是什么?
- 为缓存磁盘中的页,在 MySQL 服务器启动的时候向操作系统申请的一片连续内存。默认128M,或者启动配置innodb_buffer_pool_size(最小5M)
- 组成
- 为每个缓存页都创建控制信息,包括:页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息
- free链表
- 记录Buffer Pool哪些缓存页是可用的-将所有空闲缓存页对应的控制块作为一个节点放到一个链表中(空闲链表)
-
初始化的 Buffer Pool 中所有的缓存页都是空闲的,所有缓存页对应的控制块都会被加入到 free链表中
- 缓存页的hash处理
- 如何确定一个页在不在Buffer Pool中,采用表空间 + 页号为key,缓存页为value,存储到哈希表中
- flush链表
- 脏页:某个缓存页的数据被修改,那这个页与磁盘不一致——脏页
- 为了确定哪些页是脏页,将修改过的缓存页对应的控制块作为节点加入到flush链表
- LRU链表
- Buffer Pool缓存内存有限,就需要淘汰最近最少使用的缓存页
- 简单LRU链表:将缓存页对应的控制块加入到LRU链表中,使用到某个缓存页,就把该缓存页调整到LRU链表的头部,LRU链表尾部就是最近最少使用的缓存页,当Buffer Pool中的空闲缓存页使用完时,到LRU链表的尾部进行缓存页淘汰
- 简单LRU存在的问题:
-
加载到 Buffer Pool 中的页不一定被用到。
-
若很多使用频率偏低的页被同时加载到 Buffer Pool 时,会把那些使用频率非常高的页从Buffer Pool 中淘汰掉。
-
- 解决方案:LRU链表 按照一定比例分成两截
- 一部分存储使用频率非常高的缓存页,这部分链表叫做热数据也称young区域。
-
另一部分存储使用频率不是很高的缓存页,这部分链表叫做 冷数据也 称old区域。
-
注:按照某个比例将 LRU链表分成两半的,随着程序的运行某个节点所属的区域也可能发生变化 。对于 InnoDB 存储引擎来说,我们可以通过系统变量 innodb_old_blocks_pct 的值来确定 old 区域的比例。
- 在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录此次访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被 从old区域移动到young区域的头部,否则将它移动到young区域的头部。间隔时间是由系统变量innodb_old_blocks_time 控制。
- 小结:将 LRU 链表划分为 young 和 old 区,又添加系统变量innodb_old_blocks_time,使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,用不到的预读页面以及全表扫描的页面都只会被放到 old 区域,而不影响 young 区域中的缓存页。
- 进一步优化
-
每次访问都将缓存页移到 LRU链表 的头部,开销过大
-
只有被访问的缓存页位于 young 区域的 1/4 的后边,才被移动到 LRU链表 头部,以降低调整 LRU链表 的频率。
-
- 刷新脏页到磁盘:采用后台线程每隔一段时间将脏页刷新到磁盘,从而不影响用户线程处理正常的请求
- 从 LRU链表 的冷数据中刷新一部分页面到磁盘。从LRU尾部链表扫描,发现脏页,刷新到磁盘。BUF_FLUSH_LRU
-
从 flush链表 中刷新一部分页面到磁盘——BUF_FLUSH_LIST
- innodb_buffer_pool_chunk_size:5.75之后以chunk为单位向操作系统申请空间。一个 Buffer Pool 实例由若干个 chunk 组成的,一个 chunk 代表一片连续的内存空间,包含若干缓存页与其对应的控制块。以达到可在程序运行期间修改Buffer Pool大小的目的
- 是什么?
- 总结
- 磁盘太慢,用内存作为缓存很有必要。
- Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间,可以通过innodb_buffer_pool_size 来调整它的大小。
- Buffer Pool 向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后, Buffer Pool 剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为碎片 。
- InnoDB 使用了许多 链表 来管理 Buffer Pool 。5. free链表 中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到 Buffer Pool 时,会从 free链表中寻找空闲的缓存页。
- 为了快速定位某个页是否被加载到 Buffer Pool ,使用 表空间号 + 页号 作为 key ,缓存页作为 value,建立哈希表。
- 在 Buffer Pool 中被修改的页称为 脏页 ,脏页并不是立即刷新,而是被加入到 flush链表 中,待之后的某个时刻同步到磁盘上。
- LRU链表 分为 young 和 old 两个区域,可以通过 innodb_old_blocks_pct 来调节 old 区域所占的比例。首次从磁盘上加载到 Buffer Pool 的页会被放到 old 区域的头部,在 innodb_old_blocks_time 间隔时间内访问该页不会把它移动到 young 区域头部。在 Buffer Pool 没有可用的空闲缓存页时,会首先淘汰掉 old 区域的一些页。
四、InnoDB存储引擎的机制
-
事务
-
特性
- 原子性Atomicity
- 隔离性Isolation
- 一致性Consistency
- 原子性和隔离性对一致性产生影响
- 持久性Durabilty
-
需要保证ACID的一个或多个数据库操作称之为一个事务
-
事务的状态
-
活跃的active: 事务对应的数据库操作正在执行中
-
部分提交partially committed: 当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时
- 失败的failed:当事务处在活动的或者部分提交的状态时,可能遇到了某些错误而无法继续执行
-
终止的aborted: 如果事务执行了半截而变为 失败的 状态,就要回滚之前的操作
-
提交的commited: 一个处在 部分提交 状态的事务将修改过的数据同步到磁盘
-
状态图
-
只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算结束
-
-
mysql事务语法
-
开启事务BEGIN
-
START TRANSACTION同理
-
READ ONLY :标识当前事务是只读事务,数据库操作只能读取数据
-
READ WRITE :标识当前事务读写事务,数据库操作既可以读取数据,也可以修改数据
- WITH CONSISTENT SNAPSHOT :启动一致性读
-
-
-
提交事务COMMIT
- 中止事务ROLLBACK
- 自动提交:默认值为 ON ,若不显式事务语句开启事务,那么每一条语句都是独立的事务
- 关闭自动提交
- 显示使用事务语句开启事务
- 修改系统变量autocommit
- 关闭自动提交
- 隐式提交:默写语句会自动提交事务
-
定义或修改数据库对象的数据定义语言DDL
-
修改表ALTER 、 CREATE 、 DROP 、 GRANT 、 RENAME
-
关于 MySQL 复制的一些语句
-
- 保存点:在事务对应的数据库语句中打几个保存点,在调用 ROLLBACK 语句时可回滚到指定点;
- 创建:SAVEPOINT 保存点名称;
- 删除:RELEASE SAVEPOINT 保存点名称;
-
- 总结
-
-
redo日志
- 是什么
- 占用空间小
- redo log顺序写入磁盘
- redo log格式:本质上只是记录事务对数据库做了哪些修改
- 图示
-
type :该条 redo 日志的类型
-
space ID :表空间ID
-
page number :页号
-
data :redo log的具体内容。
- 类型
- 简单类型:只需记录在某个页面的某个偏移量处修改了几个字节的值以及具体修改的内容
- 复杂类型:
- 小结:redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来。
- redo log写入过程
- 总结
- 是什么
-
undo日志
- 是什么:为了回滚而记录的撤销日志
- 事务id:如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的事务id
-
对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个 事务id ,否则的话是不分配 事务id 的。
-
对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行、删、改操作时才会为这个事务分配一个 事务id ,否则的话也是不分配 事务id 的。
-
- 事务id生成:服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id 时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。这样就可以保证整个系统中分配的事务id值是一个递增的数字。先被分配 id 的事务得到的是较小的事务id,后被分配 id 的事务得到的是较大的事务id 。
- trx_id隐藏列:某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id
- undo日志格式
- TRX_UNDO_INSERT_REC
- 总结:
-
事务隔离级别和MVCC
- 并发问题
- 脏写:一个事务修改了另一个未提交事务修改过的数据
- 脏读:一个事务读到了另一个未提交事务修改过的数据
- 不可重复读:一个事务在读取某个数据时,若另一个事务修改了这个数据并提交,第一个事务再次读取改数据时,得到不同的数据
- 幻读:一个事务查询某个范围数据,另一个事务在同范围插入记录,再次查询读到另一个事务插入的数据。强调读取到之前没有的数据
- 数据库隔离级别(不允许暂脏写)
- READ UNCOMMITTED,可能发生脏读、不可重复读和幻读
- READ COMMITTED,可能发生不可重复读和幻读,解决脏读
- REPEATABLE READ,可能发生幻读问题,解决脏读和不可重复读
- SERIALIZABLE,各种问题都不会发生
- mysql支持上述四种隔离级别
- Mysql在REPEATABLE READ隔离级别下,可禁止幻读
- Mysql默认隔离级别为REPEATABLE READ,可修改
- SET [GLOBAL|SESSION] TRANSACTION ISOLATION level;
- MVCC原理
- InnoDB存储引擎聚簇索引记录都包含
- trx_id:每一个事务对聚簇索引记录进行修改时,都会把该事务的事务id赋值给trx_id
- roll_pointer:每次对聚簇1索引记录进行修改时,都会把旧的版本写入undo 日志中,然后roll_pointer相当于指针,可通过它找到该记录修改前的信息
- 图示
- 每次对记录进行改动,都会记录一条undo日志,每条undo日志都有一个roll_pointer属性(INSERT操作除外),可将这些undo日志连接起来,串成一个版本链,如下图:
- ReadView:对于使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本;对于使用SERIALIZABLE 隔离级别的事务来说,使用加锁的方式来访问记录;对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此,提出 ReadView 的概念
- 主要包含4个比较重要的内容:
-
m_ids: 表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表。
-
min_trx_id: 表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id, 也就是 m_ids中的最小值。
-
max_trx_id:表示生成 ReadView 时系统中分配给下一个事务的 id 值
- creator_trx_id:表示生成该ReadView的事务的事务id 。
-
- 如何判断记录的某个版本是否可见:
-
如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
-
如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id,表明生成该版本的事务在当前事务生成 ReadView前已经提交,所以该版本可以被当前事务访问。
-
如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id,表明生成该版本的事务在当前事务生成 ReadView后才开启,所以该版本不可以被当前事务访问。
-
如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下trx_id 是不是在m _ids 列表中,如果在说明创建 ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
-
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
-
-
在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 区别就是 生成 ReadView的时机不同 。
-
READ COMMITTED, 在每次查询开始时都会生成一个独立的 ReadView 。
-
REPEATABLE READ,只会在第一次执行查询语句时生成一个 ReadView
-
- 主要包含4个比较重要的内容:
- InnoDB存储引擎聚簇索引记录都包含
- PURGE
-
insert undo 在事务提交之后就可以被释放掉了,而 update undo 由于还需要支持 MVCC ,不能立即删除掉。
-
为了支持 MVCC ,对于 delete mark 操作来说,仅仅是在记录上打一个删除标记,并没有真正将它删除掉。
-
随着系统的运行,在确定系统中包含最早产生的那个 ReadView 的事务不会再访问某些 update undo日志以及被打了删除标记的记录后,有一个后台运行的purge线程会把它们真正的删除掉。
-
- 总结
-
MVCC ( Multi-Version Concurrency Control,多版本并发控制)指的就是在使用 READ COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写、 写-读 操作并发执行,从而提升系统性能。
-
READ COMMITTD、REPEATABLE READ 这两个隔离级别的区别在于: 生成 ReadView 的时机不同, READ COMMITTD在每一次进行 SELECT 操作前都会生成一个 ReadView ,而 REPEATABLE READ 只在第一次进行普通 SELECT操作前生成一个 ReadView ,之后的查询操作都重复使用 ReadView 。
-
- 并发问题
-
锁
- 解决并发事务带来的问题
- 事务并发访问相同记录
- 读读:无影响
- 写写:在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。锁其实是一个内存中的结构。
-
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在生成一个锁结构与之关联
- 锁结构中的信息
-
trx信息:代表这个锁结构是哪个事务生成的。
-
is_waiting :代表当前事务是否在等待。
-
-
当事务T1改动了这条记录后,就生成了一个锁结构与之关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。在事务 T1提交之前,另一个事务T2想对该记录做改动,那么先去看看有没有 锁结构与该记录关联,发现存在锁结构与之关联后,然后也生成了一个锁结构与该记录关联,不过锁结构的is_waiting属性值为true,表示当前事务需要等待,称之为获取锁失败,或者加锁失败。
-
在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务 T2 就获取到锁。
-
概念总结:
-
不加锁:指不需要在内存中生成对应的 锁结构,可直接执行;
-
获取锁成功:指在内存中生成了对应的 锁结构 ,而且锁结构的i s_waiting 属性为 false,事务可以继续执行操作。
-
获取锁失败:指在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,事务需要等待,不可以继续执行操作。
-
-
- 读写:可能发生脏读、不可重复读、幻读。
- 幻读问题的产生是因为某个事务读了一个范围的记录,之后别的事务在该范围内插入了新记录,该事务再次读取该范围的记录时,可以读到新插入的记录,所以幻读问题准确的说并不是因为读取和写入一条相同记录而产生的
- sql标准规定的隔离级别
-
READ UNCOMMITTED ,存在 脏读 、 不可重复读 、 幻读
-
READ COMMITTED,存在不可重复读、幻读,解决脏读
-
REPEATABLE READ ,存在 幻读 ,解决 脏读 和 不可重复读
-
SERIALIZABLE ,上述问题都不可能发生
-
-
MySQL在REPEATABLE READ 隔离级别就已经解决了 幻读 问题。
-
读操作利用多版本并发控制(MVCC),写操作进行 加锁
- REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。
-
读、写操作都采用 加锁 的方式。
-
- 一致性读(Consistent Reads):事务利用 MVCC 进行的读取操作称之为一致性读 ,或者一致性无锁读 ,有的地方也称之为快照读 。一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动
- 锁定读(Loking Reads)(加锁方式)
- 共享锁:在事务要读取一条记录时,需要先获取该记录的 S锁 。
- 独占锁:在事务要改动一条记录时,需要先获取该记录的 X锁 。
-
假如事务 T1 首先获取了一条记录的 S锁 之后,事务 T2 接着也要访问这条记录:
-
如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,也就意味着事务T1和T2在该记录上同时持有S锁 。如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉。如果事务T1首先获取了一条记录的X锁 之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。
-
- 锁定读语句:采用加锁方式解决脏读、不可重复读、幻读这些问题时,读取一条记录时需要获取该记录的S锁,其实这是不严谨的,有时候想在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此提出了两种比较特殊的 SELECT 语句格式:
-
对读取的记录加 S锁:SELECT ... LOCK IN SHARE MODE;如果当前事务执行了该语句,那么它会为读取到的记录加 S锁 ,这样允许别的事务继续获取这些记录的 S锁,但是不能获取这些记录的 X锁 。如果别的事务想要获取这些记录的X锁 ,那么它们会阻 塞,直到当前事务提交之后将这些记录上的 S锁释放掉。
-
对读取的记录加X锁:SELECT ... FOR UPDATE; 如果当前事务执行该语句,那么它会为读取到的记录加 X锁 ,既不允许别的事务获取该记录的 S锁,也不允许获取该记录的X锁。如果别的事务想要获取该记录的S锁或者 X锁,那么它们会 阻塞,直到当前事务提交将这些记录上的 X锁 释放掉。
-
-
写操作
-
DELETE:对一条记录做 DELETE 操作的过程其实是先在 B+ 树中定位到这条记录的位置,然后获取这条记录的X锁 ,然后再执行 delete mark 操作。也可以把这个定位待删除记录在 B+树中位置的过程看成是一个获取 X锁 的 锁定读 。
-
UPDATE:在对一条记录做 UPDATE操作时分为三种情况:如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在 B+ 树中定位到这条记录的位置,然后再获取一下记录的 X锁,最后在原记录的位置进行修改操作。其实我们也可以把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+ 树中定位到这条记录的位置,然后获取一下记录的 X锁,将该记录彻底删除掉,最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取X锁 的 锁定读 ,新插入的记录由 INSERT 操作提供的 隐式锁进行保护。如果修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT操作,加锁操作就需要按照 DELETE 和 INSERT 的规则进行了。
-
INSERT:一般情况下,新插入一条记录的操作并不加锁, 通过一种称之为 隐式锁保护这条新插入的记录在本事务提交前不被别的事务访问。
-
- 事务并发访问相同记录
- 多粒度锁:上述锁都是针对记录的,也可以被称之为行锁,对一条记录加锁影响的也只是这条记录而已,这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,称之为表锁,对一个表加锁影响整个表中的记录,锁的粒度比较粗。给表加的锁也可以分为 共享锁( S锁 )和独占锁( X锁 )
- 对表加锁前,先确认行记录是否有锁
-
意向共享锁,英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S锁 时,需要先在表级别加一个 IS锁 。
-
意向独占锁,英文名: Intention Exclusive Lock ,简称 IX锁 。当事务准备在某条记录上加 X锁时,需要先在表级别加一个 IX锁 。
- 小结:IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录
-
- 对表加锁前,先确认行记录是否有锁
- Mysql中的行锁与表锁
- InnoDB中的锁
- 表级锁
- IS、IX
- AUTO-INC锁:执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续
- 行级锁
-
Record Locks:仅仅把一条记录锁上
-
Gap Locks: 仅仅是为了防止插入幻影记录
- Next-Key Locks:本质就是一个记录锁和一个 gap锁的合体,它既能保护该条记录,又能阻止别的事务将记录插入被保护记录前边的间隙
-
Insert Intention Locks :
- 隐式锁:
-
- 表级锁
- 锁的内存结构:
-
锁所在的事务信息:哪个事务生成了这个锁结构
-
索引信息:对于 行锁 来说,需要记录加锁的记录是属于哪个索引
-
表锁/行锁信息:表锁结构 和 行锁结构在这个位置的内容是不同的:
-
表锁:记载着这是对哪个表加的锁,还有其他的一些信息。
-
行锁:记载了三个重要的信息:
-
Space ID:记录所在表空间
-
Page Number:记录所在页号
-
n_bits:对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。
-
-
-
type_mode:这是一个 32 位的数, lock_mode 、 lock_type和rec_lock_type
-
- InnoDB中的锁
- 语句加锁分析
- 普通的SELECT
- READ UNCOMMITTED,不加锁,直接读取最新版本,存在脏读、不可重复读、幻读
- READ COMMITTED,不加锁,在每次执行SELECT时,生成ReadView,避免脏读,存在不可重复度、幻读
- REPEATABLE,不加锁,只在第一次执行普通SELECT生成ReadVies,避免脏读、不可重复读、幻读
- SERIALIZABLE:
- 禁止自动提交:普通SELECT转换成获取S锁
- 启动自动提交:采用ReadView读取记录,执行一条语句,不会出现不可重复读和幻读
- 锁定读语句
- SELECT ...... LOCK IN SHARE MODE;
- SELECT ...... FOR UPDATE;
- UPDATE
- DELETE
- 半一致性读语句:介于一致性读和锁定读之间。当隔离级别不大于READ COMMITTED且执行UPDATE时将采用。当UPDATE读取到已经被其他事务加了X锁的记录时,InnoDB将该记录的最新版本读出来,与搜索条件相匹配。若不匹配,则不加锁,跳到下一条记录;如果匹配,进行加锁。让UPDATE语句尽量少被阻塞。
- INSERT语句
- 普通的SELECT
- 查看事务加锁情况
- 死锁
- 总结
- MVCC和加锁是解决并发事务带来一致性问题的两种方式
- S锁与S锁兼容,X锁与X锁、S锁都不兼容
- 事务利用MVCC进行的读取操作称之为一致性读
- 在读取记录前加锁的读取操作称之为锁定读。
- SELECT ...... LOCK IN SHARE MODE 记录加S锁
- SELECT ......FOR UPDATE 记录加X锁
- INSERT语句一般不需要在内存生成锁结构,仅依靠隐式锁保护插入的记录。UPDATE和DELETE在执行过程中,在B+树定位到待改动记录并给记录加锁的过程也算是锁读定
- InnoDB行锁
- Record Lock:只对记录加锁
- Gap Record:锁住记录前面的间隙,防止其他事务向该间隙插入记录
- Next-Key Lock:Record Lock + Gap Record
- Insert Intention Lock
- 隐式锁:依靠记录的trx-id属性保护不被其他事务改动记录
- 死锁:不同事务由于互相持有对方需要的锁而导致事务都无法继续执行。InnoDB会选择较小事务进行回滚。可以通过查看死锁日志进行分析。
- 解决并发事务带来的问题