后端面试题大全:并发、数据库与分布式

FreeGuideOnline 最新 2026-06-18

后端面试题大全:并发、数据库与分布式

前言

后端开发面试中,并发编程、数据库设计和分布式系统是高阶岗位必考的核心模块。本教程从实战角度出发,梳理这三大领域高频、典型且能体现深度的面试题,并给出结构清晰、便于理解的答案。无论你是准备跳槽还是想系统巩固后端知识,这份大全都能帮你精准突击。


一、并发编程

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 后优化为了减少锁竞争开销,锁状态根据竞争情况逐步升级(不可降级):

  1. 无锁:刚创建的对象 Mark Word 存有 hashCode。
  2. 偏向锁:当一个线程首次获得锁时,在对象头记录该线程 ID,以后该线程进出同步块无需 CAS 操作。若其它线程竞争,偏向锁撤销升级至轻量级锁。
  3. 轻量级锁:竞争线程通过 CAS 自旋尝试获取锁,适用于持锁时间短的场景。自旋失败一定次数后升级。
  4. 重量级锁:自旋仍不能获得锁,膨胀为重量级锁,线程挂起进入阻塞队列,由操作系统内核完成调度。

关键点:偏向锁单线程高效;轻量级锁通过自旋避免上下文切换;重量级锁系统开销大但保证高竞争时的公平和吞吐。

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 模式

  1. 一阶段:拦截业务 SQL,解析并生成前镜像(before image)后镜像(after image),存入 undo_log。本地事务提交(业务操作 + undo_log 插入)。
  2. 二阶段-提交:全局事务提交,异步删除 undo_log。
  3. 二阶段-回滚:根据 undo_log 的前镜像数据生成反向 SQL 并执行,再删除 undo_log。通过全局锁防止回滚时数据被其它事务修改。
    阿里的 Seata 在易用性和性能间取得了较好平衡。

12. 服务发现是如何工作的?比较一致哈希与普通哈希在微服务中的不同作用。

流程:服务提供者启动时向注册中心注册(IP:Port 等元数据);服务消费者从注册中心订阅所需服务列表并缓存本地,通过负载均衡策略选择实例调用;结合心跳检测剔除不健康节点。

一致哈希 vs 普通哈希

  • 普通哈希hash(key) % N,当节点数量 N 变化时,几乎所有映射关系失效,对分布式缓存或会话保持影响巨大。
  • 一致哈希:将节点和数据映射到 0~2^32-1 的哈希环上,数据顺时针寻找最近节点。加减节点时仅影响相邻一小部分数据。通过引入虚拟节点可解决数据倾斜问题。常用于负载均衡的会话保持、分布式缓存(如 Redis Cluster 的 slot 分配本质类似)等。

总结

掌握并发、数据库和分布式这三板斧,不仅能让你在面试中游刃有余,更是进阶高级后端工程师的必经之路。本文所有题目均源自真实高频场景,建议结合项目实践反复思考,把知识内化为工程直觉。持续深入,祝你拿下心仪 Offer!