java开发工程师面试总结

面试中常被问到的问题

  • 介绍
  • jvm
    • jvm内存模型
      • 程序计数器
      • 虚拟机栈
      • 本地方法栈
      • 方法区
    • 垃圾回收
      • 判断对象是否存活
        • 引用计数算法
        • 可达性分析算法
      • 垃圾收集算法
        • 复制算法
        • 标记清除算法,标记整理算法
      • 垃圾收集器
        • 新生代收集器
          • Serial
          • ParNew
          • Parallel Scavenge
        • 老年代收集器
          • Serial Old
          • Parallel Old
          • CMS
        • G1 GC
      • 分代回收流程
      • jvm优化
        • Minor GC频繁
        • 总结
  • io
    • InputStream
    • Reader
    • OutputStream
    • Writer
  • 数据结构
    • List
      • ArrayList
      • LinkedList
      • Vector
    • Set
      • HashSet
      • TreeSet
    • Map
      • HashMap
        • 工作原理
        • hash碰撞
        • 扩容
        • jdk1.7和jdk1.8区别
      • HashTable
      • ConcurrentHashMap
    • String、StringBuffer、StringBuilder
  • 排序算法
    • 非线性时间比较类排序
      • 交换排序
        • 冒泡排序
        • 快速排序
      • 插入排序
        • 简单插入排序
        • 希尔排序
      • 选择排序
        • 简单选择排序
        • 堆排序
      • 归并排序
    • 线性时间非比较类排序
      • 计数排序
      • 桶排序
      • 基数排序
  • 多线程
    • 线程池
  • 设计模式
  • http、https
    • http
    • https
    • GET和POST的区别
  • mysql
    • 范式
    • ACID
    • 隔离级别
    • 索引
      • 表级锁
      • 行级锁
    • 优化
  • 缓存
    • 区别
    • 高可用
    • 分布式
    • 缓存雪崩
    • 缓存穿透
    • 缓存击穿
    • 集群脑裂
    • 双写不一致
    • 并发竞争
  • 消息队列
    • 区别
    • 消费幂等性
    • 丢失消息
  • nginx
    • 负载均衡
  • 待续。。。
  • 参考链接

介绍

java开发工程师面试常问问题
博主开源项目,欢迎意见和star
https://gitee.com/xuelingkang/spring-boot-demo
https://gitee.com/xuelingkang/react-demo
https://gitee.com/xuelingkang/websocket
https://gitee.com/xuelingkang/zookeeper
网站
https://demo.xzixi.com

jvm

jvm内存模型

java开发工程师面试总结_第1张图片

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器,这里不会出现OutOfMemoryError。

虚拟机栈

线程私有;生命周期和线程一致,每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程;存储局部变量表、操作数栈、动态链接、方法出口等信息。(局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址);会出现的异常:1、StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度;2、OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。)

本地方法栈

与虚拟机栈类似,区别是虚拟机栈运行java方法,本地方发展运行的是native方法,也会有StackOverflowError和OutOfMemoryError。

方法区

线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量,运行时常量池,在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

线程共享,jvm管理的内存中最大的一块区域,多线程下需要同步,会抛出OutOfMemoryError,如果堆中没有内存完成实例分配,且堆无法再扩展时抛出该异常。

垃圾回收

程序计数器、本地方法栈、虚拟机栈生命周期与线程一致,由线程而生,随线程而灭,当线程执行结束后,内存会自动回收,所以无需关心。
而堆和方法区是线程共享的,不会随着线程执行完毕而回收,gc关注的也正是这部分的内存。

判断对象是否存活

引用计数算法

早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

可达性分析算法

目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
可作为GC Roots的对象有四种
①虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。
②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
③方法区中的常量引用的对象。
④本地方法栈中JNI(native方法)引用的对象。
即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。
要真正宣告对象死亡需经过两个过程。
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]

垃圾收集算法

复制算法

新生代采用复制算法,复制算法比较适合用于存活率低的内存区域,它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。

标记清除算法,标记整理算法

由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

垃圾收集器

年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年轻、老年代范畴内]

新生代收集器

Serial

最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。
优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。

ParNew

可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个,超线程cpu的话就不清楚了 - -】,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。
优点:
1.支持多线程,多核CPU下可以充分的利用CPU资源
2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。

Parallel Scavenge

Java1.8默认的收集器,采用复制算法的收集器,和ParNew一样支持多线程。
但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】
对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。
对于后台
高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算
Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。

老年代收集器

Serial Old

和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。
如果是Server模式有两大用途
1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。
2.作为CMS收集器的后备。

Parallel Old

支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】
在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old为单线程Server模式下会拖后腿【多核cpu下无法充分利用】,这种结合并不能让应用的吞吐量最大化。
Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。

CMS

CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】
它的运作分为4个阶段
1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
2.并发标记:GC Roots Tarcing过程,即可达性分析
3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
4.并发清除
以上初始标记和重新标记需要stw(停掉其它运行java线程)
之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。
缺点:
1.cms对cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。
2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC
3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

G1 GC

G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。
摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)
g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。
用到的算法为标记-清理、复制算法
g1是区域化的,它将java堆内存划分为若干个大小相同的区域【region】,jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。
g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。
g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。
g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。
几个重要的默认值
g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作
-XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域
-XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒

分代回收流程

第一次Minor GC,将Eden区存活对象复制到Survivor0区(如果Survivor区放不下则直接进入老年代),清空Eden区,所有存活对象年龄+1
第二次Minor GC,将Eden区和Survivor0区存活对象复制到Survivor1区(如果Survivor区放不下则直接进入老年代),清空Eden区,所有存活对象年龄+1
。。。
第n次Minor GC,将年龄15(默认15,可配置)的对象移到老年代,年龄小于15的对象继续重复1,2步骤

jvm优化

增加年轻代比例会减少Minor GC频率,增加单次Minor GC时间,增加老年代比例会减少Full GC频率,增加单次Full GC时间,jvm优化就是要达到一个平衡,合格的GC,括号中的值仅做参考
Minor GC执行非常迅速(50ms以内)
Minor GC没有频繁执行(大约10s执行一次)
Full GC执行非常迅速(1s以内)
Full GC没有频繁执行(大约10min执行一次)

Minor GC频繁

复制对象的成本要远高于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小,因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加,但是Minor GC的频率会变低。

总结

  1. 在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化令其性能达到一个质的飞跃。
  2. 其次,通过上述分析,可以看出虚拟机内部已有很多优化来保证应用的稳定运行,所以不要为了调优而调优,不当的调优可能适得其反。
  3. 最后,GC优化是一个系统而复杂的工作,没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能有事半功倍的效果。

io

InputStream

InputStream类是字节输入流的抽象类,是所有字节输入流的父类,InputStream类具有层次结构如下图所示。
java开发工程师面试总结_第2张图片

Reader

java中的字符是Unicode编码的,是双字节的。InputStream是用来处理字节的,在处理字符文本时很不方便。Java为字符文本的输入提供了专门的一套类Reader。Reader类是字符输入流的抽象类,所有字符输入流的实现都是它的子类。
java开发工程师面试总结_第3张图片

OutputStream

输出流OutputStream类是字节输入流的抽象类,此抽象类表示输出字节流的所有类的超类。
java开发工程师面试总结_第4张图片

Writer

Writer类是字符输出流的抽象类,所有字符输出类的实现都是它的子类。
java开发工程师面试总结_第5张图片

数据结构

List

有序,可重复

ArrayList

ArrayList用数组实现,随机访问速度快,增删元素速度慢
扩容:默认大小是10,每次扩容后长度变为原来1.5倍,调用Arrays.copyOf()
增删元素:在末尾增加元素,如果有空余空间,速度也很快,空间不足时需要先扩容,所以速度较慢
指定其他位置增加元素,需要调用System.arraycopy()方法,速度较慢

LinkedList

LinkedList用双相链表实现,随机访问速度慢,增删元素速度快
不存在扩容问题
增删元素:增删元素只需要改变指针指向,速度快

Vector

线程安全的List,速度慢

Set

无序,不重复

HashSet

用HashMap实现,基本上都是调用HashMap的方法

TreeSet

有序,用二叉排序

Map

无序,key不重复,value可以重复

HashMap

数组加链表实现,默认大小16,默认负载因子0.75,key和value可以为null。
为什么默认大小时16,当数组长度是2的整数次幂时,通过hashCode计算数组下标的到的结果碰撞概率最小。
为什么默认负载因子是0.75,扩容阈值=初始化大小*负载因子,当负载因子过小时容易浪费空间,过大是影响扩容效率,设置为0.75可以达到时间和空间上的平衡。

工作原理

put方法:调用key的hashCode()方法,根据返回的hashCode值对bucket数组长度取模,找到bucket位置来存储Entry对象
get方法:调用key的hashCode()方法,根据返回的hashCode值对bucket数组长度取模,找到bucket位置,获取Entry对象的value

hash碰撞

当key的hashCode值相同但是equals比较结果不同时,发生hash碰撞,HashMap使用链表存储Entry对象,调用get方法时,首先找到bucket位置,然后调用equals()方法比较链表中的第一个Entry对象,如果不是则遍历剩余的Entry对象。

扩容

当bucket数组元素个数大于阈值是会对HashMap扩容,调用resize()方法,将数组长度扩容为原来的两倍,然后根据key的hashCode重新将Entry对象散列到bucket数组中。

jdk1.7和jdk1.8区别

jdk1.8底层是数组+链表+红黑树,jdk1.7是数组+链表。
用红黑树遍历查找的时间复杂度比链表低,插入删除的时间负责度比链表高,提高查询性能。
扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若链表中元素个数不小于8就将链表转换为红黑树,但如果红黑树中的元素个数不大于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

HashTable

key和value都不能为null,线程安全,对整个表上锁,性能较差,一般不用。

ConcurrentHashMap

采用锁分段技术,每次只对操作的段上锁,其它段不上锁,并发场景下性能好。

String、StringBuffer、StringBuilder

String不可变字符序列,StringBuffer可变字符序列,线程安全,StringBuilder可变字符序列,线程不安全

排序算法

  • 算法分类
    java开发工程师面试总结_第6张图片
  • 算法复杂度
    java开发工程师面试总结_第7张图片
    简单排序适合n较小的情况,高阶排序适合n较大的情况
    Arrays.sort使用快速排序,插入排序,归并排序。

非线性时间比较类排序

交换排序

冒泡排序

java开发工程师面试总结_第8张图片
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

快速排序

java开发工程师面试总结_第9张图片
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
步骤:

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

插入排序

简单插入排序

java开发工程师面试总结_第10张图片
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

希尔排序


1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
步骤:

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

选择排序

简单选择排序

java开发工程师面试总结_第11张图片
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
步骤:

  1. 初始状态:无序区为R[1…n],有序区为空;
  2. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  3. n-1趟结束,数组有序化了。

堆排序


堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
步骤:

  1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  3. 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

归并排序

java开发工程师面试总结_第12张图片
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
步骤:

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。

线性时间非比较类排序

计数排序

java开发工程师面试总结_第13张图片
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
步骤:

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

桶排序

java开发工程师面试总结_第14张图片
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
步骤:

  1. 设置一个定量的数组当作空桶;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序;
  4. 从不是空的桶里把排好序的数据拼接起来。

基数排序

java开发工程师面试总结_第15张图片
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
步骤:

  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成radix数组;
  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点);

多线程

  • 什么是线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
  • 线程和进程的区别:线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
  • 继承Thread和实现Runnable哪个好:java不支持多继承,但是可以实现多个接口,所以实现Runnable接口比较好。
  • start()方法和run()方法的区别:start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
  • Runnable和Callable的区别:Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
  • volatile关键字:只有成员变量才能使用它,volatile关键字保证每次读取前必须从主内存刷新最新的值,每次写入后必须立刻同步到主内存,线程之间是可见的。
  • 什么是线程安全:如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  • notify:随机唤醒一个在此对象上wait的线程。
  • notifyAll:唤醒所有在此对象上wait的线程。
  • ThreadLocal:每个Thread对象都维护了一个ThreadLocalMap(ThreadLocal的内部类)对象,ThreadLocalMap中保存了当前线程的线程局部变量,key是ThreadLocal的实例,value是线程局部变量的值。
  • 为什么需要在循环中检查等待条件:处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因。
  • 死锁产生的条件:1. 互斥条件:一个资源每次只能被一个进程使用。2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  • 如何避免死锁:避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
  • sleep和wait:sleep需要捕获异常,可以不在synchronized代码块中调用,不会释放锁;wait不需要捕获异常,必须在synchronized代码块中调用,会释放锁。
  • 线程安全的单例:饿汉,双重检查锁定,静态内部类(基于java类加载的安全性),枚举类

线程池

主要参数:

  • corePoolSize:核心线程数,核心线程会一直存活,除非设置allowCoreThreadTimeout为false
  • queueCapacity:任务队列容量(阻塞队列),当没有空闲的核心线程时,任务会放到队列中等待执行
  • maxPoolSize:最大线程数,当线程数>=核心线程数,且任务队列满时,会创建新线程,直到线程数=最大线程数
  • keepAliveTime:线程空闲时间,线程超时会回收,当设置allowCoreThreadTimeout为false时,核心线程超时也会回收
  • allowCoreThreadTimeout:允许核心线程超时
  • rejectedExecutionHandler:任务拒绝处理器,当线程数达到最大,且任务队列满时,会拒绝任务

参数计算:
核心线程数 = CPU可用核心数 / (1 - 阻塞系数)
阻塞系数 = 阻塞时间/总时间
也就是说,当任务是计算密集型,那么线程数=CPU可用核心数,当任务是io密集型,增加线程数可以在进行IO操作时让出CPU时间给其他线程。
任务队列容量需要根据实际场景最多允许线程阻塞的时间和单个任务执行时间确定,任务队列容量 = 核心线程数 / 单个任务耗时 * 最大阻塞时间,也就是计算核心线程在最大阻塞时间内能处理多少个任务。
最大线程数 = 高峰期单位时间任务数量 * 处理单个任务所需时间 / 最大阻塞时间,同时也需要综合考虑服务器性能。

设计模式

常用设计模式:单例模式、代理模式、适配器模式、策略模式、观察者模式、责任链模式。
设计模式总的来说就是通过使用接口和抽象类使程序更好扩展和维护,只要遵循这样的原则就不必拘泥于模式。设计模式篇幅较大,请参考文末链接。

http、https

http

基于tcp的无状态协议,可以使用cookie记录状态。

http1.1新特性:

  1. 默认持久连接节省通信量,只要客户端服务端任意一端没有明确提出断开TCP连接,就一直保持连接,可以发送多次HTTP请求。
  2. 管线化,客户端可以同时发出多个HTTP请求,而不用一个个等待响应。
  3. 断点续传,实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输。

http请求流程:

  1. 建立TCP连接
  2. 客户端向服务器发送请求行
  3. 客户端向服务器发送请求头
  4. 服务器响应
  5. 服务器发送响应头
  6. 服务器向客户端发送数据
  7. 服务器关闭TCP连接

建立连接三次握手:
第一次握手:客户端发送一个带SYN的TCP报文到服务器,表示客户端想要和服务器端建立连接。
第二次握手:服务器端接收到客户端的请求,返回客户端报文,这个报文带有SYN和ACK确认标示,访问客户端是否准备好。
第三次握手:客户端再次响应服务端一个ACK确认,表示客户端已经准备好了。

为什么是三次握手:
三次可以让双方都确定连接到对方后能收到对方的回复,双方都准备好了,保证连接的可靠性。

断开连接四次挥手:
第一次挥手:客户端发送一个FIN(结束),用来关闭客户端到服务器端的连接。
第二次挥手:服务器端收到这个FIN后发回一个ACK确认标示,确认收到。
第三次挥手:服务器端发送一个FIN到客户端,服务器端关闭客户端的连接。
第四次挥手:客户端发送ACK报文确认。

为什么是四次挥手:
因为客户端主动关闭连接,发送FIN,服务器接收到后回应ACK,表示服务器知道客户端已经发送完数据,准备端开,但是服务器可能还有数据没有发送完,所以服务器也需要发送一个FIN包,表示数据已经发送完成,可以断开了,客户端响应ACK或者超时后连接断开。

https

https和http的区别:

  1. HTTP的URL以http://开头,而HTTPS的URL以https://开头。
  2. HTTP是不安全的,而HTTPS是安全的。
  3. HTTP标准端口是80,而HTTPS的标准端口是 443。
  4. 在 OSI 网络模型中,HTTPS的加密是在传输层完成的,因为SSL是位于传输层的,TLS的前身是SSL,所以同理。
  5. HTTP无需认证证书,而https需要认证证书。

对称密钥加密:
所谓的对称密钥,就是加密和解密都用同一个密钥,因此也叫做共享密钥加密。
优点:处理速度快。
缺点:但是容易被第三方盗取。

非对称密钥加密:
所谓的非对称密钥,就是这是一对密钥,一个是私钥,一个是公钥。私钥不能让其他任何人知道,而公钥可以随意发布,任何人都可以获得到,因此这种加密方式也叫公开密钥加密。公钥用来加密,私钥用来解密。
优点:更加安全,不容易被盗取。
缺点:处理效率相比对称密钥加密要慢,如果在通信时用这种方式加密,效率很低。

https使用混合加密:
使用非对称加密的方式交换密钥,然后使用对称加密进行通信。
交换密钥步骤:服务器将公钥发送给客户端,客户端生成私钥,使用服务器公钥加密发送给服务器,服务器接收到加密后的私钥,使用自己的私钥解密,得到客户端的私钥,然后使用这个私钥进行通信。

工作原理:

  1. 首先服务端给客户端传输证书,这个证书就是公钥,只是包含了很多的信息,比如说证书的办法机构,证书的过期时间。
  2. 客户端进行证书的解析,比如说验证办法机构,过期时间,如果发现没有任何问题,就生成一个随机值(私钥),然后用证书对这个私钥进行加密,并发送给服务端
  3. 服务端使用私钥将这个信息进行解密,得到客户端的私钥,然后客户端和服务端就可以通过这个私钥进行通信了
  4. 服务端将消息进行对称加密(简单来说就是将消息和私钥进行混合,除非知道私钥否则无法进行解密),私钥正好只有客户端和服务端知道,所以信息就比较安全了
  5. 服务端将进行对称加密后的消息进行传送
  6. 客户端使用私钥进行信息的解密

GET和POST的区别

  1. 一般来说,我们发送get是希望从服务器上获取数据,post请求需要向服务器传送数据。
  2. get 在浏览器回退时是无害的,post 会再次提交数据
  3. get 产生的url 地址可以被 bookmark,post 则不可以
  4. get 请求会被浏览器主动cache (缓存),post 则不会,除非手动设置
  5. get 请求参数会被完整保留在浏览器历史记录里,而post中参数不会被保留
  6. get 只接受ASCII 码字符,而post 没有限制
  7. get 请求只能进行url 编码,而post 支持多种编码方式。
  8. get 把请求参数放在url 上,即http协议头上,post 放在Request body请求体中。
    故get 比post 更不安全,不能用来传递敏感信息。
    附:get 参数放在url上,以?分割url,参数之间以&相连;英文/数字,不做改变,原样发送;空格转换为+;中文/其他字符,则用base64加密,即%加上“十六进制ASCII码”
  9. get 一般来说提交的数据最大是2k;(原则上url 长度无限制,但大多数浏览器通常都会限制url 长度在2k(2048字节byte))
    post 理论上没有限制,实际上IIS4中最大量为80k,IIS5中为100k。
  10. get 产生一个tcp 数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据)
    post 产生两个tcp 数据包,浏览器会先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200(返回数据)
    附:Http 是基于tcp/ip 关于数据如何在万维网中通信的协议。所以http的底层是tcp/ip,get、post的底层也是tcp/ip,也就是说,get、post都是tcp连接。

mysql

范式

  • 第一范式:列不可再分。
  • 第二范式:属性完全依赖于主键。
  • 第三范式:属性不依赖于其它非主属性,属性直接依赖于主键。

ACID

  • A,原子性,整个事务的所有操作要么全部完成,要么全部不完成,不能停留在中间状态。
  • C,一致性,事务开始之前和事务结束之后,数据库的完整性约束不能被破坏。
  • I,隔离性,事务之间互不干扰。
  • D,持久性,事务结束之后,该事务对数据库的更改将永久保存在数据库中,不会因为服务重启而丢失。

隔离级别

  • 脏读:一个事务读到了另一个事务未提交的数据。
  • 不可重复读:在一个事务内多次读取同一数据得到不同的结果,与脏读区别,脏读是读到了其他事务未提交的数据,不可重复读是读到了其他事务已提交的数据。
  • 幻读:事务A读的时候读出了15条记录,事务B在事务A执行的过程中增加了1条,事务A再读的时候就变成了16 条,这种情况就叫做幻读。

四种隔离级别:

  • 读未提交:最低级别,任何情况都会发生。
  • 读已提交:可避免脏读的发生。
  • 可重复读:可避免脏读、不可重复读的发生。
  • 串行化:避免脏读、不可重复读,幻读的发生。

mysql默认隔离级别是可重复读。

索引

索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据,InnoDB引擎,默认的是B+树。
B+树索引和Hash索引的区别:

  • 哈希索引适合等值查询,但是无法进行范围查询
  • 哈希索引没办法利用索引完成排序
  • 哈希索引不支持多列联合索引的最左匹配规则
  • 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题

查询优化器:
一条SQL语句的查询,可以有不同的执行方案,至于最终选择哪种方案,需要通过优化器进行选择,选择执行成本最低的方案。
在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案。
这个成本最低的方案就是所谓的执行计划。优化过程大致如下:

  1. 根据搜索条件,找出所有可能使用的索引
  2. 计算全表扫描的代价
  3. 计算使用不同索引执行查询的代价
  4. 对比各种执行方案的代价,找出成本最低的那一个

表级锁

MySQL表级锁分为读锁和写锁,开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。

  • 读锁:(LOCK TABLE table_name [ AS alias_name ] READ)成功申请读锁的前提是当前没有线程对该表使用写锁,否则该语句会被阻塞。申请读锁成功后,其他线程也可以对该表进行读操作,但不允许有线程对其进行写操作,就算是当前线程也不允许。当锁住了A表之后,就只能对A表进行读操作,对其他表进行读操作会出现错误。
  • 写锁:(LOCK TABLE table_name [AS alias_name] [ LOW_PRIORITY ] WRITE)写锁中可以指定锁的优先级。LOW_PRIORITY是一种比读锁更低优先级的锁,当多个线程同时申请多种锁(LOW_PRIORITY,READ,WRITE)时,LOW_PRIORITY的优先级最低。读锁申请成功的前提是没有线程对表加读锁和其他写锁,否则会被阻塞。

行级锁

开锁大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 共享锁:(SELECT …LOCK IN SHARE MODE)主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作,但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁。Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
  • 排他锁:(SELECT …LOCK FOR UPDATE)Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

优化

  1. 检查java代码中是否有在循环中连接数据库。
  2. 检查sql语句是否有导致索引失效的语法(通过执行计划),比如or左右两侧必须都有索引,like通配符不能在开头,复合索引满足左侧前缀原则,查询条件中对字段使用了函数。
  3. 检查是否有常用查询条件没有加索引。
  4. 开启mysql查询缓存,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。
  5. 表字段最多20-30个,太多的话需要垂直拆分。
  6. 表记录非常多,可以水平拆分,水平拆分如果不分库意义不大,分库使用当当网的 Sharding-JDBC或者阿里的TDDL

缓存

区别

  • ehcache:与java程序绑定在一起,缓存数据直接保存在java内存中,可配置持久化,java进程挂了,缓存也就挂了,分布式项目不适合
  • memcache:独立运行,不支持持久化,多线程,数据类型只支持字符串
  • redis:独立运行,支持RDB和AOF持久化,单线程,数据类型有string,list,set,zset,hash

高可用

  • memcache使用magent搭建集群
  • redis使用sentinel搭建集群

分布式

  • 一致性hash算法:对key进行hash,将hash结果连成一个环,对分布式节点进行hash,将hash结果填充到环上,在节点位置对环进行拆分,当节点很少时容易造成分布不均匀,设置虚拟节点(通常是32个或更大),只要将虚拟节点和真实节点均匀的对应上即可,当增删分布式节点时保证较少的key需要重新分布。
  • memcache:通过客户端实现。
  • redis:部署redis-cluster即可。

缓存雪崩

大量key同时过期,导致数据库压力过大甚至宕机。
解决方案:

  • 将缓存数据过期时间设置随机,防止同一时间大量数据过期。
  • 如果缓存服务是分布式部署,将热点数据分布在不同的节点上,分散压力。
  • 设置热点数据永不过期。

缓存穿透

一般是攻击者不断请求一个不存在的数据,导致数据库压力过大。
解决方案:

  • 增加参数校验,如id<=0的请求直接拦截。
  • 在数据库中没有查询到的数据也将空值写入缓存。

缓存击穿

由于某个key过期,同时有大量请求并发访问该数据,导致数据库压力过大。
解决方案:

  • 设置热点数据永不过期。
  • 当缓存中没有查询到数据时,使用分布式锁,获得锁的线程查询数据库,并将数据写入缓存,其他线程阻塞直到释放锁后重新尝试查询缓存。

集群脑裂

集群脑裂是指master节点网络故障,导致sentinel认为master节点宕机,将其他slave节点提升为master节点,当master节点网络恢复后出现了两个master节点,旧master节点降级为slave节点,网络故障期间写入的数据会丢失。
解决方案:
设置连接到master的最少slave数量和slave连接到master的最大延迟时间,如果连接到master的slave数量不足或者连接超时,master会拒绝写入数据,减少数据丢失。

双写不一致

先删除缓存,再修改数据库。如果删除缓存成功,修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,如果删除缓存失败不执行修改数据库,在并发量不是特别高,且对于双写不一致问题要求不严格的情况下可以使用。

数据库与缓存更新与读取操作进行异步串行化:

  1. 更新数据的时候(写请求),根据数据的唯一标识,将操作路由之后,发送到AarrayBlockQueue中
  2. 读取数据的时候,如果发现数据不在缓存中,那么将读取mysql数据+更新缓存的操作(读请求),根据唯一标识路由之后,也发送同一个AarrayBlockQueue中
  3. 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行
  4. 同一条数据,保证删除缓存,和修改数据库的操作不会有其他线程干扰,而读取mysql数据+更新缓存的操作队列始终最多只有一个请求等待执行,当controller里发起多个获取库存的请求时,只需要有一个读取mysql数据+更新缓存,其他的请求直接从缓存中获取即可

并发竞争

多个线程同时写一个key,如果不能保证顺序执行,有可能会使旧数据覆盖新数据。
数据库中用时间戳做版本号,写缓存时先获取分布式锁,获取到锁之后检查缓存中的数据是不是比当前数据旧,是就写,否则取消。

消息队列

目前主流的消息队列,RabbitMQ,RocketMQ,Kafka
为什么要用MQ,异步,削峰,解耦

区别

  • RabbitMQ:单机吞吐量万级,社区活跃度高,文档丰富,迭代快
  • RokcetMQ:单机吞吐量十万级,阿里研发的技术,文档不是特别丰富,当topic数量达到几十个时,吞吐量会有些许下降
  • Kafka:大数据只能用kafka,单机吞吐量十万级,当topic数量达到十几个时,吞吐量会显著下降

消费幂等性

消费消息之前使用内存set或redis的set判重,在数据库中使用唯一键约束

丢失消息

  • 发送消息使用回调重试
  • ack设置为all,当所有集群节点都同步这条消息之后才算成功
  • 消费者手动提交偏移量

nginx

负载均衡

轮询 默认策略
weight 权重策略
ip_hash 根据客户端ip负载均衡
least_conn 最少连接方式
fair(第三方) 响应时间方式
url_hash(第三方) 依据URL分配方式
  1. 轮询
    每个请求会按时间顺序逐一分配到不同的后端服务器。

在轮询中,如果服务器down掉了,会自动剔除该服务器。
缺省配置就是轮询策略。
此策略适合服务器配置相当,无状态且短平快的服务使用。

  1. weight
    权重方式,在轮询策略的基础上指定轮询的几率。

权重越高分配到需要处理的请求越多。
此策略可以与least_conn和ip_hash结合使用。
此策略比较适合服务器的硬件配置差别比较大的情况。

  1. ip_hash
    指定负载均衡器按照基于客户端IP的分配方式,这个方法确保了相同的客户端的请求一直发送到相同的服务器,以保证session会话。这样每个访客都固定访问一个后端服务器,可以解决session不能跨服务器的问题。

在nginx版本1.3.1之前,不能在ip_hash中使用权重(weight)。
ip_hash不能与backup同时使用。
此策略适合有状态服务,比如session。
当有服务器需要剔除,必须手动down掉。

  1. least_conn
    把请求转发给连接数较少的后端服务器。轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同;但是,有些请求占用的时间很长,会导致其所在的后端负载较高。这种情况下,least_conn这种方式就可以达到更好的负载均衡效果。

此负载均衡策略适合请求处理时间长短不一造成服务器过载的情况。

  1. fair
    按照服务器端的响应时间来分配请求,响应时间短的优先分配。
  2. url_hash
    按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,要配合缓存命中来使用。同一个资源多次请求,可能会到达不同的服务器上,导致不必要的多次下载,缓存命中率不高,以及一些资源时间的浪费。而使用url_hash,可以使得同一个url(也就是同一个资源请求)会到达同一台服务器,一旦缓存住了资源,再此收到请求,就可以从缓存中读取。

待续。。。

分布式事务

参考链接

jvm:https://www.jianshu.com/p/76959115d486
jvm:https://blog.csdn.net/m0_37524661/article/details/87432999
io:https://www.jianshu.com/p/8316ec8e185d
数据结构:https://blog.csdn.net/u010775025/article/details/79315361
数据结构:https://www.cnblogs.com/codingblock/p/7712596.html
数据结构:https://www.cnblogs.com/xuxinstyle/p/9537249.html
数据结构:https://www.cnblogs.com/williamjie/p/9358291.html
数据结构:https://blog.csdn.net/kingmax54212008/article/details/79662407
排序算法:https://www.cnblogs.com/nankeyimengningchenlun/p/9151701.html
多线程:https://www.cnblogs.com/Jansens520/p/8624708.html
多线程:https://www.cnblogs.com/syp172654682/p/9383335.html
多线程:https://www.cnblogs.com/jpfss/p/11016169.html
设计模式:https://blog.csdn.net/jason0539/article/details/44956775
http:https://my.oschina.net/u/3242075/blog/2245520
http:https://blog.csdn.net/Boring_Wednesday/article/details/83189743
http:https://www.cnblogs.com/Java3y/p/8444033.html
http:https://blog.csdn.net/lvyibin890/article/details/82462041
get/post:https://www.cnblogs.com/logsharing/p/8448446.html
mysql:https://www.cnblogs.com/frankielf0921/p/5930743.html
mysql:https://www.cnblogs.com/williamjie/p/11187470.html
mysql:https://blog.csdn.net/hxpjava1/article/details/80908763
mysql:https://www.liangzl.com/get-article-detail-20113.html
缓存:https://blog.csdn.net/l2009103205/article/details/83305825
缓存:https://blog.csdn.net/kongtiao5/article/details/82771694
缓存:https://blog.csdn.net/LO_YUN/article/details/97131426
缓存:https://blog.csdn.net/lzhcoder/article/details/79469123
一致性hash算法:https://blog.csdn.net/bntX2jSQfEHy7/article/details/79549368
一致性hash算法:https://blog.csdn.net/gerryke/article/details/53939212
nginx:https://www.cnblogs.com/1214804270hacker/p/9325150.html

你可能感兴趣的:(java,面试)