Skip to content

Latest commit

 

History

History
831 lines (643 loc) · 44.4 KB

interview.md

File metadata and controls

831 lines (643 loc) · 44.4 KB

Interview 备忘清单

这是一份Java常见面试题集合

Question

基础

JVM

  1. 详细说明jvm内存结构和内存模型
  2. 各分区大小调节指令?
  3. 讲讲什么情况下会出现内存溢出,内存泄漏?
  4. JVM 年轻代到年老代的晋升过程?
  5. JVM 出现 fullGC 很频繁,怎么去线上排查问题?
  6. 类加载为什么采用双亲委派模式,什么场景打破了这个模式?
  7. 类的加载顺序?
  8. 类的实例化顺序
  9. JVM垃圾回收机制,何时触发MinorGC等操作
  10. hotspot虚拟机新生代回收算法(复制算法)
  11. 老年代回收算法?
  12. 何时触发FullGC?
  13. JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的
  14. 各种回收器,各自优缺点,重点CMS. G1?
  15. 判断对象是否可以回收?
  16. 各种回收算法
  17. OOM的七种原因及解决?
  18. 强引用,软引用,弱引用,虚引用

JUC

  1. synchronized 的实现原理以及锁优化?
  2. volatile 的实现原理?
  3. Java 的信号灯?
  4. synchronized 用法及区别?
  5. 怎么实现所有线程在等待某个事件的发生才会去执行?
  6. CAS?CAS 有什么缺陷,如何解决?
  7. synchronized 和 lock 有什么区别?
  8. Hashtable 是怎么加锁的 ?
  9. AQS
  10. 如何检测死锁?怎么预防死锁?
  11. 如何保证多线程下 i++ 结果正确?
  12. 线程池的种类,区别和使用场景?
  13. 分析线程池的实现原理和线程的调度过程?
  14. 线程池如何调优,最大数目如何确认?
  15. ExecutorService中execute和submit方法的区别?
  16. ThreadLocal原理,用的时候需要注意什么?
  17. CountDownLatch 和 CyclicBarrier 的用法,以及区别?
  18. LockSupport工具
  19. Condition接口及其实现原理
  20. Fork/Join框架的理解
  21. 分段锁的原理,锁力度减小的思考
  22. 八种阻塞队列以及各个阻塞队列的特性

数据库

  1. mysql分页有什么优化?
  2. mysql数据类型?
  3. Blob 和 text 有什么区别?
  4. 索引分类?
  5. 索引哪些情况会失效?
  6. 什么情况下适合使用索引
  7. 什么是组合索引以及最左前缀原则?
  8. mysql 的表锁,页锁,行锁
  9. 事务的特性和隔离级别
  10. mysql有哪几种日志
  11. 事务是如何通过日志来控制的?
  12. 数据库存储引擎介绍
  13. 你是怎么优化 SQL 的?(步骤和方法)
  14. 如何选择合适的分布式主键方案?
  15. 在高并发情况下,如何做到安全的修改同一行数据?
  16. select for update 有什么含义?
  17. 一条 SQL 语句在 MySQL 中如何执行的?
  18. InnoDB完成一次更新操作日志层面的步骤
  19. mysql 中 int(20)和 char(20)以及 varchar(20)的 区别?
  20. drop、delete 与 truncate 的区别
  21. MySQL 的复制原理以及流程
  22. MySQL 的基础架构图

Spring

  1. Spring IOC 的理解,其初始化过程?
  2. Spring Bean 的生命周期,如何被管理的?
  3. BeanFactory、FactoryBean 和ApplicationContext?
  4. Spring循环依赖
  5. Spring 声明式事务原理
  6. Spring 的不同事务传播行为有哪些
  7. Spring 事务失效场景
  8. Spring AOP实现原理?
  9. Spring MVC 的工作原理?
  10. Springboot 自动配置原理
  11. springboot启动流程

Redis

  1. Redis 为什么这么快?
  2. Redis用过哪些数据数据,以及Redis底层怎么实现
  3. 什么是缓存击穿、缓存穿透、缓存雪崩?
  4. 什么是热 Key 问题,如何解决热 key 问题?
  5. 如何使用Redis来实现分布式锁
  6. Redis的并发竞争问题如何解决
  7. Redis持久化的几种方式,怎么实现的,优缺点是什么?
  8. Redis的缓存过期策略和内存淘汰机制
  9. 如何实现 Redis 的高可用?(Redis 主从、哨兵、集群)
  10. Redis6.0为何采用多线程?
  11. MySQL 与 Redis 如何保证双写一致性?
  12. 聊聊 Redis 事务机制

消息中间件

  1. 消息队列有哪些使用场景?
  2. RabbitMQ消息丢失的3种情况?
  3. RabbitMQ消息持久化?
  4. RabbitMQ交换机类型和区别?
  5. 消息队列如何解决消息积压问题?
  6. 消息队列有可能发生重复消费,如何避免,如何做到幂等?
  7. 如何保证数据一致性,事务消息如何实现?
  8. 如何保证消息消费的顺序性?

Mybatis

  1. Mybatis原理
  2. mybatis分页是怎么做的?
  3. mybatis 属性名和表字段名不同该怎么处理?
  4. mybatis四大接口?

分布式相关

  1. 接口的幂等性的概念
  2. 普通Hash和一致性Hash?
  3. 分布式锁实现原理及方式?

MySQL

mysql分页有什么优化

  • 使用自查询找出分页最小id再用limit
  • 使用between..and
  • 先找出id再使用in查询

索引分类

  • 主键索引(PRIMARY KEY)
  • 唯一索引(UNIQUE)
  • 全文索引(FULLTEXT)
  • 空间索引(SPATIAL)
  • 组合索引

什么情况下适合使用索引

  • 表的某个字段值离散度越高,越适合选做索引字段
  • 占用存储空间少的字段更适合选做索引
  • 存储空间固定的字段更适合选做索引
  • where语句中经常使用的字段

索引失效

  • 查询条件包含 or,可能导致索引失效
  • 如何字段类型是字符串,where 时一定用引号括起来,否则索引失效
  • like 通配符可能导致索引失效。
  • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
  • 在索引列上使用 mysql 的内置函数,索引失效。
  • 对索引列运算(如,+、-、*、/),索引失效。
  • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
  • 索引字段上使用 is null, is not null,可能导致索引失效。
  • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
  • mysql 估计使用全表扫描要比使用索引快,则不使用索引。

组合索引与最左前缀

  • 联合索引,用户可以在多个列上建立索引,这种索引叫做联合索引。 因为 InnoDB 引擎中的索引策略的最左原则,所以需要注意联合索引中的顺序
  • 最左前缀原则,就是最左优先,在创建多列索引时,要根据业务需求,where 子句中使用最频繁的一列放在最左边。
  • 当我们创建一个组合索引的时候,如(k1,k2,k3),相当于创建了(k1)、 (k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则

Blob和text有什么区别?

  • Blob主要存储二进制数据,text存储大字符串
  • Blob 值被视为二进制字符串(字节字符串),它们没有字符集,并且排序和比较基 于列值中的字节的数值。
  • text 值被视为非二进制字符串(字符字符串)。它们有一个字符集,并根据字符 集的排序规则对值进行排序和比较

select for update 有什么含义

select不会加锁,但是select for update则会加锁,具体加表锁还是行锁主要看是否使用索引:

  • 用到索引,加行锁
  • 未用到索引,加表锁

优化SQL方法

  • 加索引
  • 避免返回不必要的数据
  • 适当分批量进行
  • 优化 sql 结构
  • 分库分表
  • 读写分离

优化SQL步骤

  • show status 命令了解各种 sql 的执行频率
  • 通过慢查询日志定位那些执行效率较低的 sql 语句
  • explain 分析低效 sql 的执行计划

隔离级别

  • 读未提交
  • 读已提交
  • 可重复读
  • 串行化

事务的特性

特性 解释
原子性 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行,使用 undo log 来实现的
持久性 指在事务开始之前和事务结束以后,数据不会被破坏,假如 A 账户给 B 账户转 10 块钱,不管成功与否,A 和 B 的总金额是不变的,使用 redo log 来实现
隔离性 多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它 事务运行效果。简言之,就是事务之间是进水不犯河水的,通过锁以及 MVCC,使事务相互隔离开
一致性 表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在 数据库之中,通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性

MySQL几种锁

  • 表锁: 开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现 死锁。
  • 行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发 度高。
  • 页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和 行锁之间,并发度一般

分布式主键生成方案

  • 数据库自增长序列或字段。
  • UUID。
  • Redis 生成 ID
  • Twitter 的 snowflake 算法
  • 利用 zookeeper 生成唯一 ID
  • MongoDB 的 ObjectId

如何安全的修改一行数据

  • 悲观锁,如select for update
  • 乐观锁,如版本号机制或 CAS 算法实现

mysql日志类型

:- -
redo log 属于InnoDB存储引擎的日志,用来保证事务的持久性
undo log 属于InnoDB存储引擎的日志,用来保证事务的原子性
bin log 主要用来备份数据,恢复数据
slow query log 慢查询日志,用来定位慢查询
error log 错误日志
general log
relay log

事务是如何通过日志来实现的

  • 因为事务在修改页时,要先记 undo,在记 undo 之前要记 undo 的 redo, 然后 修改数据页,再记数据页修改的 redo。 Redo(里面包括 undo 的修改) 一定要 比数据页先持久化到磁盘。
  • 当事务需要回滚时,因为有 undo,可以把数据页回滚到前镜像的 状态,崩溃恢复 时,如果 redo log 中事务没有对应的 commit 记录,那么需要用 undo 把该事务 的修改回滚到事务开始之前。
  • 如果有 commit 记录,就用 redo 前滚到该事务完成时并提交掉。

InnoDB与MyISAM对比

类型 InnoDB MyISAM
事务 支持 不支持
外键 支持 不支持
MVCC 支持 不支持
全文索引 不支持(5.7之前) 支持
表、行锁 表锁
主键 必须有 可没有
内存和空间 相对大 相对小

一条SQL语句在 MySQL中如何执行的

  • 先检查该语句是否有权限
  • 如果没有权限,直接返回错误信息
  • 如果有权限,在 MySQL8.0 版本以前,会先查询缓存。
  • 如果没有缓存,分析器进行词法分析,提取 sql 语句 select 等的关键元素。然后 判断 sql 语句是否有语法错误,比如关键词是否正确等等。
  • 优化器进行确定执行方案
  • 进行权限校验,如果没有权限就直接返回错误信息,如果有权限就会调用数据库引 擎接口,返回执行结果

InnoDB完成一次更新操作日志层面的步骤

  1. 开启事务
  2. 查询待更新的记录到内存,并加 X 锁
  3. 记录 undo log 到内存 buffer
  4. 记录 redo log 到内存 buffer
  5. 更改内存中的数据记录
  6. 提交事务,触发 redo log 刷盘
  7. 记录 bin log
  8. 事务结束

int(20)与char(20)、varchar(20)

  • int(20) 表示字段是 int 类型,显示长度是 20
  • char(20)表示字段是固定长度字符串,长度为 20
  • varchar(20) 表示字段是可变长度字符串,长度为 20

delete、truncate与 drop 的区别

选项 delete truncate drop
类型 DML DDL DDL
回滚 可回滚 不可回滚 不可回滚
删除内容 表结构还在,删除表的全部或者一部分数据行 表结构还在,删除表中的所有数据 从数据库中删除表,所有的数据行,索引和权限也会被删除
删除速度 删除速度慢,逐行删除 删除速度快 删除速度最快

主从复制原理

  • 主数据库有个 bin-log 二进制文件,纪录了所有增删改 Sql 语句。(binlog 线程)
  • 从数据库把主数据库的 bin-log 文件的 sql 语句复制过来。(io 线程)
  • 从数据库的 relay-log 重做日志文件中再执行一次这些 sql 语句。(Sql 执行线程)

MySQL 的基础架构图

Java

List 和 Set 的区别

  1. list和set都继承自Collection接口
  2. list是有序的可重复的,且允许值为null,set是无序的,有其值的hashcode决定的,虽然无序但位置是固定的
  3. list支持for循环和迭代器遍历元素,set只能通过迭代器,无法通过下标获取值
  4. set检索元素效率低,增删快,增删不会影响元素位置

ArrayList什么时候扩容

  • 空参构造的集合第一次添加元素扩容,空参生成的集合为空的数组,长度为0,第一次添加元素,长度变为10;
  • 当前所需长度超出现有长度时扩容,扩容是原长度+原长度>>1

为什么Arrays生成的List不能增删

  • 因为源码可知,Arrays.asList()生成的ArrayList只是一个内部类,继承自AbstractList,但是却没有重写其add()和remove()方法,因为调用时调用的AbstractList的add和remove方法,故会报错。

如何在循环中删除元素而不触发并发修改异常

  • 使用迭代器去遍历,迭代器的删除方法,迭代器每次,最好使用list.listIterator()获取的ListIterator迭代器,因为ArrayList内部的迭代器只能删,不能增
  • 使用list.removeIf("cat"::equals);
  • 使用fori的逆序遍历
  • 使用普通的for循环删除,手动去处理因删除操作导致集合大小变化的问题

HashSet 是如何保证不重复的

  • 其底层维护了HashMap结构,map保证不重复的原理是: 1.先通过hash计算该元素的位置,没有则直接插入,有值则通过equals和哈希判断两个值是否相等,如果相等,则会把这个元素返回

HashMap为什么不安全

  • 主要是put方法中resize()方法线程不安全,当多个线程同时开启扩容,会各自生成新的数组进行拷贝扩容,最终结果只有一个新数组被赋值给table变量,其他的线程均会丢失

使用过哪些map各有什么特点

:- :- :-
介绍 使用场景
HashMap 最通用的 Map,非线程安全、无序 无特殊需求都可使用
CocurrentHashMap 线程安全的Map,通过 synchronized + CAS 实现安全 需要保证线程实现线程安全
Hashtable 早期的线程安全 Map,直接通过在方法加 synchronized 实现线程安全 现在理论上不会使用
LinkedHashMap 能记录访问顺序或插入顺序的Map,通过head、tail 属性维护有序双向链表,通过 Entry的 after、before属性维护节点的顺序 需要记录访问顺序或插入顺序
TreeMap 通过实现Comparator实现自定义顺序的Map,如果没有指定Comparator 则会按 key 的升序排序,key如果没有实现Comparable 接口,则会抛异常 需要自定义排序

HashMap插入和扩容过程

插入过程

扩容过程

HashMap 1.7 与 1.8 的 区别

:- :- :-
比较 HashMap1.7 HashMap1.8
数据结构 数组+链表 数组+链表+红黑树
节点 Entry Node TreeNode
Hash算法 较为复杂 异或hash右移16位
对Null的处理 单独写一个putForNull()方法处理 作为以一个Hash值为0的普通节点处理
初始化 赋值给一个空数组,put时初始化 没有赋值,懒加载,put时初始化
扩容 插入前扩容 插入后,初始化,树化时扩容
节点插入 头插法 尾插法

HashMap树和链表转化的阈值

但为什么阈值就是8和6呢?中间的7是有什么作用的呢? 当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。 但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。 不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006, 几乎是不可能事件。这种不可能事件都发生了,说明bin中的节点数很多,查找起来效率不高。至于7,是为了作为缓冲,可以有效防止链表和树频繁转换。

ConcurrentHashMap

jdk1.7时原理

通过Segment实现分段锁设计在保证线程安全的同时提高性能 Segment本身就相当于一个HashMap对象,ConcurrentHashMap是由2的N次方个Segment组成的,可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。这样的二级结构,和数据库的水平拆分有些相似。 和HashMap一样,Segment内包含一个hashEntry数组,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高:

  1. 在不同Segment写操作是可以并发执行的;
  2. 在不同Segment一读一写是可以并发执行的;
  3. 在相同Segment写操作是阻塞的;
  • Get方法: 1.为输入的Key做Hash运算,得到hash值。 2.通过hash值,定位到对应的Segment对象 3.再次通过hash值,定位到Segment当中数组的具体位置。
  • Put方法: 1.为输入的Key做Hash运算,得到hash值。 2.通过hash值,定位到对应的Segment对象 3.获取可重入锁 4.再次通过hash值,定位到Segment当中数组的具体位置。 5.插入或覆盖HashEntry对象。 6.释放锁。 读写都需要二次定位,先定位到Segment对象,再定位到对Segment对象内具体元素
  • Size方法(如何保证一致性的): ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下: 1.遍历所有的Segment。 2.把Segment的元素数量累加起来。 3.把Segment的修改次数累加起来。 4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。 5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。 6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。 7.释放锁,统计结束。
  • 设计思想:为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性 ConcurrentHashMap在对Key求Hash值的时候,为了实现Segment均匀分布,进行了两次Hash

1.8:抛弃了分段锁(Segment)设计,采用CAS+Synchronized,整个看起来就像是优化过且线程安全的HashMap

  • put过程:对当前的table进行无条件自循环直到put成功 a. 如果没有初始化就先调用initTable()方法来进行初始化过程 b. 如果没有hash冲突就直接CAS插入 c. 如果还在进行扩容操作就先进行扩容 d. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入, e. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环 f. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

深拷贝和浅拷贝的区别

HashMap底层数据结构

数组+链表+红黑树

异常分类以及处理方式

数组在内存中如何分配

  • 数组初始化分为静态初始化(成员声明数组元素,不声明长度)和动态初始化(不声明元素,声明长度,引用类型默认元素都为null)
  • 数组一旦初始化完成,长度不可变,数组对象的引用可变
  • 栈中生成数组对象引用,堆中放数组对象,如果是基本类型数组则保存的是基本类型的值,引用类型数组保存的是对象的引用

cloneable接口实现原理

如果一个类重写了 Object 内定义的 clone() ,需要同时实现 Cloneable 接口(虽然这个接口内并没有定义 clone() 方法), 否则在调用 clone() 时会报 CloneNotSupportedException 异常,也就是说, Cloneable 接口只是个合法调用 clone() 的标识(marker-interface) 如果要拷贝的对象只有基本类型,则调用super.clone(),是深拷贝,但若包含引用类型,则会使用同一个对象,即不会拷贝引用对象,需要单独拷贝引用对象

JDK 动态代理与 cglib 实现的区别

  • java 动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法 前调用 InvokeHandler 来处理。
  • cglib 动态代理是利用 asm 开源包,对代理对象类的 class 文件加载进来,通过修 改其字节码生成子类来处理。
  • JDK 动态代理只能对实现了接口的类生成代理,而不能针对类
  • cglib 是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因 为是继承,所以该类或方法最好不要声明成 final

Redis

Redis为什么这么快

1.基于内存实现

2.高效的数据结构

3.合理的数据编码

:- -
String 如果存储数字的话,是用 int 类型的编码;如果存储非数字,小于等于 39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。
List 如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节 (默认),使用 ziplist 编码,否则使用 linkedlist 编码
Hash 哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用 ziplist 编码,否则使用 hashtable 编码。
Set 如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码, 否则使用 hashtable 编码。
ZSet 当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使 用 ziplist 编码,否则使用 skiplist(跳跃表)编码

4.合理的线程模型

I/O多路复用、单线程模型

5.虚拟内存机制

通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘

Redis数据结构及底层实现

String

  • 简介:String 是 Redis 最基础的数据结构类型,它是二进制安全的,可以存储图片 或者序列化的对象,值最大存储为 512M
  • 简单使用举例: set key value、get key 等
  • 应用场景:共享 session、分布式锁,计数器、限流。
  • 内部编码有 3 种,int(8 字节长整型)/embstr(小于等于 39 字节字符串)/ raw(大于 39 个字节字符串)

List

  • 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储 2^32-1 个元素。
  • 简单实用举例: lpush key value [value ...] 、lrange key start end
  • 内部编码:ziplist(压缩列表)、linkedlist(链表)
  • 应用场景: 消息队列,文章列表,

Hash

  • 简介:在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构
  • 简单使用举例:hset key field value 、hget key field
  • 内部编码:ziplist(压缩列表) 、hashtable(哈希表)
  • 应用场景:缓存用户信息等。
  • 注意点:如果开发使用 hgetall,哈希元素比较多的话,可能导致 Redis 阻塞,可以使用 hscan。而如果只是获取部分 field,建议使用 hmget。

Set

  • 简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素
  • 简单使用举例:sadd key element [element ...]、smembers key
  • 内部编码:intset(整数集合)、hashtable(哈希表)
  • 注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞 Redis 的可能性,可以使用 sscan 来完成。
  • 应用场景: 用户标签,生成随机数抽奖、社交需求。

ZSet

  • 简介:已排序的字符串集合,同时元素不能重复
  • 简单格式举例:zadd key score member [score member ...],zrank key member
  • 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
  • 应用场景:排行榜,社交需求(如用户点赞)。

Geo

  • Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作

HyperLogLog

  • 用来做基数统计算法的数据结构,如统计网站的 UV。

Bitmaps

  • 用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于 字符串类型实现的,可以把 bitmaps 成作一个以比特位为单位的数组

缓存击穿、缓存穿透、缓存雪崩

:- :- :-
比较 说明 解决
缓存击穿 如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮 1.互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 2.不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
缓存穿透 当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增, 第一种方案,非法请求的限制; 第二种方案,缓存空值或者默认值; 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
缓存雪崩 大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机 1.均匀设置过期时间; 2.互斥锁; 3.双 key 策略; 4.后台更新缓存;

什么是热 Key 问题,如何解决热 key 问题?

在 Redis 中,我们把访问频率高的 key,称为热点 key。 如果某一热点 key 的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

产生原因

  • 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
  • 请求分片集中,超过单 Redi 服务器的性能,比如固定名称 key,Hash 落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。

如何解决热 key 问题?

  • Redis 集群扩容:增加分片副本,均衡读流量;
  • 将热 key 分散到不同的服务器中;
  • 使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。

Redis的并发竞争问题如何解决

什么是并发竞争key

多个客户端同时请求修改同一个key

解决方案

  • 分布式锁+时间戳
  • 消息队列

AOF持久化

AOF(Append Only File) 持久化

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,注意,读操作不会记录,因为没意义

写回策略:AOF日志写入硬盘的策略

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

AOF重写机制

  • Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
  • AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件(避免重写过程失败,造成文件损害),重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。
  • 重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。

AOF 日志恢复数据的方式是顺序执行日志里的每一条命令

RDB持久化

RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据 什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。 RDB 持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的 数据集快照写入磁盘中,它是 Redis 默认的持久化方式。执行完操作后,在指 定目录下会生成一个 dump.rdb 文件,Redis 重启的时候,通过加载 dump.rdb 文件来恢复数据。RDB 触发机制主要有以下几种:

  • 手动Save
  • 手动bgsave
  • 自动触发

在生成 RDB 期间,Redis 可以同时处理写请求么?

可以的,Redis 提供两个指令生成 RDB,分别是 save 和 bgsave。

  • 如果是 save 指令,会阻塞,因为是主线程执行的。
  • 如果是 bgsave 指令,是 fork 一个子进程来写入 RDB 文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。

如何选择 RDB 和 AOF

  • 如果数据不能丢失,RDB 和 AOF 混用
  • 如果只作为缓存使用,可以承受几分钟的数据丢失的话,可以只使用 RDB。
  • 如果只使用 AOF,优先使用 everysec 的写回策略

混合持久化

RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。

AOF 优点是丢失数据少,但是数据恢复不快。

为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。

混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

缓存过期策略

定时删除

在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。 缺点:占用cpu时间,影响系统性能

惰性删除

不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。 缺点:不请求,则不会删除便会一直占用内存

定期删除

每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。 缺点:不好控制删除频率,内存清理不如定时删除

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

内存淘汰策略

:- :-
volatile-lru 当内存不足以容纳新写入数据时,从设置了过期时间的 key 中 使用 LRU(最近最少使用)算法进行淘汰;
allkeys-lru 当内存不足以容纳新写入数据时,从所有 key 中使用 LRU(最近 最少使用)算法进行淘汰。
volatile-lfu 4.0 版本新增,当内存不足以容纳新写入数据时,在过期的 key中,使用 LFU 算法进行删除 key。
allkeys-lfu 4.0 版本新增,当内存不足以容纳新写入数据时,从所有 key 中 使用 LFU 算法进行淘汰;
volatile-random 当内存不足以容纳新写入数据时,从设置了过期时间的 key 中,随机淘汰数据;。
allkeys-random 当内存不足以容纳新写入数据时,从所有 key 中随机淘汰 数据。
volatile-ttl 当内存不足以容纳新写入数据时,在设置了过期时间的 key 中, 根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction 默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

MySQL 与 Redis 如何保证双写一致性

  • 缓存延时双删
  • 删除缓存重试机制
  • 读取 biglog 异步删除缓存

如何实现 Redis 的高可用?

Redis6.0以后为何采用多线程?

虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。 所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。 Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上

聊聊 Redis 事务机制

Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。 事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行 过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会 插入到事务执行命令序列中。 简言之,Redis 事务就是顺序性、一次性、排他性的执行一个队列中的一系列 命令。 Redis 执行事务的流程如下:

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD )
:- :-
命令 描述
EXEC 执行所有事务块内的命令
DISCARD 取消事务,放弃执行事务块内的所有命令
MULTI 标记一个事务块的开始
UNWATCH 取消 WATCH 命令对所有 key 的监视。
WATCH 监视 key ,如果在事务执行之前,该 key 被其他命令所改动,那么事
务将被打断。

分布式

接口的幂等性

概念

计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。

为什么需要幂等

举例:

1.我们开发一个转账功能,假设我们调用下游接口超时了。一般情况下,超时可能是网络传输丢包的问题,也可能是请求时没送到,还有可能是请求到了,返回结果却丢了。这时候我们是否可以重试呢?如果重试的话,是否会多转了一笔钱呢?

2.MQ(消息中间件)消费者读取消息时,有可能会读取到重复消息。(重复消费)

3.比如提交form表单时,如果快速点击提交按钮,可能产生了两条一样的数据(前端重复提交)

幂等设计的基本流程

幂等处理的过程,说到底其实就是过滤一下已经收到的请求,当然,请求一定要有一个全局唯一的ID标记哈。 然后,怎么判断请求是否之前收到过呢?把请求储存起来,收到请求时,先查下存储记录,记录存在就返回上次的结果,不存在就处理请求。

参考:聊聊幂等设计

幂等性的解决方案

Insert接口幂等性

1.使用分布式锁保证幂等性 秒杀场景下,一个用户只能购买同一商品一次的解决方法:采用用户ID+商品ID,存储到redis中,使用redis中的setNX操作,等待自然过期。

2.使用token机制保证幂等性 用户注册时,用户点击注册按钮多次,是不是会注册多个用户?我们可以在用户进入注册页面后由后台生成一个token,传给前端页面,用户在点击提交时,将token带给后台,后台使用该token作为分布式锁,setNX操作,执行成功后不释放锁,等待自然过期。

3.使用mysql unique key 保证幂等性 用户注册时,用户点击注册按钮多次,是不是会注册多个用户? 我们可以使用手机号作为mysql用户表唯一key。也就是一个手机号只能注册一次。

Update接口幂等性

update操作可能存在幂等性的问题:

1.用户更改个人信息,疯狂点击按钮,不会发生幂等性问题,因为数据始终为修改后的数据。

2.用户购买商品,用户在点击后,网络出现问题,可能再次点击,这样就会出现幂等性问题,导致购买了多次,可以使用乐观锁

update order set count=count-1,version=version+1 where id=1 and version=1 1.

Delete接口幂等性

根据唯一id删除不会出现幂等性问题,因为第二次删除的时候mysql中已经不存在该数据

Select接口幂等性

查询操作不会改变数据,所以是天然的幂等性操作。

混合操作(一个接口包含多种操作)

使用Token机制,或使用Token + 分布式锁的方案来解决幂等性问题

幂等性实现方案

Token机制实现

通过​​Token​​ 机制实现接口的幂等性,这是一种比较通用性的实现方法。

具体流程步骤:

客户端会先发送一个请求去获取Token,服务端会生成一个全局唯一的​​ID​​​作为​​Token​​​保存在​​Redis​​​中,同时把这个​​ID​​返回给客户端; 客户端第二次调用业务请求的时候必须携带这个​​Token​​; 服务端会校验这个 ​​Token​​​,如果校验成功,则执行业务,并删除​​Redis​​​中的 ​​Token​​; 如果校验失败,说明​​Redis​​​中已经没有对应的 ​​Token​​,则表示重复操作,直接返回指定的结果给客户端。

基于MySQL实现

通过​​MySQL​​唯一索引的特性实现接口的幂等性。

具体流程步骤:

建立一张去重表,其中某个字段需要建立唯一索引; 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中; 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑; 如果插入失败,则代表已经执行过当前请求,直接返回。

基于Redis实现

通过Redis​​​的​​SETNX​​命令实现接口的幂等性。

SETNX key value​​​:当且仅当​​key​​​不存在时将​​key​​​的值设为​​value​​​;若给定的​​key​​​已经存在,则​​SETNX​​​不做任何动作。设置成功时返回​​1​​​,否则返回​​0​​。

具体流程步骤:

客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段; 将该字段以​​SETNX​​​的方式存入​​Redis​​中,并根据业务设置相应的超时时间; 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑; 如果设置失败,则代表已经执行过当前请求,直接返回。