Redis
Redis 的 String 底层结构,hash 底层结构,rehash 过程是怎么样的?
sting是sds,特点:二进制安全,预分配机制,获取长度O(1),SDS 在操作时会自动管理内存(预分配、惰性释放),避免 C 字符串的溢出与性能问题。 hash底层是压缩列表或者hashtable。rehash是渐进式rehahs机制,负载因子>1 或者负载因子大于5。步骤:分配新表,标记开始迁移,每次操作的时候迁移一小部分数据。所有桶迁移完毕,h[1]清空,使用另一个表。
热key问题,以及如何处理
- 多级缓存,看看能不能加上应用缓存,
- 热key分片。比如某一个及其热点的key,分片10个,然后用户操作,随机操作某一个,查询的时候再聚合统计。
- 这个热key,生成多个副本。
- 请求合并。(对同一个key的高并发访问,只让一个线程去redis查询,其余等待)
- 热key预加载。
redis 跳表数据结构
- 多层的有序链表,每一层都是有序的,底层包含全部元素,
- 层数越高越稀疏。概率因子0.25,第一层100% 第二层25% 第三层6.25%。层数最高32层。
- 新节点的层数是随机生成的, 插入一个元素的过程:首先根据算法,确定具体的那一层
从最高层开始查找插入位置(定位过程,关键)
从这个随机生成的层数开始,向下到第1层,每层都需要插入这个新元素。
- 先从跳表的最高层头节点开始。
- 在当前层,沿着
forward指针往右走,直到下一个节点的 score 大于等于 E 的 score(或者到达行尾)。这时,当前位置就是该层的前驱(predecessor) ,新节点应该插在它之后。 - 记录下这个前驱到一个临时数组
update[level]中(每一层都记录一个前驱)。 - 然后向下一层,重复上面的查找,直到最低层(层 1)也找到前驱。
- 完成后,
update数组就是:在每层插入点之前的那个节点是谁。
Jvm
1. jvm类加载的过程讲讲,符号引用是什么,哪些情况会发生初始化
符号引用本质上是一个逻辑描述,一组符号来描述所引用的目标,而不是直接指向目标的内存地址。 哪些情况会发生初始化:new操作,反射调用,继承关系(父类还没初始化)
安全点
Safepoint(安全点) 是 JVM 执行过程中一个“全局安全的暂停点”,所有线程在这个点上可以被安全地暂停,执行一些必须在全局一致状态下完成的操作。
换句话说:只有当所有线程到达 Safepoint 时,JVM 才能做全局操作。
1. byte[] a = new byte[10 * 1024]内存分配过程?多大的对象直接进入老年代?通过什么参数配置?
HotSpot JVM 默认采用 TLAB(Thread Local Allocation Buffer) 机制:
每个线程在 Eden 区中预分配一小块私有内存(TLAB),用于快速分配小对象。HotSpot JVM 默认采用 TLAB(Thread Local Allocation Buffer) 机制: 每个线程在 Eden 区中预分配一小块私有内存(TLAB),用于快速分配小对象。
- JVM 首先判断该对象能否放入当前线程的 TLAB;
- 如果能放下,则直接在 TLAB 中分配;
- 如果放不下,则尝试在 Eden 区的公共空间中分配;
- 如果 Eden 空间也不足,则触发 Minor GC。
🔹 对象分配是“在新生代的 Eden 区中进行”的默认策略。
为什么要指针压缩,为什么能指针压缩?原理是什么?
在 JVM 中,对象的引用本质上是一个“指针”,指向堆中对象的起始地址。
正常情况下,这个引用(指针)在 64 位 JVM 上占 8 个字节(64 位) 。指针压缩就是用32位
好处:减少对象内存占用(通常能省30%),提高缓存命中率,减少GC压力。
JVM 在 64 位模式下,将对象引用从 64 位压缩为 32 位(4 字节)来存储。
介绍 TLAB,PLAB,CAS 分配。
TLAB 是每个线程私有的一小块 Eden 空间,用于对象的快速分配。
PLAB 则是用于 对象晋升(Promotion)到 Survivor 或老年代 时的局部分配区。> **PLAB 是 GC 线程在 Minor GC 时使用的局部分配缓冲区。
当线程不能在自己的 TLAB 中分配对象(例如 TLAB 不够用或关闭 TLAB)时,
就需要从 Eden 公共区 分配对象。为了避免全局锁,JVM 使用了 CAS(Compare-And-Swap)原子操作 实现无锁分配。
✅ 概念
CAS 分配即使用原子指令修改“堆分配指针”的方式来实现并发分配。
CMS和G1垃圾回收器的相同点和不同点
cms:仅仅回收老年代,需要配合新生代收集器,传统分带模型。标记清楚算法会产生内存碎片。无法精确控制停顿时间,停顿时间较大,低延迟。cms会产生浮动垃圾。需要等待下一次垃圾回收。cms对cpu资源比较敏感,gc线程会占用较多的cpu资源. G1,全堆可以进行垃圾回收,不需要其他垃圾收集器配合。标记整理算法,不会产生内存碎片。可预测的停顿时间,低延迟+高吞吐量的平衡。不会产生浮动垃圾,筛选回收是并行的。g1更高效的利用多核cpu,针对大内存。
- 大对象处理:G1通过Humongous区域有效处理大对象;CMS处理大对象效率较低 内存布局:java堆分为多个大小相同的redgion,逻辑上连续,物理上不连续,四种类型:-
- Eden:存储新创建的对象
- Survivor:存储从Eden晋升的存活对象
- Old:存储长期存活的对象
- Humongous:存储超大对象(大小超过Region 50%的对象) G1垃圾回收全过程:
1young GC 年轻代回收。(耗时短,处理年轻代的region)
2并发标记周期:触发条件:老年代占用达到阈值(默认45%,通过-XX:InitiatingHeapOccupancyPercent控制)
3 MixedGC(混合GC)
触发条件:并发标记完成后,老年代Region达到回收阈值(由-XX:G1HeapWastePercent=5控制)
工作流程:
-
选择CSet(Collection Set) :
- 选择所有年轻代中的Region
- 选择根据全局并发标记统计得出收集收益高的若干老年代Region(Garbage-First策略)
-
混合回收:
- 年轻代回收:与Young GC相同,处理Eden和Survivor区
- 老年代回收:将存活对象复制到其他空闲Region中
- 选中的Region被清空并加入空闲Region列表
-
循环执行:
- 老年代的Region可能不能一次暂停收集中被处理完
- G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)
特点:
- 同时回收年轻代和部分老年代
- 优先回收垃圾比例最高的区域
- 通过
-XX:MaxGCPauseMillis控制停顿时
4. Full GC(全堆回收)
触发条件:
- 如果Mixed GC无法跟上程序分配内存的速度,导致老年代填满
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
应用程序运行 → 分配新对象到Eden区 → Eden填满 → 触发Young GC
↓
老年代占用达到45% → 触发并发标记周期(Initial Mark → Concurrent Mark → Remark → Cleanup)
↓
触发Mixed GC(回收年轻代+部分老年代)→ 可能多次执行
↓
如果Mixed GC无法跟上内存分配速度 → 触发Full GC
微服务
熔断和降级
熔断:防止系统被雪崩拖垮,保护下游,针对接口,api级别。触发条件:调用下游接口失败率高,异常高。暂时断开对下游的调用。可以进行恢复。 降级:针对模块,功能层面,保护我们的核心服务,一些非必要服务暂时关闭。通常需要人工接入或者系统判断
hystrix原理,半开状态知道么,具体的一个转换过程,它的隔离是怎么实现的
Hystrix 熔断器有三种状态:关闭 (Closed) 、打开 (Open) 、半开 (Half-Open) 。
半开状态 (Half-Open) 是熔断器进行自我修复和恢复判断的关键状态。 状态转换过程
-
关闭 (Closed) → 打开 (Open)
- 触发条件: 在一个滑动时间窗口内,请求总数达到阈值,且失败率(失败、超时或拒绝)超过预设的失败百分比阈值(默认 50%)。
- 后果: 熔断器立即打开。后续对该服务的请求将不再执行,直接走降级逻辑 (Fallback),实现快速失败 (Fail-Fast)。
-
打开 (Open) → 半开 (Half-Open)
- 触发条件: 熔断器打开后,会进入一个休眠时间窗口 (sleepWindowInMilliseconds,默认 5000ms)。
- 后果: 当休眠时间到期后,熔断器自动从 Open 状态切换到 Half-Open 状态。
-
半开 (Half-Open) → 关闭 (Closed)
-
过程: 在 Half-Open 状态下,Hystrix 会允许下一个(默认仅 1 个)请求通过,去调用真实的依赖服务,进行“试探”。
-
判断逻辑:
- 如果该试探请求执行成功: 熔断器认为依赖服务已恢复,立即切换到 Closed 状态,所有请求恢复正常调用。
- 如果该试探请求执行失败: 熔断器认为依赖服务仍未恢复,立即切换回 Open 状态,并重新开始等待下一个休眠时间窗口。
-
spring bean加载过程
实例化 → 依赖注入 → Aware → BeforeInit → 初始化 → AfterInit → 使用 → 销毁
kafka提高消费速度的几种方式
- 多线程消费(提高消费者线程数量)
- 分区数量等于消费者的数量,使得达到最大。
- 消费者异步ack
- 批量消费
主线程如何捕获子线程的异常
- 默认是不会捕获的,可以通过 线程池+get,或者是thread提供的一个方法。
Spi机制
SPI(Service Provider Interface) 是一种 服务发现与动态加载机制,
它允许程序在运行时 动态地发现、加载、替换接口的实现类,而无需在编译期硬编码依赖。
Mysql
changeBuffer特点
change buffer是innodb为了减少随机磁盘IO而设计的一种写优化机制。他的核心思想:当对普通索引页进行修改,如果该页不在内存,不会立即从磁盘加载,而且先把这次修改缓存起来,放到change buffer中,等到后续该页被读入内存时再合并应用。
数据库查询,更新流程
- 对于读操作。InnoDB存储引擎会先检查 Buffer Pool 中是否存在所需的 B+树数据页,如果存在则直接返回数据。如果 Buffer Pool 中没有所需的数据页,则会从磁盘中读取相应的数据页加载到 Buffer Pool 中,再返回数据。同时,如果查询的数据是热点数据,还会将数据页加入到自适应哈希索引豪华套餐中,加速后续的查询。
- • 对于写操作,则会先将数据写入 Buffer Pool,并生成相应的 Undo Log 记录,以便在事务回滚时能够恢复数据的原始状态。接下来,会将写操作记录到 Redo Log Buffer 中,这些 redo log 会周期性地写入到磁盘中的 Redo Log 文件中,就算数据库崩了,已提交的事务也不会丢失。对于辅助索引的更新操作,InnoDB 会将这些更新暂时存储在 Change Buffer 中,等到相关的索引页被读取到 Buffer Pool 时再进行实际的更新操作,从而减少磁盘 I/O,提高写入性能。同时,所有的变更都会记录到 server 层的 binlog 中,以便进行数据恢复。
Spring bean加载顺序
-
Bean 定义加载
- Spring 扫描注解或 XML,读取 BeanDefinition。
-
实例化(Instantiation)
- 调用构造方法或工厂方法创建对象。
- 相当于
new BeanClass()。
-
依赖注入(Populate properties)
- 设置对象的属性(DI)。
- 对象已经实例化,但属性可能尚未设置完成。
-
BeanNameAware / BeanFactoryAware / ApplicationContextAware 回调
- Spring 会把 Bean 的 name、所属容器注入到 Bean 中(可选实现)。
-
BeanPostProcessor 前置处理(postProcessBeforeInitialization)
- 在初始化前,进行自定义操作。
-
初始化(InitializingBean / @PostConstruct / init-method)
- 调用
afterPropertiesSet()或自定义 init 方法。
- 调用
-
BeanPostProcessor 后置处理(postProcessAfterInitialization)
- 初始化后进行处理,例如 AOP 代理增强。
-
Bean 可用
- Bean 已经完全初始化,可以被容器使用。
-
容器关闭时销毁(DisposableBean / @PreDestroy / destroy-method)
- 执行销毁逻辑。
并发
CompletableFuture使用注意事项
- 使用的是ForkJoin,应该使用自定义线程池。
- 异常处理要显示处理,否则异常会被吞掉。比如调用链过程中,如果某一步操作抛出异常,没有处理,后续的不会执行。
- 组合操作要注意依赖关系
- 尽量避免阻塞方法调用,比如get,不在主线程调用get(),尽量使用异步链式操作。
有一个复杂接口,里面需要调用3个外部接口,其中有一个接口,超时10s,我需要在1s内拿到这些任务的返回结果,不需要等另一个接口。具体怎么实现。
import java.util.concurrent.*;
import java.util.*;
public class ParallelCallExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(3); // 线程池
// 假设三个外部接口
Callable<String> task1 = () -> callExternalApi("API1", 500);
Callable<String> task2 = () -> callExternalApi("API2", 1500);
Callable<String> task3 = () -> callExternalApi("API3", 800);
List<Callable<String>> tasks = Arrays.asList(task1, task2, task3);
List<String> results = new ArrayList<>();
try {
// invokeAll 支持超时参数,超时后未完成任务会被取消
List<Future<String>> futures = executor.invokeAll(tasks, 1, TimeUnit.SECONDS);
for (Future<String> future : futures) {
try {
if (!future.isCancelled()) {
results.add(future.get()); // 获取返回值
}
} catch (ExecutionException e) {
// 接口调用异常,忽略或记录
e.printStackTrace();
}
}
System.out.println("聚合结果:" + results);
} finally {
executor.shutdown();
}
}
// 模拟外部接口调用,sleep 模拟耗时
private static String callExternalApi(String apiName, long delayMs) throws InterruptedException {
Thread.sleep(delayMs);
return apiName + " response";
}
}
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> callApi("API1"));
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> callApi("API2"));
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> callApi("API3"));
List<CompletableFuture<String>> futures = Arrays.asList(f1, f2, f3);
// 设置超时 1s
List<String> results = futures.stream().map(f -> {
try {
return f.get(1, TimeUnit.SECONDS);
} catch (Exception e) {
return null; // 超时或异常忽略
}
}).filter(Objects::nonNull).toList();
微服务
服务端负载均衡和客户端负载均衡有什么区别
终端(APP / Web) → 服务端LB(Nginx/SLB) → 微服务 → 客户端LB(Ribbon/...) 也就是说: 入口流量走服务端 LB 服务间调用走客户端 LB
🛠 适用场景对比 ✔ 客户端负载均衡(更适合微服务) 常见于:
Spring Cloud(Ribbon、LoadBalancer)
Nacos、Eureka、Consul
gRPC
Dubbo
服务网格 Sidecar(严格来说是 proxy,又像 server-side)
典型场景:
微服务之间内部调用
每个 service 实例经常变动
要配合注册中心动态发现
优点:
不需要专用 LB
扩缩容快速感知
分布式,高并发更友好
✔ 服务端负载均衡(更适合对外流量) 常见于:
Nginx / LVS / HAProxy
阿里云 SLB / AWS ELB
F5
典型场景:
对外网关入口:Web → Nginx → 服务
静态资源、API 接入层
大型网站、浏览器访问
优点:
集中策略、可控性高
支持更多特性(TLS、WAF、安全)
对客户端透明
新技术
系统设计题目
给你一个1个G的文件,文件每一行都是一个句子。统计出现频率最高的句子。内存只有200m。怎么实现呢,
缓存更新策略
读多写少的系统如何更新
write Back 策略。核心:先写缓存,然后后台异步更新数据库。 特点:写入性能很高。为什么要用这种策略? 因为写入很高,写入缓存比写入数据库快太多了。- 缓存作为主要数据存储:系统设计以缓存数据为主,数据库数据为辅
虚拟线程
虚拟线程(Virtual Threads)
- 由 JVM 调度,轻量级线程
- 栈空间按需分配(通常几 KB)
- 可以创建成 百万级线程
- IO 阻塞不会阻塞操作系统线程
- 与传统线程一样支持同步 API