面试题-MySQL和Redis(更新版)

前言

MySQL和Redis部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮助。

系列文章:

面试题-Java基础

面试题-Java集合

面试题-Java多线程基础、实现工具和可见性保证

面试题-线程池和原子变量

面试题-Java虚拟机

面试题-计算机网络1

面试题-计算机网络-HTTP部分


MySQL

  1. 解释⼀下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接
    池?

    池化思想其实在 面试题-线程池和原子变量 中 为什么要使用线程池中解释过,具体优点如下:

    • 提高了连接的可管理性
    • 降低频繁创建和销毁资源的开销
    • 提前创建好资源,提高了任务的响应速度
  2. 范式你了解吗?简单说说

    • 第一范式:每个属性都不可再分,如果仅仅支持第一范式,那么会出现一些问题

      • 数据冗余

      • 插入/删除/修改 异常

        能具体说说这几个异常是什么意思吗?

        如果有一张学生成绩表,包含了学生的所有成绩,但是其中有所在系的字段,那么针对同一个学生的多门课程成绩,系的字段就会冗余,这个就是数据冗余异常;因为系相关的信息没有自己的表,所以如果这个系没有学生,就无法插入系的信息,这个是插入异常;删除异常同理,如果删除学生信息,那么系的信息也会随之删除;修改异常也同理,当修改学生的系信息时,就会出现修改多次的现象。

    • 第二范式:消除了 非主属性 对于 码的 部分函数依赖,简单说就是非主键的所有列,必须全部依赖于完整的主键,而不能只依赖于主键的一部分或者不依赖。

      举个例子:如果有一个订单明细表,订单明细表中包含,orderId,productId,折扣,数量,商品单价和商品名称,其中主键是OrderID+ProductID,折扣和数量是完全依赖主键的,但是商品单价和商品名称是只依赖于商品id,这个设计就不符合第二范式的需求。需要拆分为一个商品表保存商品单价和商品名称的字段。

    • 第三范式:消除了非主属性 对于 码的 传递函数依赖,简单说就是非主键的所有列,必须直接依赖于主键,不能依赖于非主键列,然后传递依赖主键列。

      举例来说:有一个课程表,课程id,教师名,教师地址,主键是课程id,一个课程id都可以完全确定一个教师名和教师地址,符合第二范式,但是教师地址依赖于非主键列教师名,存在传递依赖,所以不符合第三范式。

  3. redo log的底层实现你知道吗?简单说说

    redolog实现是一个环,大小是固定的。redolog的文件个数和大小也是可以配置的。redolog中有两个指针,如果write指针追上了checkpoint指针,就需要等待刷新脏页,然后才能写入。

    • write:代表了写入redolog的位置
    • checkpoint:代表了刷新到磁盘的位置
  4. redo log和bin log的区别?

    • 是否共有:redo log是innodb特有的;binlog是server层的,属于共有的
    • 是否追加写:redo log是一个环,是循环写;binlog是追加的覆盖写
    • 作用不同:redo log用于数据库异常宕机的恢复工作;binlog用于备份
  5. 简单说说事务的ACID特性

    • A:原子性,关注单个事务中的所有操作要么都成功要么都失败
    • C:一致性,关注事务开始和结束之后数据库的完整性约束没有改变
    • I:隔离性,关注多个事务的之间是否互相影响
    • D: 持久性 ,关注事务结束后,对数据库的修改是否可以永久的保存到数据库中
  6. 如果没有隔离性,会发生什么问题?

    如果没有隔离性,会出现下面三个问题

    • 脏读:事务读取到其他事务未提交的数据
    • 不可重复读:事务中对同一数据的查询结果不同
    • 幻读:事务中同一个sql查询到的数据行数不同
  7. 事务的隔离级别有哪些?分别可以解决什么问题?

    • 读不可提交:有脏读/不可重复读和幻读的问题
    • 读可提交:解决了脏读的问题
    • 可重复读:解决了脏读和不可重复读的问题
    • 串行:解决了三个问题,但性能最差
  8. 隔离性是如何实现的?你知道MVCC吗?

    隔离性的实现是数据库创建了一个视图,访问的时候以这个视图的逻辑结果为准

    • 读不可提交,不创建视图
    • 读可提交:每一条SQL都创建一个视图
    • 可重复读:在事务开始的时候创建一个视图,事务中的所有sql都以这个视图为准
    • 串行:用锁实现的

    MVCC是多版本并发访问,通过回滚日志实现的,每一条更新操作都对应一条回滚日志,当前的值,都可以通过回滚日志找到历史更新过的值,多事务之间可以访问到不同的数据就是使用MVCC实现的。

  9. 你知道全局锁吗?

    全局锁会锁住整个数据库实例,只能读,任何关于更新的操作都会阻塞。需要了解全局锁对业务的影响:

    • 如果锁主库,那么业务基本停摆
    • 如果锁从库,那么锁定期间,从库无法同步主库的更新,可能会导致主从不一致
  10. 简单说说表锁?

表锁是锁住整张表,以一个sql为例,lock tables t1 read,t2 write;这句sql执行完毕后:

  • t1表只能读
  • t2表执行者可以读写,其他人不可读写
  1. 你知道元数据锁吗?

    元数据锁主要控制并发的DDL和DML操作,DML操作获取的是元数据读锁;DDL操作获取的是写锁,读写互斥,读读不互斥。

  2. 你知道共享锁排他锁和意向锁吗?

    InnoDB存储引擎中支持行锁,行锁分为三种锁

    • 共享锁:可以理解为读锁
    • 排他锁:可以理解为写锁
    • 意向锁:意向锁的出现主要是为了提高加表锁的判断效率,如果没有意向锁,线程需要加表锁时需要对每一行进行判断,是否有行级锁的存在,加了意向锁相当于多一个变量保存状态,这样可以O(1)的效率判断是否可以加表锁。
  3. 行锁的三种锁定范围(一致性锁定读时使用)

    • 只锁定单行:查询条件中有索引,并且是唯一索引时,可以只锁定单行
    • 锁范围,不包括记录本身
    • next-key lock:单行和范围锁的合体:查询条件中有索引,并且不是唯一索引时使用
  4. 什么是索引?索引有什么作用?

    索引是一种数据结构,可以方便我们快速的查找数据,提高查找效率。

  5. Hash索引和B+树索引的区别?

    • Hash索引适合等值查询,不适合范围查询
    • Hash索引没办法利用索引完成排序
    • 如果有大量重复键值对,Hash索引的效率会变低,因为需要解决hash碰撞的问题,解决hash碰撞一般采用拉链法
    • B+树索引是一种查询树,天然有序,所以可以利用它做范围查询和排序,并且因为B+树的层数少,IO次数也少
  6. 聚集索引和非聚集索引的区别?

    • 聚集索引叶子节点存储的是整行数据;非聚集索引存储的是主键的值
    • 如果使用非聚集索引时,待查询的列索引无法覆盖,那么就需要回表查询
  7. 联合索引和最左匹配原则是什么意思?

    • Mysql中可以针对多列创建联合索引
    • 最左匹配原则指的是,mysql使用索引时,会从最左边的一列开始匹配,建立一个(k1,k2,k3)的联合索引,相当于建立了(k1),(k1,k2)和(k1,k2,k3)三个索引
  8. 分库分表之后,id 主键如何处理?

    分库分表之后,主键id就需要一个全局的id,一般来说生成主键id有以下几种方案:

    • UUID:太长,无序不可读,不适合作为主键
    • 利用redis生成id:性能较好,比较灵活,但是需要引入新的组件,增加了系统的复杂度
    • 美团的Leaf:分布式唯一id生成器,也需要依赖关系型数据库和zookeeper等。
  9. ⼀条SQL语句在MySQL中如何执⾏的?

    查询语句的执行流程如下:

    1. 连接器中校验是否有权限,如果没有权限,直接返回错误;mysql8.0之前的版本,会去查询缓存中检查是否有缓存,如果有直接返回
    2. 分析器进行词法分析和语法分析,词法分析主要关注关键字和相关的字段;语法分析主要检查sql语句是否有语法错误
    3. 优化器根据自己的算法确定执行计划,包括索引的选择等等。
    4. 执行器调用存储引擎接口,获取数据返回

    更新语句的执行流程如下:

    1. 相关的校验等操作和查询操作是类似的,后面的更新操作不太一样
    2. 执行器调用存储引擎的更新接口后,存储引擎会更新数据,然后记录redo log,redo log此时为preopare状态
    3. 执行器写入binlog日志
    4. 执行器调用存储引擎的提交接口,存储引擎把redo log的状态更新为commit,此为两阶段提交,可以保证 异常宕机的重启恢复和二进制日志的备份的数据是一致的。
  10. ⼀条SQL语句执⾏得很慢的原因有哪些?

    首先刨析这个问题,有两种情况:

    • SQL语句平时执行的很快,只是偶尔变慢
    • 在数据量一定的情况下,SQL语句的每次查询都很慢

    针对第一种情况,SQL本身写的应该没什么问题,偶尔变慢的情况应该出在MySQL本身,有以下几种情况可能会导致查询变慢:

    • 数据库在刷新脏页:redolog满了;内存不足需要淘汰一部分脏页
    • 获取不到锁,阻塞了:如果某张表或者行已经被别的线程加锁,暂时获取不到锁

    针对第二种情况,可能有以下的原因:

    • 字段没正确建立索引;字段中包含表达式或者函数操作,导致有索引但是没有用上
    • 数据库因为算法综合判断,可能不会选择索引,而选择全表扫描,算法中的影响因素主要有下面几个:
      • 扫描行数
      • 是否排序
      • 是否回表
      • 是否需要临时表
  11. MyISAM引擎和innoDB引擎的区别?

    • 是否支持事务
    • 是否支持外键
    • 表锁和行锁
    • 是否支持MVCC
  12. MySQL中如果有两个线程想要操作同一行数据,如何避免死锁?(重要)

    核心有两点,知道mysql的锁定机制和防止死锁的理论知识,然后结合理论知识判断如何预防死锁即可:

    防止死锁只要破坏死锁的四个条件之一就可以:

    • 互斥:互斥本身无法破坏
    • 请求并保持:一次性申请全部的锁即可,表现在sql语句中就是where条件中直接包括多个资源,直接锁定多个资源的数据。
    • 不可剥夺:如果是应用层面,可以通过设置事务超时时间来实现事务超时释放锁并回滚事务,使用Transactional注解中的timeout即可实现。
    • 循环等待条件:每个线程都按照同样的顺序申请资源,比如业务中会更新多条数据,更新操作都按照主键序号从小到大更新即可。
  13. 幻读在InnoDB中是如何被解决的?

    innodb中的间隙锁就是为了解决幻读问题出现的。因为锁定了间隙,所以无法进行插入操作。这样自然就解决了可重复读隔离级别下的幻读问题。

  14. InnoDB的MVCC如何解决脏读和不可重复读的问题?

    • 一致性读只能读取事务开始时的快照版本(不加锁的select 解决了不可重复读的问题)
    • 其他事务的读只能读到事务开始时,最新的已提交的版本快照(解决了脏读的问题)
    • 当前读(for update和share mode)读的是当前行的最新版本并且会加行锁
    • 一个事务内是可以读取到其所做的未提交的版本快照

Redis

  1. 为什么要用redis,不用map或者guava?

    缓存分为本地缓存和分布式缓存,map和guava属于本地缓存,特点是:

    • 轻量快速
    • 生命周期随着JVM销毁而结束
    • 不同实例中都需要各自持有一份本地缓存,缓存不具有一致性

    分布式缓存,可以保证缓存的一致性,但是会增加系统架构的复杂度。

  2. 简单说说Redis的RDB持久化?

    RDB持久化是指把当前进程数据生成快照保存到硬盘的过程。RDB的触发可以分为手动触发和自动触发两种。

    • 手动触发:可以用save或者bgsave命令,区别在于bgsave不会阻塞当前redis实例,bgsave会fork一个子进程完成持久化工作,阻塞只发生在fork阶段
    • 自动触发:可以在配置文件中使用save配置,配置m秒内存在n次修改,自动触发持久化过程。
  3. 说说RDB bgsave持久化的详细过程

    • 父进程检查当前是否有在运行的子进程,如果有正在运行子进程,直接返回
    • 父进程fork一个子进程出来,fork阶段会阻塞,fork完毕后不再阻塞父进程,可以响应其他指令
    • 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子性替换
    • 子进程发送信号给父进程通知完毕,父进程更新相关的统计信息
  4. RDB持久化的优缺点?

    • 优点:
      • RDB方式产生的文件是一个压缩的二进制文件,非常适合全量备份和复制场景
      • Redis加载RDB恢复数据远远快于AOF方式
    • 缺点:
      • RDB无法实时持久化/秒级持久化
      • 老版本无法兼容新的RDB文件的格式问题
  5. 简单说说Redis的AOF持久化?

    AOF持久化,是把每次写命令都记录到独立的日志中,重启恢复后再执行AOF命令重新恢复数据。AOF持久化主要是解决了实时性的问题。

  6. 说说AOF持久化的详细过程

    • 所有命令写入追加到aof_buf缓冲区中
    • 文件同步,文件同步可以在配置文件中配置三种同步机制,考虑到性能和数据安全性,一般推荐everysec
      • always:写入到aof_buf以后,直接调用fsync系统调用同步到AOF文件
      • everysec:写入到aof_buf后,先调用write系统调用,写入系统缓冲区后就会返回,然后由专门线程,每秒调用一次fsync操作
      • no:写入到aof_buf后,调用write系统调用写入缓冲区后返回,等待操作系统执行同步磁盘的工作。
  7. 讲解下Redis线程模型?

    Redis线程模型中包含四个组成部分:

    • 套接字
    • IO多路复用程序
    • 事件分发器
    • 事件处理器

    IO多路复用程序用select或者其他系统调用管理多个套接字的网络事件,然后放入到一个统一的队列中,事件分发器从队列中取事件,然后分发到不同的处理器中进行处理。

  8. 谈谈 redis 和 memcached 的区别?

    • 数据结构的丰富度:redis支持更丰富的数据结构。
    • 持久化功能:redis支持RDB或者AOF方式的持久化;memcached不支持
    • 集群功能:redis支持分布式;memcached不支持,需要客户端上使用分布式一致性hash算法来决定目标节点
    • 线程模型不同:redis是单线程的,由一个线程负责处理所有的网络事件和业务处理;memcached是一主多从的reactor线程模型。
  9. 说说Redis中的数据类型

    • String类型:一般用于复杂计数功能的缓存
    • Hash类型:hash是以一个String类型为键,多个field和value的映射表作为值的数据结构,可以用于存储包括多列的行信息
    • list类型:list简单理解就是链表,可以用来实现关注列表,消息列表等等
    • Set类型:去重的数据结构,可以方便的实现交集并集等操作
    • Sorted Set类型:增加了一个score权重,可以把Set中的数据按照score排序
  10. redis的过期策略以及内存淘汰机制

    redis中可以设置过期时间,当到了过期时间后,redis中会采取下面两种策略删除过期数据:

    • 定期删除:每隔100ms随机抽取一些过期的key进行删除
    • 惰性删除:当客户端访问到这个key了,才会实际删除

    上面两种机制都无法保证完全清除过期的key,redis中引入了内存淘汰机制来保证内存不会被过期数据占满。默认有六种淘汰机制:

    • 从已过期的数据中删除的:lru,random和ttl
    • 从所有key中删除:lru,random
    • 不删除:no-eviction
  11. 简单谈谈redis的事务

    redis支持简单的事务,使用multi和exec来开始和执行事务,类似关系型数据库中的begin和commit。redis中不支持回滚事务,当出错时,根据不同的错误类型,处理机制也不同。

    • 当有语法错误时,会导致整个事务中的命令都不会执行

    • 当有运行时错误时,不会影响事务中的其他命令的执行

      另外,redis中提供了watch命令,watch一个key之后,事务中如果有其他客户端修改了该key,整个事务不执行。

  12. 你知道缓存雪崩吗?简单说说

    缓存雪崩指的是,当redis集群不可用或者同时有大量缓存失效时,会造成大量的请求都走数据库,把数据库服务击垮。

    • 针对redis集群不可用,应该事前做好redis集群的高可用性检查,选择合适的内存淘汰策略。
    • 针对大量缓存同时失效,可以采取设置随机过期时间的方式,来避免缓存同时失效
    • 提前设计好 本地缓存和限流降级方案,本地缓存+redis中如果都没有,再去查数据库,查数据库的请求要通过限流降级组件来保护数据库。
  13. 你知道缓存穿透吗?

    缓存穿透指的是,大量请求的key本身不在缓存中,导致直接走了数据库查询。举例来说,黑客攻击时,会伪造很多不存在的key来导致大量请求落到数据库中。解决方法有两种:

    • 最基本的是做好参数校验,不合规的参数直接返回
    • 使用布隆过滤器,其实本质就是一个hash函数加位数组,报存在,有一定概率的误判(hash冲突),但是不存在一定是准确的,比如两个字符串的hash值相同得到位数组的位置就相同。提前把有效的key存储在布隆过滤器中,这样当无效key传递过来时,直接返回。
  14. 如何用redis实现分布式锁?你还知道其他更好的方式吗?

    实际业务中没有使用过,这块等待有真实业务场景应用后再来补充。

  15. 如何保证缓存与数据库双写时的数据⼀致性?

    • 简单的解决方式:如果有更新需求,先删除缓存,再更新数据库,如果数据库更新失败,缓存也为空,不会出现不一致。
    • 简单解决方式的不足:删除缓存完毕后,更新数据库之前,又有一个线程要访问该数据,结果发现缓存中没有数据,从数据库中查询出旧数据,然后更新到缓存中,这个时候更新数据库,那么缓存和数据库又发生了不一致。
    • 解决方案:使用一个队列把读请求和写请求串在一起,写请求执行完毕后才会执行读请求和更新,但是要注意测试读请求的阻塞时长 如何保证缓存与数据库的双写一致性?
posted @ 2020-10-14 16:09  Ging  阅读(952)  评论(0编辑  收藏  举报