后端面试题大全:并发、数据库与分布式
后端面试题大全:并发、数据库与分布式
前言
后端开发面试中,并发编程、数据库设计和分布式系统是高阶岗位必考的核心模块。本教程从实战角度出发,梳理这三大领域高频、典型且能体现深度的面试题,并给出结构清晰、便于理解的答案。无论你是准备跳槽还是想系统巩固后端知识,这份大全都能帮你精准突击。
一、并发编程
1. 线程与进程的本质区别是什么?为什么说线程是轻量级进程?
- 进程:操作系统资源分配的基本单位,拥有独立的地址空间(代码段、数据段、堆栈等)。进程间通信(IPC)必须依赖操作系统提供的管道、消息队列、共享内存等机制,开销大。
- 线程:CPU调度的基本单位,同一进程内的线程共享地址空间和大部分资源(如文件描述符),各自仅拥有独立的栈和寄存器上下文。线程间通信可直接通过读写共享变量进行,代价小,因此称为“轻量级”。
二者切换成本也不同:进程切换需切换页表、刷新TLB,开销远大于线程切换。
2. 如何用 Java 写一个线程安全的单例模式?双重检查锁定(DCL)为什么需要 volatile?
推荐实现(静态内部类或枚举更优雅,但 DCL 常被问及):
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子操作
}
}
}
return instance;
}
}
为什么需要 volatile?
new Singleton() 并非原子操作,可分解为:①分配内存;②初始化对象;③将引用指向内存。CPU 或编译器可能重排序为①→③→②。若线程 A 执行到③但未初始化,线程 B 看到 instance != null 就返回未初始化完毕的对象。volatile 禁止指令重排序并保证可见性,从而解决该问题。
3. synchronized 锁升级过程是怎样的?偏向锁、轻量级锁、重量级锁如何演变?
JDK 1.6 后优化为了减少锁竞争开销,锁状态根据竞争情况逐步升级(不可降级):
- 无锁:刚创建的对象 Mark Word 存有 hashCode。
- 偏向锁:当一个线程首次获得锁时,在对象头记录该线程 ID,以后该线程进出同步块无需 CAS 操作。若其它线程竞争,偏向锁撤销升级至轻量级锁。
- 轻量级锁:竞争线程通过 CAS 自旋尝试获取锁,适用于持锁时间短的场景。自旋失败一定次数后升级。
- 重量级锁:自旋仍不能获得锁,膨胀为重量级锁,线程挂起进入阻塞队列,由操作系统内核完成调度。
关键点:偏向锁单线程高效;轻量级锁通过自旋避免上下文切换;重量级锁系统开销大但保证高竞争时的公平和吞吐。
4. AQS(AbstractQueuedSynchronizer)的工作原理是什么?它是如何实现一把锁的?
AQS 维护一个 volatile int state 变量和一个 CLH 变体双向队列。
- 获取锁:尝试用 CAS 将 state 从 0 改为 1,成功则获取锁;失败则将当前线程包装成节点加入等待队列尾部,并挂起线程(调用
LockSupport.park)。 - 释放锁:将 state 设回 0,唤醒队列头部的下一个节点线程。
- 共享模式:state 可表示资源数,如
Semaphore将 state 设为许可证数量,获取时 CAS 减少,释放时增加并传播唤醒。 - 条件队列:每个 AQS 可关联多个
ConditionObject,维护一条条件等待队列,await()释放锁并加入该队列,signal()将节点转移到同步队列。
AQS 通过模板方法模式将同步状态管理、线程排队和阻塞唤醒机制封装,开发者只需实现 tryAcquire/tryRelease 等。
二、数据库
5. 数据库事务的四大特性(ACID)具体指什么?如何实现?
- 原子性 (Atomicity):事务操作要么全部成功,要么全部回滚。基于 undo log,记录修改前的数据,回滚时执行逆向操作。
- 一致性 (Consistency):事务执行前后数据库必须处于一致状态(完整性约束等)。由其它三个特性以及应用逻辑共同保证。
- 隔离性 (Isolation):多个事务并发执行时互不干扰。通过锁机制(读写锁)、MVCC(多版本并发控制)实现。
- 持久性 (Durability):事务提交后,其修改永久保存。基于 redo log,执行写操作时先顺序写日志,再异步刷新脏页,即使宕机也可通过 redo log 恢复。
6. 脏读、不可重复读、幻读分别是什么?这四种隔离级别如何解决它们?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB通过Gap Lock解决) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
- 脏读:一个事务读到另一事务未提交的修改。
- 不可重复读:同一事务内,两次读取同一数据结果不同(被另一事务已提交的 update 影响)。
- 幻读:同一事务内,两次查询结果行数不同(被另一事务已提交的 insert/delete 影响)。
InnoDB 在可重复读隔离级别下,通过 临键锁(Next-Key Lock) 防止幻读:锁定索引记录 + 间隙(Gap),阻止其它事务插入。
7. MySQL 中索引为什么用 B+ 树而不用 B 树或二叉平衡树?
- 与 B 树对比:B+ 树非叶子节点只存键,不存数据,单个节点可存储更多键,树更矮,磁盘 I/O 次数更少;所有数据都放在叶子节点的有序链表中,范围查询和排序时只需遍历叶子链表,B 树则需中序遍历,可能跨层访问。
- 与二叉平衡树(如 AVL/红黑树)对比:二叉平衡树每个节点只有两个分支,树高随数据量增大很快,导致大量随机 I/O。B+ 树是多路搜索树,出度大,更适应磁盘预读特性。
8. 慢查询如何定位与优化?请描述 EXPLAIN 输出的关键字段。
定位:开启慢查询日志 slow_query_log,设 long_query_time,或用工具如 pt-query-digest。
EXPLAIN 分析:
- type:连接类型,从好到差:
system > const > eq_ref > ref > range > index > ALL。至少达到range级别。 - possible_keys / key:可能使用和实际使用的索引。若为 NULL,考虑建索引。
- rows:估计需扫描的行数,越小越好。
- Extra:重要信息,如
Using index表明覆盖索引;Using filesort表示需要额外排序,可优化索引;Using temporary使用了临时表,常见于 GROUP BY 或 DISTINCT 优化不当。
优化方向:创建合适索引(遵循最左前缀)、避免 SELECT *、优化分页、避免在索引列上使用函数、改写子查询为 JOIN 等。
三、分布式系统
9. CAP 定理的含义是什么?在实际分布式系统中如何取舍?
CAP 指:
- 一致性 (Consistency):所有节点在同一时刻看到相同数据。
- 可用性 (Availability):每次请求都能获得非错误的响应,但不保证数据的时效性。
- 分区容错性 (Partition Tolerance):系统在出现网络分区(节点间消息丢失或延迟)时仍能正常运作。
取舍:由于网络分区不可避免,分布式系统必然选择 P,此时在 C 和 A 之间权衡。
- CP 系统:发生分区时牺牲部分可用性,确保强一致性(如 ZooKeeper、etcd,少数节点不可用时会拒绝服务)。
- AP 系统:发生分区时保证可用性,允许短暂数据不一致(如 Eureka 服务发现、Cassandra,通过最终一致弥补)。
生产中常采用 BASE 理论,追求最终一致性。
10. 什么是分布式锁?用 Redis 实现时如何避免死锁及误删锁?
用途:在分布式环境下,控制多进程对共享资源的互斥访问。
Redis 实现要点(基于单节点):
// 加锁,SET resource_name random_value NX PX 30000
// NX 表示 key 不存在才设置,PX 设置过期时间防止死锁。
// 解锁用 Lua 脚本保证原子性:先判断 value 是否和自己设置的一致,一致才删除。
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
防死锁:必须设置过期时间,即使客户端宕机锁也能自动释放。
防误删:使用唯一标识(UUID)作为 value,解锁时校验,防止自己锁过期被其它客户端持有,而删掉了别人的锁。
高可用增强:Redisson 使用看门狗自动续期;或者用 RedLock 算法(多节点独立获取锁)但存在争议,建议评估后使用。
11. 分布式事务有哪些解决方案?请简述 Seata 的 AT 模式原理。
主流方案:
- 2PC(XA):协调者询问参与者 Prepared,全部成功后再 Commit。强一致但性能差,单点故障。
- TCC:Try 预留资源,Confirm 执行业务,Cancel 释放预留。侵入业务,需实现补偿逻辑。
- 可靠消息最终一致:事务消息(RocketMQ)或本地消息表,保证消息与业务操作原子性,下游消费实现幂等。
- Seata AT 模式:无侵入的自动补偿方案。
Seata AT 模式:
- 一阶段:拦截业务 SQL,解析并生成前镜像(before image)和后镜像(after image),存入 undo_log。本地事务提交(业务操作 + undo_log 插入)。
- 二阶段-提交:全局事务提交,异步删除 undo_log。
- 二阶段-回滚:根据 undo_log 的前镜像数据生成反向 SQL 并执行,再删除 undo_log。通过全局锁防止回滚时数据被其它事务修改。
阿里的 Seata 在易用性和性能间取得了较好平衡。
12. 服务发现是如何工作的?比较一致哈希与普通哈希在微服务中的不同作用。
流程:服务提供者启动时向注册中心注册(IP:Port 等元数据);服务消费者从注册中心订阅所需服务列表并缓存本地,通过负载均衡策略选择实例调用;结合心跳检测剔除不健康节点。
一致哈希 vs 普通哈希:
- 普通哈希:
hash(key) % N,当节点数量 N 变化时,几乎所有映射关系失效,对分布式缓存或会话保持影响巨大。 - 一致哈希:将节点和数据映射到 0~2^32-1 的哈希环上,数据顺时针寻找最近节点。加减节点时仅影响相邻一小部分数据。通过引入虚拟节点可解决数据倾斜问题。常用于负载均衡的会话保持、分布式缓存(如 Redis Cluster 的 slot 分配本质类似)等。
总结
掌握并发、数据库和分布式这三板斧,不仅能让你在面试中游刃有余,更是进阶高级后端工程师的必经之路。本文所有题目均源自真实高频场景,建议结合项目实践反复思考,把知识内化为工程直觉。持续深入,祝你拿下心仪 Offer!