《Java 后端面试经》数据库篇

《Java 后端面试经》专栏文章索引:
《Java 后端面试经》Java 基础篇
《Java 后端面试经》Java EE 篇
《Java 后端面试经》数据库篇
《Java 后端面试经》多线程与并发编程篇
《Java 后端面试经》JVM 篇
《Java 后端面试经》操作系统篇
《Java 后端面试经》Linux 篇
《Java 后端面试经》设计模式篇
《Java 后端面试经》计算机网络篇
《Java 后端面试经》微服务篇

《Java 后端面试经》 数据库篇

  • MySQL
    • 通识基础
      • 数据库三范式
      • Mysql 中表的四种连接方式?
      • 在设计数据库表的时候,字段用于存储金额、余额时,选择什么类型比较好?
      • 执行一条 SQL 查询语句期间发生了什么?
      • SQL 包括哪几个部分?
      • SQL 查询语句的执行顺序是如何的?
      • 关心过业务系统里面的 SQL 耗时嘛?统计过慢查询嘛?对慢查询都怎么优化过?
      • 分表后非 sharding_key 的查询怎么处理,分表后的排序?
      • 数据表设计时,字段如何选择?
      • 存储时间数据的注意点
      • MySQL 中 VARCHAR(M) 最多存储多少数据?
      • 如何提高 insert 的性能?
    • 存储引擎
      • MySQL 中有哪些存储引擎?
      • 简述 MyISAM 和 InnoDB 的区别
    • 索引
      • 什么是索引?
      • 索引的基本原理?
      • 索引有哪些种类?
      • 如何为表字段添加索引?
      • 为什么要用索引?
      • 什么时候适合使用索引
      • 什么时候不适合使用索引
      • 索引什么时候会失效
      • 使用索引的注意事项
      • 有什么优化索引的方法?
      • 为什么索引会失效?(从 B+ 树的角度阐述)
      • MySQL 索引为什么要使用 B+ 树?
      • B 树和 B+ 树的区别?
      • 既然索引有这么多优点,为什么不对表中的每一个列创建一个索引呢?
      • MySQL 索引的底层数据结构
      • InnoDB 存储引擎中的 B+ 树
      • 三层 B+ 树能存放多少数据(从内存、操作系统、B+树角度)?
      • 聚集索引和非聚集索引
      • 什么是覆盖索引?
      • 谈谈联合索引和最左前缀原则?
    • 事务
      • 什么是事务
      • 事务有哪些特性
      • 事务并发会带来哪些问题?
      • 事务的隔离级别有哪些?
      • MySQL 默认的事务隔离级别是什么?
      • 什么是 MVCC?
      • Read View 在 MVCC 里是如何工作的?
      • 可重复读是如何工作的?
      • 读提交是如何工作的?
      • 什么是锁,锁的作用是什么?
      • MySQL 有哪些锁?
        • 全局锁
        • 表级锁
        • 行级锁
      • 如何避免数据库发生死锁?
    • 日志
      • 如何查看数据库日志?日志类型有哪些?
  • Redis
    • Redis 有哪些数据类型及底层实现?
      • Redis 常见数据结构有哪些应用场景?
    • Redis 单线程
      • Redis 单线程为什么这么快?
      • Redis 6.0 之后为什么引入多线程?
    • Redis 缓存相关
      • 缓存相关的基本概念
      • 常见的缓存更新策略有哪些?
    • 谈谈 Redis 的持久化策略?
      • Redis 持久化时,对过期键会如何处理的?
    • Redis 的过期删除策略?
      • 什么是过期删除策略?
      • Redis 使用了什么过期删除策略?
    • Redis 的内存淘汰策略?
      • Redis 是如何实现 LRU 和 LFU 算法的?
    • Redis的 zset,为什么要用跳表,不用红黑树?
  • MongoDB
    • MongoDB 是什么?
    • 为什么要用 MongoDB?
    • MongoDB 的常见应用场景?
    • MongoDB 的特点有哪些?

MySQL

通识基础

数据库三范式

  • 第一范式:要求有主键,并且要求每一个字段原子性不可再分
  • 第二范式:要求所有非主键字段完全依赖主键,不能产生部分依赖
  • 第三范式:所有非主键字段和主键字段之间不能产生传递依赖

Mysql 中表的四种连接方式?

  1. 内连接(inner join):只将两个表中满足条件的数据展示出来
  2. 左外连接(left outer join):将左表的数据完全显示,右表仅展示匹配(满足条件)的数据,没有匹配的显示为空 NULL
  3. 右外连接(right outer join):将右表的数据完全显示,左表仅展示匹配(满足条件)的数据,没有匹配的显示为空NULL
  4. 完全连接(full outer join):返回左表和右表中的所有行,当某行在另一个表中没有匹配行时,则另一个表的列显示空值

在设计数据库表的时候,字段用于存储金额、余额时,选择什么类型比较好?

一般使用 decimal,MySQL 数据库提供 decimal 和 numeric 两种类型来修饰金额之类的需要极其精确的数据。

执行一条 SQL 查询语句期间发生了什么?

答案参考小林coding个人博客

《Java 后端面试经》数据库篇_第1张图片

  • 连接器:建立连接、管理连接、校验用户身份。
  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行,MySQL 8.0 已删除该模块。
  • 解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型。
  • 执行 SQL:执行 SQL 共有三个阶段:
    1. 预处理阶段:检查表或字段是否存在,将 select * 中的 * 符号扩展为表上的所有列。
    2. 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划。
    3. 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端。
  • 返回记录:由执行器将查到的数据记录返回给连接器。

SQL 包括哪几个部分?

SQL 由四个部分组成:

  • DML(Data Manipulation Language,数据操作语言):用来插入、修改和删除表中的数据,如 insert、update、delete 语句
  • DDL(Data Definition Language,数据定义语言):在数据库中创建或删除数据库对象等操作,如 create、drop 、alter 等语句
  • DQL(Data Query Language,数据查询语言):用来对数据库中的数据进行查询,指select 语句
  • DCL(Data Control Language,数据控制语言):用来控制数据库组件的存取许可,存取权限等,如 GRANT、REVOKE 等

SQL 查询语句的执行顺序是如何的?

SQL 查询的执行顺序FROM>ON>JOIN>WHERE>GROUP BY>AGG_FUNC>WITH CUBE or WITH ROLLUP>HAVING>SELECT>DISTINCT>ORDER BY>LIMIT/OFFSET

关心过业务系统里面的 SQL 耗时嘛?统计过慢查询嘛?对慢查询都怎么优化过?

在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,MySQL 默认是禁用慢查询日志的。如果需要启动慢查询日志,--slow_query_log[={0|1}] 不指定或者为 1,将启用日志。如果参数为 0,此选项将禁用日志。定位执行效率比较低的查询可以使用命令 show processlist.

常用的慢查询优化手段有

  • 优化查询语句:检查查询语句,尽可能避免使用 SELECT *,避免使用子查询,使用正确的 JOIN 语法等。在编写复杂查询时,应该尽量避免多层嵌套查询,这会影响查询的性能。
  • 索引优化:通过添加索引来加快查询的速度,尤其是在涉及到大表的查询中。需要注意的是,索引也需要进行适当的优化,避免创建过多的索引,避免冗余索引和重复索引等问题。
  • 优化表结构:在表结构设计时,需要根据实际情况进行优化。例如,避免使用大字段、避免过多的冗余字段等。
  • 水平、垂直分表:如果一张表的记录数达到500w行或者物理内存达到5G就称为大表,水平分表就是将切分多个表存储,降低单表的存储量,查询速度自然就上去了。垂直分表就是把一张表按列分为多张表,多张表通过主键进行关联,从而组成完整的数据。垂直切分为大表和小表,大表存放访问频率低的大的文本字段,小表存放访问频率高的小字段。
  • 限制返回结果的数量:在查询结果较大的情况下,可以限制返回结果的数量,从而减少网络传输的时间和资源消耗。
  • 缓存查询结果:将经常查询的结果缓存起来,可以避免重复查询和计算,提高查询速度。

分表后非 sharding_key 的查询怎么处理,分表后的排序?

  1. 可以做一个 mapping 表,比如这时候商家要查询订单列表怎么办呢?不带 user_id 查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过 user_id 去查询。
  2. 宽表,对数据实时性要求不是很高的场景,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如 ES 提供查询服务。
  3. 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做,或者异步的形式也是可以的。

排序字段是唯一索引:

  • 首先第一页的查询:将各表的结果集进行合并,然后再次排序。
  • 第二页及以后的查询,需要传入上一页排序字段的最后一个值,及排序方式。
  • 根据排序方式,及这个值进行查询。如排序字段 date,上一页最后值为 3,排序方式降序。查询的时候 sql 为 select … from table where date < 3 order by date desc limit 0,10。这样再将几个表的结果合并排序即可。

数据表设计时,字段如何选择?

  • 字段类型优先级
    int > data, time > enum char > varchar > blob, text
    选用字段长度最小,优先使用定长型、数值型字段中避免使用 ”ZEROFILL“.
    time:定长运算快、节省时间,考虑时区,写 sql 不方便。
    enum:能约束值得目的,内部用整型来存储,但与 char 联查时,内部要经历串与值得转化。
    char:定长,考虑字符集和校对集。
    varchar:不定长,要考虑字符集的转换与排序时的校对集,速度慢。
    text,blob:无法使用内存临时表(排序操作只能在磁盘上运行)。
    注意:date,time 的选择可以直接使用时间戳,enum(“男”,”女“),内部转成数字来存储,多了一个转换的过程,可以使用 tinyint 代替最好使用 tinyint.
  • 可以选整型就不选字符串
    整型是定长的,没有国家/地区之分,没有字符集差异。例如,tinyint 和 char(1) 从空间上看都是一个字节,但是 order by 排序 tinyint 快。原因是后者需要考虑字符集与校对集(就是排序优先集)。
  • 够用就行不要慷慨
    大的字段影响内存影响速度。以年龄为例,tinyint unsigned not null 可以存储 255 岁,足够了,用 int 浪费 3 个字节。以 varchar(10),varchar(300) 存储的内容相同,但在表中查询时,varchar(300) 要花更多内存。
  • 尽量避免使用 null
    null 不利于索引,也不利于查询。==null 或者 !=null 都查询不到值,只有使用 is null 或者 is not null 才可以,因此可以在创建字段时候使用 not null default 的形式。
  • 优先选择定长而不是不定长
    char 长度固定,处理速度要比 varchar 快很多,但是相对较费存储空间。所以对存储空间要求不大,但在速度上有要求的可以使用 char 类型,反之可以使用 varchar 类型。

存储时间数据的注意点

1、不要使用字符串类型存储时间

  • 因为字符串类型占用的空间更大
  • 字符串需要对存储的时间进行逐个字符地比对,效率低下,而且还无法用日期相关的 API 进行计算和比较

2、首先 TimeStamp,而不是 DateTime:

  • Timestamp 和时区有关Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间;DateTime 类型是和时区无关的,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。当时区更换之后,就会导致从数据库中读出的时间错误。
  • Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间,Timestamp 表示的时间范围更小。

3、如果需要存储比秒更小的粒度的时间,可以使用 BIGINT 类型存储微妙级别的时间戳,或者用 DOUBLE 存储秒之后的小数部分。

MySQL 中 VARCHAR(M) 最多存储多少数据?

  • 对于 VARCHAR(M) 类型的列最多可以定义 65535 个字节,其中的 M 代表该类型最多存储的字符数量,但在实际存储时并不能放这么多。
  • MySQL 对一条记录占用的最大存储空间是有限制的,除了 BLOB 或者 TEXT 类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节。

如何提高 insert 的性能?

1、合并多条 insert 为一条

  • insert into t values(a,b,c) (d,e,f)
  • 主要原因是多条 insert 合并后日志量(MySQL 的 binlog 和 innodb 的事务日志)减少了,降低日志刷盘的数据量和频率,从而提高效率。通过合并 SQL 语句,同时也能减少 SQL 语句解析的次数,减少网络传输的 I/O.

2、修改参数 bulk_insert_buffer_size,调大批量插入的缓存

3、设置 innodb_flush_log_at_trx_commit = 0

相对于 innodb_flush_log_at_trx_commit = 1 可以十分明显的提升导入速度,innodb_flush_log_at_trx_commit 参数解释如下:
0:log_buffer 中的数据将以每秒一次的频率写入到 log_file 中,且同时会进行文件系统到磁盘的同步操作,但是每个事务的 commit 并不会触发任何 log buffer 到 log file 的刷新或者文件系统到磁盘的刷新操作。
1:在每次事务提交的时候将 log buffer 中的数据都会写入到 log file,同时也会触发文件系统到磁盘的刷新操作
2:事务提交会触发 log buffer 到 log file 的刷新,但是并不会触发磁盘文件系统到磁盘的同步。此外,每秒会有一次文件系统到磁盘同步操作

4、手动使用事务

  • MySQL 默认是 autocommit 的,这样每插入一条数据,都会进行一次 commit
  • 为了减少创建事务的消耗,我们可用手动显式使用事务。即 start transaction; insert ..,insert .. commit;即执行多个 insert 后再一起提交,一般 1000 条 insert 提交一次。

存储引擎

MySQL 中有哪些存储引擎?

✨MySQL 体系结构如下图所示,从体系结构图中可以发现,MySQL 数据库区别于其他数据库的最重要的一个特点是其插件式的表存储引擎。插件式存储引擎的好处是,每个存储引擎都有各自的特点,能够根据具体的应用建立不同存储引擎。
《Java 后端面试经》数据库篇_第2张图片

  • InnoDB 存储引擎 : InnoDB 是 MySQL 的默认事务型存储引擎,也是最重要、使用最广泛的存储引擎,它被设计用来处理大量的短期(short-lived)事务,应该优先考虑 InnoDB 引擎。
  • MyISAM 存储引擎:在 MySQL 5.5.8 之前的版本,MyISAM 是默认的存储引擎。但是MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复。同时 MyISAM 对整张表加锁,很容易因为表锁的问题导致典型的性能问题。
  • Memory 存储引擎:Memory 表至少比 MyISAM 表要快一个数量级,数据文件是存储在内存中。Memory 表的结构在重启后还会保留,但数据会丢失。
  • Archive 存储引擎Archive 存储引擎只支持 INSERT 和 SELECT 操作,会缓存所有的写并利用 zlib 对插入的行进行压缩,所以比 MyISAM 表的磁盘 I/O 更少。但是每次SELECT查询都需要执行全表扫描,所以 Archive 表更适合日志和数据采集类应用
  • CSV 存储引擎:CSV 引擎可以将普通的 CSV 文件(逗号分隔值的文件)作为 MySQL 的表来处理,但这种表不支持索引。因此 CSV 引擎可以作为一种数据交换的机制,非常有用。

简述 MyISAM 和 InnoDB 的区别

  1. 是否支持事务:MyISAM 不支持事务,InnoDB 支持事务,具有提交(commit) 和回滚(rollback) 事务的能力。
  2. 是否支持外键:MyISAM 不支持外键,而 InnoDB 支持。
  3. 是否支持行级锁:MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking) 和表级锁,默认为行级锁。如果使用 MyISAM 存储引擎,一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。
  4. 是否支持 MVVC:MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提供性能。由于 MyISAM 不支持行级锁,因此 MyISAM 支持 MVVC,而 InnoDB 支持 MVVC.
  5. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快。

索引

什么是索引?

索引是一个单独的、存储在磁盘上的数据库结构,包含着对数据表里所有记录的引用指针。使用索引可以快速找出在某个或多个列中有一特定值的行,所有 MySQL 列类型都可以被索引,对相关列使用索引是提高查询操作速度的最佳途径。

索引的基本原理?

索引的基本原理:就是把无序的查询变成有序的查询:

  1. 根据索引列的内容进行排序。
  2. 根据排序结果生成倒排表
  3. 在倒排表内容上拼上数据地址链。
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据。

索引有哪些种类?

从物理存储来说,分为:

  • 聚集索引(主键索引):数据表的主键列使用的就是主键索引:一张数据表有且只能有一个主键,并且主键不能为 NULL,不能重复。
  • 二级索引(辅助索引):二级索引的叶子节点存储的数据是主键,也就是说,通过二级索引,可以定位主键的位置。

从字段特性来说,分为:

  • 主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
  • 普通索引:普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL.
  • 唯一索引:唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
  • 前缀索引 :前缀索引只适用于字符串类型的数据,它是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符。

从字段个数来说,分为:

  • 单列索引
  • 联合索引:即将数据库表中的多个字段联合起来作为一个组合索引。为了进一步提高 MySQL 查询的效率,就要考虑建立联合索引。

如何为表字段添加索引?

✨1、添加 primary key (主键索引)

alter table 'table_name' add primary key('column')

✨2、添加 unique (唯一索引)

alter table 'table name' add unique index_name('column')

✨3、添加 index (普通索引)

alter table 'table name' add index index_name('column')

✨4、添加 fulltext (全文索引)

alter table 'table name' add fulltext index_name('column') 

✨5、添加多列索引

alter table 'table name' add index index_name('column1', 'column2', 'column3')

为什么要用索引?

使用索引的几个通常的原因如下:

  • 通过创建唯一性索引可以保证数据库表每一行数据的
  • 可以大大加快数据的检索速度(大大减少检索的数据量)
  • 帮助服务器避免排序和临时表
  • 将随机 IO 变为顺序 IO

什么时候适合使用索引

  • 有唯一性限制的字段,如学生学号。
  • 经常用于 where 查询条件中的字段:可以提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
  • 经常用于 group byorder by 的字段:建立索引之后在 B+ 树中的记录都是排好序的,不用每次都去排序了。

什么时候不适合使用索引

  • 字段中存在大量重复数据,不需要创建索引:如 sex 字段,只有男女,如果在表中男女的记录分布均匀,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
  • 表数据太少的时候,不需要创建索引
  • 被频繁更新的字段应该慎重建立索引:频繁更新的字段,索引也需要重新建立,索引维护成本高。
  • WHERE 条件,GROUP BYORDER BY 里用不到的字段 :索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。

索引什么时候会失效

  1. WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列:这是因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。
  2. 使用 LIKE 进行模糊查询时,% 在关键词前: 这是因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
  3. 联合索引违背最左匹配原则
  4. 对索引使用函数: 这是因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
  5. 对索引使用表达式: 原因同使用函数失效的情况。
  6. 对索引隐藏式类型转换: MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,这里使用了 CAST 函数。因此,这就和使用函数的情况相同了。

使用索引的注意事项

  • ✨对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引。
  • 避免 where 子句中对字段施加函数,这会造成无法命中索引
  • ✨在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键
  • 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗,MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用。
  • 在使用 limit offset 查询缓慢时,可以借助索引来提高性能

有什么优化索引的方法?

  • 前缀索引优化:使用某个字段中字符串的前几个字符建立索引,减小索引字段大小提高索引的查询速度。
  • 覆盖索引优化:在 WHERE 条件查询中的所有字段,在索引 B+ 树的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚集索引查询获得,可以避免回表的操作。
  • 主键索引最好是自增的:因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
  • 防止索引失效

为什么索引会失效?(从 B+ 树的角度阐述)

  1. 使用联合索引时,不遵循最左前缀原则:B+ 树底层的链表会按照最左列的数据的大小进行排序,当最左列数据的值相同时,下列的数据值才是有序的
  2. 使用模糊查询时:数据在 B+ 树中存储的时候,也是按照字母大小排序,第一个字母相同才会拿第二字母作比较

MySQL 索引为什么要使用 B+ 树?

B+ 树是由 B 树(平衡多叉树)和索引顺序访问方法演化而来的,它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在 B+ 树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点,各叶子节点通过指针进行链接,如下图:

《Java 后端面试经》数据库篇_第3张图片
B+ 树索引在数据库中的一个特点就是高扇出性,例如,在 InnoDB 存储引擎中,每个页的大小为 16 KB,在数据库中,B+ 树的高度一般都在 2~4 层,这意味着查找某一键值最多只需要 2 到 4 次 IO 操作,这还不错。因为现在一般的磁盘每秒至少可以做 100 次 IO 操作,2~4 次的 IO 操作意味着查询时间只需 0.02~0.04 秒。

B 树和 B+ 树的区别?

  • B+ 树非叶节点不存储数据,所有数据存储在叶节点,导致查询时间复杂度固定为 O(logN),而 B 树查询时间复杂度不固定,与 key 在树中的位置有关,最好为 O(1).
  • B+ 树叶节点两两相连可大大增加区间访问性,可使用范围查询,而 B 树每个节点 key 和 data 在一起,则无法进行区间查找。
  • B+ 树更适合外部存储,由于非叶节点不存放数据,因此每一层可以容纳更多的元素,磁盘中每一页可以存放更多的元素,查找时磁盘 IO 次数减少。

既然索引有这么多优点,为什么不对表中的每一个列创建一个索引呢?

  1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就增加了维护的难度
  2. 索引需要占用物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大
  3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加

MySQL 索引的底层数据结构

✨ 1、哈希索引就是采用哈希算法,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据,其时间复杂度接近 O(1).

《Java 后端面试经》数据库篇_第4张图片如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值,前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据。但是哈希索引有一个最大的缺点就是,它不支持顺序和范围查询。

✨2、B+ 树索引:B 树就是多路平衡查找树的意思,B+ 树是 B 树的变体,目前大部分数据库系统及文件系统都采用 B+ 树作为索引结构。

《Java 后端面试经》数据库篇_第5张图片
B+ 树与 B 树的区别:

  • B 树的所有节点既存放键(key) 也存放数据(data),而 B+ 树只有叶子节点存放 key 和 data,其他内节点只存放 key
  • B 树的叶子节点都是独立的,B+ 树的叶子节点有一条引用链指向与它相邻的叶子节点
  • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+ 树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。

Hash 索引 与 B+ 树索引的区别

  • B+ 树索引可以进行范围查询,Hash 索引不能。
  • B+ 树索引支持联合索引的最左侧原则,Hash 索引不支持。
  • B+ 树索引支持 order by 排序,Hash 索引不支持。
  • Hash 索引在等值查询上比 B+ 树效率更高。
  • B+ 树使用 like 进行模糊查询的时候,like 后面(比如%开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。

InnoDB 存储引擎中的 B+ 树

MySQL 中的存储方式根据使用存储引擎的不同而有所区别,InnoDB 存储引擎中的 B+ 树结构如下图所示:
《Java 后端面试经》数据库篇_第6张图片

从上图中可以发现:

  • B+ 树的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。
  • B+ 树叶节点内容是数据页,数据页里存放了用户的记录以及各种信息,每个数据页默认大小是 16 KB.
  • InnoDB 根据索引类型不同,分为聚集索引和非聚集索引(辅助索引)。他们区别在于,聚集索引的叶子节点存放的是实际数据,所有完整的用户记录都存放在聚集索引的叶子节点而非聚集索引的叶子节点存放的是主键值,而不是实际数据。
  • 因为表的数据都是存放在聚集索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚集索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个,而二级索引可以创建多个。

三层 B+ 树能存放多少数据(从内存、操作系统、B+树角度)?

  • 在 MySQL 中 B+ 树的一个节点大小为 “1页”,也就是16KB
  • 对于叶子节点(存索引值+数据),如果一行数据大小为 1KB,那么一页就能存 16 条数据
  • 对于非叶子节点(存索引值+指针),如果索引值为 8 字节,指针为 8 字节,一共是 16 字节,则 16kb 能存放 16*1024/16=1024 个索引指针
  • 对于高度为 2 的 B+ 树,根节点存储索引指针节点,那么它有 1024个 叶子节点存储数据,每个叶子节点可以存储 16 条数据,一共 2^10 * 2^4 = 2^14 条数据
  • 对于高度为 3 的 B+ 树,就可以存放 2^10 * 2^10 * 2^4 = 2^24条数据,也就是对于两千多万条的数据,我们只需要高度为 3 的 B+ 树就可以完成,通过主键查询只需要 3 次 IO 操作就能查到对应数据

聚集索引和非聚集索引

聚集索引叶子节点包含索引结构和数据,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,主键索引属于聚集索引。一句话就是,聚集索引能够在 B+ 树索引的叶子节点上直接找到数据。

聚集索引的优势:

  • 查询通过聚集索引可以直接获取数据,相比非聚集索引需要第二次查询(非聚集索引的情况下)效率要更高。
  • 由于定义了数据的逻辑顺序,聚集索引对于范围值查询和顺序查询的效率很高。

聚簇索引的劣势:

  • 依赖有序的数据:因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
  • 占用空间大:如果主键比较大的话,用辅助索引将会变得更大,因为辅助索引的叶子存储在的是主键值,过长的主键值,会导致非叶子节点占用更多的物理空间。
  • 更新代价高:如果被索引列的数据被修改时,那么对应的索引也将会被修改, 而且何况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, 所以对于主键索引来说,主键一般都是不可被修改的。

非聚集索引叶子节点不存储数据,存储数据行的地址(主键值),也就是说根据索引查找到数据行的位置再去磁盘查找数据,这个就有点类似一本书的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。

非聚集索引的优势

  • 更新代价小:由于非聚集索引的叶子节点不存放数据,因此非聚集索引的更新代价就没有聚集索引那么大。

非聚集索引的劣势

  • 非聚集索引也依赖于有序的数据
  • 可能会二次查询(回表)当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询
    需要注意的是,非聚集索引并不是一定要回表查询。如果查询的字段是主键或者即使不是主键但是建立了索引的话,这种情况是不需要回表的。

聚集索引和非聚集索引的区别:

  • 一个表中只能拥有一个聚集索引,而非聚集索引一个表可以存在多个。
  • 聚集索引,索引中键值的逻辑顺序决定了表中相应行的物理顺序;非聚集索引,索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。
  • 索引是通过二叉树的数据结构来描述的,我们可以这么理解聚集索引:聚集索引的叶子节点就是数据节点,而非聚集索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。

什么是覆盖索引?

✨如果一个索引包含(或者说覆盖)所有需要查询的字段的值,就称为“覆盖索引”。在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值,最终还是要“回表”,也就是通过主键再查找一次,这样就会比较慢,覆盖索引就是要把查询出来的列和索引对应起来,不做回表操作。

覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据, 而无需回表查询。

✨例如,创建一个索引(username, age),在查询数据的时候:select username, age from user where username='Java' and age=22 ,要查询出的列在叶子节点都存在,因此不用回表。

谈谈联合索引和最左前缀原则?

✨最左前缀原则和联合索引的索引存储结构和检索方式是有关系的。

✨索引的底层是一颗 B+ 树,那么联合索引当然还是一颗 B+ 树,只不过联合索引的键值数量不是一个,而是多个。但是构建一颗 B+ 树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+ 树。

✨在联合索引树中,最底层的叶子节点按照第一列 a 列从左到右递增排列,但是 b 列和 c 列是无序的,b 列只有在 a 列值相等的情况下小范围内递增有序,而 c 列只能在 a,b 两列相等的情况下小范围内递增有序。

✨就像上面的查询,B+ 树会先比较 a 列来确定下一步应该搜索的方向,往左还是往右。如果 a 列相同再比较 b 列。但是如果查询条件没有 a 列,B+ 树就不知道第一步应该从哪个节点查起。

例子:假如创建一个(a,b) 的联合索引,那么它的索引树是这样的
《Java 后端面试经》数据库篇_第7张图片
可以看到 a 的值是有顺序的,1,1,2,2,3,3,而 b 的值是没有顺序的 1,2,1,4,1,2。所以 b = 2 这种查询条件没有办法利用索引,因为联合索引首先是按 a 排序的,b 是无序的。
同时我们还可以发现在 a 值相等的情况下,b 值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如 a = 1 and b = 2, a, b 字段都可以使用索引,因为在 a 值确定的情况下 b 是相对有序的,而 a>1 and b=2,a 字段可以匹配上索引,但 b 值不可以,因为 a 的值是一个范围,在这个范围中 b 是无序的。

✨可以说创建的 idx_abc(a,b,c) 索引,相当于创建了 (a)、(a,b)(a,b,c)三个索引。

✨联合索引的最左前缀原则:使用联合索引查询时,MySQL 会一直向右匹配直至遇到范围查询(>、<、between、like) 就停止匹配。

事务

什么是事务

简单的说,事务就是逻辑上的一组操作,要么都执行,要么都不执行。

举一个经典的转账的例子,小明要给小红转账 1000 元,这里面涉及两个关键的操作:

  • 小明账户余额减去 1000 元
  • 小红账户余额加上 1000 元

事务就会把这两个操作看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。

数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体,构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行的原则。

# 开启一个事务
start transaction;
# 多条 SQL 语句
sql1,sql2...
## 提交事务
commit;

《Java 后端面试经》数据库篇_第8张图片

事务有哪些特性

事务是由数据库的引擎实现的,不是所有的引擎都支持事务,MyISAM 引擎就不支持事务,常见的 InnoDB 引擎是支持事务的,事务有以下四个基本特性

  • 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。
  • 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
  • 持久性(Durability):一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 原子性是通过回滚日志(undo log)来保证的。
  • 持久性是通过重做日志(redo log)来保证的。
  • 隔离性是通过MVCC(多版本并发控制)锁机制来保证的。
  • 一致性则是通过持久性+原子性+隔离性来保证。

事务并发会带来哪些问题?

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:

  • 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
  • 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
  • 幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
  • 丢失修改:一个事务先对数据进行修改,之后另一个事务也对该数据修改,那么第一个事务修改的值就被第二个事务修改的值覆盖了,这就意味着发生了「丢失修改」现象。

事务的隔离级别有哪些?

SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  • 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被其他事务看到,可能会发生【脏读】、【不可重复读】、【幻读】 现象
  • 读已提交(read committed):一个事务提交之后,它做的变更才能被其他事务看到,可能会发生【不可重复读】和【幻读】现象
  • 可重复读(repeatable read):一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,它是InnoDB 引擎的默认隔离级别可能会发生【幻读】现象
  • 串行化(serializable):对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
隔离级别 脏读 不可重复度 幻读
read uncommitted
read committed ×
repeatable read × ×
serializable × × ×

从上面可以得出,隔离水平从高到低排序:串行化>可重复读>读已提交>读未提交

需要注意的是,事务的隔离级别越高,那么事务请求的锁就越多,因此效率就越低。 InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

MySQL 默认的事务隔离级别是什么?

InnoDB 存储引擎默认支持的隔离级别是可重复读。可以通过 select @@tx_isolation; 命令来查看,MySQL 8.0 将该命令改为 select @@transaction_isolation;

《Java 后端面试经》数据库篇_第9张图片

InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它通过 next-key lock 锁(行锁和间隙锁的组合)来锁住记录之间的“间隙”和记录本身,防止其他事务在这个记录之间插入新的记录,这样就避免了幻读现象。

什么是 MVCC?

多版本并发控制(Multiple Version Concurrent Control): 读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务会看到自己特定版本的数据,叫做版本链。

Read View 在 MVCC 里是如何工作的?

Read View 有四个重要的字段

《Java 后端面试经》数据库篇_第10张图片

  • m_ids:指的是在创建 Read View 时,当前数据库中 「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务m_ids 不包括当前事务自己和已提交的事务(正在内存中)
  • min_trx_id :指的是在创建 Read View 时,当前数据库中 「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值
  • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1
  • creator_trx_id :指的是创建该 Read View 的事务的事务 id

在插入或更新一条数据之后,InnoDB 存储引擎会为每行数据添加了三个隐藏字段

  • DB_TRX_ID(6 字节):表示最后一次插入或更新该行的事务 id,此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除。
  • DB_ROLL_PTR(7 字节):回滚指针,指向该行的 undolog ,如果该行未被更新,则为空。
  • DB_ROW_ID(6 字节):如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该
    id 来生成聚簇索引。

在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

《Java 后端面试经》数据库篇_第11张图片

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有以下几种情况:

  • 如果记录的 trx_id小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 值在 Read View 的 min_trx_idmax_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
    (1)如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
    (2)如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

可重复读是如何工作的?

参考博客文章小林coding《图解数据库》之可重复读是如何工作的

读提交是如何工作的?

参考博客文章小林coding《图解数据库》之读提交是如何工作的

什么是锁,锁的作用是什么?

锁是数据库系统区别文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问,保持数据的完整性和一致性

MySQL 有哪些锁?

基于锁的粒度,可以将锁分为三大类:全局锁、表级锁和行级锁

全局锁

全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样的情况。

相关语法命令

//对全库加上锁,执行后整个数据库处于只读状态,其他线程的任何读以外的操作都会被阻塞
flush tables with read lock  
unlock tables  //释放全库上的锁

全局锁带来的问题:加上全局锁,意味着整个数据库都是只读状态。如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。

解决方案

  • 如果数据库的存储引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
  • 因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的 Read View,这就是事务四大特性中的隔离性,这样备份期间备份的数据一直是在开启事务时的数据。
  • 备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 –single-transaction 参数的时候,就会在备份数据库之前先开启事务,这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。
  • InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。
  • MyISAM 存储引擎不支持事务,因此只能使用全局锁的方法。

表级锁

表级别的锁包括表锁元数据锁意向锁AUTO-INC 锁

1、表锁:对用户信息表 user_info 加表锁和解锁的命令如下:

//表级别的共享锁,也就是读锁
lock tables t_student read
//表级别的排他锁,也就是写锁
lock tables t_stuent write
unlock tables  //释放表锁

表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。如果本线程对学生表加了「共享表锁」,那么本线程接下来如果要对学生表执行写操作的语句,是会被阻塞的,当然其他线程对学生表进行写操作时也会被阻塞,直到锁被释放。

2、元数据锁:元数据锁不需要显式地进行使用,当对数据库表进行操作时,会自动给表加上元数据锁。

  • 对一张表进行 CRUD 操作时,加的是元数据读锁
  • 对一张表做结构变更操作的时候,加的是元数据写锁

**元数据锁就是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。**元数据锁不需要进行手动释放,它会在事务提交之后自动释放,只要事务未提交,就会一直持有元数据锁。

元数据锁带来的问题

  • 例如有下面这个顺序的场景:
  • 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上 MDL 读锁。
  • 然后,线程 B 也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突。
  • 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞。
  • 那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。
  • 原因在于,元数据锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 元数据写锁等待,会阻塞后续该表的所有 CRUD 操作。

解决方案:为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了元数据读锁,如果可以考虑,先结束掉这个长事务,然后再做表结构的变更。

3、对于意向锁

  • 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」
  • 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」

普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。

如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。

那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。

意向锁的目的是为了快速判断表里是否有记录被加锁。

4、AUTO-INC 锁:在为某个字段声明 AUTO_INCREMENT 属性时,之后可以在插入数据时,可以不指定该字段的值,数据库会自动给该字段赋值递增的值,这主要是通过 AUTO-INC 锁实现的。

  • AUTO-INC 锁是特殊的表锁机制,锁不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放
  • 在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉
  • 一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的
  • AUTO-INC 锁在对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞

行级锁

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

行级锁的类型主要有三类

  • 记录锁(Record Lock):仅仅把一条记录锁上。
  • 间隙锁(Gap Lock):锁定一个范围,但是不包含记录本身。
  • Next-Key Lock:记录锁和间隙锁的组合,锁定一个范围,并且锁定记录本身。

如何避免数据库发生死锁?

死锁的四个必要条件:互斥、占有且等待、不可抢占、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

  • 设置事务等待锁的超时时间:当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 存储引擎中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
  • 开启主动死锁检测:主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

日志

如何查看数据库日志?日志类型有哪些?

以 MySQL 数据库为例,查看日志需要如下操作。

首先,查看是否开启了查询日志功能

//查看是否启用了日志
show variables like 'general_log%';

如果得到的 value 为 “On”,则表示查询日志开启,如果 value 为 “False” 则表示查询日志关闭。则需要开启 MySQL 查询日志,使用如下命令

set global general_log = on;

其次,设置日志输出方式为表(如果设置 log_output=table 的话,则日志结果会记录到名为gengera_log 的表中,这表的默认引擎是 CSV)

set global log_output='table';

最后,查询日志信息

select * from mysql.general_log;

常见的日志类型有

  • 重做日志(redo log)
  • 回滚日志(undo log)
  • 慢查询日志(slow query log)
  • 错误日志(error log)
  • 普通查询日志(general query log)
  • 二进制日志(binary log)

Redis

Redis 有哪些数据类型及底层实现?

1、Redis 支持 5 种核心的数据类型,分别是:

  • String:底层数据结构实现主要是简单动态字符串(SDS),可以保存文本数据和二进制数据,SDS 获取字符串长度的时间复杂度是 O(1).
  • List:底层数据结构是由双向链表或压缩列表实现的,如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节,Redis 会使用压缩列表作为 List 类型的底层数据结构;如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构。
  • Hash:底层数据结构是由压缩列表或哈希表实现的,如果哈希类型元素个数小于 512 个,所有值小于 64 字节的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
  • Set:底层数据结构是由哈希表或整数集合实现,如果集合中的元素都是整数且元素个数小于 512 个,Redis 会使用整数集合作为 Set 类型的底层数据结构;如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
  • ZSet:底层数据结构是由压缩列表或跳表实现,如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构。

2、Redis 还提供了 Bitmap、HyperLogLog、Geo 类型,但这些类型都是基于上述核心数据类型实现的。

Redis 常见数据结构有哪些应用场景?

  • String:缓存对象、常规计数、分布式锁、共享 Session 信息等。
  • List:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  • Hash:缓存对象、购物车等。
  • Set:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • ZSet:排序场景,比如排行榜、电话和姓名排序等。

Redis 单线程

Redis 单线程为什么这么快?

  1. 单线程避免了线程切换和锁竞争所产生的时间消耗
  2. Redis 的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因
  3. Redis 采用了 IO 多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率

Redis 6.0 之后为什么引入多线程?

  • 在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
  • 为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理

Redis 缓存相关

缓存相关的基本概念

缓存雪崩:当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

对于缓存雪崩问题,我们可以采用两种方案解决

  • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
  • 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

缓存击穿:当缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题,缓存击穿可以看成是缓存雪崩的子集

对于缓存击穿问题,我们可以采用两种方案解决

  • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。

缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

对于缓存穿透问题,我们可以采用三种方案解决

  • 非法请求的限制:在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 设置空值或者默认值:针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

常见的缓存更新策略有哪些?

常见的缓存更新策略共有 3 种:

  • 旁路缓存(Cache Aside)策略:在更新数据时,不更新缓存而是删除缓存中的数据,然后等到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
  • Read/Write Through(读穿 / 写穿)策略
  • Write Back(写回)策略

实际开发中,Redis 和 MySQL 的更新策略用的是 旁路缓存(Cache Aside).

  • 旁路缓存(Cache Aside)策略,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
  • 先进行写策略,它的步骤为先更新数据库中的数据,再删除缓存中的数据。不可以先删除缓存中的数据,再更新数据库中的数据,会导致缓存和数据库中的数据不一致的情况
  • 后进行读策略的步骤:(1) 如果读取的数据命中了缓存,则直接返回数据。(2) 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
  • Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。

谈谈 Redis 的持久化策略?

Redis 支持 RDB 持久化、AOF 持久化、RDB-AOF 混合持久化这三种持久化方式。

RDB(Redis Database) 是 Redis 默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中。RDB 会创建一个经过压缩的二进制文件,文件以 “.rdb” 结尾,内部存储了各个数据库的键值对数据等信息,RDB持久化的触发方式有两种:

  • 手动触发:通过 SAVEBGSAVE 命令触发 RDB 持久化操作,创建 “.rdb” 文件
  • 自动触发:通过配置选项,让服务器在满足指定条件时自动执行 BGSAVE 命令

其中,SAVE 命令执行期间,Redis 服务器将阻塞,直到“.rdb”文件创建完毕为止。而 BGSAVE 命令是异步版本的 SAVE 命令,它会使用 Redis 服务器进程的子进程,创建“ .rdb” 文件。BGSAVE 命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。总之,BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及 RDB 的操作都采用 BGSAVE 的方式,而 SAVE 命令已经废弃。

BGSAVE 命令的执行流程,如下图:

《Java 后端面试经》数据库篇_第12张图片
BGSAVE 命令的原理,如下图:

《Java 后端面试经》数据库篇_第13张图片

RDB 持久化的优缺点如下:

  • 优点:RDB 生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快
  • 缺点:BGSAVE 每次运行都要执行 fork 操作创建子进程,属于重量级操作,不宜频繁执行,没有办法做到实时持久化

AOF(Append Only File),解决了数据持久化的实时性,是目前 Redis 持久化的主流方式。AOF以独立日志的方式,记录了每次写入命令,重启时再重新执行 AOF 文件中的命令来恢复数据。AOF的工作流程包括:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load),如下图:

《Java 后端面试经》数据库篇_第14张图片
AOF 持久化的文件同步机制,为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作:

  • 当程序调用 write 对文件写入时,系统不会直接把数据写入硬盘,而是先将数据写入内存的缓冲区中
  • 当达到特定的时间周期或缓冲区写满时,系统才会执行 flush 操作,将缓冲区中的数据冲洗至硬盘中

上诉这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性。

  1. 对于 AOF 这样的持久化功能来说,冲洗机制将直接影响 AOF 持久化的安全性。
  2. 为了消除上述机制的不确定性,Redis 向用户提供了 appendfsync 选项,来控制系统冲洗 AOF 的频率。
  3. Linux 的 glibc 提供了 fsync 函数,可以将指定文件强制从缓冲区刷到硬盘,上述选项正是基于此函数。

appendfsync 选项的取值和含义如下:

取值 说明
always 每执行一个写入命令,就对 AOF 文件执行一次冲洗操作。这种情况下,服务器在停机时最多丢失一个命令,但这种方式会大大降低 Redis 服务器的性能
everysec 每隔1秒就对 AOF 文件执行一次冲洗操作。在这种情况下,服务器在停机时最多丢失1秒内的命令,是一种兼顾性能和安全性的折中方案
no 不主动对 AOF 执行冲洗操作,由操作系统决定何时冲洗。在这种情况下,服务器在停机时将丢失最后一次冲洗后执行的写入命令,数据量取决于系统的冲洗频率

AOF 持久化的优缺点如下:

  • 优点:与 RDB 持久化可能丢失大量的数据相比,AOF 持久化的安全性要高很多。通过使用 everysec 选项,用户可以将数据丢失的时间窗口限制在 1 秒之内。
  • 缺点:AOF 文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF 需要通过执行 AOF 文件中的命令来恢复数据库,其恢复速度比 RDB 慢很多。AOF 在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。

RDB-AOF 混合持久化:Redis从4.0 开始引入 RDB-AOF 混合持久化模式,这种模式是基于AOF 持久化构建而来的。用户可以通过配置文件中的 “aof-use-rdb-preamble yes” 配置项开启AOF 混合持久化。Redis 服务器在执行 AOF 重写操作时,会按照如下原则处理数据:

  • 像执行 BGSAVE 命令一样,根据数据库当前的状态生成相应的 RDB 数据,并将其写入 AOF文件中
  • 对于重写之后执行的 Redis 命令,则以协议文本的方式追加到 AOF 文件的末尾,即 RDB 数据之后

通过使用 RDB-AOF 混合持久化,用户可以同时获得 RDB 持久化和 AOF 持久化的优点,服务器既可以通过 AOF 文件包含的 RDB 数据来实现快速的数据恢复操作,又可以通过 AOF 文件包含的 AOF 数据来将丢失数据的时间窗口限制在 1s 之内。

Redis 持久化时,对过期键会如何处理的?

Redis 持久化可以采取 RDB 和 AOF 两种方式,主要看在这两种持久化方式下过期键的状态。

RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段:

  • RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
  • RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
    (1) 如果 Redis 是「主服务器」运行模式的话在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中,所以过期键不会对载入 RDB 文件的主服务器造成影响。
    (2) 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段:

  • AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值
  • AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响

Redis 的过期删除策略?

什么是过期删除策略?

Redis 可以对 key 设置过期时间,当 key 过期之后需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。

每当对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。当查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:

  • 如果不存在,则正常读取键值
  • 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比较,大于当前系统时间表示该 key 未过期,小于则表示过期

Redis 使用了什么过期删除策略?

Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。

惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

《Java 后端面试经》数据库篇_第15张图片

惰性删除策略的优缺点:

  • 每次访问时才会检查 key 是否过期,所以此策略只会使用很少的系统资源,对 CPU 时间最友好。
  • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费,对内存不友好

定期删除每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key。

Redis 的定期删除的流程:

  1. 从过期字典中随机抽取 20 个 key
  2. 检查这 20 个 key 是否过期,并删除已过期的 key
  3. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1
  4. 如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查

值得注意的是,定期删除是一个循环的过程,为了避免定期删除过程循环过度,导致线程卡死发生,设置了循环时间的时间上限,默认为 25 ms.

《Java 后端面试经》数据库篇_第16张图片
定期删除策略的优缺点:

  • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用
  • 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。

Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

Redis 的内存淘汰策略?

在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory.

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略:

(1) 不进行数据淘汰的策略

  • noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。

(2) 进行数据淘汰的策略,针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。

  1. 在设置了过期时间的数据中进行淘汰
    volatile-random:随机淘汰设置了过期时间的任意键值。
    volatile-ttl:优先淘汰更早过期的键值。
    volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值。
    volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值。
  2. 在所有数据范围内进行淘汰
    allkeys-random:随机淘汰任意键值;
    allkeys-lru:淘汰整个键值中最久未使用的键值;
    allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

Redis 是如何实现 LRU 和 LFU 算法的?

LRU(Least Recently Used) 最近最少使用,会选择淘汰最近最少使用的数据。

Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

Redis 实现的 LRU 算法的优缺点:

  • 不用为所有的数据维护一个大链表,节省了空间占用
  • 不用在每次数据访问时都移动链表项,提升了缓存的性能
  • 无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染

LFU(Least Frequently Used) 最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

Redis 实现 LFU 算法相比于 LRU 算法的实现,多记录了**「数据的访问频次」**的信息,Redis 对象的结构如下:

typedef struct redisObject {
    ...
    // 24 bits,用于记录对象的访问信息
    unsigned lru:24;  
    ...
} robj;

值得注意的是,Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同:

  • 在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis 可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key.
  • 在 LFU 算法中,Redis 对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time) 用来记录 key 的访问时间戳,低 8bit 存储 logc(Logistic Counter) 用来记录 key 的访问频次。

Redis的 zset,为什么要用跳表,不用红黑树?

MongoDB

MongoDB 是什么?

MongoDB 是由 C++ 语言编写的,是一个基于分布式文件存储的开源数据库系统。 在高负载的情况下,添加更多的节点,可以保证服务器性能。 MongoDB 旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

为什么要用 MongoDB?

随着当前数据量的急剧增长,传统的关系型数据库(Oracle、MySQL)在数据操作的“三高”需求以及在应对 Web2.0 的网站需求方面,已经显得力不从心。

这里的 “三高”需求是指:

  • 高性能:对数据库在高并发读写场景下保持高性能的需求。
  • 高存储:对海量数据的高效率存储和访问的需求。
  • 高扩展性&高可用性:对数据库高扩展性和高可用性的需求。

作为分布式文档型的数据库 MongoDB 可以应对以上的“三高”需求。

MongoDB 的常见应用场景?

MongoDB 作为分布式文档型数据库的代表,它的具体的应用场景如下:

  • 社交场景:使用 MongoDB 存储用户信息以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
  • 游戏场景:使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
  • 物流场景:使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
  • 物联网场景:使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行分析。
  • 视频直播场景:使用 MongoDB 存储用户信息、点赞和互动信息。

以上的具体应用场景中,数据操作方面的共同特点是:

  • 数据量大
  • 写入操作频繁
  • 存储的都是价值较低的数据,对事务性要求不高

MongoDB 的特点有哪些?

  • 高性能:MongoDB 提供高性能的数据持久性,特别是对嵌入式数据模型的支持减少了数据库系统上的 I/O 活动。索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键(文本索引解决搜索的需求、TTL 索引解决历史数据自动过期的需求、地理位置索引可用于构建各种 O2O 应用)
  • 高可用性:MongoDB 的复制工具称为副本集(replica set),它可提供自动故障转移和数据冗余。
  • 高扩展性:MongoDB 提供了水平可扩展性作为其核心功能的一部分,分片将数据分布在一组集群的机器上,海量数据存储,服务能力水平扩展。
  • 丰富的查询支持:MongoDB 支持丰富的查询语言,支持读和写操作(CRUD),例如数据聚合、文本搜索和地理空间查询等。
  • 无模式(动态模式)、灵活的文档模型。

你可能感兴趣的:(Java,#,Java,后端面试经,数据库,java,后端,数据库)