前言
MySQL和Redis部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮助。
系列文章:
MySQL
-
解释⼀下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接
池?池化思想其实在 面试题-线程池和原子变量 中 为什么要使用线程池中解释过,具体优点如下:
- 提高了连接的可管理性
- 降低频繁创建和销毁资源的开销
- 提前创建好资源,提高了任务的响应速度
-
范式你了解吗?简单说说
-
第一范式:每个属性都不可再分,如果仅仅支持第一范式,那么会出现一些问题
-
数据冗余
-
插入/删除/修改 异常
能具体说说这几个异常是什么意思吗?
如果有一张学生成绩表,包含了学生的所有成绩,但是其中有所在系的字段,那么针对同一个学生的多门课程成绩,系的字段就会冗余,这个就是数据冗余异常;因为系相关的信息没有自己的表,所以如果这个系没有学生,就无法插入系的信息,这个是插入异常;删除异常同理,如果删除学生信息,那么系的信息也会随之删除;修改异常也同理,当修改学生的系信息时,就会出现修改多次的现象。
-
-
第二范式:消除了 非主属性 对于 码的 部分函数依赖,简单说就是非主键的所有列,必须全部依赖于完整的主键,而不能只依赖于主键的一部分或者不依赖。
举个例子:如果有一个订单明细表,订单明细表中包含,orderId,productId,折扣,数量,商品单价和商品名称,其中主键是OrderID+ProductID,折扣和数量是完全依赖主键的,但是商品单价和商品名称是只依赖于商品id,这个设计就不符合第二范式的需求。需要拆分为一个商品表保存商品单价和商品名称的字段。
-
第三范式:消除了非主属性 对于 码的 传递函数依赖,简单说就是非主键的所有列,必须直接依赖于主键,不能依赖于非主键列,然后传递依赖主键列。
举例来说:有一个课程表,课程id,教师名,教师地址,主键是课程id,一个课程id都可以完全确定一个教师名和教师地址,符合第二范式,但是教师地址依赖于非主键列教师名,存在传递依赖,所以不符合第三范式。
-
-
redo log的底层实现你知道吗?简单说说
redolog实现是一个环,大小是固定的。redolog的文件个数和大小也是可以配置的。redolog中有两个指针,如果write指针追上了checkpoint指针,就需要等待刷新脏页,然后才能写入。
- write:代表了写入redolog的位置
- checkpoint:代表了刷新到磁盘的位置
-
redo log和bin log的区别?
- 是否共有:redo log是innodb特有的;binlog是server层的,属于共有的
- 是否追加写:redo log是一个环,是循环写;binlog是追加的覆盖写
- 作用不同:redo log用于数据库异常宕机的恢复工作;binlog用于备份
-
简单说说事务的ACID特性
- A:原子性,关注单个事务中的所有操作要么都成功要么都失败
- C:一致性,关注事务开始和结束之后数据库的完整性约束没有改变
- I:隔离性,关注多个事务的之间是否互相影响
- D: 持久性 ,关注事务结束后,对数据库的修改是否可以永久的保存到数据库中
-
如果没有隔离性,会发生什么问题?
如果没有隔离性,会出现下面三个问题
- 脏读:事务读取到其他事务未提交的数据
- 不可重复读:事务中对同一数据的查询结果不同
- 幻读:事务中同一个sql查询到的数据行数不同
-
事务的隔离级别有哪些?分别可以解决什么问题?
- 读不可提交:有脏读/不可重复读和幻读的问题
- 读可提交:解决了脏读的问题
- 可重复读:解决了脏读和不可重复读的问题
- 串行:解决了三个问题,但性能最差
-
隔离性是如何实现的?你知道MVCC吗?
隔离性的实现是数据库创建了一个视图,访问的时候以这个视图的逻辑结果为准
- 读不可提交,不创建视图
- 读可提交:每一条SQL都创建一个视图
- 可重复读:在事务开始的时候创建一个视图,事务中的所有sql都以这个视图为准
- 串行:用锁实现的
MVCC是多版本并发访问,通过回滚日志实现的,每一条更新操作都对应一条回滚日志,当前的值,都可以通过回滚日志找到历史更新过的值,多事务之间可以访问到不同的数据就是使用MVCC实现的。
-
你知道全局锁吗?
全局锁会锁住整个数据库实例,只能读,任何关于更新的操作都会阻塞。需要了解全局锁对业务的影响:
- 如果锁主库,那么业务基本停摆
- 如果锁从库,那么锁定期间,从库无法同步主库的更新,可能会导致主从不一致
-
简单说说表锁?
表锁是锁住整张表,以一个sql为例,lock tables t1 read,t2 write;这句sql执行完毕后:
- t1表只能读
- t2表执行者可以读写,其他人不可读写
-
你知道元数据锁吗?
元数据锁主要控制并发的DDL和DML操作,DML操作获取的是元数据读锁;DDL操作获取的是写锁,读写互斥,读读不互斥。
-
你知道共享锁排他锁和意向锁吗?
InnoDB存储引擎中支持行锁,行锁分为三种锁
- 共享锁:可以理解为读锁
- 排他锁:可以理解为写锁
- 意向锁:意向锁的出现主要是为了提高加表锁的判断效率,如果没有意向锁,线程需要加表锁时需要对每一行进行判断,是否有行级锁的存在,加了意向锁相当于多一个变量保存状态,这样可以O(1)的效率判断是否可以加表锁。
-
行锁的三种锁定范围(一致性锁定读时使用)
- 只锁定单行:查询条件中有索引,并且是唯一索引时,可以只锁定单行
- 锁范围,不包括记录本身
- next-key lock:单行和范围锁的合体:查询条件中有索引,并且不是唯一索引时使用
-
什么是索引?索引有什么作用?
索引是一种数据结构,可以方便我们快速的查找数据,提高查找效率。
-
Hash索引和B+树索引的区别?
- Hash索引适合等值查询,不适合范围查询
- Hash索引没办法利用索引完成排序
- 如果有大量重复键值对,Hash索引的效率会变低,因为需要解决hash碰撞的问题,解决hash碰撞一般采用拉链法
- 而 B+树索引是一种查询树,天然有序,所以可以利用它做范围查询和排序,并且因为B+树的层数少,IO次数也少
-
聚集索引和非聚集索引的区别?
- 聚集索引叶子节点存储的是整行数据;非聚集索引存储的是主键的值
- 如果使用非聚集索引时,待查询的列索引无法覆盖,那么就需要回表查询
-
联合索引和最左匹配原则是什么意思?
- Mysql中可以针对多列创建联合索引
- 最左匹配原则指的是,mysql使用索引时,会从最左边的一列开始匹配,建立一个(k1,k2,k3)的联合索引,相当于建立了(k1),(k1,k2)和(k1,k2,k3)三个索引
-
分库分表之后,id 主键如何处理?
分库分表之后,主键id就需要一个全局的id,一般来说生成主键id有以下几种方案:
- UUID:太长,无序不可读,不适合作为主键
- 利用redis生成id:性能较好,比较灵活,但是需要引入新的组件,增加了系统的复杂度
- 美团的Leaf:分布式唯一id生成器,也需要依赖关系型数据库和zookeeper等。
-
⼀条SQL语句在MySQL中如何执⾏的?
查询语句的执行流程如下:
- 连接器中校验是否有权限,如果没有权限,直接返回错误;mysql8.0之前的版本,会去查询缓存中检查是否有缓存,如果有直接返回
- 分析器进行词法分析和语法分析,词法分析主要关注关键字和相关的字段;语法分析主要检查sql语句是否有语法错误
- 优化器根据自己的算法确定执行计划,包括索引的选择等等。
- 执行器调用存储引擎接口,获取数据返回
更新语句的执行流程如下:
- 相关的校验等操作和查询操作是类似的,后面的更新操作不太一样
- 执行器调用存储引擎的更新接口后,存储引擎会更新数据,然后记录redo log,redo log此时为preopare状态
- 执行器写入binlog日志
- 执行器调用存储引擎的提交接口,存储引擎把redo log的状态更新为commit,此为两阶段提交,可以保证 异常宕机的重启恢复和二进制日志的备份的数据是一致的。
-
⼀条SQL语句执⾏得很慢的原因有哪些?
首先刨析这个问题,有两种情况:
- SQL语句平时执行的很快,只是偶尔变慢
- 在数据量一定的情况下,SQL语句的每次查询都很慢
针对第一种情况,SQL本身写的应该没什么问题,偶尔变慢的情况应该出在MySQL本身,有以下几种情况可能会导致查询变慢:
- 数据库在刷新脏页:redolog满了;内存不足需要淘汰一部分脏页
- 获取不到锁,阻塞了:如果某张表或者行已经被别的线程加锁,暂时获取不到锁
针对第二种情况,可能有以下的原因:
- 字段没正确建立索引;字段中包含表达式或者函数操作,导致有索引但是没有用上
- 数据库因为算法综合判断,可能不会选择索引,而选择全表扫描,算法中的影响因素主要有下面几个:
- 扫描行数
- 是否排序
- 是否回表
- 是否需要临时表
-
MyISAM引擎和innoDB引擎的区别?
- 是否支持事务
- 是否支持外键
- 表锁和行锁
- 是否支持MVCC
-
MySQL中如果有两个线程想要操作同一行数据,如何避免死锁?(重要)
核心有两点,知道mysql的锁定机制和防止死锁的理论知识,然后结合理论知识判断如何预防死锁即可:
防止死锁只要破坏死锁的四个条件之一就可以:
- 互斥:互斥本身无法破坏
- 请求并保持:一次性申请全部的锁即可,表现在sql语句中就是where条件中直接包括多个资源,直接锁定多个资源的数据。
- 不可剥夺:如果是应用层面,可以通过设置事务超时时间来实现事务超时释放锁并回滚事务,使用Transactional注解中的timeout即可实现。
- 循环等待条件:每个线程都按照同样的顺序申请资源,比如业务中会更新多条数据,更新操作都按照主键序号从小到大更新即可。
-
幻读在InnoDB中是如何被解决的?
innodb中的间隙锁就是为了解决幻读问题出现的。因为锁定了间隙,所以无法进行插入操作。这样自然就解决了可重复读隔离级别下的幻读问题。
-
InnoDB的MVCC如何解决脏读和不可重复读的问题?
- 一致性读只能读取事务开始时的快照版本(不加锁的select 解决了不可重复读的问题)
- 其他事务的读只能读到事务开始时,最新的已提交的版本快照(解决了脏读的问题)
- 当前读(for update和share mode)读的是当前行的最新版本并且会加行锁
- 一个事务内是可以读取到其所做的未提交的版本快照
Redis
-
为什么要用redis,不用map或者guava?
缓存分为本地缓存和分布式缓存,map和guava属于本地缓存,特点是:
- 轻量快速
- 生命周期随着JVM销毁而结束
- 不同实例中都需要各自持有一份本地缓存,缓存不具有一致性
分布式缓存,可以保证缓存的一致性,但是会增加系统架构的复杂度。
-
简单说说Redis的RDB持久化?
RDB持久化是指把当前进程数据生成快照保存到硬盘的过程。RDB的触发可以分为手动触发和自动触发两种。
- 手动触发:可以用save或者bgsave命令,区别在于bgsave不会阻塞当前redis实例,bgsave会fork一个子进程完成持久化工作,阻塞只发生在fork阶段
- 自动触发:可以在配置文件中使用save配置,配置m秒内存在n次修改,自动触发持久化过程。
-
说说RDB bgsave持久化的详细过程
- 父进程检查当前是否有在运行的子进程,如果有正在运行子进程,直接返回
- 父进程fork一个子进程出来,fork阶段会阻塞,fork完毕后不再阻塞父进程,可以响应其他指令
- 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子性替换
- 子进程发送信号给父进程通知完毕,父进程更新相关的统计信息
-
RDB持久化的优缺点?
- 优点:
- RDB方式产生的文件是一个压缩的二进制文件,非常适合全量备份和复制场景
- Redis加载RDB恢复数据远远快于AOF方式
- 缺点:
- RDB无法实时持久化/秒级持久化
- 老版本无法兼容新的RDB文件的格式问题
- 优点:
-
简单说说Redis的AOF持久化?
AOF持久化,是把每次写命令都记录到独立的日志中,重启恢复后再执行AOF命令重新恢复数据。AOF持久化主要是解决了实时性的问题。
-
说说AOF持久化的详细过程
- 所有命令写入追加到aof_buf缓冲区中
- 文件同步,文件同步可以在配置文件中配置三种同步机制,考虑到性能和数据安全性,一般推荐everysec
- always:写入到aof_buf以后,直接调用fsync系统调用同步到AOF文件
- everysec:写入到aof_buf后,先调用write系统调用,写入系统缓冲区后就会返回,然后由专门线程,每秒调用一次fsync操作
- no:写入到aof_buf后,调用write系统调用写入缓冲区后返回,等待操作系统执行同步磁盘的工作。
-
讲解下Redis线程模型?
Redis线程模型中包含四个组成部分:
- 套接字
- IO多路复用程序
- 事件分发器
- 事件处理器
IO多路复用程序用select或者其他系统调用管理多个套接字的网络事件,然后放入到一个统一的队列中,事件分发器从队列中取事件,然后分发到不同的处理器中进行处理。
-
谈谈 redis 和 memcached 的区别?
- 数据结构的丰富度:redis支持更丰富的数据结构。
- 持久化功能:redis支持RDB或者AOF方式的持久化;memcached不支持
- 集群功能:redis支持分布式;memcached不支持,需要客户端上使用分布式一致性hash算法来决定目标节点
- 线程模型不同:redis是单线程的,由一个线程负责处理所有的网络事件和业务处理;memcached是一主多从的reactor线程模型。
-
说说Redis中的数据类型
- String类型:一般用于复杂计数功能的缓存
- Hash类型:hash是以一个String类型为键,多个field和value的映射表作为值的数据结构,可以用于存储包括多列的行信息
- list类型:list简单理解就是链表,可以用来实现关注列表,消息列表等等
- Set类型:去重的数据结构,可以方便的实现交集并集等操作
- Sorted Set类型:增加了一个score权重,可以把Set中的数据按照score排序
-
redis的过期策略以及内存淘汰机制
redis中可以设置过期时间,当到了过期时间后,redis中会采取下面两种策略删除过期数据:
- 定期删除:每隔100ms随机抽取一些过期的key进行删除
- 惰性删除:当客户端访问到这个key了,才会实际删除
上面两种机制都无法保证完全清除过期的key,redis中引入了内存淘汰机制来保证内存不会被过期数据占满。默认有六种淘汰机制:
- 从已过期的数据中删除的:lru,random和ttl
- 从所有key中删除:lru,random
- 不删除:no-eviction
-
简单谈谈redis的事务
redis支持简单的事务,使用multi和exec来开始和执行事务,类似关系型数据库中的begin和commit。redis中不支持回滚事务,当出错时,根据不同的错误类型,处理机制也不同。
-
当有语法错误时,会导致整个事务中的命令都不会执行
-
当有运行时错误时,不会影响事务中的其他命令的执行
另外,redis中提供了watch命令,watch一个key之后,事务中如果有其他客户端修改了该key,整个事务不执行。
-
-
你知道缓存雪崩吗?简单说说
缓存雪崩指的是,当redis集群不可用或者同时有大量缓存失效时,会造成大量的请求都走数据库,把数据库服务击垮。
- 针对redis集群不可用,应该事前做好redis集群的高可用性检查,选择合适的内存淘汰策略。
- 针对大量缓存同时失效,可以采取设置随机过期时间的方式,来避免缓存同时失效
- 提前设计好 本地缓存和限流降级方案,本地缓存+redis中如果都没有,再去查数据库,查数据库的请求要通过限流降级组件来保护数据库。
-
你知道缓存穿透吗?
缓存穿透指的是,大量请求的key本身不在缓存中,导致直接走了数据库查询。举例来说,黑客攻击时,会伪造很多不存在的key来导致大量请求落到数据库中。解决方法有两种:
- 最基本的是做好参数校验,不合规的参数直接返回
- 使用布隆过滤器,其实本质就是一个hash函数加位数组,报存在,有一定概率的误判(hash冲突),但是不存在一定是准确的,比如两个字符串的hash值相同得到位数组的位置就相同。提前把有效的key存储在布隆过滤器中,这样当无效key传递过来时,直接返回。
-
如何用redis实现分布式锁?你还知道其他更好的方式吗?
实际业务中没有使用过,这块等待有真实业务场景应用后再来补充。
-
如何保证缓存与数据库双写时的数据⼀致性?
- 简单的解决方式:如果有更新需求,先删除缓存,再更新数据库,如果数据库更新失败,缓存也为空,不会出现不一致。
- 简单解决方式的不足:删除缓存完毕后,更新数据库之前,又有一个线程要访问该数据,结果发现缓存中没有数据,从数据库中查询出旧数据,然后更新到缓存中,这个时候更新数据库,那么缓存和数据库又发生了不一致。
- 解决方案:使用一个队列把读请求和写请求串在一起,写请求执行完毕后才会执行读请求和更新,但是要注意测试读请求的阻塞时长。 如何保证缓存与数据库的双写一致性?