Skip to content

调试一个诡异的并发Bug

问题描述

线上服务偶发 500 错误,错误率约 0.1%。日志显示 NullPointerException,但从代码逻辑上看,这个变量不可能为 null。

java
// 错误堆栈
java.lang.NullPointerException
    at com.example.service.OrderService.processOrder(OrderService.java:127)
    at com.example.controller.OrderController.createOrder(OrderController.java:45)

本地无法复现,只在高并发时偶发。

传统思路

  1. 盯着代码看 - 反复阅读,找不到问题
  2. 加大量日志 - 上线后等待复现,可能要等几天
  3. 猜测+验证 - 凭经验猜测可能的原因,逐个排除
  4. 求助同事 - 组织 Code Review 会议,多人一起看

预计耗时:数小时到数天

AI思维

第一步:整理上下文

把所有相关信息整理清楚,一次性提供给 AI:

我遇到一个生产环境的偶发问题,请帮我分析可能的原因。

## 错误信息
java.lang.NullPointerException
    at com.example.service.OrderService.processOrder(OrderService.java:127)

## 相关代码
```java
@Service
public class OrderService {

    @Autowired
    private UserCache userCache;

    public void processOrder(Order order) {
        // 第127行
        User user = userCache.get(order.getUserId());
        String username = user.getName(); // NPE发生在这里

        // ... 后续逻辑
    }
}

@Component
public class UserCache {
    private Map<Long, User> cache = new HashMap<>();

    public User get(Long userId) {
        if (!cache.containsKey(userId)) {
            User user = userRepository.findById(userId);
            cache.put(userId, user);
        }
        return cache.get(userId);
    }

    public void refresh() {
        cache.clear();
        // 异步重新加载
        loadAllUsers();
    }
}

上下文

  • 问题在高并发时更容易出现(QPS > 1000)
  • 有一个定时任务每5分钟刷新缓存
  • 最近没有代码变更
  • 错误率约 0.1%

请分析可能的原因,从最可能到最不可能排序。


### 第二步:AI 分析结果

AI(Claude)的回复:

> 这是一个典型的**并发安全问题**。我按可能性排序分析:
>
> **最可能的原因:HashMap 的线程安全问题**
>
> 你的 `UserCache` 使用了普通的 `HashMap`,这不是线程安全的。当 `refresh()` 方法清空缓存的同时,`get()` 方法可能正在执行,导致:
>
> 1. 线程A执行 `get()`,检查 `containsKey` 返回 false
> 2. 线程B执行 `refresh()`,调用 `cache.clear()`
> 3. 线程A从数据库加载用户,执行 `put()`
> 4. 线程A执行 `return cache.get(userId)` - 但此时线程B的 `clear()` 可能刚好执行完
>
> 更严重的是,HashMap 在并发修改时可能导致内部结构损坏,`get()` 返回 null 即使 key 存在。
>
> **修复方案:**
>
> ```java
> // 方案1:使用 ConcurrentHashMap
> private Map<Long, User> cache = new ConcurrentHashMap<>();
>
> // 方案2:使用读写锁(如果需要原子性刷新)
> private final ReadWriteLock lock = new ReentrantReadWriteLock();
> ```

### 第三步:验证假设

根据 AI 的分析,设计验证方案:

AI的分析很有说服力。请帮我设计一个验证方案,确认这确实是并发问题:

  1. 如何在本地复现这个问题?
  2. 如何在不影响线上的情况下验证?

AI 给出的验证方案:

```java
// 本地压测复现
@Test
void testConcurrencyIssue() throws InterruptedException {
    UserCache cache = new UserCache();
    ExecutorService executor = Executors.newFixedThreadPool(100);
    AtomicInteger nullCount = new AtomicInteger();

    // 模拟并发读写
    for (int i = 0; i < 10000; i++) {
        executor.submit(() -> {
            User user = cache.get(1L);
            if (user == null) {
                nullCount.incrementAndGet();
            }
        });
        if (i % 100 == 0) {
            executor.submit(() -> cache.refresh());
        }
    }

    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);

    System.out.println("Null count: " + nullCount.get());
    // 如果 nullCount > 0,说明存在并发问题
}

第四步:实施修复

验证确认是并发问题。请给我完整的修复代码,要求:
1. 使用 ConcurrentHashMap
2. 确保 refresh 操作的原子性
3. 不影响读取性能

最终修复代码:

java
@Component
public class UserCache {
    private volatile Map<Long, User> cache = new ConcurrentHashMap<>();

    public User get(Long userId) {
        return cache.computeIfAbsent(userId,
            id -> userRepository.findById(id));
    }

    public void refresh() {
        // 构建新缓存,然后原子替换
        Map<Long, User> newCache = new ConcurrentHashMap<>();
        loadAllUsers(newCache);
        this.cache = newCache;  // 原子替换引用
    }
}

结果对比

指标传统方式AI思维
定位时间数小时-数天10分钟
准确性依赖经验和运气系统性分析
验证方案可能想不全面完整的测试代码
修复质量可能引入新问题考虑边界情况

关键洞察

  1. 信息完整性:给 AI 足够的上下文(代码、错误、环境信息)
  2. 选对模型:并发问题需要深度推理,用 Claude Opus 或 Sonnet
  3. 验证思维:AI 的分析需要验证,让 AI 帮你设计验证方案
  4. 迭代优化:修复后让 AI Review,避免引入新问题

延伸学习

Released under the MIT License.