前言
这是一篇记录笔者 搜集和整理的java面试题的文章,里面加入了不少自己的想法,希望对大家有帮助。
更新记录
| 序号 | 更新时间 | 更新内容 |
|---|---|---|
| 1 | 2023-11-08 | mysql原理 |
一、MYSQL
mysql篇共分三块内容 mysql原理、mysql应用、mysql面试题。
1、mysql原理
mysql原理主要写几个常用的引擎、数据结构等。
1.1 架构模型
- 客户端:redis-cli、navicat等。
- 连接器:负责mysql和客户端之间的通信,提供连接处理和身份验证功能。
- 分析器:提供词法分析和语法分析。
- 缓存:查询结果缓存,mysql8.0 后删除该功能。
- 优化器:使用他认为最正确的方式执行,可使用explain分析。
- 执行器:调用存储引擎,执行最终sql。
- 存储引擎:可插拔式存储引擎,有innodb、myisam等
1.2 db引擎
mysql常用的存储引擎有 innodb、myisam、memory、merge,最常用的是innodb和myisam,这里做一个特征介绍。
innodb
- innodb数据采用B+树存储数据。
- innodb文件格式 .frm .idb (frm存储表结构数据,idb存储表数据和索引数据)
myisam
- myisam和innodb数据结构类似,唯一区别是,叶子节点存放的是数据地址而非全量数据。
- myisam文件格式 .frm .myi .myd (frm存储表结构数据,myi存储索引数据,myd存储表数据)
memory
- memory索引可以是hash或B树,数据实际存放于内存中,重启会丢失(hash索引 针对精确查询速度极快,但是不支持范围查询;),适合做中间结果表或临时表;
- memory文件格式 .frm (frm存储表结构数据,实际数据存放于内存中)
merge
- merge表是一组myisam的组合表,实际上merge表是没有数据的,对于merge表的增删改查操作实际上是对下层 myisam表的操作。
| 特点 | innodb | myisam | memory | merge |
|---|---|---|---|---|
| 存储位置 | 磁盘 | 磁盘 | 内存 | 磁盘 |
| 索引结构 | B+tree | B+tree | Btree\hash | 同myisam |
| 文件格式 | frm .idb | .frm .myi .myd | .frm | 同myisam |
| 事务 | 支持 | 不支持 | 不支持 | 同myisam |
| 外键 | 支持 | 不支持 | 不支持 | 同myisam |
| 表压缩 | 不支持 | 支持 | 不支持 | 同myisam |
tips
- 查看所有存储引擎 show engines ;
- 查看你的mysql当前默认的存储引擎 show variables like '%storage_engine%';
- 显示某个引擎的参数设置 show engine innodb status;
- 看某一个表的存储引擎,就是看该表的创建过程: show create table [tb_name]; show table status like '[tb_name]'; select table_name,engine from tables where table_name='[tb_name]' and table_schema='[db_name]';
- 修改存储引擎:alter table city engine=MyISAM;
扩展:
- B+树:每个节点可以有多个子节点,被称为度;每个节点的大小通常与磁盘IO的大小一致(例:4k);非叶子节点不存储数据,只存储键值区间,用于指示子节点的范围;叶子节点存储全量数据;叶子节点之间存在双向链表连接,方便范围查找;
- B树:每个节点可以有多个子节点,被称为度;每个子节点存储键值区间和数据;平衡的多路搜索树,是二叉树的一种改进;每个节点的大小通常与磁盘IO的大小一致(例:4k);
- 二叉树:每个节点只有两个子节点;左中右大小排序原则;每个节点要存储键值和数据;
| 特点 | 二叉树 | B树 | B+树 |
|---|---|---|---|
| 非叶子节点数据格式 | 键值+数据 | 键值区间+数据 | 键值区间 |
| 叶子节点数据格式 | 键值+数据 | 键值+数据 | 键值+数据+双向链表 |
| 节点度数 | 2 | 磁盘IO/每份数据大小 | 磁盘IO/每份数据大小 |
| 层高 | 高 | 中 | 低 |
1.3 MYSQL 事务
innodb的一大特征是支持事务,基于acid,划分事务的隔离级别解决可能出现的问题。
ACID
- 原子性(Atomicity):整个事务中的所有操作,必须作为一个单元全部完成,借助undolog实现
- 一致性(Consistency):在事务开始之前与结束之后,数据库都保持一致状态,一般使用业务逻辑上来保证
- 隔离性(Isolation):一个事务不会影响其他事务的运行,使用锁机制和MVCC机制来实现
- 持久性(Durability):在事务完成以后,该事务对数据库所作的更改将持久地保存在数据库之中,并不会被回滚
隔离级别
- 读未提交:读未提交存在脏读问题
- 读已提交:读已提交解决了脏读问题但是存在不可重复读和幻读问题。
- 可重复读:隔离级别设置成为可重复,解决了不可重复读和幻读的问题
- 串行化:隔离级别设置成为串行化实际上数据库已经是同步的了,实际使用过程中不会使用该隔离级别
数据一致性问题
- 脏读:一个事务在提交之前,在事务过程中修改的数据,被其他事务读取到
- 不可重复读:一个事务在提交之前,在事务过程中读取以前曾读取过的数据却发现数据发生了改变。
- 幻读:一个事务在提交之前,在事务过程中读取以前曾读取过的区间数据发现数据量增多或减少。
| 隔离级别\问题 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | 不可能 |
| 串行化 | 不可能 | 不可能 | 不可能 |
tips
- 查看事务隔离级别:
5.7.2之前 使用 show variables like 'tx_isolation' 或者 select @@tx_isolation;
5.7.2之后 使用 show variables like 'transaction_isolation'或者 select @@transaction_isolation;
- 修改事务隔离级别:
5.7.2之前 使用 set [global 全局范围 /session 会话范围 ] tx_isolation = ‘隔离级别’
5.7.2之后 使用 set [global 全局范围 /session 会话范围 ] transaction_isolation = ‘隔离级别’
示例:set global tx_isolation = 'READ-COMMITTED';
1.4 mysql锁
数据库的acid特性中的隔离性实现方式是依赖MYSQL的锁机制,LBCC和MVCC。
锁分类
锁原理
- record锁:锁住的是一条记录,有 S 锁和 X 锁之分;
示例:select * from t_test where id = 1 for update;
-
gap锁:存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
-
临键锁:是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
- 行锁:行锁只对某一条数据加锁,应用算法是record锁、gap锁、临键锁。
原理:当sql执行按照索引进行数据检索时,查询条件为范围查找(between and ,<,>等)并有数据命中, 这时sql语句加的锁为next-key-locks,锁住了索引记录所在区间以及索引记录的下一个区间(都是左开右闭), 没有查到结果的话那么退化为间隙锁或记录锁。
- 表锁:如果sql语句没有增加索引条件,就无法触发索引准确查询,从而触发表锁,需要区分S和X锁。
- 页锁:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。因此,采取了这种的页级锁,一次锁定相邻的一组记录,DB 支持页级锁。
- 共享锁:多个事务对于同一数据可以共享一把锁,都可以访问到数据,但是只能读不能修改因此又叫做读锁、s锁。 加锁方式:查询语句后使用lock in share mode
当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
- 排他锁:排他锁不能与其他锁共存,又叫做写锁、x锁 加锁方式:更新自动加排他锁 、查询语句使用for update
当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)
- 意向锁:当事务准备在某条记录上加 S 锁时,需要先在表级别加上一个 IS 锁。当事务准备在某记录上加 X 锁时,需要先在表级别加上一个 IX 锁。
IS、IX 锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时可以快速判断表中的记录是否被加上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS 和 IX 锁就避免了判断表中行有没有加锁对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁
- 悲观锁:关系数据库管理系统中,悲观并发控制(悲观锁)是一种并发控制的方法;悲观锁指的是采用一种持悲观消极的态度,默认数据被外界访问时,必然会产生冲突,所以在数据处理的整个过程中都采用加锁的状态,保证在同一时间、只有一个线程可以访问到数据,实现数据的排他性;通常,数据库的悲观锁是利用数据库本身提供的锁机制去实现的;
实现:1.外界要访问某条数据,那它就要首先向数据库申请改数据的锁(某种锁);2.如果获取成功,那它就可以操作该数据,在它操作期间,其它客户端就无法操作该数据了;3.如果获取失败,则代表同一时间已有其它客户端获取了该锁,那就必须等待其他用户释放锁;
优点:在写多读少的并发环境中使用,可以做到数据的安全性;
缺点:加锁会增加系统开销,降低数据的吞吐量低,不适合在读多写少的场景下使用;
- 乐观锁: 假设认为即使在并发环境中,外界对数据的操作一般是不会造成冲突,所以并不会去加锁(所以乐观锁并不是一把锁),而是在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,知道成功为止;数据库的乐观锁,并不是利用数据库本身的锁去实现的,可能是利用某种实现逻辑去实现做到乐观锁的思想。
实现:1.在表中增加一个字段作为该记录的版本标识,比如叫 version,每次对该记录的写操作都会让 version + 1;2.做出更新,要提交的时候,就会拿取得的 version 取跟数据库中的 version 比较是否一致,如果一致则代表这个时间段,并没有其它的县城也对这个数据进行修改、更新,同时 version + 1;如果不一致,则代表这个时间段,该记录已经被其它线程修改过了,认为是过期数据,返回冲突信息,比如重试;3. update table set num = num + 1, version = version + 1 where version = #{version} and id = #{id}
优点:读多写少的并发场景下,可以避免数据库加锁的开销,提高 DAO 层的响应性能;
缺点:写多读少的并发场景下,即在写操作的竞争激烈的情况下,会导致 CAS 多次重试,冲突频率过高,导致开销比悲观锁更高;
MVCC
// todo:当前读和快照读。。。。,可见算法,额外字段,等等待完善