MySQL基础学习(待整理)

MySQL 简介

学习路径

MySQL 安装

  1. 卸载预安装的mariadb

    bash 复制代码
    rpm -qa | grep mariadb
    rpm -e --nodeps mariadb-libs
  2. 安装网络工具

    bash 复制代码
    yum -y install net-tools
    yum -y install libaio
  3. 下载rpm-bundle.tar安装包,并解压,使用rpm进行安装

    bash 复制代码
    rpm -ivh \
    mysql-community-common-5.7.28-1.el7.x86_64.rpm \
    mysql-community-libs-5.7.28-1.el7.x86_64.rpm \
    mysql-community-libs-compat-5.7.28-1.el7.x86_64.rpm \
    mysql-community-server-5.7.28-1.el7.x86_64.rpm
  4. 初始化mysql

    bash 复制代码
    mysqld --initialize
  5. 更改所属组

    bash 复制代码
    chown mysql:mysql /var/lib/mysql -R
  6. 启动 MySQL 服务

    bash 复制代码
    systemctl start mysqld.service
  7. 查看生成的初始密码

    sh 复制代码
    cat /var/log/mysqld.log | grep password
  8. 登录 MySQL,修改密码并设置允许远程访问

    sql 复制代码
    # 修改密码
    alter user user() identified by "root";
    
    # 设置允许远程访问
    use mysql;
    grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option;
    flush privileges;
  9. 设置 MySQL 服务开机自启动

    sh 复制代码
    systemctl enable mysqld
  10. 查看开机自启动是否设置成功

    sh 复制代码
  11. 退出 MySQL 服务

    sh 复制代码
    systemctl stop mysqld.service

启动mysql

  • 启动mysql服务

    shell 复制代码
    service mysql start
    或
    systemctl start mysqld.service
  • 查看mysql的初始密码

    shell 复制代码
    grep "password" /var/log/mysqld.log
  • 登录数据库

    shell 复制代码
    mysql -uroot -p
  • 修改mysql的默认密码

    说明 新密码设置的时候如果设置的过于简单会报错,必须同时包含大小写英文字母、数字和特殊符号中的三类字符。

    shell 复制代码
    ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
  • 执行如下命令,创建wordpress库

    shell 复制代码
    create database wordpress; 
  • 查看是否创建成功

    shell 复制代码
    show database
  • 输入exit退出数据库

修改MySQL配置文件(拷贝一份,不修改原配置文件)

shell 复制代码
cp my-huge.cnf /etc/my.cnf #5.5版本
cp my-default.cnf /etc/my.cnf #5.6版本配置文件名称不一样

如果先建表, 再配置文件改为utf-8, 还是会导致乱码

配置文件介绍

  • 二进制日志文件log-bin(主从复制)

  • 错误日志log-error

    默认是关闭, 记录严重的警告和错误信息, 每次启动和关闭的详细信息

  • 查询日志log

    默认关闭, 记录查询的sql语句,如果开启会降低mysql的整体性能,

  • 数据文件

    myi文件(存放表索引)

    myd文件(存放表数据)

    myfrm(存放表结构)

  • 配置文件路径(linux下是在/etc/my.cnf, 而windows下是my.ini)

基础篇

行式数据库和列式数据库

  1. OLAP分析型数据库(列式数据库,数据分析)

    计算均值,比较大小,求最大最小值等分析数据的时候速度快,是将一个属性的数据存储到一起。只有取出所有的属性值之后才得到某一个对象的整体信息

  2. OLTP事务型数据库(行式数据库,增删改查)

    将一个个对象作为存储的单位,在取出一个对象的所有信息的时候速度快。只有当去除所有的对象的属性值之后才能对数据进行求均值,比大小等等

SQL

SQL分类

  • DDL(Data Definition Language,数据定义语言)

    用来定义数据库对象:数据库、表、字段

  • DML(Data Manipulation Language,数据操作语言)

    用来对表中的数据进行增删改操作

  • DQL(Data Query Language,数据查询语言)

    用来对表中的数据进行查询操作

  • DCL(Data Control Language,数据控制语言)

    用来创建数据库用户、控制数据库的访问权限

DDL 数据定义语言

  • 增(创建数据库)

    CREATE DATABASE [IF NOT EXISTS] <数据库名> [DEFAULT CHARSET <字符集>] [COLLATE <排序规则>];

  • 删(删除数据库)

    DROP DATABASE [IF EXISTS] <数据库名>

  • 查(查询数据库)

    查询所有数据库:SHOW DATABASES;

    查询当前数据库:SELECT DATABASE();

  • 使用

    USE <数据库名>;

sql 复制代码
####################################
# 1. 创建
####################################
# 1.1 创建数据库(增)
CREATE DATABASE
    IF NOT EXISTS `ddl`;

USE `ddl`;

# 1.2 创建数据表
CREATE TABLE
    IF NOT EXISTS `user`
(
    #字段 字段类型 注释
    id     INT
        COMMENT 'id主键',
    name   VARCHAR(10)
        COMMENT '姓名',
    age    TINYINT UNSIGNED
        COMMENT '年龄',
    gender TINYINT
        COMMENT '性别',

    PRIMARY KEY (id)
)
    COMMENT '用户表';


####################################
# 3. 查询
####################################
# 3.1 查询数据库
# 3.1.1 查询所有的数据库
SHOW DATABASES;
# 3.1.2 查看当前使用的数据库
SELECT DATABASE();

# 3.2 查询表
# 3.2.1 查询当前数据库中的所有表
SHOW TABLES;
# 3.2.2 查看特定的表的详细信息
DESC `user`;
# 3.2.3 查看特定表的建表语句
SHOW CREATE TABLE `user`;

####################################
# 4. 删除
####################################
DROP DATABASE IF EXISTS `ddl`;

DML 数据操作语言

mysql 复制代码

DQL 数据查询语言

DCL 数据控制语言

在 MySQL 中,用户及用户权限信息都保存在 mysql.user 表中,通过对该表的操作即可实现对用户权限的控制管理了。

用户名和主机名两个字段才能唯一标识一个用户,主机名表示该用户可以在哪些主机上访问,如果是 localhost 则表示不可以远程访问,如果是 % 则表示任意主机均可以访问。

用户管理
  • 查询用户

    sql 复制代码
    USE `mysql`;
    
    SELECT *
    FROM user;
  • 创建用户

    sql 复制代码
    CREATE USER
    '<用户名>'@'<主机名>'
    IDENTIFIED BY '<密码>';
  • 修改用户密码

    sql 复制代码
    ALTER USER
    '<用户名>'@'<主机名>'
    IDENTIFIED WITH mysql_native_password BY '<新密码>';
  • 删除用户

    sql 复制代码
    DROP USER
    '<用户名>'@'<主机名>';
权限控制
  • 查询用户拥有的权限

    sql 复制代码
    SHOW GRANTS FOR '<用户名>'@'<主机名>';
  • 授予用户某种权限(增)

    如果需要为所有的数据库和所有的表授予权限,那么需要使用 *.*

    sql 复制代码
    GRANT <权限列表>
    ON <数据库名.表名>
    TO '<用户名>'@'<主机名>';
  • 撤销用户某种权限(删)

    sql 复制代码
    REVOKE <权限列表>
    ON <数据库名.表名>
    FROM '<用户名>'@'<主机名>';

函数

约束

sql 复制代码

事务

隔离级别

设置不同的事务隔离级别这种功能的底层原理是什么?如何实现的?是对修改后的数据写入磁盘文件的时间点决定的吗?例如,读未提交级别,则事务中执行的修改操作都会立即写入磁盘,此时其它事务就可能在该事务提交前感知到这个修改操作,因此出现脏读问题?

  • 脏读问题:

    事务A中读取到事务B中未提交的数据。具体来说,在t1时刻,事务A中读取到事务B中未提交的数据,并将该数据用于计算得到一个结果值;而在t2时刻,事务B执行回滚,那么事务A中得到的结果值就没有任何意义,这种现象就称为脏读。

  • 不可重复读:

    事务A在一次事务的执行过程中,两次读取结果不相同。具体来说,在t1时刻,事务A读取数据x1;在t2时刻,事务B将数据修改为x2并提交事务;在t3时刻,事务A再次读取数据得到x2;站在事务A的角度,可能在t1时刻到t3时刻并没有对数据进行修改,却得到两个不同的值(都是正确值),这种现象称为不可重复读。

  • 幻读:

    解决了不可重复读的问题,即在一次事务中,可以保证对数据的两次读取的结果是相同的。但是会出现幻读的问题。幻读问题是指事务A在查询时明明没有查到该数据,但是却无法插入成功。具体来说,在t1时刻,事务A查询是否有id=1的数据,发现没有;在t2时刻,事务B插入一条id=1的数据并提交;在t3时刻,事务A再次查询是否有id=1的数据,还是发现没有(因为可重复读),因此事务A打算插入一条id=1的数据,但是此时插入失败并报错,这时事务A就会纳闷,明明查询id=1的结果是没有,但是插入又报错说id=1的数据已经存在,这种现象称为幻读。

三个问题,对应四种方案,分别是解决0个问题(读未提交)、解决1个问题(读已提交)、解决2个问题(可重复读)、解决3个问题(串行化)。

串行是一切并行问题的终点,没有并行就不会有并行导致的伴生问题

sql 复制代码
SELECT @@autocommit;

# 关闭自动提交(开启事务)
SET @@autocommit=0;

# 提交事务
COMMIT;

# 回滚事务
ROLLBACK;

# 查看事务的隔离级别
SELECT @@TRANSACTION_ISOLATION;

# 设置事务的隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

进阶篇

  • 连接层
  • 服务层
  • 存储引擎层(索引、表级别)
  • 存储层

存储引擎

不同的存储引擎有着不同的使用场景,各有优缺点。存储引擎就是存储数据、建立索引、更新、查询数据等技术的实现方式。存储引擎是基于表的,不是基于库的,因此存储引擎又被称为表类型。

sql 复制代码
SHOW ENGINES;

InnoDB 引擎

特点
  • DML 操作遵循 ACID 模型,支持事务
  • 支持行级锁,相较于表锁而言可以提高并发访问性能
  • 支持外键约束,保证数据的完整性
  • xxx.idb:xxx代表表名,InnoDB引擎的每张表都会对应一个表空间文件(TableSpace),存储该表的表结构(frm、sdi)、数据和索引。
逻辑存储结构

MyISAM 引擎(可被MongoDB替代)

特点
  • 不支持事务、不支持外键、不支持行级锁
  • 支持表锁
  • 访问速度快
  • xxx.sdi:存储表结构信息(本质上是一个JSON文本数据)
  • xxx.MYD:存储数据
  • xxx.MYI:存储索引

Memory 引擎(可被Redis替代)

特点
  • 表数据信息存储在内存中,会受到断电等问题的影响,因此这些表只适合用于作为临时表或缓存表
  • 支持hash索引(默认)
  • xxx.sdi:表结构信息(Memory 引擎只会有这一个文件,因为数据保存在内存中)

索引

提高了查询速度,但同时提高了增删改的成本,因为需要维护B+树这种数据结构

不需要建立索引的情况

  1. 表记录太少的时候
  2. 经常增删改的表
  3. 某个数据列包含许多重复的内容,那这个表字段就没有必要建立索引

索引结构

一张表对应一种存储引擎,而存储引擎之间可能使用相同或不同的索引。那如果使用相同索引的不同存储引擎之间有区别吗?还是可以认为存储引擎只是不同索引的集合,而如果两个不同的存储引擎使用相同的索引,则效果完全相同呢?暂时把索引和存储引擎的关系理解成接口和实现类的关系,即使是使用相同的索引结构,不同存储引擎对于该索引结构的实现和优化可能是不同的。这种区别应该只局限于效率,应该不会有功能上的不同。

InnoDB 存储引擎虽然不支持 hash 索引,但是具有 自适应hash 的功能,即根据 B+ Tree 索引在指定条件下自动构建 hash 索引。

面试题:为什么 MySQL 索引采用 B+ Tree,而不使用 RB Tree 或 B Tree?

  1. B+ Tree 和 B Tree 是一种多叉平衡树,相较于二叉树和RB Tree而言,每层节点数更多,因此树的高度会更少,搜索效率会更高些。
  2. B+ Tree 和 B Tree 相比,由于在非叶子结点上不存储数据,因此一个页面能够存放更多的索引,即用更少的页面就能够完全保存索引,搜索效率也会更高些。(假设用B Tree保存,那1个页面保存2条索引;而用B+ Tree保存,一个页面保存200条索引,显然保存20000条索引,B Tree需要10000个页面,而B+ Tree树只需要100个页面)

索引分类

聚集索引(唯一)

聚集索引的选取规则:

  1. 如果存在主键,那主键索引就是聚集索引
  2. 如果不存在主键,那第一个unique索引是聚集索引
  3. 如果上面两种索引都不存在,那么InnoDB存储引擎会自动生成一个rowid作为隐藏的聚集索引。

对于非主键字段上建立的索引,同样按照 B+ Tree 的结构去建立索引,但是在叶子结点上保存的不是数据,而是聚集索引(Clustered Index)中节点的id。而只有在聚集索引中,叶子结点上才保存这一行的所有数据。

查询案例:使用 SELECT * FROM user WHERE name='root'; 这条 SQL 语句进行查询,其中在 user.name 字段上已经建立了索引。展示通过二级索引 来查询数据的过程,这个过程也称为回表查询

现在二级索引树中查到id,然后在聚集索引树中查询。二级索引也称为辅助索引

索引语法

  • 创建索引

    sql 复制代码
    CREATE [UNIQUE | FULLTEXT] INDEX <索引名> ON <表名>(<列名1>, <列名2>...);

    注:普通索引不添加 UNIQUEFULLTEXT 即可创建

  • 查看索引

    sql 复制代码
    SHOW INDEX FROM <表名>;
  • 删除索引

    sql 复制代码
    DROP INDEX <索引名> ON <表名>;

SQL 性能分析(重点)

SQL 优化主要是优化查询操作,要进行 SQL 优化,需要先对数据库的特点进行分析。优化不是拍脑袋,而是以事实为依据。

查看 SQL 执行频率
sql 复制代码
SHOW [GLOBAL|SESSION] STATUS LIKE 'Com_______';

# 每个下划线"_"代表一个字符, 为什么非得是7个下划线呢? 有什么含义吗?
# 目前来看, 返回结果会是Com_update、Com_insert, 正好每一个操作都是6个字符
# session表示查看当前会话的状态信息
慢查询日志

在确定了哪些查询操作占比大的数据库之后,可以通过慢查询日志来定位哪些 SQL 操作耗费时间。

在Linux系统中,慢查询日志文件的文件地址为 /var/lib/mysql/localhost-slow.log,可以通过tail -f命令来实时监控日志的新增信息。

可以通过 SHOW VARIABLES LIKE 'slow_query_log来查看慢查询日志是否开启。MySQL 的慢查询日志默认没有开启,需要在配置文件中配置如下信息:

properties 复制代码
# 开启 MySQL 慢查询日志
# 1表示开启, 0表示关闭
show_query_log=1
# 设置慢查询的阈值, 超出指定时间则被认为是慢查询, 会被记录到慢查询日志中
long_query_time=2

常见的慢查询:select count(*) from xxx;

Profile 详情

慢查询日志的局限在于,只有 SQL 语句的执行时间超过了阈值,才会被认为是慢查询。但有些简单的 SQL 语句可能没有超过阈值,但是超出期望的时间,例如期待执行时间是1ms,实际执行时间是1s。这种情况下,需要使用 Profile 来进行分析,帮助我们了解时间都消耗在什么地方上。

sql 复制代码
# 查看是否开启
SELECT @@have_profiling;

# 将其设置为开启
SET profiling = 1;
sql 复制代码
# 查看每一条SQL的耗时情况, 返回结果中可以看到query_id
SHOW PROFILES;

# 查看指定query_id的SQL语句各个阶段的耗时情况
SHOW PROFILE FOR QUERY <query_id>;

# 查看指定query_id的SQL语句的CPU使用情况
SHOW PROFILE CPU FOR QUERY <query_id>;
Explain 执行计划

SQL 执行时间的长短并不总是能够反映 SQL 的性能,执行时间稍长的 SQL 可能是由于业务逻辑极其复杂导致的。往往通过 explain 执行计划作为 SQL 性能判断的依据。通过 explain 执行计划,可以看到在 SQL 的执行过程中,表是如何连接的,以及连接顺序是怎样的等信息。

Explain 作用:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

使用方法 :在 SQL 语句前面添加关键字 EXPLAINDESC

字段解释

explain返回结果中的字段 字段含义
id 代表不同表的操作顺序,涉及多表查询的 SQL 才有实际意义。 id 大的先执行;如果 id 相同,则执行顺序从上到小
select_type
table derived2表示的就是id为2的表的衍生表,相当于临时变量
type(重点) 最好到最差:system>const>eq_ref>ref>range>index>ALL 一般来说,得保证查询至少达到range级别,最好能达到ref,再往上比较理想,实际情况不太可能实现 const:主键索引或unique索引 ref:非unique索引 range: index:全索引扫描 all:全表扫描
possible_keys和key possible_keys:理论上可能用到的索引 key:mysql实际使用的索引,如果为null则表示索引失效,没有使用索引; 如果查询中使用了覆盖索引,则该索引仅仅出现在key列表中
key_len key_len显示的值为索引字段的最大可能长度,并非实际使用长度 在不损失精确度的情况下,长度越短越好
ref 显示索引的哪一列被使用,可以的话最好是常量const
rows 大致估算出找到所需的记录所需要读取的行数
Extra(重点) 1. Using filesort:尽可能优化sql, 这是一个糟糕的信息 2. Using temporary:比上面的更糟糕,使用了临时表保存中间结果 3. Using index:效率不错 1. 如果同时出现Using where表明索引被用来执行索引键值的查找 2. 如果没有同时出现Using where表明索引用来读取数据而非执行查找动作 4. Using where:表明使用了where过滤 5. using joing buffer:使用了连接缓存,当sql语句中的join过多时,可以调大配置文件中的joining buffer 6. impossible where:where子句的值是false, 相当于逻辑错误 7. distinct:找到第一匹配的元组后即立即停止找同样值得动作 8. null表示回表查询

索引使用规则(重点)

最左前缀法则(复合索引)

假设为 name、age 和 gender 建立联合索引,那么在查询时,索引顺序分别是 name -> age -> gender,如果查询条件中缺少某个字段,那么索引链将会断链,例如,缺少name,则不使用索引;缺少age,则仅仅使用name索引,不会使用gender索引。有种先按name分类,再按age分类,最后按gender分类。

WHERE子句中的字段顺序不会对联合索引产生影响,只需要字段存在即可,例如,gender=1 AND age=18 AND name='root'

满足最左前缀法则的字段可以出现在不同的子句中,例如WHERE和GROUP BY

范围查询(复合索引)

在复合索引中,出现了范围查询(><),则范围查询右侧的列索引失效。

和上面的索引顺序链的逻辑相同,相当于破坏索引链的方式不仅仅是某个字段没有出现,同时如果某个字段使用了范围查询,则其右侧的所有失效。

解决方式:尽可能使用 >=<=。例如,>30 在某些情况下可以改写为 >= 31

为什么复合索引中,使用 > 会使得右侧的索引失效,而使用 >= 右侧的索引仍然有效呢?

索引失效的情况
  • **对索引字段进行运算时,索引失效。**例如使用字符串函数等操作 substring(name, 0, 4),此时会使得name字段上的索引失效
  • 如果字符串类型的数据不添加单引号 '' 时,索引失效。
  • 使用 LIKE %xxx 进行头部模糊匹配时,索引失效。但是尾部的模糊匹配 LIKE xxx%,索引有效。解决方法是什么?
  • OR 连接的条件中,如果其中有一个字段没有索引,那么所有的索引都失效。

前缀索引

使用场景:长字符串、大文本等

TODO

覆盖索引

SELECT 中尽量使用建立了索引的字段,而不是使用 SELECT *。本质上就是对于SELECT涉及的数据建立除主索引外的复合索引。例如下面案例中,id作为主索引,会出现在二级索引的叶子结点中,因此id字段不需要复合索引中。如果id字段作为复合索引的第一列,往往WHERE条件中不会出现id,根据最左前缀法则,此时还会造成复合索引失效。因此对username和password建立复合索引,且username作为复合索引的第一列是最优方案。

复合索引

复合索引优于单列索引:涉及多个字段时,即使每个字段上都有一个单列索引,但此时只会选择使用其中一个效率比较高的索引,然后通过回表查询找到其他的属性。创建联合索引的时候应该考虑各个列的先后顺序。

索引设计原则

SQL 优化

插入数据(注意)

  • 不要一条一条数据插入,而是通过insert批量插入,即一条insert插入多条数据(建议保持在1条insert对应1000条数据左右)

  • 手动提交事务:执行insert前,开启事务;执行完毕后,提交事务。避免一条insert对应一次自动提交

  • 主键顺序插入:主键顺序插入 > 主键乱序插入

  • 大批量插入数据时不使用 insert,而是使用 load

    shell 复制代码
    # 使用load需要一些配置
    
    # 在连接MySQL客户端时添加上--local-infile参数
    mysql --local-infile -uroot -proot
    sql 复制代码
    # 查看是否开启加载本地数据文件
    SELECT @@local_infile;
    
    # 开启从本地加载数据文件
    SET GLOBAL local_infile=1;
    
    # 加载本地数据文件到表结构中
    LOAD DATA 
    LOCAL INFILE '/root/sql01.log' 
    INTO TABLE `my_user` 
    FIELDS TERMINATED BY ',' 
    LINES TERMINATED BY '\n';

主键优化

为什么主键顺序插入的性能优于主键乱序插入?

主键乱序插入的情况下,可能会出现页分裂的现象。页分裂的原因是,主索引在叶子结点中是有序的双向链表。

主键设计原则:

  • 在满足业务需求的情况下,尽量降低主键的长度。(主键会存在于各个二级索引的叶子结点中,因此主键长度减少可以减少索引占用的空间)
  • 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键
  • 尽量不要使用UUID作为主键,或者是其他自然主键
  • 业务操作时,避免对主键进行修改

ORDER BY 优化

Using filesort:通过读取出满足要求的数据,然后在排序缓冲区Sort Buffer中完成排序操作。(取出的数据还要额外经过一个排序步骤)

Using index:通过有序索引顺序扫描,直接获得有序的目标数据。这种情况下不需要额外的排序操作,效率更高。

索引默认情况下按照字段升序排序

GROUP BY 优化

LIMIT 优化

在大数据量下,分页查询中越往后的页面查询时间越长

COUNT 优化

UPDATE 优化

索引优化

  1. 单表索引优化案例

    range类型的索引后面的索引失效 where id>1

    id = 1 and name_length>2 order by name这部分失效

  2. 双表索引优化案例

  3. 三表索引优化案例

日期时间数据

mysql 复制代码
# 查找某一天的数据
SELECT *
FROM `table_x`
WHERE DATE_FORMAT(date, '%Y%m%d') = 'xxxx-xx-xx'

上面 sql 语句存在的问题:

  • 一般表中对于日期类型数据都有一个索引,但上面的代码将不会使用索引来查询数据,造成查询效率低下。
生日问题

根据某个用户出生日期和当前日期,计算他最近的生日。

重叠问题

视图/存储过程/触发器

视图

存储过程

触发器

触发器是在DML语句(insert、update、delete)之前或之后执行的一段 SQL 代码。触发器的这种特性可以应用在:

  • 实现数据的动态完整性约束
  • 日志记录
  • 进行数据校验

MySQL 中的触发器目前还只支持行级触发,不支持语句级触发 。使用预定义的 OLDNEW 来引用修改前和修改后的记录。

  • 创建触发器

    sql 复制代码
    CREATE TRIGGER <触发器名>
    BEFORE|AFTER
    INSERT|UPDATE|DELETE
    ON <表名>
    FOR EACH ROW
    BEGIN
    	<触发器操作>
    END;
  • 查看触发器

    sql 复制代码
    SHOW TRIGGERS;
  • 删除触发器

    sql 复制代码
    DROP TRIGGER <数据库名>.<触发器名>
练习
sql 复制代码

锁(保证隔离性)

锁是一种解决并发导致的各种冲突问题的通用方法。并发问题本质上是多线程、多进程、多事务等环境下,对共享资源的访问造成的数据不一致问题,具体来说,有读写冲突、写写冲突。

共享锁和排他锁

sql 复制代码
# FOR SHARE 表示添加共享锁
SELECT *
FROM `user`
FOR SHARE;

# FOR UPDATE 表示添加排他锁
SELECT *
FROM `user`
FOR UPDATE;

MySQL 8.0 新特性,可以添加后缀 NOWAITSKIP LOCKED,分别表示加锁不等待直接报错、忽略掉加锁的记录。

全局锁

整个 MySQL 数据库实例都添加上读锁,只允许 DQL 语句。类似 insert、update、commit 等可能改变数据库状态的语句都会被阻塞。

典型使用场景:做全库的备份,保证数据一致性

sql 复制代码
# 添加全局锁
FLUSH TABLES WITH READ LOCK;

# 释放全局锁
UNLOCK TABLES;
sh 复制代码
// 备份数据库
mysqldump -uroot -proot <数据库名> > <导出文件名.sql>

存在的问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新操作,业务只能提供读取服务。
  • 如果在从库上备份,那么在备份期间,从库不能执行从主库同步过来的 binlog 日志中的写操作,导致主从库之间的延迟。

在 InnoDB 引擎中,可以在备份时添加 --single-transaction 来完成不加锁的一致性数据备份。(实现原理是快照读snapshot read)

表锁

读锁和写锁

如果使用 InnoDB 引擎,由于 X 锁和 S 锁有更细粒度的行锁,所以一般不会使用到表级别的 X 锁和 S 锁。表锁可以避免死锁,而行锁可能产生死锁问题。

sql 复制代码
# 为表添加读锁/写锁
LOCK TABLES <表名> READ|WRITE;

# 释放锁
UNLOCK TABLES;

# 查看表级别的锁, 其中in_use字段表示锁
SHOW OPEN TABLES;

MySQL 中的读锁会禁止当前 session 的写操作,会阻塞其它 session 的写操作。而对于当前 session 和其它 session 的读操作,则都可以执行。(感觉为表添加 S 锁的 session 是一个倒霉蛋,没有享有什么特权,写入操作的优先级还给让出去了。)

MySQL 中的写锁相比于读锁更能体现专属的感觉,添加写锁的 session 可以读写表中的数据,而其它 session 既不可以读,也不可以写。

思考:为什么写锁是当前session可以读写,而其它session不可以读写?

其实在并发控制中,读操作和写操作本身就是不可以并发的,因此当前session添加写操作后,其它session不能读写是很好理解的。所以主要解释为什么当前session可以读写呢?这是因为在一个session内,顺序是天生的,而并发问题的根源是由于顺序的不确定性,对于一个session内是不存在并发问题的(类似:单线程不需要考虑并发问题)

元数据锁

执行 DDL 语句时使用的并不是表级别的 X 锁、S 锁,而是称为元数据锁(Metadata Locks,简称MDL)

总结:

  • DML、DQL语句对应的是操作表中的数据,此时添加MDL中的共享锁。
    • SELECT FOR SHARE:共享读锁
    • SELECT FOR UPDATE:共享写锁
    • INSERT、DELETE、UPDATE:共享写锁
    • SELECT:不添加任何锁
  • DDL 修改表结构,例如添加、删除列,此时添加MDL中的排他锁。
sql 复制代码
# 查看元数据锁
SELECT *
FROM performance_schema.metadata_locks;
意向锁

在事务A为表添加行锁后,如果事务B此时想要为表添加表锁,那么事务B需要判断表中每一行是否添加了行锁,这样效率太低,因此产生了意向锁。

意向锁是事务A在添加行锁后,再为表添加一个意向锁。事务B检查表的意向锁与当前想要添加的表锁之间是否兼容,如果兼容则事务B添加表锁成功;否则事务B阻塞。

意向共享锁(IS)

意向排他锁(IX)

意向锁产生是作为一种辅助工具,或称为行锁的副产品,因此意向锁之间都是兼容的,例如,对行1读和对行2写,分别产生IS和IX锁,但本质上都是对独立的行进行处理。只有IS锁能够和表锁中的S锁兼容,其它都不兼容。

sql 复制代码
# 查看意向锁以及行锁的加锁情况
SELECT *
FROM performance_schema.data_locks;

总结:

  • 同级别的锁,只有S锁和S锁是兼容的
  • 低级别的锁在高级别中对应的锁可能会降级(也可能不会),例如,行锁中的X锁,在MDL中降级为S锁,而在意向锁中仍然对应IX锁。(这里写的有点不清楚,并不是说行锁中X锁是MDL中的S锁。而是某一个行为,导致行锁中添加X锁,MDL中添加S锁,意向锁中添加IX锁。)

行锁

InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。(查询数据最终都要落到主索引上)

InnoDB的行锁是针对索引加的锁,如果不通过索引条件检索数据,那么行锁会自动升级为表锁,InnoDB引擎会对表中的所有记录添加锁。

  • 在唯一索引上(UNIQE)进行等值查询,给不存在的记录加锁时,会添加间隙锁

    例如,数据库中存在id=3和id=8的数据,此时给id=5的记录(不存在的数据)添加锁,这把锁即为间隙锁。在其它事务insert一条id=7的数据时,会因为间隙锁的存在而被阻塞,在这个场景下,间隙锁的范围是(3,8)。

  • 在非唯一索引上进行等值查询时,会在前后都添加间隙锁

    例如,数据库中存在age=1、age=3、age=8的数据,现在对age=3的数据进行操作,由于age上的索引是非唯一索引,因此除了对age=3的数据本身添加记录锁外,还会添加(1,3)和(3,8)的间隙锁,相当于(1,8)的范围全部被锁住。实际实现中,将分为两把锁 (1,3] 的临键锁和 (3,8) 的间隙锁。

  • 在唯一索引上进行范围查询时,可能会添加大量的锁

    例如,存在数据 [1,3,5,19,25,38],执行 id > 19 时,会在 25、38、supremum pseudo-record(+∞) 上都添加临键锁。

    25 上的临键锁只锁 (19,25] 这部分范围,38 上的临键锁只锁 (25,38] 这部分范围,因此需要多把临键锁才能满足 id > 19 的范围表示。

    sql 复制代码
    SELECT *
    FROM `user`
    WHERE id >= 19
    FOR SHARE;
记录锁

Record Lock

S锁

X锁

间隙锁

Gap Lock

间隙锁不锁记录,是为了确保索引记录之间的间隙不变,防止其它事务在这个间隙进行insert,避免产生幻读现象。

间隙锁的目的是防止其它事务插入间隙,间隙锁可以共存。

临键锁

记录锁和间隙锁的组合,锁住数据的同时还锁住间隙。

InnoDB 存储引擎

逻辑存储结构

架构

内存结构

Buffer Pool(缓冲池)

Buffer Pool 是主内存的一个区域,在执行增删改查操作时,先操作缓冲区中的数据,然后以一定的频率刷新到磁盘,从而减少磁盘IO。

缓冲池以 Page(页)为单位,底层采用链表结构管理。根据状态,将 Page 分为三种类型:

  • free Page:空闲页,没有将磁盘数据导入,处于未使用状态
  • clean Page:磁盘数据导入,但是只执行了查询操作,并没有执行修改操作
  • dirty Page:脏页,缓冲池中的数据和磁盘中的数据不一致。所谓的 dirty page,就是本应该直接写入磁盘的数据暂时还保存在内存缓冲区中,此时内存中的page 称为 dirty page。将 dirty page 刷新到磁盘中即可以实现数据的一致。(TODO:但这样似乎 dirty page 应该称为 real page 才对吧?哪里的理解不对呢?)

Change Buffer(更改缓冲区)

Change Buffer 是针对非 UNIQUE 索引二级索引页。在执行 DML 语句时,如果

TODO

Log Buffer(日志缓冲区)

日志缓冲区,用来保存写入到磁盘中的 log 日志数据(redo log、undo log)。默认大小为 16 MB。

日志缓冲区中的数据会定期刷新到磁盘中,增加日志缓冲区的大小可以节省磁盘I/O。

properties 复制代码
# 设置日志缓冲区大小
innodb_log_buffer_size=16MB
# 设置日志刷新到磁盘的时机
# 0: 每秒写入并刷新一次
# 1: 每次事务提交时写入并刷新到磁盘
# 2: 每次事务提交后写入, 每秒刷新一次 
innodb_flush_log_at_trx_commit=0|1|2
磁盘结构

System Tablespace(系统表空间)

TableSpace(表空间)

sql 复制代码
CREATE TABLESPACE <表空间名>
ADD DATAFILE '<idb文件名>'
ENGINE = INNODB;

Doublewrite Buffer Files(双写缓冲区)

InnoDB 引擎在将 Buffer Pool 中的 Page 刷新到磁盘前,会先将数据也写入双写缓冲区文件中,便于系统异常时恢复数据。

事务原理(InnoDB的价值)

  • ACD 特性:由 redo log 和 undo log 实现

  • I(隔离性):由 锁 和 MVCC 实现

redo log(保证持久性)
  • redo log buffer(重做日志缓冲区)

    保存在内存中

  • redo log file(重做日志文件)

    保存在磁盘中

当事务提交后,所有的修改信息都保存在 redo log file 中。当脏页(dirty page)刷新到磁盘时,如果不幸发生错误,可以使用 redo log file 进行数据恢复使用。这里的发生错误是指,数据在内存中已经被成功保存,事务提交成功,并且告知用户事务执行成功,但是实际上在将内存数据保存到磁盘这个过程中发生错误时,持久性就没有得到保障。

大致流程:

  1. 对某个数据执行 update、delete 等操作
  2. 在 Buffer Pool 中查找是否存在该数据的缓存;如果不存在,则通过后台线程从 idb 文件中将 page 读取到 buffer pool 中
  3. 对 buffer pool 中的 page 进行相应的操作(update、delete等),修改 buffer pool 中的数据,此时 buffer pool 和 idb 文件存在差异,即 page 称为 dirty page。
  4. 每隔一定的时间间隔,后台线程会将 dirty page 刷新到 idb 文件中。

redo log 的功能在于:步骤3到步骤4之间,添加一个redo log buffer,将当前数据执行的操作的信息保存到 redo log buffer 中,然后 redo log buffer 中的数据立即刷新到磁盘中的 redo log file 中保存。保存成功后,才向用户返回事务提交成功?

**redo log buffer 是顺序的追加方式,操作 I/O 的效率高。**而执行数据的 update、delete 等操作时,数据所在的 page 是随机分布的,需要执行大量的随机读写 I/O,而这种效率低。因此每次事务提交后,可以将 redo log buffer 中的数据刷新到磁盘中,但是出于效率考虑,而 buffer pool 中的数据则是每隔一段时间刷新一次。换言之,如果每次刷新都提交 buffer pool 中的数据,那 buffer pool 作为缓冲的价值也就没有必要了。

在 dirty page 成功刷新到磁盘中后,实际上 redo log file 也就没有存在意义了,可以认为 redo log file 是一个环形队列,会覆盖掉原来的数据,文件不会无限制增长。

undo log(保证原子性)

undo log 用于记录数据被修改前的信息,用于事务回滚或MVCC(多版本并发控制)。

与 redo log 记录实际执行的物理操作不同,undo log 并不是记录实际执行的数据操作,甚至是记录与当前操作相反的操作(逆操作),例如当前实际执行的是delete 操作,那 undo log 中需要对该操作进行还原,因此记录 insert 操作。

  • insert undo log:保存插入记录的主键值,回滚时只需要删除该主键对应的记录即可

  • update undo log:保存记录的旧值,回滚时将记录更新回旧值

  • delete undo log:理论上将记录的旧值保存下来,回滚的时候重新插入记录即可(主键变化怎么办呢?)。但实际为了效率并没有这么处理。

    设置删除标志位(隐藏字段),InnoDB 引擎通过专门的 purge 线程来清理这些删除标志位被设置true的记录。这种情况下如何实现回滚呢?

undo log 在事务提交之后,并不会立即删除,因为这些日志还可能用于 MVCC。undo log 采用 segment(段)的方式进行管理和记录,存放在 rollback segment (回滚段)中,内部包含 1024 个 rollback segment。这里是什么的内部?

undo log 日志中,insert undo log 日志只在事务回滚的情况下需要,因此在事务commit之后,insert undo log 可以被立即删除。但是 delete undo log 和 update undo log 不仅在回滚的时候需要,在快照读的时候也需要,因此不能被立即删除。

MySQL 管理

面试题

  • 什么是事务?事务的四大特性是什么?
  • 事务的隔离级别有哪些?MySQL 默认是哪个?
  • 内连接和左外连接的区别是什么?
  • 常用的存储引擎有哪些?InnoDB 和 MyISAM 的区别?
  • MySQL 默认 InnoDB 引擎的索引是什么数据结构?
  • 如何查看 MySQL 的执行计划?
  • 索引失效的情况有哪些?
  • 什么是回表查询?
  • 什么是 MVCC?
  • MySQL 的主从复制的原理是什么?
  • 主从复制之后的读写分离是如何实现的?
  • 数据库的分库分表是如何实现的?
相关推荐
stars_User1 小时前
MySQL数据库面试题(下)
数据库·mysql
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
yanwushu1 小时前
Xserver v1.4.2发布,支持自动重载 nginx 配置
mysql·nginx·php·个人开发·composer
蓝眸少年CY2 小时前
MySQL 【流程控制】函数
mysql
掘金-我是哪吒3 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
zmgst4 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
令狐少侠20114 小时前
explain执行计划分析 ref_
mysql
随心............4 小时前
python操作MySQL以及SQL综合案例
数据库·mysql
xjjeffery4 小时前
MySQL 基础
数据库·mysql
恒辉信达5 小时前
hhdb数据库介绍(8-4)
服务器·数据库·mysql