欢迎光临
我们一直在努力

精选后端场景题,不要纯看八股了!面试官惊呆:你这实力不一般!

1. Java 并发与集合

1.1 HashMap

Q1. 如果在高并发环境下,多个线程同时往 HashMap 里 put,可能会发生什么?你如何验证?

HashMap 不是线程安全的。在并发写场景下,会有三类典型风险:

  • 数据丢失/覆盖 —— 两个线程同时更新同一 key,会出现后写覆盖先写的情况;

  • 可见性/不一致 —— 一个线程 put 后,另一个线程不一定能立刻看到正确的结果;

  • 结构性异常(最严重) —— 在扩容并发时(JDK7 有经典案例)链表可能被改造成环导致 get/遍历死循环、CPU 飙高;JDK8 虽然改进了迁移逻辑,但并发修改仍会产生中间不一致或丢数据的问题。

  • 我验证的方法通常是做压力复现:把 HashMap 初始容量设得很小(比如 new HashMap<>(2))强制频繁扩容,启动 10–50 个线程并发执行大量 put(比如写 100k 条),用 CountDownLatch 同步“开枪”,最后检查 map.size() 与预期是否一致;同时用 ConcurrentHashMap 做对照,或者用并发测试工具(如 jcstress)做更严谨的竞态验证。

    Q2. 电商购物车并发量大,直接用 HashMap 存吗?为什么?

    不会直接用 HashMap。原因简单:并发读写情况下它不保证线程安全,会丢数据或出现不一致。我的实践方案会根据部署架构分两类:

    • 单 JVM / 本地缓存场景:用 ConcurrentHashMap<userId, Cart>,对单用户购物车的修改使用 compute/computeIfAbsent 等原子 API,或者对单个 userId 做Keyed Lock(分段/细粒度锁)以避免全局串行化。

    • 分布式/多实例场景:把购物车放在 Redis(Hash/JSON)或数据库为主的数据存储,应用层做本地缓存 + 异步刷新/失效通知,写操作尽量通过原子后端操作(如 HINCRBY、事务或 Lua)保证一致性。

    同时会考虑:缓存失效策略、热点用户限流、幂等设计(防重入)、以及避免把“购物车”过多地保存在内存导致 OOM。

    1.2 ConcurrentHashMap

    Q1. 实时监控统计接口调用次数,如何用 ConcurrentHashMap?如何缓解热点 key?

    ConcurrentHashMap<String, LongAdder>,每次计数用 computeIfAbsent(k -> new LongAdder()).increment()。LongAdder 是条带化的累加器,能在高并发更新时比单点 AtomicLong 更可伸缩(更少 CAS 冲突)。周期性把 LongAdder.sum() 或快照上报到监控系统。

    避免热点 key 的常见策略:

    • 本地 buffer + 批上报:每个线程/实例本地累加到一定阈值或定时 flush,减少对共享结构的频繁更新;

    • 分片/多槽计数:把一个热点 key 拆成 key#shard 多个计数槽,读取时聚合;

    • 令牌桶/限流:对极端热点做限速或降级,必要时专门隔离到单独实例。

    这种组合既保证高并发写的吞吐也能在读取时做可控合并。

    Q2. 在 ConcurrentHashMap 上实现“按访问频率的 TopN”会遇到哪些挑战?怎么解决?

    挑战主要是两个:一是 ConcurrentHashMap 原生是无序的,若每次查询都全量排序代价大(O(M log M));二是计数是近似/非瞬时一致(LongAdder.sum() 有并发波动),对严格实时性有影响。

    我常用的解决方案有几种,选哪种取决于对准确性、延迟、内存三者的权衡:

  • 周期性快照 + 小根堆:定期(比如每分钟)遍历 map,构建大小为 N 的最小堆,输出 TopN。适用于对实时性要求不是极高的场景。

  • 分层汇总:各线程/实例维护局部 TopN(或局部计数),定期合并到全局 TopN,避免每次全表扫描。

  • 近似算法(Count-Min Sketch) + TopK heap:在流量极大、内存受限时用近似算法节省空间,允许少量误差。

  • 窗口化:对不同时间窗口单独统计并快照(如滚动时间桶),可以得到滑动窗口 TopN。

  •                            免费获取资料点击下方小卡片Java资料https://mp.weixin.qq.com/s/iBmSycbmeTCSRNKJQ10EuA

    1.3 线程池

    Q1. 定时拉取第三方接口、每次几千条,如何设计线程池?如何避免积压?

    我实际会把“触发”与“执行”分开:用一个 ScheduledExecutor 或调度器负责按时拉取并把原始数据切分为批后提交到后端工作线程池(ThreadPoolExecutor)。理由是调度线程不应被耗时任务阻塞。

    ThreadPoolExecutor 的配置要看任务是 IO 密集 还是 CPU 密集:有个粗略公式 threads = cores * (1 + wait/compute) 可参考(IO 密集时设置更多线程)。队列必须有界(比如 ArrayBlockingQueue),避免无限堆积。拒绝策略我倾向 CallerRunsPolicy 做反压,或者自定义拒绝:上报/落盘/降级。并且对第三方调用要强制超时 + 限流 + 熔断,避免单个慢主机拖垮整个池。监控(队列长度、任务平均耗时、P95/P99)必须到位,超过阈值触发降频或扩容。

    Q2. 线程池里有慢任务导致堆积,怎么排查和优化?

    排查步骤我通常是:

  • 看指标:队列长度、提交率 vs 完成率、线程利用率、任务耗时 P95/P99。

  • 看线程快照(jstack):定位是否在等待外部 IO、被锁、或 GC 卡顿。

  • 排查外部依赖:数据库/HTTP/Redis 等延迟或阻塞。

  • 解决方法:

    • 把慢任务拆小或改成流水线(先入队,异步处理后续耗时步骤);

    • 给慢任务单独的线程池,避免它们污染快速任务池(隔离/舱壁模式);

    • 批量处理(合并多条 DB 写入减少 IO 次数);

    • 强制超时、重试限次数并退化到补偿流程;

    • 在入口做限流/降级,必要时临时拒绝非关键请求。

    1.4 synchronized / ReentrantLock

    Q1. 全局唯一订单号生成,选什么锁机制?为什么?

    我不会用普通 JVM 锁来做跨实例的全局唯一 ID,这会成为性能瓶颈。常见且更可靠的做法是:

    • Snowflake(分布式 ID):无中心、可横向扩展,生成速度快,适合大流量场景(需要处理时钟回拨);

    • 数据库 Sequence:强一致但吞吐有限,适合强顺序/少量写场景;

    • Redis INCR:简单、快速,但单点需做主从/持久化策略。

    Q2. 方法内要加锁但业务耗时长导致竞争严重,如何优化?能否拆分锁?

    优先思路是缩小临界区:把耗时操作(网络、IO、计算)移出锁,锁内只做必要的共享状态更新。其他策略包括:

    • 细粒度锁 / KeyedLock:按业务 key(如 userId)做分段锁,避免全局串行;

    • 读写锁:对读多写少的场景用 ReentrantReadWriteLock;

    • 乐观并发:对冲突概率低的场景用 CAS、版本号(乐观锁);

    • tryLock + 降级:用 tryLock 尝试获取锁失败则降级为异步/返回错误/重试。

    • 异步化:将耗时步骤放入队列并异步执行,锁只保护入队一致性。

    1.5 CAS / AQS

    Q1. 用 AtomicInteger 统计在线人数,为什么比 synchronized 合适?有没有不适合的场景?

    AtomicInteger 用 CAS 实现,无需进入内核阻塞,轻量、延迟低,很适合简单的计数器(比如在线人数的 increment/decrement)。相比 synchronized,它避免了上下文切换和线程阻塞的开销。

    但在极高并发下单个计数器仍可能成为热点(CAS 重试频繁),此时 LongAdder 更合适,因为它通过分段(cells)减少竞争,吞吐更高但占用更多空间。另外,如果要做复合操作(必须同时更新多个字段且保证原子一致性),CAS 单变量不够,这时仍需锁或事务性手段。

    Q2. 设计基于 AQS 的自定义锁,要考虑哪些问题?(公平性、可重入性等)

    一般会沿着 AbstractQueuedSynchronizer 的示例实现 tryAcquire/tryRelease,并在 Lock 的接口方法里委托 AQS 提供的 acquire/release 流程。这样既能利用 AQS 的排队/唤醒机制,也能自定义公平/非公平行为。

    • 独占 vs 共享:先决定锁是独占(像 ReentrantLock)还是共享(像读锁);实现不同;

    • 可重入:是否允许同一线程重复加锁?可重入需要记录 owner 线程和重入计数;

    • 公平性:是否 FIFO 排队(公平)或允许 barging(非公平);公平会降低吞吐但避免饥饿;

    • 中断/超时:是否支持 lockInterruptibly() 或 tryLock(timeout);

    • Condition 支持:是否需要条件队列(newCondition());AQS 提供 ConditionObject 支持。

    • state 管理:使用 getState()/setState() 保存锁持有计数,compareAndSetState 做原子变更。


    1.6 BlockingQueue

    Q1. 秒杀系统的异步下单模块选什么队列?ArrayBlockingQueue vs LinkedBlockingQueue 区别?

    对秒杀这类流量突发且希望可控背压的场景,我倾向用有界的 ArrayBlockingQueue(环形数组结构)。原因:固定容量更容易控制内存,延迟可预测,触达满队列后能触发拒绝策略(反压)。ArrayBlockingQueue 使用数组并通常以单锁实现(或一个锁但两个条件),而 LinkedBlockingQueue 基于链表且有两个锁(putLock / takeLock),吞吐在某些并发场景下更高但如果容量不设限会导致队列无限增长,增加 OOM 风险。秒杀场景优先考虑有界 + 反压。

    Q2. 当生产者远快于消费者,如何避免内存溢出?

    常用做法:

    • 有界队列 + 拒绝策略:严格限制队列大小,用 CallerRunsPolicy 或自定义拒绝策略做反压;

    • 入口限流:在服务入口用令牌桶/漏斗算法限制请求速率,避免大量无用请求涌入;

    • 扩容消费者:增加消费线程或横向扩容实例;同时注意不要盲增导致其他资源瓶颈(DB/IO)。

    • 批量/合并处理:消费者按批处理以提高吞吐;

    • 降级/丢弃策略:对非关键或可重试的请求做采样或丢弃,关键任务则保留。

    • 使用高性能队列:对低延迟需求可考虑 LMAX Disruptor 等环形缓冲方案(但实现/维护复杂)。


    2. MySQL

    2.1 事务与隔离级别

    Q1:在一个“用户下单 – 库存扣减”的场景中,如果不用事务,可能会发生什么问题?如何保证数据一致性? 如果没有事务保护,可能出现用户下了单但是库存没减成功,或者库存扣减了但是订单没生成的情况,这样数据就不一致了。 我一般会把这两个操作放在一个事务里,确保要么都成功,要么都回滚。如果业务上允许,我还会在库存扣减时加一些幂等设计,比如乐观锁检查库存是否足够,避免重复下单导致超卖。

    Q2:你们项目用的哪种隔离级别?为什么选它?如果改成 SERIALIZABLE,会有什么性能问题? 大多数情况下我们用的是 Read Committed 或 Repeatable Read。MySQL 默认是 RR,这个级别能避免不可重复读,配合 MVCC 性能也不错。 如果改成 SERIALIZABLE,所有读操作都会变成加锁读,冲突概率大幅增加,基本等于串行化执行,会对高并发系统带来明显的性能瓶颈,所以很少在生产里用。

    Q3:在 RC 隔离级别下,高并发扣减库存如何避免超卖? 我会结合业务情况来选:

    • 悲观锁:在扣减时直接 SELECT … FOR UPDATE 把库存行锁住,这样能保证并发安全,但会有锁等待。

    • 乐观锁:常见的做法是更新时带一个版本号或者条件,比如 UPDATE … WHERE stock = old_stock,如果返回 0 行说明有人抢先修改了,就重试。这个方式并发度更高,但要做好失败重试机制。

    2.2 MVCC

    Q1:用户 A 正在读一条商品数据,用户 B 同时修改了这条数据。请用 MVCC 的机制解释为什么 A 不会被阻塞。 因为 InnoDB 的 MVCC 会在每一行记录后面维护两个隐藏字段:创建版本号和删除版本号。 当 A 在读的时候,其实读到的是事务开始时“可见”的快照,而不是 B 改完的数据,所以不会和 B 的写操作冲突。这样就实现了读写不互相阻塞。

    Q2:在 RC 和 RR 隔离级别下,MVCC 的实现有何不同?分别能避免哪些并发问题? 在 RC 下,每次读都会生成一个新的快照,所以可能出现不可重复读;在 RR 下,快照在事务开始时就固定了,整个事务里看到的数据是一致的,可以避免不可重复读。不过幻读在 RR 下需要通过 next-key lock 来避免。

    Q3:如果一个大事务长时间不提交,会对 MVCC 产生什么影响? 它会拖住很多老版本的数据不能被清理,导致 undo log 不断增长,占用磁盘甚至影响性能。所以在生产上我们会避免长事务,或者用批处理拆分掉。

    2.3 锁机制

    Q1:SELECT … FOR UPDATE 是什么意思?在“秒杀扣库存”和“银行转账”两个场景下,使用它有什么不同? 这个语句表示在读取的时候加行级锁,直到事务结束才释放。

    • 在秒杀场景下,一般用它来锁住库存行,确保不会被多个用户同时扣减导致超卖。

    • 在银行转账场景下,可能涉及两条账户记录,如果两个事务交叉执行,就容易死锁,所以需要注意加锁顺序。

    Q2:在表上有一个二级索引,执行 SELECT … FOR UPDATE 时,InnoDB 会加什么锁?可能引发什么问题? InnoDB 会在二级索引和对应的主键记录上都加锁,如果是范围条件,还会加间隙锁。 这可能会导致锁范围比预期大,从而阻塞其他事务的插入或更新。

    Q3:在订单表上有一个范围查询 WHERE create_time BETWEEN …,可能会触发什么锁?会不会阻塞其他更新? 这会触发 next-key lock,也就是记录锁加间隙锁的组合。这样不仅锁住符合条件的行,还会锁住范围内的“空隙”,所以可能会阻塞一些插入操作。

    2.4 死锁

    Q1:请构造一个简单的 SQL 导致数据库死锁的场景。数据库会如何检测和处理? 比如事务 A 先更新用户表,再更新订单表;事务 B 先更新订单表,再更新用户表。两个事务都执行到第二步时,就会互相等待,形成死锁。 MySQL 会自动检测到死锁,回滚其中一个事务来打破僵局。

    Q2:在代码层面,你们如何避免死锁? 几个常见手段:

    • 保证访问表和行的顺序一致;

    • 拆分大事务,尽量缩短锁的持有时间;

    • 出现死锁时捕获异常,做重试。

    Q3:死锁和长事务之间有关系吗?请举例说明。 有关系。长事务持有锁时间长,更容易和别的事务形成死锁。比如一个事务开了很久,还没提交,另一个事务在尝试访问同一行时就可能出现互相等待。

    2.5 索引(最左前缀)

    Q1:有一个联合索引 (user_id, status, create_time),请问以下 SQL 哪些能用上索引?为什么?

  • WHERE status = 1 —— 不能,因为没从最左边字段 user_id 开始。

  • WHERE user_id = 123 AND status = 1 —— 可以,前两列都能用到。

  • WHERE user_id = 123 AND create_time > '2023-01-01' —— 可以用到 user_id,但 create_time 不能单独跳过 status,所以只能部分用上索引。

  • Q2:如果 where 条件里对 user_id 使用了函数,比如 WHERE user_id + 1 = 100,还能用索引吗? 不能,因为对字段做了计算,破坏了索引的有序性,优化器就没法走索引了。

    点击下方小卡片免费获取资料Java资料https://mp.weixin.qq.com/s/iBmSycbmeTCSRNKJQ10EuA

    2.6 索引(设计与优化)

    Q1:分页查询 LIMIT 100000, 10 很慢,你有什么优化思路? 这种深分页问题,我一般会用“延迟关联”的方式:先通过覆盖索引拿到主键 ID,再用这些 ID 去查具体数据。或者在业务上改成“基于上次翻页的最大 ID”来做分页,这样性能好很多。

    Q2:在一个高并发的业务系统里,设计索引时会遇到哪些权衡? 主要是读写的平衡。索引多了,查询会快,但写入和更新代价也会更高;索引还会占空间。所以一般是针对核心查询做精准索引,冷门场景可能通过缓存、异步计算来补。

    Q3:大表频繁做 LIKE '%abc%' 怎么优化? 普通 B+ 树索引没法用,因为前缀不固定。可以考虑全文索引,如果 MySQL 自带的性能不够,就会引入 ES 或者 Solr 这类搜索引擎。

    2.7 慢查询与执行计划

    Q1:一条 SQL 跑得很慢,你会用什么工具来分析? 我会先看慢查询日志,然后用 EXPLAIN 分析执行计划。关注的点包括 type(访问方式)、rows(扫描行数)、key(是否走索引)、Extra(有没有 filesort、临时表)。

    Q2:如果发现 rows 特别大,Extra 显示 Using filesort,你会如何优化? 说明优化器走了全表扫描,还需要排序。我会检查 where 条件和 order by 字段是否能覆盖索引,必要时加复合索引。

    Q3:你遇到过哪些索引失效的场景? 常见的有:对字段做函数计算;where 条件隐式类型转换(字符串和数字混用);联合索引中跳过中间字段;like 前缀模糊查询 %xxx。

    3. Redis

    3.1 缓存穿透 / 缓冲击穿 / 缓存雪崩

    Q1:分别是什么场景?举例说明。

    • 缓存穿透(penetration):请求的 key 在后端就不存在,缓存不会命中,所有请求直接落到 DB。典型被滥用的场景是攻击者扫大量随机 id(不存在的用户/订单),把后端打垮。防护手段常见的是布隆过滤器或在缓存里写空对象短期缓存.

    • 缓存击穿(stampede / hotspot expiry):某个极热的 key(比如热门商品详情)在短时间内过期,很多并发请求同时去读 DB,瞬时压垮后端。实战上我见过促销商品详情被误设置短 TTL,导致瞬时 DB QPS 激增。

    • 缓存雪崩(avalanche):大量缓存 key 在同一时间过期或缓存层故障,流量成批落到后端,造成更广泛的可用性问题。常用缓解:TTL 随机化、多级缓存、限流降级、预热/异步刷新。

    Q2:黑客持续请求不存在的 user id,会怎样防?

    在网关或应用层先查一个布隆过滤器(只要 Bloom 说“不在”,就直接拒绝 / 返回 404),这样不会访问 Redis/DB;Bloom 会有误判率(可能误把存在的判为存在),但能极大减少对后端的无效压力。配合速率限制与封禁策略就更稳。

    3.2 热 Key / 大 Key

    Q1:怎么发现热 Key?明星出轨场景缓存会怎样,你如何处理?

    发现方法实际且直接:看 Redis 命令统计、单 key QPS、INFO、监控面板(CPU、网络、命中率)以及 Redis 的 hotkey 探测工具/云厂商提供的探针。 

    如果评论区 QPS 爆发,表现通常是某个 key 的 QPS 极高、该 Redis 实例 CPU/网络饱和或出现 latency spike。我通常的应对是:把评论按子 key/页拆分(comments:postId:page),把热点分片或单独迁移到独立实例,或者做读扩展(只读副本)并在业务层做写排队/限流。

    Q2:删除大 Key(百万 field 的 Hash)会有什么影响?如何处理?

    直接 DEL/HDEL 这样的大操作会在主线程同步释放内存,可能导致 Redis 阻塞导致延迟或短暂不可用。

    实战建议是用 UNLINK(异步释放内存)或分批逐步删除,或把数据设计为多个小 key 而非一个超大 key。Redis 的 UNLINK 提供异步回收以避免阻塞。

    Q3:如何拆分/优化大 Key,避免阻塞主线程?

    • 设计上避免把百万条放在一个 key;按业务逻辑分页/分片存储。

    • 删除时用 UNLINK 或 SCAN + 批次删除/后台异步清理。

    • 避免对大 key 做全量读取(如 HGETALL),采用分页读取或范围查询。

    3.3 分布式锁

    Q1:你们用过 Redis 分布式锁吗?怎么做的?

    我们在一些需要短时间串行化写操作(例如单条缓存重建)场景用过 Redis 锁。现在常用的单实例实现是:SET resource-name my_random_value NX PX 30000,即“尝试设置且带过期时间”,这样可以在原子层面防止并发加锁。释放锁时用 Lua 脚本先校验 value(标识持有者)再删除,避免误删别人的锁。官方文档也推荐这种模式。

    Q2:为什么用 SETNX + EXPIRE 或 SET … NX PX,为什么用 Lua?

    SET … NX PX 可以在一次命令里完成“仅当不存在才设置 + 设置过期时间”的原子操作(比先 SETNX 再 EXPIRE 更安全)。Lua 脚本用于释放锁的“检查值再删除”步骤(if get(KEY)==val then del(KEY) end)作为原子操作执行,避免竞态条件。Redis 的脚本是原子执行的,这点在实现分布式锁时很重要。

    Q3:锁过期但业务没做完怎么办?怎么续期?

    有两种思路:

  • 短临界区优先:尽量让锁里的代码非常短,避免续期需要;

  • 看门狗式续期:使用客户端库(比如 Redisson)提供的看门狗自动续期机制——持锁客户端在超时前不断续期。但要注意网络分区情况下的安全边界(可能续期错误),所以我更倾向“缩短锁持有时间 + 设计幂等/补偿”。

  • 3.4 内存淘汰策略

    Q1:电商商品缓存:LRU / LFU / TTL 的利弊?

    • LRU:对短期热点响应好,简单实用;短期突发热点表现好。

    • LFU:对长期热门更稳,能保留长期热数据,但对短时热点不够敏感且实现复杂。

    • TTL(volatile-ttl):适合数据带时间语义的缓存,优先剔除快过期的键。我通常根据商品访问模式选择:长期热品单独隔离,普通商品用 TTL+LRU 混合策略。

    3.6 事务与 Lua 脚本

    Q1:Redis 的事务与 DB 事务有什么区别?能否保证原子性?

    Redis 的 MULTI/EXEC 是把命令排队然后一次性执行,但它不做像关系型数据库那样的复杂回滚机制(如果队列中某条命令出错,其他命令仍会执行);而 Lua 脚本 在 Redis 中是原子执行的(服务器在脚本运行期间阻塞其他命令),所以当需要服务器端的原子复合操作(如复杂的检查-写流程)我更倾向用 Lua。注意脚本不能太长,否则会阻塞主线程。

    Q2:为什么锁和计数器常用 Lua 实现?能解决什么?

    因为 Lua 可以把“读-判定-写”流程在服务器端一次性执行,避免多次网络往返和中间竞态。例如释放锁前先 GET 检查值再 DEL,用 Lua 可原子化这一步。它解决了经典的“检查后删除”竞态问题。缺点是脚本期间会阻塞主线程,所以脚本应短小高效。

    4. 计算机网络 & 操作系统

    4.1 Epoll

    Q1:如果一个连接长时间空闲,epoll 会有什么表现?如何避免空连接占用资源?

    epoll 本身只是事件通知,它不会主动关闭空闲连接。空闲连接会占用 socket、文件描述符、内核缓冲和少量内存。工程上通常有几种做法:

    • TCP 层:启用 OS 的 SO_KEEPALIVE / 调整 tcp_keepalive_*,在 TCP 层发现对端死掉后回收。

    • 应用层心跳:在应用协议里做心跳/超时检测(比如 60s 无数据就主动关闭),并用定时器(timer wheel/堆)驱动超时处理。

    • 连接池与限额:对长连接数设置上限(并发连接阈值),超限拒绝或排队。 这几条通常组合使用:keepalive + 应用心跳 + 合理的超时策略,既能保护资源也能对突发流量做 graceful degrade。

    4.2 TCP 三次握手 / 四次挥手

    Q1:为什么 TCP 用三次握手而不是两次?两次会有什么问题?

    三次握手的设计是为了双方确认。

    客户端希望确认服务器收到了它的 SYN,并且服务器也要知道客户端准备好了接收数据。如果只做两次(客户端发 SYN,服务器回 SYN-ACK 就结束),服务器不知道客户端是否已经准备好并进入 ESTABLISHED(客户端可能没收到响应),也无法区分网络中陈旧的 SYN。三次握手通过最后一条 ACK 来确认双方都进入了 ESTABLISHED,避免了重复/过期报文引起的混淆(这是 RFC 的基本设计意图)。

    Q2:描述 TCP 四次挥手过程,如果客户端突然断电,服务端会有什么表现?

    主动关闭方发 FIN → 对端 ACK → 对端完成自己的数据后发 FIN → 主动方 ACK(四次完成)。如果客户端突然断电,服务端在短时间内不会收到 FIN/ACK,会保持连接在 ESTABLISHED(或半开)状态直到 TCP 超时或 keepalive 超时;应用层会因为写/读超时出错或收到 RST(如果对端立刻关机会触发)。另外,主动关闭一侧会进入 TIME_WAIT 用来等待迟到的报文,防止旧连接的延迟报文影响新的连接。

    4.3 HTTP / HTTPS

    Q1:高并发场景为什么有时选 HTTP/2 或 gRPC 比 HTTPS/1.1 更合适?

    HTTP/2 的关键优势是单连接多路复用、头部压缩(HPACK)、流优先级等,能减少连接建立与队头阻塞,从而在大量并发小请求(比如微服务或页面资源)场景下显著更高效。gRPC 基于 HTTP/2 + Protobuf,提供高效的二进制序列化、IDL 定义、双向流等,更适合高吞吐的服务间 RPC。简言之:当场景是“很多短请求/服务间调用”时,HTTP/2/gRPC 往往能减少连接与编码开销并提升并发吞吐。

    4.4 进程 / 线程

    Q1:什么时候选多线程(如 Java 线程池)?

    多线程(Java 线程池):适用于业务逻辑比较复杂、线程上下文里能方便操作(例如调用阻塞 IO、DB、需要线程本地变量),并且语言/生态对线程支持良好。线程池便于并发请求调度、限流、拒绝策略等控制。 选择时会根据业务特性(CPU-bound vs IO-bound)、故障隔离、可观测性和运维成本来定。

    Q1:线程上下文切换代价在哪里?如何优化频繁切换?

    上下文切换代价主要在于保存/恢复寄存器、切换页表/TLB(可能)、内核调度开销以及因缓存被替换引起的 CPU cache miss。看到切换频繁时的常用优化:减少线程总数、使用事件驱动/异步模型(避免大量阻塞线程)、批量处理任务、合并短任务到单线程执行、使用协程/用户态线程(如 fiber)或改为 lock-free 结构减少阻塞。实践上先用监控(vmstat, pidstat, perf)定位切换源,再针对性优化。

    5. 消息队列

    5.1 消息可靠性

    Q1:如果 MQ 宕机了,如何确保消息不丢?

    消息写入必须持久化并做到复制(同步或可配置的准同步),生产者等待 broker 确认后才认为发送成功;当 broker 宕机时由副本接管(leader election),并且生产者有重试和回退逻辑。如果系统允许,生产者也可以在本地做临时持久化(文件/数据库)作为兜底,等 MQ 恢复后再重放。总体原则是:不把消息放到单点内存,保证多副本或持久化。

    Q2:消费者消费失败如何保证消息不丢,重试机制如何设计?

    • 首先消费要 ack 成功,失败时 nack/requeue 或把消息投到 DLQ(并带上失败次数、错误信息);

    • 设计指数退避(exponential backoff)、有限重试次数与最终入 DLQ 的策略,避免无限重试导致热点消息挂死队列。

    • 重试可能要带上延时(Delay / Scheduled Retry),常见做法是:用延时队列、或者把失败消息放入延时队列(或使用 Broker 的延时特性、DLX+TTL),定时重试并在超过阈值后人工介入。对关键业务,消费端需要做幂等校验避免重复产生副作用。

    5.2 顺序消息

    Q1:订单创建 → 下单 → 付款 → 发货,如何保证消息顺序?

    顺序通常按业务 key(比如 orderId)路由到同一个队列/分区来保证:只要同一 key 的消息总是写到同一分区并由同一个消费者线程顺序处理,就能保证顺序。重要的是发送端和路由规则要一致(相同 key → 相同 partition/queue)。如果希望横向扩展,同时保持顺序,可以把“顺序需求”限定到消息组/业务 key 级别(即只保证同一订单内的顺序,而不是全局顺序)。

    5.3 延时 / 死信队列

    Q1:订单 30 分钟未支付自动关闭,我们怎么实现?

    我会说两种常见实现:

  • 延时队列:下单时把“迟延任务(30min 关闭订单)”放到延时队列,到期进入消费队列后执行关闭逻辑。实现方式不同中间件不同(RabbitMQ 用 TTL+DLX 或 Delayed Plugin,Redis 可用 sorted set + 定时轮询/stream + consumer)。RabbitMQ 的 TTL + DLX 是常见做法(发送到 TTL 队列,过期后 dead-letter 到目标队列)。

  • 定时扫描/延迟任务表:把待处理任务写入 DB(或调度系统),定时扫描(比如 cron/Quartz 或分布式调度)来处理。这种方式实现简单、稳定,适合对精确时效性要求不极高的场景。

  • 5.4 消息堆积

    Q1:如果 MQ 消息积压了几个小时,系统该如何处理?生产端还是消费端先优化?

    我会给出实战思路:先判断瓶颈在哪里:是消费者处理慢(CPU/IO/外部依赖慢)还是生产端流量爆发超出消费能力?优先从消费端入手(因为消费端扩容通常更直接):

    • 消费端:扩容消费者、提升并行度、优化处理逻辑(批量/合并写),或做降级处理(先把数据写到冷存储再异步补处理)。

    • 生产端:临时限流(入口 throttling)、熔断或降级,避免继续增长 backlog。

    • 其他:如果是分区有限制(Kafka partition 数),可能需要 re-partition 或重新设计分区键。总体原则:短期内保护业务可用(限流/降级),中长期提升消费能力或做后端优化。

    Q2:消费者消费不过来,是扩容消费者还是限流?为什么?

    面试里我会说两者结合最稳妥:若能水平扩容且无共享资源瓶颈(如 DB 写入能扩),优先扩容消费者;但如果消费涉及到下游无法扩(例如单库写入吞吐达到瓶颈),扩容没用,此时需要 限流 或做系统降级(例如批量/采样/优先级队列)。所以决策取决于下游能力与成本,单纯“扩容”不是万能的。

    5.5 幂等性

    Q1:MQ 发送了重复消息,如何保证消费者幂等?举例。

    我会回答常见做法:在消息上携带唯一业务 id(messageId 或 requestId),消费者消费前先在幂等存储(Redis/数据库)做 SETNX messageId -> processed 或使用 DB 的唯一约束(insert 幂等键),只有首次成功处理才执行业务副作用。举例:在订单支付处理中,把 paymentId 存为唯一索引,重复通知到来时插入会失败或被忽略,从而避免重复扣款。

    Q2:支付系统里“支付成功”消息重复投递会发生什么?如何防止重复扣款?

    实操:如果重复投递且消费端没有幂等保护,会导致多次入账/扣款的灾难。防护措施:使用幂等键 + 事务(数据库层做唯一约束并在一条事务中记录消费记录与执行业务),或者用分布式事务/外部支付平台的幂等接口。最稳妥的是在消费逻辑里先判断 trade_no 是否已经处理(数据库唯一索引),如果已处理则返回成功,不再重复扣款。


    点击下方小卡片,免费获取资料Java面试https://mp.weixin.qq.com/s/iBmSycbmeTCSRNKJQ10EuA

    赞(0)
    未经允许不得转载:171主机测评 » 精选后端场景题,不要纯看八股了!面试官惊呆:你这实力不一般!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址