先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注go)
正文
read thread
: 负责读取操作,将数据从磁盘加载到缓存page页。4个write thread
:负责写操作,将缓存脏页刷新到磁盘。4个log thread
:负责将日志缓冲区内容刷新到磁盘。1个insert buffer thread
:负责将写缓冲内容刷新到磁盘。1个
- Purge Thread
事务提交之后,其使用的undo日志将不再需要,因此需要Purge Thread回收已经分配的undo页。
show variables like '%innodb_purge_threads;
- Page Cleaner Thread
作用是将脏数据刷新到磁盘
,脏数据刷盘后相应的redo log
也就可以覆盖,即可以同步数据,又能达到redo log循环使用的目的。会调用write thread
线程处理。
show variables like '%innodb_page_cleaners%';
- Master Thread
Master thread是InnoDB的主线程,负责调度其他各线程,优先级最高。作用是将缓冲池中的数据异步刷新到磁盘 ,保证数据的一致性。包含:脏页的刷新(page cleaner thread)、undo页回收(purge thread)、redo日志刷新(log thread)、合并写缓冲等。内部有两个主处理,分别是每隔1秒和10秒处理。
每1秒的操作:
-
刷新日志缓冲区,刷到磁盘
-
合并写缓冲区数据,根据IO读写压力来决定是否操作
-
刷新脏页数据到磁盘,根据脏页比例达到75%才操作(innodb_max_dirty_pages_pct,innodb_io_capacity)
每10秒的操作:
-
刷新脏页数据到磁盘
-
合并写缓冲区数据
-
刷新日志缓冲区
-
删除无用的undo页
InnoDB数据文件
一、InnoDB文件存储结构
InnoDB数据文件存储结构
:分为一个ibd
数据文件–>Segment
(段)–>Extent
(区)–>Page
(页)–>Row
(行)
- Tablesapce
表空间,用于存储多个ibd数据文件,用于存储表的记录和索引。一个文件包含多个段。 - Segment
段,用于管理多个Extent,分为数据段(Leaf node segment)、索引段(Non-leaf nodesegment)、回滚段(Rollback segment)。一个表至少会有两个segment,一个管理数据,一个管理索引。每多创建一个索引,会多两个segment。 - Extent
区,一个区固定包含64个连续的页,大小为1M。当表空间不足,需要分配新的页资源,不会一页一页分,直接分配一个区。 - Page
页,用于存储多个Row行记录,大小为16K。包含很多种页类型,比如数据页,undo页,系统页,事务数据页,大的BLOB对象页。 - Row
行,包含了记录的字段值,事务ID(Trx id)、滚动指针(Roll pointer)、字段指针(Fieldpointers)等信息。
Page
是文件最基本的单位,无论何种类型的page,都是由page header,page trailer和pagebody组成。如下图所示,
二、Innodb文件存储格式
– 查看表的状态
SHOW TABLE STATUS
– 查看指定表的文件格式
select * from information_schema.innodb_sys_tables;
三、File文件格式(File-Format)
在早期的InnoDB版本中,文件格式只有一种,随着InnoDB引擎的发展,出现了新文件格式,用于支持新的功能。目前InnoDB只支持两种文件格式:Antelope 和 Barracuda。
- Antelope: 先前未命名的,最原始的InnoDB文件格式,它支持两种行格式:COMPACT和REDUNDANT,MySQL 5.6及其以前版本默认格式为Antelope。
- Barracuda: 新的文件格式。它支持InnoDB的所有行格式,包括新的行格式:COMPRESSED和 DYNAMIC。
通过innodb_file_format 配置参数可以设置InnoDB文件格式,之前默认值为Antelope,5.7版本开始改为Barracuda。
四、Row行格式
表的行格式决定了它的行是如何物理存储的,这反过来又会影响查询和DML操作的性能。如果在单个page页中容纳更多行,查询和索引查找可以更快地工作,缓冲池中所需的内存更少,写入更新时所需的I/O更少。
InnoDB存储引擎支持四种行格式:REDUNDANT
、COMPACT
、DYNAMIC
和COMPRESSED
。
DYNAMIC
和COMPRESSED
新格式引入的功能有:数据压缩
、增强型长列数据的页外存储
和大索引前缀
。
每个表的数据分成若干页来存储,每个页中采用B树结构存储;如果某些字段信息过长,无法存储在B树节点中,这时候会被单独分配空间,此时被称为溢出页,该字段被称为页外列。
- REDUNDANT 行格式
使用REDUNDANT行格式,表会将变长列值的前768字节存储在B树节点的索引记录中,其余的存储在溢出页上。对于大于等于786字节的固定长度字段InnoDB会转换为变长字段,以便能够在页外存储。 - COMPACT 行格式
与REDUNDANT行格式相比,COMPACT行格式减少了约20%的行存储空间,但代价是增加了某些操作的CPU使用量。如果系统负载是受缓存命中率和磁盘速度限制,那么COMPACT格式可能更快。如果系统负载受到CPU速度的限制,那么COMPACT格式可能会慢一些。 - DYNAMIC 行格式
使用DYNAMIC行格式,InnoDB会将表中长可变长度的列值完全存储在页外,而索引记录只包含指向溢出页的20字节指针。大于或等于768字节的固定长度字段编码为可变长度字段。DYNAMIC行格式支持大索引前缀,最多可以为3072字节,可通过innodb_large_prefix参数控制。 - COMPRESSED 行格式
COMPRESSED行格式提供与DYNAMIC行格式相同的存储特性和功能,但增加了对表和索引数据压缩的支持。
在创建表和索引时,文件格式都被用于每个InnoDB表数据文件(其名称与*.ibd匹配)。修改文件格式的方法是重新创建表及其索引,最简单方法是对要修改的每个表使用以下命令:
alter table 表名 row_format = 格式类型
日志文件
Undo log
Undo Log介绍
Undo
:意为撤销或取消,以撤销操作
为目的,返回指定某个状态的操作。
Undo Log
:数据库事务开始之前,会将要修改的记录存放到 Undo 日志里,当事务回滚
时或者数据库崩溃
时,可以利用 Undo 日志,撤销未提交事务
对数据库产生的影响。
Undo Log产生和销毁
:Undo Log在事务开始前产生;事务在提交时,并不会立刻删除undolog,innodb会将该事务对应的undo log放入到删除列表中,后面会通过后台线程purge thread进行回收处理。Undo Log属于逻辑日志,记录一个变化过程。例如执行一个delete,undolog会记录一个insert;执行一个update,undolog会记录一个相反的update。
Undo Log存储
:undo log采用段的方式管理和记录。在innodb数据文件中包含一种rollback segment回滚段,内部包含1024个undo log segment。可以通过下面一组参数来控制Undo log存储。
show variables like ‘%innodb_undo%’
Undo Log作用
实现事务的原子性
Undo Log 是为了实现事务的原子性
而出现的产物。事务处理过程中,如果出现了错误
或者用户执行了ROLLBACK
语句,MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。实现多版本并发控制(MVCC)
Undo Log 在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未提交之前,Undo Log保存了未提交之前的版本数据,Undo Log 中的数据可作为数据旧版本快照供其他并发事务进行快照读。
事务A手动开启事务,执行更新操作,首先会把更新命中的数据备份到 Undo Buffer 中。
事务B手动开启事务,执行查询操作,会读取 Undo 日志数据返回,进行快照读
Redo log
Redo Log介绍
Redo
:顾名思义就是重做。以恢复操作为目的,在数据库发生意外
时重现操作。
Redo Log
:指事务中修改的任何数据,将最新的数据
备份存储的位置(Redo Log),被称为重做日志。
Redo Log 的生成和释放
:随着事务操作的执行,就会生成Redo Log,在事务提交时会将产生Redo Log
写入Log Buffer
,并不是随着事务的提交就立刻写入磁盘文件。等事务操作的脏页
写入到磁盘
之后,Redo Log 的使命也就完成了Redo Log占用的空间就可以重用
(被覆盖写入)。
Redo Log工作原理
Redo Log 是为了实现事务的持久性
而出现的产物。防止在发生故障的时间点,尚有脏页未写入表的 IBD 文件中,在重启 MySQL 服务的时候,根据 Redo Log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
Redo Log写入机制
Redo Log 文件内容是以顺序循环的方式写入文件,写满时则回溯到第一个文件,进行覆盖写
如图所示:
- write pos 是当前记录的位置,一边写一边后移,写到最后一个文件末尾后就回到 0 号文件开头;
- checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件;
write pos
和 checkpoint
之间还空着的部分,可以用来记录新的操作。如果 write pos 追上checkpoint,表示写满,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint推进一下。
Redo Log相关参数配置
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文件,默认为ib_logfile0和ib_logfile1。可以通过下面一组参数控制Redo Log存储:
show variables like ‘%innodb_log%’;
Redo Buffer 持久化到 Redo Log 的策略,可通过Innodb_flush_log_at_trx_commit
设置:
- 0:每秒提交 Redo buffer ->OS cache -> flush cache to disk,可能丢失一秒内的事务数据。由后台Master线程每隔 1秒执行一次操作。
- 1(默认值):每次事务提交执行 Redo Buffer -> OS cache -> flush cache to disk,最安全,性能最差的方式。
- 2:每次事务提交执行 Redo Buffer -> OS cache,然后由后台Master线程再每隔1秒执行OScache -> flush cache to disk 的操作。
一般建议选择取值2,因为 MySQL 挂了数据没有损失,整个服务器挂了才会损失1秒的事务提交数据。
Bin log
BinLog记录模式
Redo Log 是属于InnoDB引擎
所特有的日志,而MySQL Server也有自己的日志,即 Binarylog(二进制日志)
,简称Binlog。Binlog是记录所有数据库表结构变更
以及表数据修改
的二进制日志,不会记录SELECT和SHOW这类操作。Binlog日志是以事件形式
记录,还包含语句所执行的消耗时间。开启Binlog日志有以下两个最重要的使用场景。
主从复制:
在主库
中开启Binlog
功能,这样主库就可以把Binlog传递给从库,从库拿到Binlog后实现数据恢复达到主从数据一致性
。数据恢复:
通过mysqlbinlog
工具来恢复数据。
Binlog文件名默认为“主机名_binlog-序列号
”格式,例如oak_binlog-000001,也可以在配置文件中指定名称。文件记录模式有STATEMENT
、ROW
和MIXED
三种,具体含义如下。
- ROW(row-based replication, RBR):日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。
- 优点:能清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。
- 缺点:批量操作,会产生大量的日志,尤其是alter table会让日志暴涨。
- STATMENT(statement-based replication, SBR):每一条被修改数据的SQL都会记录到master的Binlog中,slave在复制的时候SQL进程会解析成和原来master端执行过的相同的SQL再次执行。简称SQL语句复制。
- 优点:日志量小,减少磁盘IO,提升存储和恢复速度
- 缺点:在某些情况下会导致主从数据不一致,比如last_insert_id()、now()等函数。
- MIXED(mixed-based replication, MBR):以上两种模式的混合使用,一般会使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,MySQL会根据执行的SQL语句选择写入模式。
BinLog文件结构
MySQL的binlog文件中记录的是对数据库的各种修改操作,用来表示修改操作的数据结构是Logevent
。不同的修改操作对应的不同的log event。比较常用的log event有:Query event
、Row event
、Xid event
等。binlog文件的内容就是各种Log event的集合。
Binlog文件中Log event结构如下图所示:
BinLog写入机制
- 根据记录模式和操作触发event事件生成log event(事件触发执行机制)
- 将事务执行过程中产生log event写入缓冲区,每个事务线程都有一个缓冲区
Log Event保存在一个binlog_cache_mngr数据结构中,在该结构中有两个缓冲区,一个是stmt_cache,用于存放不支持事务的信息;另一个是trx_cache,用于存放支持事务的信息。 - 事务在提交阶段会将产生的log event写入到外部binlog文件中。
不同事务以串行方式将log event写入binlog文件中,所以一个事务包含的log event信息在binlog文件中是连续的,中间不会插入其他事务的log event。
BinLog文件操作
- Binlog状态查看
show variables like ‘log_bin’
- 开启Binlog功能
mysql> set global log_bin=mysqllogbin;
ERROR 1238 (HY000): Variable ‘log_bin’ is a read only variable
需要修改my.cnf或my.ini配置文件,在[mysqld]下面增加log_bin=mysql_bin_log,重启
MySQL服务。
#log-bin=ON
#log-bin-basename=mysqlbinlog
binlog-format=ROW
log-bin=mysqlbinlog
- 使用show binlog events命令
show binary logs; //等价于show master logs;
show master status;
show binlog events;
show binlog events in ‘mysqlbinlog.000001’;
- 使用mysqlbinlog命令
mysqlbinlog “文件名”
mysqlbinlog “文件名” > “test.sql”
- 使用 binlog 恢复数据
//按指定时间恢复
mysqlbinlog --start-datetime=“2020-04-25 18:00:00” --stopdatetime=“2020-04-26 00:00:00” mysqlbinlog.000002 | mysql -uroot -p1234
//按事件位置号恢复
mysqlbinlog --start-position=154 --stop-position=957 mysqlbinlog.000002 | mysql -uroot -p1234
mysqldump
:定期全部备份数据库数据。mysqlbinlog可以做增量备份和恢复操作
- 删除Binlog文件
purge binary logs to ‘mysqlbinlog.000001’; //删除指定文件
purge binary logs before ‘2020-04-28 00:00:00’; //删除指定时间之前的文件
reset master; //清除所有文件
可以通过设置expire_logs_days
参数来启动自动清理功能。默认值为0表示没启用。设置为1表示出1天binlog文件会自动删除掉。
Redo Log和Binlog区别
- Redo Log是属于InnoDB引擎功能,Binlog是属于MySQL Server自带功能,并且是以二进制文件记录。
- Redo Log属于物理日志,记录该数据页更新状态内容,Binlog是逻辑日志,记录更新过程。
- Redo Log日志是循环写,日志空间大小是固定,Binlog是追加写入,写完一个写下一个,不会覆盖使用。
- Redo Log作为服务器异常宕机后事务数据自动恢复使用,Binlog可以作为主从复制和数据恢复使用。Binlog没有自动crash-safe能力。
MySQL索引原理
索引可以提升查询速度,会影响where查询,以及order by排序。MySQL索引类型如下:
- 从索引存储结构划分:B Tree索引、Hash索引、FULLTEXT全文索引、R Tree索引
- 从应用层次划分:普通索引、唯一索引、主键索引、复合索引
- 从索引键值类型划分:主键索引、辅助索引(二级索引)
- 从数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)、非聚集索引(非聚簇索引)
索引类型
普通索引
这是最基本的索引类型,基于普通字段建立的索引,没有任何限制。
创建普通索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名);
CREATE TABLE tablename ( […], INDEX [索引的名字] (字段名) );
唯一索引
与"普通索引"类似,不同的就是:索引字段的值必须唯一,但允许有空值 。在创建或修改表时追加唯一约束,就会自动创建对应的唯一索引。
创建唯一索引的方法如下:
CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD UNIQUE INDEX [索引的名字] (字段名);
CREATE TABLE tablename ( […], UNIQUE [索引的名字] (字段名) ;
主键索引
它是一种特殊的唯一索引,不允许有空值。在创建或修改表时追加主键约束即可,每个表只能有一个主键。
创建主键索引的方法如下:
CREATE TABLE tablename ( […], PRIMARY KEY (字段名) );
ALTER TABLE tablename ADD PRIMARY KEY (字段名);
复合索引
单一索引是指索引列为一列的情况,即新建索引的语句只实施在一列上;用户可以在多个列上建立索引,这种索引叫做组复合索引(组合索引)。复合索引可以代替多个单一索引,相比多个单一索引复合索引所需的开销更小。
索引同时有两个概念叫做窄索引和宽索引,窄索引是指索引列为1-2列的索引,宽索引也就是索引列超过2列的索引,设计索引的一个重要原则就是能用窄索引不用宽索引,因为窄索引往往比组合索引更有效。
创建组合索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2…);
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名1,字段名2…);
CREATE TABLE tablename ( […], INDEX [索引的名字] (字段名1,字段名2…) );
复合索引使用注意事项:
- 何时使用复合索引,要根据where条件建索引,注意不要过多使用索引,过多使用会对更新操作效率有很大影响。
- 如果表已经建立了(col1,col2),就没有必要再单独建立(col1);如果现在有(col1)索引,如果查询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。
全文索引
查询操作在数据量比较少时,可以使用like模糊查询,但是对于大量的文本数据检索,效率很低。如果使用全文索引,查询速度会比like快很多倍。在MySQL 5.6 以前的版本,只有MyISAM存储引擎支持全文索引,从MySQL 5.6开始MyISAM和InnoDB存储引擎均支持。
创建全文索引的方法如下:
CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD FULLTEXT [索引的名字] (字段名);
CREATE TABLE tablename ( […], FULLTEXT KEY [索引的名字] (字段名) ;
和常用的like模糊查询不同,全文索引有自己的语法格式,使用 match
和against
关键字,比如
select * from user where match(name) against(‘aaa’);
全文索引使用注意事项:
- 全文索引必须在字符串、文本字段上建立。
- 全文索引字段值必须在
最小字符
和最大字符
之间的才会有效。(innodb:3-84;myisam:4-84) - 全文索引字段值要进行切词处理,按
syntax
字符进行切割,例如b+aaa,切分成b和aaa - 全文索引匹配查询,默认使用的是
等值匹配
,例如a匹配a,不会匹配ab,ac。如果想匹配可以在布尔模式下搜索a*
select * from user where match(name) against(‘a*’ in boolean mode);
索引原理
MySQL官方对索引定义:是存储引擎用于快速查找记录的一种数据结构。需要额外开辟空间和数据维护工作。
- 索引是物理数据页存储,在数据文件中(InnoDB,ibd文件),利用数据页(page)存储。
- 索引可以加快检索速度,但是同时也会降低增删改操作速度,索引维护需要代价。
索引涉及的理论知识:二分查找法
、Hash
和B+Tree
。
二分法查找
二分查找法也叫作折半查找法
,它是在有序数组
中查找指定数据的搜索算法。它的优点是等值查询
、范围查询
性能优秀,缺点是更新数据、新增数据、删除数据维护成本高。
- 首先定位left和right两个指针
- 计算(left+right)/2
- 判断除2后索引位置值与目标值的大小比对
- 索引位置值大于目标值就-1,right移动;如果小于目标值就+1,left移动
Hash结构
Hash底层实现是由Hash表
来实现的,是根据键值<key,value>
存储数据的结构。非常适合根据key查找value值,也就是单个key查询,或者说等值查询。其结构如下所示
从上面结构可以看出,Hash索引可以方便的提供等值查询,但是对于范围查询就需要全表扫描了。Hash索引在MySQL 中Hash结构主要应用在Memory原生的Hash索引 、InnoDB 自适应哈希索引。
InnoDB提供的自适应哈希索引功能强大,接下来重点描述下InnoDB 自适应哈希索引。
InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。
InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据,等值查询效率要优于B+Tree。
自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。
show engine innodb status \G;
show variables like ‘%innodb_adaptive%’;
B+Tree结构
MySQL数据库索引采用的是B+Tree
结构,在B-Tree
结构上做了优化改造
- B-Tree结构
索引值和data数据分布在整棵树结构中
每个节点可以存放多个索引值及对应的data数据
树节点中的多个索引值从左到右升序排列
B树的搜索: 从根节点开始,对节点内的索引值序列采用二分法查找,如果命中就结束查找。没有命中会进入子节点重复查找过程,直到所对应的的节点指针为空,或已经是叶子节点了才结束。 - B+Tree结构
非叶子节点不存储data数据,只存储索引值,这样便于存储更多的索引值
叶子节点包含了所有的索引值和data数据
叶子节点用指针连接,提高区间的访问性能
相比B树,B+树进行范围查找
时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进行遍历即可。而B树需要遍历范围内所有的节点和数据,显然B+Tree效率高。
聚簇索引和辅助索引
聚簇索引和非聚簇索引: B+Tree的叶子节点存放主键索引值和行记录就属于聚簇索引;如果索引值和行记录分开存放就属于非聚簇索引。
主键索引和辅助索引: B+Tree的叶子节点存放的是主键字段值就属于主键索引;如果存放的是非主键值就属于辅助索引(二级索引)。
在InnoDB引擎
中,主键索引
采用的就是聚簇索引
结构存储。
聚簇索引(聚集索引)
聚簇索引是一种数据存储方式,InnoDB的聚簇索引就是按照主键顺序
构建B+Tree
结构。B+Tree的叶子节点
就是行记录
,行记录
和主键值
紧凑地存储在一起。 这也意味着 InnoDB 的主键索引
就是数据表本身,它按主键顺序存放了整张表的数据,占用的空间就是整个表数据量的大小。通常说的主键索引
就是聚集索引
。
InnoDB的表要求必须要有聚簇索引:
- 如果表定义了主键,则主键索引就是聚簇索引
- 如果表没有定义主键,则第一个非空unique列作为聚簇索引
- 否则InnoDB会从建一个隐藏的row-id作为聚簇索引
辅助索引
InnoDB辅助索引,也叫作二级索引
,是根据索引列构建B+Tree
结构。但在 B+Tree 的叶子节点中只存了索引列和主键的信息。二级索引占用的空间会比聚簇索引小很多, 通常创建辅助索引就是为了提升查询效率。一个表InnoDB只能创建一个聚簇索引,但可以创建多个辅助索引。
非聚簇索引
与InnoDB表存储不同,MyISAM数据表的索引文件
和数据文件
是分开
的,被称为非聚簇索引
结构。
索引分析与优化
EXPLAIN
MySQL 提供了一个 EXPLAIN 命令,它可以对 SELECT 语句进行分析,并输出 SELECT 执行的详细信息,供开发人员有针对性的优化。例如:
mysql> explain select * from user where id = 1 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
- select_type
表示查询的类型。常用的值如下:
SIMPLE
: 表示查询语句不包含子查询或union- PRIMARY:表示此查询是最外层的查询
- UNION:表示此查询是UNION的第二个或后续的查询
- DEPENDENT UNION:UNION中的第二个或后续的查询语句,使用了外面查询结果
- UNION RESULT:UNION的结果
- SUBQUERY:SELECT子查询语句
- DEPENDENT SUBQUERY:SELECT子查询语句依赖外层查询的结果最常见的查询类型是
SIMPLE
,表示我们的查询没有子查询
也没用到UNION
查询。
- type
表示存储引擎查询数据时采用的方式
。比较重要的一个属性,通过它可以判断出查询是全表扫描
还是基于索引的部分扫描
。常用属性值如下,从上至下效率依次增强。
ALL
:表示全表扫描,性能最差。index
:表示基于索引的全表扫描,先扫描索引再扫描全表数据。- range:表示使用索引范围查询。使用>、>=、<、<=、in等等。
- ref:表示使用非唯一索引进行单值查询。
- eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果。
const
:表示使用主键
或唯一索引
做等值查询,常量查询。- NULL:表示不用访问表,速度最快。
- possible_keys
表示查询时能够使用到的索引
。注意并不一定会真正使用,显示的是索引名称。 - key
表示查询时真正使用到的索引
,显示的是索引名称。 - rows
MySQL查询优化器会根据统计信息,估算SQL要查询到结果需要扫描多少行记录。原则上rows是越少效率越高,可以直观的了解到SQL效率高低。 - key_len
表示查询使用了索引的字节数量。可以判断是否全部使用了组合索引。
key_len的计算规则如下:
- 字符串类型
字符串长度跟字符集有关:latin1=1、gbk=2、utf8=3、utf8mb4=4
char(n):n*字符集长度
varchar(n):n * 字符集长度 + 2字节 - 数值类型
TINYINT:1个字节
SMALLINT:2个字节
MEDIUMINT:3个字节
INT、FLOAT:4个字节
BIGINT、DOUBLE:8个字节 - 时间类型
DATE:3个字节
TIMESTAMP:4个字节
DATETIME:8个字节 - 字段属性
NULL属性占用1个字节,如果一个字段设置了NOT NULL,则没有此项。
- Extra
Extra表示很多额外的信息,各种操作会在Extra提示相关信息,常见几种如下:
- Using where
表示查询需要通过索引回表查询数据。 - Using index
表示查询需要通过索引,索引就可以满足所需数据。 - Using filesort
表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,因此有Using filesort建议优化。 - Using temprorary
查询使用到了临时表,一般出现于去重、分组等操作
回表查询
在之前介绍过,InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点
存储行记录
,InnoDB必须要有,且只有一个。辅助索引的叶子节点存储的是主键值
和索引字段值
,通过辅助索引无法直接定位行记录,通常情况下,需要扫码两遍
索引树。先通过辅助索引定位主键值
,然后再通过聚簇索引定位行记录,这就叫做回表查询,它的性能比扫一遍索引树低。
总结:通过索引查询主键值,然后再去聚簇索引查询记录信息
覆盖查询
不管是SQL-Server官网,还是MySQL官网,都表达了:只需要在一棵索引树
上就能获取SQL所需的所有列数据,无需回表
,速度更快
,这就叫做索引覆盖。
实现索引覆盖最常见的方法就是:将被查询的字段,建立到组合索引。
最左前缀原则
复合索引
使用时遵循最左前缀原则,最左前缀顾名思义,就是最左优先
,即查询中使用到最左边的列,那么查询就会使用到索引,如果从索引的第二列
开始查找,索引将失效
。
name age gender生成复合索引
复合索引起作用
name
name age
name gender
name age gender
name gender age # mysql优化器自动优化
复合索引不起作用
age
gender
age gender
LIKE查询
面试题:MySQL在使用like模糊查询时,索引能不能起作用?
回答:MySQL在使用Like模糊查询时,索引是可以被使用的,只有把%
字符写在后面才会使用到索引。
- select * from user where name like ‘%o%’; //不起作用
- select * from user where name like ‘o%’; //起作用
- select * from user where name like ‘%o’; //不起作用
NULL查询
面试题:如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?
对MySQL来说,NULL是一个特殊的值,从概念上讲,NULL意味着“一个未知值”,它的处理方式与其他值有些不同。比如:不能使用=,<,>这样的运算符,对NULL做算术运算的结果都是NULL,count时不会包括NULL行等,NULL比空字符串需要更多的存储空间等。
NULL列需要增加额外空间来记录其值是否为NULL。对于MyISAM表,每一个空列额外占用一位,四舍五入到最接近的字节。
虽然MySQL可以在含有NULL的列上使用索引,但NULL和其他数据还是有区别的,不建议列上允许为NULL。最好设置NOT NULL,并给一个默认值,比如0和 ‘’ 空字符串等,如果是datetime类型,也可以设置系统当前时间或某个固定的特殊值,例如’1970-01-01 00:00:00’。
索引与排序
MySQL查询支持filesort
和index
两种方式的排序,filesort是先把结果查出,然后在缓存或磁盘进行排序操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。
filesort有两种排序算法:双路排序
和单路排序
。
- 双路排序:需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二次去读取其他字段数据。
- 单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存sort_buffer,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO,反而会增加负担。解决方案:少使用select *;增加sort_buffer_size容量和max_length_for_sort_data容量。
如果我们Explain分析SQL,结果中Extra属性显示Using filesort,表示使用了filesort排序方式,需要优化。如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用index排序方式,建议大家尽可能采用覆盖索引。
- 以下几种情况,会使用index方式的排序。
- ORDER BY 子句索引列组合满足索引最左前列
explain select id from user order by id; #对应(id)、(id,name)索引有效
- WHERE子句+ORDER BY子句索引列组合满足索引最左前列
explain select id from user where age=18 order by name; #对应(age,name)索引
- 以下几种情况,会使用filesort方式的排序。
- 对索引列同时使用了ASC和DESC
explain select id from user order by age asc,name desc; # 对应(age,name)索引
- WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询(例如>、<、in等)
explain select id from user where age>10 order by name; //对应(age,name)索引
- ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列
explain select id from user order by name; //对应(age,name)索引
- 使用了不同的索引,MySQL每次只采用一个索引,ORDER BY涉及了两个索引
explain select id from user order by name,age; //对应(name)、(age)两个索引
- WHERE子句与ORDER BY子句,使用了不同的索引
explain select id from user where name=‘tom’ order by age; //对应(name)、(age)索引
- WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式
explain select id from user order by abs(age); //对应(age)索引
查询优化
慢查询定位
开启慢查询日志
查看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:
SHOW VARIABLES LIKE ‘slow_query_log%’
通过如下命令开启慢查询日志:
SET global slow_query_log = ON;
SET global slow_query_log_file = ‘OAK-slow.log’;
SET global log_queries_not_using_indexes = ON;
SET long_query_time = 10;
long_query_time
:指定慢查询的阀值,单位秒。如果SQL执行时间超过阀值,就属于慢查询记录到日志文件中。
log_queries_not_using_indexes
:表示会记录没有使用索引的查询SQL。前提是slow_query_log的值为ON,否则不会奏效
查看慢查询日志
- 文本方式查看
- 使用mysqldumpslow查看
MySQL 提供了一个慢查询日志分析工具mysqldumpslow,可以通过该工具分析慢查询日志内容。
在 MySQL bin目录下执行下面命令可以查看该使用格式。perl mysqldumpslow.pl --help
运行如下命令查看慢查询日志信息:
perl mysqldumpslow.pl -t 5 -s at C:\ProgramData\MySQL\Data\OAK-slow.log
除了使用mysqldumpslow工具,也可以使用第三方分析工具,比如pt-query-digest、mysqlsla等。
慢查询优化
索引和慢查询
- 如何判断是否为慢查询?
MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执行时间跟 long_query_time 参数做比较,如果语句的执行时间 >long_query_time
qqqqqqqqqqqq,就会把这条执行语句记录到慢查询日志里面。long_query_time 参数的默认值是 10s,该参数值可以根据自己的业务需要进行调整。 - 如何判断是否应用了索引?
SQL语句是否使用了索引,可根据SQL语句执行过程中有没有用到表的索引,可通过 explain命令分析查看,检查结果中的 key 值,是否为NULL。 - 应用了索引是否一定快?
下面我们来看看下面语句的 explain 的结果,你觉得这条语句有用上索引吗?比如select * from user where id>0;
,虽然使用了索引,但是还是从主键索引的最左边的叶节点开始向右扫描整个索引树,进行了全表扫描,此时索引就失去了意义。而像 select * from user where id = 2; 这样的语句,才是我们平时说的使用了索引。它表示的意思是,我们使用了索引的快速搜索功能,并且有效地减少了扫描行数。
查询是否使用索引,只是表示一个SQL语句的执行过程;而是否为慢查询,是由它执行的时间决定的,也就是说是否使用了索引和是否是慢查询两者之间没有必然的联系。
我们在使用索引时,不要只关注是否起作用,应该关心索引是否减少了查询扫描的数据行数,如果扫描行数减少了,效率才会得到提升。对于一个大表,不止要创建索引,还要考虑索引过滤性,过滤性好,执行速度才会快
提高索引过滤性
假如有一个5000万记录的用户表,通过sex='男’索引过滤后,还需要定位3000万,SQL执行速度也不会很快。其实这个问题涉及到索引的过滤性,比如1万条记录利用索引过滤后定位10条、100条、1000条,那他们过滤性是不同的。索引过滤性与索引字段、表的数据量、表设计结构都有关系。
表:student
字段:id,name,sex,age
造数据:insert into student (name,sex,age) select name,sex,age fromstudent;
SQL案例:select * from student where age=18 and name like ‘张%’;(全表扫描)
- 优化1
alter table student add index(name); //追加name索引
- 优化2
alter table student add index(age,name); //追加age,name索引
- 优化3
可以看到,index condition pushdown 优化的效果还是很不错的。再进一步优化,我们可以把名字的第一个字和年龄做一个联合索引,这里可以使用 MySQL 5.7 引入的虚拟列来实现。
//为user表添加first_name虚拟列,以及联合索引(first_name,age)
alter table student add first_name varchar(2) generated always as(left(name, 1)), add index(first_name, age);
explain select * from student where first_name=‘张’ and age=18;
慢查询原因总结
- 全表扫描:explain分析type属性all
- 全索引扫描:explain分析type属性index
- 索引过滤性不好:靠索引字段选型、数据量和状态、表设计
- 频繁的回表查询开销:尽量少用select *,使用覆盖索引
分页查询优化
一般性分页
SELECT * FROM 表名 LIMIT [offset,] rows
- 第一个参数指定第一个返回记录行的偏移量,注意从0开始;
- 第二个参数指定返回记录行的最大数目;
- 如果只给定一个参数,它表示返回最大的记录行数目;
思考1:如果偏移量固定,返回记录量对执行时间有什么影响
select * from user limit 10000,1;
select * from user limit 10000,10;
select * from user limit 10000,100;
select * from user limit 10000,1000;
select * from user limit 10000,10000;
结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录量越大,所花费的时间也会越来越多。
思考2:如果查询偏移量变化,返回记录数固定对执行时间有什么影响?
select * from user limit 1,100;
select * from user limit 10,100;
select * from user limit 100,100;
select * from user limit 1000,100;
select * from user limit 10000,100;
结果:在查询记录时,如果查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间急剧的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而且查询的数据越多,也会拖慢总查询速度。)
分页优化方案
- 第一步:利用覆盖索引优化
select * from user limit 10000,100;
select id from user limit 10000,100;
- 第二步:利用子查询优化
select * from user limit 10000,100;
select * from user where id>= (select id from user limit 10000,1) limit 100;
原因:使用了id做主键比较(id>=),并且子查询使用了覆盖索引进行优化。
MySQL事务和锁
ACID特性
在关系型数据库管理系统中,一个逻辑工作单元要成为事务,必须满足这 4 个特性,即所谓的 ACID:原子性(Atomicity)
、一致性(Consistency)
、隔离性(Isolation)
和持久性(Durability)
。
原子性
原子性: 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
修改—》Buffer Pool修改—》刷盘。可能会有下面两种情况:
- 事务提交了,如果此时Buffer Pool的脏页没有刷盘,如何保证修改的数据生效? Redo
- 如果事务没提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销?Undo
每一个写事务,都会修改BufferPool
,从而产生相应的Redo/Undo
日志,在Buffer Pool
中的页被刷到磁盘之前,这些日志信息都会先写入到日志文件中,如果 Buffer Pool
中的脏页没有刷成功
,此时数据库挂了,那在数据库再次启动之后,可以通过Redo 日志
将其恢复出来,以保证脏页写的数据不会丢失。如果脏页刷新成功
,此时数据库挂了,就需要通过Undo
来实现了。
持久性
持久性: 指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,不会丢失。
如下图所示,一个“提交”动作触发的操作有:binlog落地
、发送binlog
、存储引擎提交
、flush_logs
,check_point
、事务提交标记
等。这些都是数据库保证其数据完整性
、持久性
的手段。
MySQL的持久性也与WAL技术相关,redo log
在系统Crash重启之类的情况时,可以修复数据,从而保障事务的持久性
。通过原子性
可以保证逻辑上
的持久性
,通过存储引擎的数据刷盘
可以保证物理上
的持久性
隔离性
隔离性: 指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。
InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交
、读提交
、可重复读
、可串行化
。锁和多版本控制(MVCC) 技术就是用于保障隔离性的(后面课程详解)。
一致性
一致性: 指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内容,分别是约束一致性和数据一致性。
- 约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束,可惜在 MySQL 中不支持Check 。
- 数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。
一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性
来保证的,而这3个特性又是通过Redo/Undo
来保证的。逻辑上的一致性
,包括唯一索引
、外键约束
、check 约束
,这属于业务逻辑范畴。
ACID 及它们之间的关系如下图所示,4个特性中有3个与 WAL 有关系,都需要通过Redo、Undo
日志来保证等。
WAL的全称为Write-Ahead Logging
,先写日志
,再写磁盘
。
事务控制的演进
事务并发
事务并发处理
可能会带来一些问题,比如:更新丢失
、脏读
、不可重复读
、幻读
等。
事务并发 | 介绍 |
---|---|
更新丢失 | 当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。回滚覆盖: 一个事务回滚操作,把其他事务已提交的数据给覆盖了。提交覆盖: 一个事务提交操作,把其他事务已提交的数据给覆盖了。 |
脏读 | 一个事务读取到了另一个事务修改但未提交的数据 |
不可重复读 | 一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。 |
幻读 | 一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。 |
排队
最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。
排他锁
引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。
注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。
读写锁
读和写操作: 读读、写写、读写、写读。
读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务就可以同时被执行了。
读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。
MVCC
多版本控制MVCC,也就是Copy on Write的思想。MVCC除了支持读和读并行,还支持读和写、写和读的并行,但为了保证一致性,写和写是无法并行的。
在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影响其他事务对此记录的读取,实现写和读并行。
一、MVCC概念
MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发
的数据访问,对数据进行多版本处理
,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。多版本控制很巧妙地将稀缺资源的独占互斥
转换为并发
,大大提高了数据库的吞吐量
及读写性能
。
如何生成的多版本? 每次事务修改操作之前,都会在Undo日志
中记录修改之前的数据状态和事务号,该备份记录可以用于其他事务的读取,也可以进行必要时的数据回滚。
二、MVCC实现原理
MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突是非常重要的,极大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持 MVCC 的原因,不过目前MVCC只在 Read Commited 和 Repeatable Read 两种隔离级别下工作。
在 MVCC 并发控制中,读操作可以分为两类: 快照读
(Snapshot Read)与当前读
(Current Read)。
- 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。(select)
- 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。(select… for update 或lock in share mode,insert/delete/update)
为了让大家更直观地理解 MVCC 的实现原理,举一个记录更新的案例来讲解 MVCC 中多版本的实现。假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID
、事务号
和回滚指针
,如下图所示。
具体的更新过程如下:
假如一条数据是刚 INSERT 的,DB_ROW_ID 为 1,其他两个字段为空。当事务 1 更改该行的数据值时,会进行如下操作,如下图所示。
- 用排他锁锁定该行;记录 Redo log;
- 把该行修改前的值复制到 Undo log,即图中下面的行;
- 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行。
接下来事务2操作,过程与事务 1 相同,此时 Undo log 中会有两行记录,并且通过回滚指针连在一起,通过当前记录的回滚指针回溯到该行创建时的初始内容,如下图所示。
MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用下面两种方案:
- 乐观锁
- 悲观锁
事务隔离级别
隔离级别类型
前面提到的“更新丢失”、”脏读”、“不可重复读”和“幻读”等并发事务问题,其实都是数据库一致性问题,为了解决这些问题,MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下 4 种事务隔离级别供用户选择。
事务隔离级别 | 回滚覆盖 | 脏读 | 不可重复读 | 提交覆盖 | 幻读 |
---|---|---|---|---|---|
读未提交 | Y | Y | Y | Y | Y |
读已提交 | N | N | Y | Y | Y |
可重复读 | N | N | N | N | Y |
幻读 | N | N | N | N | N |
- 读未提交
Read Uncommitted 读未提交: 解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是可能读取到其他会话中未提交事务修改的数据。 - 已提交读
Read Committed 读已提交: 只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生不可重复读现象,也就是可能在一个事务中两次查询结果不一致。 - 可重复度
Repeatable Read 可重复读: 解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。 - 可串行化
Serializable 串行化: 所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。
数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别,比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔离级别。
事务隔离级别,针对Innodb引擎,支持事务的功能。像MyISAM引擎没有关系。
事务隔离级别和锁的关系
1)事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁
和MVCC
使用的封装,隐藏了底层细节。
2)锁是数据库实现并发控制的基础,事务隔离性是采用锁
来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。
3)对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。MySQL默认隔离级别:可重复读 Oracle、SQLServer默认隔离级别:读已提交一般使用时,建议采用默认隔离级别,然后存在的一些并发问题,可以通过悲观锁、乐观锁等实现处理。
MySQL隔离级别控制
MySQL默认的事务隔离级别是Repeatable Read
,查看MySQL当前数据库的事务隔离级别命令如下:
show variables like ‘tx_isolation’;
select @@tx_isolation;
设置事务隔离级别可以如下命令:
set tx_isolation=‘READ-UNCOMMITTED’;
set tx_isolation=‘READ-COMMITTED’;
set tx_isolation=‘REPEATABLE-READ’;
set tx_isolation=‘SERIALIZABLE’;
锁机制和实战
锁分类
- 从操作的粒度可分为表级锁、行级锁和页级锁。
- 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB 等存储引擎中。
- 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。
- 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。应用在BDB 存储引擎中。
- 从操作的类型可分为读锁和写锁。
- 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响
- 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。
IS锁、IX锁:意向读锁、意向写锁,属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之前,会先对表添加IS或IX锁。
S锁:事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加
S锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。
X锁:事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操作。
- 从操作的性能可分为乐观锁和悲观锁。
- 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
- 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。
行锁原理
在InnoDB引擎中,我们可以使用行锁和表锁,其中行锁又分为共享锁和排他锁。InnoDB行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。
- RecordLock锁:锁定单个行记录的锁。(记录锁,RC、RR隔离级别都支持)
- GapLock锁:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。(范围锁,RR隔离级别支持)
- Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持)
在RR隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引时,Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非范围。
1)select … from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句,InnoDB不加锁
2)select … from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
3)select … from for update语句:追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
4)update … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
5)delete … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
6)insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。
下面以“update t1 set name=‘XX’ where id=10
”操作为例,举例子分析下 InnoDB 对不同索引的加锁行为,以RR隔离级别为例。
- 主键加锁
加锁行为:仅在id=10的主键索引记录上加X锁。
- 唯一键加锁
加锁行为:现在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。
- 非唯一键加锁
加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)-(11,f)范围分别加Gap Lock。
- 无索引加锁
加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎锁机制是基于索引实现的记录锁定)。
悲观锁
悲观锁(Pessimistic Locking),是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机制实现。从广义上来讲,前面提到的行锁
、表锁
、读锁
、写锁
、共享锁
、排他锁
等,这些都属于悲观锁范畴。
- 表级锁
表级锁每次操作都锁住整张表,并发度最低。常用命令如下:
手动增加表锁
lock table 表名称 read|write,表名称2 read|write;
查看表上加过的锁
show open tables;
删除表锁
unlock tables;
表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作会报错,其他连接增删改会被阻塞。
表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都被阻塞(包括查询)。
总结:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。
- 共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。使用共享锁的方法是在select … lock in share mode,只适用查询语句。
总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。 - 排他锁(行级锁-写锁)
排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。
使用排他锁的方法是在SQL末尾加上for update,innodb引擎默认会在update,delete语句加上for update。行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记录。
总结:事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录锁(select… for update)。如果查询没有使用到索引,将会锁住整个表记录。
乐观锁
乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时,想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁,而是在进行事务提交时再去判断是否有冲突了。
乐观锁实现的关键点:冲突的检测。
悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要求高的选择乐观锁;对于并发率要求低的可以选择悲观锁。
乐观锁实现原理
- 使用版本字段(version)
先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version是用来查看被读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改。
- 使用时间戳(Timestamp)
与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp时间戳。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则提交更新,否则就是版本冲突,取消操作。
乐观锁案例
- 第一步:查询商品信息
select (quantity,version) from products where id=1;
- 第二步:根据商品信息生成订单
insert into orders ... insert into items ...
- 第三步:修改商品库存
update products set quantity=quantity-1,version=version+1 where id=1 and version=#{version};
除了自己手动实现乐观锁之外,许多数据库访问框架也封装了乐观锁的实现,比如hibernate框架。MyBatis框架大家可以使用OptimisticLocker插件来扩展。
死锁与解决方案
- 一、表锁死锁
产生原因:
用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
用户A–》A表(表锁)–》B表(表锁)
用户B–》B表(表锁)–》A表(表锁)
解决方案:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。 - 二、行级锁死锁
产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁。
解决方案1:
SQL语句中不要使用太复杂的关联多表的查询;使用explain“执行计划"对SQL语句进行分析,对于有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。
产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。
解决方案2:
在同一个事务中,尽可能做到一次锁定所需要的所有资源按照id对资源排序,然后按顺序进行处理 - 三、共享锁转换为排他锁
产生原因:
事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时,此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经有一个排他锁请求,并且正在等待事务A 释放其共享锁。
事务A: select * from dept where deptno=1 lock in share mode; //共享锁,1
update dept set dname=‘java’ where deptno=1;//排他锁,3
事务B: update dept set dname=‘Java’ where deptno=1;//由于1有共享锁,没法获取排他锁,需等待,2
解决方案:
对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;
- 四、死锁排查
MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。 - 查看死锁日志
通过show engine innodb status\G命令查看近期死锁日志信息。
使用方法:1、查看近期死锁日志信息;2、使用explain查看下SQL执行计划 - 查看锁状态变量
通过show status like’innodb_row_lock%‘命令检查状态变量,分析系统中的行锁的争夺情况
- Innodb_row_lock_current_waits:当前正在等待锁的数量
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度
- Innodb_row_lock_time_avg: 每次等待锁的平均时间
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着手定制优化。
MySQL集群架构
集群架构设计
架构设计理念
在集群架构设计时,要遵从一下三种维度:
- 可用性
- 扩展性
- 一致性
可用性设计
- 站点高可用,冗余站点
- 站点高可用,冗余服务
- 站点高可用,冗余数据
保证高可用的方法是冗余,但是数据的冗余带来的是数据一致性的问题
实现高可用的方案有以下几种架构模式:
- 主从模式
简单灵活,能够满足多种需求,比较主流的用法,但是写操作高可用需要自行处理 - 双主模式
互为主从,有主从双写,主从单写两种方式,建议使用双主单写
扩展性设计
扩展性主要围绕着读操作扩展
和写操作扩展
展开。
- 如何扩展以提高读性能
- 加从库
简单易操作,方案成熟
从库过多会导致主库的性能损耗,建议不要作为长期的扩充方案,应该设法用良好的设计避免持续加从库来缓解读性能问题 - 分库分表
可以分为垂直分库和水平分库,垂直分库可以缓解部分压力,水平分库理论上可以无限扩展
- 如何扩展以提高写性能
- 分库分表
一致性设计
一致性主要考虑集群中各数据库同步以及同步延迟的问题,可以采用以下几种方案
- 不使用从库
扩展读性能的问题需要单独考虑,否则容易出现系统瓶颈 - 增加访问路由层
可以先得到主从同步最长时间t,在数据发生改变后的t时间内,先访问主库
主从模式
使用场景
MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,从节点可以复制主数据库中的所有数据库,或者特定的数据库,或者特定的表。
mysql主从复制用途:
- 实时灾备,用于故障切换(高可用)
- 读写分离,提供查询服务(读扩展)
- 数据备份,避免影响业务(高可用)
主从部署必要条件:
- 从库服务器能连通主库
- 主库开启binlog日志(设置log-bin参数)
- 主从server-id不同
实现原理
1.主从复制
主从复制整体分为以下三个步骤:
主库
将数据库的变更
操作记录
到Binlog
日志文件中- 从库读取
主库
中的Binlog
日志文件信息写入
到从库
的Relay Log
中继日志中 从库
读取中继日志
信息在从库中进行Replay,更新从库
数据信息
在上述三个过程中,涉及了Master
的BinlogDump Thread
和Slave
的I/O Thread
、SQL Thread
,它们的作用如下:
- Master服务器对数据库更改操作记录在Binlog中,BinlogDump Thread接到写入请求后,读取
- Binlog信息推送给Slave的I/O Thread。
- Slave的I/O Thread将读取到的Binlog信息写入到本地Relay Log中。
- Slave的SQL Thread检测到Relay Log的变更请求,解析relay log中内容在从库上执行。
上述过程都是异步操作
,俗称异步复制,存在数据延迟现象。
mysql主从复制存在的问题:
- 主库宕机后,数据可能丢失
- 从库只有一个SQL Thread,主库写压力大,复制很可能延时
解决方法:
- 半同步复制—解决数据丢失的问题
- 并行复制----解决从库复制延迟的问题
半同步复制
为了提升数据安全,MySQL让Master在某一个时间点等待Slave节点的 ACK
(Acknowledgecharacter)消息,接收到ACK消息后才进行事务提交,这也是半同步复制
的基础,MySQL从5.5版本开始引入了半同步复制机制来降低数据丢失的概率。介绍半同步复制之前先快速过一下 MySQL 事务写入碰到主从复制时的完整过程,主库事务写入分为 4个步骤:
- InnoDB Redo File Write (Prepare Write)
- Binlog File Flush & Sync to Binlog File
- InnoDB Redo File Commit(Commit Write)
- Send Binlog to Slave
当Master不需要关注Slave是否接受到Binlog Event时,即为传统的主从复制。
当Master需要在第三步等待Slave返回ACK时,即为 after-commit,半同步复制(MySQL 5.5引入)。
当Master需要在第二步等待 Slave 返回 ACK 时,即为 after-sync,增强半同步(MySQL 5.7引入)。
下图是 MySQL 官方对于半同步复制的时序图,主库等待从库写入 relay log 并返回 ACK 后才进行Engine Commit。
并行复制
MySQL的主从复制延迟一直是受开发者最为关注的问题之一,MySQL从5.6版本开始追加了并行复制功能,目的就是为了改善复制延迟问题,并行复制称为enhanced multi-threaded slave(简称MTS)。在从库中有两个线程IO Thread和SQL Thread,都是单线程模式工作,因此有了延迟问题,我们可以采用多线程机制来加强,减少从库复制延迟。(IO Thread多线程意义不大,主要指的是SQL Thread多线
程)在MySQL的5.6、5.7、8.0版本上,都是基于上述SQL Thread多线程思想,不断优化,减少复制延迟。
MySQL 5.6并行复制原理
MySQL 5.6版本也支持所谓的并行复制,但是其并行只是基于库的。如果用户的MySQL数据库中是多个库,对于从库复制的速度的确可以有比较大的帮助。
MySQL 5.7并行复制原理
MySQL 5.7是基于组提交的并行复制,MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就是slave服务器的回放与master服务器是一致的,即master服务器上是怎么并行执行的slave上就怎样进行并行回放。不再有库的并行复制限制。
MySQL 5.7中组提交的并行复制究竟是如何实现的?
MySQL 5.7是通过对事务进行分组,当事务提交时,它们将在单个操作中写入到二进制日志中。如果多个事务能同时提交成功,那么它们意味着没有冲突,因此可以在Slave上并行执行,所以通过在主库上的二进制日志中添加组提交信息。
MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这些当然也可以在从库中并行提交,因为处理这个阶段的事务都是没有冲突的。在一个组里提交的事务,一定不会修改同一行。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分发算法,等待策略等复杂的而又效率底下的工作。
InnoDB事务提交采用的是两阶段提交模式。一个阶段是prepare,另一个是commit。
为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,其可以配置的值有:DATABASE(默认值,基于库的并行复制方式)、LOGICAL_CLOCK(基于组提交的并行复制方式)。
那么如何知道事务是否在同一组中,生成的Binlog内容如何告诉Slave哪些事务是可以并行复制的?
在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。为了避免用户没有开启GTID功能(gtid_mode=OFF),MySQL 5.7又引入了称之为Anonymous_Gtid的二进制日志event类型ANONYMOUS_GTID_LOG_EVENT
。
通过mysqlbinlog工具分析binlog日志,就可以发现组提交的内部信息。可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和sequence_number,last_committed表示事务提交的时候,上次事务提交的编号,如果事务具有相同的last_committed,表示这些事务都在一组内,可以进行并行的回放。
MySQL8.0 并行复制
MySQL8.0 是基于write-set
的并行复制。MySQL会有一个集合变量来存储事务修改的记录信息(主键哈希值),所有已经提交的事务所修改的主键值经过hash后都会与那个变量的集合进行对比,来判断改行是否与其冲突,并以此来确定依赖关系,没有冲突即可并行。这样的粒度,就到了 row级别了,此时并行的粒度更加精细,并行的速度会更快。
并行复制配置与调优
- binlog_transaction_dependency_history_size
用于控制集合变量的大小。 - binlog_transaction_depandency_tracking
用于控制binlog文件中事务之间的依赖关系,即last_committed值。
- COMMIT_ORDERE: 基于组提交机制
- WRITESET: 基于写集合机制
- WRITESET_SESSION: 基于写集合,比writeset多了一个约束,同一个session中的事务last_committed按先后顺序递增
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
的工作。
InnoDB事务提交采用的是两阶段提交模式。一个阶段是prepare,另一个是commit。
为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,其可以配置的值有:DATABASE(默认值,基于库的并行复制方式)、LOGICAL_CLOCK(基于组提交的并行复制方式)。
那么如何知道事务是否在同一组中,生成的Binlog内容如何告诉Slave哪些事务是可以并行复制的?
在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。为了避免用户没有开启GTID功能(gtid_mode=OFF),MySQL 5.7又引入了称之为Anonymous_Gtid的二进制日志event类型ANONYMOUS_GTID_LOG_EVENT
。
通过mysqlbinlog工具分析binlog日志,就可以发现组提交的内部信息。可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和sequence_number,last_committed表示事务提交的时候,上次事务提交的编号,如果事务具有相同的last_committed,表示这些事务都在一组内,可以进行并行的回放。
MySQL8.0 并行复制
MySQL8.0 是基于write-set
的并行复制。MySQL会有一个集合变量来存储事务修改的记录信息(主键哈希值),所有已经提交的事务所修改的主键值经过hash后都会与那个变量的集合进行对比,来判断改行是否与其冲突,并以此来确定依赖关系,没有冲突即可并行。这样的粒度,就到了 row级别了,此时并行的粒度更加精细,并行的速度会更快。
并行复制配置与调优
- binlog_transaction_dependency_history_size
用于控制集合变量的大小。 - binlog_transaction_depandency_tracking
用于控制binlog文件中事务之间的依赖关系,即last_committed值。
- COMMIT_ORDERE: 基于组提交机制
- WRITESET: 基于写集合机制
- WRITESET_SESSION: 基于写集合,比writeset多了一个约束,同一个session中的事务last_committed按先后顺序递增
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-hzWf0DSk-1713467230316)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!