微服务面试题 - 2026-06-19 17:24
生成时间:2026-06-19 17:24 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. 分布式锁用 Redis 和 Zookeeper 各自有什么优缺点?实际选型怎么判断?
【标准答案】Redis 分布式锁:性能高、部署简单——Redisson 的看门狗机制自动续期、可重入、读写锁都有封装。致命缺点是锁不住——主从切换或哨兵选主时,主节点挂掉但锁还没同步到从节点,新主不知道这把锁存在,另一个客户端可能同时拿到锁。这就是"Redis 锁不安全"的根源,Redlock 算法试图解决但争议很大。Zookeeper 分布式锁:利用临时顺序节点 + Watcher 机制,客户端创建临时顺序节点后,判断自己是不是最小序号——是就拿锁,不是就 Watch 前一个节点。临时节点的特性是客户端断开连接自动删除,天然防死锁。选型口诀:业务对性能要求高、允许小概率并发冲突(比如幂等兜底)→ Redis;业务对一致性要求苛刻、锁错了会造成资损(比如:金融扣款)→ ZK。折中方案:Redisson 的 RLock + 业务自身幂等校验双保险,Redis 锁只是"第一道防线",真锁不住幂等兜底。面试官最想听的是你知道 Redis 锁"不是绝对安全"这件事。
【面试题】2. Eureka 的自保护模式是什么?触发条件是什么?为什么不能随便关掉?
【标准答案】自保护是 Eureka 的经典容错设计。触发条件:Server 端统计最近 15 分钟内心跳续约的失败率超过 85%(默认阈值)。这种场景下 Eureka 认为不是服务挂了而是自己网络出问题了——大量 Client 续约失败但 Eureka 觉得自己没问题,那就进入自保护模式:不再剔除任何已过期的服务实例,宁可列表里有僵尸 IP 也绝不误杀活着的服务。为什么不能随便关?关了叫"宁可错杀一千不放过一个"——一次网络抖动 + 自保护关闭 → Eureka 认为所有没及时续约的服务都死了 → 全量剔除 → 整个微服务体系的调用链全断 → 人工恢复比 502 更恐怖。自保护的代价是"服务列表可能不精确"但好处是"系统高可用不受网络抖动影响",这是一个典型的 AP 取舍——牺牲一致性(实例列表可能不准)保可用性(服务不误剔除)。关闭自保护的正确姿势:不是"把开关关掉",而是在客户端加熔断重试机制,这样即使自保护保留了僵尸实例,调用方也能自动跳过,既不用关自保护也不会被僵尸实例坑。
【面试题】3. 微服务网关路由的四种主流负载均衡策略的选型依据是什么?
【标准答案】轮询(Round Robin):请求挨个分,最公平也最蠢——默认适合所有场景的前提是"每个请求的成本一样"。接口有快有慢就跪了,快接口分到 100 个请求瞬间处理完空闲,慢接口分到 100 个还堵着。加权轮询(Weighted Round Robin):根据机器配置设权重,8 核 16G 权重 5,4 核 8G 权重 2,请求按权重比例分发。解决了硬件异构问题,但没解决"当前实时负载差异"——高权重机器可能刚好在处理一堆慢请求,低权重机器反而空闲。最小连接数(Least Connections)或最短响应时间(Weighted Response Time):选当前活跃连接数最少或 P99 响应时间最短的实例,动态反映实时负载,最"智能"但实现最复杂——需要持续收集各实例的 RT 数据并加权计算。一致性哈希:同一用户或同一 key 的请求永远路由到同一台机器,解决了"有状态"的问题——比如需要粘性 Session 或本地缓存命中率最大化的场景。选型一句话:无状态服务 → 加权轮询打底,异构且关心实时负载 → 最小连接,需要粘性 → 一致性哈希。别过度设计——90% 场景加权轮询够用。
【面试题】4. TCC 分布式事务的 Try、Confirm、Cancel 三个阶段到底做了什么?
【标准答案】TCC 不是二阶段提交的包装,是业务补偿模式。Try 阶段:预留资源。不是直接扣库存而是"冻结库存",可卖 100 件的商品,Try 后冻结 1 件→可卖 99 件+冻结 1 件,库存总量还是 100。Confirm 阶段:确认提交。把冻结的库存真正扣掉→可卖变成 99 件,冻结归零。Cancel 阶段:回滚释放。把冻结的库存还回去→可卖恢复 100 件。三个阶段的"原子性"靠的是:Confirm 和 Cancel 必须幂等——如果一个服务成功了但返回超时,协调者会重试 Confirm/Cancel,服务收到重复请求要正确处理。关键是每个业务操作都要设计 Try 的"预留语义",不是所有操作都能做预留——调用第三方支付就是没法预留,这种操作要安排在 Confirm 阶段通过重试+对账来兜底。TCC 的设计精髓在幂等而不是两阶段——Confirm 必须是"无论执行多少次结果一样",Cancel 也必须有"空回滚"能力(收到 Cancel 但 Try 都没执行过也要成功返回)。TCC 适合对一致性要求极高但链路短的场景(转账、秒杀扣库存),链路长日志爆炸。
【面试题】5. Seata AT 模式的 undo_log 表是怎么实现自动回滚的?它的性能开销在哪?
【标准答案】Seata AT 模式核心是一张 undo_log 表。执行流程:第一阶段——业务 SQL 执行前,Seata 从数据库里查出修改前的数据作为"前镜像"(before image),SQL 执行后再查出修改后的数据作为"后镜像"(after image),把前后镜像序列化写入 undo_log 表,然后和业务 SQL 在同一个本地事务里提交。第二阶段——如果全局事务提交,协调者通知各分支异步删 undo_log;如果全局事务回滚,协调者通知各分支根据 undo_log 里的前镜像生成反向 UPDATE 语句回滚数据。如果回滚时发现后镜像和当前数据不一致——说明被其他事务改了——抛异常走人工介入。性能开销在三处。第一,每条 DML 多两条 SELECT(取前镜像+后镜像)和一次 INSERT(写 undo_log),SQL 量翻 3~4 倍。第二,undo_log 表本身就是热点——所有写操作都往一张表怼,高并发下 undo_log 表的行锁和 IO 可能比业务表更先成为瓶颈。第三,全局锁——AT 模式默认在第二阶段提交前持有全局锁防止脏写,串行化代价不小。Seata AT 的定位就是"对业务零侵入的轻量级 2PC",零侵入的代价是性能开销。高并发写场景谨慎使用——适合事务量不大但一致性要求高的内部管理类接口。
【面试题】6. 缓存穿透、缓存击穿、缓存雪崩三个概念分别怎么解决?各自最核心的一招是什么?
【标准答案】三兄弟名字像,根因完全不同。缓存穿透:查一个不存在的数据(id=-1),缓存没命中 → 打到 DB → DB 也返回空 → 缓存没存(因为空值)→ 下次请求继续打到 DB。高并发恶意攻击时 DB 被打穿。最核心一招:缓存空对象 + 短过期时间(30s)。id=-1 查出来是空,就在缓存里存一个"空值标记",下次查直接返回空不走 DB。进阶版用布隆过滤器——把所有合法 id 存到位图里,请求先过布隆,不在位图里直接返回不存在。缓存击穿:一个热点 key(双 11 爆款商品详情)刚好在过期瞬间,大量请求同时穿透到 DB,把 DB 压死。核心一招:互斥锁(Redis SETNX),第一个请求拿到锁去查 DB 重建缓存,其他请求等在锁外,锁释放后从缓存直接取。千万别把所有热点 key 的过期时间设成一样——加个随机值把过期时间打散。缓存雪崩:大量 key 同时过期或 Redis 集群挂掉,全部流量瞬间打到 DB。核心一招:过期时间加随机偏移(TTL × random(0.8~1.2)),让 key 的过期时间分散开。Redis 集群层面做高可用——主从 + 哨兵/Cluster,缓存挂了还有降级兜底。一句话区分:穿透=查不存在的数据、击穿=一个热点 key 到期、雪崩=大面积 key 同时到期或 Redis 挂了。
【面试题】7. DDD 的限界上下文怎么识别?事件风暴具体在会议上怎么开才能不跑偏?
【标准答案】限界上下文不是拍脑袋画的,是"统一语言"的自然边界——当同一个词在不同场景下含义不同时,限界上下文就出来了。比如"商品":在商品管理域里关心的属性是名称、规格、进价;在订单域里关心的是名称、售价、库存状态;在物流域里关心的是重量、体积、易碎标记。同一个"商品"在三个上下文里是不同的模型——这就是拆分边界。事件风暴正确开法:拉上产品、领域专家、开发、测试,一块白板。第一步贴橙色事件卡——所有"发生了什么"(用户下单、支付成功、库存扣减),只写过去式,不讨论细节;第二步贴蓝色命令卡——每个事件由哪个命令触发("下单"命令触发"订单已创建"事件);第三步贴黄色聚合卡——命令操作了哪个聚合(订单聚合、库存聚合);第四步贴绿色读模型卡——用户需要看到什么数据(订单列表、库存快照)。关键纪律:主持人负责别让讨论沉入"订单状态机到底几个状态"这种细节,先出骨架再填血肉。一个下午 3 小时出完初版,之后分上下文各自细化。能完成一场高效事件风暴的前提是业务方愿意参与且主持人能控场——如果业务方没时间,用"用户旅程映射"代替,从用户操作出发倒推服务边界。
【面试题】8. 事件溯源(Event Sourcing)和 CQRS 是什么关系?不引入 CQRS 单独用 Event Sourcing 有什么致命问题?
【标准答案】事件溯源和 CQRS 常被绑在一起说,但它们是两个独立模式。Event Sourcing 核心是:不存当前状态,存导致状态变化的所有事件序列。比如账户余额不是存 balance=500 而是存 [存入100, 存入200, 取款50, 存入250] 四个事件,当前余额通过重放事件计算出来。CQRS 核心是:读写分离,写走领域模型,读走专门优化的查询模型。二者关系:Event Sourcing 天然驱动 CQRS——写端产生事件流,读端订阅事件流构建读模型。致命问题:只做 Event Sourcing 不做 CQRS——查询账户余额时要从几百万个历史事件里重放一遍才能算出余额,性能灾难。CQRS 恰好解决了这个问题——读端提前消费事件、以"快照+增量事件"维护一份可直接查询的读模型,读请求不用重放事件。反过来单独用 CQRS 没问题(写 MySQL 读 ES 就是 CQRS),单独用 Event Sourcing 不行——查询一定会是大坑。面试关键一句话:Event Sourcing 是"以事件为事实来源",CQRS 是"把事件投影成可查询视图",二者像齿轮一样咬合,但不绑定。
【面试题】9. 线上 Java 服务 CPU 飙到 100%,你从收到告警到定位到具体代码行,完整排查链路是怎样的?
【标准答案】一二三四步,逐层下钻。第一步:确认进程。top -c 看哪个 Java 进程吃 CPU,记下 PID。如果 top 按 1 看多核——是单核打满还是全核打满?单核大概率是死循环或正则回溯,全核大概率是 GC 频繁或线程池全忙。第二步:找线程。top -Hp <PID> 看进程中哪个线程 CPU 最高,记下线程 ID(十进制)。第三步:定位线程栈。jstack <PID> > jstack.log 导出线程栈,把线程 ID 十进制转十六进制,在 jstack.log 里搜索 nid=0x<hex>,找到对应线程的调用栈——如果是业务线程,从栈里就能看到哪行代码在跑;如果是 GC 线程(如 GC task thread),说明是 GC 频繁。第四步:确认 GC 问题。jstat -gc <PID> 1000 10 看每秒 GC 情况——如果 Full GC 频率很高且每次回收很少,那就是内存泄漏或堆配小了。jmap -histo:live <PID> | head -30 看对象占用排行,哪个对象实例数异常大就是泄漏点。最后一步如果定位到业务代码:arthas 的 trace 命令直出某方法的调用耗时分布,watch 看入参和返回值,秒级定位。核心心法:CPU 高先从线程入手,线程栈指向什么就是什么——没思路时先把 jstack 扔出来看一眼。
【面试题】10. 跨服务数据一致性怎么设计对账方案?每天 1000 万笔订单,你怎么在不对线上业务造成压力的前提下核对?
【标准答案】对账方案分三层。第一层:实时对账——下单时订单服务发"订单创建"事件,支付服务消费后支付完成发"支付完成"事件,用一个独立的对账服务消费两边事件,在内存里做 join。30 秒内订单没匹配到支付记录 → 推送告警。这一步覆盖绝大多数异常,但丢消息或消费延迟会导致假阳性。第二层:T+1 离线全量对账——凌晨从订单库和支付库各拉一份前一天的全量数据,导出到对账平台,按订单号 join 比对:订单有支付没有→"已支付未回调"异常,支付有订单没有→"幽灵支付"异常。千万级数据别用 MySQL join,用 SparkSQL 或 Flink 的批处理 10 分钟跑完。关键设计:只读备库不碰主库,不对线上业务造成任何压力。第三层:兜底补偿——对账发现的差异记录到"差异表",按场景自动补偿——订单超时未支付直接取消+释放资源,已支付未回调主动查支付渠道确认并补推回调。三层的时间窗口互补:实时对账抓秒级异常,T+1 对账做全量兜底,补偿机制自动修复。面试要点:不是"保证完美一致"——是"在可接受的时间窗口内发现并修复差异"。问清楚业务能容忍多长时间的"数据不一致窗口"——这是设计对账频率的锚点。