文章目录
- 1:mysql基础面试题
- 什么是mysql
- 什么是事务
- 并发事务带来的影响
- 事物的隔离级别
- 索引
- 大表优化
- 什么是池化思想,什么是数据库连接池,为什么要用数据库连接池
- ⾏锁,表锁;乐观锁,悲观锁
- MySQL主备同步的基本原理
- SQL什么情况下不会使⽤索引(不包含,不等于,函数)
- —般在什么字段上建索引(过滤数据最多的字段)
- mysql调优
- innodb如何实现mysql的事务?
- 为什么索引采用B+Tree
- mysql如何处理慢查询
- 1:定位慢查询
- 2:分析慢查询sql语句
- 3:字段解析
- 4:通过sql分析,找到问题所在,
- 查询操作方法是否需要使用事务呢
- 如何选择mysql的隔离级别
- 长事务的影响
- 长事务的优化
- 大事务与长事务的区别
- 2:一条SQL在MySQL中是如何执行的
- 1:mysql的基本架构:
- 2:Mysql主要分为server层和存储引擎层:
- Server 层:
- 连接器
- 查询缓存
- 分析器
- 优化器
- 执行器
- 存储引擎-Store层:
- 3:查询语句的执行流程如下:
- 4:更新语句执行流程如下:
- 3:[Explain详解与索引最佳实践](https://blog.csdn.net/weixin_43761434/article/details/128182880)
- 4:[Mysql索引优化实战](https://blog.csdn.net/weixin_43761434/article/details/128201450?csdn_share_tail=%7B%22type%22:%22blog%22,%22rType%22:%22article%22,%22rId%22:%22128201450%22,%22source%22:%22weixin_43761434%22%7D)
- 5:深入理解Mysql索引底层数据结构与算法
- 索引的本质
- 各种数据结构作为索引的底层数据结构的问题
- 二叉树
- 红黑树(二叉平衡树)
- B-Tree
- B+Tree
- B+Tree与B-Tree相比的优势
- 根据索引查询数据的一次完整过程:
- mysql针对根节点默认是可以存储16kb的数据的,
- 那为什么不把整张表的所有的索引元素都放在一个节点上面去?
- 可以按照上图就算树高只有三层进行计算
- MyISAM与InnoDB
- MyISAM存储引擎底层数据文件存储结构
- MyISAM执行sql索引查找数据原理
- InnoDB存储引擎底层数据文件存储结构
- InnoDB执行sql索引查找数据原理
- 聚集索引与非聚集索引的区别
- 为什么建议InnoDB表必须建主键?
- 为什么推荐使用整型的自增主键?
- 为什么要求自增
- 为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)
- 联合索引
- 6:深入理解Mysql锁与事务隔离级别
- 概述
- 事务及其ACID属性
- 并发事务处理带来的问题
- 更新丢失(Lost Update)或脏写
- 脏读(Dirty Reads)
- 不可重读(Non-Repeatable Reads)
- 幻读(Phantom Reads)
- 事务隔离级别
- 常看当前数据库的事务隔离级别:
- 设置事务隔离级别:
- 锁详解
- 锁分类
- 表锁
- 基本操作
- 手动增加表锁
- 查看表上加过的锁
- 删除表锁
- 表锁案例
- 加读锁
- 加写锁
- 案例结论
- 行锁
- InnoDB与MYISAM的最大不同有两点:
- 总结:
- 行锁与事务隔离级别案例分析
- 建表
- 读未提交:
- 读已提交
- 可重复读
- 串行化
- 间隙锁(Gap Lock)
- 无索引行锁会升级为表锁
- 结论
- 行锁分析
- 查看INFORMATION_SCHEMA系统库锁相关数据表
- 死锁
- 锁优化建议
- 7:深入理解MVCC与BufferPool缓存机
- 了解copyOnWrite机制
- 为什么搞一套copyOnWrite机制
- MVCC多版本并发控制机制
- undo日志版本链与read view机制详解
- undo日志版本链
- 一致性视图read-view
- 版本链比对规则:
- 总结:
- Innodb引擎SQL执行的BufferPool缓存机制
- 为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
- 为什么不顺写磁盘idb文件
- bin-log相关知识
- 什么是bin-log呢?
- 开启MySQL的binlog功能。
- 数据归档操作
1:mysql基础面试题
什么是mysql
Mysql是一种关系型数据库,默认端口为3306。
什么是事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。
- 原子性:
事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性:
执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; - 隔离性:
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 永久性:
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来的影响
- 脏读:
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据。 - 丢失修改:
指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 - 不可重复读:
一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。 - 幻读:
一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。
事物的隔离级别
-- 读未提交read-uncommitted;
-- 读已提交read-committed;
-- 可重复读repeatable-read;
-- 串行化 serializable
- 1:读取未提交:
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 - 2:读取已提交:
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 - 3:可重复读:
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 - 4:可串行化:
最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
索引
Mysql 使用的索引主要由BTree索引和哈希索引, 对于哈希索引来说,底层使用的数据结构就是哈希表。 因此在绝大多数需求为单挑记录查询时,可以选择哈希索引,查询性能最快;其余大部分场景,选择BTree 索引。
Mysql 中的BTree 索引使用的时 B树 中的 B+Tree, 但对于主要的两种存储引擎的实现方式是不同的。
大表优化
当Mysql 单表记录数过大时,数据库的CRUD 性能会明显下降,一些常见的优化措施如下:
- 限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句。比如,我们在查询订单历史数据时,可以控制在一个月 - 读写分离
经典的数据库拆分方案,主库负责写,从库负责读 - 垂直分区
根据数据库里面的数据表的相关性进行拆分,例如用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放入单独的库做分库。
优点:
可以使得列数据变小,在查询时减少读取的Block 数,减少 I/O 次数,此外,垂直分区可以简化表的结构,易于维护。
缺点:
主键会出现冗余,需要管理冗余列,并会引起 Join 操作,可以通过在应用层进行join 来解决。此外,垂直分区也会让事务变得更复杂。
什么是池化思想,什么是数据库连接池,为什么要用数据库连接池
数据库连接本质就是一个 socket 的连接。可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。连接池还减少了用户必须等待建立与数据库的连接的时间。
⾏锁,表锁;乐观锁,悲观锁
- ⾏锁:
数据库表中某⼀⾏被锁住。 - 表锁:
整个数据库表被锁住。 - 乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别⼈不会修改,具体实现是给表增加⼀个版本号的字段,在执⾏update操作时⽐较该版本号是否与当前数据库中版本号⼀致,如⼀致,更新数据,反之拒绝。 - 悲观锁:
顾名思义,就是很悲观,每次去拿数据的时候都认为别⼈会修改。读数据的时候会上锁,直到update完成才释放锁,使⽤悲观锁要注意不要锁住整个表。
MySQL主备同步的基本原理
mysql主备复制实现分成三个步骤:
- master将改变记录到⼆进制⽇志(binary log)中(这些记录叫做⼆进制⽇志事件,binary log events,可以通过show binlog events进⾏查看);
- slave将master的binary log events拷⻉到它的中继⽇志(relay log);
- slave重做中继⽇志中的事件,将改变反映它⾃⼰的数据。
SQL什么情况下不会使⽤索引(不包含,不等于,函数)
- select * 可能导致不⾛索引;
- 空值会导致不⾛索引,因为hashset不能存空值;
- 索引列有函数运算,不⾛索引,可以在索引列建⽴⼀个函数的索引。
- 隐式转换可能导致不⾛索引;
- 表的数据库⼩或者需要选择⼤部分数据,不⾛索引;
- !=或者<>可能导致不⾛索引;
- 字符型的索引列会导致优化器认为需要扫描索引⼤部分数据且聚簇因⼦很⼤,最终导致弃⽤索引扫描⽽改⽤全表扫描⽅式
- like ‘%liu’ 百分号在前不⾛索引;
- not in, not exist不⾛索引;
—般在什么字段上建索引(过滤数据最多的字段)
- 表的主键、外键必须有索引;
- 数据量超过300的表应该有索引;
- 经常与其他表进⾏连接的表,在连接字段上应该建⽴索引;
- 经常出现在Where⼦句中的字段,特别是⼤表的字段,应该建⽴索引;
- 索引应该建在选择性⾼的字段上;
- 索引应该建在⼩字段上,对于⼤的⽂本字段甚⾄超⻓字段,不要建索引;
mysql调优
- explain select语句;
- 当只要⼀条数据时使⽤limit 1;
- 为搜索字段建索引;
- 避免select *;
- 字段尽量使⽤not null;
- 垂直分割;
- 拆分⼤的delete和insert语句:delete和insert会锁表;
- 分表分库分区。
- 尽量避免管理太多的表进行查询,一般超过三张表就需要拆分,在业务逻辑中去处理,这是因为在关联表查询的时候会涉及到比较复杂的join算法,比较影响mysql性能,而且mysql性能是有瓶颈且不好扩展的,相比而言,业务处理,机器性能好扩展的多。
innodb如何实现mysql的事务?
事务进⾏过程中,每次sql语句执⾏,都会记录undo log和redo log,然后更新数据形成脏⻚,然后redo log按照时间或者空间等条件进⾏落盘,undo log和脏⻚按照checkpoint进⾏落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发⽣崩溃,则⾸先检查checkpoint记录,使⽤相应的redo log进⾏数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使⽤undo log进⾏事务回滚。事务执⾏COMMIT操作时,会将本事务相关的所有redo log都进⾏落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏⻚继续按照checkpoint进⾏落盘。如果此时发⽣了崩溃,则只使⽤redo log恢复数据。
为什么索引采用B+Tree
- 什么是索引:
索引的存在是为了加速对表中数据行的检索,所以他是分散的一种数据结构。
索引的数据结构到底是什么样的呢?
索引里存储的是数据的磁盘地址,这样查找数据库的时候,并不需要全表搜索,而只需要在索引里先找到数据对应的地址,直接到数据库里取就好了。 - 为什么是b+tree呢:
为什么不是二叉查找树?
二叉查找树是不平衡的,如果我们要查找22这个数就几乎要遍历所有的数据,这个结构就看起来很不对头。肯定不能作为索引的数据结构。(7654321)
为什么平衡二叉树不行?
因为一个节点只有两个子节点,而且每个节点里面只有一条数据,那一百万条数据的索引,那效率就会很低。
为什么不是B-tree?
mysql如何处理慢查询
1:定位慢查询
- mysql慢查询日志的开启
#开启慢查询日志记录
mysql> set global slow_query_log=on;
Query OK, 0 rows affected (0.00 sec)#查询时间超过0.1秒的sql语句会被记录
mysql> set global long_query_time=0.1;
Query OK, 0 rows affected (0.03 sec)#记录慢查询日志的文件地址
mysql> set global slow_query_log_file="/var/lib/mysql/localhost-slow.log";
Query OK, 0 rows affected (0.04 sec)#记录没有使用索引的查询
mysql> set global log_queries_not_using_indexes=on;
Query OK, 0 rows affected (0.00 sec)show variables like 'slow_query_log';show variables like 'long_query_time';show variables like 'slow_query_log_file';show variables like 'log_queries_not_using_indexes';
2:分析慢查询sql语句
用 EXPLAIN 查看SQL执行计划。
3:字段解析
-
type列
-
连接类型。一个好的sql语句至少要达到range级别。杜绝出现all级别
-
key列
使用到的索引名。如果没有选择索引,值是NULL。可以采取强制索引方式 -
key_len列
索引长度 -
rows列
扫描行数。该值是个预估值 -
extra列
详细说明。注意常见的不太友好的值有:Using filesort, Using temporary
4:通过sql分析,找到问题所在,
比如这个sql是否全表扫描了,索引是否正常生效(前提是表建了索引),对sql进行一个优化。
查询操作方法是否需要使用事务呢
- 要根据具体业务场景以及数据库的隔离级别来判断是否需要事务
- 假如数据库使用的是可重复读且在业务中多次查询,那么就需要使用事务,我们可以把事物的read-only属性设置为true。
- 查询操作通常不需要使用事务,因为它们不修改数据,只是读取数据。事务主要用于确保写操作(如插入、更新、删除)的原子性、一致性、隔离性和持久性(ACID属性)。然而,在某些情况下,如果需要保证读取操作的一致性视图,比如在一个复杂的查询中需要确保数据在查询过程中不被其他事务修改,要保证查询出来的数据是在同一时间维度上的,那么可以使用事务。这种情况下,通常会使用一种称为“可重复读”或“串行化”的隔离级别,
如何选择mysql的隔离级别
选择MySQL的隔离级别取决于应用程序的需求和性能考虑。一般来说,可以按照以下原则选择隔离级别:
- 如果应用程序对数据的一致性要求很高,可以选择较高的隔离级别,如SERIALIZABLE或REPEATABLE READ。
- 如果应用程序对性能要求较高,可以选择较低的隔离级别,如READ COMMITTED或READ UNCOMMITTED。
- 考虑并发性能和数据一致性之间的平衡,选择适当的隔离级别。
- 在选择隔离级别时,还需要考虑数据库中的并发操作情况,以避免出现死锁等并发问题。
综合考虑以上因素,可以根据具体情况选择适合的隔离级别。
长事务的影响
- 并发情况下,数据库连接池容易被撑爆
- 锁定太多的数据,会造成大量的阻塞和锁超时
- 执行时间太长,会造成主从延迟
- 回滚所需要的时间也比较长
- undo log膨胀
- 容易导致死锁
长事务的优化
- 将查询等数据准备操作放到事务外
- 事务中避免远程调用,就算有,也是用异步的
- 事务中避免一次处理太多的数据,可以拆分为多个事务分次处理
- 更新等设计加锁的操作尽可能的放在事务后面操作
- 能异步处理的尽量异步处理
- 业务代码保证数据一致性,非事务执行
大事务与长事务的区别
大事务和长事务是两个不同的概念,它们分别关注事务的不同属性:
- 大事务:
- 关注点
大事务主要关注事务所涉及的数据量大小。一个事务如果修改或访问了大量的数据,就被称为大事务。 - 问题
大事务可能会消耗大量的数据库资源,如内存和事务日志空间,同时可能会持有大量的锁,增加死锁的风险,影响数据库的并发能力。 - 优化策略
可以通过拆分事务、减少每个事务处理的数据量、优化SQL语句和索引等方式来优化大事务。
- 长事务:
- 关注点:
长事务主要关注事务的持续时间。如果一个事务从开始到提交的时间很长,就被称为长事务。 - 问题:
长事务可能会长时间占用数据库资源,如锁资源,导致其他事务等待,增加系统的响应时间,降低并发性能。 - 优化策略:
可以通过减少事务中的操作、优化查询性能、适时提交事务等方式来缩短事务的持续时间。
总的来说,大事务关注的是事务操作的数据量,而长事务关注的是事务的持续时间。两者都可能对数据库性能产生负面影响,需要根据实际情况采取不同的优化策略。
2:一条SQL在MySQL中是如何执行的
1:mysql的基本架构:
- 连接器:
身份认证和权限相关(登录 MySQL 的时候)。 - 查询缓存:
执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 - 分析器:
没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 - 优化器:
按照 MySQL 认为最优的方案去执行。 - 执行器:
执行语句,然后从存储引擎返回数据。
2:Mysql主要分为server层和存储引擎层:
主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
连接器
-
连接器负责跟客户端建立连接、获取权限、维持和管理连接。
在完成经典的 TCP 握手后,连接器就要开始认证你的身份,
如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。用户的权限表在系统表空间的mysql的user表中。
-
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。文本中这个图是 show processlist 的结果,其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。
-
客户端如果长时间不发送command到Server端,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值
是 8 小时。
-
mysql常用命令
mysql>show databases; 显示所有数据库
mysql>use dbname; 打开数据库:
mysql>show tables; 显示数据库mysql中所有的表;
mysql>describe user; 显示表mysql数据库中user表的列信息);
查询缓存
-
连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
-
大多数情况查询缓存就是个鸡肋,为什么呢?
因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。一般建议大家在静态表里使用查询缓存,什么叫静态表呢?就是一般我们极少更新的表。比如,一个系统配置表、字典表,那这张表上的查询才适合使用查询缓存。好在 MySQL 也提供了这种“按需使用”的方式。你可以将my.cnf参数query_cache_type 设置成 DEMAND。
my.cnf
#query_cache_type有3个值 0代表关闭查询缓存OFF,1代表开启ON,2(DEMAND)代表当sql语句中有SQL_CACHE关键词时才缓存
query_cache_type=2
- 如何指定sql语句使用查询缓存
这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:
select SQL_CACHE * from test where ID=5;
这样这个sql查询的数据就会进入缓存。
- 查看当前mysql实例是否开启缓存机制
show global variables like "%query_cache_type%";
- 监控查询缓存的命中率:
show status like'%Qcache%'; //查看运行的缓存信息
Qcache_free_blocks:
表示查询缓存中目前还有多少剩余的blocks,如果该值显示较大,则说明查询缓存中的内存碎片过多了,可能在一定的时间进行整理。
Qcache_free_memory:
查询缓存的内存大小,通过这个参数可以很清晰的知道当前系统的查询内存是否够用,是多了,还是不够用,DBA可以根据实际情况做出调整。
Qcache_hits:
表示有多少次命中缓存。我们主要可以通过该值来验证我们的查询缓存的效果。数字越大,缓存效果越理想。
Qcache_inserts:
表示多少次未命中然后插入,意思是新来的SQL请求在缓存中未找到,不得不执行查询处理,执行查询处理后把结果insert到查询缓存中。这样的情况的次数,次数越多,表示查询缓存应用到的比较少,效果也就不理想。当然系统刚启动后,查询缓存是空的,这很正常。
Qcache_lowmem_prunes:
该参数记录有多少条查询因为内存不足而被移除出查询缓存。通过这个值,用户可以适当的调整缓存大小。
Qcache_not_cached:
表示因为query_cache_type的设置而没有被缓存的查询数量。
Qcache_queries_in_cache:
当前缓存中缓存的查询数量。
Qcache_total_blocks:
当前缓存的block数量。
mysql8.0已经移除了查询缓存功能
分析器
-
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。
MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 from 写成了"rom"。
-
词法分析器原理
词法分析器分成6个主要步骤完成对sql语句的分析
1、词法分析
2、语法分析
3、语义分析
4、构造执行树
5、生成执行计划
6、计划的执行
-
SQL语句的分析分为词法分析与语法分析,
mysql的词法分析由MySQLLex[MySQL自己实现的]完成,语法分析由Bison生成。那么除了Bison外,Java当中也有开源的词法结构分析工具例如Antlr4,ANTLR从语法生成一个解析器,可以构建和遍历解析树,可以在IDEA工具当中安装插件:antlr v4 grammar plugin。插件使用详见课程。
-
过bison语法分析之后,会生成一个这样的语法树
优化器
- 按照 MySQL 认为最优的方案去执行。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 - 比如你执行下面这样的语句,这个语句是执行两个表的 join:
select * from test1 join test2 using(ID) where test1.name=yangguo and test2.name=xiaolongnv;
既可以先从表 test1 里面取出 name=yangguo的记录的 ID 值,再根据 ID 值关联到表 test2,再判断 test2 里面 name的值是否等于 yangguo。
也可以先从表 test2 里面取出 name=xiaolongnv 的记录的 ID 值,再根据 ID 值关联到 test1,再判断 test1 里面 name的值是否等于 yangguo。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。
执行器
> select * from test where id=1;
-
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
-
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
-
比如我们这个例子中的表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:
1:调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
2:调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
3:执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。 -
存储引擎-Store层:
主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始就被当做默认存储引擎了。也就是说如果我们在create table时不指定表的存储引擎类型,默认会给你设置存储引擎为InnoDB。
3:查询语句的执行流程如下:
- 权限校验(如果命中缓存)—》查询缓存—》分析器—》优化器—》权限校验—》执行器—》引擎
4:更新语句执行流程如下:
- 分析器----》权限校验----》执行器—》引擎—redo log(prepare 状态—》binlog—》redo log(commit状态)
3:Explain详解与索引最佳实践
4:Mysql索引优化实战
5:深入理解Mysql索引底层数据结构与算法
索引的本质
索引是帮助mysql高效获取数据的排好序的数据结构(Hash或者B+Tree)
- 如上图,如果没有索引,那我们需要一行一行的在磁盘上去查询数据,如果数据量特别大,就会导致做很多次的I/O,效率特别的低。所以索引就特别重要。
- 做一次磁盘I/O所花费的时间是很昂贵的。
- 数据在磁盘上存放的地址是并不连续的
各种数据结构作为索引的底层数据结构的问题
推荐一个外国的数据结构在线演示网站
二叉树
可以看出来,就比如我们以自增id作为索引的时候,在比较极端的情况下,这不是二叉树了,变成一个链表了,和没有索引一样,也需要一条一条的去遍历查询,也需要对磁盘做很多的I/O(索引和数据都是存在磁盘上的),所以二叉树pass。
红黑树(二叉平衡树)
如上图红黑树看上去不错,但其实它的本质也是二叉树,只是加入了变色和自旋,在数据量比较大的情况下,树的高度不可控,也需要I/O很多次。所以红黑树pass。
B-Tree
如上图,索引的结点是存储在磁盘空间上的,不管是二叉树还是红黑树,一个节点是只存了一个索引的,而B-Tree呢就针对一个节点分配更大的空间,就可以在这个节点上面存储更多的索引元素(包含了索引和数据–指索引所在行的磁盘文件的地址),从而也能更好的控制树的高度,尽可能的做少的I/O次数。而且在此节点的磁盘空间中,对所有从左到有依次进行排序,便于更好的查找。
B+Tree
当节点中的数据大于我们规定值的15/16的时候,树的结构就会发生变化。
B+Tree与B-Tree相比的优势
B+Tree叶节点只存储索引,不存储具体的数据(Data指索引所在行的磁盘文件的地址),数据只会存在叶子结点上,而且从左到右依次排序,支持双向查找。B+Tree会存储一些冗余的处于中间位置的索引,这样就可以在同样大的磁盘空间上存储更多的索引,提高查询效率,而且,每一行,索引都会从左到右依次递增。但是B-Tree叶节点即存储了索引也存储了起对应的内存地址,就会导致一个数据页存储的数据变少,当数据量过大的时候,树的高度也是很大的,所以最终选择了B+Tree。B+Tree相邻的叶子结点之间是有相互指向的(会存相邻叶子节点之间的内存地址),所以在范围查询的时候,效率也会更高。
除了以上优点,B+Tree还有很多优点,比如,每个父节点的值都小于等于他右子节点的值,大于他左子节点的值,这样一种左开右闭原则也能很好的提升查询的效率。
根据索引查询数据的一次完整过程:
比如就上图我们要查找30这个索引,
1:从根节点出发,根节点是常驻内存的,首先将根节点加载到RAM内存中,在内存中对半查询查找,30在15-56之间,就将中间这块数据(其实就是下一级节点的磁盘文件地址-指针)取出加载到内存。30在20-49之间,就将这中间的节点取出加载到内存,然后就能从内存中定位到索引为30的元素,这元素下面就含有这个索引缩影的数据在磁盘中的内存地址。
mysql针对根节点默认是可以存储16kb的数据的,
SHOW GLOBAL STATUS LIKE 'Innodb_page_size'
那为什么不把整张表的所有的索引元素都放在一个节点上面去?
如果数据库表很多,而且索引也不少,可能会把内存撑爆,而且同时把那么多数据加载到内存中去再去查到,速率也不会很快。而且一次磁盘I/O也加载不了这么多数据。而且根节点是常驻内存的。
可以按照上图就算树高只有三层进行计算
数据库表主键索引,用bigint(8字节),(索引+下一级节点的磁盘文件地址-指针(mysql默认为6字节)),相加就14字节,16k/14~~1000多,第二层一样,第三层有叶子结点,含有data(不同的存储引擎存储结构不一样,后面再讲),就算一行数据有1kb,那么三层就可以存储1000x1000x16=16000000。而且mysql会提前将非叶子结点中的数据加载到内存中,相当于只需要做一次i/o就能定位到索引数据。
MyISAM与InnoDB
MyISAM与InnoDB是形容数据库表而不是数据库。
MyISAM存储引擎底层数据文件存储结构
一个表主要由三个文件组成:见下图
MyISAM执行sql索引查找数据原理
sql执行的时候,会在表索引文件(B+Tree的数据结构存储索引)中,通过B+Tree的数据结构找到我们的索引,叶子结点下面存储的是存在表数据文件中的数据的地址,就可能达到对应的数据的内存地址,然后通过这个地址去表数据文件中定位获取到对应的数据。
InnoDB存储引擎底层数据文件存储结构
一个表由两个文件组成,见下图
- frm:表结构文件。
- ibd:表索引以及数据文件。
ibd文件中,叶子结点下面存储的是索引所在行的其他关联的数据,就不用通过地址再去其他磁盘上查找了。
InnoDB执行sql索引查找数据原理
去表索引以及数据文件中查询到具体的索引后,就可以直接获取到具体的数据,不用再去其他文件查询了。
对于普通的索引,叶子节点存储的是主键id,然后更具主键再去主键索引中查询,查找具体数据。如果我们没有建主键,那么mysql就回去数据库中查找一列中数据唯一的字段作为主键,如果没有这样的数据,会自己生成rowid作为主键id。从节约空间以及通过比较大小快速查找索引角度考虑,主键建议使用自增的整型而不用其他的比如uuid。
聚集索引与非聚集索引的区别
- Myisam索引文件与数据文件是分离的就称为非聚集索引,InnoDB索引与数据在同一个文件,称为聚集索引。
- 聚集索引叶子结点包含了完整的数据记录。
- 稀疏索引其实就是一种非聚集索引。
- 相对而言聚集索引不用跨文件去查找数据,相对而言会更快一点。
为什么建议InnoDB表必须建主键?
在创建表的时候,ibd文件是按照B+Tree的数据结构来组织索引的,建立主键后,mysql就可以使用我们建立的主键来组织索引,如果我们我们没有建立自己的主键。那么mysql就会在我们的表中选择一个没有重复的数据的字段作为主键,如果没有这样的字段,mysql会自己生成一列隐藏的数据rowid来作为我们的主键,如果我们自己建立了主键,mysql就不用做后面这些工作了,可以提示mysql的性能,mysql的性能是很重要的,资源也是很宝贵的。所以我们最好自己建立个主键。
为什么推荐使用整型的自增主键?
整型自增首先他是有序的,而且相对于uuid或者字符串,在生成B+Tree数据结构的时候,因为在一个数据页,从左到右,主键是逐渐变大的。更好比较大小,而且整型占用的空间相对而言要小一点。
为什么要求自增
-
在讨论这个问题之前先来看看hash索引。
hash会对存入来的索引数据进行一次hash运算,并且把这个值放入hash桶中,存在hash冲突的情况。
hash计算出索引的hash值后,一般情况下只需要进行一次磁盘I/O就能定位到数据,Hash相对于B+Tree效率很高,不出现Hash冲突的情况下,查询某一条数据时间复杂度为O(1),但是不支持范围查找,所以用的比较少。相反B+Tree叶子节点中是从左到右一次递增的,而且是个双向链表,不管是查询大于还是小于都很快,相邻的节点直接存有对方的磁盘文件地址,查找起来也很快。 -
回到为什么采用自增作为主键
B+Tree是排好序的。B+Tree叶子节点中是从左到右一次递增的,而且是个双向链表,不管是查询大于还是小于都很快,相邻的节点直接存有对方的磁盘文件地址,查找起来也很快。
如果非自增,在插入数据的时候可能会存在要向已经填满的节点里面强插数据,这样就可能会导致节点分裂,甚至还需要往父节点写入数据等等来平衡B+Tree,效率不如自增。
为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)
- InnoDB表只会存在一个聚集索引,非主键索引(二级索引)叶子结点只会存放聚集索引的值,不会存在具体的数据,这样可以节约存储空间,如果我一张表有很多的非主键索引,我每个B+Tree的叶子结点下面都像主键索引那样存放所有的具体的数据,那样会很占用空间内存。
- 还可以保持一致性,比如在插入数据的时候,非主键索引像主键索引那样存放所有的具体的数据,我们需要确保所有的索引插入的数据正确才算完事,很比较麻烦,但是我们如果只存主键值,就可以通过主键值再去查询其他的数据。
- 每加一个索引就会在ibd文件中维护一个B+Tree的数据结构。
联合索引
也是B+Tree的数据结构,
联合索引首先在构建B+Tree的数据结构的时候,排序的时候,会从左到右一次比较字段的大小去进行排序,第一个字段开始排序,如果第一个字段一样就用第二个字段进行比较排序,依次类推。
在查询的时候,遵循最左前缀原则,我们查询的时候,需要用联合索引的第一个字段来开始查,不能直接跳过这个字段去使用到其他的索引。假如这三个字段是联合索引,在我们写sql的时候,如果我们没有加name这个索引条件字段,那么这个联合索引是不会起效的,不会走索引的,因为单单从后面几个字段我们是没办法判断索引在B+Tree中的排序的。因为我们的联合索引是排好了序的,而且是用第一个字段来一次排序,如果第一个字段不相等,那么就不用以后面的字段来排序,如果不用联合索引中的第一个字段,我们是没办法判断索引在B+Tree中的排序的。
6:深入理解Mysql锁与事务隔离级别
概述
我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。接下来,我们会深入讲解这些机制,让大家彻底理解数据库内部的执行原理。
事务及其ACID属性
- 原子性(Atomicity) :
事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。(操作层面) - 一致性(Consistent) :
在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性(数据层面) - 隔离性(Isolation) :
数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。 - 持久性(Durable) :
事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
并发事务处理带来的问题
更新丢失(Lost Update)或脏写
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
不可重读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
一句话:事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
幻读(Phantom Reads)
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
一句话:事务A读取到了事务B提交的新增数据,不符合隔离性
事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。
常看当前数据库的事务隔离级别:
show variables like 'tx_isolation';
设置事务隔离级别:
set tx_isolation='REPEATABLE-READ';
-- 读未提交read-uncommitted;
-- 读已提交read-committed;
-- 可重复读repeatable-read;
-- 串行化 serializable
Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。
锁详解
- 锁是计算机协调多个进程或线程并发访问某一资源的机制
- 在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
锁分类
- 从性能上分为乐观锁(用版本对比来实现)和悲观锁
- 从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁)
读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响,但不允许其他事务修改,可在sql语句后面加lock in share mode
写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁,在sql语句后加for update
- 从对数据操作的粒度分,分为表锁和行锁
表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。
基本操作
CREATE TABLE `mylock` (`id` INT (11) NOT NULL AUTO_INCREMENT,`NAME` VARCHAR (20) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE = MyISAM DEFAULT CHARSET = utf8;INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('1', 'a');INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('2', 'b');INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('3', 'c');INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('4', 'd')
手动增加表锁
lock table 表名称 read(write),表名称2 read(write);
查看表上加过的锁
show open tables;
删除表锁
unlock tables;
表锁案例
lock table mylock read
当前session和其他session都可以读该表
当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待
lock table mylock write
当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞
案例结论
- 对MyISAM表的读操作(加读锁)
不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。 - 对MylSAM表的写操作(加写锁)
会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作
行锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB与MYISAM的最大不同有两点:
- InnoDB支持事务(TRANSACTION)
- InnoDB支持行级锁
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
总结:
- MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
- InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
行锁与事务隔离级别案例分析
建表
CREATE TABLE `account` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`balance` int(11) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lilei', '450');INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lucy', '2400');
读未提交:
1:打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
set tx_isolation='read-uncommitted';
2:在客户端A的事务提交之前,打开另一个客户端B,更新表account:
3:这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
4:一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
5:在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别。
读已提交
set tx_isolation='read-committed';
(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
(4)客户端B的事务提交
(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
可重复读
(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
set tx_isolation='repeatable-read';
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
(4)在客户端A,接着执行update account set balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤2中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
(5)重新打开客户端B,插入一条新数据后提交
(6)在客户端A查询表account的所有记录,没有查出新增数据,所以没有出现幻读;
(7)验证幻读
在客户端A执行update account set balance=888 where id = 4;能更新成功,再次查询能查到客户端B新增的数据。
串行化
(1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:
set tx_isolation='serializable';
(2)打开一个客户端B,并设置当前事务模式为serializable,更新相同的id为1的记录会被阻塞等待,更新id为2的记录可以成功,说明在串行模式下innodb的查询也会被加上行锁。
如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围(就算该行数据还未被插入也会加锁,这种是间隙锁)都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读。
这种隔离级别并发性极低,开发中很少会用到。
间隙锁(Gap Lock)
- 间隙锁,锁的就是两个值之间的空隙。Mysql默认级别是repeatable-read,有办法解决幻读问题吗?
- 间隙锁在某些情况下可以解决幻读问题。
假设account表里数据如下:
- 那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间
- 在Session_1下面执行 update account set name = ‘zhuge’ where id > 8 and id <18;,则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
- 间隙锁是在可重复读隔离级别下才会生效
无索引行锁会升级为表锁
- 锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
- session1 执行:update account set balance = 800 where name = ‘lilei’;
- session2 对该表任一行操作都会阻塞住。
- InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。
- 锁定某一行还可以用lock in share mode(共享锁) 和for update(排它锁),例如:select * fromtest_innodb_lock where a = 2 for update;这样其他session只能读这行数据,修改则会被阻塞,直到锁定 行的session提交
结论
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb的整体性能和MYISAM相比就会有比较明显的优势了。
但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差。
行锁分析
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
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:
系统启动后到现在总共等待的次数
对于这5个状态变量,比较重要的主要是
- Innodb_row_lock_time_avg (等待平均时长)
- Innodb_row_lock_waits (等待总次数)
- Innodb_row_lock_time(等待总时长)
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
查看INFORMATION_SCHEMA系统库锁相关数据表
‐‐ 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
‐‐ 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
‐‐ 查看锁等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;‐‐ 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id‐‐ 查看锁等待详细信息
show engine innodb status\G;
死锁
set tx_isolation=‘repeatable-read’
- Session_1执行:select * from account where id=1 for update;
- Session_2执行:select * from account where id=2 for update;
- Session_1执行:select * from account where id=2 for update;
- Session_2执行:select * from account where id=1 for update;
查看近期死锁日志信息:show engine innodb status\G;
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁。
锁优化建议
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
- 尽可能低级别事务隔离
7:深入理解MVCC与BufferPool缓存机
了解copyOnWrite机制
为什么搞一套copyOnWrite机制
- 为了保证读取到的数据的安全性,尽可能的避免并发问题,我们可以通过加锁或者copyOnWrite机制机制来实现,通过加锁可以,但是会导致其性能降低。所以为了实现读写高并发的性能,就搞了一套copyOnWrite机制(读写分离)。
- 在nacos中会去搞一个注册表的副本,注册或者修改的时候是修改的副本,然后再去替换真正的注册表内存数据。也就是copy on write机制。可能会出现读取到的数据不是最新数据的问题,但是性能更高。
MVCC多版本并发控制机制
- Mysql在可重复读隔离级别下如何保证事务较高的隔离性,我们上节课给大家演示过,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。
这个隔离性就是靠MVCC(Multi-Version ConcurrencyControl)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
undo日志版本链与read view机制详解
undo日志版本链
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。
- 每一次的修改后的新的数据会有一个回滚指针指向修改前的上一条旧的数据,二最终形成一个undo日志的链条,就被称为undo日志版本链。
一致性视图read-view
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),
这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则:
- 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
- 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若row 的 trx_id 就是当前自己的事务是可见的);
- 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id就是当前自己的事务是可见的);
若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
- 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
注意:
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
总结:
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
Innodb引擎SQL执行的BufferPool缓存机制
为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
- 因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。
- 因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。
- 更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几千的读写请求。
为什么不顺写磁盘idb文件
- 因为不同的表是在不同的磁盘文件中的,而且如果有更新或者删除操作,也没法顺序写磁盘,就只能随机写了,而随机写磁盘的性能是很低的,所以mysql就顺序写日志文件,可以保证数据的安全,而用一个线程来随机写磁盘idb文件来保证其效率。
bin-log相关知识
删库是不需要跑路的,因为我们的SQL执行时,会将sql语句的执行逻辑记录在我们的bin-log当中,
什么是bin-log呢?
binlog是Server层实现的二进制日志,他会记录我们的cud操作。Binlog有以下几个特点:
1、Binlog在MySQL的Server层实现(引擎共用)
2、Binlog为逻辑日志,记录的是一条语句的原始逻辑
3、Binlog不限大小,追加写入,不会覆盖以前的日志
如果,我们误删了数据库,可以使用binlog进行归档!要使用binlog归档,首先我们得记录binlog,因此需要先开启MySQL的binlog功能。
开启MySQL的binlog功能。
- 配置my.cnf
配置开启binlog
log‐bin=/usr/local/mysql/data/binlog/mysql‐bin
注意5.7以及更高版本需要配置本项:server‐id=123454(自定义,保证唯一性);
#binlog格式,有3种statement,row,mixed(建议使用row或者mixd);statement会记录sql的执行逻辑,row会记录影响的执行的结果,mixed两者结合。
binlog‐format=ROW
#表示每1次执行写入就与硬盘同步,会影响性能,为0时表示,事务提交时mysql不做刷盘操作,由系统决定
sync‐binlog=1
- binlog命令
mysql> show variables like ‘%log_bin%’; 查看bin‐log是否开启
flush logs; 会多一个最新的bin‐log日志
show master status; 查看最后一个bin‐log日志的相关信息
reset master; 清空所有的bin‐log日志
- 查看binlog内容
/usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin.000001 查看binlog内容
binlog里的内容不具备可读性,所以需要我们自己去判断恢复的逻辑点位,怎么观察呢?看重点信息,比如begin,commit这种关键词信息,只要在binlog当中看到了,你就可以理解为begin-commit之间的信息是一个完整的事务逻辑,然后再根据位置position判断恢复即可。binlog内容如下:
数据归档操作
从bin‐log恢复数据
- 恢复全部数据
/usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin.000001|mysql ‐uroot ‐p tuling(数据库名) - 恢复指定位置数据
/usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults ‐‐start‐position=“408” ‐‐stop‐position=“731”/usr/local/mysql/data/binlog/mysql‐bin.000001 |mysql ‐uroot ‐p tuling(数据库) - 恢复指定时间段数据
/usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin.000001‐‐stop‐date= “2018‐03‐02 12:00:00” ‐‐start‐date= “2019‐03‐02 11:55:00”|mysql ‐uroot ‐p test(数据库)