Skip to content

【面试题】1. 微服务拆分时,DDD 的限界上下文怎么落地?举个例子说说你是怎么划边界的? ​

【标准答案】DDD 不是画框框,是找"业务的内聚点"。核心就一句话:数据强一致在一起的放一个服务,最终一致的拆出去。 举个例子,电商里"下单"和"扣库存"——订单创建后库存必须立即扣,这是强一致,所以很多人把它们放一个服务里。但更好的做法是把"库存"独立成一个限界上下文:扣库存变成一个"预留库存"的动作,用 Redis Lua 原子扣减,订单服务只负责生成订单记录和发"库存预留成功"事件。边界怎么定?三问法: ① 这个领域有没有独立的业务语言?比如"订单"领域说"待支付","物流"领域说"待揽收",语言不同就是边界。② 数据能不能接受短暂不一致?订单状态变了物流 5 秒后才更新——能接受,就拆。③ 改动频率是否不同?订单规则每周改,支付规则三个月不动——拆。核心:不要按数据库表拆,按业务能力拆。

【面试题】2. Nacos 做注册中心时,服务实例的临时节点和持久节点有什么区别?什么时候用哪个? ​

【标准答案】Nacos 注册实例分两种模式,这个选错了线上会出问题。临时节点(ephemeral=true,默认): 客户端和 Nacos 保持心跳,心跳断了 15 秒后 Nacos 自动踢掉这个实例。服务挂了自动下线,不需要人工介入。适合 K8s 里频繁扩缩容的微服务——Pod 没了自动摘掉,新 Pod 启动自动注册。持久节点(ephemeral=false): 实例不靠心跳存活,即使心跳断了 Nacos 也不摘除,标记为"不健康"但不剔除。适合"基础设施类"服务——比如数据库代理、消息队列桥接,这些挂了也不能随便摘,要人工介入。选型口诀: 业务服务用临时节点(挂了就摘,靠集群冗余兜底),中间件代理用持久节点(挂了要报警而不是盲摘)。还有个坑: K8s 里 Pod IP 会变,别用 IP 做持久节点的唯一标识,用 service name。

【面试题】3. Spring Cloud Gateway 的过滤器链是怎么工作的?自定义一个全局过滤器要踩哪些坑? ​

【标准答案】Gateway 的核心是过滤器链 = 路由匹配 + 过滤器链执行。请求来了 → RoutePredicateHandlerMapping 找匹配的路由 → 组装过滤器链 → 依次执行。过滤器分两种:GatewayFilter 作用于某个路由,GlobalFilter 全局生效。自定义全局过滤器实现 GlobalFilter 接口就行,核心逻辑在 filter() 方法里,记得调 chain.filter(exchange) 放行。三个常踩的坑: ① Order 值没设——过滤器执行顺序按 @Order 注解或 Ordered 接口决定,Netty 路由过滤器 Order 是 -1,你的自定义过滤器 Order 设大了可能在路由之后才执行,那时候 response 已经 commit 了——写 response body 会抛异常。② 缓存请求 Body——Gateway 里读取 exchange.getRequest().getBody() 后 Body 流就没了,下游拿不到。用 readBody() 配合 cachedRequestBodyObject 缓存。③ 不要把阻塞操作写在过滤器里——Gateway 是 Reactor 模型的,直接 Thread.sleep() 会阻塞事件循环线程,所有请求一起卡住。

【面试题】4. CAP 理论里 P 必须保证的前提下,怎么在 C 和 A 之间做取舍?给三个实际场景的选择。 ​

【标准答案】分布式系统里 P(分区容错)没得选——网络分区是客观存在的,你只能保证 P。剩下的就是在 C(一致性)和 A(可用性)之间取舍。场景一:银行转账——选 CP。 转账时宁可返回"系统繁忙请稍后"也不能出现钱扣了但对方没收到。用 ZK 做协调,Leader 挂了宁可等服务重选也不能乱写。场景二:微博热搜——选 AP。 你刷到的热搜榜差几条无所谓,但不能刷不出来。用 Nacos/Eureka 做注册中心,优先保证服务可用。场景三:电商下单——按环节区分。 下单价段选 AP(先让用户把钱付了),扣库存阶段选 CP(库存绝对不能超卖)。关键认知: CAP 不是全系统二选一,是按"操作粒度"做取舍。一个系统里可以同时有 CP 操作和 AP 操作,用 BASE 理论(基本可用 + 软状态 + 最终一致)来平衡。

【面试题】5. 分布式事务:TCC 和可靠消息最终一致性各适合什么场景?Seata 的 AT 模式又是什么定位? ​

【标准答案】三种方案解决不同问题,别拿 TCC 去实现一个"发短信通知"——杀鸡用牛刀。可靠消息最终一致性——适合"异步解耦"场景。 比如下单后发积分,订单服务扣库存成功后发一条 MQ 消息,积分服务消费消息加积分。核心要求:上游事务和发消息必须原子(RocketMQ 事务消息),下游必须幂等(消息可能重复消费)。优点是性能高、不阻塞,缺点是中间有一致性窗口期。TCC——适合"强一致要求 + 短事务"。 比如转账,Try 阶段冻结双方金额,Confirm 阶段扣减,Cancel 阶段解冻。三阶段都需要业务代码实现,成本高但一致性最强。Seata AT 模式——定位是"弱侵入的 TCC"。 它通过代理数据源自动生成 undo log,不需要你写 Try/Confirm/Cancel。适合不想改代码太多但要分布式事务的场景。但 AT 依赖数据库,性能不如前面两种。一句话: 异步用消息,同步强一致用 TCC,不想改代码用 Seata AT。

【面试题】6. 一致性哈希在微服务负载均衡里怎么用?跟普通哈希比解决了什么问题? ​

【标准答案】一致性哈希解决的是"加一台机器,缓存全炸"这个经典问题。普通哈希取模的问题: key 对节点数取模 hash(key) % N,加一个节点 N 从 3 变 4,90% 的 key 映射关系全变了——缓存命中和路由全崩。一致性哈希怎么做的: 把哈希值空间围成一个环(0 ~ 2^32-1),每个节点在环上占一个位置。请求的 key 哈希后落在环上,顺时针走遇到的第一个节点就是目标。加节点时,只有该节点"逆时针方向"上的一小段 key 受影响,其他不变。微服务里的实际应用: ① 网关层做会话保持——同一个用户 ID 哈希到同一台应用服务器。② 分布式缓存分片——一个 Redis Cluster 里 16384 个 slot 就是一致性哈希的变种。③ 带权重的玩法——给性能强的节点加虚拟节点,在环上多占几个位置,请求会自动偏向它。选型建议: 一致性哈希更适合"节点数会变化"的场景,如果节点数永远不变,普通哈希取模更简单直接。

【面试题】7. 雪花算法生成的 ID 真的是全局唯一吗?时钟回拨了怎么办? ​

【标准答案】雪花算法的 ID 结构:1 位符号位 + 41 位毫秒时间戳 + 10 位机器 ID + 12 位序列号。理论上单机每毫秒 4096 个 ID,但有两个致命问题。问题一:时钟回拨。 NTP 时间同步可能让服务器时钟往回跳,同一毫秒内可能生成重复 ID,这在天蝎座里是灾难。解决方案: ① 美团 Leaf 的做法——用 ZK 持久节点记录上一次的时间戳,发现时钟回拨就等,等到时间追上为止。等太久(超过 5ms)就报错。② 百度 UidGenerator 的做法——用数据库自增 ID 做 workerId 的分配,结合 RingBuffer 预生成 ID,不依赖时钟,彻底绕开了回拨问题。问题二:workerId 的分配。 10 位机器 ID 最多 1024 台机器,K8s 里 Pod 频繁重建,怎么保证 workerId 不冲突?用 ZK 或 Redis 做分布式自增分配,Pod 启动时申请一个 workerId,Pod 销毁时释放。结论:雪花算法在时钟回拨面前是脆弱的,生产环境优先用 Leaf 或 UidGenerator。

【面试题】8. 微服务链路追踪:SkyWalking 的 traceId 是怎么在跨服务调用时传递的?断链了怎么排查? ​

【标准答案】链路追踪的核心是"一个 traceId 串起所有调用"。SkyWalking 自动埋点原理: 通过 Java Agent 字节码增强,在 HTTP/gRPC/Dubbo 调用前自动注入 traceId 到 Header(比如 HTTP 的 sw8 头),下游服务的 Agent 从 Header 里提取 traceId 继续记录。不需要业务代码写任何东西。断链排查三步: ① 先看 SkyWalking UI 上链路是不是断在某个 Span 之后就没了——说明下游 Agent 没装上或没提取到 Header。② 确认下游服务的 -javaagent:skywalking-agent.jar 参数有没有配,serviceName 不能跟其他服务重复。③ 如果跨了非 HTTP 协议(比如直接 Socket 通信),Agent 埋不了——这种情况需要手动在代码里用 TraceContext.traceId() 显式传递,写在消息体里传过去。常见断链原因: 异步线程没传递 traceId、MQ 消费端没继承生产者的 traceId、自定义线程池没有包装 Runnable。SkyWalking 提供了 CallableWrapper 和 RunnableWrapper 来解决线程切换丢 traceId 的问题。

【面试题】9. 线上某个微服务 RT 突然从 50ms 飙到 3 秒,你怎么在 3 分钟内定位到根因? ​

【标准答案】面试官考的是你"不慌、有章法、能抓关键信号"。第一分钟——看三板斧数据: ① SkyWalking/APM 看调用链路,RT 是在哪个 Span 飙高的?是数据库查询慢了、调下游慢了、还是自身 GC?② 看该服务的 GC 日志——Full GC 频繁直接上 jstat -gcutil,老年代涨到 90% 大概率是内存泄漏。③ 看该服务的线程池——用 Arthas 的 thread -b 找阻塞线程,哪个线程在等什么锁?第二分钟——隔离确认范围: 如果 RT 全量飙是全局问题(比如 DB 慢查询影响所有请求),只影响某接口是局部问题(比如参数变异触发了慢 SQL)。看 MySQL 慢查询日志和连接数,连接池打满说明 DB 是瓶颈。第三分钟——止损优先: 定位的同时,该熔断的接口先熔断,该降级的逻辑先降级。用 Sentinel 的 @SentinelResource 一键切 fallback。黄金法则:先恢复、后分析。 用户不关心根因,用户只关心服务恢复没。在 Arthas 里留 watch 和 trace 命令的日志,服务恢复后再慢慢复盘。

【面试题】10. 一个初创团队的电商系统,从单体到微服务的演进路径怎么规划?你会分几个阶段?每个阶段的触发条件是什么? ​

【标准答案】不要一上来就微服务——微服务是结果,不是目标。阶段一:单体先行(0-10 万用户)。 一个 Spring Boot 打天下,所有模块在一个工程里,部署一个 jar 包。这个阶段的架构目标是"快速验证业务",不是技术炫技。代码里做好模块化,service 层按订单/商品/用户分包,为后续拆分铺路。触发条件: 日活过 10 万,某个模块(比如订单)的发布频率远超其他模块,每次改订单都牵一发动全身——开始拆分。阶段二:核心业务拆分(10-100 万用户)。 优先拆"改动最频繁"和"瓶颈最明显"的模块。通常是订单服务和商品服务先出来。订单和商品之间用 Feign 同步调用 + MQ 异步解耦。网关用 Gateway,注册中心上 Nacos。触发条件: 某个服务成了瓶颈,订单服务的数据库连接数被打满,其他服务受影响——继续拆。阶段三:全链路微服务(100 万+ 用户)。 引入配置中心、链路追踪、限流熔断、容器化部署。数据库按服务垂直拆分。全程红线: 团队不足 6 人的时候不要拆超过 5 个服务——运维成本会吃掉研发效率。一句话:微服务不是银弹,它解决的是"组织协作"问题,不是"技术炫技"问题。