JAVA面试题汇总
- java基础
- 二 多线程
- 三 线程池
- 4 高并发编程中的优化
- 5 事务
- 六 微服务
- 七 Spring Cloud
- 八 数据库
- 9 分布式事务
- 10 Redis
- 11 Redis分布式锁
- 12 Elasticsearch
- 12.4 底层的 lucene 是什么?
- 12.5 为什么要使用 Elasticsearch?
- 12.6 Elasticsearch工作原理
- 12.7 Elasticsearch 主分片写数据的详细流程?
- 12.8 Elasticsearch 更新和删除文档的流程?
- 12.9 query 和 filter 的区别?
- 12.10 Elasticsearch 查询数据的流程?
- 12.11 Elasticsearch 索引文档的流程?
- 12.12 Elasticsearch 的分布式原理?
- 12.13 Elasticsearch 的 master 选举流程?
- 12.14 Elasticsearch 集群脑裂问题?
- 12.15 Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?
- 12.16 Elasticsearch 如果保证读写一致?
- 12.17 Elasticsearch性能优化
- 12.18 Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法?
- 12.19 GC 方面,在使用 Elasticsearch 时要注意什么?
- 12.20 建立索引阶段性能提升方法?
- 12.22 ES的深度分页与滚动搜索scroll
- 13 jvm 排查内存工具流程
- 14 消息队列
- 15 MongoDB
java基础
内容比较长,建议先收藏,求大大给点打赏,给我点动力
如果怕记不住,只需要看特殊加粗的部分即可
一 JVM
java 虚拟机,是运行java字节码的虚拟机,它针对不同的系统有着不同的实现,目的就是在不同的系统上运行相同的字节码文件时,有相同的结果。同时字节码和不同系统的jvm也是java“一次编译,随处运行”的关键所在。
1.1 JVM的运行过程
Java 文件->编译器>字节码->JVM->机器码
1.2 JVM组成
方法区、堆、虚拟机栈、本地方法栈、程序计数器
1.2.1 程序计数器
指向当前线程正在执行的字节码指令的地址
各线程之间独立存储,互不影响。
由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。
程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域
1.2.2 虚拟机栈
Java 虚拟机栈用于管理 Java 函数的调用
栈是先进后出(FILO)的数据结构
栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。
每个栈帧,都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k
局部变量表
顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。
操作数据栈
存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法就是JVM一直运行入栈/出栈的操作
动态连接
Java语言特性多态(需要类运行时才能确定具体的方法)。
返回地址
正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)
1.2.3 本地方法栈
本地方法栈用于管理本地方法的调用。
1.2.4 方法区/永久代
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。
常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。
而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。
Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地
元空间大小参数
jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)
1.2.5 堆
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
堆空间一般是程序启动时,就申请了,但是并不一定会全部使用
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。
堆大小参数:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m
1.3 堆和栈的区别
功能
- 以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
- 而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
线程独享还是共享
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
- 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
空间大小
栈的内存要远远小于堆内存
1.4 内存溢出
-
栈溢出
栈的大小是固定的,是不支持拓展的。
java.lang.StackOverflowError 一般的方法调用是很难出现的,如果出现了可能会是无限递归
OutOfMemoryError:不断建立线程,JVM申请栈内存,机器没有足够的内存。
-
堆溢出
内存溢出:申请内存空间,超出最大堆内存空间。
如果是内存溢出,则通过 调大 -Xms,-Xmx参数。
1.5 对象存活性判断
-
引用技术法
在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1。
缺点:因为存在对象相互引用的情况,需要引入额外的机制来处理,这样做影响效率。
-
可达性分析
基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
1.6 垃圾回收算法
1.6.1 分代收集理论
-
绝大部分的对象都是朝生夕死
-
熬过多次垃圾回收的对象就越难回收。
根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代和老年代。
1.6.2 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,优点:内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。缺点:只是这种算法的代价是将内存缩小为了原来的一半。
注意:内存移动是必须实打实的移动(复制),不能使用指针玩。
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
1.6.3 标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
回收效率不稳定,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率很低。
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。复制回收算法适用于新生代。
二 多线程
2.1 进程、线程、并行、并发、同步、异步
进程:进程是程序在操作系统中的一次运行活动。进程是资源分配的基本单位。进程是线程的容器。
线程:线程是操作系统调度的最小单位。线程是一系列作业的组合。线程是独立调度和分派的基本单位。
并行:在同一时刻,做多件事情。并行针对的时间点。
并发:在一段时间内,同时做多件事情。并发针对的时间段。
同步:在请求资源后没有做任何事情,直到获取资源。
异步:在请求资源后做了其他的事情,等到获取资源后,再执行请求资源后面发操作。
同步是阻塞的,异步是非阻塞的。异步的性能高于同步
2.2 多线程
Java 中的多线程是通过java.lang.Thread类来实现的。
一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。 当然如果发生异常,会影响主线程。
2.2.1 多线程
多线程可以同时执行多个任务,提高计算机系统CPU的利用率。
何时需要多线程?
- 程序需要同时执行两个或多个任务。
- 需要一些后台运行的程序时。比如:Java后台运行的GC功能。
主线程
- 即使Java程序没有显示的来声明一个线程,Java也会有一个线程存在该线程叫做主线程
- 可以调用Thread.currentThread()来获得当前线程。
2.3 线程的创建方法
- 继承Thread类(MyThread extends Thread),需要覆盖run方法。
- 实现Runnable接口 Runnable 中有一个方法run用来定义线程执行代码。
- 实现Callable接口 实现call()方法来定义线程执行代码。
Callable和Runnable的区别
- Callable规定的方法是call(),Runnable规定的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能有返回值。
- call方法可以抛出异常,run方法不可以。
开发时,优先使用实现Runnable()接口的方式创建线程
- 实现接口的方式没有类的单继承性的局限性。
- 实现接口的方式更适合来处理多个线程有共享数据的情况。
2.4 线程的启动和终止
线程的启动:
线程的启动需要调用Thread的start方法,不能直接调用run方法,如果直接调用run方法相当于方法调用。
线程的终止:
当run方法返回,线程终止,一旦线程终止后不能再次启动。
线程的终止可以调用线程的interrupt方法,但该方法不是最佳方法,最好是设置一个标记来控制线程的终止。
注意事项:一个线程只能启动一次,不能多次启动同一个线程。
2.5 线程控制基本方法
- Thread类的有关方法
启动线程并执行对象的run()方法。
void start()
线程在被调度时执行的操作。
run()
返回线程的名称。
String getName()
设置该线程名称。
void setName(String name)
返回当前线程。
static Thread currentThread()
- 线程控制的基本方法
判断线程是否存活
isAlive()
获得线程的优先级
getPriority()
设置线程的优先级 填入 1~10 数字越大,优先级越高,占用cpu资源的时间越长
setPriority(int newPriority)
使线程睡眠 参数是毫秒值 注意:线程是阻塞状态,并没有释放cpu资源
sleep(long miliseconds)
合并线程
join()
使线程礼让出cpu资源,但不一定生效,可能存在礼让后重新竞争到cpu资源
yield()
使线程等待 注意:线程为非阻塞状态,释放了cpu资源
wait()
通知线程/通知所有线程
notify()/notifyAll()
2.6 线程的同步
为什么需要线程同步:
一条数据如果被多个线程同时访问操作,会造成脏数据的生成。
线程安全问题需要同时满足三个条件:
1. 同一时间
2. 多个线程
3. 操作同一条数据
2.7 如何解决线程安全问题
** 1. 加锁**
锁是一种用于控制多个线程对共享资源访问的机制。它确保在同一时间只有一个线程可以访问被锁保护的代码块或对象。
2. 锁有哪些
按照类别来说,常见的锁有以下几种
| 序号 | 锁名称 | 应用 |
|---|---|---|
| 1 | 乐观锁 | CAS |
| 2 | 悲观锁 | synchronized、vector、hashtable |
| 3 | 自旋锁 | CAS |
| 4 | 可重入锁 | synchronized、Reentrantlock、Lock |
| 5 | 读写锁 | ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet |
| 6 | 公平锁 | Reentrantlock(true) |
| 7 | 非公平锁 | synchronized、reentrantlock(false) |
| 8 | 共享锁 | ReentrantReadWriteLock中读锁 |
| 9 | 独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中写锁 |
| 10 | 重量级锁 | synchronized |
| 11 | 轻量级锁 | 锁优化技术 |
| 12 | 偏向锁 | 锁优化技术 |
| 13 | 分段锁 | concurrentHashMap |
| 14 | 互斥锁 | synchronized |
| 15 | 同步锁 | synchronized |
| 16 | 死锁 | 相互请求对方的资源 |
| 17 | 锁粗化 | 锁优化技术 |
| 18 | 锁消除 | 锁优化技术 |
3. 乐观锁
- 是一种乐观思想,抽象的锁,认为读多写少,每次拿数据时默认对方不会修改数据。
- 但是会在更新的时候进行判断期间是否有其他线程对其进行修改,写之前读取当前版本号,一样则更新,不一样重复 读-比较-写 的操作。
- 基本通过CAS实现。CAS是一种原子操作,比较当前值跟传入值一样则更新,否则失败
3. 悲观锁
- 是一种悲观思想,抽象的锁,认为写多读少,每次拿数据都认为别人会修改。
- 所以每次读取时都会上锁,这样别人想读写这个数据时会Block至拿到锁。
- JAVA中悲观锁就是synchronized;AQS(AbstractQueuedSynchronizer)框架下的锁先尝试CAS乐观锁去获取锁,获取不到才转换为悲观锁,如RetreenLock。
4. 互斥锁和同步锁
- 都是悲观锁。
- 同步锁在很多情况下可以理解为是一种互斥锁,但它们并不完全等同。
相似之处:
1、互斥性:
- 同步锁和互斥锁都具有互斥的特性,即在同一时间只允许一个线程访问被保护的资源或代码区域。
2、保证数据一致性:
- 两者都是为了防止多个线程同时访问共享资源时出现数据不一致的问题。通过对资源的互斥访问,确保线程在修改共享数据时不会被其他线程干扰。
不同之处:
1、概念范围:
- 同步锁更侧重于实现线程之间的同步操作,确保线程按照特定的顺序执行或访问共享资源。同步可以包括多种方式,如互斥锁只是其中一种实现同步的手段。
2、实现方式:
- 同步锁可以通过多种方式实现,如synchronized关键字、ReentrantLock等。这些实现方式在某些方面可能有不同的特性和功能。
- 互斥锁通常更强调对资源的独占访问,实现方式相对较为单一,主要是通过类似ReentrantLock这样的明确的互斥锁机制来实现。
综上所述,同步锁和互斥锁有很多相似之处,但在概念范围和实现方式上存在一些差异。在很多情况下,同步锁可以表现为互斥锁的行为,但不能简单地认为同步锁就是互斥锁。
3. 锁优化
1)减少锁持有时间
只用在有线程安全要求的程序上加锁
2)减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
3)锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。
读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据。
4)锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。锁粗化是一种优化技术,指的是在程序执行过程中,当一系列连续的对同一锁的操作被检测到时,虚拟机可能会将这些锁操作合并为一个范围更大的锁操作,以减少锁获取和释放的次数,从而提高性能。
5)锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。
2.8 死锁
死锁 是指两个或多个线程因互相等待对方释放资源而导致的无法继续执行的状态。死锁通常发生在多线程之间的资源竞争和不当的锁管理中。
2.9 避免死锁的策略
- 锁的顺序:通过规定获取锁的顺序来避免死锁。
- 使用 tryLock():在尝试获取锁时,使用非阻塞式的 tryLock() 方法。
- 超时机制:设置超时时间,避免因无法获取锁而长时间阻塞。
三 线程池
3.1 什么是线程池
Java 线程池是并发编程中重要的工具之一,它通过管理和复用线程,避免了频繁创建和销毁线程带来的开销,从而提高了应用程序的性能和响应速度。
3.2 线程池的优势
- 提高性能:线程池通过复用线程,避免了创建和销毁线程的开销,尤其在处理大量短期任务时,性能提升显著。
- 更好的资源管理:线程池限制了并发线程的数量,防止系统因创建过多线程而耗尽资源。
- 提高系统的稳定性:通过线程池,可以更好地控制任务的执行和线程的使用,防止线程泄漏和过载。
3.3 线程池的实现
3.3.1 Executor 和 ExecutorService
- Executor接口:是一个简单的接口,用于提交任务,但不提供管理线程池的方法。
public interface Executor {
void execute(Runnable command);
}
- ExecutorService 接口:继承了 Executor,增加了管理线程池的方法,例如启动、关闭和提交任务。
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
<T> Future<T> submit(Callable<T> task);
// 其他方法...
}
3.3.2 ThreadPoolExecutor 类
ThreadPoolExecutor 是 ExecutorService 接口的主要实现类,它提供了对线程池的全面控制。使用 ThreadPoolExecutor 可以精确配置线程池的行为,包括核心线程数、最大线程数、线程空闲时间、任务队列等。
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
) {
// 实现代码...
}
- 核心线程数:线程池中始终保持存活的线程数量。
- 最大线程数:线程池中允许的最大线程数量。
- 线程空闲时间:当线程池中的线程数大于核心线程数时,多余的空闲线程在等待新任务时最多保持多长时间的存活。
- 任务队列:用于保存等待执行的任务,常用的队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- 线程工厂:用于创建新线程,通常用于为线程命名或设置线程优先级。
- 拒绝策略:当任务无法执行时的处理策略,常见的策略有 AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最早的任务)。
3.4 线程池的工作流程
线程池的工作流程如下:
- 当任务提交到线程池时,如果当前线程数小于核心线程数,线程池会创建一个新线程来执行任务。
- 如果当前线程数已达到核心线程数,任务会被放入任务队列等待执行。
- 如果任务队列已满,并且线程数未达到最大线程数,线程池会创建新线程执行任务。
- 如果线程数已达到最大线程数,并且任务队列也已满,则执行拒绝策略。
- 当线程空闲超过指定时间且线程数超过核心线程数时,空闲线程会被终止,直到线程数等于核心线程数。
3.5 常见的线程池类型
Java 提供了几个常用的线程池实现,可以通过 Executors 工具类快速创建:
3.5.1 FixedThreadPool
- 定义:一个固定大小的线程池,核心线程数和最大线程数相等,并且线程不会被回收。
- 适用场景:适用于需要稳定执行任务数的场景。
ExecutorService executor = Executors.newFixedThreadPool(5);
3.5.2 CachedThreadPool
- 定义:一个根据需要创建新线程的线程池,空闲线程超过60秒会被回收。线程池可以无限扩展以适应高负载。
- 适用场景:适用于大量短期异步任务或轻负载场景。
ExecutorService executor = Executors.newCachedThreadPool();
3.5.3 SingleThreadExecutor
- 定义:只有一个线程的线程池,所有任务按照提交顺序依次执行。
- 适用场景:适用于需要顺序执行任务的场景,例如日志记录等。
ExecutorService executor = Executors.newSingleThreadExecutor();
3.5.4 ScheduledThreadPool
- 定义:支持定时和周期性任务调度的线程池。
- 适用场景:适用于需要执行定时任务或周期性任务的场景,例如定时备份、定时任务调度等。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
3.6 线程池的使用场景和最佳实践
3.6.1 使用场景
- 高并发处理:在线程池中复用线程可以提高系统处理高并发请求的能力,特别是在Web服务器、数据处理系统等场景下。
- 资源管理:通过线程池控制最大并发线程数,避免系统资源被过度消耗。
- 定时任务调度:使用 ScheduledThreadPool 可以方便地实现定时任务调度,如定时报告生成、定时数据采集等。
3.6.2 最佳实践
- 合理配置线程池大小:线程池大小应根据任务的性质(CPU密集型、I/O密集型)来配置。对于CPU密集型任务,线程数应设置为 CPU核数+1;对于I/O密集型任务,线程数可以设置为 CPU核数×2 或更高。
- 使用合适的任务队列:根据任务的特点选择适合的任务队列,如 LinkedBlockingQueue 用于无界队列,ArrayBlockingQueue 用于有界队列。
- 避免阻塞任务:如果任务可能会阻塞线程,尽量不要在核心线程中执行,或者调整线程池的配置以容纳更多线程。
- 监控线程池:使用 ThreadPoolExecutor 提供的监控方法(如 getPoolSize、getActiveCount、getTaskCount 等)来监控线程池的运行情况,并根据需要调整配置。
3.7 线程池的常见问题与解决方案
3.7.1 线程泄露
线程泄漏是指线程池中的线程未被正常回收,导致线程池中的线程数逐渐增加,最终可能耗尽系统资源。为避免线程泄漏:
- 确保任务能正常完成,不要让线程永久阻塞。
- 设置合理的线程空闲时间,及时回收不再需要的线程。
3.7.2 拒绝策略的选择
当线程池满载时,任务会被拒绝。选择合适的拒绝策略可以避免任务丢失或性能下降。
- AbortPolicy:抛出异常,适合需要立即发现问题的场景。
- CallerRunsPolicy:由调用线程执行任务,适合任务可回退执行的场景。
- DiscardPolicy:直接丢弃任务,适合允许部分任务失败的场景。
- DiscardOldestPolicy:丢弃最早的未处理任务,适合优先处理新任务的场景。
3.7.3 任务积压
任务积压是指任务队列中的任务数量逐渐增加,最终可能导致队列溢出或任务延迟过长。解决方法包括:
- 增加线程池的大小,以提高并发处理能力。
- 使用有界队列,防止任务无限积压。
- 优化任务
4 高并发编程中的优化
4.1 使用线程池
线程池是一种常见的资源池管理方式,可以有效地减少线程创建和销毁的开销。Java 提供了 ExecutorService 接口及其实现类,如 ThreadPoolExecutor,用于线程池的管理
4.2 无锁编程
无锁编程 使用原子操作来避免线程同步带来的性能损耗。Java 提供了多种原子类(如 AtomicInteger、AtomicReference)来进行无锁操作。
AtomicInteger atomicCounter = new AtomicInteger(0);
atomicCounter.incrementAndGet(); // 原子增加操作
4.3 高效的数据结构
在高并发场景中,选择合适的数据结构非常重要。Java 提供了几种适合高并发的线程安全数据结构:
- ConcurrentHashMap:线程安全的哈希表,支持并发读写操作。
- CopyOnWriteArrayList:适用于读多写少的场景,写操作会复制整个数组。
- BlockingQueue:用于多线程之间的通信,提供线程安全的队列操作,如LinkedBlockingQueue、ArrayBlockingQueue。
4.3.1 ConcurrentHashMap
通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。
如果把整个ConcurrentHashMap看作是一个父哈希表的话,那么每个Segment就可以看作是一个子哈希表
它既不允许key值为null,也不允许value值为null。
ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:
- 用HashEntery对象的不变性来降低读操作对加锁的需求
- 用Volatile变量协调读写线程间的内存可见性
- 若读时发生指令重排序现象,则加锁重读
4.4 常见并发问题及解决方案
4.4.1 线程安全与并发控制
- 线程安全:指多个线程同时访问某个资源时,能够正确地协调操作,确保数据的一致性和正确性。
- 并发控制:通过合理的锁机制或无锁编程,控制线程对共享资源的访问,避免竞争条件。
4.4.2 性能优化
- 减少锁竞争:尽量减小锁的粒度,避免长时间持有锁,使用非阻塞算法。
- 避免频繁创建线程:线程池可以复用线程,避免频繁的线程创建和销毁。
- 合理使用原子性操作:对于简单的加减操作,使用 Atomic 类避免不必要的同步开销。
4.4.3 可伸缩性与高吞吐量设计
- 垂直扩展与水平扩展:设计高并发应用时,考虑如何垂直扩展(增强单机性能)和水平扩展(增加更多节点)。
- 事件驱动与异步编程:采用异步处理机制,例如基于消息队列的设计,来提高系统的吞吐量和响应能力。
5 事务
5.1 实现事务:
实现事务的方式大类共有两大类,一种是编程式事务,另一种是声明式事务。
- 编程式事务:好处在于他的最小事务粒度可以达到某个代码块上,且可以根据具体的业务逻辑进行调整;但是坏处在于事务的代码实现和业务逻辑混合在一块,这样在管理业务代码和事务代码上就有了很强的耦合性,不方便管理。
- 声明式事务:好处在于主要是通过aop的方式实现的,在方法上加上注解就可以在方法开始之前开启事务,在方法结束后选择提交事务或者是回滚事务,这样将业务逻辑和事务代码相隔开来更容易管理,实现了非侵入式管理;但是缺点也显而易见, 那就是事务的最小粒度为一个方法,无法做到一个代码块上。
5.1.1 编程式事务
这类实现事务的方式主要是通过两种方式实现的,一种是通过PlatformTransactionManager,另一种是通过TransactionTemplate。
这两种都可以实现事务,但是各有千秋,比较推荐使用TransactionTemplate这种来实现。
对于这种有两个内部类实现:TransactionCallbackWithoutResult 和 TransactionCallback
第一种无返回值,第二种有返回值,根据具体业务代码进行选择。
5.1.2 声明式事务
这类事务实现方式主要就是通过 @Transaction 注解来实现的,我们又知道注解大部分有事通过aop的方式进行实现的,所以我们就可以在aop中来定义我们具体的事务处理。
首先,这里还是简单说一下什么是事务的ACID性质:
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency):一旦事务完成(不管是成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏;
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏;
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中;
5.1.3 一致性
对于一致性的处理,我们通常采用事务进行解决,这里就要说到事务的7个传播机制了,这里先一一列出,然后我们具体来看区别:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。

Propagation.REQUIRED事务的默认传播机制。
这种传播机制就是当两个方法都存在事务的话,在一个方法中调用另一个方法如果成功就会更新到数据库,如果其中一个出现问题,就不会更新到数据库,而是进行回滚。
5.2 场景分析
5.2.1 方法A有事务,方法B没有事务
在这种情况下,方法A开启了事务,而方法B没有事务注解,因此它的事务行为完全取决于方法A的传播机制。
5.2.1.1 Propagation.REQUIRED
- 调用流程:方法A有事务,方法B没有事务。
- 行为:方法B会加入方法A的事务。即使方法B没有事务注解,它仍然会在方法A的事务中运行。
- 回滚:如果方法A抛出异常,整个事务(包括方法B的操作)都会回滚。
5.2.1.2 Propagation.REQUIRES_NEW
- 调用流程:方法A有事务,方法B没有事务。
- 行为:当方法A调用方法B时,方法B会开启一个新的事务,并且方法A的事务会被挂起。方法A和方法B的事务相互独立。
- 回滚:方法A的异常不会影响方法B的事务,反之亦然。方法B的事务会在方法B执行完后提交,而方法A的事务会在方法A执行完后提交或回滚。
5.2.1.3 Propagation.SUPPORTS
- 调用流程:方法A有事务,方法B没有事务。
- 行为:方法B会加入方法A的事务(因为方法A有事务)。因此,方法B的操作在方法A的事务中运行。
- 回滚:方法A回滚时,方法B的操作也会回滚。
5.2.1.4 Propagation.NOT_SUPPORTED
- 调用流程:方法A有事务,方法B没有事务。
- 行为:当方法A调用方法B时,方法B会挂起方法A的事务,在无事务的上下文中执行。方法B的操作不会受到任何事务的影响。
- 回滚:方法A回滚时,不会影响方法B的操作,因为方法B没有在事务中运行。
5.2.1.5 Propagation.MANDATORY
- 调用流程:方法A有事务,方法B没有事务。
- 行为:方法B要求在事务中运行,因此它会加入方法A的事务。如果方法A没有事务,方法B会抛出异常。
- 回滚:方法A回滚时,方法B的操作也会回滚。
5.2.1.6 Propagation.NEVER
- 调用流程:方法A有事务,方法B没有事务。
- 行为:方法B不允许在事务中运行,但方法A有事务,因此当方法A调用方法B时,会抛出异常,因为方法B拒绝在事务环境下运行。
- 回滚:方法B不会执行,直接抛出异常。
5.2.1.7 Propagation.NESTED
- 调用流程:方法A有事务,方法B没有事务。
- 行为:方法B会在方法A的事务内启动一个嵌套事务。嵌套事务允许方法B的部分事务可以独立提交或回滚,而不影响方法A的事务。
- 回滚:如果方法A回滚,嵌套事务也会回滚;但是如果方法A不回滚,方法B可以单独回滚自己的嵌套事务。
5.2.2 方法A没有事务,方法B有事务
在这种情况下,方法A没有事务,方法B有事务,因此传播机制决定方法B是否创建或加入现有事务。
5.2.2.1 Propagation.REQUIRED
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B会开启一个新的事务,因为方法A没有事务。
- 回滚:方法B的事务完全独立于方法A的调用环境,异常回滚仅限于方法B的事务。
5.2.2.2 Propagation.REQUIRES_NEW
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B会强制创建一个新的事务,与方法A无关。
- 回滚:方法B的事务独立,方法A无事务,异常回滚仅限于方法B。
5.2.2.3 Propagation.SUPPORTS
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B在无事务的上下文中运行,因为方法A没有事务。因此,方法B不会开启事务。
- 回滚:无事务情况下,方法B的操作不受回滚影响。
5.2.2.4 Propagation.NOT_SUPPORTED
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B会在无事务的上下文中执行,因为传播机制要求暂停任何事务。
- 回滚:方法B没有事务,因此不会回滚。
5.2.2.5 Propagation.MANDATORY
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B要求有事务才能执行,而方法A没有事务,因此会抛出异常。
- 回滚:方法B不会执行,直接抛出异常。
5.2.2.6 Propagation.NEVER
- 调用流程:方法A没有事务,方法B有事务。
- 行为:方法B要求无事务才能执行,而方法A没有事务,因此方法B会执行。
- 回滚:方法B在无事务的上下文中执行,因此不受回滚影响。
5.2.2.7 Propagation.NESTED
- 调用流程:方法A没有事务,方法B有事务。
- 行为:因为方法A没有事务,所以方法B会启动一个新的事务,类似于REQUIRED。
- 回滚:方法B的事务独立运行,回滚仅限于方法B。
5.2.3 方法A和方法B都有事务
在这种情况下,两个方法都有事务,因此传播机制决定它们是否共享同一个事务,或者在独立事务中执行。
5.2.3.1 Propagation.REQUIRED
- 调用流程:方法A和方法B都有事务
- 行为:方法B会加入方法A的事务,二者共享同一个事务。
- 回滚:如果方法A回滚,方法B的操作也会回滚,反之亦然。
5.2.3.2 Propagation.REQUIRES_NEW
- 调用流程:方法A和方法B都有事务。
- 行为:方法B会启动一个新的事务,方法A的事务会被挂起。二者在独立的事务中执行。
- 回滚:方法A和方法B的回滚互不影响。
5.2.3.3 Propagation.SUPPORTS
- 调用流程:方法A和方法B都有事务。
- 行为:方法B会加入方法A的事务,二者共享同一个事务。
- 回滚:如果方法A回滚,方法B的操作也会回滚,反之亦然。
5.2.3.4 Propagation.NOT_SUPPORTED
- 调用流程:方法A和方法B都有事务。
- 行为:方法B会暂停方法A的事务,在无事务的上下文中执行。
- 回滚:方法B的操作不受事务控制,方法A的回滚不影响方法B的操作。
5.2.3.5 Propagation.MANDATORY
- 调用流程:方法A和方法B都有事务。
- 行为:方法B会加入方法A的事务,二者共享同一个事务。
- 回滚:如果方法A回滚,方法B的操作也会回滚,反之亦然。
5.2.3.6 Propagation.NEVER
- 调用流程:方法A和方法B都有事务。
- 行为:方法B拒绝在事务中执行,因此会抛出异常。
- 回滚:方法B不会执行,直接抛出异常。
5.2.3.7 Propagation.NESTED
- 调用流程:方法A和方法B都有事务。
- 行为:方法B会启动一个嵌套事务,方法A和方法B共享相同的事务边界,但方法B的操作可以独立回滚。
- 回滚:方法B回滚时不影响方法A,方法A回滚时方法B的嵌套事务也会回滚。
5.3 隔离性
隔离性我们就要说到隔离级别了,在事务中有五种隔离级别:
- DEFAULT:Spring 中默认的事务隔离级别,以连接的数据库的事务隔离级别为准;
- READ_UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读;
- READ_COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读;
- REPEATABLE_READ:可重复读,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read);
- SERIALIZABLE:串行化,最高的事务隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。

一般默认事务隔离级别为REPEATABLE_READ
5.3.1 问题场景和解决方案
- 读未提交(READ_UNCOMMITTED):这种就可能会出现脏读的情况,解决脏读的办法就是将隔离级别提高到读已提交(READ_COMMITTED)
- 不可重复读(REPEATABLE_READ):这种情况就是事务A读取数据后,另一个事务修改了该数据,然后事务A再次读取这个数据,发现两次读取的数据不一样,处理方法可以使用MVCC来进行处理
- 幻读:这种就是事务A读取数据后,事务B对这个区间的数据进行了增加或者删除,导致事务A再次读取这个区间的数据的时候发现两次读取的数据行数不一样,处理方法有两种:第一种,在RR隔离级别下,使用MVCC+间隙锁的方式进行控制;第二种,直接使用**串行化(SERIALIZABLE)**这种隔离级别处理。
六 微服务
目前以Spring Cloud 为例进行总结讲解
6.1 单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:
- 架构简单
- 部署成本低
缺点:
- 耦合性高(维护困难、升级困难)
6.2 分布式架构
分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
优点:
- 降低服务耦合
- 有利于服务的升级和拓展
缺点:
- 服务调用关系错综复杂
6.3 微服务
微服务就是将一个单体架构的应用按业务划分为一个个的独立运行的程序即服务,它们之间通过 HTTP 协议(也可以采用消息队列来通信,如 RabbitMQ,Kafaka 等)进行通信。
微服务是一个架构风格。一个大型的复杂软件应用,由一个或多个微服务组成。
6.4 特点
-
单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责。
-
微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
-
面向服务:面向服务是说每个服务都要对外暴露服务接口API,指的就是提供统一标准的接口。也就是不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供 Rest 的接口即可。
-
自治:自治是说服务间互相独立,互不干扰,即团队独立、技术独立、数据独立,独立部署和交付。
-
团队独立:每个服务都是一个独立的开发团队,人数不能过多。
-
技术独立:因为是面向服务,提供 Rest 接口,使用什么技术没有别人干涉。
-
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
-
前后端分离:采用前后端分离开发,提供统一 Rest 接口,后端不用再为 PC、移动端开发不同接口。
-
数据库分离:每个服务都使用自己的数据源。
-
部署独立:服务间虽然有调用,但要做到服务重启不影响其它服务。
所以,如果我们要搭建一个微服务项目,则需要遵循以下要点:
- 将系统服务层完全独立出来,并将服务层抽取为一个一个的微服务。
- 微服务中每一个服务都对应唯一的业务能力,遵循单一原则。
- 微服务之间采用 RESTful 等轻量协议传输。

6.5 微服务技术栈
| 微服务条目 | 落地技术 |
|---|---|
| 服务开发 | SpringBoot,Spring,SpringMVC |
| 服务配置与管理 | Netflix公司的Archaius、阿里的Diamond等 |
| 服务注册与发现 | Eureka、Consul、Zookeeper等 |
| 服务调用 | Rest、RPC、gRPC |
| 服务熔断器 | Hystrix、Envoy等 |
| 负载均衡 | Ribbon、Nginx等 |
| 服务接口调用(客户端调用服务的简化工具) | Feign等 |
| 消息队列 | Kafka、RabbitMQ、ActiveMQ等 |
| 服务配置中心管理 | SpringCloudConfig、Chef等 |
| 服务路由(API网关) | Zuul等 |
| 服务监控 | Zabbix、Nagios、Metrics、Specatator等 |
| 全链路追踪 | Zipkin、Brave、Dapper等 |
| 服务部署 | Docker、OpenStack、Kubernetes等 |
| 数据流操作开发包 | SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接收消息) |
| 事件消息总线 | SpringCloud Bus |
七 Spring Cloud
SpringCloud, 基于SpringBoot提供了一套微服务解决方案,包括服务注册与发现,配置中心,全链路监控,服务网关,负载均衡,熔断器等组件,除了基于NetFlix的开源组件做高度抽象封装之外,还有一些选型中立的开源组件。
7.1 什么是服务熔断?什么是服务降级?
- 熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。
- 服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端 可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用, 比直接挂掉强。
7.1.1 雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应”
7.1.2 微服务中常见的容错方案
要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施, 常见的服务容错思路有:
隔离
超时
限流
熔断
降级
-
隔离:它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离.
-
超时:在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
-
限流:限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
-
熔断:在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
- 服务熔断一般有三种状态:
- 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
- 熔断开启状态(Open):后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
- 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
- 服务熔断一般有三种状态:
-
降级:降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
**Hystrix 相关注解 **:
@EnableHystrix:开启熔断
@HystrixCommand(fallbackMethod=”XXX”) :声明一个失败回滚处理函数 XXX ,当被注解的方法执行超时(默认是 1000 毫秒),就会执行 fallback 函数,返回错误提示。
7.2 Eureka和zookeeper都可以提供服务注册与发现的功能,请 说说两个的区别?
Zookeeper 保证了 CP ( C :一致性, P :分区容错性), Eureka 保证了 AP ( A :高可用)
-
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接 down掉不可用。也就是说,服务注册功能对高可用性要求比较高。但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。 在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够 恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。
-
Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失 败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
- Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。
- Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
- 当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka 可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像 Zookeeper 那样使整个微服务瘫痪
7.3 SpringBoot和SpringCloud的区别?
- SpringBoot专注于快速方便的开发单个个体微服务。
- SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来, 为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策 竞选、分布式会话等等集成服务。
- SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于 依赖的关系. SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
7.4 负载平衡的意义什么?
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
7.5 什么是Hystrix?它如何实现容错?
Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避
免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。
通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。
7.6 什么是Hystrix断路器?我们需要它吗?
由于某些原因,employee-consumer 公开服务会引发异常。在这种情况下使用 Hystrix 我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。
如果 methodA() 中的异常继续发生,则 Hystrix 电路将中断,并且调用者将一起跳过 methodA() 方法,并直接调用回退方法。 断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。
7.7 说说 RPC 的实现原理
首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 务接口的一个代理实现,这个 代理实现负责收集数据、编码并传输给服务器然后等待结果返回。
7.8 eureka自我保护机制是什么?
当 Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户
端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保
护模式。
7.9 什么是Ribbon?
ribbon 是一个负载均衡客户端,可以很好的控制 htt 和 tcp 的一些行为。 feign 默认集成了 ribbon 。
7.10 什么是 feigin ?它的优点是什么?
- feign 采用的是基于接口的注解
- feign 整合了 ribbon ,具有负载均衡的能力
- 整合了 Hystrix ,具有熔断的能力
使用 :
- 添加 pom 依赖。
- 启动类添加 @EnableFeignClients
- 定义一个接口 @FeignClient(name=“xxx”)指定调用哪个服务
7.11 Ribbon和Feign的区别?
- Ribbon 都是调用其他服务的,但方式不同。
- 启动类注解不同, Ribbon 是@RibbonClient feign 的是 @EnableFeignClients。
- 服务指定的位置不同, Ribbon 是在 @RibbonClient 注解上声明,Feign 则是在定义抽象方法的接口中使用 @FeignClient 声明。
- 调用方式不同, Ribbon 需要自己构建http请求,模拟 http 请求。
八 数据库
8.1 索引
8.1.1 索引类型
-
主键索引(PRIMARY KEY):
唯一标识表中的每行,不能有重复值。
主键自动建立索引。
-
唯一索引(UNIQUE KEY):
确保某一列的每行数据是唯一的。
可以为NULL值,但只能有一个NULL值。
-
普通索引(INDEX):
用于加速查询。
可以在同一张表的不同列上创建多个索引。
-
全文索引(FULLTEXT):
用于全文检索。
适用于大型数据集,提高查询响应时间。
只能用于MyISAM和InnoDB引擎。
-
组合索引:
针对多个列创建的索引。
用于复合查询,其效率高于多个单列索引。
-
空间索引(SPATIAL):
用于对空间数据类型的列(如GEOMETRY)创建索引。
主要用于MyISAM引擎,支持OpenGIS的几何数据。
8.1.2 适合使用索引的场景
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其它表关联的字段,外键建立索引
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组字段
8.1.3 不适合使用索引的场景
- 表数据量太少
- where条件里用不到或关联条件用不到的字段
- 值大量重复或差异化很小的字段,不适合建立索引,性价比不高
8.2 sql优化
8.2.1 使用索引优化查询
当你的数据库表中有大量数据,而你需要频繁进行搜索查询时,索引是提高查询效率的关键。
--为department_id字段创建索引
CREATE INDEX idx_department ON employees(department_id);
为department_id字段创建一个索引idx_department。这个操作会让基于department_id的查询更快
8.2.2 优化查询语句
避免使用高成本的SQL操作,如 SELECT *,尽量指定需要的列,减少数据传输和处理时间。
-- 不推荐的查询方式
SELECT * FROM employees;
-- 推荐的查询方式
SELECT id, name FROM employees;
第一个查询语句使用了SELECT * ,它会获取所有列,这在数据量大时非常低效。
第二个查询仅请求需要的id和name列,减少了数据处理的负担。
8.2.3 使用查询缓存
当相同的查询被频繁执行时,使用查询缓存可以避免重复的数据库扫描。
-- 启用查询缓存
SET global query_cache_size = 1000000;
SET global query_cache_type = 1;
-- 执行查询
SELECT name FROM employees WHERE department_id = 5;
通过设置query_cache_size和query_cache_type,我们启用了查询缓存。
当我们执行查询时,MySQL会检查缓存中是否已经有了该查询的结果,如果有,则直接返回结果,避免了重复的数据库扫描。
8.2.4 避免全表扫描
当表中数据量巨大时,全表扫描会非常耗时。通过使用合适的查询条件来避免全表扫描,可以显著提高查询效率。
假设我们需要查询员工表中特定部门的员工
-- 不推荐的查询方式,会导致全表扫描
SELECT * FROM employees WHERE name LIKE '%张%';
-- 推荐的查询方式
SELECT * FROM employees WHERE department_id = 3 AND name LIKE '%张%';
第一个查询使用了模糊匹配LIKE,但缺乏有效的过滤条件,可能导致全表扫描。
第二个查询在name字段的模糊匹配前,增加了对department_id的条件过滤,这样就可以先缩小查找范围,避免全表扫描。
8.2.5 使用JOIN代替子查询
在需要关联多个表的复杂查询中,使用JOIN代替子查询可以提高查询效率。
-- 不推荐的子查询方式
SELECT * FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'IT');
-- 推荐的JOIN查询方式
SELECT employees.* FROM employees JOIN departments ON employees.department_id = departments.id WHERE departments.name = 'IT';
第一个查询使用了子查询,这在执行时可能效率较低,特别是当子查询或主查询的结果集较大时。
第二个查询使用了JOIN操作,这通常比子查询更有效,尤其是在处理大型数据集时。
8.2.6 合理分页
在处理大量数据的列表展示时,合理的分页策略可以减少单次查询的负担,提高响应速度。
假设我们需要分页显示员工信息
-- 不推荐的分页方式,尤其是当off set值很大时
SELECT * FROM employees LIMIT 10000, 20;
-- 推荐的分页方式,使用更高效的条件查询
SELECT * FROM employees WHERE id > 10000 LIMIT 20;
第一个查询使用了LIMIT和较大的偏移量offset,在大数据集上执行时会逐行扫描跳过大量记录,效率低下。
第二个查询通过在WHERE子句中添加条件来避免不必要的扫描,从而提高分页效率。
8.2.7 利用分区提高性能
对于大型表,特别是那些行数以百万计的表,使用分区可以提高查询性能和数据管理效率。
假设我们需要对一个大型的订单表 orders 进行分区
CREATE TABLE orders (
order_id INT AUTO_INCREMENT,
order_date DATE,
customer_id INT,
amount DECIMAL(10, 2),
PRIMARY KEY (order_id)
) PARTITION BY RANGE ( YEAR(order_date) ) (
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023)
);
-- 查询特定年份的订单
SELECT * FROM orders WHERE order_date BETWEEN '2021-01-01' AND '2021-12-31';
我们为orders表创建了基于order_date字段的年份范围分区。
查询特定年份的数据时,MySQL只会在相关分区中搜索,提高了查询效率。
8.2.8 利用批处理减少I/O操作
在进行大量数据插入或更新时,批处理可以减少数据库的I/O操作次数,从而提高性能。
-- 批量插入数据
INSERT INTO employees (name, department_id)
VALUES
('张三', 1),
('李四', 2),
('王五', 3),
-- 更多记录
;
-- 批量更新数据
UPDATE employees
SET department_id = CASE name
WHEN '张三' THEN 3
WHEN '李四' THEN 2
-- 更多条件
END
WHERE name IN ('张三', '李四', -- 更多名称);
在批量插入示例中,我们一次性插入多条记录,而不是对每条记录进行单独的插入操作。
在批量更新示例中,我们使用CASE语句一次性更新多条记录,这比单独更新每条记录更有效率。
8.2.9 使用临时表优化复杂查询
对于复杂的多步骤查询,使用临时表可以存储中间结果,从而简化查询并提高性能。
-- 创建一个临时表来存储中间结果
CREATE TEMPORARY TABLE temp_employees
SELECT department_id, COUNT(*) as emp_count
FROM employees
GROUP BY department_id;
-- 使用临时表进行查询
SELECT departments.name, temp_employees.emp_count
FROM departments
JOIN temp_employees ON departments.id = temp_employees.department_id;
首先,我们通过聚合查询创建了一个临时表temp_employees,用于存储每个部门的员工计数。
然后,我们将这个临时表与部门表departments进行连接查询,这样的查询通常比直接在原始表上执行复杂的聚合查询要高效。
8.2.10 优化数据类型
在设计数据库表时,选择合适的数据类型对性能有显著影响。优化数据类型可以减少存储空间,提高查询效率。
-- 原始表结构
CREATE TABLE example (
id INT AUTO_INCREMENT,
description TEXT,
created_at DATETIME,
is_active BOOLEAN,
PRIMARY KEY (id)
);
-- 优化后的表结构
CREATE TABLE optimized_example (
id MEDIUMINT AUTO_INCREMENT,
description VARCHAR(255),
created_at DATE,
is_active TINYINT(1),
PRIMARY KEY (id)
);
在原始表中,使用了INT和TEXT这样的宽泛类型,这可能会占用更多的存储空间。
在优化后的表中,id字段改为MEDIUMINT,description改为长度有限的VARCHAR(255),created_at只存储日期,而is_active使用**TINYINT(1)**来表示布尔值。这样的优化减少了每行数据的大小,提高了存储效率。
8.2.11 避免使用函数和操作符
在WHERE子句中避免对列使用函数或操作符,可以让MySQL更有效地使用索引。
-- 不推荐的查询方式,使用了函数
SELECT * FROM employees WHERE YEAR(birth_date) = 1980;
-- 推荐的查询方式
SELECT * FROM employees WHERE birth_date BETWEEN '1980-01-01' AND '1980-12-31';
在第一个查询中,使用 YEAR() 函数会导致MySQL无法利用索引,因为它必须对每行数据应用函数。
第二个查询直接使用日期范围,这样MySQL可以有效利用birth_date字段的索引。
8.2.12 合理使用正规化和反正规化
数据库设计中的正规化可以减少数据冗余,而反正规化可以提高查询效率。合理平衡这两者,可以获得最佳性能。
-- 正规化设计
CREATE TABLE departments (
department_id INT AUTO_INCREMENT,
name VARCHAR(100),
PRIMARY KEY (department_id)
);
CREATE TABLE employees (
id INT AUTO_INCREMENT,
name VARCHAR(100),
department_id INT,
PRIMARY KEY (id),
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
-- 反正规化设计
CREATE TABLE employees_denormalized (
id INT AUTO_INCREMENT,
name VARCHAR(100),
department_name VARCHAR(100),
PRIMARY KEY (id)
);
在正规化设计中,departments和employees表被分开,减少了数据冗余,但可能需要JOIN操作来获取完整信息。
在反正规化设计中,employees_denormalized表通过直接包含部门信息来简化查询,提高读取性能,但可能会增加数据冗余和更新成本。
9 分布式事务
9.1 为什么需要分布式事务
一般单体架构中,基于数据库本身的特性,其实已经可以满足ACID了。但是,如果放到微服务中,可能一个业务就会跨越多个服务,每个服务又有各自的数据库。这个时候如果单单靠数据库本身的特性,就无法保证ACID了。
9.2 分布式事务的思路
我们分布式事务会出现问题,主要是因为要处理的事务中,往往会包含多个子事务,他们会各自执行与提交,结果他们一些成功,一些失败。他们的状态不一致。我们只需要保证他们最后的状态一致就可以解决了。
9.3 解决分布式事务的思路(基于CAP与BASE)
我们分布式事务会出现问题,主要是因为要处理的事务中,往往会包含多个子事务,他们会各自执行与提交,结果他们一些成功,一些失败。他们的状态不一致。我们只需要保证他们最后的状态一致就可以解决了。
9.3.1 两种模式(AP、CP)
-
AP模式:各个子事务分别执行与提交,可以有失败有成功的。然后我们只需要统计状态采取补救措施恢复数据就可以了,实现最终一致。
比如,有A,B两个子事务,A:扣减库存,B:删除金额。 他们两个各自执行完后,如果A成功了,直接提交,B没成功,回滚, 我们通过他们的状态得需要采取补救措施,只需将A扣减的库存加回来即可。 -
CP模式:各个子事务执行后互相等待,等都执行完了,统计状态,决定是同时提交还是同时回滚,达成强一致。但是在等待的过程中,处于弱可用状态。
比如,有A,B两个子事务,A:扣减库存,B:删除金额。 他们两个各自执行完后,如果A成功了, 等待B执行完成,B执行完了,失败了,则他们一起回滚。
9.3.2 TC(事务协调者)
从上面的描述来看,我们会发现不管哪种模式,要解决分布式事务,各个子系统之间必须要能感知到彼此的事务状态,这样才能保证状态一致,因此需要一个事务协调者来协调(TC)每一个事务的参与者(子系统事务)。
其中,我们把子系统事务子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
也就是说,实现分布式事务的两个模式 AP、CP
- AP(最终一致思想):各分支事务分别执行并提交,如果有失败(不一致)的情况,再想办法恢复数据
- CP(强一致思想):各分支事务执行完后不提交,等待彼此的执行结果,然后一起提交或者一起回滚
9.4 分布式解决方案——Seata
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
9.4.1 Seata的架构
Seata事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
整体的架构如图:

关于这个架构,我个人的想法。TM是管理板块,用来决定,哪些是本次业务中需要用到的事务(分支)。
就像订单服务,我们需要1.添加新订单,2.扣除库存,3.扣除金额。而TM就是管理这三个事务,让他们开始执行。
而RM则是每个事务的监督者,他会想TC也就是事务协调者报告,自己事务的执行状态,然后TC再通过RM报告过来的状态,
从而告诉TM是提交事务还是回滚事务。从而达到一致性。
9.4.2 分布式事务的四种解决方案
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
10 Redis
10.1 Redis有5种基本数据类型
-
String(字符串):普通的key、value可以归为此类
-
List(链表):可以用作消息队列,利用List的push操作将任务放在List中,元素可以重复
-
Set(集合):交集可以看共同好友
-
Zset(有序集合):用在排行榜,取TopN操作
-
Hash(哈希):存储对象,比如用户(姓名、性别、爱好)、文章(标题、作者、内容)
10.2 持久化相关
10.2.1 缓存穿透、缓存击穿、缓存雪崩
总结:
- 缓存穿透:缓存中没有,数据库中也没有
- 缓存击穿:缓存中没有,数据库中有,一般是某个key过期导致
- 缓存雪崩:大量key过期

10.2.2 RDB和AOF有什么区别

10.2.3 AOF有哪几种日志策略
- AOF_FSYNC_ALWAYS:每执行一次命令就保存一下数据,性能低
- AOF_FSYNC_NO:不主动将AOF文件的内存从缓存保存到磁盘中,交给操作系统去决定何时保存到磁盘(比如Redis被关闭,AOF功能被关闭的时候)
- AOF_FSYNC_EVERYSE:每秒保存一次
AOF重写是什么
读取Redis数据库中的数据,而不是读取现有的AOF文件。
比如旧AOF文件记录了6条rpush命令,那么新AOF文件会从数据库中读取键现在的值,
然后用一条命令去记录键值对,这样6条命令变成了一条。
重新完后新的AOF文件会替换旧AOF文件。但是新AOF文件不会包含浪费空间的冗余命令,
所以体积较小,这就解决了AOF文件体积膨胀的问题
10.2.4 持久化期间,新的写操作怎么办
Redis专门设置了一个AOF重写缓冲区,Redis执行完写命令后,这个写命令会保存到AOF重写缓冲区。当子进程完成AOF重写工作后,父进程会把AOF重写缓冲区中的所有内容写入到新生成的AOF文件中
10.3 主从复制相关
10.3.1 主从复制的作用
-
读写分离:master写,slave读,提高服务器的读写负载能力
-
负载均衡:基于主从复制、读写分离,由slave分担master负载,通过多个从节点分担数据读取负载,提高服务器的并发量和吞吐量
-
故障恢复:mater出现问题时,由slave提供服务,实现快速的故障恢复
-
高可用基石:基于主从复制,构建哨兵与集群,实现Redis的高可用方案
10.3.2 Redis主从复制原理
-
slave成功连接到maser后,会向master发送SYNC命令。
-
mater收到SYNC命令后,执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
-
mater会将BGSAVE命令生成的RDB文件发送给slave,slave接收并载入RDB文件,将自己的数据库状态更新至master执行BGSAVE命令时的数据库状态。
-
mater将记录在缓冲区的所有写命令发送给从slave,slave执行这些写命令,将自己的数据库状态更新至master数据库当前所处的状态。
全量复制:slave在接受到master的RDB文件后,将其存盘并载入到内存中
增量复制:master将写命令发送给从slave执行,避免每次都是全量复制。比如同步完成后,master删除了key,slave没删除,这时主从不一致,master就需要将写操作发送给salve并执行。这就是命令传播
10.3.3 部分重同步是怎么实现的
部分重同步由以下三个部分构成:
-
主服务器的复制偏移量和从服务器的复制偏移量
-
主服务器的复制积压缓冲区
-
服务器的运行ID
复制偏移量:主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。如果主从服务器处于一致时,那两者的偏移量总是相同的。
复制积压缓冲区:假如从服务器断线了,等连上master后,如何复制呢?是应该执行完整重同步还是部分重同步呢?这就需要复制积压缓冲区。复制积压缓冲区是由主服务器维护的一个固定长度的先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区。
10.4 哨兵有什么作用
-
集群监控:负责监控master和slave服务是否正常工作
-
消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
-
故障转移:如果master挂掉了,会选一个slave来当master
-
配置中心:如果故障转移发生了,会通知client客户端新的master地址
10.5 Redis有几种部署模式,集群和哨兵模式有什么区别
Redis有4种模式:单机、主从模式、哨兵模式、集群模式
10.5.1 单机模式
单机模式就是部署一台Redis机器
优点:部署简单、高性能,单机不需要同步数据
缺点:可靠性无法保证,有宕机风险
10.5.2 主从复制模式
主从复制是指将一台Redis服务器的数据,复制到其它Redis服务器,一个主节点可以挂多个从节点,如何mater宕机,只能人工干预选择一个slave来当主节点
优点:slave帮master分担了读的压力
缺点:一旦master宕机,只能人工干预选一个slave当主节点,同时需要修改其它slave对应的master地址,命令其它slave去复制新的master,全程需要人工干预
主从复制,是可以部署多台主节点的,各个主节点相互独立,互不影响,至于我想把数据存在哪台主节点上,靠自己选择。
10.5.3 哨兵模式
哨兵监控整个Redis集群,可以很好实现故障转移。一个master宕机了,哨兵会选举出一个slave来当master。
哨兵模式下也是允许有多台主节点的,各个主节点也是相互独立,互不影响,至于我想把数据存在哪台主机上,靠自己选择
优点:哨兵模式是基于主从模式的,可以实现自动切换。
缺点:部署更复杂了
10.5.4 集群模式
主从和哨兵都没有解决一个问题:单个节点的存储能力和访问能力是有限的。集群模式把数据进行分片存储。集群的键空间被分割成16384个slots(即hash卡槽),通过hash的方式将数据分到不同的分片上。某个主节点宕机,那这个主节点下的从节点会通过选举产生一个主节点,替换原来的故障节点
集群模式下有多个主节点,各个主节点之后有联系,因为数据是分片存储的,并不是我想存哪台节点上就可以存哪台节点上,是集群自动计算key是属于哪个卡槽,存放在哪台节点上的
所以集群模式和哨兵模式最大的区别就是分片
10.6 Redis事务

11 Redis分布式锁
11.1分布式锁特性:
-
「互斥性」: 任意时刻,只有一个客户端能持有锁。
-
「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
-
「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
-
「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
-
「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
11.2 Redis分布式锁方案
一:SETNX + EXPIRE
即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。
SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,
如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。
缺点: 但是这个方案中,setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」。
二:SETNX + value值是(系统时间+过期时间)
为了解决方案一,「发生异常锁得不到释放的场景」,把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。
优点:
巧妙移除expire单独设置过期时间的操作,把「过期时间放到setnx的value值」里面来。解决了方案一发生异常,锁得不到释放的问题。
缺点:
1、过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),
必须要求分布式环境下,每个客户端的时间必须同步。
2、如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),
最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖
3、该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
三:使用Lua脚本(包含SETNX + EXPIRE两条指令)
四:SET的扩展命令(SET EX PX NX)
我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!
SET key value[EX seconds][PX milliseconds][NX|XX]
NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,
而其他客户端请求只能等其释放锁,才能获取。
EX seconds :设定key的过期时间,时间单位是秒。
PX milliseconds: 设定key的过期时间,单位为毫秒
XX: 仅当key存在时设置值
缺点:
-
问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
-
问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢
五:SET EX PX NX + 校验唯一随机值,再删除
既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,不就OK了嘛。
缺点:
「判断是不是当前线程加的锁」 和 「释放锁」 不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
六:Redisson框架
给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。 开源框架Redisson解决了这个问题。
只要线程一加锁成功,就会启动一个watch dog看门狗,
它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,
那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了
「锁过期释放,业务没执行完」问题。
七:多机实现的分布式锁Redlock+Redisson
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
搞多个Redis master部署,以保证它们不会同时宕掉。
并且这些master节点是完全相互独立的,相互之间不存在数据同步。
同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。
RedLock的实现步骤:如下
1.获取当前时间,以毫秒为单位。
2.按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,
并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,
则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,
跳过该master节点,尽快去尝试下一个master节点。
3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),
得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)
的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),
客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,
也需要解锁,以防止有些漏网之鱼)。
简化下步骤就是:
- 按顺序向5个master节点请求加锁
- 根据设置的超时时间来判断,是不是要跳过该master节点。
- 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
- 如果获取锁失败,解锁!
12 Elasticsearch
Elasticsearch 是基于 Lucene 的 Restful 的分布式实时全文搜索引擎,每个字段都被索引并可被搜索,可以快速存储、搜索、分析海量的数据。
全文检索是指对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当查询时,根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
12.1 相关名词解释
-
映射(Mapping):类比于MySQL中schema和数据库的设计(比如字段类型,长度…),ES中通过mapping定义哪些字段是否可以分词操作,哪些字段是否可以被查询等。
-
分片(Shards):类比于MySQL的水平分表,作用是容量扩容,提高吞吐量。
-
副本(Replicas):分片数据的副本,保障数据安全。
-
分配(allocation):将分片分给某个节点的过程(包括主分片和副本),有master节点完成。
12.2 Elasticsearch 中的集群、节点、索引、文档、类型是什么?
-
集群是一个或多个节点(服务器)的集合,它们共同保存您的整个数据,并提供跨所有节点的联合索引和搜索功能。群集由唯一名称标识,默认情况下为"elasticsearch"。此名称很重要,因为如果节点设置为按名称加入群集,则该节点只能是群集的一部分。
-
节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
-
索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一个或多个主分片,并且可以有零个或多个副本分片。MySQL =>数据库,Elasticsearch=>索引。
-
文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但是对于通用字段应该具有相同的数据类型。MySQL => Databases => Tables => Columns / Rows,Elasticsearch=> Indices => Types =>具有属性的文档Doc。
-
类型是索引的逻辑类别/分区,其语义完全取决于用户。
12.3 倒排索引是什么?
倒排索引是搜索引擎的核心。 搜索引擎的主要目标是在查找发生搜索条件的文档时提供快速搜索。ES中的倒排索引其实就是 lucene 的倒排索引,区别于传统的正向索引, 倒排索引会再存储数据时将关键词和数据进行关联,保存到倒排表中,然后查询时,将查询内容进行分词后在倒排表中进行查询,最后匹配数据即可。
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。
那么,倒排索引就是关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。
倒排索引的两个重要细节:
- 倒排索引中的所有词项对应一个或多个文档
- 倒排索引中的词项根据字典顺序升序排列
12.4 底层的 lucene 是什么?
lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。
12.5 为什么要使用 Elasticsearch?
系统中的数据, 随着业务的发展,时间的推移, 将会非常多, 而业务中往往采用模糊查询进行数据的搜索, 而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ES 索引库里,可以提高查询速度。
12.6 Elasticsearch工作原理
Elasticsearch 写入数据的工作流程是什么?
- 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)。
- coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)。[路由的算法是哈希然后取模]
- 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node。
- coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。
12.7 Elasticsearch 主分片写数据的详细流程?
-
主分片先将数据写入ES的 memory buffer,然后定时(默认1s)将 memory buffer 中的数据写入一个新的 segment 文件中,并进入操作系统缓存 Filesystem cache(同时清空 memory buffer),这个过程就叫做 refresh;每个 segment 文件实际上是一些倒排索引的集合, 只有经历了 refresh 操作之后,这些数据才能变成可检索的。
ES 的近实时性:数据存在 memory buffer 时是搜索不到的, 只有数据被 refresh 到 Filesystem cache 之后才能被搜索到, 而 refresh 是每秒一次, 所以称 es 是近实时的; 可以手动调用 es 的 api 触发一次 refresh 操作, 让数据马上可以被搜索到; -
由于 memory Buffer 和 Filesystem Cache 都是基于内存,假设服务器宕机,那么数据就会丢失,所以 ES 通过 translog 日志文件来保证数据的可靠性,在数据写入 memory buffer 的同时,将数据也写入 translog 日志文件中,当机器宕机重启时,es 会自动读取 translog 日志文件中的数据,恢复到 memory buffer 和 Filesystem cache 中去。
ES 数据丢失的问题:translog 也是先写入 Filesystem cache, 然后默认每隔 5 秒刷一次到磁盘中,所以默认情况下, 可能有 5 秒的数据会仅仅停留在 memory buffer 或者 translog 文件的 Filesystem cache中, 而不在磁盘上,如果此时机器宕机,会丢失 5 秒钟的数据。 也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。 -
flush 操作:不断重复上面的步骤,translog 会变得越来越大,不过 translog 文件默认每30分钟或者 阈值超过 512M 时,就会触发 commit 操作,即 flush操作,将 memory buffer 中所有的数据写入新的 segment 文件中, 并将内存中所有的 segment 文件全部落盘,最后清空 translog 事务日志。
1、 将 memory buffer 中的数据 refresh 到 Filesystem Cache 中去,清空 buffer; 2、 创建一个新的 commit point(提交点),同时强行将 Filesystem Cache 中目前所有的数据都 fsync 到磁盘文件中; 3、 删除旧的 translog 日志文件并创建一个新的 translog 日志文件,此时 commit 操作完成
12.8 Elasticsearch 更新和删除文档的流程?
删除和更新都是写操作,但是由于 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;所以 ES 利用 .del 文件 标记文档是否被删除,磁盘上的每个段都有一个相应的.del 文件
-
如果是删除操作,文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉。
-
如果是更新操作,就是将旧的 doc 标识为 deleted 状态,然后创建一个新的 doc。
memory buffer 每 refresh 一次,就会产生一个 segment 文件 ,所以默认情况下是 1s 生成一个 segment 文件,这样下来 segment 文件会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment 文件合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,不写入到新的 segment 中,然后将新的 segment 文件写入磁盘,这里会写一个 commit point ,标识所有新的 segment 文件,然后打开 segment 文件供搜索使用,同时删除旧的 segment 文件。
12.9 query 和 filter 的区别?
-
query:查询操作不仅仅会进行查询,还会计算分值,用于确定相关度;
-
filter:查询操作仅判断是否满足查询条件,不会计算任何分值,也不会关心返回的排序问题,同时,filter 查询的结果可以被缓存,提高性能。
12.10 Elasticsearch 查询数据的流程?
搜索被执行成一个两阶段过程,即 Query Then Fetch:
-
Query阶段:
客户端发送请求到 coordinate node,协调节点将搜索请求广播到所有的 primary shard 或 replica,每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。接着每个分片返回各自优先队列中 所有 docId 和 打分值 给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 -
Fetch阶段:
协调节点根据 Query阶段产生的结果,去各个节点上查询 docId 实际的 document 内容,最后由协调节点返回结果给客户端。
coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,
此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,
让读请求负载均衡。
接收请求的 node 返回 document 给 coordinate node 。
coordinate node 返回 document 给客户端。
12.11 Elasticsearch 索引文档的流程?
-
协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片:shard = hash(document_id) % (num_of_primary_shards)
-
当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 Memory Buffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 Memory Buffer 到 Filesystem Cache 的过程就叫做 refresh;
-
当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失, ES 是通过 translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中,当 Filesystemcache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush;
-
在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync 将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。
-
flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时;
12.12 Elasticsearch 的分布式原理?
Elasticsearch 会对存储的数据进行切分,划分到不同的分片上,同时每一个分片会生成多个副本,
从而保证分布式环境的高可用。ES集群中的节点是对等的,节点间会选出集群的 Master,
由 Master 会负责维护集群状态信息,并同步给其他节点。
Elasticsearch 的性能会不会很低:不会,ES只有建立 index 和 type 时需要经过 Master,而数据的写入有一个简单的 Routing 规则,可以路由到集群中的任意节点,所以数据写入压力是分散在整个集群的。
12.13 Elasticsearch 的 master 选举流程?
Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)
和Unicast(单播模块包含-一个主机列表以控制哪些节点需要ping通)这两部分。
第一步:对所有可以成为master的节点(node master: true)根据nodeId字典排序,每次选举每个节点都把自
己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。
第二步:如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,
那这个节点就是master。否则重新选举一直到满足上述条件。
master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。
12.14 Elasticsearch 集群脑裂问题?
脑裂问题可能的成因:
-
网络问题:集群间的网络延迟导致一些节点访问不到master, 认为master 挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片。
-
节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
-
内存回收:data 节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。
脑裂问题解决方案:
-
减少误判:discovery.zen ping_ timeout 节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。
-
选举触发:discovery.zen.minimum. master nodes:1,该参數是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个數大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(n / 2) +1, n为主节点个数(即有资格成为主节点的节点个数)。
-
角色分离:即master节点与data节点分离,限制角色
主节点配置为:node master: true,node data: false
从节点配置为:node master: false,node data: true
12.15 Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?
Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct或者 unique 值的数目。它是基于 HLL 算法的。 HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。
12.16 Elasticsearch 如果保证读写一致?
- 对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖
每个文档都有一个_version 版本号,这个版本号在文档被改变时加一。
Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,
它会被简单的忽略。利用_version的这一优点确保数据不会因为修改冲突而丢失,
比如指定文档的version来做更改,如果那个版本号不是现在的,我们的请求就失败了。
- 对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。
one:写操作只要有一个primary shard是active活跃可用的,就可以执行
all:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
quorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
- 对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。
12.17 Elasticsearch性能优化
如何监控 Elasticsearch 集群状态?
elasticsearch-head 插件。
通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、索引和节点指标
12.18 Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法?
-
64 GB 内存的机器是非常理想的, 但是 32 GB 和 16 GB 机器也是很常见的。少于 8 GB 会适得其反。
-
如果你要在更快的 CPUs 和更多的核心之间选择,选择更多的核心更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
-
如果你负担得起 SSD,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。如果你负担得起, SSD 是一个好的选择。
-
即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。
-
请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 在 Elasticsearch 的几个地方,使用 Java 的本地序列化。
-
通过设置 gateway.recover_after_nodes、 gateway.expected_nodes、 gateway.recover_after_time 可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。
-
Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。
-
不要随意修改垃圾回收器(CMS)和各个线程池的大小。
-
把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过 ES_HEAP_SIZE 环境变量设置。
-
内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可怕。
-
Lucene 使用了大量的文件。同时, Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,如 64,000。
12.19 GC 方面,在使用 Elasticsearch 时要注意什么?
-
倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segment memory 增长趋势。
-
各类缓存, field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache 等“自欺欺人”的方式来释放内存。
-
避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用 scan & scroll api 来实现。
-
cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。
-
想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。
12.20 建立索引阶段性能提升方法?
-
如果是大批量导入,可以设置 index.number_of_replicas: 0 关闭副本,等数据导入完成之后再开启副本
-
使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
-
如果搜索结果不需要近实时性,可以把每个索引的 index.refresh_interval 改到30s
-
增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB
-
使用 SSD 存储介质
-
段和合并:Elasticsearch 默认值是 20 MB/s。但如果用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。
12.22 ES的深度分页与滚动搜索scroll
- 深度分页:
深度分页其实就是搜索的深浅度,比如第1页,第2页,第10页,第20页,是比较浅的;第10000页,第20000页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用cpu。而且es为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供100页的展示,从第101页开始就没了,毕竟用户也不会搜的那么深。
- 滚动搜索
一次性查询1万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll。 滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。
13 jvm 排查内存工具流程
13.1 监控与日志分析
-
监控内存使用趋势
利用 Prometheus、Grafana、JConsole、VisualVM 等工具,监控堆内存、非堆内存、GC 次数和停顿时间。
如果发现堆内存使用率持续上升,或者 GC 次数频繁增加(尤其是 Full GC 频率增加),可能存在内存泄漏。
-
开启 GC 日志
配置 JVM 参数(例如:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log)记录垃圾回收日志。
分析 GC 日志,观察垃圾回收前后堆内存的变化,是否存在“GC overhead limit exceeded”异常等
13.2 Heap Dump 分析
总结:
排查 JVM 内存泄漏问题的基本步骤是:
1、监控与日志分析:观察内存使用趋势和 GC 日志,确认异常现象。
2、Heap Dump 分析:采集堆转储,利用 MAT 或其他工具查找内存占用最多的对象和引用链。
3、代码审查与调试:检查资源释放、缓存管理和事件监听等代码,结合专业工具进行实时监控。
4、调优与验证:调整 JVM 参数,优化代码后通过测试验证内存泄漏是否得到解决。
-
采集 Heap Dump
当发现内存使用异常时,可以使用 jmap -dump 或者通过监控工具(如 JVisualVM、Java Mission Control)生成堆转储文件(.hprof 文件)。
可以在 OOM(OutOfMemoryError)发生时自动生成 Heap Dump(通过 -XX:+HeapDumpOnOutOfMemoryError)。
-
使用内存分析工具
利用 Eclipse Memory Analyzer Tool(MAT)、VisualVM 的插件或 JProfiler 分析 Heap Dump。
- 主要分析点:
找出占用内存最多的对象及其数量(疑似泄漏点)。
查看对象的引用链(Reference Chain),定位哪些引用没有及时释放,比如长生命周期的静态变量、集合(List、Map)等未清理的容器。
分析类加载器问题,排查是否存在因为类未卸载导致的内存泄漏(尤其在动态部署的场景)。
13.3 代码审查与排查工具
-
代码审查
-
检查是否存在未关闭的资源(如数据库连接、IO 流)、缓存未清理、静态集合持续累积数据等常见内存泄漏原因。
-
注意循环引用、事件监听器未注销、定时任务中使用匿名内部类等情况。
-
-
使用专业工具
- 使用 JProfiler、YourKit 等商业分析工具,进行实时内存监控和泄漏检测,观察对象分配和垃圾回收情况。
-
断点调试与日志记录
- 对可疑模块加入内存监控日志、统计特定对象的创建与释放,辅助排查内存泄漏的细节。
13.4 调优与验证
-
调整 GC 参数
- 根据监控结果和 Heap Dump 分析结果,调优垃圾回收参数(如堆大小、年轻代比例、GC 算法),改善内存回收效果,并观察是否能缓解内存增长问题。
-
持续集成和测试
-
编写压力测试和长时间运行测试,验证问题修复情况。
-
采用内存泄漏检测工具(如 Java 的 LeakCanary 或 Apache JMeter 配合内存监控)持续监控内存使用。
-
JAVA面试题知识:JVM、多线程等全面总结

230

被折叠的 条评论
为什么被折叠?



