昨天去面了一家做高并发电商的公司,P7岗。面试官没问那些背背八股文就能过的API,反而盯着 分布式环境下的边缘Case穷追猛打。

特别是ThreadLocal和缓存同步这几个点,要是没实战过,真能被问得当场自闭。

好在我复盘过项目的这些深坑,这波极限对答,建议大家反复研读!
在这里插入图片描述


第一回合:ThreadLocal 里的“幽灵”数据

面试官看着我的鉴权代码,眉头一皱:“你用 ThreadLocal 存用户信息,方便是方便,但你想过线程复用的问题吗?”

(心里一惊) 来了,这是在考Tomcat线程池机制!

我立刻坐直:“务必在拦截器的 afterCompletion 方法中强制调用 remove(),这是防止内存泄漏和脏读的最终防线。

Tomcat 处理请求是基于线程池的。

如果线程 1 处理完用户 A 的请求,没清理 ThreadLocal,紧接着复用去处理用户 B 的请求。

这时候用户 B 就能读到用户 A 的 Session 信息,这就是严重的数据串扰(串号)

面试官:“只在 postHandle 清理行不行?”

绝对不行!

如果 Controller 抛异常了,postHandle 根本不会执行,只有 afterCompletion 是类似 finally 块的存在。

🎯 标准代码示范

// 拦截器实现 HandlerInterceptor
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(...) {
        // 1. 获取用户信息存入 ThreadLocal
        User user = userService.getUser(ticket);
        HostHolder.setUser(user);
        return true;
    }

    @Override
    public void afterCompletion(...) {
        // 🔥 关键点:请求结束必须清理!
        // 否则Tomcat线程池复用时,下个请求会读取到脏数据
        HostHolder.clear(); 
    }
}

// HostHolder 工具类
public class HostHolder {
    private static ThreadLocal<User> users = new ThreadLocal<>();
    public static void clear() {
        users.remove(); // 防止内存泄漏的核心
    }
}

面试官微微点头:“算你基础扎实,那我们聊聊分布式的。”


第二回合:本地缓存怎么这就“裂开”了?

面试官继续追问:“我看你用了 Caffeine 做本地缓存。如果节点 A 更新了数据,节点 B 的缓存还是旧的,用户看到的界面不就乱套了?”

这就问到了分布式缓存的一致性痛点。

我:“利用 Redis 的 Pub/Sub(发布订阅)机制!当某节点修改数据时发布广播,通知集群内所有其他节点。

单机缓存快是快,但没有上帝视角。

我们要构建一个简单的消息总线,一旦有数据变更,大吼一声“删数据啦”,大家一起动手。

面试官:“具体流程是怎样的?”

💡 实现技巧:广播清除

其他节点监听到消息后,立即清除各自 Caffeine 中的对应 Key,从而保证最终一致性。

// 🎯 更新数据的业务逻辑
public void updatePost(int postId) {
    // 1. 更新数据库
    postMapper.update(postId);
    
    // 2. 清除Redis(二级缓存)
    redisTemplate.delete(getKey(postId));
    
    // 3. 🔥 发送广播通知其他节点清除本地缓存
    // Channel: "cache_evict_topic"
    redisTemplate.convertAndSend("cache_evict_topic", postId);
}

// 🎯 消息监听器 (运行在所有节点)
public class CacheMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 收到通知,解析postId
        int postId = Integer.parseInt(message.toString());
        
        // 🧹 清除当前节点的本地缓存
        caffeineCache.invalidate(postId);
        log.info("节点收到广播,已清除本地缓存: " + postId);
    }
}

这种方案比引入 ZooKeeper 轻量得多,对于读多写少的社区场景简直完美。


第三回合:ES挂了,数据就丢了?

面试官:“最后一个问题。Kafka 消费消息去写入 Elasticsearch 建索引,如果 ES 突然挂了,或者网络抖动,你的消息怎么办?”

(推眼镜) 这是考系统容灾和降级思维。

我:“写入侧走死信队列(DLQ)补偿,读取侧实施 MySQL 降级策略,优先保障核心业务可用性。

绝对不能直接 catch 异常打印个日志就完了,那是对用户数据的不负责。

面试官:“如果 ES 宕机一整天呢?”

🔥 进阶方案:双重保障

  1. 写不进去? 扔进死信队列,等 ES 恢复了写脚本批量重放。
  2. 搜不出来? 自动切换回 MySQL 模糊查询(虽然慢点,但比报错强)。
// 🎯 Kafka消费者:写入ES
@KafkaListener(topics = TOPIC_PUBLISH)
public void handlePublishMessage(ConsumerRecord record) {
    try {
        // 尝试写入ES
        elasticService.savePost(post);
    } catch (Exception e) {
        // ❌ ES挂了,不要无限重试阻塞消费者
        log.error("ES写入失败,转入死信队列");
        
        // 🔥 发送到死信Topic,后续人工或脚本补偿
        kafkaTemplate.send(TOPIC_DLQ, record.value());
    }
}

// 🎯 搜索业务层:降级查询
public Page<Post> searchPosts(String keyword) {
    try {
        // 1. 优先查ES (高性能)
        return elasticService.search(keyword);
    } catch (Exception e) {
        // 💡 降级策略:ES挂了查DB (保可用)
        log.warn("ES服务不可用,降级为数据库模糊查询");
        return postMapper.selectByTitleLike("%" + keyword + "%");
    }
}

面试官:“可以,既考虑了数据不丢失,又考虑了用户体验。明天来谈薪资吧。”


📚 知识点复盘

在这里插入图片描述

这场面试其实就盯着**“不一致”“不可靠”**这两个点。

大家在做项目时,千万别只顾着堆中间件,要多想想这些极端情况:

  • 📚 ThreadLocal:一定要在 afterCompletionremove(),否则 Tomcat 线程池会教你做人。
  • 📚 本地缓存同步:Redis Pub/Sub 是最轻量的集群通知方案,别搞太复杂。
  • 📚 ES可靠性:写入靠 死信队列 兜底,读取靠 DB降级 续命。

💡 最后一句

技术没有完美的架构,只有最适合业务的权衡。能把这些“坑”填平,你就是资深开发!

兄弟们,这波干货怎么样?评论区懂的打个 666! 👇

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐