后端开发知识点总结

86 阅读14分钟

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,针对大内存。

  1. 大对象处理: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控制)

工作流程

  1. 选择CSet(Collection Set)

    • 选择所有年轻代中的Region
    • 选择根据全局并发标记统计得出收集收益高的若干老年代Region(Garbage-First策略)
  2. 混合回收

    • 年轻代回收:与Young GC相同,处理Eden和Survivor区
    • 老年代回收:将存活对象复制到其他空闲Region中
    • 选中的Region被清空并加入空闲Region列表
  3. 循环执行

    • 老年代的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) 是熔断器进行自我修复和恢复判断的关键状态。 状态转换过程

  1. 关闭 (Closed) → 打开 (Open)

    • 触发条件: 在一个滑动时间窗口内,请求总数达到阈值,且失败率(失败、超时或拒绝)超过预设的失败百分比阈值(默认 50%)。
    • 后果: 熔断器立即打开。后续对该服务的请求将不再执行,直接走降级逻辑 (Fallback),实现快速失败 (Fail-Fast)。
  2. 打开 (Open) → 半开 (Half-Open)

    • 触发条件: 熔断器打开后,会进入一个休眠时间窗口 (sleepWindowInMilliseconds,默认 5000ms)。
    • 后果: 当休眠时间到期后,熔断器自动从 Open 状态切换到 Half-Open 状态。
  3. 半开 (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

image.png