面试:MySQL篇,详尽知识点总结

面试:MySQL篇

本篇文章是本人在结合书籍和博客并结合个人理解总结的相对完全的MySQL面试知识点,部分内容在下方给出参考博客的链接和书籍,以供读者进行参考和深入的理解该部分知识点,针对部分知识点可能存在错误还望读者指出,本人会在第一时间进行改正.

WhyMysql?

NoSQL数据库四大家族

  • 列存储 Hbase
  • K-V存储 Redis
  • 图像存储 Neo4j
  • 文档存储 MongoDB

云存储OSS

海量Aerospike

​ Aerospike(简称AS)是一个分布式,可扩展的键值存储的NoSQL数据库。T级别大数据高并发的结构化数据存储,采用混合架构,索引存储在内存中,而数据可存储在机械硬盘(HDD)或固态硬盘(SSD) 上,读写操作达微妙级,99%的响应可在1毫秒内实现。

Aerospike Redis
类型 Nosql数据库 缓存
线程数 多线程 单线程
数据分片 自动处理相当于分片 提供分片算法、平衡各分片数据
数据扩容 动态增加数据卷平衡流量 需停机
数据同步 设置复制因子后可以透明的完成故障转移 手动故障转移和数据同步
载体 内存存储索引+SSD存储数据 内存

​ Aerospike作为一个大容量的NoSql解决方案,适合对容量要求比较大,QPS相对低一些的场景,主要用在广告行业,个性化推荐厂告是建立在了和掌握消费者独特的偏好和习性的基础之上,对消费者的购买需求做出准确的预测或引导,在合适的位置、合适的时间,以合适的形式向消费者呈现与其需求高度吻合的广告,以此来促进用户的消费行为。

image-20210103170039711

​ (ETL数据仓库技术)抽取(extract)、转换(transform)、加载(load)

  • 用户行为日志收集系统收集日志之后推送到ETL做数据的清洗和转换

  • 把ETL过后的数据发送到推荐引擎计算每个消费者的推荐结果,其中推荐逻辑包括规则和算法两部分

  • 收集用户最近浏览、最长停留等特征,分析商品相似性、用户相似性、相似性等算法。

  • 把推荐引擎的结果存入Aerospike集群中,并提供给广告投放引擎实时获取

    分别通过HDFS和HBASE对日志进行离线和实时的分析,然后把用户画像的标签(tag : 程序猿、宅男…)结果存入高性能的Nosql数据库Aerospike中,同时把数据备份到异地数据中心。前端广告投放请求通过决策引擎(投放引擎)向用户画像数据库中读取相应的用户画像数据,然后根据竞价算法出价进行竞价。竞价成功之后就可以展现广告了。而在竞价成功之后,具体给用户展现什么样的广告,就是有上面说的个性化推荐广告来完成的。

Aerospike Mysql
库名 Namespace Database
表名 Set Table
记录 Bin Column
字段 Record Row
索引 key 、 pk 、kv pk

图谱Neo4j

Neo4j是一个开源基于java开发的图形noSql数据库,它将结构化数据存储在图中而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎。程序数据是在一个面向对象的、灵活的网络结构下,而不是严格的表中,但具备完全的事务特性、企业级的数据库的所有好处。

一种基于图的数据结构,由节点(Node)和边(Edge)组成。其中节点即实体,由一个全局唯一的ID标示,边就是关系用于连接两个节点。通俗地讲,知识图谱就是把所有不同种类的信息,连接在一起而得到的一个关系网络。知识图谱提供了从“关系”的角度去分析问题的能力。

互联网、大数据的背景下,谷歌、百度、搜狗等搜索引擎纷纷基于该背景,创建自己的知识图Knowledge Graph(谷歌)、知心(百度)知立方(搜狗),主要用于改进搜索质量。

自己项目主要用作好友推荐,图数据库(Graph database)指的是以图数据结构的形式来存储和查询数据的数据库。关系图谱中,关系的组织形式采用的就是图结构,所以非常适合用图库进行存储。

  • image-20210103191540372

    优势总结:

  • 性能上,使用cql查询,对长程关系的查询速度快

  • 擅于发现隐藏的关系,例如通过判断图上两点之间有没有走的通的路径,就可以发现事物间的关联

image-20210103192653004

// 查询三层级关系节点如下:with可以将前面查询结果作为后面查询条件
match (na:Person)-[re]-(nb:Person) where na.name="林婉儿" WITH na,re,nb match (nb:Person)- [re2:Friends]->(nc:Person) return na,re,nb,re2,nc
// 直接拼接关系节点查询
match data=(na:Person{name:"范闲"})-[re]->(nb:Person)-[re2]->(nc:Person) return data
// 使用深度运算符
显然使用以上方式比较繁琐,可变数量的关系->节点可以使用-[:TYPE*minHops..maxHops]-。
match data=(na:Person{name:"范闲"})-[*1..2]-(nb:Person) return data

文档MongoDB

MongoDB 是一个基于分布式文件存储的数据库,是非关系数据库中功能最丰富、最像关系数据库的。在高负载的情况下,通过添加更多的节点,可以保证服务器性能。由 C++ 编写,可以为 WEB 应用提供可扩展、高性能、易部署的数据存储解决方案。

image-20210103194830654

什么是BSON

{key:value,key2:value2}和Json类似,是一种二进制形式的存储格式,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,比如 value包括字符串,double,Array,DateBSON可以做为网络数据交换的一种存储形式,它的优点是灵活性高,但它的缺点是空间利用率不是很理想。

BSON有三个特点:轻量性、可遍历性、高效性

/* 查询 find() 方法可以传入多个键(key),每个键(key)以逗号隔开*/
db.collection.find({key1:value1, key2:value2}).pretty()
/* 更新 $set :设置字段值 $unset :删除指定字段 $inc:对修改的值进行自增*/
db.collection.update({where},{$set:{字段名:值}},{multi:true})
/* 删除 justOne :如果设为true,只删除一个文档,默认false,删除所有匹配条件的文档*/
db.collection.remove({where}, {justOne: , writeConcern: <回执> } )

优点:

  • 文档结构的存储方式,能够更便捷的获取数据。

    对于一个层级式的数据结构来说,使用扁平式的,表状的结构来查询保存数据非常的困难。

  • 内置GridFS,支持大容量的存储。

    GridFS是一个出色的分布式文件系统,支持海量的数据存储,满足对大数据集的快速范围查询。

  • 性能优越

    千万级别的文档对象,近10G的数据,对有索引的ID的查询 不会比mysql慢,而对非索引字段的查询,则是全面胜出。 mysql实际无法胜任大数据量下任意字段的查询,而mongodb的查询性能实在牛逼。写入性能同样很令人满意,同样写入百万级别的数据,mongodb基本10分钟以下可以解决。

缺点:

  • 不支持事务
  • 磁盘占用空间大

MySQL 8.0 版本

1. 性能:MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍。

2. NoSQL:MySQL 从 5.7 版本开始提供 NoSQL 存储功能,在 8.0 版本中nosql得到了更大的改进。

3. 窗口函数:实现若干新的查询方式。窗口函数与 SUM()、COUNT() 这种集合函数类似,但它不会将多行查询结果合并为一行,而是将结果放回多行当中,即窗口函数不需要 GROUP BY。

4. 隐藏索引:在 MySQL 8.0 中,索引可以被“隐藏”和“显示”。当对索引进行隐藏时,它不会被查询优化器所使用。我们可以使用这个特性用于性能调试,例如我们先隐藏一个索引,然后观察其对数据库的影响。如果数据库性能有所下降,说明这个索引是有用的,然后将其“恢复显示”即可;如果数据库性能看不出变化,说明这个索引是多余的,可以考虑删掉。

云存储

OSS 自建
可靠性 可用性不低于99.995%
数据设计持久性不低于99.9999999999%(12个9)
受限于硬件可靠性,易出问题,一旦出现磁盘坏道,容易出现不可逆转的数据丢失。人工数据恢复困难、耗时、耗力。
安全 服务端加密、客户端加密、防盗链、IP黑白名单等。多用户资源隔离机制,支持异地容灾机制。 需要另外购买清洗和黑洞设备。需要单独实现安全机制。
成本 多线BGP骨干网络,无带宽限制,上行流量免费。无需运维人员与托管费用,0成本运维。 单线或双线接入速度慢,有带宽限制,峰值时期需人工扩容。需专人运维,成本高。

使用步骤

​ 1、开通服务

​ 2、创建存储空间

​ 3、上传文件、下载文件、删除文件

​ 4、域名绑定、日志记录

​ 5、根据开放接口进行鉴权访问

功能

​ 图片编辑(裁剪、模糊、水印)

​ 视频截图

​ 音频转码、视频修复

CDN加速

​ 对象存储OSS与阿里云CDN服务结合,可优化静态热点文件下载加速的场景(即同一地区大量用户同时下载同一个静态文件的场景)。可以将OSS的存储空间(Bucket)作为源站,利用阿里云CDN将源内容发布到边缘节点。当大量终端用户重复访问同一文件时,可以直接从边缘节点获取已缓存的数据,提高访问的响应速度

FastDFS

开源的轻量级分布式文件系统。它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。如相册网站、视频网站

扩展能力: 支持水平扩展,可以动态扩容;

高可用性: 一是整个文件系统的可用性,二是数据的完整和一致性;

弹性存储: 可以根据业务需要灵活地增删存储池中的资源,而不需要中断系统运行。

image-20210107221022658

特性

  • 和流行的web server无缝衔接,FastDFS已提供apache和nginx扩展模块
  • 文件ID由FastDFS生成,作为文件访问凭证,FastDFS不需要传统的name server
  • 分组存储,灵活简洁、对等结构,不存在单点
  • 文件不分块存储,上传的文件和OS文件系统中的文件一一对应
  • 中、小文件均可以很好支持,支持海量小文件存储
  • 支持相同内容的文件只保存一份,节约磁盘空间
  • 支持多块磁盘,支持单盘数据恢复
  • 支持在线扩容 支持主从文件
  • 下载文件支持多线程方式,支持断点续传

组成

  • 客户端(client)

    通过专有接口,使用TCP/IP协议与跟踪器服务器或存储节点进行数据交互。

  • 跟踪器(tracker)

    Trackerserver作用是负载均衡和调度,通过Tracker server在文件上传时可以根据策略找到文件上传的地址。Tracker在访问上起负载均衡的作用。

  • 存储节点(storage)

    Storageserver作用是文件存储,客户端上传的文件最终存储在Storage服务器上,Storage server没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。存储节点中的服务器均可以随时增加或下线而不会影响线上服务

上传

image-20210107222155291

下载

image-20210107222312338

断点续传

​ 续传涉及到的文件大小MD5不会改变。续传流程与文件上传类似,先定位到源storage,完成完整或部分上传,再通过binlog进行同group内server文件同步

配置优化

配置文件:tracker.conf 和 storage.conf

// FastDFS采用内存池的做法。 
// v5.04对预分配采用增量方式,tracker一次预分配 1024个,storage一次预分配256个。 
max_connections = 10240
// 根据实际需要将 max_connections 设置为一个较大的数值,比如 10240 甚至更大。
// 同时需要将一个进程允许打开的最大文件数调大
vi /etc/security/limits.conf 重启系统生效 
* soft nofile 65535 
* hard nofile 65535
work_threads = 4 
// 说明:为了避免CPU上下文切换的开销,以及不必要的资源消耗,不建议将本参数设置得过大。
// 公式为: work_threads + (reader_threads + writer_threads) = CPU数
// 对于单盘挂载方式,磁盘读写线程分 别设置为 1即可 
// 如果磁盘做了RAID,那么需要酌情加大读写线程数,这样才能最大程度地发挥磁盘性能
disk_rw_separated:磁盘读写是否分离 
disk_reader_threads:单个磁盘读线程数 
disk_writer_threads:单个磁盘写线程数 

避免重复

​ 如何避免文件重复上传 解决方案 上传成功后计算文件对应的MD5然后存入MySQL,添加文件时把文件MD5和之前存入MYSQL中的存储的信息对比 。DigestUtils.md5DigestAsHex(bytes)。

事务

1、事务4大特性

**事务4大特性:**原子性、一致性、隔离性、持久性

原⼦性: 事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么全不执行

一致性: 执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结果是相同的;

隔离性: 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发事务之间相互独立;

持久性: ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发⽣故障也不应该对其有任何影响。

实现保证:

​ MySQL的存储引擎InnoDB使用重做日志保证一致性与持久性,回滚日志保证原子性,使用各种锁来保证隔离性。

概念说明

以下几个概念是事务隔离级别要实际解决的问题,所以需要搞清楚都是什么意思。

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

可重复读

可重复读指的是在一个事务内,不同的时刻读到的同一批数据是一样的,事务不会读到其他事务对已有数据的修改,及时其他事务已提交。通常针对数据**更新(UPDATE)**操作。

不可重复读

不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,如其他事务改了这批数据并提交了。通常针对数据**更新(UPDATE)**操作。

幻读

​ 幻读是针对数据**插入(INSERT)**操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。

Undo Log作用

实现事务的原子性
Undo Log 是为了实现事务的原子性而出现的产物。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。

实现多版本并发控制(MVCC)
Undo Log 在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未提交之前,Undo Log保存了未提交之前的版本数据,Undo Log 中的数据可作为数据旧版本快照供其他并发事务进行快照读。

2、事务隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED)
  2. 读提交 (READ COMMITTED)
  3. 可重复读 (REPEATABLE READ)
  4. 串行化 (SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。

img

只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。

如何设置隔离级别

我们可以通过以下语句查看当前数据库的隔离级别,通过下面语句可以看出我使用的 MySQL 的隔离级别是 REPEATABLE-READ,也就是可重复读,这也是 MySQL 的默认级别。

# 查看事务隔离级别 5.7.20 之后
show variables like 'transaction_isolation';
SELECT @@transaction_isolation

# 5.7.20 之后
SELECT @@tx_isolation
show variables like 'tx_isolation'

+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+

稍后,我们要修改数据库的隔离级别,所以先了解一下具体的修改方式。

修改隔离级别的语句是:set [作用域] transaction isolation level [事务隔离级别],
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。

其中作用于可以是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只针对当前回话窗口。隔离级别是 {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} 这四种,不区分大小写。

比如下面这个语句的意思是设置全局隔离级别为读提交级别。

mysql> set global transaction isolation level read committed;
MySQL 中执行事务

事务的执行过程如下,以 begin 或者 start transaction 开始,然后执行一系列操作,最后要执行 commit 操作,事务才算结束。当然,如果进行回滚操作(rollback),事务也会结束。

说明

读未提交,其实就是可以读到其他事务未提交的数据,既然读未提交没办法解决脏数据问题,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。

可重复是对比不可重复而言的,上面说不可重复读是指同一事务不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题

不可重复读和幻读的区别

很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。

3、MySQL 中是如何实现事务隔离的

我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么。

首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

再来说串行化。读的时候加共享锁(s锁),也就是其他事务可以并发读(可以加共享锁),但是不能写。写的时候加排它锁(x锁),其他事务不能并发写也不能并发读。

最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。

实现提交读和可重复读

为了实现可重复读和脏读,MySQL 采用了 MVVC (多版本并发控制,multi-version concurrency control) 的方式。

我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,每开启一个新事务,事务的版本号就会递增。

面试:MySQL篇,详尽知识点总结_第1张图片

按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。

在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。

在上面介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照

对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:

  1. 当前事务内的更新,可以读到;
  2. 版本未提交,不能读到;
  3. 版本已提交,但是却在快照创建后提交的,不能读到;
  4. 版本已提交,且是在快照创建前提交的,可以读到;

利用上面的规则,再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调,两者主要的区别就是在快照的创建上,可重复读仅在事务开始是创建一次,而读提交每次执行语句的时候都要重新创建一次。

上文的“读”,实际是讲的快照读(因此对于快照读来看可以避免幻读问题,因为其他事务新插入并提交的数据对之前快照来说不存在);而这里说的“写”就是当前读了(针对最新的数据来写入而不是快照),因此能够出现幻读问题。为了解决当前读中的幻读问题,MySQL事务使用了Next-Key锁。

并发写问题

存在这样情况,两个事务,对同一条数据做修改。最后结果应该是哪个事务的结果呢,肯定要是时间靠后的那个对不对。并且更新之前要先读数据,这里所说的读和上面说到的读不一样,更新之前的读叫做“当前读”,总是当前版本的数据,也就是多版本中最新一次提交的那版。

假设事务A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常。如下图所示。

面试:MySQL篇,详尽知识点总结_第2张图片

加锁的过程要分有索引和无索引两种情况,比如下面这条语句

update user set age=11 where id = 1

id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落的加上行锁就可以了。

而下面这条语句

update user set age=11 where age=10

表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据。那怎么办呢,当然也不是加表锁了。MySQL 会为这张表中所有行加行锁,没错,是所有行。但是呢,在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。

解决幻读(可重复读解决幻读)

上面介绍可重复读的时候,那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现,MySQL 已经在可重复读隔离级别下解决了幻读的问题

前面刚说了并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁

假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。

面试:MySQL篇,详尽知识点总结_第3张图片

此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。

面试:MySQL篇,详尽知识点总结_第4张图片

如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。

之后,我用下面的两个事务演示一下加锁过程。

面试:MySQL篇,详尽知识点总结_第5张图片

在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行update user set name='风筝2号’ where age = 10; 的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10

这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。

行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。

间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。

总结

​ MySQL 的 InnoDB 引擎才支持事务,其中可重复读是默认的隔离级别。

​ 读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。

Mysql使用乐观锁思想的MVCC实现提交读和可重复读,行锁解决了并发更新的问题,并且 MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的,是悲观的。

参考:https://zhuanlan.zhihu.com/p/117476959

参考:https://tech.meituan.com/2014/08/20/innodb-lock.html

4、RR和RC使用场景

​ 事务隔离级别RC(read committed)和RR(repeatable read)两种事务隔离级别基于多版本并发控制MVCC(multi-version concurrency control)来实现

RC RR
实现 读提交则是每次执行语句的时候都重新生成一次快照 在事务开始的时候生成一个当前事务全局性的快照
粒度 语句级读一致性 事务级读一致性
准确性 每次语句执行时间点的数据 第一条语句执行时间点的数据

5、Innodb锁机制详解

InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。关于事务我们之前有专题介绍,这里就着重介绍下它的锁机制。

总的来说,InnoDB按照不同的分类共有七种类型的锁:

共享/排它锁(Shared and Exclusive Locks)

意向锁(Intention Locks)

间隙锁(Gap Locks)

记录锁(Record Locks)

临键锁(Next-key Locks)

插入意向锁(Insert Intention Locks)

自增锁(Auto-inc Locks)

共享/排它锁(Shared and Exclusive Locks)

按照兼容性来分类,InnoDB有共享锁和排它锁两种行级锁

  • 共享锁(S):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据。 对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。**排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁,但是仍然可以进行普通无锁查询。**mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

另外还需要注意的是:InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

意向锁(Intention Locks)

InnoDB为了支持多粒度锁机制(multiple granularity locking),即允许行级锁与表级锁共存,而引入了意向锁(intention locks)。意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。

意向锁是一个表级别的锁(table-level locking);

意向锁又分为:

  • 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁;
  • 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁;

加锁的语法为:

select … lock in share mode;  //要设置IS锁

select … for update;      //要设置IX锁

意向锁和共享级排它锁的兼容性关系如下:

面试:MySQL篇,详尽知识点总结_第6张图片

前面讨论的行锁按照兼容性分为共享/排它锁,但是从实现算法上来分的话,Innodb的三种行锁分别是:间隙锁、记录锁和临键锁(Next-Key Lock),下面分别阐述。

间隙锁(Gap Locks)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁(记录锁);

对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

举例来说,假如lock_example表中只有101条记录,其id的值分别是1,2,…,100,101,下面的SQL:

Select * from lock_example where id > 100 for update;

InnoDB 不仅会对符合条件的 id值为 101 的记录加锁;

也会对 id大于101(这些记录并不存在)的“间隙”加锁。

间隙锁的目的

  • 防止幻读,以满足相关隔离级别的要求

对于上例,若不使用间隙锁,如果其他事务插入 id大于 100 的任何记录,那么本事务如果再次执行上述语句,就会发生幻读。

  • 满足其恢复和复制的需要

在使用范围条件检索并锁定记录时,InnoDB 这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待,因此,在实际开发中,尤其是并发插入较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

记录锁(Record Locks)

顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录

– id 列为主键列或唯一索引列

SELECT * FROM lock_example WHERE id = 1 FOR UPDATE;

id 为 1 的记录行会被锁住。

需要注意的是:该列(id)列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁。

同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。

其他实现

在通过 主键索引与唯一索引对数据行进行UPDATE 操作时,也会对该行数据加记录锁:

– id 列为主键列或唯一索引列

UPDATE lock_example SET age = 50 WHERE id = 1;

临键锁(Next-Key Locks)

​ Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临键锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁,会降级为记录锁,即仅锁住索引本身,不是范围。

假设有如下表:

MySql,InnoDB,Repeatable-Read:lock_example(id PK, age KEY, name)

面试:MySQL篇,详尽知识点总结_第7张图片

该表中 age 列潜在的临键锁有:

(-∞, 10],

(10, 24],

(24, 32],

(32, 45],

(45, +∞],

在事务 A 中执行如下命令:

– 根据非唯一索引列 UPDATE 某条记录

UPDATE lock_example SET name = Vladimir WHERE age = 24;

– 或根据非唯一索引列 锁住某条记录

SELECT * FROM lock_example WHERE age = 24 FOR UPDATE;

不管执行了上述 SQL 中的哪一句,之后如果在事务 B 中执行以下命令,则该命令会被阻塞:

INSERT INTO table VALUES(100, 16, ‘Ezreal’);

很明显,事务 A 在对 age 为 24 的列进行 UPDATE 操作的同时,也获取了 (10, 24] 这个区间内的临键锁。

不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:

INSERT INTO table VALUES(100, 30, ‘Tom’);

那最终我们就可以得知,在根据非唯一索引 对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的临键锁 ,并同时获取该记录行下一个区间的间隙锁。

即事务 A在执行了上述的 SQL 后,最终被锁住的记录区间为 (10, 32]。

插入意向锁(Insert Intention Locks)

对已有数据行的修改与删除,必须加强互斥锁(X锁),那么对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生。

插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。先看下官方的解释:

Insert Intention Lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.

举个例子(表依然是如上的例子lock_example,数据依然是如上),事务A先执行,在10与24两条记录中插入了一行,还未提交:

insert into lock_example values(11,23, ‘Jim’);

事务B后执行,也在10与24两条记录中插入了一行:

insert into lock_example values(12,24, ‘Bob’);

因为是插入操作,虽然是插入同一个区间,但是插入的记录并不冲突,所以使用的是插入意向锁,此处A事务并不会阻塞B事务。

自增锁(Auto-inc Locks)

自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。官方解释如下:

AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

举个例子(表依然是如上的例子lock_example),但是id为AUTO_INCREMENT,如果A事务执行如下语句:

insert into lock_example values(23, ‘Jim’);

B事务执行的语句如下:

insert into lock_example values(24, ‘Bob’);

此时事务B插入操作会阻塞,直到事务A提交。

总结

以上总结的7种锁,可以按两种方式来区分:

  1. 按锁的兼容性来划分,可以分为:

    共享锁(S锁、IS锁),可以提高读并发;

    排它锁(X锁、IX锁),InnoDB使用强互斥锁(X锁、IX锁),保证同一行记录修改与删除的串行性;

  2. 按锁的粒度来划分,可以分为:

    ​ 表锁:意向锁(IS锁、IX锁)、自增锁;

    ​ 行锁:记录锁、间隙锁、临键锁、插入意向锁;

其中,InnoDB的细粒度锁(即行锁),是实现在索引记录上的(如果未命中索引则会失效变为临键锁);

记录锁锁定索引记录;间隙锁锁定间隔,防止间隔中被其他事务插入;临键锁锁定索引记录+间隔,防止幻读;

InnoDB使用插入意向锁,可以提高插入并发;

间隙锁(gap lock)与临键锁(next-key lock)只在Repeatable-Read(RR可重复读)和以上的级别生效。

在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。

在未提交读级别中,没有锁。

串行锁利用共享锁和排它锁实现的。

可重复读RR和提交读RC是通过MVCC这种乐观锁思想解决脏读和不可重复读没有采用上面说的锁是出于性能考虑,但是数据的写入、修改和删除是需要加锁的,其中RR利用临建锁解决了幻读的问题。

6、MVCC多版本并发控制

​ MVCC是一种多版本并发控制机制,通过事务的可见性看到自己预期的数据,能降低其系统开销.(RC和RR级别工作)

​ InnoDB的MVCC,是通过在每行记录后面保存系统版本号(可以理解为事务的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID。这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的,防止幻读的产生(相对读的幻读来说,而对于写的话幻读还会存在)。

​ 1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read).

​ 2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC.

​ 3.简单的select快照度不会加锁,删改及select for update等需要当前读的场景会加锁

​ 原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。客观上,mysql使用的是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。Innodb的MVCC使用到的快照存储在Undo日志中,该日志通过回滚指针把一个数据行所有快照连接起来。

版本链

​ 在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:

trx_id

​ 这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。

roll_pointer

​ 每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)。

​ 每次修改都会在版本链中记录。**SELECT可以去版本链中拿记录(如快照),这就实现了读-写,写-读的并发执行,**提升了系统的性能。

索引

1、Innodb和Myisam引擎

**Myisam:**支持表锁,适合读密集的场景,不支持外键,不支持事务,索引与数据在不同的文件

**Innodb:**支持行、表锁,默认为行锁,适合并发场景,支持外键,支持事务,索引与数据同一文件

索引目的

索引的目的在于提高查询效率

2、哈希索引

​ 哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存储该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能

3、B+树索引

面试:MySQL篇,详尽知识点总结_第8张图片

b+树的查找过程

如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

b+树性质

1.通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。

最左前缀问题:

​ 针对组合索引来说,当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

参考:https://tech.meituan.com/2014/06/30/mysql-index.html

优点:

​ B+树的磁盘读写代价低,更少的查询次数,查询效率更加稳定,有利于对数据库的扫描

​ B+树是B树的升级版,B+树只有叶节点存放数据,其余节点用来索引。索引节点可以全部加入内存,增加查询效率,叶子节点可以做双向链表,从而提高范围查找的效率,增加的索引的范围

​ 在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,B+树一个节点可以有多个元素,可以降低树的高度。

对比B树,B+树的优势是

​ 1. 每个节点存储的key数量更多,树的高度更低

​ 2. 所有的具体数据都存在叶子节点上,所以每次查询都要查到叶子节点,查询速度比较稳定

​ 3. 所有的叶子节点构成了一个有序链表,做区间查询时更方便

为了减小IO操作数量,一般把一个节点的大小设计成最小读写单位的大小

磁盘预读原理:将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。

4、创建索引

CREATE  [UNIQUE | FULLTEXT]  INDEX  索引名 ON  表名(字段名) [USING 索引方法];

说明:
UNIQUE:可选。表示索引为唯一性索引。
FULLTEXT:可选。表示索引为全文索引。
INDEXKEY:用于指定字段为索引,两者选择其中之一就可以了,作用是一样的。
索引名:可选。给创建的索引取一个新名称。
字段名1:指定索引对应的字段的名称,该字段必须是前面定义好的字段。
注:索引方法默认使用B+TREE。

5、聚簇索引和非聚簇索引

​ **聚簇(cu)索引:**将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据(主键索引

​ **非聚簇索引:**将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置(辅助索引

​ 聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。

SQL查询

1、SQL语句的执行过程

查询语句:

select * from student A where A.age='18' and A.name='张三';
img

结合上面的说明,我们分析下这个语句的执行流程:

①通过客户端/服务器通信协议与 MySQL 建立连接。并查询是否有权限

②Mysql8.0之前看是否开启缓存,开启了 Query Cache 且命中完全相同的 SQL 语句,则将查询结果直接返回给客户端;

③由解析器进行语法语义解析,并生成解析树。如查询是select、表名tb_student、条件是id=‘1’

④查询优化器生成执行计划。根据索引看看是否可以优化

⑤查询执行引擎执行 SQL 语句,根据存储引擎类型,得到查询结果。若开启了 Query Cache,则缓存,否则直接返回。

2、回表查询和覆盖索引

普通索引需要扫描两遍索引树

(1)先通过普通索引定位到聚簇索引的值;

(2)再通过聚簇索引值定位到行记录;

这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。

覆盖索引

​ 如果where查询条件字段和被查询的字段在同一索引中,那么不需要回查表。

实现覆盖索引:常见的方法是,将被查询的字段,建立到联合索引里去。

聚簇索引
如果表设置了主键,则主键就是聚簇索引
如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引
以上都没有,则会默认创建一个隐藏的row_id作为聚簇索引

InnoDB的聚簇索引的叶子节点存储的是行记录(其实是页结构,一个页包含多行数据),InnoDB必须要有至少一个聚簇索引。

由此可见,使用聚簇索引查询会很快,因为可以直接定位到行记录。

普通索引

普通索引也叫二级索引,除聚簇索引外的索引,即非聚簇索引。

InnoDB的普通索引叶子节点存储的是聚簇索引的值,而MyISAM的普通索引存储的是记录指针。

示例
建表
mysql> create table user(
    id int(10) auto_increment,
    name varchar(30),
    age tinyint(4),
    primary key (id),
    index idx_age (age)
)engine=innodb charset=utf8mb4;

id 字段是聚簇索引,age 字段是普通索引(二级索引)

填充数据
insert into user(name,age) values('张三',30);
insert into user(name,age) values('李四',20);
insert into user(name,age) values('王五',40);
insert into user(name,age) values('刘八',10);

mysql> select * from user;
+----+--------+------+
| id | name  | age |
+----+--------+------+
| 1 | 张三  |  30 |
| 2 | 李四  |  20 |
| 3 | 王五  |  40 |
| 4 | 刘八  |  10 |
+----+--------+------+
复制代码
索引存储结构

id 是主键,所以是聚簇索引,其叶子节点存储的是对应行记录的数据

面试:MySQL篇,详尽知识点总结_第9张图片

普通索引(secondaryIndex)

age 是普通索引(二级索引),非聚簇索引,其叶子节点存储的是聚簇索引的值

面试:MySQL篇,详尽知识点总结_第10张图片

聚簇索引(ClusteredIndex)

如果查询条件为主键(聚簇索引),则只需扫描一次B+树即可通过聚簇索引定位到要查找的行记录数据。

如:select * from user where id = 1;

面试:MySQL篇,详尽知识点总结_第11张图片

普通索引查找过程

如果查询条件为普通索引(非聚簇索引),需要扫描两次B+树,第一次扫描通过普通索引定位到聚簇索引的值,然后第二次扫描通过聚簇索引的值定位到要查找的行记录数据。 如:select * from user where age = 30;

  1. 先通过普通索引 age=30 定位到主键值 id=1
  2. 再通过聚集索引 id=1 定位到行记录数据

普通索引查找过程第一步

面试:MySQL篇,详尽知识点总结_第12张图片

普通索引查找过程第二步

面试:MySQL篇,详尽知识点总结_第13张图片
回表查询

先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低。

索引覆盖

只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。

例如:select id,age from user where age = 10;

如何实现覆盖索引

常见的方法是:将被查询的字段,建立到联合索引里去。

1、如实现:select id,age from user where age = 10;

explain分析:因为age是普通索引,使用到了age索引,通过一次扫描B+树即可查询到相应的结果,这样就实现了覆盖索引

面试:MySQL篇,详尽知识点总结_第14张图片

2、实现:select id,age,name from user where age = 10;

explain分析:age是普通索引,但name列不在索引树上,所以通过age索引在查询到id和age的值后,需要进行回表再查询name的值。此时的Extra列的NULL表示进行了回表查询

面试:MySQL篇,详尽知识点总结_第15张图片

为了实现索引覆盖,需要建组合索引idx_age_name(age,name)

drop index idx_age on user;
create index idx_age_name on user(`age`,`name`);

explain分析:此时字段age和name是组合索引idx_age_name,查询的字段id、age、name的值刚刚都在索引树上,只需扫描一次组合索引B+树即可,这就是实现了索引覆盖,此时的Extra字段为Using index表示使用了索引覆盖。

MySQL 的覆盖索引与回表

参考:https://juejin.cn/post/6844904062329028621

3、Explain及优化

参考:https://www.jianshu.com/p/8fab76bbf448

mysql> explain select * from staff;
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | staff | ALL  | NULL          | 索引  | NULL    | NULL |    2 | NULL  |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
1 row in set

索引优化:

​ ①最左前缀索引:like只用于’string%’,语句中的=和in会动态调整顺序

​ ②唯一索引:唯一键区分度在0.1以上

​ ③无法使用索引:!= 、is null 、 or、>< 、(5.7以后根据数量自动判定)in 、not in

​ ④联合索引:避免select * ,查询列使用覆盖索引

SELECT uid From user Where gid = 2 order by ctime asc limit 10
ALTER TABLE user add index idx_gid_ctime_uid(gid,ctime,uid) #创建联合覆盖索引,避免回表查询

语句优化:

​ ①char固定长度查询效率高,varchar第一个字节记录数据长度

​ ②应该针对Explain中Rows增加索引

​ ③group/order by字段均会涉及索引

​ ④Limit中分页查询会随着start值增大而变缓慢,通过子查询+表连接解决

select * from mytbl order by id limit 100000,10  改进后的SQL语句如下:
select * from mytbl where id >= ( select id from mytbl order by id limit 100000,1 ) limit 10
select * from mytbl inner ori join (select id from mytbl order by id limit 100000,10) as tmp on tmp.id=ori.id;

​ ⑤count会进行全表扫描,如果估算可以使用explain

​ ⑥delete删除表时会增加大量undo和redo日志, 确定删除可使用trancate

表结构优化:

​ ①单库不超过200张表

​ ②单表不超过500w数据

​ ③单表不超过40列

​ ④单表索引不超过5个

数据库范式

​ ①第一范式(1NF)列不可分割

​ ②第二范式(2NF)属性完全依赖于主键 [ 消除部分子函数依赖 ]

​ ③第三范式(3NF)属性不依赖于其它非主属性 [ 消除传递依赖 ]

配置优化:

​ 配置连接数、禁用Swap、增加内存、升级SSD硬盘

4、JOIN查询

left join(左联接) 返回包括左表中的所有记录和右表中关联字段相等的记录

right join(右联接) 返回包括右表中的所有记录和左表中关联字段相等的记录

inner join(等值连接) 只返回两个表中关联字段相等的行

集群

1、主从复制过程

MySQl主从复制:

MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。

  • 原理:将主服务器的binlog日志复制到从服务器上执行一遍,达到主从数据的一致状态。

  • 过程:从节点开启一个I/O线程,向主节点请求bin-log日志文件的指定位置(或者从最开始的日志)之后的内容。主节点开启一个binlog dump线程,检查自己的二进制日志,并发送给从节点;返回信息中除了日志所包含的信息之外,还包括本次返回的信息的bin-log file 的以及bin-log position。从库将接收到的数据保存到中继日志(Relay log)中,并将读取到的binary log文件名和位置保存到master-info 文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log 的哪个位置开始往后的日志内容,请发给我”;另外开启一个SQL线程,把Relay-log中的新增的操作在自身机器上执行一遍。

  • 优点

    • 数据实时备份,当系统中某个节点发生故障时,可以方便的故障切换
    • 读写分离,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。
    • 架构扩展,随着系统中业务访问量的增大,如果是单机部署数据库,就会导致I/O访问频率过高。有了主从复制,增加多个数据存储节点,将负载分布在多个从节点上,降低单机磁盘I/O访问的频率,提高单个机器的I/O性能。
    • 高可用
  • MySQL 主从复制模式

MySQL 主从复制默认是异步的模式。
l 异步模式(mysql async-mode)
在异步复制中,主库执行完操作后,写入binlog日志后,就返回客户端,这一动作就结束了,并不会验证从库有没有收到,完不完整,所以这样可能会造成数据的不一致

面试:MySQL篇,详尽知识点总结_第16张图片

l 半同步模式(mysql semi-sync)
当主库每提交一个事务后,不会立即返回,而是等待所有从库中其中一个从库接收到Binlog并成功写入Relay-log响应后才返回客户端,保证了数据的安全性和一致性,否则则会等待超时切换到异步模式执行。这样做的目的可以使主从数据库的数据延迟缩小,可以提高数据安全性,确保了事务提交后,binlog至少传输到了一个从节点上,不能保证从节点将此事务更新到db中。性能上会有一定的降低,响应时间会变长。如下图所示:

面试:MySQL篇,详尽知识点总结_第17张图片

半同步模式不是mysql内置的,从mysql 5.5开始集成,需要master 和slave 安装插件开启半同步模式。

l 全同步模式
指当主库执行完一个事务,所有的从库也都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响

MySQL 主从复制有三种方式:基于SQL语句的复制(statement-based replication,SBR),基于行的复制(row-based replication,RBR),混合模式复制(mixed-based replication,MBR)。对应的binlog文件的格式也有三种:STATEMENT,ROW,MIXED。

GTID复制模式

@ 在传统的复制里面,当发生故障,需要主从切换,需要找到bin-log和position点,然后将主节点指向新的主节点,相对来说比较麻烦,也容易出错。在MySQL 5.6里面,不用再找binlog和pos点,我们只需要知道主节点的ip,端口,以及账号密码就行,因为复制是自动的,MySQL会通过内部机制GTID自动找点同步。
@ 多线程复制(基于库),在MySQL 5.6以前的版本,slave的复制是单线程的。一个事件一个事件的读取应用。而master是并发写入的,所以延时是避免不了的。唯一有效的方法是把多个库放在多台slave,这样又有点浪费服务器。在MySQL 5.6里面,我们可以把多个表放在多个库,这样就可以使用多线程复制。

  • 基于GTID复制实现的工作原理

  • 主节点更新数据时,会在事务前产生GTID,一起记录到binlog日志中。

  • 从节点的I/O线程将变更的bin log,写入到本地的relay log中。

  • SQL线程从relay log中获取GTID,然后对比本地binlog是否有记录(所以MySQL从节点必须要开启binary log)。

  • 如果有记录,说明该GTID的事务已经执行,从节点会忽略。

  • 如果没有记录,从节点就会从relay log中执行该GTID的事务,并记录到bin log。

  • 在解析过程中会判断是否有主键,如果没有就用二级索引,如果有就用全部扫描。

参考:https://zhuanlan.zhihu.com/p/50597960

3、集群架构

Keepalived + VIP + MySQL 主从/双主

​ 当写节点 Master db1 出现故障时,由 MMM Monitor 或 Keepalived 触发切换脚本,将 VIP 漂移到可用的 Master db2 上。当出现网络抖动或网络分区时,MMM Monitor 会误判,严重时来回切换写 VIP 导致集群双写,当数据复制延迟时,应用程序会出现数据错乱或数据冲突的故障。有效避免单点失效的架构就是采用共享存储,单点故障切换可以通过分布式哨兵系统监控。

img

**架构选型:**MMM 集群 -> MHA集群 -> MHA+Arksentinel。

img

4、故障转移和恢复

转移方式及恢复方法

1. 虚拟IP或DNS服务 (Keepalived +VIP/DNS  和 MMM 架构)

​ 问题:在虚拟 IP 运维过程中,刷新ARP过程中有时会出现一个 VIP 绑定在多台服务器同时提供连接的问题。这也是为什么要避免使用 Keepalived+VIP 和 MMM 架构的原因之一,因为它处理不了这类问题而导致集群多点写入。

2. 提升备库为主库(MHA、QMHA)

​ 尝试将原 Master 设置 read_only 为 on,避免集群多点写入。借助 binlog server 保留 Master 的 Binlog;当出现数据延迟时,再提升 Slave 为新 Master 之前需要进行数据补齐,否则会丢失数据。

面试题

MySQL 是如何保证一致性、原子性和持久性的!

答案:https://cloud.tencent.com/developer/article/1600883

如何进行分库分表

分表用户id进行分表,每个表控制在300万数据。

分库根据业务场景和地域分库,每个库并发不超过2000

Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是各个系统都需要耦合 Sharding-jdbc 的依赖,升级比较麻烦

Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了

分库分表前的问题

任何问题都是太大或者太小的问题,我们这里面对的数据量太大的问题。

用户请求量太大

因为单服务器TPS,内存,IO都是有限的。 解决方法:分散请求到多个服务器上; 其实用户请求和执行一个sql查询是本质是一样的,都是请求一个资源,只是用户请求还会经过网关,路由,http服务器等。

单库太大

单个数据库处理能力有限;单库所在服务器上磁盘空间不足;单库上操作的IO瓶颈 解决方法:切分成更多更小的库

单表太大

CRUD都成问题;索引膨胀,查询超时 解决方法:切分成多个数据集更小的表。

分库分表的方式方法

一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。 我们从面临的问题,开始解决,阐述: 首先是用户请求量太大,我们就堆机器搞定(这不是本文重点)。

然后是单个库太大,这时我们要看是因为表多而导致数据多,还是因为单张表里面的数据多。 如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。

如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。 分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。

垂直拆分
  1. 垂直分表

    也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。

  2. 垂直分库

    垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。

    数据库业务层面的拆分,和服务的“治理”,“降级”机制类似,也能对不同业务的数据分别的进行管理,维护,监控,扩展等。 数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。 数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。

水平拆分
  1. 水平分表

    针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。

  2. 水平分库分表

    将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。

  3. 水平分库分表切分规则

    1. RANGE

      从0到10000一个表,10001到20000一个表;

    2. HASH取模

      一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。

    3. 地理区域

      比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。

    4. 时间

      按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。

分库分表后面临的问题
事务支持

分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

多库结果集合并(group by,order by)

TODO

跨库join

TODO 分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 粗略的解决方法: 全局表:基础数据,所有库都拷贝一份。 字段冗余:这样有些字段就不用join去查询了。 系统层组装:分别查询出所有,然后组装起来,较复杂。

参考:https://juejin.cn/post/6844903648670007310#heading-0

不是写瓶颈优先进行分表

  • 分库数据间的数据无法再通过数据库直接查询了。会产生深分页的问题

  • 分库越多,出现问题的可能性越大,维护成本也变得更高。

  • 分库后无法保障跨库间事务,只能借助其他中间件实现最终一致性。

分库首先需考虑满足业务最核心的场景:

1、订单数据按用户分库,可以提升用户的全流程体验

2、超级客户导致数据倾斜可以使用最细粒度唯一标识进行hash拆分

3、按照最细粒度如订单号拆分以后,数据库就无法进行单库排重了

三个问题:

  • 富查询:采用分库分表之后,如何满足跨越分库的查询?使用ES的宽表

    借助分库网关+分库业务虽然能够实现多维度查询的能力,但整体上性能不佳且对正常的写入请求有一定的影响。业界应对多维度实时查询的最常见方式便是借助 ElasticSearch

  • 数据倾斜:数据分库基础上再进行分表

  • 分布式事务:跨多库的修改及多个微服务间的写操作导致的分布式事务问题?

  • 深分页问题:按游标查询,或者叫每次查询都带上上一次查询经过排序后的最大 ID

如何将老数据进行迁移

双写不中断迁移

  • 线上系统里所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改
  • 系统部署以后,还需要跑程序读老库数据写新库,写的时候需要判断updateTime,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写
  • 循环执行,直至两个库的数据完全一致,最后重新部署分库分表的代码就行了

参考:https://www.pianshen.com/article/9609846571/

如何生成自增的id主键

  • 使用redis可以
  • 并发不高可以单独起一个服务,生成自增id
  • 设置数据库step自增步长可以支撑水平伸缩
  • UUID适合文件名、编号,但是不适合做主键
  • snowflake雪花算法,综合了41时间(ms)、10机器12序列号(ms内自增)

其中机器预留的10bit可以根据自己的业务场景配置

线上故障及优化

更新失败 | 主从同步延时

以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。

是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。

我们通过 MySQL 命令:

show slave status

查看 Seconds_Behind_Master ,可以看到从库复制主库的数据落后了几 ms。

一般来说,如果主从延迟较为严重,有以下解决方案:

  • 分库,拆分为多个主库,每个主库的写并发就减少了几倍,主从延迟可以忽略不计。
  • 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
  • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库或者延迟查询。主从复制延迟一般不会超过50ms

应用崩溃 | 分库分表优化

​ 我们有一个线上通行记录的表,由于数据量过大,进行了分库分表,当时分库分表初期经常产生一些问题。典型的就是通行记录查询中使用了深分页,通过一些工具如MAT、Jstack追踪到是由于sharding-jdbc内部引用造成的。

​ 通行记录数据被存放在两个库中。如果没有提供切分键,查询语句就会被分发到所有的数据库中,比如查询语句是 limit 10、offset 1000,最终结果只需要返回 10 条记录,但是数据库中间件要完成这种计算,则需要 (1000+10)*2=2020 条记录来完成这个计算过程。如果 offset 的值过大,使用的内存就会暴涨。虽然 sharding-jdbc 使用归并算法进行了一些优化,但在实际场景中,深分页仍然引起了内存和性能问题。

​ 这种在中间节点进行归并聚合的操作,在分布式框架中非常常见。比如在 ElasticSearch 中,就存在相似的数据获取逻辑,不加限制的深分页,同样会造成 ES 的内存问题。

业界解决方案:

方法一:全局视野法

(1)将order by time offset X limit Y,改写成order by time offset 0 limit X+Y

(2)服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录

这种方法随着翻页的进行,性能越来越低。

方法二:业务折衷法-禁止跳页查询

(1)用正常的方法取得第一页数据,并得到第一页记录的time_max

(2)每次翻页,将order by time offset X limit Y,改写成order by time where time>$time_max limit Y

以保证每次只返回一页数据,性能为常量。

方法三:业务折衷法-允许模糊数据

(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y/N

方法四:二次查询法

(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y

(2)找到最小值time_min

(3)between二次查询,order by time between $time_min and $time_i_max

(4)设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset

(5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y

查询异常 | SQL 调优

分库分表前,有一段用用户名来查询某个用户的 SQL 语句:

select * from user where name = "xxx" and community="other";

为了达到动态拼接的效果,这句 SQL 语句被一位同事进行了如下修改。他的本意是,当 name 或者 community 传入为空的时候,动态去掉这些查询条件。这种写法,在 MyBaits 的配置文件中,也非常常见。大多数情况下,这种写法是没有问题的,因为结果集合是可以控制的。但随着系统的运行,用户表的记录越来越多,当传入的 name 和 community 全部为空时,悲剧的事情发生了:

select * from user where 1=1

数据库中的所有记录,都会被查询出来,载入到 JVM 的内存中。由于数据库记录实在太多,直接把内存给撑爆了。由于这种原因引起的内存溢出,发生的频率非常高,比如导入Excel文件时。

通常的解决方式是强行加入分页功能,或者对一些必填的参数进行校验

img

Controller 层

现在很多项目都采用前后端分离架构,所以 Controller 层的方法,一般使用 @ResponseBody 注解,把查询的结果,解析成 JSON 数据返回。这在数据集非常大的情况下,会占用很多内存资源。假如结果集在解析成 JSON 之前,占用的内存是 10MB,那么在解析过程中,有可能会使用 20M 或者更多的内存

因此,保持结果集的精简,是非常有必要的,这也是 DTO(Data Transfer Object)存在的必要。互联网环境不怕小结果集的高并发请求,却非常恐惧大结果集的耗时请求,这是其中一方面的原因。

Service 层

Service 层用于处理具体的业务,更加贴合业务的功能需求。一个 Service,可能会被多个 Controller 层所使用,也可能会使用多个 dao 结构的查询结果进行计算、拼装。

int getUserSize() {
        List<User> users = dao.getAllUser();
        return null == users ? 0 : users.size();
}

代码review中发现了定时炸弹,这种在数据量达到一定程度后,才会暴露问题。

ORM 层

比如使用Mybatis时,有一个批量导入服务,在 MyBatis 执行批量插入的时候,竟然产生了内存溢出,按道理这种插入操作是不会引起额外内存占用的,最后通过源码追踪到了问题。

这是因为 MyBatis 循环处理 batch 的时候,操作对象是数组,而我们在接口定义的时候,使用的是 List;当传入一个非常大的 List 时,它需要调用 List 的 toArray 方法将列表转换成数组(浅拷贝);在最后的拼装阶段,又使用了 StringBuilder 来拼接最终的 SQL,所以实际使用的内存要比 List 多很多。

事实证明,不论是插入操作还是查询动作,只要涉及的数据集非常大,就容易出现问题。由于项目中众多框架的引入,想要分析这些具体的内存占用,就变得非常困难。所以保持小批量操作和结果集的干净,是一个非常好的习惯。

你可能感兴趣的:(MySQL,mysql,面试)