Java

访问权限控制符有哪些

访问权限控制符 本类 包内 包外子类 任何地方
public
protected ×
× ×
private × × ×

子类初始化的顺序

推荐文章:Java类的初始化顺序

① 父类静态代码块和静态变量。

② 子类静态代码块和静态变量。

③ 父类普通代码块和普通变量。

④ 父类构造方法。

⑤ 子类普通代码块和普通变量。

⑥ 子类构造方法。

扩展:

  • 静态代码块和静态变量只加载一次

  • 静态加载完后如果有main方法会先执行main方法

  • 静态变量和静态代码块之间,普通变量和普通代码块之间的顺序取决于它们在类中出现的先后顺序。

HashMap的底层实现

推荐文章:Java集合之一—HashMap

HashMap线程不安全的,效率高;可以存储null的key和value。

JDK 7 和 JDK 8 的区别:

  • 数据结构:

  • JDK 7是数组+链表结构(即为链地址法),结点的实现类是Entry类

  • JDK 8是数组+链表+红黑树(节点数>=8并且数组 长度>=64转换),结点的实现类是Node类

  • 插入位置:

  • JDK 7:新添加的元素作为链表的头结点,新元素指向旧元素

  • JDK 8:新添加的元素作为链表的尾结点或树的叶子结点,旧元素指向新元素

  • hash算法简化:

  • JDK 7:h^ =(h>>>20)^(h>>>12) return h ^(h>>>7) ^(h>>>4)

  • JDK 8:(key==null)?0:(h=key.hashCode())^(h>>>16)

  • 扩容机制:

  • JDK 7扩容条件:元素个数 > 容量(16) * 加载因子 (0.75) && 插入的数组位置有元素存在

  • JDK 8扩容条件 :元素个数 > 容量 (16) * 加载因子(0.75)

HashMap的默认负载因子:

默认负载因子是0.75。

  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内存空间。而且经常扩容也会影响性能。
  • 选择0.75作为默认的负载因子是时间和空间成本上寻求的一种折中选择。

扩容机制:

ConcurrentHashMap的底层实现

ArrayList和LinkedList的区别

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层是 Object 数组LinkedList 底层是 双向链表 数据结构
  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  5. 内存空间占用: ArrayList 在 list 列表的结尾会预留一定的容量空间,而 LinkedList 在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放前驱节点和后继节点的引用)。

线程池

推荐文章:线程池原理

讲一下ThreadPoolExecutor的几个重要参数:

  • corePoolSize: 核心线程数,定义了最小可以同时运行的线程数量。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在任务缓存队列中。
  • maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • keepAliveTime: 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit: keepAliveTime 参数的时间单位。
  • threadFactory: 线程工厂,主要用来创建线程。
  • handler: 任务拒绝策略。

线程池的执行流程:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务(添加失败的任务);
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果非线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

有哪几种任务缓存队列:

推荐文章:线程池的三种缓存队列

  • **ArrayBlockingQueue**:基于数组的先进先出队列,此队列创建时必须指定大小;
  • **LinkedBlockingQueue**:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  • SynchronousQueue :它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

有哪几种任务拒绝策略:

  • ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出 RejectedExecutionException异常。默认

  • ThreadPoolExecutor.DiscardPolicy 也是丢弃任务,但是不抛出异常。

  • ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务

  • ThreadPoolExecutor.CallerRunsPolicy 由调用execute方法的线程运行被拒绝的任务

线程池有哪些:

推荐文章:四种线程池

  • CachedThreadPool没有核心线程,非核心线程是无界的,空闲线程等待新任务的最长时间是60s,任务队列采用SynchronousQueue,适用于处理大量、耗时少的任务

  • SecudleThreadPool:按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程是无界的。任务队列采用DelayedWorkQueue,会将任务排序,适用于执行周期性的任务。

  • SingleThreadPool只有一个核心线程来执行任务,适用于有顺序的任务的应用场景。任务队列采用LinkedBlockingQueue

  • FixedThreadPool:只有核心线程且数量固定,没有非核心线程。任务队列采用LinkedBlockingQueue

如何合理配置线程池的大小:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU核数

线程等待时间(比如IO时间)所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程,减少线程上下文的切换。

可以通过控制台输入jconsole,打开图形监控界面,监控线程运行情况。

动态代理

推荐文章:动态代理总结,面试你要知道的都在这里,无废话!

三种代理方式之间对比:

  • 静态代理:
  • 实现:手写代理类,一是通过继承委托类的方式实现其子类,重写父类方法;二是与委托类实现共同的一个接口
  • 特点:硬编码效率低,代码重用性不强
  • JDK动态代理
  • 实现:代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler接口并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理。
  • 创建动态代理对象:
    • Proxy.newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h)
  • 特点:底层使用反射机制进行方法的调用,代码复用率高。只能够代理实现了接口的委托类同时继承了Proxy类。
  • CGLIB动态代理
  • 实现:代理类将被委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法,在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理。
  • 创建动态代理对象:
    • Enhancer enhancer = new Enhancer();
    • 设置被代理类:enhancer.setSuperclass(Car.class);
    • 设置回调函数:enhancer.setCallback(new MyMethodInterceptor());
    • 创建代理对象:Car car = (Car) enhancer.create();
  • 特点:底层方法的调用并不是通过反射来完成的,而是将方法全部存入一个数组中,通过数组索引直接进行方法调用(FastClass机制),可以在运行时对类或者是接口进行增强操作,且被代理类无需实现接口。不能对final类以及final方法进行代理。

Spring底层是如何选择哪种动态代理方式:

Spring会根据具体的Bean是否有实现接口去选择动态代理方式,如果有实现接口,使用的是JDK动态代理的方式,如果没有实现接口,使用的是CGLIB动态代理的方式。

Spring

Spring的IoC理解

IoC (Inversion of Control)就是控制反转,我们将对象的控制权转移给Spring,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。

在IoC容器运行时,动态地为某个对象提供它所需要的外部依赖,是通过 DI (Dependency Injection)依赖注入来实现的。而 Spring 的 DI 是通过反射实现的。

Spring的AOP理解

推荐文章:【SpringBoot-3】切面AOP实现权限校验:实例演示与注解全解

AOP(Aspect Oriented Programming)是面向切面编程,在切面(Aspect)将相应通知(Advice)织入(Weaving)切点(Pointcut)指定的连接点(Join point)处。主要是通过动态代理来实现的。通过AOP可以帮我们实现到像事务、权限、日志和安全等方便的扩展。

Spring基于xml依赖注入四种方式

推荐文章:Spring中bean的注入方式

  • 构造器注入

  • set方法注入

  • 静态工厂注入

  • 实例工厂注入

注意:一般答前两种

Spring的自动装配

推荐文章:Spring中的@Autowired自动装配

自动装配也就是 Spring 会在容器中自动的查找,并自动的给 bean 装配及其关联的属性

Spring 基于XML提供了 5 种自动装配策略:

  • no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
  • byName:通过bean的名称进行自动装配。
  • byType:通过参数的数据类型进行自动装配。
  • constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
  • autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用byType的方式自动装配。 Spring 3.0 之后已经被标记为 @Deprecated

Spring基于注解的自动装配方式:

使用@Autowired、@Resourc或者@Inject注解来自动装配指定的bean。

  • @Autowired默认是byType 装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。如果匹配到类型的多个实例,再通过 byName 来确定 bean。可以使用@Qualifier来指定注入哪个beanName的bean。

  • @Resource默认是 byName 来装配注入的,只有当找不到与名称匹配的bean才会byType 来装配注入。

  • @Inject也是byType 来查找bean注入的,如果需要指定名称beanName,则可以结合使用@Named注解,而@Autowired是结合@Qualifier注解来指定名称beanName。

Spring中的通知类型

  • Before(前置通知):@Before 指定的方法在切面切入目标方法之前执行
  • After(后置通知): @After 指定的方法在切面切入目标方法之后执行
  • After Retuning(返回通知): @AfterReturning 可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理
  • After Throwing(异常通知): 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 的方法中执行,在该方法中可以做一些异常的处理逻辑
  • Around(环绕通知):如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用@Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用@Around增强处理了。

执行顺序:

不抛异常:@Around @Before @AfterReturning @After @Around

抛异常:@Around @Before @AfterThrowing @After

bean的作用域

推荐文章:Spring Bean的作用域

  • singleton:在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的默认值。
  • prototype:每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。
  • request:每次HTTP请求都会创建一个新的Bean,该作用域仅适用于web的Spring WebApplicationContext环境。
  • session:同一个HTTP Session共享一个Bean,不同Session使用不同的Bean。该作用域仅适用于web的Spring WebApplicationContext环境。
  • application:限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。

bean的生命周期

推荐文章:可以先参考Spring 了解Bean的一生(生命周期),再参考Spring的Bean加载流程

(1)实例化Bean:

  • 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

  • 对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息以及通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

  • 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
  • 如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
  • 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;

(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

循环依赖

推荐文章:Spring如何解决循环依赖Spring-bean的循环依赖以及解决方式

什么情况下循环依赖可以被处理?

  • 出现循环依赖的Bean必须要是单例(singleton),如果依赖prototype则完全不会有此需求。
  • 依赖注入的方式不能全是构造器注入的方式(只能解决setter方法的循环依赖,这是错误的)

Spring如何解决循环依赖的

Spring通过三级缓存解决了循环依赖,其中一级缓存为singletonObjects,二级缓存为earlySingletonObjects,三级缓存为singletonFactories

当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。

当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取:

  1. 先获取到三级缓存中的工厂;

  2. 调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。

  3. 当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

为什么要使用三级缓存呢?二级缓存能解决循环依赖吗

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

SpringMVC

SpringMVC 的执行流程

  1. 用户发送请求至前端控制器DispatcherServlet
  2. 前端控制器收到请求后,调用处理器映射器HandlerMapping,请求获取Handler处理器(也可以称为controller);
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给前端控制器DispatcherServlet;
  4. 前端控制器通过处理器适配器HandlerAdapter调用处理器Handler;
  5. 处理器适配器执行处理器;
  6. 处理器执行完成给适配器返回ModelAndView;
  7. 处理器适配器将ModelAndView返回给前端控制器;
  8. 前端控制器将ModelAndView传给视图解析器ViewReslover进行解析;
  9. 视图解析器解析后返回具体View;
  10. 前端控制器对View进行渲染视图(即将模型数据填充至视图中)
  11. 前端控制器向用户响应结果。

Mybatis

什么是SqlSession

SqlSession是mybatis框架中的一个对象,类似JDBC的Connection对象,是Java程序端和数据库之间的会话。
框架底层通过sqlSession对象去执行sql语句,帮助我们实现增删改查操作

#{}和${}的区别

#{}底层采用的是?占位符赋值的方式,调用PreparedStatement的set方法来赋值,可以对SQL语句预编译

${}底层采用的是字符串拼接sql的方式,容易产生sql注入,如果需要一些字段名或者关键字(比如order by)可以用${}

MyBatis的缓存机制

作用:减少web应用和数据库(磁盘)访问次数,提高查询效率,减轻数据库访问压力。

一级缓存(默认级别):

SqlSession级别的缓存,作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句(包括参数相同),第一次执行完毕会将数据库中查询结果写到缓存,第二次会直接从缓存中获取数据,从而提高查询效率。

当SqlSession执行了DML操作(增删改),并且提交到数据库,MyBatis则会清空SqlSession中的一级缓存,避免出现脏读。当一个 SqlSession结束后,该SqlSession中的一级缓存也就不存在了。

二级缓存:

mapper级别的缓存,其作用域是mapper的同一个namespace,两次执行相同namespace下的sql语句(包括参数相同),第一次执行完毕会将数据库中查询结果写到缓存,第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。

mapper文件中增加标签,实体类实现序列化。
当执行了增删改操作,清空当前sql所对应的namespace的缓存。

MyBatis和Mybatis-Plus的区别

Mybatis-Plus是无侵入的,只对Mybatis做了增强不做改变。

  • 内置通用 Mapper、Service,仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求

  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错。

  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。

  • 内置代码生成器、内置分页插件

  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。

  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。

Mysql

数据库三大范式

推荐文章:数据库三大范式是什么?(3NF详解)

第一范式:属性不可分割

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

脏读、不可重复读、幻读

推荐文章:数据库事务隔离级别(脏读、幻读、不可重复读)

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

事务的隔离级别

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED(读未提交)
READ-COMMITTED(读已提交) ×
REPEATABLE-READ(可重复读) × ×
SERIALIZABLE(可串行化) × × ×

MyISAM与InnoDB区别

https://blog.csdn.net/ThinkWon/article/details/104778621

MyISAM Innodb
文件格式 数据和索引是分别存储的,数据.MYD,索引.MYI 数据和索引是集中存储的.ibd
记录存储顺序 按记录插入顺序保存 按主键大小有序插入
外键 不支持 支持
事务 不支持 支持
SELECT、INSERT、UPDATE、DELETE SELECT更优 INSERT、UPDATE、DELETE更优
表的行数 存储 不存储
锁支持 表锁 行锁、表锁
索引 非聚簇索引 聚簇索引

索引类型

按数据的物理存储分类,可分为聚簇索引和非聚簇索引

按索引字段特性分类,可分主键索引、普通索引、唯一索引、全文索引

Reidis

缓存穿透

缓存穿透指一个一定不存在的数据,由于缓存未命中这条数据,就会去查询数据库,数据库也没有这条数据,所以返回结果是 null。如果并发量大且缓存都没有命中,每次查询都请求数据库时,缓存就失去了保护后端持久层的意义,这会给持久层数据库造成很大的压力。

解决方案:

  • 缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null),缓存空对象会有两个问题:
  • value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  • 缓存层和持久层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和持久层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
  • 布隆过滤器拦截
  • 在访问缓存层和持久层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在再进入缓存层、持久层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
  • 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

缓存击穿

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,数据库有数据并且回写缓存,会导使数据库瞬间压力过大。

解决方案:

  • 分布式互斥锁,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  • 这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
  • 热点Key永不过期,从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存。
  • 这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

缓存雪崩

由于缓存层承载着大量请求,有效地保护了持久层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。

解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。
  • 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底。
  • 数据加热,在正式部署之前先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。

过期的数据的删除策略

如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?

常用的过期数据的删除策略就两个:

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

  • 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。

Redis 内存淘汰机制

Redis一开始有6种数据淘汰策略,4.0版本后增加2种(7和8),总共8种

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
  7. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  8. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中移除最不经常使用的 key