面灵AI→

智元机器人一面:TCC、缓存击穿与链路排查

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 个人介绍。
  2. 项目背景介绍。
  3. 说一下 TCC 的概念,以及你们为什么要用到 TCC?
  4. 为什么不选择使用分布式锁解决?
  5. 短 TTL 缓存的作用是啥?
  6. 缓存的数据同时失效,导致数据库压力激增,你有什么解决办法?
  7. 说一下南北流量和东西流量的区别?
  8. 东西链路中如果出现了较多的链路延迟 / 报错,怎么排查?
  9. 如果端上用户接口调用出现了很多报错,又该怎么排查?
  10. 你用到了 buffer channel 目的是啥?
  11. 分片区域故障隔离是做了哪些优化?
  12. 假设我的机器出现了 CPU 没有打满,但 GC 很频繁,这种是什么情况?
  13. 反问。

《参考解析》

TCC 与分布式锁:为什么选前者

TCC 是「Try-Confirm-Cancel」三段式补偿事务:Try 阶段做资源预留与业务校验(冻结库存、预扣额度、写中间状态),Confirm 阶段确认提交(真正扣减、把中间态置为完成),Cancel 阶段在任一环节失败时释放预留并回滚。它的价值在于全程没有长事务锁、不做数据库层面的两阶段提交,把「资源占用」这件事用业务字段显式表达,因而吞吐高、跨服务(跨库、跨语言)可用;代价是每个参与方都要实现三个接口,且必须做空回滚、幂等、悬挂这三类异常处理。空回滚指 Try 没执行成功但收到了 Cancel(要先判断 Try 是否发生过),幂等指 Confirm/Cancel 会被重复调用(用事务 ID 去重),悬挂指 Cancel 比 Try 先到(用状态机拒绝迟到的 Try)。

「为什么不用分布式锁」要从两者解决的问题不同来答:分布式锁解决的是「同一时刻只有一个执行者」,它保护的是临界区,不解决跨服务的状态一致性——用锁把整个下单流程串起来会让持锁时间等于业务耗时,吞吐骤降、锁超时与续期带来新风险,而且一旦某个服务提交后失败,锁早已释放,脏状态依然留存。TCC 解决的是「多个资源要么都成、要么都退回」,与并发控制不是一回事,实践中两者会叠加使用:用锁或唯一索引防并发重复提交,用 TCC 或本地消息表保证跨服务最终一致。答题时把「锁管并发、事务管状态」这句话说出来,通常就到位了。

短 TTL 缓存与缓存同时失效

短 TTL 缓存(几秒到几十秒的过期时间)的用途是扛瞬时热点与防击穿:对同一个 key 的并发请求只让一个打到下游,其余用这份短命但足够新的值返回;也可以用它在时间窗口内做幂等或去重(同一请求号在 10 秒内只处理一次),以及做本地与远端之间的近似一致性视图。它的取舍是「新鲜度换稳定性」——允许极短时间内的旧数据,换来下游不被突发流量打穿。

「缓存同时失效导致数据库压力激增」就是缓存雪崩,解决手段按层给:一是给过期时间加随机抖动,把同一时刻的集中失效摊开;二是逻辑过期 / 后台续期,缓存不设物理 TTL 而是存逻辑过期时间,读取时发现过期就由一个线程异步重建、其余请求先返回旧值;三是互斥重建(分布式锁或单飞 singleflight)保证只有一个请求回源,注意锁要有超时且失败要降级;四是多级缓存(本地 Caffeine + Redis),本地扛住绝大部分读;五是对数据库做保护,限流、连接池上限、以及缓存不命中时走降级逻辑(返回兜底数据或排队),保证 DB 不被压垮;六是热点 key 的分片与预热。要顺带区分三个近邻问题:击穿(单个热点 key 失效)用互斥重建 + 逻辑过期,穿透(查不存在的数据)用空值缓存 + 布隆过滤器 + 参数校验,雪崩(大面积同时失效)用抖动 + 多级缓存 + 限流。

南北流量与东西流量,以及两类故障的排查路径

南北向流量是客户端与数据中心之间的流量(用户到网关再到服务,跨机房、走公网或专线),特征是入口集中、受公网与网关能力约束;东西向流量是数据中心内部服务之间调用的流量(微服务互相调用、RPC、消息、副本同步),特征是链路长、跳数多、单次量小但总量大。这个区分决定了排查的抓手不同:南北问题先看入口(DNS、CDN、LB、网关限流、证书、地域网络),东西问题先看服务依赖图与单跳耗时。

东西链路延迟或报错变多时的排查顺序:先确认范围(哪些接口、哪些机房、哪个时间段、是否与某次发布重合),再用链路追踪按 P99 与 P50 分开看——P50 抬头说明单跳普遍变慢(下游依赖、资源瓶颈、序列化开销),只有 P99 抬头说明是长尾(慢查询、GC 停顿、个别实例、连接池等待、网络重传)。接着按依赖逐个切分:调一次依赖的耗时分布、是否有重试放大、熔断与超时配置是否合理(超时设置比下游的 P99 还小会人为制造失败)、线程池与连接池是否打满。指标上重点看四个黄金信号的对应物:流量、错误、延迟、饱和度。常见根因包括:连接池耗尽导致的排队、日志同步写盘、锁竞争、DNS 解析超时、实例冷启动被流量打进来、以及一次发布引入的 N+1 调用。

端上(客户端)接口大量报错的排查顺序不同,因为你要先区分「真错」还是「客户端感知到的错」:第一步看错误码分布与地域/运营商/版本维度,是全局还是集中在某个客户端版本(版本问题优先怀疑兼容性与灰度)、某个网络环境(运营商 DNS 劫持、弱网超时);第二步看服务端同一时间窗的真实成功率(如果服务端指标正常,问题在链路或客户端);第三步看客户端到服务端的路径(CDN/网关证书过期、跨域、限流返回、四层中断);第四步再看客户端自身的重试与降级逻辑有没有把一次失败放大成雪崩。要能说出「先分层定责,再往下追」的原则,而不是一上来就说去查服务端日志。

buffer channel、分片故障隔离与「GC 频繁但 CPU 不满」

Go 里的 buffered channel 与无缓冲 channel 的区别是「发送方是否会阻塞」:无缓冲 channel 要求发送与接收同时就绪,天然做同步点;带缓冲的 channel 允许发送方在缓冲未满时立即返回,作用是削峰与解耦——上游突发时不必等下游处理,代价是增加了内存与「消息在缓冲里还没落地」的不确定性风险。实际用途包括:把同步的日志/埋点写入异步化、控制并发度(用带缓冲的 channel 当信号量)、在 worker pool 里做任务分发、以及跨 goroutine 传递结果。要讲的取舍是缓冲区大小的选择依据(按峰值与处理速率估算,太大则内存与延迟堆积、太小则失去削峰意义),以及退出时必须关闭 channel 并处理「发送方已退出而接收方仍在等」的收尾问题,避免 goroutine 泄漏。

「分片区域故障隔离做了哪些优化」要按故障域来讲:把服务按区域/单元分片,每个单元内自成闭环(含自身的数据副本与依赖),单元之间不做同步强依赖,这样单区域故障只影响该区域的用户;路由层做就近接入与故障切换(健康检查、权重摘除、超时与熔断),并把「跨区域的强一致调用」改成异步或最终一致,避免一个区域的抖动把全局拖垮;数据层按分片键切分并做副本与降级读(降级到只读或缓存),关键路径要有兜底返回值。度量要看「单个单元宕掉影响多少比例的用户」以及切换时长(RTO)与数据丢失窗口(RPO)。

「CPU 没打满但 GC 很频繁」是一个很典型的诊断题。要点是:GC 频率由分配速率而非 CPU 使用率决定,所以先看分配速率与堆增长曲线,而不是先加 CPU。常见原因包括:对象分配过快(循环里创建临时对象、字符串拼接而不是 builder、装箱、日志参数提前求值)、堆太小导致 Minor GC 频繁(堆小则阈值低)、老年代增长快导致 Full GC(缓存无界、大对象、集合只增不减)、元空间或类加载问题、以及显式调用 System.gc()(可能来自框架或直接内存回收触发)。排查路径是打开 GC 日志与堆直方图,区分是 Minor 频繁(调堆或降分配)还是 Full 频繁(找内存泄漏或大对象),再配合火焰图找分配热点。答这题时明确说「先看分配速率与堆曲线」,会显得你真的排查过而不是背题。