MySQL数据库
一、MySQL基础
1、什么是关系型数据库?
关系型数据库是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)
关系型数据库中,我们的数据都被存放在各种表中,表中的每一行存放着一条数据
常见的关系型数据库有:MySQL,SQL Server ,Oracle ,PostgreSQL
2、MySQL是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户消息
优点:
- 成熟稳定,功能完善
- 开源免费
- 文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习
- 操作简单,维护成本低
- 兼容性好,支持常见的操作系统,支持多种开发语言
- 社区活跃,生态完善
- 支持分库分表,读写分离,高可用
二、MySQL字段类型
-
数值类型:
整型:tinyint,smallint,mediumint,int,bigint
浮点型:float,double
定点型:decimal
-
字符串类型:
常用:char,varchar
text类:tinytext,text,mediumtext,longtext
blob类(二进制):tinyblob,blob,mediumblob,longblob
-
日期时间类型:
常用:year,time,date,datetime,timestamp
1、char和varchar区别?
char 是定长字符串,char在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;char更适合存储长度较短或者长度都差不多 的字符串,如加密后的密码,身份证号码
varchar 是变长字符串,在存储时需要使用1或2个额外字节记录字符串的长度,检索时不需要处理。varchar类型适合存储长度不确定或者差异较大 的字符串,如用户昵称,文章标题
varchar(100)和varchar(10)的区别是什么?
varchar(100)和varchar(10)都是变长类型,表示能存储最多100个和10个字符。因此varchar(100)可以满足更大范围的字符存储需求,有更好的业务拓展性。虽然二者能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的 。不过varchar(100)会消耗更多的内存,这是因为varchar类型在内存中操作时,通常会分配固定大小的内存块来保存值,即用字符类型中定义的长度。
2、decimal和float/double的区别?
decimal 是定点数,可以存储精确的小数值,一般用于存储有精度要求 的小数比如与金钱相关的数据,可以避免浮点数带来的精度损失
float/double 是浮点数,只能存储近似的小数值
3、datetime和timestamp的区别?
datetime 类型没有时区信息,需要消耗8个字节的存储空间,但是表示的时间范围更大
timestamp 和时区有关,只需要使用4个字节的存储空间,表示的时间范围更小
不要用字符串存储日期,因为字符串占用的空间很大,字符串存储的日期效率比较低,因为无法用日期相关的API进行计算和比较;
一般推荐使用Timestamp来存储时间 。因为它比Datetime空间效率更高,而用整数保存时间戳的格式存储时间通常不方便处理。
4、NULL和 ’ ‘ (空字符串)的区别是什么
- NULL代表一个不确定的值,就算是两个NULL,他俩也不一定相等。
- 空字符串的长度是0,是不占用空间的,而NULL是需要占用空间的
- NULL会影响聚合函数的结果,例如COUNT的处理结果会统计所有的记录数包括null
- 查询NULL值时,必须使用IS NULL 或 IS NOT NULL 来判断,而不能使用=、!=、<、>之类的比较运算符,而空字符串是可以使用这些比较运算符的
为什么MySQL不建议使用NULL作为列的默认值?
语义模糊:NULL的含义不明确,它可以表示缺失的数据、未知的数据或者值为NULL的数据。因此,将NULL作为列的默认值可能会导致数据的歧义和混淆。
索引失效:使用NULL作为列的默认值可能会导致索引无效,进而影响查询性能。对于包含NULL值的列,MySQL需要额外的处理来处理NULL值的逻辑,这会增加查询的开销,并且可能导致索引失效。
数据一致性问题:将NULL作为列的默认值可能导致数据一致性问题。例如,如果某个列的默认值为NULL,当用户忘记在插入数据时为该列赋值时,就会导致数据不一致。
为了避免以上问题,MySQL建议将具有明确语义的默认值用于列,而不是使用NULL。例如,对于数值类型的列,可以设置默认值为0或者-1 ;对于日期和时间类型的列,可以设置默认值为当前时间 等。这样做不仅可以保证数据的一致性和语义的明确性,还可以提高查询性能和索引的有效性
三、MySQL基础架构
1、MySQL基础架构分析
从上图可以看出, MySQL 主要由下面几部分构成:
- 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作。如果账号密码校验通过,就会到权限表中查询该用户的所有权限。验证用户是否具有连接数据库的权限
- 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
查询缓存主要用来缓存我们所执行的SELECT语句以及该语句的结果集。
连接建立后,执行查询语句的时候,会先查询缓存,缓存是以key-value的形式缓存在内存中,key是查询语句,value是结果集。如果缓存击中,直接返回结果集给客户端;如果没击中继续走后面的组件,完成后也会将结果缓存起来供下一次使用
MySQL查询不建议使用缓存,因为查询缓存失效在实际业务场景中会非常频繁 ,当你对一个表更新时,这个表上的所有缓存数据都会消失,因此该功能的使用场景很少(仅对于不经常更新的数据来说),所以在MySQL8.0版本后将缓存的功能删除了
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
词法分析:一条sql语句有多少个字符串组成,提取关键字,select,查询的表,字段名等等
语法分析:判断你输入的sql语句是否正确,是否符合MySQL的语法
- 优化器: 按照 MySQL 认为最优的方案去执行。
按照它认为的最优执行方案去执行(有时候也并不是最优的)。例如多个索引的时候该如何选择索引,多表查询时如何选择关联顺序等
- 执行器: 当选择了执行方案后,MySQL就准备开始执行了。执行语句,然后从存储引擎返回数据。
执行器在执行SQL语句前会对涉及到的表进行权限验证,确保用户具有执行该语句所需的所有权限,没有权限会返回错误消息,有权限则调用引擎的接口,返回接口执行的结果
- 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎
2、语句分析
SQL可以分为两种,一种是查询,一种是更新(增加、修改、删除)。
查询语句
select * from tb_student A where A.age='18' and A.name=' 张三 ';
执行过程为:
- 先身份认证和权限认证,如果没有连接数据库的权限直接返回错误信息,如果有权限,在MySQL8之前,会先查询缓存,以这条SQL语句为key在内存中查询是否有结果,如果有直接返回缓存结果,如果没有执行下一步
- 通过分析器进行词法分析,提取SQL语句的关键元素,例如上例的 select 、tb_student、A.age=18等等。然后判断这个SQL语句是否有语法错误,比如关键词是否正确等,如果检查没问题就执行下一步
- 通过优化器进行确定执行方案,例如本例中,可以先找年龄为18的然后找名字为张三的,也可以先找名字为张三的学生然后匹配年龄为18的。优化器根据自己的优化算法进行选择执行效率最好的一个方案。然后就准备开始执行了
- 执行器对用户是否具有执行该语句的全部权限作验证,如果没有权限直接返回错误信息;如果有权限,则调用数据库引擎接口,返回引擎的执行查询结果
更新语句
update tb_student A set A.age='19' where A.name=' 张三 ';
执行流程基本上也是走上面的那个流程,但是执行更新时会记录日志:
- 连接验证
- 先做查询操作----- select * from tb_studnt A where A,name = ‘张三’
(MySQL8之前)先查询缓存,如果有缓存直接使用缓存,如果没有缓存,就继续执行接下来的操作,分析器分析,然后优化器优化,然后执行器执行,从引擎返回查询结果
- 拿到查询的语句,把age改为19,然后调用引擎API接口,写入这一行数据,InnoDB引擎把数据保存在内存中,同时记录redo log(重做日志,InnoDB引擎自带),此时redo log 进入prepare状态 ,然后告诉执行器,执行完成,随时可以提交
- 执行器收到通知后记录binlog(归档日志,MySQL自带),然后调用引擎接口提交, redo log 为提交状态
-
为什么用两个日志
最开始MySQL并没有InnoDB引擎,(InnoDB引擎是其他公司以插件形式插入MySQL的),之前MySQL就是采用binlog的日志模块。现在引入InnoDB引擎后,redo log是InnoDB引擎特有的(自带的),InnoDB引擎就是通过redo log来支持事务的,有crash—safe的能力(数据库发生重启时之前提交的记录会恢复),所以才有了两个日志模块
为什么redo log要先进入prepare状态,然后再是提交状态?
现在的顺序是,先写redo log,但是不立马提交,而是记录binlog之后,再提交redo log。
如果写完redo log后立马提交,然后再写binlog,假设写完redo log后立马提交,然后机器挂了,binlog日志并没有写入 ,那么重启,通过redo log可以恢复数据,但是binlog没有记录该数据,后续机器备份时会丢失这一条数据
如果写完binlog 然后写redolog,写完binlog时机器异常重启,由于没有redolog本机无法恢复这一条记录,但是binlog又有记录,会造成数据不一致
其实就是为了保证binlog和redo log都要有数据,要么就都没有,要么都有,binlog的数据是为后续机器备份使用,而redo log的数据是机器重启后恢复使用
写完 binlog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性 (恢复也能恢复,备份也有记录)。
假设 redo log 处于预提交状态,binlog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:
- 判断 redo log 是否完整,如果判断是完整的,就立即提交。
- 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。
四、MySQL存储引擎
1、MySQL当前默认的存储引擎是InnoDB,并且所有的存储引擎中只有InnoDB是事务性存储引擎,只有InnoDB支持事务。(MySQL 5.5.5之前,MyISAM是MySQL的默认存储引擎,在这之后,InnoDB是MySQL的默认存储引擎)
2、MySQL存储引擎采用的是插件式架构 ,支持多种存储引擎,我们甚至可以为不同数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的而不是数据库。
InnoDB刚开始就是一个第三方存储引擎,后面过于优秀,然后被Oracle直接收购
3、MyISAM和InnoDB存储引擎有什么区别
- 是否支持行级锁
MyISAM支持表级锁,不支持行级锁 ;
而InnoDB两者都支持,默认是行级锁。
MyISAM一锁就是锁住了整张表,并发性非常不好;相比之下InnoDB在并发的情况下性能更好
- 是否支持事务
MyISAM不提供事务支持 。
InnoDB提供事务支持,实现了SQL标准定义的四个隔离级别,具有commit和rollback事务的能力。
- 是否支持外键
MyISAM不支持,而InnoDB支持
- 是否支持数据库异常崩溃后的安全恢复
MyISAM不支持,而InnoDB支持
使用InnoDB的数据库在异常崩溃后,数据库重新启动时会保证数据库恢复到崩溃前的状态,依赖于redo log
- 是否支持MVVC
MyISAM不支持,InnoDB支持
- 索引实现不一样
MyISAM和InnoDB都是使用B+Tree作为索引结构,但是两者的实现方式不太一样
- 性能
InnoDB的性能比MyISA更强大
总之,综合下来,各方面InnoDB都比MyISAM强大,这也是为什么后面MySQL将InnoDB作为默认存储引擎的原因。
五、MySQL查询缓存
执行查询语句的时候,会先查询缓存。在MySQL8版本后移除。
my.conf加入以下配置,重启MySQL开启查询缓存
query_cache_type=1
query_cache_size=600000
MySQL执行以下命令也可以开启查询缓存
set global query_cache_type=1;
set global query_cache_size=600000;
开启查询缓存后,在同样的查询条件以及数据情况 下,会直接在缓存中返回结果。
查询缓存不命中的情况:
- 任何两个查询在任何字符上的不同都会导致缓存不命中
- 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存
- 缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表发生变化,那么和这张表相关的所有缓存数据都将失效
缓存虽然能提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁,尤其是对写密集的场景,开启查询缓存要谨慎!
如果开启,要注意合理控制缓存空间大小。此外,还可以通过sql_cache 和 sql_no_cache来控制某个查询语句是否需要缓存:
SELECT sql_no_cache COUNT(*) FROM usr;
六、MySQL日志
MySQL中常见的日志类型主要有下面几类(这里是以InnoDB存储引擎为例):
- 错误日志(error log) :对MySQL的启动、运行、关闭过程进行了记录
- 二进制日志(binary log ,binlog) :主要记录的是更改数据库数据的SQL语句
- 一般查询日志(general query log) :已建立连接的客户端发送给MySQL服务器的所有SQL记录,因为SQL的量比较大,默认是不开启的,也不建议开启
- 慢查询日志(slow query log) :执行时间超过long_query_time 的查询,解决SQL慢查询问题的时候会用到
- 事务日志(redo log 和 undo log) :redo log是重做日志,undo log是回滚日志
- 中继日志(relay log) :relay log 是复制过程中产生的日志,undo log是回滚日志
- DDL日志(metadata log) :DDL语句(用于定义和管理数据库对象,如表、索引、视图等)执行的元数据操作
6.1、slow query log
1、慢查询日志(slow query log)
慢查询日志有什么用?
慢查询日志记录了执行时间超过 long_query_time (默认是10s,可以自定义)的所有查询语句,在解决SQL慢查询(SQL执行时间过长)问题的时候经常用到
找到慢SQL是优化SQL语句性能的第一步,然后再用EXPLAIN命令可以对慢SQL进行分析,获取执行计划的相关信息。
-- 查看慢查询日志是否开启,默认是关闭的
show variables like 'slow_query_log';
-- 开启慢查询日志
set global slow_query_log = ON ;
-- 查看long_query_time,默认是10s
show variables like '%long_query_time%';
-- 设置long_query_time,设置为1s
set global long_query_time = 1;
-- 查询当前慢查询语句的个数
show global status like '%Slow_queries%';
在实际项目中,慢查询日志可能会比较大,直接分析的话并不方便,我们可以借助MySQL官方的慢查询分析调优工具 mysqldumpslow
6.2、binlog
2、binlog(二进制日志)
binlog是什么?
binlog即二进制日志文件,主要记录了对MySQL数据库执行了更改的所有的操作 ,比如数据库执行的所有DDL和DML语句,包括表结构更改(create,alter,drop table)和 表数据修改(insert,update,delete),但不包括select、show 这类不会对数据库造成更改的操作
可以使用 show binary logs; 命令查看所有二进制日志列表
binlog通过追加的方式进行写入,大小没有限制。并且,我们可以通过max_binlog_size 参数设置每个binlog文件的最大容量,当文件大小达到给定值之后,会生成新的binlog文件 来保存日志,不会出现前面写的日志被覆盖的情况
binlog的模式有哪几种?
- Statement模式:每一条会修改数据的sql都会被记录在binlog中
- Row模式(推荐):每一行的具体变更事件都会被记录在binlog中
- Mixed模式:Statement模式和Row模式的混合。默认使用Statement模式,少数特殊场景自动切换到Row模式
相比较于Row模式来说,Statement模式下的日志文件更小,磁盘IO压力也较小,性能更好;但是其准确性相比于Row模式要差
binlog主要用来做什么?
binlog最主要的应用场景是主从复制 ,需要依靠binlog来同步数据,保证数据一致性
主从复制的原理:
主库将数据库中数据的变化写入到binlog
从库连接主库
从库会创建一个I/O线程向主库请求更新的binlog
主库会创建一个binlog dump 线程来发送binlog ,从库中的I/O线程负责接收
从库的I/O线程将接收的binlog写入到relay log中
从库的SQL线程读取relay log 同步数据本地
binlog的刷盘时机如何选择?
对于InnoDB存储引擎来说,事务在执行过程中,会先把日志写入到binlog cache中,只有在事务提交的时候,才会把binlog cache 中的日志持久化到磁盘上的binlog文件中 。
因为一个事务的binlog不能拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache ,事务提交的时候将binlog cache 刷到磁盘。
可以通过sync_binlog 参数控制binlog的刷盘时机,默认是0:
- 0:不去强制要求,由系统自行判断何时写入磁盘
- 1:每次提交事务的时候都要将binlog写入磁盘
- N:每N个事务,才会将binlog写入磁盘
MySQL5.7之前,默认是0,之后默认是1
什么情况下会重新生成binlog?
当遇到以下3种情况时,MySQL会重新生成一个新的日志文件,文件序号递增:
- MySQL服务器停止或重启;
- 使用flush logs 命令后;
- binlog 文件大小超过 max_binlog_size变量的阈值后
6.3、redo log
3、redo log(重做日志)
redo log 如何保证事务的持久性?
MySQL 的InnoDB引擎使用redo log来保证事务的持久性。redo log主要做的事情就是记录页的修改,比如某个页面某个偏移量处改了几个字节的值以及具体修改的内容是什么。redo log中的每一条记录包含了表空间浩、数据页号、偏移量、具体修改的数据 ,甚至还可能会记录修改数据的长度(取决于redo log 类型)。redo log先存在log buffer区,然后根据刷盘策略在对应的时机刷到磁盘上。
在事务提交时,我们会将redo log 按照刷盘策略刷到磁盘上去,这样即使MySQL宕机,重启之后也能恢复未能写入磁盘的数据,从而保证事务的持久性。
InnoDB将 redo log 刷到磁盘上有这几种情况:
- 事务提交:当事务提交时,log buffer 里的redo log会被刷新到磁盘
- log buffer 空间不足时:log buffer中缓存的redo log 已经占满了log buffer 总容量的大约一半左右 ,就需要把这些日志刷新到磁盘上
- Checkpoint(检查点):InnoDB定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入到磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性
- 后台刷新线程:InnoDB启动了一个后台线程,负责周期性(每隔1s)地将脏页(已经修改但未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。也就是说一个没有提交事务的redo log记录,也可能会被刷盘
- 正常关闭服务器:MySQL关闭的时候,redo log都会刷入到磁盘
redo log的刷盘策略:通过设置参数innodb_flush_log_at_trx_commit来设置策略:
- 0:设置为0时,表示每次事务提交不进行刷盘操作。性能最高,但是不安全,因为如果MySQL挂了或宕机,可能会丢失最近1s的事务
- 1:设置为1的时候,表示每次事务提交时都将进行刷盘操作,这种方式性能最低,但是最安全,只要事务提交成功,redo log就一定在磁盘里,不会有任何数据丢失。
- 2:设置为2的时候,表示每次事务提交都只把log buffer 里的redo log内容写入page cache(文件系统缓存)。
性能:0>2>1 ,安全性:1>2>0
刷盘策略innodb_flush_log_at_trx_commit默认为1,为了保证事务的持久性,设置为1的时候才不会丢失任何数据
什么情况下会出现数据丢失?
redo log写入log buffer 但还未写入page cache,此时数据库崩溃,就会出现数据丢失(策略为0时,会出现这种数据丢失)
redo log已经写入page cache 但还未写入磁盘,操作系统崩溃,也可能出现数据丢失(刷盘策略为2时,可能会出现这种数据丢失)
页修改之后为什么不直接刷盘,而是通过记录在redo log 然后刷盘?
性能非常差。InnoDB页的大小一般为16kb,页是磁盘和内存交互的基本单位 。这就导致即使我们只修改了页中的几个字节数据,如果直接刷到磁盘需要将16kb大小的页整个都刷新到磁盘中 ,性能非常差
采用redo log 的方式可以避免这种性能问题,因为redo log 的刷盘性能很好,redo log 的写入属于顺序 IO,一行redo log 记录只占几十个字节
binlog 和 redo log 有什么区别?
- binlog主要用于数据库还原,属于数据库级别的数据恢复,主从复制是binlog最常见的一个应用场景;redo log 主要用于保证事务的持久性,属于事务级别的数据恢复
- redo log 是属于InnoDB引擎特有的;而binlog是所有存储引擎都有的,因为binlog是MySQL的Server层实现的
- redo log属于物理日志,主要记录的是某个页的修改;binlog属于逻辑日志,主要记录的是数据库执行的所有DDL和DML语句
- binlog通过追加的方式进行写入,大小没有限制;redo log采用循环写的方式进行写入,大小固定,当写到结尾时,会回到开头 循环写日志
6.4、undo log
4、undo log(撤销日志)
undo log 如何保证事务的原子性?
每一个事务对数据的修改 都会被记录到 undo log,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL可以利用 undo log将数据恢复到事务开始之前的状态
undo log 属于逻辑日志,记录的是SQL语句,比如说事务执行一条delete语句,那undo log 就会记录一条相对应的insert 语句(插入一条修改记录,用的sql语句来记录)
除了保证事务的原子性,undo log 还有什么用?
InnoDB存储引擎中MVCC的实现用到了 undo log 。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo log 读取之前的行版本信息,以此实现非锁定读取
七、事务
1、事务概念
1、事务是什么?
事务是逻辑上的一组操作,要么都执行,要么都不执行
例如在转账操作中,A给B转账,使得A的钱减少,B的钱增加,事务会把这两个操作可以看成逻辑上的一个整体,要么都成功(A减少B增加),要么就都不成功 。不存在A减少了B却增加的情况,那样不合理
2、数据库事务
大多数情况下,如果没有特指分布式事务,往往指的就是数据库事务
分布式事务和数据库事务有以下区别:
作用范围:数据库事务通常是在单个数据库中执行的,而分布式事务涉及多个数据库或服务 之间的操作。
数据一致性 :数据库事务可以通过ACID特性(原子性、一致性、隔离性、持久性)来确保数据的一致性,而在分布式事务中,需要使用额外的机制来保证数据的一致性,如两阶段提交协议或三阶段提交协议等。
故障处理:数据库事务通常在单个数据库中执行,如果发生故障,可以使用数据库的回滚和日志恢复机制来回滚事务 ,保证数据的一致性。而在分布式事务中,由于涉及多个参与者,需要使用分布式事务协议 来处理故障,确保数据的一致性。
性能开销:由于涉及多个参与者和网络通信,分布式事务通常比单个数据库事务的性能开销要高。
综上所述,分布式事务相较于数据库事务更复杂且需要额外的机制来保证数据的一致性,但也使得分布式系统能够实现更高的可伸缩性和可用性。
事务的ACID特性
- Atomicity:原子性。事务是最小的执行单位,不可分割。确保动作要么全部完成,要么完全不起作用(如果能拆解,会出现动作没完成但是里面的小动作完成)。事务在执行过程中发生错误,会回滚到事务开始前的状态。
- Consistency:一致性。执行事务前后,数据保持一致。数据不会凭空消失,A钱减少,那么相应的B钱就会增加
- Isolation:隔离性。并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离级别有 RU(读未提交),RC(读提交),RR(可重复读),Serializable(串行化)
- Durability:持久性。一个事务被提交后,它对数据库中的数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保证 ,(A,I,D)–> C
2、并发事务
3、并发事务会带来什么问题?
在典型的引用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发事务可能会导致以下问题:
- 脏读
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,如果此时另外一个事务读取了这个还未提交的数据,而第一个事务突然回滚,导致数据并没有提交到数据库 ,那么第二个事务读取的就是脏数据(数据库中并不存在的数据)。
- 丢失修改
在一个事务读取一个数据时,另一个事务也读取该数据,在第一个事务中修改了这个数据,然后第二个事务也修改了这个数据,这样第一个事务内的修改结果就会丢失
- 不可重复读
在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据,那么在第一个事务中的两次读数据之间,由于第二个事务对这个数据进行修改导致第一个事务两次读取的数据不一致 ,称为不可重复读
- 幻读
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时,第一个事务再次读取时就会发现多了一些原本不存在的记录 ,称为幻读
幻读其实可以看作是不可重复读的一种特殊情况。幻读的重点在于第二次读取数据时发现查到的记录增加 。
4、并发事务的控制方式有哪些
MySQL中并发事务的控制方式无非就两种:锁和MVCC。锁可以看做是悲观控制的模式,MVCC可以看作是乐观控制的模式.
锁 控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL主要通过读写锁来实现并发控制
- 共享锁(S锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取
- 排他锁(X锁):又称写锁/独占锁,事务在修改记录的时候获取排它锁,不允许多个事务同时获取 。如果一个记录已经被加了排他锁,其他事务不能再对这条记录加任何类型的锁
根据锁粒度不同,又被分为表级锁和行级锁 ,InnoDB二者都支持,默认是行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说,InnoDB性能更高。不论是表级锁还是行级锁,都存在共享锁和独占锁这两种。
MVCC(多版本并发控制) :使用版本来控制并发情况下的数据问题 ,在B事务开始修改账户且事务未提交时,A事务需要读取账户余额时,此时会读取到B事务修改操作之前的账户余额的副本数据 ,但是如果A事务需要修改账户余额数据就必须要等待B事务提交事务 。能读不能写。
MVCC使得数据库读不会对数据库加锁,普通的select请求不会加锁,提高了数据库的并发处理能力。 MVCC只在RC和RR两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容。
3、MVCC
5、InnoDB存储引擎对MVCC的实现
5.1、MVCC是什么?
MVCC是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现,当一个事务要对数据库中的数据进行修改时,MVCC会为该事务创建一个数据快照,而不是直接修改实际的数据行
读操作 :当一个事务执行读操作时,它会使用快照读取,快照读取是基于事务开始时数据库中的状态 创建的,因此事务不会读取其他事务尚未提交的修改,具体如下:
- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取
- 如果某个数据行有多个版本,事务会选择不晚于其开始时间 的最新版本,确保事务只读取在它开始之前已经存在的数据
- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作
写操作 :当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体如下:
- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本
- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据
- 原始版本的数据仍然存在,供其他事务使用快照读取 ,这保证了其他事务不受当前事务的写操作影响
事务提交和回滚 :
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见
版本的回收 :
为了防止数据库中的版本无限增长,MVCC会定期进行版本的回收,回收机制会删除已经不再需要的旧版本数据,从而释放空间
MVCC通过创建数据的多个版本和使用快照来实现并发控制,读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用(其他读操作可以照常进行)。这样不同的事务可以在一定程度上并发执行,从而不会相互干扰,提高了数据库的并发性能和数据一致性。
5.2、一致性非锁定读和锁定读
一致性非锁定读:
对于一致性非锁定读的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号+1或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行对比,如果记录的版本小于可见版本 ,则表示该记录可见
锁定读:
锁定读下,读取的是数据的最新版本,这种读也被称为当前读(current read) 。锁定度会对读取到的记录加锁。
在一致性非锁定读下,即使读取的记录已被其他事务加上写锁,这时记录也是可以被读取的,即读取的是快照数据。在Repeatable Read 下,MVCC防止了部分幻读,部分指的就是在一致性非锁定读的情况下防止了幻读 ,只能读取第一次查询之前所插入的数据(如果在查询后插入的数据,版本号大于可见版本,是不可见的,第二次读取的仍然是第一次读到的数据,不会幻读)。但是如果是在锁定读的情况下,每次读取的都是最新数据,如果两次查询中有其他事务插入数据,就会产生幻读 。
在Repeatable Read 和 Read Committed 两个隔离级别下,如果是执行普通的select语句,则会使用一致性非锁定锁(MVCC) ,所以在Repeatable Read 下MVCC实现了可重复读和防止部分幻读
5.3、InnoDB存储引擎对MVCC的实现
MVCC的实现依赖于:隐藏字段、Read View 、undo log 。
在内部实现中,InnoDB通过数据行的DB_TRX_ID和Read View 来判断数据的可见性 ,如不可见,则通过数据行的DB_ROLL_PTR找到undo log 中的历史版本 。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。
隐藏字段
在内部,InnoDB存储引擎为每行数据添加了三个隐藏字段:
- DB_TRX_ID :表示最后一次插入或更新该行的事务id。每开始一个新的事务,都会自增产生一个新的事务id
- DB_ROLL_PTR:回滚指针,指向改行的undo log
- DB_ROW_ID:如果没有设置主键且该表没有唯一非空索引时,InnoDB会使用该id来生成聚簇索引
ReadView
主要用来做可见性判断,里面保存了“当前对本事务不可见的其他活跃事务”。对于RC和RR隔离级别的实现就是通过上面的版本控制来完成,两种隔离级别下的核心处理逻辑是判断所有版本中哪个版本是当前事务可见的处理,针对这个问题InnoDB在设计上增加了ReadView ,ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,命名为m_ids
- 如果被访问版本的 trx_id 属性值⼩于 m_ids 列表中最⼩的事务id,表明⽣成该版本的事务在⽣成ReadView 前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值⼤于 m_ids 列表中最⼤的事务id,表明⽣成该版本的事务在⽣成ReadView 后才⽣成,所以该版本不可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值在 m_ids 列表中最⼤的事务id和最⼩事务id之间,那就需要判断⼀下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时⽣成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时⽣成该版本的事务已经被提交,该版本可以被访问
undo-log
- 当事务回滚时用于将数据恢复到修改前的样子;
- MVCC,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log 读取之前的版本数据,以此实现非锁定读
在InnoDB存储引擎中undo log 分为两种:
- insert undo log :指在insert操作中产生的 undo log 。因为insert 操作的记录只对事务本身可见,对其他事务不可见,故该undo log 可以在事务提交后直接删除
- update undo log :update 或 delete 操作中产生的undo log ,该 undo log可能需要提供MVCC机制,不能在事务提交时就进行删除,提交时放入 undo log 链表。
5.4、MVCC+Next-key-Lock(临键锁) 防止幻读
InnoDB存储引擎默认隔离级别是RR(可重复读),在这种情况下通过MVCC,可以防止脏读和不可重复读的问题,也可以解决部分幻读 的问题----部分是因为在非锁定读的情况下,可以通过版本机制防止幻读,但是在锁定读的情况下,仍然会有两次查询中间有事务添加数据导致两次查询结果不一样而产生的幻读。
在锁定读的情况下,也叫当前读,因为每次读取的都是当前最新版本的数据,InnoDB使用间隙锁来防止这种情况下可能发生的幻读,当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙 ,防止其他事务在查询范围内插入数据。
总结
所谓的MVCC指的就是在使用RR和RC这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样可以使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。
在MySQL中,RC和RR隔离级别的一个非常大的区别就是,它们生成的ReadView的时机不同,在RC中每次查询都会生成一个实时的ReadView ,做到保证每次提交后的数据是处于当前的可见状态;而RR中,在当前事务第一次查询时生成当前的ReadView ,并且当前的ReadView会一直沿用到当前事务提交,以此来保证可重复读
4、MySQL的隔离级别
6、SQL标准定义了四个隔离级别:
Read-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能导致脏读、幻读或不可重复读
READ-COMMITED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读) :对同一字段的多次读取结果是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读 ,但幻读仍可能发生(在当前读的情况下,通过加临键锁解决幻读)
SERIALIZABLE(可串行化): 最高的隔离级别。将整个表锁起来,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,可以防止脏读、不可重复读、幻读。所有的数据库的读或者写操作都为串行执行 ,当前隔离级别下只支持单个请求同时执行,所有的操作都需要队列执行,这种隔离级别下的所有的数据是最稳定的,性能也是最差的。数据库的锁实现就是这种隔离级别的更小粒度版本 ‘
MySQL的隔离级别是基于锁和MVCC 共同实现的。SERIALIZABLE隔离级别通过锁来实现,RC和RR隔离级别基于MVCC实现。MySQL的默认隔离级别是RR
5、MySQL保证ACID特性
如何保证ACID特性?
InnoDB引擎有两个日志(特有的): undo log和redo log ,mysql服务端有日志 bin log
- Atomicity:由undo log 日志保证,它记录了需要回滚的日志消息,事务回滚时撤销已经执行成功的sql;
例如,当服务器错误、操作系统崩溃等不可抗因素,或者事务执行过程中使用rollback指令回滚,对于这种执行了一半的事务,可能已经修改了很多数据,而InnoDB则会通过undo log 将只完成一半的事务回滚,撤销已经执行了的sql,没有完成的事务必须回滚到执行前的状态,来保证事务的原子性
-
Consistency:由其他三大特征保证 ,保证了原子性、持久性、隔离性----数据库的一致性才能得到保证;数据的一致性通过程序代码来保证业务上的一致性从而得到保证
-
Isolation:由MVCC保证(多版本控制来隔离每个事务)
-
Durability:binlog + redo log来保证,mysql修改数据时,同时在binlog和redo log记录这次操作,如果出现宕机时可以从redo log恢复,需要主从复制时可以从binlog中备份
八、锁
1、MySQL锁有哪些,如何理解
按锁粒度分类:
- 行锁:锁某行数据,锁粒度最小,针对索引字段加的锁 ,只对当前操作的行记录加锁,并发度高
在InnoDB事务中,行锁是在需要的时候才加上,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这就是两阶段锁协议
- 表锁:锁整张表,锁粒度最大,针对非索引字段加的锁 ,对当前操作的整张表加锁,实现简单,并发度低
表锁的语法是 lock tables…read/write ,可以用unlock tables 主动释放锁,也可以在客户端断开的时候自动释放;
另一类表级的锁是MDL(元数据锁),MDL锁是系统默认会加的,当对一个表做增删改查操作时,加MDL锁;当要对表做结构变更操作时,加MDL写锁
- 全局锁:Flush tables with read lock ,全局锁的典型使用场景是,做全库逻辑备份
按对数据操作的类型分类:
- 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务可以给它再加读锁,但是不能加写锁。多个读操作可以同时进行不会互相影响
- 排它锁:也就是写锁,一个事务对某行数据加了写锁,在当前操作没完成之前,其他事务不能再加读锁和写锁。
注意的是:即便某行数据加了读锁或者写锁,但如果现在一个事务只是对数据去读而不是加锁,是可以读到的
2、InnoDB有哪几类行锁?
- 记录锁(Record Lock):单个行记录上的锁
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身
- 临键锁(Next-Key Lock):记录锁+间隙锁。锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
在InnoDB默认的隔离级别RR下,行锁默认使用的是Next-key Lock(为了防止幻读)。但是如果操作的索引是唯一索引或主键,InnoDB会对Next-key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围
3、当前读和快照读有什么区别?
快照读 ,也就是一致性非锁定读,就是单纯的SELECT语句,不包括下面这两类的SELECT语句
SELECT ... FOR UPDATE
# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;
快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE操作,读取操作不会因此去等待记录上写锁的释放,而是会去读取行的一个快照。
快照读比较适合对于数据一致性不是特别高且追求极致性能的业务场景
当前读 ,也就是锁定读,给行记录加X锁或S锁
4、自增锁
关系型数据库设计表的时候,通常会有一列作为自增主键,InnoDB中的自增主键会涉及一种比较特殊的表级锁–自增锁 ,不止是自增主键,有些列也会设置AUTO_INCREMENT ,这也会涉及到自增锁。
如果一个事务正在插入数据到有自增列的表时,会先获取自增锁,拿不到就可能会被阻塞。