JAVA基础

java字符串常量池

intern()方法

源码

image-20210429103211694

解释
  • 方法区和运行时常量池溢出
    • 由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行,HotSpot从JDK7开始逐步 去永久代 计划,并在JDK8中完全使用元空间来替代永久代的背景故事,在此我们就以测试代码来观察一下,使用永久代还是元空间来实现方法区,对程序有什么实际影响
    • String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串(首次遇到的字符串实例),则返回代表池中这个字符串的String对象的引用,在JDK6或者更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过 -XX:PermSize和 -XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量
代码示例
  • 测试

    image-20210429131447187

why

测试结果
  • 按照代码结果,只要不是java字符串,其他58tongcheng meituan alibaba,返回的都是true;java字符串答案为false,必然是两个不同的java,那另外一个java字符串如何加载进来的?
  • 为什么?
    • sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时,它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时sun.misc.Version.launcher静态常量字段所引用的java字符串字面量就被intern到HotSpot VM的字符串常量池 — StringTable里了
    • 这段代码在JDK 6中运行,会得到两个false,而在JDK7中运行,会的带一个true和一个false。产生差异的原因是,JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在java堆上,所以不然不可能是同一个引用,结果将返回false
    • 而JDK7(及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要在拷贝字符串的实例到永久代,既然字符串常量池已经移到Java堆中,拿只需要在常量池中记录一下首次出现的实例引用即可,因此intern()返回的引用和有StringBuilder创建的那个字符串实例就是同一个,而对str2比较返回 false,这是因为java这个字符串在执行StringBuilder.tostring()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求的首次遇到原则
  • 再来看看方法区的其他部分的内容,方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这部分区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止,虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等)

OpenJDK8底层源码说明

递推步骤
1、System代码解析
  • SysteminitializeSystemClassVersion

    image-20210429160407778

    image-20210429160455431

    image-20210429160543797

2、类加载器和rt.jar
  • 根加载器提前部署加载rt.jar

    image-20210506135114798

3、OpenJDK8源码
  • 加载文件位置

    image-20210506141847194

    动态获取 version 类中配置的launcher_name的值 :javaimage-20210506141942382

总结

考查点

  • 1、字符串intern()方法,判断 true/flase?
  • 2、《深入理解java虚拟机》书中原题,是否读过经典JVM书籍(周志明 著)

LeetCode两数之和

  • 题目详情

    image-20210506155859604

  • 算法复杂度

    • 最优:O(1) ,只找一次,参照redis的k-v键值对
    • 其次:O(log2N)
    • 再次:O(N)
    • 差:O(N^2)
  • 哈希 k 值 v 下标

JUC

常见juc相关题目

  • 1、Synchronized相关问题
    • Synchronized用过吗,其原理是什么?
    • 你刚刚提到获取对象的锁,这个锁到底是什么?如何确定对象的锁
    • 什么是可重入性,为什么说Synchronized是可重入锁?
    • JVM对Java的原生锁做了哪些优化?
    • 为什么说Synchronized是非公平锁?
    • 什么是锁消除和锁粗化?
    • 为什么说Synchronized是悲观锁?乐观锁的实现原理是什么?什么是CAS?
    • 乐观锁一点就是好的吗?
  • 2、可重入锁ReentrantLock以及其他显式锁相关问题
    • 跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?
    • 那么请谈谈AQS框架是怎么回事?
    • 请尽可能详尽的对比下Synchronized和ReentrantLock的异同
    • ReentrantLock是如何实现可重入性的?

可重入锁

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象,不会因为之前已经获取过还没释放而阻塞)
  • Java中ReentrantLockSynchronized都是可重入锁,可重入锁的一个优点就是可一定程度的避免死锁

可重入锁解释

  • 拆开解释
    • 可:可以
    • 重:再次
    • 入:进入
    • 锁:同步锁
    • 进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
  • 一句话解释:
    • 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入
    • 自己可以获取自己的内部锁

可重入锁种类

隐式锁(即Synchronized关键字使用的锁)默认是可重入锁
  • 同步块

  • 同步方法

  • 可重入锁的代码验证(同步代码块/同步方法)

    image-20210508090244154

    image-20210508090319719

Synchronized的重入的实现机制
  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

    当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1

    在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁

    当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1,计数器为零代表锁已被释放

显示锁(即 Lock )也有ReentrantLock这样的可重入锁
  • ReentrantLock代码验证

    image-20210508092309571

  • 进一步验证锁的释放(一定要两两匹配,加一次锁就需要释放一次锁)

    image-20210508093806368

LockSupport

为什么要学习LockSupport?

  • Java — JVM

  • JUC — AQS — 前置知识(可重入锁、LockSupport)

  • AB — after | before

    image-20210508111732214

LockSupport是什么

  • 官方文档

  • 用于创建锁和其他同步类的基本线程阻塞原语

    image-20210508095951833

    一句话总结LockSupport线程等待唤醒机制(wait/notify的改良加强版)

LockSupport中的park()unpark()的作用分别是阻塞线程解除阻塞线程

线程等待唤醒机制(wait/notify)

3种让线程等待和唤醒的方法
  • 方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  • 方式二:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • 方式三:LockSupport类的park()可以阻塞当前线程以及unpark()唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
  • 代码演示

    • 1、正常情况

      image-20210508135625479

    • 2、异常1:waitnotify去掉Synchronized关键字修饰,会报错

      image-20210508160616489

    • 3、异常2:将notify放在wait方法前面,程序一直无法结束,无法唤醒

  • 小总结

    • waitnotify方法必须要在同步块或者方法里面且成对出现使用
    • waitnotify才有效
Condition接口中的await后signal方法实现线程的等待和唤醒
  • 代码演示

    • 1、正常情况

      image-20210508164735271

    • 2、异常1:不加lockunlock锁操作,会报错

      image-20210508164845810

    • 3、异常2:先唤醒后等待,程序无法完成

      image-20210508165205232

传统的synchronized和Lock实现等待唤醒通知的约束(*)
  • 线程必须先要获得并持有锁,必须在锁块(Synchronized或Lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
是什么
  • 通过park()unpark(thread)的方法来实现线程的阻塞和唤醒操作

    • park():除非许可证可用,否则禁用当前线程以镜像线程调度
    • unpark(Thread thread):如果给定线程尚不可用,则为其提供许可
  • LockSupport类使用了一种名为Permit许可)的概念来做的阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0

    可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1

主要方法
  • 阻塞

    • park()/park(Object blocker)

    • 阻塞当前线程/阻塞传入的具体线程

      image-20210508172255268

      permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回

  • 唤醒

    • unpark(Thread thread)
    • 唤醒处于阻塞状态的指点线程
代码演示
  • 1、正常+无锁块要求

    image-20210510095350465

  • 2、之前错误的先唤醒后等待,LockSupport照样支持(*)

    image-20210510095817226

重点说明(*)

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

    LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,归根结底,LockSupport调用的Unsafe中的native代码

  • LockSupport提供park()unpark()方法实现阻塞线程和解除线程阻塞的过程

    LockSupport和每个使用它的线程都有一个许可(permit)关联,permit相当于1,0的开关,默认是0

    调用一次unpark就加1变成1

    调用一次park会消费permit,也就是将1变成0,同时park立即返回

    如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这是调用unpark会把permit设置为1

    每个线程都有一个相关的permitpermit最多只有一个,重复调用unpark也不会积累凭证

  • 形象的理解

    线程阻塞需要消耗凭证(permit),这个凭证最多只有1个

    • 当调用park方法时

      如果有凭证,则会直接消耗掉这个凭证然后正常退出

      如果无凭证,就必须阻塞等待凭证可用

    • unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效

常见题目

1、为什么可以先唤醒线程后阻塞线程?
  • 答:因为unpark获得一个凭证(permit加1变成1),之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞
2、为什么唤醒两次后阻塞两次,但最终结果还是会线程阻塞?
  • 答:因为同一个线程的凭证数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而两次调用park却需要消耗两个凭证,凭证数量不够,不能放行,故线程阻塞

AbstractQueuedSynchronizer之AQS

常见题目

  • 题目总结

    image-20210511134924529

前置知识

CAS(CompareAndSwap)
  • 比较并替换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

  • 目的:利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的;而整个JUC都是建立在CAS之上的,因此对于synchronized阻塞算法,JUC在性能上有了很大的提升

  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁

公平锁非公平锁
  • 公平锁(FairSync):多个线程按照申请锁的顺序去获取锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

    • 优点:所有的线程都能得到资源,不会饿死在队列中

    • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程开销会很大

    • 公平锁的创建

      final ReentrantLock lock = new ReentrantLock(true);
      复制代码
  • 非公平锁(NonfairSync):多个线程去获取锁的时候,会**直接去尝试获取锁,**获取不到,再去进入队列,如果能获取到,就直接获取锁

    • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤醒线程的数量

    • 缺点:你们可能也发现了,这样可能导致队列中间的线程一致获取不到锁或者长时间获取不到锁,导致饿死

    • 非公平锁的创建

      final ReentrantLock lock = new ReentrantLock();
      final ReentrantLock lock = new ReentrantLock(false);
      复制代码
可重入锁
LockSupport
自旋锁
  • 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
  • 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting
数据结构之链表
设计模式之模板设计模式

是什么

字面意思
  • 抽象的队列同步器

  • 源代码

    image-20210511140439836

技术解释

  • 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态

    image-20210511150253064

AQS为什么是JUC内容中最重要的基石

和AQS有关的
  • ReentrantLock

    image-20210511151916183

  • CountDownLatch

    image-20210511152031353

  • ReentrantReadWriteLock

    image-20210511152148627

  • Semaphore

    image-20210511152221718

  • 等等

    image-20210511152313653

    image-20210511152334029

进一步理解锁和同步器的关系
  • 锁,面向锁的使用者:定义了开发工程师和锁交互的使用层API,隐藏了实现细节,调用即可
  • 同步器,面向锁的实现者:比如Java并发大神Dougee,提出了统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等待

能干嘛

加锁会导致阻塞
  • 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
解释说明
  • 抢到资源的线程之间使用处理业务逻辑,抢不到资源的必然涉及到一致排队等候机制,抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
  • 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
  • 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node,通过CAS、自旋以及ReentrantLock.park()等方式,维护state变量的状态,是并发达到同步的控制效果

AQS初步

AQS初识
  • 官方解释

    image-20210511165150220

  • 有阻塞就需要排毒,实现排队必然需要队列

    • AQS使用一个volatileint类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对State值的修改

      image-20210511165930501

AQS内部体系架构
  • 体系架构图

    image-20210512104844185

AQS自身
  • AQS的int变量

    • AQS的同步状态State成员变量

       /**
       * The synchronization state.
       */
       private volatile int state;
      复制代码
    • 类似银行办理业务的受理窗口状态

      • 等于0 :就是没人,自由状态可以办理
      • 大于等于1:有人占用窗口,排队等着去
  • AQS的CLH队列

    • CLH队列(三个大牛的名字组成),是一个双向队列

    • 类比银行候客区的等待客户

      image-20210512112403893

  • 小总结

    • 有阻塞就需要排队,实现排队必然需要队列
    • state变量+CLH变种的双端队列
内部类Node(Node类在AQS类的内部)
  • Node的int变量

    • Node的等待状态waitState成员变量:volatile int waitStatus

    • 等候区的其他客户(没有抢到锁资源的线程)的等待状态

      队列中每个排队的个体就是一个Node

  • Node此类的讲解

    • 内部结构

        static final class Node {
              // 共享
              static final Node SHARED = new Node();
              // 独占
              static final Node EXCLUSIVE = null;
              // 线程被取消了
              static final int CANCELLED =  1;
              // 后继线程需要被唤醒
              static final int SIGNAL    = -1;
              // 等待condition唤醒
              static final int CONDITION = -2;
              // 共享式同步状态获取将无条件的传播下去
              static final int PROPAGATE = -3;
              // 初始为0,状态是上面的几种
              volatile int waitStatus;
              // 前置结点
              volatile Node prev;
      		// 
              volatile Node next;
      		//
              volatile Thread thread;
            	//
              Node nextWaiter;
      复制代码

      image-20210512134825276

    • 属性说明

      image-20210512134929287

      image-20210512135016248

AQS同步队列的基本结构
  • 同步器

    image-20210512135327786

从ReentrantLock开始解读AQS

Lock接口的实现类
  • 基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReenteantLock的原理
  • ReentrantLock架构图

    image-20210512140036105

从最简单的lock方法开始看看公平非公平
  • 可以明显的看出公平锁与非公平锁的lock()方法唯一区别就在于公平锁在获取同步状态时,多了一个限制条件:hasQueuedPredecessors()

    hasQueuedPredecessors()方法是公平锁加锁时判断等待队列中是否存在有效结点的方法

    image-20210512141901211

采用最常用的lock/unlock作为案例突破口
  • 公平锁和非公平锁的差异

    • 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程再等待,那么当前线程就会进入等待队列中
    • 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占用锁,也就是说队列的第一个排队现在在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

    image-20210512144612142

    image-20210512153213878

AQS源码分析走起
  • 1、lock()

    image-20210521085434359

  • 2、acquire()

    image-20210521111707072

  • 3、tryAcruire(arg)

    • nonfairTryAcquire(acquires)
      • return true:结束
      • return false :继续推进条件,走下一个方法addWaiter

    image-20210521092205136

  • 4、addWaiter(Node.EXCLUSIVE)

    • addWaiter(Node mode) B线程
      • enq(node)
      • 双向链表中,第一个结点为虚结点(也叫做哨兵结点),其实并不存储任何信息,只是占位;真正的第一个有数据的结点,是从第二个结点开始的
    • 假如3号ThreadC线程进来
      • prev
      • compareAndSetTail
      • next

    image-20210521093423918

    B线程抢占锁资源

    image-20210521110621017

    image-20210521110647815

    C线程抢占锁资源

    image-20210521132107825

    image-20210521132145409

  • 实际问题:AQS的抢占锁

    • 1、第一个线程抢占到锁了,第二个线程是否应该进入队列等待?

      答:是的

    • 2、再问等候队列中的第一个结点,是否就是当前需进入等待的B结点?

      答:不是的,第一个结点是哨兵(傀儡结点),负责通知唤醒,出队等

  • 5、acquireQueued(addWaiter(Node.EXCLUSIVE),arg)

    • 假如再抢抢占失败就会进入

      • shouldParkAfterFailedAcquire:如果前驱结点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true;程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起

        image-20210521145536118

        image-20210521145623315

      • parkAndCheckInterrupt

        image-20210521150230427

      image-20210521143606487

      image-20210521144625851

      image-20210521144803013

  • 6、unlock()方法

    • sync.release(1);

      image-20210521150520548

      image-20210521150742067

      唤醒并出队

      image-20210521150821409

      原来的哨兵结点出队,B线程变成新的哨兵结点

      image-20210521152112220

      image-20210521152151050

三大流程走向
  • AQS acquire 主要有三大流程走向

    image-20210521134603077

AQS小总结

  • 抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍然在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)

    既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

    如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现,它将请求共享的线程封装成队列的结点(Node)通过CAS、自旋以及LockSupport.park()的方式维护state变量的状态,是并发达到同步的控制效果

    image-20210521153334892

    image-20210521153457561

  • 重点流程说明

    • addWaiter:将当前线程封装成Node对象,并加入排队队列中,根据排队队列是否执行过初始化,执行1、2不同处理逻辑

      • 1、表示排队队列不为空,即之前已经初始化过了,此时只需将新的node加入排队队列末尾即可

      • 2、表示排队队列为空,需执行队列初始化,enq会初始化一个空的Node,作为排队队列的head,然后将需要排队的线程,作为head的next节点插入

        队列尚未初始化,调用enq方法;该方法生成一个空的Node对象(new Node()),插入到aqs队列头部,然后将参数node,作为其后继节点,插入队列

      image-20210521155436199

  • 整个aqs的核心和难点之一:

    • acquireQueuedshouldParkAfterFailedAcquire

      • 注意这里使用的for(;;)自旋

      • 首先判断node的前辈结点,是不是head,如果是,说明它是下一个可以获得锁的线程,则调用一次tryAcquire,尝试获取锁,若获取到,则将链表关系重新维护下(node设置为head,之前的head从链表移除),然后返回

      • 如果node的前辈结点不是head,或获取锁失败,再判断其前辈结点的waitState,是不是SIGNAL,如果是,则当前线程调用park,进入阻塞状态,如果不是

        • 1、==0,则设置为SIGNAL;
        • 2、>0(==1),则表示前辈结点已经被取消了,将取消的节点,从队列移出,重新维护下排队链表关系

        然后再次进入for循环,上面的逻辑重新执行一遍,注意和doAcquireInterruptibly方法对比,二者区别主要在,发现线程被中断过之后的逻辑处理

    • parkAndCheckInterrupt

      • 两种情况会导致阻塞结束:
        • 1、持有锁的线程,释放锁后,将这个线程unpark了,此时该线程,一定排在队列的队头(不包括head节点)

        • 2、线程被interrupt了;(注意,在外部interrupt这个线程,不是抛出InterruptException,这一点和sleep、wait阻塞不一样)

Spring

Spring的aop顺序

Aop常用注解

  • @Before:前置通知:目标方法之前执行
  • @After:后置通知:目标方法之后执行(始终执行)
  • @AfterReturning:返回后通知:执行方法结束前执行(异常不执行)
  • @AfterThrowing:异常通知:出现异常时候执行
  • @Around:环绕通知:环绕目标方法执行

常见题问

问题1:AOP的全部通知执行顺序
  • 1、Spring4转到Spring5,AOP的全部通知执行顺序; Spring4、5对于AOP的执行顺序是不同的,Springboot1转到Springboot2,其实就是Spring4转到Spring5

  • 代码演示

    • 想在除法方法前后置入各种通知,引入切面编程
    • 新建一个切面类MyAspect,并为切面类新增两个注解
      • @Aspect:指定一个类为切面类
      • @Component:纳入spring容器管理
问题2:AOP常见使用问题
  • 2、使用AOP过程中有哪些坑,你遇到过的?

Spring4+springboot1.5.9演示

  • 添加依赖,防止启动报错

    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-core</artifactId>
    	<version>1.1.3</version>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-access</artifactId>
    	<version>1.1.3</version>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>1.1.3</version>
    </dependency>
    复制代码
  • 切换springboot版本为1.5.9

  • 运行结果

    • 正常运行结果

      image-20210601081958929

    • 运行异常时

      image-20210601082221163

Spring4下Aop正常顺序+异常顺序
  • 正常执行:@Before(前置通知)—> @After(后置通知)—> @AfterReturning(正常返回)
  • 异常执行:@Before(前置通知)—> @After(后置通知)—> @AfterThrowing(方法异常)

Spring5+springboot2.3.3演示

  • 运行结果

    • 正常时

      image-20210601083503100

    • 异常时

      image-20210601083541274

Spring5下Aop正常顺序+异常顺序
  • 正常执行:@Before(前置通知)—> @AfterReturning(正常返回)—> @After(后置通知)
  • 异常执行:@Before(前置通知)—> @AfterThrowing(异常返回) —> @After(后置通知)

Spring循环依赖

常见提问

  • 解释下spring中的三级缓存
  • 三级缓存分别是什么?三个Map有什么异同
  • 什么是循环依赖?看过spring源码吗?一般说的spring容器是什么?
  • 如何检测是否存在循环依赖?日常开发中是否见过循环依赖的异常?
  • 多例的情况下,循环依赖问题为什么无法解决?

什么是循环依赖

  • 多个bean之间相互依赖,形成了一个闭环;比如:A依赖于B、B依赖于C、C依赖于A

    image-20210601085528459

  • 通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性互相引用的场景

    image-20210601085647848

两种注入方式对循环依赖的影响

循环依赖官网说明
总结
  • 我们AB循环依赖问题只要A的注入方式是settersingleton(单例),就不会有循环依赖问题

循环依赖报错演示-BeanCurrentlyInCreationException

循环依赖现象在Spring容器中,两种注入方式
构造器方式注入依赖
  • 代码演示

    • ServiceA

      image-20210601092851147

    • ServiceB

      image-20210601092906057

    • ClientConstructor

      image-20210601092929253

  • 结论构造器循环依赖式无法解决的,你想让构造器注入支持循环依赖,是不存在的

以set方法注入依赖
  • 代码演示

    • ServiceAA

      image-20210601094552369

    • ServiceBB

      image-20210601094605467

    • ClientSet

      image-20210601094635005

重要Code案例演示
基础代码
  • A

    image-20210601110237239

  • B

    image-20210601110251115

  • ClientCode

    image-20210601110303414

  • ClientSpringContainer

    image-20210601110315815

加入spring容器
  • 步骤

    • applicationContext.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
                         http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                         http://www.springframework.org/schema/context
                         http://www.springframework.org/schema/context/spring-context-4.0.xsd  ">
      
          <bean id="a" class="com.touch.air.basic.spring.circulardepend.A">
              <property name="b" ref="b"></property>
          </bean>
          <bean id="b" class="com.touch.air.basic.spring.circulardepend.B">
              <property name="a" ref="a"></property>
          </bean>
      </beans>
      复制代码

      正常运行image-20210601105817692

    • 默认单例,修改为原型 scope="prototype"

      image-20210601105849222

      循环依赖报错image-20210601110646623

    • ClientSpringContainer

    • 循环依赖异常

  • 结论

    • 默认的单例(singleton)的场景是支持循环依赖的,不报错
    • 原型(Prototype)的场景是不支持循环依赖的,会报错
重要结论(Spring内部通过3级缓存来解决循环依赖)
  • DefaultSingletonBeanRegistry

    image-20210601113134574

    • 第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
    • 第二级缓存:earlySingletonObjects,存放早期暴露出来的bean对象,bean的生命周期未结束(属性还未填充完)
    • 第三级缓存:singletonFactories,存放可以生成Bean的工厂
  • 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,如果是非单例的bean,每次从容器中获取都是一个全新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中

    image-20210601131314176

深度Debug循环依赖

前置知识
  • 实例化/初始化

    • 实例化:内存中申请一块内存空间(租好房子,自己的家具还没有搬进去)
    • 初始化属性填充:完成属性的各种赋值(装修、家电家具进场)
  • 3个Map和四大方法,总体相关对象

    image-20210601132509479

    getSingleton:获取单例对象

    doCreateBean:获取不到,则创建

    populateBean:创建过程中,挨个填充属性

    addSingleton: 最终放入到单例池中,也就是一级缓存

    第一层singletonObjects存放的是已经初始化好了的Beanimage-20210601132928241

    第二层earlySingletonObjects存放的是实例化了,但未初始化的Beanimage-20210601133003113

    第三层singletonFactories存放的是FactoryBean,加入A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Beanimage-20210601133058295

A/B对象在三级缓存中的迁移说明
  • 1、A创建的过程中需要B,于是A将自己放到三级缓存里面,去实例化B
  • 2、B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
  • 3、B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面

总结Spring是如何解决的循环依赖

  • 整体Debug,深入体验Spring解决循环依赖的过程

    image-20210602085303150

    • 1、调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
    • 2、在getSingleton()方法中,从一级缓存中查找,没有,返回null
    • 3、doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
    • 4、在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中,然后回调匿名内部类的createBean方法
    • 5、进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断;是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中(即是否在第四步的集合中);判断为true则将beanA添加到三级缓存中
    • 6、对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
    • 7、调doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
    • 8、此时beanB依赖于beanA,调用getSingleton()获取beanA,依次从一级、二级、三级缓存中查找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面的doCreateBean()方法实例化的beanA
    • 9、这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
    • 10、随后beanA继续它的属性填充工作,此时也获取到了beanB,beanA也随之完成创建,回到getSingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存
  • 流程解释

    image-20210602090528632

    • 1、Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化

    • 2、每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个

    • 3、当我们创建beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完beanB填充属性时又发现它依赖了beanA又是同样的流程

      不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建

    • 4、既然B创建完毕,beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成

  • Spring解决循环依赖靠的是Bean的**“中间态”这个概念,而这个中间态指的是已经实例化但还没有初始化的状态(半成品);实例化的过程又是通过构造器创建的,如果A还没有创建出来怎么可能提前曝光,所以构造器的循环依赖无法解决**

    Spring为了解决单例的循环依赖问题,使用了三级缓存

    • 一级缓存为单例池(singletonObjects
    • 二级缓存为提前曝光对象(earlySingletonObjects
    • 三级缓存为提前曝光对象工厂(singletonFactories

    假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性的时候发现B自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果没有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程

Redis

redis传统五大数据类型的落地应用

八大类型

  • 1、String(字符串类型)
  • 2、Hash(散列类型)
  • 3、List(列表类型)
  • 4、Set(集合类型)
  • 5、SortedSet(有序集合类型,简称zset)
  • 6、Bitmap(位图)
  • 7、HyperLogLog(统计)
  • 8、GEO(地理

备注: redis命令不区分大小写,而key是区分大小写的;帮助命令:help @ 类型名词

String

最常用
  • set key value
  • get key
同时设置/获取多个值
  • MSET key value [key value ….]
  • MGET key [key ….]
数值增减
  • 递增数字:INCR key
  • 增加指定的整数:INCRBY key increment
  • 递减数值:DECR key
  • 减少指定的整数:DECRBY key decrement
获取字符串长度
  • STRLEN key
分布式锁SETNX
  • setnx key value
  • set key value[EX seconds] [PX milliseconds] [NX|XX]
    • EX:key在多少秒之后过期
    • PX:key在多少毫秒之后过期
    • NX:当key不存在的时候,才创建key,效果等同于setnx
    • XX:当key存在的时候,覆盖key
应用场景
  • 商品编号、订单号采用INCR命令生成,是否喜欢的商品

    image-20210602132410890

  • 是否喜欢的文章:阅读数,只要点击了rest地址,直接可以使用incr key 命令增加一个数字1,完成数字记录

    image-20210602132556425

hash

  • 对应java中的数据结构:Map<String,Map<Object,Object>>
一次设置一个字段值
  • HSET key field value
一次获取一个字段值
  • HGET key field
一次设置多个字段值
  • HMSET key field value [field value ….]
一次获取多个字段值
  • HMGET key field [field ….]
获取所有字段值
  • hgetall key
获取某个key内的全部数量
  • hlen
删除一个key
  • hdel
应用场景
  • 简易版购物车

    image-20210602134122330

    image-20210602134150345

list
向列表左边添加元素
  • LPUSH key value [value ….]

    image-20210602134612326

向列表右边添加元素
  • RPUSH key value [value ….]
查看列表
  • LRANGE key start stop
获取列表中元素的个数
  • LLEN key
应用场景
  • 微信订阅公众号:

    1、例如订阅了掘金和CSDN的公众号,然后它们分别发布了文章id 11 和 22

    2、只要它们发布了新文章,就会装进我的List:lpush likeArticle:yy1024 11 22

    3、查看自己的订阅号全部文章,类似分页:lrange likeArticle:yy1024 0 10

    image-20210602135612175

    image-20210602135305757

set

添加元素
  • SADD key member [member …]
删除元素
  • SREM key member [memeber …]
获取集合中的所有元素
  • SMEMBERS key
判断元素是否在集合中
  • SISMEMBER key member
获取集合中的元素个数
  • SCARD key
从集合中随机弹出一个元素,元素不删除
  • SRANDMEMBER key [数字]
从元素中随机弹出一个元素,出一个删一个
  • SPOP key [数字]
集合运算
  • 集合的差集运算A-B:属于A但不属于B的元素构成的集合

    SDIFF key [key]

  • 集合的交集运算A∩B:属于A同时也属于B的共同拥有的元素构成的集合

    SINTER key [key …]

  • 集合的并集运算A∪B:属于A或者属于B的元素合并后的集合

    SUNION key [key …]

应用场景
  • 1、微信抽奖小程序(SRANDMEMBER、SPOP)

    image-20210602143240955

  • 2、微信朋友圈点赞

    image-20210602143448587

  • 3、微博好友关注社交关系(集合运算)

    • 共同关注的人(交集)

      image-20210602143708859

    • 我关注的人也关注他(大家爱好相同)

      image-20210602143901976

  • 4、QQ内推可能认识的人(差集)

    image-20210602143958068

zset

向有序集合中加入一个元素和该元素的分数
添加元素
  • ZADD key score member [score memeber]
排序,返回索引之间的所有元素
  • ZRANGE key start stop [WITHSCORES]
获取元素的分数
  • ZSCORE key memeber
删除元素
  • ZREM key memebr [memebr …]
获取指定分数范围的元素
  • ZRANGEBYSCORE key min max [WITHSCORES][LIMIT OFFSET COUNT ]
增加某个元素的分数
  • ZINCRBY key increment member
获取集合中元素的数量
  • ZCARD key
获得指定分数范围内的元素个数
  • ZCOUNT key min max
按照排名范围删除元素
  • ZREMRANGEBYRANK key start stop
获取元素的排名
  • 从小到大:ZRANK key memebr
  • 从大到小:ZREVRANK key member
应用场景
  • 根据商品销售对商品进行排序显示

    image-20210602145433966

  • 抖音热搜

    image-20210602145705467

分布式锁,为何选择redis分布式锁

  • 分布式微服务架构,拆分后各个微服务之间为了避免冲突和数据故障而加入的一种锁,分布式锁
  • redis分布式锁实现
    • 自己手写简单版本:string -- setnx + lua脚本
    • 集群模式下:redisson

常见使用场景:防止超卖

常见提问

  • 1、Redis除了拿来做缓存,还有哪些用法?
  • 2、Redis做分布式锁的时候需要注意什么问题?
  • 3、如果Redis是单点部署的,会带来什么问题?怎么解决这些单点问题?
  • 4、集群模式下,比如主从模式,有没有什么问题呢?
  • 5、简单介绍下RedisLock?谈谈redisson?
  • 6、Redis分布式锁如何续期?看门狗知道吗?

Base案例(Springboot+Redis)

使用场景
  • 多个服务间+保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
代码实现
  • 1、建Module:boot_redis01、boot_redis02

  • 2、改POM

  • 3、写YML

  • 4、主启动

  • 5、业务代码

    image-20210603094235744

存在的问题

1、单机版没加锁
问题
  • 没有加锁,并发请求下数字肯定不对,会出现超卖现象
思考
  • 加synchronized?

    • 不释放,就一直等待
  • 加ReentrantLock?

    • tryLock(),设置过期时间

      image-20210603104813290

  • 还是都可以?

    • 根据具体业务需求,选择合适的
解决
  • 修改为2.0版

    image-20210603110142767

2、nginx分布式微服务架构
问题
  • 2.1、分布式部署后,单机锁还是会出现超卖现象,需要分布式锁

  • 2.2、Nginx配置负载均衡

    image-20210603172854373

    本地host文件image-20210603134326771

  • 2.3、启动两个微服务

    image-20210603134355847

  • 2.4、模拟高并发(jmeter 压测)

    image-20210603140009982

    image-20210603140032596

    image-20210603135931336

解决
  • 上redis的分布式锁 setnx

    image-20210603140532741

    image-20210603141749776

3、始终释放锁(承接2)
问题
  • 如果出现异常,可能无法释放锁,必须要在代码层面finally释放锁
解决
  • image-20210603142406070
4、宕机了(承接3)
问题
  • 部署了微服务的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
解决
  • 需要对lockKey过期时间设定

    image-20210603143444037

5、key和过期时间的设置,并不具备原子性(承接4)
解决
  • 合并成一行,加锁并设置过期时间

    image-20210603144341084

6、误删锁
问题
  • 由于内部业务执行或远程调用超时,当前线程执行时间超过了设置的锁过期时间,导致A线程的锁,因为到过期时间,自动删除了,A线程的执行的代码finally,删除锁其实是删除了B线程的锁,删除了别人的锁

    image-20210603145830429

解决
  • 只能删除自己的锁,不许动别人的

    image-20210603150358098

7、误解锁
问题
  • finally块的判断+del删除操作不是原子性的

    image-20210603151140719

解决
  • 用Redis自身的事务

    • 事务介绍

      • Redis的事务是通过MULTI、EXEC、DISCARD和WATCH这四个命令来完成的

      • Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合

      • Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行

      • Redis不支持回滚的操作

    • 相关命令

      • MULTI:用于标记事务块的开始

        Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列

      • EXEC:在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态(类似于Commit)

      • DISCARD:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态

      • WATCH:当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态(类似乐观锁)

        WATCH key [key ….]

      image-20210603153616719

      image-20210603154813344

  • lua脚本(推荐)

    image-20210603160307296

8、过期时间怎么定
问题
  • 8.1、确保redisLock过期时间大于业务执行时间的问题
    • Redis锁如何续期
  • 8.2、集群+CAP对比zookeeper
    • 对比Zookeeper
    • CAP
      • Redis(集群) — AP :Redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来的这条数据给从节点,就宕机了
      • Zookeeper — CP
9、综上所述
  • Redis集群环境下,我们自己写的分布式锁也不ok,直接上RedLock(理念:redis分布式锁)之Redisson落地实现

    image-20210603170636376

    image-20210604083842908

    • Redisson提供的看门狗机制(WATCH DOG)会帮助锁自动续期

      Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放
      默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
      另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期

    • 结论

      • 1、watch dog 在当前节点存活时每10s给分布式锁的key续期 30s
      • 2、watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期
      • 3、从可2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中

    image-20210603172232054

  • 再次使用jemeter压测

    依据redis性能,调整线程参数,我本地虚拟机,0.4s 100个请求才能出现单机锁超卖

    使用redisson之后,基本解决超卖问题

    image-20210603173044384

  • 再次完善

    在更大级别的高并发下,简单的调用redissonlock()、unlock()方法,会出现如下报错:

    IllegalMonitorStateException:attempt to unlock lock,not locked by current thread by node id:0da6385f-81a5-4edaf9ewa(尝试去解锁,但当前线程和解锁的线程不是同一个)

    image-20210604082342265

Redis分布式锁总结

Synchronized单机版OK
分布式系统下
  • 1、nginx分布式微服务,单机锁不行
  • 2、取消单机锁,上redis分布式锁setnx
  • 3、只加了锁,没有释放锁
  • 4、出了异常,可能无法释放锁,必须要在代码层面finally释放锁
  • 5、宕机了,部署的微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockkey的过期时间设定
  • 6、为redis的分布式锁key,增加过期时间,此外setnx+过期时间必须同一行
  • 7、必须规定只能自己删除自己的锁,你不能把别人的锁删除了,A删B的,B删C的
  • 8、redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现

redis缓存过期淘汰策略

常见提问

  • 1、生产上你们的redis内存设置多少?
  • 2、如何配置、修改redis的内存大小
  • 3、如果内存满了你怎么办
  • 4、redis清理内存的方式?定期删除和惰性删除了解过吗?
  • 5、redis缓存淘汰策略
  • 6、redis的LRU了解过吗?可否手写一个LRU算法

Redis内存满了怎么办

Redis默认内存多少?在哪里查看?如何设置修改?
查看Redis最大占用内存

image-20210604102211354

redis默认内存多少可以用?
  • 如果不设置最大内存大小或者设置最大为0,在64位操作系统下不限制内存的大小,在32位操作系统下最多使用3GB
一般生产上你如何配置?
  • 一般推荐Redis设置内存为最大物理内存的四分之三
如何修改redis内存设置?
  • 1、通过配置文件修改

    image-20210604103559119

  • 2、通过命令修改

    config get maxmemory

    config set maxmemory 104857600

    image-20210604103857856

什么命令查看redis内存的使用情况?
  • info memory

    image-20210604104122631

超出最大值会怎样?(OOM)
  • 改改配置,故意把最大值设为1个byte试试

    image-20210604105037526

  • 这时候,再尝试set,发现报错:OOM

    image-20210604105123230

结论
  • 设置了maxmemory的选项,假如redis内存使用达到上限,没有加上过期时间,就会导致数据写满maxmemory,为了避免类似情况,引出了下一章内存淘汰策略

Redis缓存淘汰策略

  • 当前版本有八种缓存策略,如果未设置,默认使用noeviction

    image-20210604110358250

往redis里写的数据是怎么没了的
redis过期键的删除策略
  • image-20210604111346402
三种不同的删除策略
  • 定时删除:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)

    Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除

    立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放,但是立即删除对cpu的最不友好的,因为删除操作会占用cpu的时间,如果刚好碰到cpu很忙的时候,比如正在做交集或者排序等计算的时候,就会给cpu造成额外的压力

    这会产生大量的性能消耗,同时也会影响数据的读取操作

  • 惰性删除:对memory不友好,用存储空间换取处理器性能(拿空间换时间)

    数据到达过期时间,不做处理,等下次访问该数据时,如果未过期返回数据,已过期,删除,返回不存在

    缺点:对内存最不友好

    如果一个键已经过期,而这个键又仍然保留中,那么只要这个过期键不被删除,它所占用的内存就不会释放

    在使用惰性删除策略时,如果这个数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么他们占用的内存将永远不会释放(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看做是一种内存泄露

  • 上面两种方案都走极端 — 定期删除

    定期删除策略是前两种策略的折中:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

    image-20210604133834232

上述步骤都过堂了,还有漏洞吗?
  • 1、定期删除时,从来没有被抽查到

  • 2、惰性删除时,也从来没有被点中过

  • 上述2步骤 ===> 大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

    必须要有一个好更的兜底方案…

内存淘汰策略登场
淘汰策略有哪些
  • 8种策略

    • noeviction:不会驱逐任何key

    • allkeys-lru:对所有key使用LRU算法进行删除

    • volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除

    • allkeys-random:对所有key随机删除

    • volatile-random:对所有设置了过期时间的key随机删除

    • volatile-ttl:删除马上要过期的key

    • allkeys-lfu:对所有key使用LFU算法进行删除

    • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

  • 总结

    • LRU(Least Recently Used):最近最少使用
    • LFU(Least Frequently Used):使用频率最小
    • 两个维度
      • 过期键中筛选
      • 所有键中筛选
    • 四个方面
      • LRU
      • LFU
      • random
      • ttl
平时用哪一种?
  • allkeys-lru
如何配置、修改
  • 配置文件

    image-20210604142931880

  • 命令:config set maxmemory-policy

    image-20210604143131567

redis的LRU算法简介

Reids的LRU是什么

  • LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰

算法来源

  • leetcode

    image-20210604144743925

设计思想

  • 特性要求

    • 必须要有顺序之分,以区分最近使用的和很久没有使用的数据排序
    • 写和读操作一次搞定,时间复杂度O(1)
    • 如果容量(坑位)满了要删除最不常用的数据,每次新访问还要把新的数据插入到队头
    • 查找快、插入快、删除快,且还需要先后排序
  • LRU的算法核心是:哈希链表

    本质就是HashMap+DoubleLinkedList,时间复杂度是O(1),哈希表+双向链表的结合体

手写编码如何实现LRU

案例1
  • 参考LinkedHashMap,依赖JDK

    image-20210604153703712

    image-20210604155548583

案例2
  • 不依赖JDK

    image-20210604172620828

    image-20210604172656293

    image-20210604172749967

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享