技术面试题汇总(答案) #

共三轮技术面试,每轮约1小时,难度逐轮递增。


第一轮技术面(基础 + 项目 + 算法,1h) #

Java 基础 #

1. JVM 内存划分、对象初始化流程 #

原理分析:

JVM 内存结构(Java 8+):

JVM内存

堆内存 Heap

方法区 Metaspace

虚拟机栈 VM Stack

本地方法栈 Native Stack

程序计数器 PC Register

年轻代 Young

Eden区

Survivor0

Survivor1

老年代 Old

各区域详解:

区域 作用 线程私有 是否GC
堆内存 存放对象实例和数组,GC主要区域 ❌ 共享 ✅ 是
方法区 类信息、常量、静态变量 ❌ 共享 ✅ 是
虚拟机栈 方法调用栈帧,局部变量表 ✅ 私有 ❌ 否
本地方法栈 Native方法调用栈 ✅ 私有 ❌ 否
程序计数器 当前字节码行号指示器 ✅ 私有 ❌ 否

对象初始化流程:

未加载

已加载

new关键字

检查类是否已加载

类加载过程

分配内存

初始化零值

设置对象头Mark Word

执行构造方法

对象初始化完成

加载

链接 验证/准备/解析

初始化

详细步骤:

  1. 类加载检查

    • 检查类是否已被加载、链接、初始化
    • 如果没有,执行类加载过程
  2. 分配内存

    • 指针碰撞(Bump the Pointer):内存规整时
    • 空闲列表(Free List):内存不规整时
    • 本地线程分配缓冲(TLAB):减少竞争
  3. 初始化零值

    • 所有字段设为零值:0/false/null
    • 保证对象可以立即使用
  4. 设置对象头

    • Mark Word:hashCode、GC分代年龄、锁状态
    • 类型指针:指向类元数据
    • 数组长度(如果是数组)
  5. 执行 <init>

    • 执行实例构造方法
    • 按代码逻辑初始化字段
    • 执行构造方法体

最佳实践:

  • 合理设置堆内存大小:-Xms4g -Xmx4g -Xmn2g
  • 避免在构造方法中执行复杂耗时操作
  • 优先使用依赖注入而非直接 new 复杂对象
  • 大对象考虑对象池复用
  • 理解逃逸分析,可能在栈上分配对象

2. HashMap 底层原理、线程安全问题 #

原理分析:

HashMap 数据结构(Java 8):

数组 + 链表 + 红黑树

table数组

[0] → 链表 → 红黑树(链表长度>=8)

[1] → null

[2] → 链表 → 红黑树

...

[n-1] → 链表/红黑树

核心参数:

参数 含义 默认值
initialCapacity 初始容量 16
loadFactor 负载因子 0.75
threshold 扩容阈值 capacity * loadFactor
TREEIFY_THRESHOLD 树化阈值 8
UNTREEIFY_THRESHOLD 链化阈值 6
MIN_TREEIFY_CAPACITY 树化最小容量 64

Hash 寻址:

// 1. 计算 hash 值(扰动函数,高低位异或)
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 2. 取模运算确定数组下标:hash & (length-1)
// 要求 length 必须是 2 的幂
int index = hash & (table.length - 1);

扩容(resize):

hash & oldCap == 0

hash & oldCap != 0

size > threshold

创建新数组 2倍

节点迁移

原位置 or 原位置+原容量

留在原位置

移到新位置

新索引 = 原索引 + 原容量

线程安全问题:

问题 场景 后果
数据丢失 多线程同时 put,哈希碰撞时 某个线程数据被覆盖
死循环 Java 7 扩容时,头插法 链表成环,get 死循环
数据不一致 一个线程正在扩容,另一个线程读写 可能读到不完整的数据

最佳实践:

  • 线程安全方案

    • ConcurrentHashMap(推荐,性能最好)
    • Collections.synchronizedMap(map)(简单,但性能较差)
    • Hashtable(已过时,不推荐)
  • 性能优化

    • 预估容量,避免频繁扩容:new HashMap<>(16, 0.75f)
    • 合理设置负载因子,默认 0.75 平衡时间空间
    • 关键对象重写 hashCode()equals()
  • 注意事项

    • HashMap 允许 key/value 为 null
    • HashMap 无序,LinkedHashMap 保持插入顺序
    • Java 8 后扩容不会死循环,但仍线程不安全

3. ConcurrentHashMap 实现原理 #

原理分析:

Java 7 vs Java 8 对比:

特性 Java 7 Java 8+
数据结构 Segment 分段锁 Node + CAS + synchronized
锁粒度 Segment 级别 Node 级别
并发度 Segment 数量(默认16) 更高,理论上无上限
性能 较低 更高

Java 8+ 实现核心:

ConcurrentHashMap

数组 + 链表 + 红黑树

Node节点

TreeBin节点

并发控制

CAS乐观锁

synchronized悲观锁

volatile保证可见性

关键机制:

  1. volatile 数组保证可见性
transient volatile Node<K,V>[] table;
  1. CAS 无锁化
// 使用 CAS 插入节点
U.compareAndSwapObject(tab, i, null, new Node<K,V>(hash, key, value, null));
  1. synchronized 锁节点
// 链表头节点作为锁
synchronized (f) {
    // 操作链表
}

put 流程:

链表

红黑树

put key value

table为空则初始化

计算索引位置

位置是否为空

CAS插入节点

synchronized锁住节点

节点类型

遍历链表查找

树操作

找到相同key

覆盖value

尾插新节点

链表长度>=8

数组>=64则树化,否则扩容

扩容机制(transfer):

  • 多线程协助扩容:多个线程一起迁移节点
  • ForwardingNode 占位:标记该节点正在迁移
  • 分片迁移:每个线程负责一段区间,步长为 CPU 核数

sizeCtl 含义:

含义
-1 正在初始化
-N 正在扩容,N-1个线程参与
0 还没初始化
>0 下次扩容阈值

最佳实践:

  • 何时使用:多线程并发读写场景
  • 性能调优
    • 合理设置初始容量,减少扩容
    • 默认并发度已足够,不要盲目调整
  • 注意事项
    • 不允许 null key / null value
    • 迭代器是弱一致性的(fail-safe)
    • 统计 size 时计算可能不准确(高并发时)

4. volatile 与 synchronized 区别 #

原理分析:

volatile 特性:

volatile

保证可见性

禁止指令重排序

不保证原子性

可见性实现:

// 伪代码展示内存屏障
public volatile int a = 0;

// 写操作后加 StoreLoad 屏障
a = 1;
StoreLoad(); // 禁止后面的读操作重排序到前面

// 读操作前加 LoadLoad 屏障
LoadLoad(); // 禁止前面的读重排序到后面
int b = a;

CPU 层面实现:

  • Lock 前缀指令
  • 缓存一致性协议(MESI)
  • 刷新缓存到主内存

synchronized 特性:

synchronized

保证原子性

保证可见性

保证有序性

锁升级过程(Java 6+):

单线程访问

多线程交替

多线程竞争

无锁

偏向锁

轻量级锁 CAS自旋

重量级锁 mutex

对比总结:

特性 volatile synchronized
原子性 ❌ 不保证 ✅ 保证
可见性 ✅ 保证 ✅ 保证
有序性 ✅ 禁止重排序 ✅ 保证
锁类型 无锁 监视器锁
性能开销
作用对象 变量 方法/代码块

单例模式对比:

// volatile + 双重检查锁定
class Singleton {
    private static volatile Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

// 为什么需要 volatile?
// 防止指令重排序:对象初始化可能被重排序
// 正常顺序:分配内存 → 初始化 → 赋值
// 重排序后:分配内存 → 赋值 → 初始化
// 其他线程可能读到未初始化完成的对象

最佳实践:

  • volatile 使用场景

    • 状态标记位:volatile boolean running = true;
    • 双重检查锁定单例
    • 读写锁中的读标记
  • synchronized 使用场景

    • 复合操作(count++
    • 保护临界区
    • 对象锁(synchronized(this))
    • 类锁(synchronized(ClassName.class))
  • 优先选择顺序

    • 原子类(AtomicInteger)
    • volatile
    • synchronized
    • ReentrantLock

5. ThreadLocal 用法与应用场景 #

原理分析:

核心结构:

Thread线程

ThreadLocalMap

Entry数组

Entry key弱引用 value强引用

ThreadLocal实例作为key

实际存储对象作为value

关键代码:

class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
}

内存泄漏问题:

Thread

ThreadLocalMap

Entry

key WeakReference

value StrongReference

ThreadLocal实例

问题

ThreadLocal没有强引用时,被GC回收

key变成null,但value还在

如果线程长期存活,value无法回收

内存泄漏

为什么是弱引用?

  • 如果是强引用,即使 ThreadLocal 不再使用,也无法回收
  • 弱引用让 ThreadLocal 可以被回收
  • 但 value 还是强引用,需要手动 remove

最佳实践:

  • 标准用法
private static final ThreadLocal<UserContext> USER_CONTEXT = new ThreadLocal<>();

public void doSomething() {
    try {
        USER_CONTEXT.set(userContext);
        // 业务逻辑
    } finally {
        USER_CONTEXT.remove(); // 必须在 finally 中清理
    }
}
  • 经典应用场景

    • 数据库连接、Session 管理
    • 用户上下文、权限信息
    • 日志 MDC(Mapped Diagnostic Context)
    • SimpleDateFormat 等非线程安全对象复用
  • 注意事项

    • 务必在 finally 中调用 remove()
    • 避免在 ThreadLocal 中存储大对象
    • 线程池场景下要特别注意,线程会复用
    • 父子线程继承使用 InheritableThreadLocal

6. 同步 / 阻塞 / 异步 / 非阻塞区别 #

原理分析:

四个概念的关系:

同步/异步:关注的是消息通知机制(结果怎么通知调用方)
阻塞/非阻塞:关注的是等待结果时的状态(等的时候能不能做别的)

组合效果:

组合 调用方表现 示例
同步阻塞 等结果,不做别的 BIO
同步非阻塞 轮询检查结果,没结果时做别的 NIO selector 轮询
异步阻塞 等回调通知 Future.get()
异步非阻塞 发请求就返回,结果通过回调通知 CompletableFuture / Reactor

同步 vs 异步:

// 同步:等待返回结果
String result = doSomething();
System.out.println(result);

// 异步:不等待,回调通知
doSomethingAsync(result -> {
    System.out.println(result);
});

阻塞 vs 非阻塞:

// 阻塞:没数据时线程挂起
int data = inputStream.read(); // 线程阻塞在这里

// 非阻塞:没数据时立即返回,继续做别的
if (selector.selectNow() > 0) {
    // 有数据才处理
}

IO 模型演进:

模型 特点 Java API
BIO 同步阻塞 InputStream, ServerSocket
NIO 同步非阻塞 Selector, Channel, Buffer
AIO 异步非阻塞 AsynchronousSocketChannel
Netty 事件驱动 基于 NIO 封装

最佳实践:

  • 选择建议

    • 连接数 < 1000:BIO 简单够用
    • 连接数 > 1000:NIO/Netty
    • 高并发异步场景:Reactor/RxJava
  • 异步编程工具

    • Java 8+:CompletableFuture
    • Spring:@Async
    • 响应式:WebFlux, Reactor

7. 反射机制与应用场景 #

原理分析:

反射核心类:

Class类对象

Field字段

Method方法

Constructor构造器

获取Class方式

类名.class

对象.getClass

Class.forName

基础用法示例:

// 1. 获取 Class 对象
Class<?> clazz = User.class;
// 或
Class<?> clazz = user.getClass();
// 或
Class<?> clazz = Class.forName("com.example.User");

// 2. 反射创建对象
User user = clazz.newInstance(); // 调用无参构造
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
User user = (User) constructor.newInstance("Alice", 25);

// 3. 反射调用方法
Method method = clazz.getMethod("setName", String.class);
method.setAccessible(true); // 强制访问私有方法
method.invoke(user, "Bob");

// 4. 反射操作字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
String name = (String) field.get(user);
field.set(user, "Charlie");

// 5. 获取注解
Annotation[] annotations = clazz.getAnnotations();

原理:JVM 类元数据

Class 文件 → 类加载 → 方法区 → Class 对象 → 反射访问

最佳实践:

  • 经典应用场景

    • Spring IoC 容器:通过反射实例化 Bean
    • ORM 框架(MyBatis/Hibernate):反射填充对象
    • 注解处理:通过反射读取注解
    • JDBC 加载驱动:Class.forName("com.mysql.Driver")
  • 性能考虑

    • 反射比直接调用慢 10-100 倍
    • 缓存反射结果(如缓存 Method 对象)
    • setAccessible(true) 关闭安全检查提高速度
    • 高性能场景考虑字节码生成(ASM、CGLIB)
  • 安全注意事项

    • 注意 setAccessible(true) 的安全风险
    • 不要反射调用 JDK 内部 API
    • 模块化下(Java 9+)访问受限

8. Java 8 Lambda、Stream、Optional 新特性 #

原理分析:

Lambda 表达式:

// 函数式接口定义
@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

// Lambda 写法
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;

// 等价匿名内部类
Calculator add = new Calculator() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
};

Lambda 实现原理:

  • 编译时 invokedynamic 指令
  • 运行时动态生成内部类
  • 可能使用 CGLIB/ASM

Stream API:

源 Source

中间操作 Intermediate

filter

map

sorted

终端操作 Terminal

collect

forEach

count

Stream 示例:

List<Integer> result = list.stream()
    .filter(n -> n > 0)          // 中间操作
    .map(n -> n * 2)             // 中间操作
    .sorted()                    // 中间操作
    .collect(Collectors.toList()); // 终端操作

Optional 优雅处理空值:

// 传统写法
String name = null;
if (user != null) {
    if (user.getAddress() != null) {
        name = user.getAddress().getCity();
    }
}

// Optional 写法
String name = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("Unknown");

最佳实践:

  • Lambda 使用建议

    • 保持简短,不超过 3 行
    • 避免在 Lambda 中修改外部变量(闭包)
    • 优先使用方法引用:User::getName
  • Stream 使用建议

    • 避免在中间操作中进行 IO/网络操作
    • 大数据量时考虑 parallelStream()
    • 合理使用基本类型流:IntStream, LongStream
  • Optional 使用建议

    • 不要用 Optional 作为字段或方法参数
    • 不要用 Optional 做集合元素
    • 只用作返回值,表示可能为空的结果
    • 不要用 if (opt.isPresent()),那和空检查没区别

Spring 框架 #

9. Spring IoC、AOP 原理与使用场景 #

原理分析:

IoC(控制反转)/ DI(依赖注入):

IoC容器

BeanFactory

ApplicationContext

Bean生命周期

事件机制

国际化

实例化 Instantiation

属性赋值 Populate

初始化 Initialization

销毁 Destruction

Bean 生命周期:

// 1. 实例化(创建 Bean 实例)
Bean bean = new Bean();

// 2. 属性赋值(DI 注入)
bean.setXxx(xxx);

// 3. Aware 回调
if (bean instanceof BeanNameAware) ((BeanNameAware)bean).setBeanName(name);
if (bean instanceof ApplicationContextAware) ((ApplicationContextAware)bean).setApplicationContext(ctx);

// 4. BeanPostProcessor 前置处理
bean = beanPostProcessor.postProcessBeforeInitialization(bean, name);

// 5. InitializingBean 接口
if (bean instanceof InitializingBean) ((InitializingBean)bean).afterPropertiesSet();

// 6. init-method
invokeCustomInitMethod(bean);

// 7. BeanPostProcessor 后置处理
bean = beanPostProcessor.postProcessAfterInitialization(bean, name);

// 8. Bean 就绪,可以使用

// 9. 销毁
if (bean instanceof DisposableBean) ((DisposableBean)bean).destroy();
invokeCustomDestroyMethod(bean);

AOP(面向切面编程):

JoinPoint连接点

Pointcut切点

Advice通知

Before前置

AfterReturning后置

AfterThrowing异常

After最终

Around环绕

Weaving织入

Aspect切面

AOP 实现原理:

// JDK 动态代理(面向接口)
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) {
        // 前置通知
        // 目标方法调用
        // 后置/异常/最终通知
    }
}

// CGLIB 动态代理(继承)
public class CglibAopProxy implements AopProxy {
    // 生成目标类的子类
    // 重写方法,织入切面逻辑
}

最佳实践:

  • IoC 使用场景

    • 业务服务组件注入
    • DAO/Repository 注入
    • 配置参数注入(@Value)
    • 第三方组件集成
  • AOP 使用场景

    • 日志记录
    • 事务管理(@Transactional)
    • 性能监控
    • 权限检查
    • 异常处理
  • 注意事项

    • AOP 只对 public 方法生效(CGLIB 可以 protected)
    • 内部调用(this.method())不会触发 AOP
    • 事务注解失效的常见坑就是内部调用

10. Spring Boot 自动配置原理 #

原理分析:

核心注解:

@SpringBootApplication

@SpringBootConfiguration

@EnableAutoConfiguration(关键!)

@ComponentScan

@Import(AutoConfigurationImportSelector.class)

从 META-INF/spring.factories 加载自动配置类

自动配置流程:

@SpringBootApplication

@EnableAutoConfiguration

AutoConfigurationImportSelector

读取 spring.factories

筛选条件注解

@ConditionalOnClass

@ConditionalOnBean

@ConditionalOnMissingBean

按需加载配置类

创建 Bean

spring.factories 文件(Spring Boot 2.x):

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

Spring Boot 3.x 新方式:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

常用条件注解:

注解 作用
@ConditionalOnClass 类路径下存在指定类时生效
@ConditionalOnMissingClass 类路径下不存在指定类时生效
@ConditionalOnBean 容器中存在指定 Bean 时生效
@ConditionalOnMissingBean 容器中不存在指定 Bean 时生效
@ConditionalOnProperty 配置文件中存在指定属性时生效
@ConditionalOnWebApplication Web 应用时生效

最佳实践:

  • 自定义 Starter 步骤

    1. 创建 Maven 模块,命名为 xxx-spring-boot-starter
    2. 创建自动配置类
    3. 编写 spring.factories(或新的 imports 文件)
    4. 编写配置属性类 @ConfigurationProperties
  • 配置优先级(从高到低)

    1. 命令行参数
    2. application-{profile}.properties
    3. application.properties
    4. @PropertySource
    5. 默认配置

11. Spring Boot 同步非阻塞实现方式 #

原理分析:

Spring WebFlux 响应式框架:

Spring WebFlux

Reactor Netty

Reactor Mono/Flux

事件循环 EventLoop

Servlet Stack

Spring MVC

Reactive Stack

对比 Spring MVC vs WebFlux:

特性 Spring MVC Spring WebFlux
编程模型 命令式 响应式
线程模型 Servlet 容器线程池 Netty EventLoop
阻塞 同步阻塞 异步非阻塞
背压支持
API RestController RestController / WebFlux.fn

WebFlux 代码示例:

// RestController 写法
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public Mono<User> getUser(@PathVariable Long id) {
        return userService.findById(id); // 返回 Mono,异步
    }
    
    @GetMapping("/users")
    public Flux<User> listUsers() {
        return userService.findAll(); // 返回 Flux,流式
    }
    
    @PostMapping("/users")
    public Mono<User> createUser(@RequestBody Mono<User> user) {
        return user.flatMap(userService::save);
    }
}

背压(Backpressure):

生产者 → 快于 → 消费者 → 背压机制 → 生产者减速

最佳实践:

  • WebFlux 适用场景

    • 高并发 IO 密集型
    • 需要背压控制
    • 响应式全链路
  • 不适用场景

    • CPU 密集型
    • 大量阻塞操作
    • 团队不熟悉响应式
  • 实现方式选择

    • WebFlux 注解方式(类似 MVC,迁移成本低)
    • WebFlux.fn 函数式方式(更灵活,更复杂)
    • 可以 Spring MVC 和 WebFlux 混用

12. 全局异常处理、拦截器、定时任务实现 #

原理分析:

全局异常处理 @ControllerAdvice:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("500", "系统异常");
    }
}

拦截器 HandlerInterceptor:

@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) {
        // 前置处理,返回 true 继续,false 拦截
        String token = request.getHeader("token");
        return validateToken(token);
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Object handler, 
                          ModelAndView modelAndView) {
        // 后置处理
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) {
        // 完成后处理
    }
}

// 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private AuthInterceptor authInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

定时任务 @Scheduled:

@EnableScheduling
@SpringBootApplication
public class App {}

@Component
public class ScheduledTasks {
    
    // cron 表达式
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
    public void scheduledTask1() {
        log.info("定时任务1执行");
    }
    
    // 固定间隔,上次执行结束后开始计时
    @Scheduled(fixedDelay = 1000)
    public void scheduledTask2() {
        log.info("固定延迟任务执行");
    }
    
    // 固定频率,上次执行开始后开始计时
    @Scheduled(fixedRate = 1000)
    public void scheduledTask3() {
        log.info("固定频率任务执行");
    }
}

// 异步定时任务
@EnableAsync
@SpringBootApplication
public class App {}

@Component
public class AsyncScheduledTasks {
    
    @Async
    @Scheduled(fixedDelay = 1000)
    public void asyncTask() {
        // 异步执行,不阻塞调度线程
    }
}

最佳实践:

  • 异常处理最佳实践

    • 区分业务异常和系统异常
    • 异常码标准化
    • 记录详细日志(堆栈信息)
    • 对用户友好的错误信息
  • 拦截器最佳实践

    • 优先考虑 Spring AOP 还是 Interceptor
    • 只在 Interceptor 中做简单逻辑
    • 复杂逻辑下沉到 Service
  • 定时任务最佳实践

    • 幂等性设计:重复执行不影响结果
    • 异常处理:捕获异常,避免影响后续执行
    • 监控告警:记录执行时间、结果
    • 分布式场景:考虑 xx-job/elastic-job

项目经验 #

13. 项目架构、前后端分离、API 设计 #

原理分析:

典型分层架构:

前端 Vue/React

网关 Gateway

Controller控制层

Service业务层

DAO数据层

数据库 MySQL

缓存 Redis

消息队列 MQ

前后端分离架构:

JSON/REST

前端应用
Vue/React/Uniapp

后端 API
Spring Boot + RESTful

数据存储
MySQL + Redis + MQ

RESTful API 设计原则:

GET    /users          获取用户列表
GET    /users/{id}     获取单个用户
POST   /users          创建用户
PUT    /users/{id}     更新用户(完整)
PATCH  /users/{id}     更新用户(部分)
DELETE /users/{id}     删除用户

标准响应格式:

{
    "code": 200,
    "message": "success",
    "data": {
        "id": 1,
        "name": "Alice"
    },
    "timestamp": 1620000000000
}

最佳实践:

  • 架构设计原则

    • 高内聚低耦合
    • 分层清晰
    • 易于扩展
    • 考虑故障隔离
  • API 设计最佳实践

    • 资源命名用名词复数
    • 版本化:/api/v1/users
    • 使用标准 HTTP 状态码
    • 统一响应格式
    • 接口文档化(Swagger/OpenAPI)

14. 项目难点、性能优化、解决方案 #

原理分析:

常见性能问题:

性能问题

慢 SQL

接口响应慢

并发能力差

数据库压力大

索引优化

缓存

异步

线程池调优

读写分离

分库分表

性能优化全链路:

层级 优化手段
前端 CDN、缓存、压缩、懒加载
网关 限流、降级、缓存
应用 缓存、异步、池化、JVM 调优
数据库 索引、SQL 优化、读写分离、分库分表
架构 微服务、消息队列削峰

最佳实践:

  • 优化流程

    1. 压测定位瓶颈(JMeter/Gatling)
    2. 监控分析(Prometheus/Grafana)
    3. 针对性优化
    4. 回归验证
    5. 持续监控
  • 常见解决方案

    • 热点数据 → Redis 缓存
    • 耗时操作 → 异步 + 消息队列
    • 数据库压力 → 读写分离
    • 单表数据量大 → 分库分表
    • 突发流量 → 限流 + 熔断

15. Session 跨域、ThreadLocal 实际使用 #

原理分析:

跨域问题与解决方案:

浏览器同源策略限制:协议、域名、端口三者必须一致

常见解决方案对比:

方案 说明 适用场景
CORS 后端配置响应头 前后端分离,推荐
JSONP 利用 script 标签 仅 GET,已过时
Nginx 反向代理 同域下转发 有 Nginx 环境
后端重定向 服务端跳转 特定场景

CORS 配置示例:

@Configuration
public class CorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

Session 共享方案:

// Spring Session + Redis
@EnableRedisHttpSession
public class SessionConfig {}

ThreadLocal 实际使用:

// 用户上下文传递
public class UserContextHolder {
    private static final ThreadLocal<UserContext> HOLDER = new ThreadLocal<>();
    
    public static void set(UserContext context) {
        HOLDER.set(context);
    }
    
    public static UserContext get() {
        return HOLDER.get();
    }
    
    public static void clear() {
        HOLDER.remove();
    }
}

// 使用
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) {
        String token = request.getHeader("token");
        UserContext context = parseToken(token);
        UserContextHolder.set(context); // 设置
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) {
        UserContextHolder.clear(); // 清理,防止内存泄漏
    }
}

@Service
public class UserService {
    
    public void doSomething() {
        UserContext context = UserContextHolder.get(); // 获取
        // 使用上下文
    }
}

最佳实践:

  • 跨域安全

    • 生产环境不要用 * 允许所有来源
    • 明确配置 allowedOrigins
    • 考虑 CSRF 防护
  • ThreadLocal 正确使用

    • 务必在 finally/afterCompletion 中 remove
    • 线程池场景特别注意线程复用问题
    • 父子线程传递使用 InheritableThreadLocal

第二轮技术面(深度 + 系统 + 算法,1h) #

JVM 与并发 #

1. JVM 调优、GC 算法、GC Root、内存泄漏排查 #

原理分析:

GC 算法对比:

算法 区域 优点 缺点
标记-清除 老年代 简单 内存碎片
标记-整理 老年代 无碎片 移动对象,慢
复制 年轻代 无碎片,简单 需要额外空间
分代收集 全部 综合以上优点 -

垃圾收集器:

Serial 串行

Parallel 并行

CMS 并发标记

G1 Garbage-First

ZGC 新一代

G1 收集器特点:

  • Region 分区,不固定年轻代老年代
  • 优先回收垃圾多的 Region
  • 可预测停顿时间模型
  • -XX:MaxGCPauseMillis=200

GC Roots 判定:

GC Roots

虚拟机栈中引用

方法区静态变量引用

方法区常量引用

本地方法栈 JNI 引用

活跃线程

同步锁对象

从 GC Roots 开始向下搜索,可达对象存活,不可达对象回收

内存泄漏排查:

OOM发生

获取堆转储

MAT分析

大对象/泄漏对象

查看引用链

定位泄漏点

修复

最佳实践:

  • JVM 参数调优
# G1 收集器
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

# ZGC 收集器(JDK 15+)
-Xms8g -Xmx8g
-XX:+UseZGC

# 打印 GC 日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags
  • 排查工具

    • jps/jstat/jmap/jstack/jcmd
    • VisualVM/Arthas
    • MAT/JProfiler
  • 避免内存泄漏

    • ThreadLocal 记得 remove
    • 静态集合注意清理
    • 资源用完关闭(IO/连接)

2. 线程池原理、自定义线程池、拒绝策略 #

原理分析:

ThreadPoolExecutor 核心参数:

public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 空闲线程存活时间
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue, // 工作队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
)

工作流程:

提交任务

核心线程满?

创建核心线程

队列满?

入队等待

最大线程满?

创建非核心线程

执行拒绝策略

四大拒绝策略:

策略 行为
AbortPolicy 抛出 RejectedExecutionException(默认)
CallerRunsPolicy 调用者线程自己执行
DiscardPolicy 直接丢弃,不抛异常
DiscardOldestPolicy 丢弃队列中最老的任务

自定义线程池示例:

@Configuration
public class ThreadPoolConfig {
    
    @Bean
    public ThreadPoolExecutor businessThreadPool() {
        return new ThreadPoolExecutor(
            8,  // 核心线程数:CPU * 2(IO 密集型)
            16, // 最大线程数
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000), // 有界队列,防止 OOM
            new ThreadFactoryBuilder().setNameFormat("business-pool-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
    }
}

最佳实践:

  • 不推荐 Executors 创建的原因

    • FixedThreadPool:无界队列 → OOM
    • CachedThreadPool:无界线程 → OOM
    • SingleThreadPool:无界队列 → OOM
  • 合理配置线程池

    • CPU 密集:N + 1
    • IO 密集:2 * N
    • N = Runtime.getRuntime().availableProcessors()
  • 监控线程池

    • getActiveCount()
    • getPoolSize()
    • getQueue().size()

3. 锁升级、CAS、AQS 原理 #

原理分析:

synchronized 锁升级:

单线程访问

多线程交替 CAS

多线程竞争挂起

无锁

偏向锁

轻量级锁

重量级锁

各阶段说明:

锁状态 Mark Word 说明
无锁 hashCode、分代年龄 对象刚创建
偏向锁 线程ID、epoch 单线程进入
轻量级锁 栈中锁记录指针 CAS 自旋
重量级锁 互斥量指针 内核态

CAS(Compare-And-Swap):

// 三个操作数:内存值 V、预期值 A、新值 B
// if V == A then V = B
public final boolean compareAndSwapInt(Object o, long offset, int expected, int x)

ABA 问题及解决:

线程1:A → B
线程2:B → A
线程1CAS检查:还是 A,认为没变化,但实际被修改过了

解决:版本号 AtomicStampedReference

AQS(AbstractQueuedSynchronizer):

AQS

state状态

Node双向队列

head首节点

tail尾节点

共享/独占模式

ReentrantLock

CountDownLatch

Semaphore

AQS 核心方法:

  • tryAcquire(int):独占方式获取
  • tryRelease(int):独占方式释放
  • tryAcquireShared(int):共享方式获取
  • tryReleaseShared(int):共享方式释放

最佳实践:

  • 锁选择

    • 简单场景:synchronized
    • 高级功能:ReentrantLock(可中断、公平锁、多条件)
    • 原子操作:Atomic 类
  • 避免死锁

    • 固定加锁顺序
    • 锁超时
    • 死锁检测

数据库 #

4. MySQL 索引、索引失效、慢查询优化 #

原理分析:

B+Tree 索引结构:

根节点 (Root)
30 | 70

内部节点
10 | 20

内部节点
40 | 50 | 60

内部节点
80 | 90

🍃 10 | 15
data ptrs

🍃 20 | 25
data ptrs

🍃 30 | 35
data ptrs

🍃 40 | 45
data ptrs

🍃 50 | 55
data ptrs

🍃 60 | 65
data ptrs

🍃 70 | 75
data ptrs

🍃 80 | 85
data ptrs

🍃 90 | 95
data ptrs

查找 SELECT * WHERE id = 45 根节点 30|70 → 走 30~70 分支 → 内部节点 40|50|60 → 走 40~50 分支 → 叶子节点找到 45 对应的数据行指针

范围查询 WHERE id BETWEEN 30 AND 60 先定位到叶子 🍃30,再沿叶子双向链表顺序扫描到 🍃60,无需回退到上层

B+Tree 优势:

  • 稳定查询:都要查到叶子节点
  • 范围查询高效:叶子节点有链表
  • 磁盘读写少:节点可以存更多索引

索引类型:

类型 说明
聚簇索引 主键索引,数据和索引在一起
二级索引 普通索引,存主键值
联合索引 多列索引,最左前缀匹配
覆盖索引 查询列都在索引中,不用回表

索引失效常见情况:

-- 1. 索引列上使用函数或表达式
SELECT * FROM user WHERE YEAR(create_time) = 2024; -- 失效

-- 2. 隐式类型转换
SELECT * FROM user WHERE phone = 13800000000; -- phone是varchar,失效

-- 3. LIKE 以通配符开头
SELECT * FROM user WHERE name LIKE '%张'; -- 失效

-- 4. OR 条件中有未索引列
SELECT * FROM user WHERE name = '张三' OR age = 20; -- age没索引,失效

-- 5. 不等于、NOT
SELECT * FROM user WHERE name != '张三'; -- 失效

最佳实践:

  • 索引设计原则

    • 区分度高的列优先
    • 联合索引遵循最左前缀
    • 避免过多索引(占用空间、影响写入)
    • 小表不需要索引(直接全表扫描)
  • 慢查询优化步骤

    1. 开启慢查询日志
    2. EXPLAIN 分析执行计划
    3. 检查 type、key、rows、Extra
    4. 针对性优化
  • SQL 优化技巧

    • SELECT 具体字段,不要 SELECT *
    • 避免大事务,控制事务大小
    • 合理使用 LIMIT,避免深分页

5. 事务隔离级别、MVCC、死锁处理 #

原理分析:

事务 ACID:

A:Atomicity 原子性
C:Consistency 一致性
I:Isolation 隔离性
D:Durability 持久性

四个隔离级别:

隔离级别 脏读 不可重复读 幻读
Read Uncommitted
Read Committed
Repeatable Read
Serializable

MySQL 默认:Repeatable Read

MVCC(多版本并发控制):

ReadView

当前活跃事务

trx_ids事务ID列表

min_trx_id最小

max_trx_id最大

UndoLog版本链

回滚指针ROLL_PTR

事务IDTRX_ID

可见性判断:

1. trx_id < min_trx_id → 可见(已提交)
2. trx_id > max_trx_id → 不可见(未开始)
3. min_trx_id <= trx_id <= max_trx_id:
   - trx_id 在活跃列表 → 不可见(未提交)
   - 不在活跃列表 → 可见(已提交)

死锁处理:

事务1持有锁A

等待锁B

事务2持有锁B

等待锁A

循环等待

最佳实践:

  • 避免死锁

    • 固定加锁顺序
    • 降低锁粒度
    • 大事务拆小
    • 合理设置超时
  • 死锁排查

    • SHOW ENGINE INNODB STATUS
    • 查看 LATEST DETECTED DEADLOCK

6. 分库分表、读写分离思路 #

原理分析:

为什么要分库分表:

问题 影响
单表数据量大 查询慢
写入并发高 数据库压力大
单库存储容量 硬件限制

分库分表策略:

垂直拆分

垂直分库

垂直分表

水平拆分

水平分库

水平分表

分片键选择:

// 1. Hash 分片:均匀分布,但范围查询难
userId % 4

// 2. Range 分片:范围查询方便,但可能数据倾斜
userId < 1000 → db0
1000 <= userId < 2000 → db1

// 3. Location 分片:按地域分片,数据可能不均匀

读写分离:

INSERT / UPDATE / DELETE

SELECT

🖥️ 应用层

⚙️ 读写分离中间件
ShardingSphere / MyCat / MyBatis-Plus

SQL 类型?

🔴 主库 Master
负责写操作

📦 binlog 异步/半同步复制

🔀 负载均衡
轮询 / 加权随机

🟢 从库 Slave1
负责读操作

🟢 从库 Slave2
负责读操作

延迟问题: 主库写入后 binlog 同步到从库存在延迟(通常 ms~s 级),刚写入的数据立刻读从库可能读不到

解决方案: ①关键业务写后强制走主库读 ②半同步复制(等至少一个 Slave 确认) ③设置延迟容忍度

中间件方案:

方案 说明
Sharding-JDBC 客户端分片,轻量
ShardingSphere-Proxy 服务端代理
MyCat 服务端代理

最佳实践:

  • 分库分表原则

    • 能不分就不分
    • 能少分就少分
    • 预估数据量,提前规划
  • 常见问题解决

    • 跨库 join:应用层处理,数据冗余
    • 分布式事务:Seata/最终一致性
    • 跨库分页:全局表/二次查询

系统设计 #

7. 高并发秒杀/抢票:库存扣减、防超卖、流量削峰 #

原理分析:

系统架构:

用户请求

CDN静态资源

Nginx限流

Redis缓存预热

MQ异步处理

DB最终一致性

库存扣减方案:

-- 方案1:数据库乐观锁
UPDATE product SET stock = stock - 1 
WHERE id = 1 AND stock > 0;

-- 方案2:Redis 原子操作
DECR stock

-- 方案3:Lua 脚本保证原子性
if redis.call('GET', KEYS[1]) >= ARGV[1] then
    return redis.call('DECRBY', KEYS[1], ARGV[1])
else
    return -1
end

防超卖措施:

层级 方案
前端 按钮置灰、倒计时
网关 限流、黑名单
应用 一人一单、预扣库存
数据 乐观锁、Redis 原子

最佳实践:

  • 流量削峰

    • 答题验证:过滤恶意请求
    • 分批放号:避免瞬间冲击
    • 排队系统:有序处理
  • 数据一致性

    • Redis 预扣 + DB 最终确认
    • 消息队列异步下单
    • 定时任务对账补偿

原理分析:

典型架构:

车载设备

MQTT网关

Kafka消息队列

Flink实时计算

告警通知

时序数据库 TDengine/InfluxDB

数据湖

Kafka 选型要点:

分区数:吞吐量考虑
副本数:高可用考虑
ISR:同步副本

Flink 关键概念:

Checkpoint:状态持久化
Exactly Once:语义保证
Watermark:事件时间处理
Window:窗口计算

最佳实践:

  • 数据采集

    • 边缘计算:本地预处理,减少上传
    • 压缩传输:减少带宽
    • 断点续传:网络波动处理
  • 流处理

    • 状态后端选择(RocksDB)
    • Checkpoint 配置
    • Exactly Once 保证

9. 微服务:服务发现、熔断降级、分布式事务 #

原理分析:

服务发现:

服务提供者 → 注册到注册中心
服务消费者 ← 从注册中心发现
组件 说明
Eureka Netflix,已停更
Nacos 阿里,推荐
Consul HashiCorp

熔断降级:

探测成功

探测失败

正常状态 Closed

失败次数达标

熔断开启 Open

等待时间窗口

半熔断 Half-Open

组件 说明
Hystrix Netflix,已停更
Sentinel 阿里,推荐
Resilience4j 轻量

分布式事务:

2PC两阶段提交

强一致性

性能差

TCC事务补偿

性能好

实现复杂

Seata框架

AT模式自动补偿

Saga长事务

事件驱动

本地消息表

最终一致

最佳实践:

  • 微服务拆分原则

    • 按业务域拆分
    • 独立部署、独立扩展
    • 数据隔离
  • 分布式事务方案选择

    • 强一致性要求高:Seata AT
    • 性能要求高:TCC/本地消息表
    • 跨公司服务:Saga

算法 #

10. 矩阵旋转、最小窗口子串、LRU 缓存、接雨水、二叉树最大宽度 #

原理分析:

矩阵旋转:

// 顺时针旋转 90 度
// 1. 对角线交换
// 2. 左右翻转
public void rotate(int[][] matrix) {
    int n = matrix.length;
    // 对角线翻转
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
    // 左右翻转
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n / 2; j++) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[i][n - 1 - j];
            matrix[i][n - 1 - j] = temp;
        }
    }
}

最小窗口子串:

// 滑动窗口
public String minWindow(String s, String t) {
    Map<Character, Integer> need = new HashMap<>();
    Map<Character, Integer> window = new HashMap<>();
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }
    
    int left = 0, right = 0, valid = 0;
    int start = 0, len = Integer.MAX_VALUE;
    
    while (right < s.length()) {
        char c = s.charAt(right);
        right++;
        if (need.containsKey(c)) {
            window.put(c, window.getOrDefault(c, 0) + 1);
            if (window.get(c).equals(need.get(c))) {
                valid++;
            }
        }
        
        while (valid == need.size()) {
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            char d = s.charAt(left);
            left++;
            if (need.containsKey(d)) {
                if (window.get(d).equals(need.get(d))) {
                    valid--;
                }
                window.put(d, window.get(d) - 1);
            }
        }
    }
    return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}

LRU 缓存:

// 双向链表 + HashMap
class LRUCache {
    
    static class Node {
        int key, value;
        Node prev, next;
        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    private Map<Integer, Node> map;
    private Node head, tail;
    private int capacity;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        remove(node);
        addToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            remove(node);
            addToHead(node);
        } else {
            if (map.size() >= capacity) {
                Node last = tail.prev;
                remove(last);
                map.remove(last.key);
            }
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            addToHead(newNode);
        }
    }
    
    private void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private void addToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }
}

接雨水:

// 双指针
public int trap(int[] height) {
    int left = 0, right = height.length - 1;
    int leftMax = 0, rightMax = 0;
    int res = 0;
    
    while (left < right) {
        if (height[left] < height[right]) {
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                res += leftMax - height[left];
            }
            left++;
        } else {
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                res += rightMax - height[right];
            }
            right--;
        }
    }
    return res;
}

二叉树最大宽度:

// BFS + 索引
public int widthOfBinaryTree(TreeNode root) {
    if (root == null) return 0;
    
    Queue<Pair<TreeNode, Integer>> queue = new LinkedList<>();
    queue.offer(new Pair<>(root, 0));
    int maxWidth = 0;
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        int first = queue.peek().getValue();
        
        for (int i = 0; i < size; i++) {
            Pair<TreeNode, Integer> pair = queue.poll();
            TreeNode node = pair.getKey();
            int index = pair.getValue();
            
            if (i == size - 1) {
                maxWidth = Math.max(maxWidth, index - first + 1);
            }
            
            if (node.left != null) {
                queue.offer(new Pair<>(node.left, 2 * index));
            }
            if (node.right != null) {
                queue.offer(new Pair<>(node.right, 2 * index + 1));
            }
        }
    }
    return maxWidth;
}

最佳实践:

  • 算法面试准备

    • LeetCode Hot 100
    • 高频面试题
    • 分类刷题(数组、链表、树、动态规划)
  • 面试回答要点

    • 先讲思路
    • 再写代码
    • 分析复杂度
    • 优化方案

第三轮技术面(架构 + 综合 + 英文,1h) #

架构设计 #

1. OTA 升级包分发架构设计(P2P、CDN、断点续传) #

原理分析:

整体架构:

升级包源站

CDN分发

P2P节点

车辆终端

断点续传

增量升级

核心功能:

功能 方案
CDN 加速 阿里云/腾讯云 CDN
P2P 分发 WebRTC 数据通道
断点续传 HTTP Range 请求
增量升级 bsdiff/rsync 算法
版本管理 灰度发布、回滚

断点续传实现:

GET /ota/update-v2.0.bin HTTP/1.1
Host: cdn.example.com
Range: bytes=1024-2048  // 下载指定范围

HTTP/1.1 206 Partial Content
Content-Length: 1024
Content-Range: bytes 1024-2048/1048576
// 二进制数据...

最佳实践:

  • 设计原则

    • 高可用:多 CDN 主备
    • 低成本:P2P 减轻源站压力
    • 安全性:升级包签名校验
  • 车载特殊考虑

    • 网络不稳定:断点续传必备
    • 升级安全:签名 + 回滚方案
    • 带宽控制:不影响车机其他功能

2. 全球车联网传感器数据处理(MQTT、流处理、时序库) #

原理分析:

全球架构:

亚太区车辆

亚太区 MQTT 接入

欧洲区车辆

欧洲区 MQTT 接入

美洲区车辆

美洲区 MQTT 接入

Kafka集群

Flink流处理

时序数据库 TDengine

实时告警

MQTT 设计要点:

QoS 说明 适用场景
0 最多一次 非关键数据
1 至少一次 普通数据
2 恰好一次 关键数据

Topic 设计:

vehicles/{vin}/sensors/{sensorType}
vehicles/VIN001/sensors/gps
vehicles/VIN001/sensors/speed

时序数据库选型:

数据库 特点
InfluxDB 生态好,单机能力强
TDengine 国产,性能好
TimescaleDB 基于 PostgreSQL
Prometheus 监控场景

最佳实践:

  • 全球部署考虑

    • 就近接入:减少延迟
    • 数据同步:跨区域一致性
    • 合规:数据本地化
  • 海量数据处理

    • 边缘计算:本地预处理
    • 采样降准:历史数据压缩
    • 冷热分离:近期热数据,远期冷存储

3. 高并发预订/下单系统:订单、支付、库存、分布式事务 #

原理分析:

系统架构:

用户请求

订单服务

库存服务

支付服务

库存DB

订单DB

消息队列

异步通知

核心流程:

创建订单

预扣库存

库存是否足够

返回失败

等待支付

支付超时?

回滚库存

支付成功?

确认扣库存

分布式事务方案:

// Seata AT 模式
@GlobalTransactional
public void createOrder(Order order) {
    // 1. 创建订单
    orderService.create(order);
    // 2. 扣库存
    stockService.deduct(order.getProductId(), order.getCount());
    // 3. 扣余额
    accountService.deduct(order.getUserId(), order.getAmount());
}

最佳实践:

  • 性能优化

    • Redis 预扣库存
    • 订单号生成:雪花算法/ID 生成器
    • 热点商品:库存分片
  • 一致性保证

    • 最终一致性优先
    • 定时任务补偿
    • 操作日志可追溯

性能与选型 #

4. 全链路性能调优(JVM、GC、DB、中间件) #

原理分析:

全链路优化图:

前端
资源优化
压缩

CDN
缓存
预热

Nginx
限流
缓存

应用
缓存
异步

数据
索引
SQL优

各层级优化要点:

层级 手段
JVM 堆大小、GC 选择、参数调优
GC G1/ZGC、日志分析、停顿优化
数据库 索引、SQL、连接池、读写分离
中间件 Redis、MQ、连接池配置
应用 缓存、异步、池化、业务优化

最佳实践:

  • 调优步骤

    1. 压测基准线
    2. 监控找瓶颈
    3. 针对性优化
    4. 回归验证
    5. 持续监控
  • 工具推荐

    • 压测:JMeter/Gatling
    • APM:SkyWalking/Pinpoint
    • 监控:Prometheus+Grafana

5. 技术选型思路与对比 #

原理分析:

选型维度:

技术选型

业务场景

团队熟悉度

生态成熟度

性能

成本

社区支持

常见技术对比:

类别 选项1 选项2 选项3
缓存 Redis Memcached Caffeine
消息队列 Kafka RocketMQ RabbitMQ
服务注册 Nacos Eureka Consul
熔断降级 Sentinel Resilience4j Hystrix
分库分表 ShardingSphere MyCat 客户端分片

最佳实践:

  • 选型原则

    • 成熟优先,不要追新
    • 团队熟悉,降低学习成本
    • 考虑未来 3-5 年发展
    • 不盲目追求大而全
  • 决策流程

    • 需求分析
    • 技术调研
    • POC 验证
    • 决策选型

综合能力 #

6. 技术债务、重构、团队协作 #

原理分析:

技术债务来源:

来源 例子
时间压力 为了赶进度写烂代码
缺乏规范 没有代码规范,风格不一
人员流动 交接不清,接手人乱改
知识不足 不了解最佳实践

重构策略:

识别债务

优先级排序

制定计划

小步快跑

测试保护

持续改进

最佳实践:

  • 如何推进重构

    • 数据说话:展示技术债务影响
    • 小步快跑:不要大动干戈
    • 测试先行:先加测试再重构
    • 定期安排:预留重构时间
  • 团队协作

    • Code Review
    • 技术分享
    • 文档积累
    • 结对编程

7. 分布式 ID、限流算法等场景题 #

原理分析:

分布式 ID 生成方案:

方案 优点 缺点
UUID 简单 无序、太长
数据库自增 简单 单点、性能差
Redis 生成 高性能 依赖 Redis
雪花算法 趋势递增、高性能 时钟回拨问题
美团 Leaf 综合 复杂

雪花算法(Snowflake):

最高位0表示正数

1位 符号位(始终0)

41位 时间戳(毫秒)

5位 数据中心ID

5位 机器ID

1位 预留

12位 序列号

限流算法:

固定窗口计数器

简单有临界点问题

滑动窗口

精确但复杂

漏桶算法

匀速处理

令牌桶

允许突发

令牌桶实现(Guava RateLimiter):

RateLimiter rateLimiter = RateLimiter.create(10); // 每秒 10 个令牌

public void doSomething() {
    if (rateLimiter.tryAcquire()) {
        // 处理请求
    } else {
        // 被限流
    }
}

最佳实践:

  • 分布式 ID 选择

    • 简单场景:数据库自增
    • 高性能:雪花算法
    • 高要求:Leaf/Sonyflake
  • 限流实现

    • 网关层限流:Nginx/Sentinel
    • 应用内限流:Guava RateLimiter
    • 分布式限流:Redis+Lua

英文面试 #

8. 英文自我介绍、项目讲解、技术方案描述 #

英文自我介绍模板:

Hi, my name is [Your Name]. I have [X] years of experience in Java development.

In my current role at [Company], I work as a Senior Engineer/Architect, responsible for designing and developing distributed systems.

Previously, I worked at [Previous Company] where I participated in [Project Name].

My technical expertise includes Java, Spring Boot, MySQL, Redis, and distributed system design.

I'm looking forward to joining your team and contributing to your success.

项目讲解常用表达:

- The project is about...
- My responsibility was...
- We used technologies like...
- The challenges we faced were...
- The solution we designed was...
- The results we achieved were...

技术方案描述:

Architecture:
- We adopted a microservice architecture...
- The system is divided into several services...
- Each service is responsible for...

Technical choices:
- We chose X because...
- The reason we used Y is...
- Compared to Z, X has advantages in...

Challenges and solutions:
- One of the main challenges was...
- We solved this by...
- The key insight was...

Results and metrics:
- Performance improved by...
- Latency reduced from... to...
- System availability achieved...

最佳实践:

  • 面试准备

    • 背熟自我介绍
    • 准备项目描述(STAR 原则)
    • 掌握技术词汇
  • 常见面试问题

    • Tell me about yourself
    • Describe your most challenging project
    • What's your greatest strength/weakness
    • Where do you see yourself in 5 years

9. 职业规划、对技术栈接受度 #

职业规划回答思路:

Short-term (1 year):
- Master the tech stack
- Contribute to the team
- Learn the business domain

Mid-term (2-3 years):
- Grow into a tech lead/architect
- Design complex systems
- Mentor junior developers

Long-term (3-5 years):
- Technical expert in a domain
- Architecture leadership
- Driving technical strategy

技术栈接受度:

I'm always open to learning new technologies. I believe the core principles of software engineering are more important than specific tools.

In my previous roles, I've worked with [tech list], and I'm confident I can quickly pick up [new tech] as needed.

I also enjoy exploring new technologies in my free time.

总结 #

这篇面试题汇总覆盖了从基础到高级的技术点,希望能帮助你系统性地准备面试。建议:

  1. 先理解原理,不要死记硬背
  2. 结合自己的项目经验讲
  3. 多写代码,算法题要熟练
  4. 保持学习,技术是不断进步的

祝面试顺利!


最后更新:2026-05-09