Java
同程旅⾏ Java ⾯试
如果同学们觉得⼤⼚有难度,可以考虑试⼀下中 ⼚,⼤⼚薪资普遍是 22k+ ,中⼚普遍是 15k+ ,
也有能到 20k 的,有了中 ⼚⼯作经历,社招跳⼤⼚还是有很⼤机会的。
这次来分享互 联⽹中⼚的⾯经,⾯试难度也是刚好介于 ⼤⼚和⼩⼚之间。
今天让我们来看看 「同程旅⾏」Java 后端开发的⾯经,问题相⽐⼤⼚是少了⼀些,总共 20 多个问
题,其中有 10 多个是⼋股,剩下有些是项⽬问题,这次我们重点看看 ⼋股的问题

考察的知识点,我给⼤家罗列了⼀下:
Java :反射、stream 、线程创建与同步、线程池、JWT
RocketMQ :使⽤场景
Redis :缓存雪崩、缓存穿透
Java
介绍⼀下反射的特性
Java 反射机制是在运⾏状态中,对于任意⼀个类,都能够知道这个类中的所有属性和⽅法,对于
任意⼀个对象,都能够调⽤它的任意⼀个⽅法和属性;这种动态获取的信息以及动态调⽤对象的
⽅法的功能称为 Java 语⾔的反射机制。反射具有以下特性:
- 运⾏时类信息访问:反射机制允许程序在运⾏时获取类的完整结构信息,包括类名、包名、⽗
类、实现的接⼝、构造函数、⽅法和字段等。
- 动态对象创建:可以使 ⽤反射API 动态地创建对象实例,即使在编译时不知道具体的类名。这是
通过Class 类的newInstance() ⽅法或Constructor 对象的newInstance() ⽅法实现的。- 动态⽅法调⽤:可以在运⾏时动态地调⽤对象的⽅法,包括私有⽅法。这通过Method 类的
invoke() ⽅法实现,允许你传 ⼊对象实例和参数值来执⾏⽅法。- 访问和修改字段值:反射还允许程序在运⾏时访问和修改对象的字段值,即使是私有的。这是

通过Field 类的get() 和set() ⽅法完成的。应⽤场景:
Java 反射机制在现代软件开发中,尤其是在企业级应⽤和框架设计 中扮演着重要⻆⾊,尤其是
在我们平时⽤的spring 框架中,很多地⽅都⽤到了反射,让我们来看看 Spring 的IoC 和AOP 是如
何使 ⽤反射技术的:
- Spring 框架的依赖注⼊(DI )和控制反转(IoC )
Spring 框架是Java ⽣态系统中最流⾏的框架之⼀,它⼤量使⽤反射来实现其核⼼特性—— 依赖注
⼊。在Spring 中,开发者可以通过XML 配置⽂件或者基于注解的⽅式声明组件之间的依赖关系。当
应⽤程序启动时,Spring 容器会扫描这些配置或注解,然后利⽤反射来实例化Bean (即Java 对
象),并根据配置⾃动装配它们的依赖。
例如,当⼀个Service 类需要依赖另⼀个DAO 类时,开发者可以在Service 类中使⽤@Autowired 注
解,⽽⽆需⾃⼰编写创建DAO 实例的代码。Spring 容器会在运⾏时解析这个注解,通过反射找到对
应的DAO 类,实例化它,并将其注⼊到Service 类中。这样不仅 降低了组件之间的耦合度,也极⼤
地增强了代码的可维护性和可 测试性。
- 动态代理的实现
在需要对现有类的⽅法调⽤进⾏拦截 、记录⽇志、权限控制或是事务管理等场景中,反射结合动
态代理技术被⼴泛应⽤。⼀个典型的例⼦是Spring AOP (⾯向切⾯编程)的实现。Spring AOP 允
许开发者定义切⾯(Aspect ),这些切⾯可以横切关注点(如⽇志记录、事务管理),并将其插⼊
到业务逻辑中,⽽不需要修改业务逻辑代码。
例如,为了 给所有的服务层⽅法添加⽇志记录功能,可以定义⼀个切⾯,在这个切⾯中,Spring 会使⽤JDK 动态代理或CGLIB (如果⽬标类没有实现接⼝)来创建⽬标类的代理对象。这个代理对象
在调⽤任何 ⽅法前或后,都会执⾏切⾯中定义的代码逻辑(如记录⽇志),⽽这⼀切都是在运⾏时
通过反射来动态构建和执⾏的,⽆需硬编码到每个⽅法调⽤中。
这两个 例⼦展⽰了反射机制如何在实际⼯程中促进松耦合、⾼内聚的设计 ,以及如何提供动态、
灵活的编程能⼒,特别是在框架层⾯和解决跨切⾯问题时。
Java 中stream 的API 介绍⼀下
Java 8 引⼊了Stream API ,它提供了⼀种⾼效且易于使⽤的数据处理⽅式,特别适合集合对象的操
作,如过滤、映射、排序等。Stream API 不仅 可以提⾼代码的可读性和简洁性,还能利⽤多核处理
器的优势进⾏并⾏处理。让我们通过两个 具体的例⼦来感受下Java Stream API 带来的便利,对⽐
在Stream API 引⼊之前的传统做法。
案例1:过滤并收集满⾜条件的元素
问题场景:从⼀个列表中筛选出所有⻓度⼤于3的字符串,并收集到⼀个新的列表中。
没有Stream API 的做法:
List <String > originalList = Arrays .asList ("apple" , "fig" , "banana" , "kiwi" );
List <String > filteredList = new ArrayList <>();
if (item.length() > 3) {
filteredList.add(item);
}
}这段代码需要显式地创建⼀个新的ArrayList ,并通过循环遍历原 列表,⼿动检查每个元素是否满⾜
条件,然后添加到 新列表中。
使⽤Stream API 的做法:
List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi");
List<String> filteredList = originalList.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
这⾥,我们直接在原始列表上调⽤ .stream() ⽅法创建了⼀个流,使⽤ .filter() 中间操作筛选
出⻓度⼤于3的字符串,最后使⽤ .collect(Collectors.toList()) 终端操作将结果收集到⼀个新的列表中。代码更加简洁明了,逻辑⼀⽬了然。
案例2:计算列表中所有数字的总和
问题场景:计算⼀个数字列表中所有元素的总和。
没有Stream API 的做法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer number : numbers) {
sum += number;
}这个传统的for-each 循环遍历列表中的每⼀个元素,累加它们的值来计算总和。
使⽤Stream API 的做法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
通过Stream API ,我们可以先使⽤ .mapToInt() 将Integer 流转换为IntStream (这是为了 ⾼效处理
基本类型),然后直接调⽤ .sum() ⽅法来计算总和,极⼤地简化了代码。线程的创建⽅式有哪些
- 继承Thread 类
这是最直接的⼀种⽅式,⽤⼾⾃定义类继承java.lang.Thread 类,重写其 run() ⽅法,run() ⽅法中定
义了 线程执⾏的具体任 务。创建该类的实例后,通过调⽤start() ⽅法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}采⽤继承Thread 类⽅式
优点: 编写简单,如果需要访问当前线程,⽆需使⽤Thread.currentThread () ⽅法,直接使⽤this ,即可获得当 前线程
缺点:因为线程类已经继 承了Thread 类,所以不能再继承其他的⽗类
- 实现Runnable 接⼝
如果⼀个类已经继 承了其他类,就不能再继承Thread 类,此时可以实现java.lang.Runnable 接⼝。
实现Runnable 接⼝需要重写run() ⽅法,然后将此Runnable 对象作为参数传递给Thread 类的构造
器, 创建Thread 对象后调⽤其start() ⽅法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}采⽤实现Runnable 接⼝⽅式:
优点:线程类只是实现了Runable 接⼝,还可以继承其他的类。在这种⽅式下,可以多个线程共
享同⼀个⽬标对象,所以⾮常适合多个相同线程来处理同⼀份资源的情况,从⽽可以将CPU 代
码和数据分开,形成清晰的模型,较好地体现了⾯向对象的思想。
缺点:编程稍 微复杂,如果需要访问当前线程,必须使⽤Thread.currentThread() ⽅法。- 实现Callable 接⼝与FutureTask
java.util.concurrent.Callable 接⼝类似于Runnable ,但Callable 的call() ⽅法可以有返回值并且可以抛出异常。要执⾏Callable 任务,需将它包装进⼀个FutureTask ,因为Thread 类的构造器只接受Runnable 参数,⽽FutureTask 实现了Runnable 接⼝。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码,这里返回一个整型结果
return 1;
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
try {
Integer result = futureTask.get(); // 获取线程执行结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}采⽤实现Callable 接⼝⽅式:
缺点:编程稍 微复杂,如果需要访问当前线程,必须调⽤Thread.currentThread() ⽅法。优点:线程只是实现Runnable 或实现Callable 接⼝,还可以继承其他类。这种⽅式下,多个线程
可以共享⼀个target 对象,⾮常适合多线程处理同⼀份资源的情形。
- 使⽤线程池(Executor 框架)
从Java 5 开始引⼊的java.util.concurrent.ExecutorService 和相关类提供了线程池的⽀持,这是⼀种
更⾼效的线程管理⽅式,避免了频繁创建和销毁线程的开销。可以通过Executors 类的静态⽅法创
建不同类型的线程池。
class Task implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
for (int i = 0; i < 100; i++) {
executor.submit(new Task()); // 提交任务到线程池执行
}
executor.shutdown(); // 关闭线程池
}采⽤线程池⽅式:
缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可
能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂。
优点:线程池可以重⽤预先创建的线程,避免了线程创建和销毁的开销,显著提⾼了程序的性
能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并
且,线程池能够有效控制运⾏的线程数量,防⽌因创建过多线程导致的系统资源耗尽(如内存
溢出)。通过合理配置线程池⼤⼩,可以最⼤化CPU 利⽤率和系统吞吐 量。
线程同步介绍⼀下
线程同步是多线程编程中的⼀个重要概念,⽤于控制多个线程对共享资源的访问,确保在任⼀时
刻只有⼀个线程能够访问共享资源,以防⽌数据不⼀致、脏读等问题。Java 提供了多种线程同步机
制来保证线程安全,主要包括:
synchronized 关键字
⽅法同步:在⽅法声明上使⽤synchronized 关键字,这样⼀次只有⼀个线程可以访问该⽅法。
public synchronized void method() {
// 方法体
}代码块同步:可以在特定的代码块前加 上synchronized 关键字,指定⼀个锁对象,这样同步代
码块在 执⾏时会锁定该对象,其他任何 需要该锁的代码必须等待锁释放。
public void method() {
synchronized(this) { // 或者是特定的对象实例
// 需要同步的代码块
}
}Lock 接⼝及其实现
从Java 5 开始,提供了java.util.concurrent.locks.Lock 接⼝作为synchronized 的替代,它提供了⽐
synchronized 更灵活的锁定机制,如尝试获取锁、可定时获取锁以及公平锁等。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
// 增量操作
} finally {
lock.unlock(); // 一定要在finally块中释放锁
}
}
}volatile 关键字
虽然volatile 主要⽤于变量的可⻅性保证,⽽不是同步,但它可以⽤来确保多线程环境下的变量修
改对其他线程是⽴即可⻅的,适⽤于状态标记等简 单场景。
private volatile boolean flag = false;
public void setFlag(boolean newValue) {
flag = newValue;
}
public boolean getFlag() {
return flag;
}Atomic 类
java.util.concurrent.atomic 包提供了⼀系列原⼦操作类,如AtomicInteger 、AtomicBoolean 等,它们通过CAS (Compare and Swap )⽆锁算法实现线程安全的原⼦操作,适合于简单的数值操作场景。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}介绍⼀下线程池
线程池原理
线程池是为了 减少频繁的创建线程和销毁线程带来的性能损耗。
线程池分为核⼼线程池,线程池的最⼤容量,还有等待任务的队列,提交⼀个任务,如果核⼼线
程没有满,就创建⼀个线程,如果满了,就是会加⼊等待队列,如果等待队列满了,就会增加线
程,如果达到最⼤线程数量,如果都达到最⼤线程数量,就会按照⼀些丢 弃的策略进⾏处理。

线程池的参数有哪些?
线程池的构造函数有7个参数:

corePoolSize :线程池核⼼线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize ,那么即使这些线程处于空闲状态,那也不 会被销毁。maximumPoolSize :限制了线程池能创建的最⼤线程总数(包括核⼼线程和⾮核⼼线程),当
corePoolSize
已满 并且 尝试将新任务加 ⼊阻塞队列失败(即队列已满)并且 当前线程数 <maximumPoolSize
,就会创建新线程执⾏此任务,但是当 corePoolSize 满 并且 队列满 并且
线程数已达 maximumPoolSize 并且 ⼜有新任务提交时,就会触发拒绝策略。
keepAliveTime :当线程池中线程的数量⼤于corePoolSize ,并且某个线程的空闲时间超过了
keepAliveTime ,那么这个线程就会被销毁。
unit :就是keepAliveTime 时间的单位。
workQueue :⼯作队列。当没有空闲的线程执⾏新任务时,该任务就会被放⼊⼯作队列中,等
待执⾏。
threadFactory :线程⼯⼚。可以⽤来给线 程取名字等等
handler :拒绝策略。当⼀个新任务交给线 程池,如果此时线程池中有空闲的线程,就会直接执
⾏,如果没有空闲的线程,就会将该任务加 ⼊到阻塞队列中,如果阻塞队列满了,就会创建⼀
个新线程,从阻塞队列头部取出⼀个任务来执⾏,并将新任务加 ⼊到阻塞队列末尾。如果当前
线程池中线程的数量等于maximumPoolSize ,就不会创建新线程,就会去执⾏拒绝策略
线程池种类
FixedThreadPool :它的核⼼线程数和最⼤线程数是⼀样的,所以可以把它看作是固定线程数的
线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就
是固定的,就算任务数超过线程数,线程池也不 会再创建更多的线程来处理任务,⽽是会把超
出线程处理能⼒的任务放到任务队列中进⾏等待。⽽且就算任务队列满了,到了本该继续增加
线程数的时候,由于它的最⼤线程数和核⼼线程数是⼀样的,所以也⽆法再增加新的线程了。
ExecutorService executor = Executors.newFixedThreadPool(5);CachedThreadPool :可以称作可缓存线程池,它的特点在于线程数是⼏乎可以⽆限增加的(实
际最⼤可以达到 Integer.MAX_VALUE ,为 2^31-1 ,这个数⾮常⼤,所以基本不可能达到),⽽当线程闲置时还可以对线程进⾏回收。也就是说该线程池的线程数量不是固定不变的,当然它
也有⼀个⽤于存储提交任务的队列,但这个队列是 SynchronousQueue ,队列的容量为0,实际
不存储任何任 务,它只负责 对任务进⾏中转和传递,所以效率⽐较⾼。
ExecutorService executor = Executors.newCachedThreadPool();SingleThreadExecutor :它会使 ⽤唯⼀的线程去执⾏任务,原理和 FixedThreadPool 是⼀样的,
只不过这 ⾥线程只有⼀个,如果线程在执⾏任务的过程中发⽣异常,线程池也会重新创建⼀个
线程来执⾏后续的任务。这种线程池由于只有⼀个线程,所以⾮常适合⽤于所有任务都需要按
被提交的顺序依次执⾏的场景,⽽前⼏种线程池不⼀定能够保障任务的执⾏顺序等于被提交的
顺序,因为它们是多线程并⾏执⾏的。
ExecutorService executor = Executors.newSingleThreadExecutor();SingleThreadScheduledExecutor :它实 际和 ScheduledThreadPool 线程池⾮常相似,它只是
ScheduledThreadPool 的⼀个特例,内部只有⼀个线程。
ExecutorService executor = Executors.newScheduledThreadPool(5);前端是如何存储JWT 的?
JSON Web Token (缩写 JWT )是⽬前最流⾏的跨域认证 解决⽅案。互联⽹服务离不开⽤⼾认证 。
⼀般流程如下:
⽤⼾向服务器发送⽤⼾名和 密码。
服务器验证通过后,在当前对话(session )⾥⾯保存相关数据,⽐如⽤⼾⻆⾊、登录时间等等。
服务器向⽤⼾返回⼀个 session_id ,写⼊⽤⼾的 Cookie 。
⽤⼾随后的每⼀次请求,都会通过 Cookie ,将 session_id 传回服务器。
服务器收到 session_id ,找到前 期保存的数据,由此得知⽤⼾的⾝份。
这种模式的问题在于,扩展性(scaling )不好。单机当然没有问题,如果是服务器集群,或者是
跨域的服务导向架构 ,就要求 session 数据共享,每台服务器都能够读取 session 。
举例来说,A ⽹站和 B ⽹站是同⼀家公司的关联服务。现在要求,⽤⼾只要在其中⼀个⽹站登
录,再访问另⼀个⽹站就会⾃动登录,请问怎么实现?
⼀种解决⽅案是 session 数据持 久化,写⼊数据库或别的持久层。各种服务收到请求后,都向持久
层请求数据。这种⽅案的优点是架构 清晰,缺点是⼯程量⽐较⼤。另外,持久层万⼀挂了,就会
单点失败。
另⼀种⽅案是服务器索性不保存 session 数据了,所有数据都保存在客⼾端,每次 请求都发回服务
器。JWT 就是这种⽅案的⼀个代表。
客⼾端收到服务器返回的 JWT ,可以储存在 Local Storage ⾥⾯,也可以储存在Cookie ⾥⾯,还可
以存储在Session Storage ⾥⾯。下⾯将说明存在上述各个地⽅的优劣势 :
- Local Storage (本地存储)
优点:Local Storage 提供了较⼤的存储空间(⼀般为5MB ),且不 会随着HTTP 请求⼀起发送到
服务器, 因此不会出现在HTTP 缓存或⽇志中。
缺点:存在XSS (跨站脚本攻击)的⻛险,恶意脚本可以通过JavaScript 访问到存储在Local Storage 中的JWT ,从⽽盗取⽤⼾凭证。
- Session Storage (会话存储)
优点:与Local Storage 类似,但仅限于当前浏览器窗⼝或标签⻚,当窗⼝关闭后数据会被清
除,这在⼀定程度上减少了数据泄露的⻛险。
缺点:⽤⼾体验可能受影响,因为刷新⻚⾯或在新标签⻚打开相同应⽤时需要重新认证 。
- Cookie
优点:可以设置HttpOnly 标志来防⽌通过JavaScript 访问,减少XSS 攻击的⻛险;可以利⽤
Secure 标志确保仅通过HTTPS 发送,增加安全性。
缺点:⼤⼩限制较⼩(通常4KB ),并且每次 HTTP 请求都会携带Cookie ,可能影响性能;设置不
当可能会受到CSRF (跨站请求伪造)攻击。
cookie 和session 之间区别,介绍⼀下
Cookie 和Session 都是Web 开发中⽤于跟踪⽤⼾状态的技术,但它们在存储位置、数据容量、安全
性以及⽣命周 期等⽅⾯存在显著差异:
** 存储位置:**Cookie 的数据存储在客⼾端(通常是浏览器) 。当浏览器向服务器发送请求时,
会⾃动附带Cookie 中的数据。Session 的数据存储在服务器端。服务器为每个⽤⼾分配⼀个唯⼀
的Session ID ,这个ID 通常通过Cookie 或URL 重写的⽅式发送给客⼾端,客⼾端后续的请求会带
上这个Session ID ,服务器根据ID 查找对应的Session 数据。
** 数据容量:** 单个Cookie 的⼤⼩限制通常在4KB 左右,⽽且⼤多数浏览器对每个域名的总
Cookie 数量也有限制。由于Session 存储在服务器上,理论上不 受数据⼤⼩的限制,主要受限于
服务器的内存⼤⼩。
** 安全性:**Cookie 相对不安全,因为数据存储在客⼾端,容易受到XSS (跨站脚本攻击)的威
胁。不过,可以通过设置HttpOnly 属性来防⽌JavaScript 访问,减少XSS 攻击的⻛险,但仍然可
能受到CSRF (跨站请求伪造)的攻击。Session 通常认为⽐Cookie 更安全,因为敏感数据存储在
服务器端。但仍然需要防范Session 劫持(通过获取他⼈的Session ID )和会话固定攻击。
** ⽣命周 期:**Cookie 可以设置过期时间,过期后⾃动删 除。也可以设置为会话Cookie ,即浏
览器关闭时⾃动删 除。Session 在默认情况下,当⽤⼾关闭浏览器时,Session 结束。但服务器也
可以设置Session 的超时时 间,超过这 个时间未活动,Session 也会失效。
** 性能:** 使⽤Cookie 时,因为数据随每个请求发送到服务器, 可能会影响⽹络传输效率,尤其
是在Cookie 数据较⼤时。使⽤Session 时,因为数据存储在服务器端,每次 请求都需要查询服务
器上的Session 数据,这可能会增加服务器的负载,特别是在⾼并发场景下。
如果客⼾端禁⽤了cookie ,session 还能⽤吗?
默认情况下禁⽤ Cookie 后,Session 是⽆法正常使⽤的,因为⼤多数 Web 服务器都是依赖于
Cookie 来传递 Session 的会话 ID 的。
客⼾端浏览器禁⽤ Cookie 时,服务器将⽆法把会话 ID 发送给客⼾端,客⼾端也⽆法在后续请求
中携带会话 ID 返回给服务器, 从⽽导致服务器⽆法识别⽤⼾会话。
但是,有⼏种⽅法可以绕过这 个问题,尽管它们可能会引⼊额外的复杂性和/或降低⽤⼾体验:
- URL 重写: 每当服务器响应需要保持状态的请求时,将Session ID 附加到 URL 中作为参数。例
如,原本的链接 http://example.com/page 变为
> http://example.com/page;jsessionid=XXXXXX,服务器端需要相应地解析 URL 来获取 Session ID ,并维护⽤⼾的会话状态。这种⽅式的缺点是URL 变得不那么整洁,且如果⽤⼾通过电⼦邮件
或其他⽅式分享了 这样的链接,可能导致Session ID 的意外泄露。
- 隐藏表单字段:在每个需要Session 信息的HTML 表单中包含⼀个隐藏字段,⽤来存储Session ID 。当表单提交时,Session ID 随表单数据⼀起发送回服务器, 服务器通过解析表单数据中的
Session ID 来获取⽤⼾的会话状态。这种⽅法仅适⽤于通过表单提交的交互 模式,不适合链接
点击或Ajax 请求。
RocketMQ
RocketMQ 的使⽤场景有哪些?
** 解耦:** 可以在多个系统之间进⾏解耦,将原本通过⽹络之间的调⽤的⽅式改为使⽤MQ 进⾏
消息的异步通讯,只要该操作不是需要同步的,就可以改为使⽤MQ 进⾏不同系统之间的联系,
这样项⽬之间不会存在耦合,系统之间不会产⽣太⼤的影响,就算⼀个系统挂了,也只是消息
挤压在MQ ⾥⾯没⼈进⾏消费⽽已,不会对其他的系统产⽣影响。
** 异步:** 加⼊⼀个操作设计 到好⼏个步骤,这些步骤之间不需要同步完成,⽐如客⼾去创建了
⼀个订单,还要去客⼾轨迹系统添加⼀条轨迹、去库存系统更新库存、去客⼾系统修改客⼾的
状态等等 。这样如果这个系统都直接进⾏调⽤,那么将会产⽣⼤量的时间,这样对于客⼾是⽆
法接收的;并且像添加客⼾轨迹这种操作是不需要去同步操作的,如果使⽤MQ 将客⼾创建订单
时,将后⾯的轨迹、库存、状态等信息的更新全都放到MQ ⾥⾯然后去异步操作,这样就可加快
系统的访问速度,提供更好的客⼾体验。
** 削峰:** ⼀个系统访问流量有⾼峰时期,也有低峰时期,⽐如说,中午整点有⼀个抢购活动等等。⽐如系统平时流量并不⾼,⼀秒钟只有100 多个并发请求,系统处理没有任何 压⼒,⼀切⻛
平浪静,到了某个抢购活动时间,系统并发访问了剧增,⽐如达到了每秒5000 个并发请求,⽽
我们的系统每秒只能处理2000 个请求,那么由于流量太⼤,我们的系统、数据库可能就会崩
溃。这时如果使⽤MQ 进⾏流量削峰,将⽤⼾的⼤量消息直接放到MQ ⾥⾯,然后我们的系统去
按⾃⼰的最⼤消费能⼒去消费这些消息,就可以保 证系统的稳定,只是可能要跟进业务逻辑,
给⽤⼾返回特定⻚⾯或者稍后通过其他⽅式通知其结果
Redis
Redis 雪崩和穿透介绍⼀下,解决⽅案是什么 ?
缓存雪崩
当⼤量缓存数据在同⼀时间过期(失效)时,如果此时有⼤量的⽤⼾请求,都⽆法在 Redis 中处
理,于是全部请求都直接访问数据库,从⽽导致数据库的压⼒骤增,严重的会造成数据库宕机,
从⽽形成⼀系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

对于缓存雪崩问题,我们可以采⽤两种⽅案解决。
将缓存失效时间随 机打散: 我们可以在原有的失效时间基础上增加⼀个随机值(⽐如 1 到 10
分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
设置缓 存不过期: 我们可以通过后台 服务来更 新缓存数据,从⽽避免因为缓存失效造成的缓存
雪崩,也可以在⼀定程度上避免缓存并发问题。
缓存穿透
当⽤⼾访问的数据,既不在缓存中,也不 在数据库中,导致请求在访问缓存时,发现缓存缺失,
再去访问数据库时,发现数据库中也 没有要访问的数据,没办法构建缓存数据,来服 务后续的请
求。那么当有⼤量这样的请求到来时,数据库的压⼒骤增,这就是缓存穿透的问题。

缓存穿透的发⽣⼀般有这两种情况:
业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有
数据;
⿊客恶意攻击,故意⼤量访问某些读取不存在数据的业务;
应对缓存穿透的⽅案,常⻅的⽅案有三种。
Powered by GitHub & Vssue
⾮法请求的限制:当有⼤量恶意请求访问不存在的数据的时候,也会发⽣缓存穿透,因此在
API ⼊⼝处我们要判断求请求参数是否合 理,请求参数是否含 有⾮法值、请求字段是否存在,
如果判断出是恶意请求就直接返回错误,避免进⼀步访问缓存和数据库。
设置空值或者默认值:当我们线上业 务发现缓存穿透的现象时,可以针对查询的数据,在缓存
中设置⼀个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应
⽤,⽽不会继续查询数据库。
使⽤布隆过滤器快速判断数 据是否存在,避免通过查询数据库来判断数 据是否存在:我们可以
在写⼊数据库数据时,使⽤布隆过滤器做个标记,然后在⽤⼾请求到来时,业务线程确认缓存
失效后,可以通过查询布隆过滤器快速判断数 据是否存在,如果不存在,就不⽤通过查询数据
库来判断数 据是否存在,即使发⽣了缓存穿透,⼤量请求只会查询 Redis 和布隆过滤器, ⽽不
会查询数据库,保证了数据库能正常运⾏,Redis ⾃⾝也是⽀持布隆过滤器的。
