京东 ⼆⾯
京东 Java ⾯试
京东 早在秋招开展的时候,就在官⽅上明确说今年的校招薪资会有明确的涨幅,⽽且说上调不低
于 20% 。
等了好⼏个⽉,那就在前⼏天,终于等到京东 25 届校招开奖了,我们⼀起来看看 今年京东 相⽐去
年多了多少?
我们先来看看 去年24 届京东 的校招开发岗位的薪资,跟其他⼀线⼤⼚基本差不多,已经是⽐较有
竞争⼒的了。

京东 往年统⼀都是 16 薪,今年就开始有点不⼀样的了,不同部⻔的年终奖⽉数开始不⼀样的了,
⽐如京东 科技今年是 20 薪,京东 零售是 19 薪,⽽京东 物流还是 16 薪。
我从⽹上⼀些同学爆料的薪资来看,今年京东 整体是⽐去年多了不 少。
今年开的薪资极具有竞争⼒,⼀般互联⽹⼤⼚的⽩菜总包是 35w+ ,⽽今年京东 ⽩菜年包基本都是
40w+ 的,甚⾄ sp offer 都能突破 50w 年薪了。
京东 科技是能拿到 20 薪,⽬前已知部⻔最⾼的,也就是拿 8 个⽉年终奖,下⾯是 25 届京东 科技
的后端开发岗位的校招薪资情况:

27k x 20 ,同学 bg 未知,base 北京,部⻔京东 科技
24k x 20 ,同学 bg 硕⼠ 211 ,base 北京,部⻔京东 科技
21k x 20 ,同学 bg 未知,base 北京,部⻔京东 科技
京东 零售年终奖是 7 个⽉,也就是 19 薪,虽然⽐京东 科技少⼀个⽉,但是 base 还是⽐京东 科技
多⼀些,下⾯是 25 届京东 零售的后端开发岗位的校招薪资情况:

33.5k x 19 ,同学 bg 硕⼠ 211 ,base 北京,部⻔京东 零售
31k x 19 ,同学 bg 硕⼠双⼀流,base 北京,部⻔京东 零售
29k x 19 ,同学 bg 硕⼠ 211 ,base 北京,部⻔京东 零售
25.5k x 19 ,同学 bg 硕⼠985 ,base 北京,部⻔京东 零售
京东 物流年终奖没变化 ,还是 4 个⽉年终奖,也就是 16 薪,下⾯是 25 届京东 物流的后端开发岗
位的校招薪资情况:

30k x 16 ,同学 bg 硕211 ,base 北京,部⻔京东 物流
27k x 16 ,同学 bg 硕⼠985 ,base 北京,部⻔京东 物流
26.5k x 16 ,同学 bg 硕⼠海⻳,base 北京,部⻔京东 物流
算法岗那就更夸张了,基本都是 30k-40k+ x 19 ,最⾼的⽬前看到是 80w 年薪:
42k x 19 + 5w 签字费,同学 bg top2 本硕,base 北京
那肯定很多⼈就好奇 ,今年京东 多了这么多年终奖,真的能拿满吗?
也有同学问了京东 hr ,hr 反馈是这样说的:「 19 薪⼤部分⼈能拿满,20% 可以拿 19+ ,⽉薪绩效
占⽐ 20% ,也就是⼤部分都能拿满,少数上下 浮动」。
虽然现在不少⼤⼚都开始开奖了,但是并不代表秋招就已经结 束了。
实际上秋招时间还是很⻓的,能持续到年底 ,原因很简单,⽐较早的秋招能拿到⼤⼚的同学,⼿
上都会有好⼏个⼤⼚ offer ,但是他们最终只能选择⼀个,等他们毁约了,hc ⾃然就会空缺出来
了,于是就会从简历池继续捞⼈⾯试了,往往 11-12 ⽉份捡漏的机会还是很多的。
那这次,我们来看看 今年京东 校招后端开发的⾯经,这是⼆⾯的⾯经,可能是笔试成绩⽐较好的
原因,没有⼿撕算法,主要以纯⼋股拷打为主 ,并且问得不深,⾯试官还是⽐较友善的。

京东 ⼆⾯
Java 运⾏线程的⼏种⽅式?
继承Thread 类
这是最直接的⼀种⽅式,⽤⼾⾃定义类继承java.lang.Thread 类,重写其 run() ⽅法,run() ⽅法中定
义了 线程执⾏的具体任 务。创建该类的实例后,通过调⽤start() ⽅法启动线程。
class MyThread extends Thread {
@Override
}
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 类的静态⽅法创
建不同类型的线程池。
@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 利⽤率和系统吞吐 量。
类加载过 程是怎么样的?
类从被加载到虚拟机内存开始,到卸载出内存为⽌,它的整个⽣命周 期包括以下 7 个阶段:

加载:通过类的全限定名(包名 + 类名),获取到该类的.class ⽂件的⼆进制字节流,将⼆进制字节流所代表的静态存储结构,转化为⽅法区运⾏时的数据结构,在内存中⽣成⼀个代表该类
的java.lang.Class 对象,作为⽅法区这个类的各种数据的访问⼊⼝
连接:验证、准备、解析 3 个阶段统称为连接。
验证:确保class ⽂件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class 类的正确性,不会危害到虚拟机的安全。验证阶段⼤致会完成以下四个阶段的检验动
作:⽂件格式校验、元数据验证、字节码验证、符号引⽤验证
准备:为类中的静态字段分配内存,并设置默认的初始值,⽐如int 类型初始值是0。被final
修饰的static 字段不会设置,因为final 在编译的时候就分配了
解析:解析阶段是虚拟机将常量池的「符号引⽤」直接替换为「直接引⽤」的过程。符号引
⽤是以⼀组符号来描述所引⽤的⽬标,符号可 以是任何 形式 的字⾯量,只要使⽤的时候可以
⽆歧义地定位到⽬标即可。直接引⽤可以是直接指 向⽬标的指针、相对偏移量或是⼀个能间
接定位到⽬标的句柄,直接引⽤是和虚拟机实现的内存布局相关的。如果有 了直接引⽤, 那
引⽤的⽬标必定已经存在在 内存中了 。
初始化:初始化是整个类加载过 程的最后⼀个阶段,初始化阶段简单来说就是执⾏类的构造器
⽅法 ,要注意的是这⾥的构造器⽅法() 并不是开发者写的,⽽是编译器⾃动⽣成的。使⽤:使⽤类或者创建对象
卸载:如果有 下⾯的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是java 堆中不存在该类的任何 实例。2. 加载该类的ClassLoader 已经被回收。 3. 类对应的java.lang.Class 对
象没有任何 地⽅被引⽤,⽆法在任何 地⽅通过反射访问该类的⽅法。
实际中类加载会遇到哪些问题?
⾸先我们来捋⼀捋类加载的基础知识。

以上是⼤家⽐较熟悉的类加载器模型,主要包含 3 种类加载器:
BootstrapClassloader 根加载器, 也就是系统类加载器, 加载核⼼库,如 rt.jar 。
ExtensionClassloader 扩展类加载器, 主要加载/ext/ 下⾯的 jar 包
AppClassloader 离我们最近的类加载器, 负责 加载 classpath 下的类,开发时候我们的代码⼤部
分由其加载。
此外我们⽐较需要知道的⼏点:
⼀个类是由 jvm 加载是通过类加载器+全限定类名确定唯⼀性的。
双亲委派,众所周知,⼦加载器会尽量委托给⽗加载器进⾏加载,⽗加载器找不到再⾃⼰加载
线程上下 ⽂类加载,为了 满⾜ spi 等需求突破双亲委派机制,当⾼层类加载器想加载底层类时
通过 Thread.contextClassLoader 来获取当前线程的类加载器(往往 是底层类加载器)去加载类。
ClassNotFoundException
ClassNotFoundException 表⽰类找不到异常,是⼀种 Exception ,通常发⽣在载⼊阶段,当开 发者主动调⽤ Class.forName() 、 ClassLoader.loadClass() 或 ClassLoader.findSystemClass() 动态加载指定类时候,类加载器就会去 classpath 下寻找类,如果找不到就会抛出此错误。
还有另外⼀种情况是当⼀个类已经被某个类加载器加载到内存中,另外⼀个类加载器试图去加载
时也会发⽣错误。
ClassNotFoundException 是⼀个 exception 类,同时发⽣在主动执⾏动态加载时,所以我们应该
去 catch 它,防⽌发⽣⼀些运⾏时错误。
NoClassDefFoundError
NoClassDefFoundError 是⼀种和 ClassNotFoundException 很像的错误,只不过它是更严重的
error 类型。它发⽣在链接阶段,表⽰ jvm 在编译阶段可以找到相应的类,但在执⾏过程中却找不
到相应的类。
⼀种原因是由于在编译后运⾏前类被更改或者删除了。另外⼀种则是 classpath 本⾝被修改过了,
这可以通过 System.getProperty("java.classpath") 来找到程序实际运⾏的 classpath ,或者通过-classpath 命令来指定正确的 classpath 。那如果是在 ide 中开发,很多时候出现的情况是我们可以通过 ide 编译通过,但在实际运⾏的
WEB-INF/lib 下却是没有的。所以排查的时候我们需要去实际的 war 包下⾯确定是否有类。
NoSuchMethodError
我们还会遇到 NoSuchMethodError 错误,它表⽰找不到⽅法,但找不到⽅法归根结底是找到了不
正确的类。
通常情况下是因为 jar 包冲突问题,即加载了不 匹配版本的类导致的。例如应⽤中有 A、B 两个 ⼆
⽅包,A 依赖 C-v1 包,⽽ B 依赖 C-v2 包,如果 maven 仲裁最后使⽤的是 C-v1 包,那么当 B 加
载到 C-v2 中有⽽ C-v1 中没有的⽅法时就会报 NoSuchMethodError 。
介绍⼀下mysql 索引?
MySQL InnoDB 引擎是⽤了B+ 树作为了 索引的数据结构。
B+Tree 是⼀种多叉树,叶⼦节点才存放数 据,⾮叶⼦节点只存放索引,⽽且每个节点⾥的数据是
按主键顺序存放的。每⼀层⽗节点的索引值都会出现在下层⼦节点的索引值中,因此在叶⼦节点
中,包括了所有的索引值信息,并且每⼀个叶⼦节点都有两个 指针,分别 指向下⼀个叶⼦节点和
上⼀个叶⼦节点,形成⼀个双向链表。
主键索引的 B+Tree 如图所⽰:

⽐如,我们执⾏了下 ⾯这条查 询语句:
select * from product where id= 5;这条语句使⽤了主 键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会⾃顶向下逐层进⾏
查找:
将 5 与根节点的索引数据 (1 ,10 ,20) ⽐较,5 在 1 和 10 之间,所以根据 B+Tree 的搜索逻
辑,找到第⼆层的索引数据 (1 ,4,7) ;
在第⼆层的索引数据 (1 ,4,7) 中进⾏查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数
据(4,5,6);在叶⼦节点的索引数据(4,5,6)中进⾏查找,然后我们找到了索引值为 5 的⾏数据。
数据库的索引和数据都是存储在硬盘的 ,我们可以把读取⼀个节点当作⼀次磁盘 I/O 操作。那么上⾯的整个查询过程⼀共经历了 3 个节点,也就是进⾏了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层⾼度就可以满⾜,这意味着从千万级的表查询⽬标数据最
多需要 3-4 次磁盘 I/O ,所以B+Tree 相⽐于 B 树和⼆叉树来说,最⼤的优势在于查询效率很⾼,
因为即使在数据量很⼤的情况,查询⼀个数据的磁盘 I/O 依然维持在 3-4 次。
mysql ⽇志你知道哪些?
redo log 重做⽇志,是 Innodb 存储引擎层⽣成的⽇志,实现了事 务中的持久性,主要⽤于掉电
等故障恢复;
undo log 回滚⽇志,是 Innodb 存储引擎层⽣成的⽇志,实现了事 务中的原⼦性,主要⽤于事
务回滚和 MVCC 。
bin log ⼆进制⽇志,是 Server 层⽣成的⽇志,主要⽤于数据备份和主从 复制;
relay log 中继⽇志,⽤于主从 复制场景下,slave 通过io 线程拷⻉master 的bin log 后本地⽣成的
⽇志
慢查询⽇志,⽤于记录执⾏时间过⻓的sql ,需要设置阈值后⼿动开启
mysql 订阅binlog 的中间件是什么 ?有什么 作⽤?
是阿⾥巴巴 开源的 Canal 中间件,它通过模拟 MySQL 主从 复制的交互 协议,把⾃⼰伪装成⼀个
MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog
给 Canal ,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使⽤。
下图是 Canal 的⼯作原理:

将binlog ⽇志采集发送到MQ 队列⾥⾯,然后编写⼀个简单的缓存删除消息者订阅binlog ⽇志,根
据更新log 删除缓存,并且通过ACK 机制确认处理这条更 新log ,保证mysql 和 redis 数据缓存⼀致
性。
谈⼀谈对spring ioc 、aop 的理解?
Spring IoC 和AOP 区别:
loC :即控制反转的意思,它是⼀种创建和获取对象的技术思想,依赖注⼊(DI) 是实现这种技术的⼀种⽅式。传统开发过程中,我们需要通过new 关键字来创建对象。使⽤IoC 思想开发⽅式的
话,我们不通过new 关键字创建对象,⽽是通过IoC 容器来帮我们实例化对象。 通过IoC 的⽅
式,可以⼤⼤降低对象之间的耦合度。
AOP :是⾯向切⾯编程,能够将那些与业 务⽆关,却为业 务模块所共同调⽤的逻辑封装起来,
以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理
的对象,实现了某个接⼝,那么 Spring AOP 会使 ⽤ JDK Proxy ,去创建代理对象,⽽对于没有
实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候 Spring AOP 会使 ⽤ Cglib ⽣成
⼀个被代理对象的⼦类来作为代理。
Spring IOC 实现机制
反射:Spring IOC 容器利⽤Java 的反射机制动 态地加载类、创建对象实例及调⽤对象⽅法,反射
允许在运⾏时检查类、⽅法、属性等信息,从⽽实现灵活的对象实例化和管理。
依赖注⼊:OC 的核⼼概念是依赖注⼊,即容器负责 管理应⽤程序组件之间的依赖关系。Spring
通过构造函数注⼊、属性注⼊或⽅法注 ⼊,将组件之间的依赖关系描述在配置⽂件中或使⽤注
解。
设计 模式 - ⼯⼚模式:Spring IOC 容器通常采⽤⼯⼚模式来管理对象的创建和⽣命周 期。容器作
为⼯⼚负责 实例化Bean 并管理它们的⽣命周 期,将Bean 的实例化过程交给容器来管理。
容器实现:Spring IOC 容器是实现IOC 的核⼼,通常使⽤BeanFactory 或ApplicationContext 来管
理Bean 。BeanFactory 是IOC 容器的基本形式 ,提供基本的IOC 功能;ApplicationContext 是
BeanFactory 的扩展,并提供更多企业级功能。
Spring AOP 实现机制
Spring AOP 的实现依赖于动态代理技术。动态代理是在运⾏时动态⽣成代理对象,⽽不是在编译
时。它允许开发者在运⾏时指定要代理的接⼝和⾏为,从⽽实现在不修改源码的情况下增强⽅法
的功能。
Spring AOP ⽀持两种动态代理:
基于JDK 的动态代理:使⽤java.lang.reflect.Proxy 类和java.lang.reflect.InvocationHandler 接⼝实
现。这种⽅式需要代理的类实现⼀个或多个接⼝。
基于CGLIB 的动态代理:当被代理的类没有实现接⼝时,Spring 会使 ⽤CGLIB 库⽣成⼀个被代理
类的⼦类作为代理。CGLIB (Code Generation Library )是⼀个第三⽅代码⽣成库,通过继承⽅
式实现代理。
redis 中常⽤数据结构,以及应⽤场景?
Redis 提供了丰 富的数据类型,常⻅的有五种数据类型:String (字符串),Hash (哈希),List
(列表),Set (集合)、Zset (有序集合)。


随着 Redis 版本的更新,后⾯⼜⽀持了四种数据类型:BitMap (2.2 版新增)、HyperLogLog (2.8 版新增)、GEO (3.2 版新增)、Stream (5.0 版新增)。Redis 五种数据类型的应⽤场景:
String 类型的应⽤场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应⽤场景:消息队列(但是有两个 问题:1. ⽣产者需要⾃⾏实现全局唯⼀ ID ;2. 不
能以消费组形式 消费数据)等。
Hash 类型:缓存对象、购物⻋等。
Set 类型:聚合计算(并集、交集、差集)场景,⽐如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,⽐如排⾏榜、电话和姓名排序等。
Redis 后续版本⼜⽀持四种数据类型,它们的应⽤场景如下:
BitMap (2.2 版新增):⼆值状态统计的场景,⽐如签到、判断⽤⼾登陆状态、连续签到⽤⼾总
数等;
HyperLogLog (2.8 版新增):海量数据基数统计的场景,⽐如百万级⽹⻚ UV 计数等;
GEO (3.2 版新增):存储地理位置信息的场景,⽐如滴滴 叫⻋;
Stream (5.0 版新增):消息队列,相⽐于基于 List 类型实现的消息队列,有这两个 特有的特
性:⾃动⽣成全局唯⼀消息ID ,⽀持以消费组形式 消费数据。
redis 持久化机制?
Redis 的读写操作都是在内存中,所以 Redis 性能才会⾼,但是当 Redis 重启后 ,内存中的数据就
会丢失,那为了 保证内存中的数据不会丢失,Redis 实现了数据持 久化的机制,这个机制会把数据
存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。Redis 共有三种数据持 久化的⽅
式:
AOF ⽇志:每执⾏⼀条写操作命令,就把该命令以 追加的⽅式写⼊到⼀个⽂件⾥;
RDB 快照:将某⼀时刻的内存数据,以⼆进制的⽅式写⼊磁盘;
AOF ⽇志是如何实现的?
Redis 在执⾏完⼀条写操作命令后,就会把该命令以 追加的⽅式写⼊到⼀个⽂件⾥,然后 Redis 重
启时,会读取该⽂件记录的命令,然后逐⼀执⾏命令的⽅式来进⾏数据恢复。

我这⾥以「set name xiaolin 」命令作 为例⼦,Redis 执⾏了这条命令后,记录在 AOF ⽇志⾥的内
容如下图:

Redis 提供了 3 种写回硬盘的 策略, 在 Redis.conf 配置⽂件中的 appendfsync 配置项可以有以下
3 种参数可填:
Always ,这个单词的意思是「总是」,所以它的意思是每次 写操作命令执⾏完后,同步将 AOF
⽇志数据写回硬盘;
Everysec ,这个单词的意思是「每秒」,所以它的意思是每次 写操作命令执⾏完后,先将命令写
⼊到 AOF ⽂件的内核缓冲区,然后每隔⼀秒将缓冲区⾥的内容写回到硬盘;
No ,意味着不由 Redis 控制写回硬盘的 时机,转交给操作系统控制写回的时机,也就是每次 写
操作命令执⾏完后,先将命令写⼊到 AOF ⽂件的内核缓冲区,再由操作系统决定何时将缓冲区
内容写回硬盘。
我也把这 3 个写回策略的优缺点总结成了⼀张表格:

RDB 快照是如何实现的呢?
因为 AOF ⽇志记录的是操作命令,不是实际的数据,所以⽤ AOF ⽅法做故障恢复时,需要全量把
⽇志都执⾏⼀遍,⼀旦 AOF ⽇志⾮常多,势必会造成 Redis 的恢复操作缓慢。为了 解决这个问
题,Redis 增加了 RDB 快照。
所谓的快照,就是记录某⼀个瞬间东西,⽐如当我们给⻛景拍照时,那⼀个瞬间的画⾯和信息就
记录到了⼀张照⽚。所以,RDB 快照就是记录某⼀个瞬间的内存数据,记录的是实际数据,⽽
AOF ⽂件记录的是命令操作的⽇志,⽽不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数
据的效率会⽐ AOF ⾼些,因为直接将 RDB ⽂件读⼊内存就可以,不需要像 AOF 那样还需要额外
执⾏操作命令的步骤才能恢复数据。
Redis 提供了两个 命令来⽣成 RDB ⽂件,分别 是 save 和 bgsave ,他们的区别就在于是否在「主线程」⾥执⾏:
执⾏了 save 命令,就会在主线程⽣成 RDB ⽂件,由于和执⾏操作命令在同⼀个线程,所以如
果写⼊ RDB ⽂件的时间太⻓,会阻塞主线程;
执⾏了 bgsave 命令,会创建⼀个⼦进程来⽣成 RDB ⽂件,这样可以避免主线程的阻塞;
AOF 和RDB 优缺点
AOF :
优点:⾸先,AOF 提供了更好的数据安全性,因为它默认每接收到⼀个写命令就会追加到 ⽂件
末尾。即使Redis 服务器宕机,也只会丢失最后⼀次写⼊前的数据。其次,AOF ⽀持多种同步策
略(如everysec 、always 等),可以根据需要调整数 据安全性和性能之间的平衡。同时,AOF ⽂
件在Redis 启动时可以通过重写机制优化,减少⽂件体 积,加快恢复速度。并且,即使⽂件发⽣
损坏,AOF 还提供了redis-check-aof ⼯具来修复损坏的⽂件。
缺点:因为记录了每⼀个写操作,所以AOF ⽂件通常⽐RDB ⽂件更⼤,消耗更多的磁盘空间。
并且,频繁的磁盘IO 操作(尤其是同步策略设置为always 时)可能会对Redis 的写⼊性能造成⼀
定影响。⽽且,当问个⽂件体 积过⼤时,AOF 会进⾏重写操作,AOF 如果没有开启AOF 重写或者
重写频率较低,恢复过程可能较慢,因为它需要重放所有的操作命令。
RDB :
优点: RDB 通过快照的形式 保存某⼀时刻的数据状态,⽂件体 积⼩,备份和恢复的速度⾮常快。
并且,RDB 是在主线程之外通过fork ⼦进程来进⾏的,不会阻塞服务器处理命令请求,对Redis
服务的性能影响较⼩。最后,由于是定期快照,RDB ⽂件通常⽐AOF ⽂件⼩得多。
缺点: RDB ⽅式在两次快照之间,如果Redis 服务器发⽣故障,这段时间的数据将会丢失。并且,
如果在RDB 创建快 照到恢复期间有写操作,恢复后的数据可能与故障前的数据不完全⼀致
限流算法有哪些?
限流是当⾼并发或者瞬时⾼并发时,为了 保证系统的稳定性、可⽤性,对超出服务处理能⼒之外
的请求进⾏拦截 ,对访问服务的流量进⾏限制。
常⻅的限流算法有四种:固定窗⼝限流算法、滑动窗⼝限流算法、漏桶限流算法和令牌桶限流算
法。
固定窗⼝限流算法实现简单,容易理解,但是流量曲线可能不够平滑,有“突刺现象”,在窗⼝
切换时可能会产⽣两倍于阈值流量的请求。
滑动窗⼝限流算法是对固定窗⼝限流算法的改进,有效解决了窗⼝切换时可能会产⽣两倍于阈
值流量请求的问题。
漏桶限流算法能够对流量起到整流的作⽤,让随机不稳定的流量以固定的速率流出,但是不能
解决流量突发的问题。
令牌桶算法作为漏⽃算法的⼀种改进,除了能够起到平滑流量的作⽤,还允许⼀定程度的流量
突发。
固定窗⼝限流算法
固定窗⼝限流算法就是对⼀段固定时间窗⼝内的请求进⾏计数,如果请求数超过了阈值,则舍弃
该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗⼝结束时,重置计数器
为0。

固定窗⼝限流优点是实现简单,但是会有“流量吐刺”的问题,假设窗⼝⼤⼩为1s ,限流⼤⼩为
100 ,然后恰好在某个窗⼝的第999ms 来了100 个请求,窗⼝前期没有请求,所以这100 个请求都会通过。再恰好,下⼀个窗⼝的第1ms 有来 了100 个请求,也全部通 过了,那也就是在2ms 之内通过了200 个请求,⽽我们设定的阈值是100 ,通过的请求达到了阈值的两倍,这样可能会给系统造成
巨⼤的负载压⼒。

滑动窗⼝限流算法
改进固定窗⼝缺陷的⽅法是采⽤滑动窗⼝限流算法,滑动窗⼝就是将限流窗⼝内部切分 成⼀些更
⼩的时间⽚,然后在时间轴上滑动,每次 滑动,滑过⼀个⼩时间⽚,就形成⼀个新的限流窗⼝,
即滑动窗⼝。然后在这个滑动窗⼝内执⾏固定窗⼝算法即可。滑动窗⼝可以避免固定窗⼝出现的
放过两倍请求的问题,因为⼀个短时间内出现的所有请求必然在⼀个滑动窗⼝内,所以⼀定会被
滑动窗⼝限流。

漏桶限流算法
漏桶限流算法是模拟⽔流过⼀个有漏洞的桶进⽽限流的思路,如图。
⽔⻰头的⽔先流⼊漏桶,再通过漏桶底部的孔流出。如果流⼊的⽔量太⼤,底部的孔来不及流
出,就会导致⽔桶太满溢 出去。从系统的⻆度来看,我们不知道什么 时候会有请求来,也不 知道
请求会以 多⼤的速率来,这就给系统的安全性埋下了 隐患。但是如果加了⼀层漏⽃算法限流之
后,就能够保证请求以恒定的速率流出。在系统看来,请求永 远是以平滑的传输速率过来,从⽽
起到了保护系统的作⽤。使⽤漏桶限流算法,缺点有两个 :
即使系统资源很空闲,多个请求同时到达时,漏桶也是慢慢 地⼀个接⼀个地去处理请求,这其
实并不符合⼈们的期望 ,因为这样就是在浪费计算资源。
不能解决流量突发的问题,假设漏⽃速率是2个/秒,然后突然来了10 个请求,受限于漏⽃的容
量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏⽃速率是2个/秒,然后瞬间接受了5
个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有⻢上被处
理,处理的速度仍然是我们设定的2个/秒,所以没有解决流量突发的问题
令牌桶限流算法
令牌桶是另⼀种桶限流算法,模拟⼀个特定⼤⼩的桶,然后向 桶中以特定的速度放⼊令牌
(token ),请求到达后,必须从桶中取出⼀个令牌才能继续处理。如果桶中已经没有令牌了,那
么当前请求就被限流。如果桶中的令牌放满了,令牌桶也会溢出。放令牌的动作是持续不断进⾏
的,如果桶中令牌数达到上限,则丢弃令牌,因此桶中可能⼀直持有⼤量的可⽤令牌。此时请求
进来可以直接拿 到令牌执⾏。⽐如设置 qps 为 100 ,那么限流器初始化完成 1 秒后,桶中就已经
有 100 个令牌了,如果此前还没有请求过来,这时突然来了 100 个请求,该限流器可以抵挡瞬时
的 100 个请求。由此可⻅,只有桶中没有令牌时,请求才会进⾏等待,最终表现的效果即为以⼀
定的速率执⾏。令牌桶的⽰意图如下:

令牌桶限流算法综合效果⽐较好,能在最⼤程度利⽤系统资源处理请求的基础上,实现限流的⽬
标,建议通常场景中优先使⽤该算法。
谈谈 对rocketmq 的理解?
消息队列主要有三⼤作⽤:
解耦:可以在多个系统之间进⾏解耦,将原本通过⽹络之间的调⽤的⽅式改为使⽤MQ 进⾏消息
的异步通讯,只要该操作不是需要同步的,就可以改为使⽤MQ 进⾏不同系统之间的联系,这样
项⽬之间不会存在耦合,系统之间不会产⽣太⼤的影响,就算⼀个系统挂了,也只是消息挤压
在MQ ⾥⾯没⼈进⾏消费⽽已,不会对其他的系统产⽣影响。
异步:加⼊⼀个操作设计 到好⼏个步骤,这些步骤之间不需要同步完成,⽐如客⼾去创建了⼀
个订单,还要去客⼾轨迹系统添加⼀条轨迹、去库存系统更新库存、去客⼾系统修改客⼾的状
态等等 。这样如果这个系统都直接进⾏调⽤,那么将会产⽣⼤量的时间,这样对于客⼾是⽆法
接收的;并且像添加客⼾轨迹这种操作是不需要去同步操作的,如果使⽤MQ 将客⼾创建订单
时,将后⾯的轨迹、库存、状态等信息的更新全都放到MQ ⾥⾯然后去异步操作,这样就可加快
系统的访问速度,提供更好的客⼾体验。
削峰:⼀个系统访问流量有⾼峰时期,也有低峰时期,⽐如说,中午整点有⼀个抢购活动等等。⽐如系统平时流量并不⾼,⼀秒钟只有100 多个并发请求,系统处理没有任何 压⼒,⼀切⻛
平浪静,到了某个抢购活动时间,系统并发访问了剧增,⽐如达到了每秒5000 个并发请求,⽽
我们的系统每秒只能处理2000 个请求,那么由于流量太⼤,我们的系统、数据库可能就会崩
溃。这时如果使⽤MQ 进⾏流量削峰,将⽤⼾的⼤量消息直接放到MQ ⾥⾯,然后我们的系统去
按⾃⼰的最⼤消费能⼒去消费这些消息,就可以保 证系统的稳定,只是可能要跟进业务逻辑,
给⽤⼾返回特定⻚⾯或者稍后通过其他⽅式通知其结果
