阅读 54

面试要点

1 多线程

1.1 同步和异步

参考文献(啃碎并发(二):Java线程的生命周期

  • 区别比较:

    同步:执行一个方法,该方法执行完才能执行下面的代码

    异步:执行一个方法,不需要执行完该方法就能继续执行下面的代码。可以防止某些方法出现异常影响程序的正常执行。

1.2 多线程状态

  • join,wait,notify,park,sleep,synchronized底层
  • sleep与wait区别:sleep没有释放锁,wait释放了锁。
  • yield: 让出cpu资源给其他线程,当前线程进入就绪状态。重新调度可能又获得时间片进入运行状态。理论上调用该方法后,与该线程级别相同或更高级别的线程更有机会获得cpu资源获得执行机会,当然级别低的线程也有机会。
  • 确认线程是否活着使用isAlive()。

1.3 线程创建方式

  • 实现Runnable接口,继承Thread类,创建匿名内部类,实现Callable,项目中一般用线程池去管理线程。用Runnable方式比Thread好因为一个类只能继承一个类。Callable有返回值。

1.3 线程其他知识

  • setPriority:设置线程级别1-10,默认5
  • 守护线程:守护线程与普通线程写法上基本没啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。

1.4 Java内存模型

  • 多线程情况下代码的执行结果与我们的期望值不一致那么这个线程就是不安全的,否则线程安全。
  • JMM抽象图,为什么要缓存工作内存(本地内存)?因为cpu的处理速度和内存的读取速度不在一个级别,所以cpu需要将操作的变量缓存自己的工作内存(本地内存)。
  • 工作内存数据没有及时更新到主内存会引起‘脏读’,导致线程不安全。volatile提供该变量的可见性,每次更改变量都马上刷新到主内存。
  • 重排序:Java内存模型以及happens-before规则

1.5 synchronized

  • synchronized线程可见性分析,synchronized的代码块执行完后释放锁把工作内存的数据刷新到主内存,其他线程获取锁后强制读取主内存的数据更新工作内存。
  • 偏向锁->轻量级锁->重量级锁

1.6 volatile

  • volatile变量写回主内存时会导致其他线程的工作内存失效,其他线程必须重新读取主内存数据。
  • volatile禁止重排序
  • synchronized: 具有原子性,有序性和可见性; volatile:具有有序性和可见性

1.7 final

  • final修饰的class不可以被继承,修饰的方法不可被重写,修饰的对象引用不可变,修饰的基本数据类型值不可变

1.8 Lock

  • AQS->模板方法设计模式
  • AQS(独占锁和共享锁区别),数据结构CHL队列
  • 与sychorized区别,lock可以更手动的去获取锁和释放锁,还可以可中断、超时获取锁。sychorized是独占式,lock有独占式也有非独占式。由于不像sychorized可以自动释放锁所以要在finally中要释放锁以免出现异常没有释放锁。

1.9 ConcurrentHashMap/HashMap

  • HashMap:jdk1.8之前使用数组加链表的方式存储数据,由于链表过长会导致查询效率低,所以1.8中会判断链表长度达到TREEIFY_THRESHOLD(8)会把该链表该成红黑树红黑树。
  • HashMap多线程不安全,比如死循环情况,数据丢失。
  • ConcurrentHashMap:jdk1.8之前使用分段锁技术,数据结构为数组加链表,现在使用cas+sychorized,数据结果为数组加红黑树。

2.0 ConcurrentLinkedQueue

2.1 ThreadLocal

  • 底层是一个类似Map的容器ThreadLocalMap,当前线程为key进行set,get操作。
  • 内存泄漏问题

2.2 线程池

2.2.1 线程池优点:

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

2.2.2 新建线程池

  • 新建线程池的构造函数
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

复制代码
  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数

  • keepAliveTime:非核心线程的空闲存活时间

  • unit:keepAliveTime时间单位

  • workQueue: 存放线程池的队列

  • threadFactory: 用于设置创建线程的工厂

  • handler:线程池的饱和策略事件,主要有四种类型。

  • 线程池流程图

  • 四种拒绝策略

    1.AbortPolicy(抛出一个异常,默认的)

    2.DiscardPolicy(直接丢弃任务)

    3.DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)

    4.CallerRunsPolicy(交给线程池调用所在的线程进行处理)

2.2.3 线程池异常处理

2.2.4 常用的线程池

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
复制代码

特点:

  • 手动设置核心线程数
  • 核心线程数和最大线程数相同
  • 阻塞队列选用LinkedBlockingQueue无解阻塞队列
  • keepAliveTime为0

流程图

如果核心线程数线程执行时间较长,会导致阻塞队列中的任务累积过多,导致内存飙升,导致OOM。

使用场景

  • 适合处理CPU密集行的任务,确保CPU长时间处理工作线程,尽可能的少分配线程,也就是适用线程数都比较固定,线程执行时间比较长的任务。

newCachedThreadPool

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
复制代码
  • 核心线程数为0,每次都创建一个新的非核心线程
  • 非核心线程的空闲时间为60s
  • 阻塞队列为SynchronousQueue
  • 最大线程数为Integer.MAX_VALUE

工作流程

  • 由于没有核心线程数所以每次提交任务都放入阻塞队列中,取出任务判断是否有空闲线程,有的话复用空闲线程执行任务,否则创建新的一个线程执行任务。
  • 如果线程的执行时间比较长,会导致可以复用的空闲线程很少,这样会不停创建新的线程,非常消耗CPU和内存资源

使用场景 并发高,线程执行时间短的任务

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
复制代码
  • 核心线程数和最大线程数都为1
  • keepAliveTime为0

工作流程

  • 只能有一个线程在执行,队列的任务复用该线程一个一个的执行完。

使用场景

执行串行任务,一个一个的执行。

newScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
复制代码
  • 自定核心线程数
  • 最大线程数为Integer.MAX_VALUE
  • keepAliveTime为0
  • 阻塞队列使用DelayedWorkQueue
  • 周期的执行队列的任务

使用场景

  • 需要周期的执行任务,而且需要控制线程数的任务

2.2.5 线程池状态

2.3 atomic包下原子类

  • 底层使用cas思想

2.4 生产者--消费者问题

2 jvm

2.1 jvm结构

  • 各区域储存的信息

  • 程序计数器:记录当前线程的虚拟机执行的字节码地址,执行native方法时计数器记· 录为空(undefined)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfM emoryError 情况的区域。

  • 本地方法栈和虚拟机栈可能会出现StackOverFlowError

  • 虚拟机栈

    • 请求深度大于虚拟机所允许的最大深度时会StackOverFlowError(比如执行一- 个无限的递归方法)
    • 虚拟机扩展栈时无法申请到空间会发生oom
    • 内存泄露和内存溢出都会发生oom
  • 方法区

    • 运行时常量池溢出:运行时常量池所用内存过大,内存不足。
    • 方法区溢出:常量,字节码信息,静态变量等信息占用内存过多,内存不足。

2.2 对象创建

  1. 类加载检查:执行new指令时,先检查这个指令的参数能否在常量池的定位到这个类的符号引用。再检查这个符号引用对应的类是否已经被加载、解析、初始化过。如果没有那么先进行该类的加载过程。
  2. 分配内存空间:加载完成确定所需空间大小,在堆中分配空间。分配方式有 “指针碰撞” 和 “空闲列表”两种,选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  1. 初始化零值分配完空间后将分配到的内存空间初始化为零值,使对象的实例字段可以不用初始化就可以访问,程序可以访问对象字段的初始值(零值)

  2. 设置对象头包括偏向线程ID,线程持有的锁,对象的hashcode,对象的gc分代年龄,类型指针等。如果是数组类型还需要记录数组长度。

  3. 执行<init>方法

2.4 访问对象

  • 句柄访问:reference储存句柄池地址,句柄池包含对象实例,类型信息地址。

对象移动时(GC)只需要改变句柄池的实例数据指针,引用不用变。适合频繁GC中使用

  • 直接指针:reference直接存储对象实例数据地址,实例数据中也需要储存对象类型地址。

快速访问,适合频繁访对象中使用

2.5 JVM类加载机制

2.5.1 类加载器

  • 把.class文件的字节码信息转化为具体的java.lang.Class对象的过程的工具。
  1. jvm加载字节码信息读到内存,然后导入到方法区。
  2. 如果是首次加载(主动或被动),类加载器对该类进行加载、验证、准备、解析、初始化
  3. 加载成功后会在堆空间生成一个新的.class对象。

  • 加载:.class转化为二进制。
  • 验证:验证文件语法等合法性。
  • 准备:为类变量(static)分配内存,并将其初始化为默认值。
  • 解析:把类中对常量池中的符号引用转化为直接引用。
  • 初始化:把静态变量赋予正确的初始值。

2.5.2 主动引用和被动引用

主动引用

  • 类加载阶段,会进行加载,连接(验证、准备,解析),初始化。
  1. 主动创建实例(new)
  2. 对类变量访问或赋值
  3. java.lang.reflect包(反射)中的方法(如:Class.forName(“xxx”))
  4. 访问类方法
  5. 初始化一个类的子类,其父类也会被初始化
  6. 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。

被动引用

  • 类加载阶段,只进行加载,连接(验证、准备、解析),不进行初始化。
  1. 通过子类访问父类静态变量,不会导致子类初始化。
  2. 定义类的数组而没有赋值,不会导致类的初始化。
  3. 访问类的常量,不会导致类的初始化。

2.5.3 三种类加载器

  • 层级结构:装载器有父子层级结构,bootstrap类装载器是所有装载器的父亲。
  • 层级结构:当类的装载器装载一个类时,如果他的父装载器已经装载了这个类,那么可以直接使用这个类。
  • 可见性限制:子装载器类可以看见父类装载器的类,父类装载器无法看到子类装载器。
  • 不允许卸载:类装载器不能卸载已经装载的类,但是可以删除类装载器重新创建一个类装载器。

2.5.4 双亲委派机制

  • 自低向上检查类加载器是否已经加载该类,自上往下尝试加载类。

2.6 垃圾回收算法

2.6.1对象生死判定

  1. 引用计数算法
  • 每个对象都有一计数器,对象被创建时计数器设置为1,每当有一个地方引用他计数器加1,当引用失效时计数器减1。当计数器为0时会被垃圾回收。当一个对象被回收时,他引用的任何一个对象计数器减1。
  • 优点:简单,效率高。
  • 缺点:对于循环引用的对象无法回收。
  1. 可达性分析算法
  • Gc Roots为起点,向下搜索。搜索所走过的路径叫引用链。当一个对象到Gc Roots没有引用链时,称该对象为不可达。该对象就可以被回收。
  • Gc Roots
    1. 虚拟机栈中引用的对象
    2. 本地方法栈中引用额对象
    3. 方法区静态变量引用的对象
    4. 方法区中常量引用的对象

2.6.2 对象引用分类

  1. 强引用:不会被回收
  2. 软引用:将要发生OOM之前被回收,如果内存还是不足发生OOM。
  3. 弱引用:存活到下次垃圾回收之前
  4. 虚引用:会被回收,被垃圾回收可以给系统一个通知。

2.6.3 finalize标记

  • 对象被垃圾回收需要被二次标记,第一次在可达性分析算法被标记为不可达对象。被标记为不可达对对象时判断该对象是否重写finalize方法,如果被重写就调用该方法。重写finalize后也不是会一定被调用

2.6.4 垃圾回收算法

  1. 标记清除算法:扫描根集合标记存活对象。扫描没有标记的对象进行回收。
    • 优点:简单,不需要移动对象
    • 缺点:标记、清楚效率低。容易导致内存碎片化,增加垃圾垃圾回收频率。
  2. 复制算法:把内存分为两个区域,GC时把可达对象复制到另一个空间,然后把该空间清空掉,永远保持一个空间为空。
    • 优点:按顺序分配内存就行,简单高效,不会产生内存碎片。
    • 缺点:多出一半空间,如果对象存活率很高会频繁复制。
  3. 标记整理算法:和标记清楚算法一样先标记可达对象,再把可达对象往一端空闲空间移动。然后清除端边界以外的空间。
    • 优点:解决标记清除算法的内存碎片化问题。
    • 缺点:增加了移动对象的效率。
  4. 分代算法:

  • 刚创建的对象分配到新生代Endn区,如果刚创建的对象比较大则直接分配到老年区,Endn区的对象Gc后还留下来的年龄加1,当达到一定年龄后则分配到老年区。
  • Endn区要满时会进行minor Gc(新生代Gc),minor Gc时会计算新生代对象将要分配到老年代的空间大小,如果老年代空间不够那么会进行一次full Gc(整个堆Gc)。

2.7JVM垃圾回收器

3 Spring

3.1 事务

  • ACID:原子性、一致性、隔离性、持久性。
  • 底层使用AOP的环绕通知拦截,在事务中要把异常抛出给外层,不能try/catch。

3.1.1事务的传播行为

  • PROPAGATION_REQUIRED:如果当前有事务就用当前事务,如果当前没事务就新建一个事务
  • PROPAGATION_SUPPORT:如果当前有事务就使用当前事务,如果当前没有事务就用非事务的方式执行。
  • PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务则抛出异常。
  • PROPAGATION_REQUIRES_NEW:新建事务,如果当前有事务则挂起。
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前有事务则挂起。
  • PROPAGATION_NEVER:以非实物方式执行如果当前有事务则抛出异常。

3.1.2 AOP

事务底层使用了AOP技术。AOP底层使用了动态代理:jdk动态代理(类需要实现接口)和cglib动态代理。

  • 两种动态代理方式的区别:
  1. JDK 动态代理使用的是反射技术,被代理的类要实现方法接口。
  2. CGLIB 动态代理使用的是字节码增强技术,被代理的类不用实现方法接口。
  • CGLIB动态代理创建的对象运行性能比JDK动态代理生成的对象高。
  • CGLIB动态代理创建的对象的所花的时间比JDK动态代理要长。
  • 所以一般单例的对象动态代理使用CGLIB比较好。

3.2 IOC

  • 依赖注入、控制反转:反射、扫包、dom4j。

3.3 SpringMvc

  • servlet:
  1. 生命周期: init->services->destory
  • Servlet生命周期可分为5个步骤

    • 加载Servlet。当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
    • 初始化。当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象 处理服务。
    • 当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求 销毁。
    • 当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
    • 卸载。当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作。
  • 简单总结:只要访问Servlet,service()就会被调用。init()只有第一次访问Servlet的时候才会被调用。destroy()只有在Tomcat关闭的时候才会被调用。

springmvc流程图:

(1)用户发送请求至前端控制器DispatcherServlet;

(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;

(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;

(4)DispatcherServlet通过HandlerAdapter处理器适配器调用处理器;

(5)执行处理器(Handler,也叫后端控制器);

(6)Handler执行完成返回ModelAndView;

(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;

(8)DispatcherServlet将ModelAndView传给ViewReslover视图解析器进行解析;

(9)ViewReslover解析后返回具体View;

(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)

(11)DispatcherServlet响应用户。效果 相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。

3.4 SpringBoot

  • SpringBoot是基于Spring的一个快速整合包,减少Spring的Xml的繁琐配置,让我们快速开发。
  • Springboot的WEB组件默认集成SpringMvc
  • SpringCloud:基于SpringBoot的一套完整微服务解决框架。
  • @Lombok和@Slf4j:底层使用ASM字节码增强技术
  • Mybatis多数据源配置方式:注解方式声明(注解声明数据源)或者分包方式(指定不同包下面使用不同数据源)

4 数据库

4.1 数据库连接池

  • 什么是空闲线程数,活动线程数,最大线程数?

5 redis

5.1 基本知识

  • 数据类型:String,List,Set,Zset,Hash。
  • 数据持久化:Rdb和Aof,默认使用Rdb,如果Rdb,如果两种方式都开启那么redis启动时会从Aof文件加载数据。
  • 内存模型
    • dictEntry:每个键值对都有一个dictEntry,dictEntry包含了key和value对应的指针,以及next指向下个dictEntry指针。
    • key:如图key是直接存在sds里。
    • redisObject:储存value的类型(type)以及所存sds的指针地址(ptr)。
    • jemalloc:称为内存分配器,dictEntry,sds,redisObject都需要jemalloc分配内存。

5.2 jemalloc

redis默认的内存分配器:分成小,中,大三个范围,三个范围又划分多个不同大小的内存块使存储数据时选择合适的内存块存储。

5.3 redisObject

  • redis的value不会像key直接存储在sds而是通过redisObject封装。
  • redisObject:value的数据类型,编码,内存回收,共享对象等都需要redisObject支持。
   typedef struct redisObject {
       unsigned type:4;
       unsigned encoding:4;
       unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
       int refcount;
       void *ptr;
   } robj;

复制代码
  • type:value对象的数据类型(String,List,Set,Zset,Hash)
  • encoding:value对象的数据编码。每种数据类型都至少有两数据编码。redis根据不同场景使用不同的数据编码,这样大大提高redis的灵活性和效率。
  • lru:记录对象最后一次被命令访问的时间。与redis内存回收有关系。
  • refcount:对象被引用的次数,每次对象被新程序引用时加1,每次一个新程序不引用该对象时减一。refcount为0时内存释放该对象。refcount>1说明该对象是共享对象,redis为了节省内存不会创建重复的对象,共享对象仅支持整数型字符串。因为共享对象要对比对象内容是否一致,整数型的字符对比时间复扎度为O(1),非整数型字符串时间复杂度为O(n),list、set、Zset、Hash类型时间复杂度为O(n^2)。但是在list,set,zset,hash内部的元素可以使用共享对象。
  • ptr:指向存储value值的sds地址。

5.4 SDS

  • SDS和C字符串
struct sdshdr {
    //存储字符长度
    int len;
    //空闲长度
    int free;
    char buf[];
};
复制代码

5.5 对象类型与内部编码

  • 内部编码转化不可逆,只能从小内存编码到大内存编码。

  1. 字符串
  • 长度不能大于512MB
  • 内部编码:
    • int:不超过8个字节的整型。当int不再是整型或者长度超过Long,会转化为raw。
    • embstr:<=39个字节的字符串,同时分配redisObject和sds内存。只读。所以只要修改了embstr后都为raw无论字符长度是超过39。
    • raw:>39个字节,单独分配redisObject和sds内存。
    • 同时分配redisObject和sds内存好处在于是连续空间能减少一次创建时分配内存次数和回收时的次数,但是字符串长度改变时重新分配内存空间时redisObject和sds都要重新分配。
  1. 列表
  • 内部编码:

    • 压缩列表:压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块。
    • 双向链表:
  • 只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于512个;列表中所有字符串对象都不足64字节。如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。

  1. 哈希
  • 内部编码:
    • 压缩列表:

    • 哈希表:1个dict结构,2个ht结构,一个bucket(dictEntry数组)和多个dictEntry。

    • dict:

      1. type和privdae用于适应不同的数据类型的键值对,用于创建多态字典。
      2. ht和rehashidx用于rehash,rehash时把ht[0]数据rehash到ht[1],再把ht[1]赋值给ht[0]。
    • dicht:

      1. table指向bucket
      2. size表示hash表大小(bucket)
      3. sizemask为size-1,与hash值决定key在bucket存储的位置。
      4. used表示已使用的dictEntry数量。
    • buck:

      1. bucket是一个dictEntry链表结构的数组。
    • dictEntry:

      1. dictEntry是存储key,value的结构,next指向下一个dictEntry对象用于解决hash冲突。
  1. 集合
  • 无序,不能重复
  • 内部编码:
    • 整数集合和hash表,使用hash表时值都被设置为null。
    • 整数集合:节省空间
    typedef struct intset{
        uint32_t encoding;
        uint32_t length;
        int8_t contents[];
    } intset;
    复制代码
    encoding表示contents储存类型(int16_t、int32_t或int64_t),length表示元素个数,只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于512个;集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。
  1. 有序集合
  • 有序,不重复
  • 内部编码:压缩列表,跳跃列表。

5.6 持久化

  1. RDB
    • 手动触发:save命令会阻塞当前命令操作直到持久化完成,bgsave命令会fork一个进程进行持久化操作只会在fork进程的时候进行阻塞。要用bgsave。
    • 自动触发:save m n在配置文件中设置,指在m秒内n次变化,就会自动触发bgsave。
    • shutdown命令会自动触发bgsave
    • 主从架构时,从节点从主节点进行全量复制时主节点会bgsave然后把rdb文件发给从节点。

  1. AOF
    • 手动触发:bgrewriteaof命令AOF文件重写,可以降低文件的大小。
    • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。

AOF文件重写时redis处理的新命令会同时写入Aof_buf和Aof_rewrite_buf缓冲区,防止 重写时丢失数据,重写完毕后合并俩份AOF文件。

  1. 策略
    • rdb:对性能影响低,文件体积小,恢复速度快,但是无法做到数据实时持久化,数据容易丢失。
    • aof:对性能影响大,文件体积大,恢复速度较于rdb慢,但是数据实时持久化程度高。

5.7 主从复制

  • 心跳机制检测
  • 主节点将执行的写的命令发送给从节点
  • 如果主从节点中断了,重新连接后从节点想主节点发送psync命令,在一定时间内恢复该时间内主节点会保存该时间内的命令,恢复后发送给从节点,如果断开时间过长会进行全量复制。具体是判断部分复制还是全量复制要看psync命令内部逻辑。

5.7.1 全量复制

  1. 主节点收到全量复制后,bgsave生成当前时间点数据的RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从当前时间点开始的全部命令。
  2. bgsave后,发送给从节点,从节点首先清楚自己的数据,然后载入接受的RDB文件,将数据更新至主节点bgsave时时间点的数据状态。
  3. 主节点将缓冲区的所有命令发送给从节点,从节点执行这些命令将数据更新至主节点最新状态。
  4. 如果从节点开启了AOF,会触发执行bgrewriteaof,更新AOF至主节点最新状态。

5.7.2 部分量复制

  1. 复制偏移量(offset):主从节点分别维护偏移量,主节点向从节点发送N个字节数时,主节点的复制偏移量加N,从节点接收到后从节点的复制偏移量也加N。主从offset保持一致说明主从数据状态一致,否则根据offset之差进行部分复制。
  2. 复制缓冲区:是一个有长度限制的FIFO队列,每次主节点发送命令给从节点时还会,复制该命令存入复制缓冲区,缓冲区还记录每个字节对应的offset,如果主从offset之差的数据在复制缓冲区完整存在,那么会发送复制缓冲区的命令给从节点,进行部分复制保持数据库一致。
  3. RunId:每个redis启动都会生成一个随机RunId,主从初次复制时,主节点把自己的RunId发送给从节点。从节点会保存起来,下次断开重连时会发送保存的RunId给主节点,如果与从节点的RunId一致那么说明之前同步过,再根据offset和复制缓冲区判断能否进行部分复制。如果RunId不同那么说明之前不是该主节点的从节点,进行全量复制。
  • psync命令的执行过程可以参见下图(图片来源:《Redis设计与实现》):

5.8 集群

5.8.1 数据分区

  • 对比:哈希取余分区,一致性哈希分区,带虚拟节点的一致性哈希分区。

    • 哈希取余分区:根据key的hash结果与节点数取余。
      • 优点:数据分布均匀。
      • 缺点:节点加入或删除时所有数据都要rehash,数据重新迁移。
    • 一致性哈希分区:将整个hash值空间组织形成一个封闭的环,范围为0-2^32-1,找到数据key的hash结果在环上的位置,然后在该位置顺时针走,遇到的第一个节点就是该数据存放的节点。
      • 优点:节点增加或减少只会影响相近节点的数据。
      • 缺点:会导致数据分布不均匀。单节点可能会负载过高。

    • 带虚拟节点的一致性哈希分区:在一致性哈希分区基础上加入虚拟节点,在redis中称为槽,槽介于数据和redis节点之间,每个槽包含一定数量 的数据,每个槽分配一定范围的hash值。这样就是数据key的hash值存入对应的槽,然后分配到对应的节点上。
      • 优点:节点增加或减少时,只要移动槽到对应的节点就好了。
关注下面的标签,发现更多相似文章
评论