前言

后端系统里,Redis 基本是高频组件。

它通常被用来做:

  • 热点数据缓存;
  • 分布式锁;
  • 登录态存储;
  • 限流计数;
  • 排行榜;
  • 临时状态保存。

Redis 本身性能很高,但项目中真正容易出问题的地方,往往不是 Redis 慢,而是缓存使用方式不合理。

比如:

  • 缓存命中率突然下降;
  • 接口偶发变慢;
  • 数据库压力突然升高;
  • 缓存和数据库数据不一致;
  • 热点 key 过期后大量请求打到数据库;
  • 空数据被频繁查询;
  • 分布式锁没有正确释放;
  • 缓存过期时间设置不合理。

这些问题单看某一段代码,未必能马上定位。
这时候可以用 ChatGPT 5.5 辅助整理排查思路,把缓存问题拆成几个常见方向逐一验证。

本文以一个商品详情缓存问题为例,记录一次排查过程。


一、问题场景:商品详情接口偶发变慢

假设有一个商品详情接口:

http

GET /api/product/detail?id=10086

业务逻辑很常见:

  1. 先查 Redis 缓存;
  2. 缓存命中,直接返回;
  3. 缓存未命中,查询数据库;
  4. 查询结果写入 Redis;
  5. 返回商品详情。

代码简化如下:

java

public ProductDetailVO getProductDetail(Long productId) {    String cacheKey = "product:detail:" + productId;
    String cacheValue = redisTemplate.opsForValue().get(cacheKey);    if (StringUtils.hasText(cacheValue)) {        return JSON.parseObject(cacheValue, ProductDetailVO.class);    }
    Product product = productMapper.selectById(productId);    if (product == null) {        return null;    }
    ProductDetailVO vo = convert(product);
    redisTemplate.opsForValue().set(            cacheKey,            JSON.toJSONString(vo),            30,            TimeUnit.MINUTES    );
    return vo;}

最近测试反馈:

  • 大部分请求很快;
  • 偶尔接口会变慢;
  • 数据库 QPS 会出现短时间尖峰;
  • Redis 没有明显慢查询;
  • 商品详情接口在促销活动开始时更容易抖动。

接口日志类似:

text

2026-01-22 10:00:01.233 INFO  [product-service]GET /api/product/detail?id=10086 cost=28ms
2026-01-22 10:00:02.146 WARN  [product-service]GET /api/product/detail?id=10086 cost=1260ms
2026-01-22 10:00:02.152 WARN  [product-service]db query product detail cost=1188ms, productId=10086

从日志看,慢请求基本落在数据库查询上。
但问题是:为什么这些请求没有命中缓存?


二、先让 ChatGPT 5.5 帮忙整理问题边界

遇到缓存问题,不建议一上来就直接改代码。
因为缓存问题有很多类型:

  • 缓存穿透;
  • 缓存击穿;
  • 缓存雪崩;
  • 缓存更新不及时;
  • 缓存 key 设计不合理;
  • 过期时间设置不合理;
  • 序列化和反序列化异常;
  • Redis 连接池问题。

可以先把日志、代码和现象交给 ChatGPT 5.5,让它帮忙分类。

提问示例:

text

你是一名 Java 后端缓存问题排查助手。下面是商品详情接口的缓存代码、接口日志和问题现象。
请帮我完成:1. 整理已确认事实;2. 区分可能原因和暂不能确认的结论;3. 判断更像缓存穿透、缓存击穿还是缓存雪崩;4. 给出后续排查优先级;5. 输出适合研发排查记录使用。
【代码】粘贴代码
【日志】粘贴日志
【现象】大部分请求很快,偶发变慢,数据库 QPS 有短时间尖峰,促销活动开始时更明显。

可能得到类似输出:

markdown

## 已确认事实
1. 商品详情接口使用 Redis 缓存商品详情。2. 缓存 key 格式为 `product:detail:{productId}`。3. 缓存过期时间为 30 分钟。4. 慢请求中数据库查询耗时较高。5. Redis 本身没有明显慢查询。6. 活动开始时更容易出现接口抖动。
## 可能原因
1. 热点商品 key 过期后,大量请求同时未命中缓存,集中访问数据库。2. 不存在的商品 ID 没有缓存空值,可能导致缓存穿透。3. 大量商品缓存设置了相同过期时间,可能产生局部缓存雪崩。4. 缓存写入失败或序列化异常导致缓存一直未命中。5. 商品更新逻辑可能删除缓存后没有及时重建。6. Redis 连接池偶发等待,也可能导致整体耗时增加。
## 初步判断
根据“活动开始时热点商品更明显”“数据库 QPS 短时间尖峰”“单个商品详情请求偶发变慢”等现象,更优先怀疑缓存击穿。
但还不能排除缓存穿透和局部雪崩,需要补充缓存命中率、key 过期时间分布、请求商品 ID 分布等信息。

这个输出有一个好处:它不会只给一个结论,而是帮我们把排查范围压缩到几个方向。


三、缓存穿透、击穿、雪崩先分清楚

在继续排查前,可以先让 ChatGPT 5.5 帮忙整理概念。

1. 缓存穿透

缓存穿透通常指:请求的数据在缓存和数据库中都不存在。

例如:

http

GET /api/product/detail?id=-1GET /api/product/detail?id=999999999

如果代码是这样:

java

Product product = productMapper.selectById(productId);if (product == null) {    return null;}

那么不存在的商品不会写入缓存。
下次同样的非法 ID 再来,还是会查数据库。

如果大量这种请求打进来,数据库会被持续访问。

2. 缓存击穿

缓存击穿通常指:某个热点 key 过期的一瞬间,大量请求同时访问这个 key。

例如商品 10086 是活动页主推商品,访问量很高。
它的缓存刚好过期,此时几百个请求同时进来:

text

请求 1:缓存未命中,查数据库请求 2:缓存未命中,查数据库请求 3:缓存未命中,查数据库...

结果就是数据库瞬间被打高。

3. 缓存雪崩

缓存雪崩通常指:大量 key 在同一时间过期,导致请求集中打到数据库。

例如所有商品缓存都设置成固定 30 分钟:

java

redisTemplate.opsForValue().set(cacheKey, value, 30, TimeUnit.MINUTES);

如果这些缓存又是在同一批任务中集中写入的,那么 30 分钟后可能集中失效。


四、增加缓存命中日志

当前代码只记录了接口总耗时和数据库查询耗时,不能直接判断缓存是否命中。

可以先补充日志:

java

public ProductDetailVO getProductDetail(Long productId) {    String cacheKey = "product:detail:" + productId;    long start = System.currentTimeMillis();
    String cacheValue = redisTemplate.opsForValue().get(cacheKey);    if (StringUtils.hasText(cacheValue)) {        log.info("product detail cache hit, productId={}, cost={}ms",                productId, System.currentTimeMillis() - start);        return JSON.parseObject(cacheValue, ProductDetailVO.class);    }
    log.warn("product detail cache miss, productId={}", productId);
    long dbStart = System.currentTimeMillis();    Product product = productMapper.selectById(productId);    long dbCost = System.currentTimeMillis() - dbStart;
    if (product == null) {        log.warn("product not found, productId={}, dbCost={}ms", productId, dbCost);        return null;    }
    ProductDetailVO vo = convert(product);
    redisTemplate.opsForValue().set(            cacheKey,            JSON.toJSONString(vo),            30,            TimeUnit.MINUTES    );
    log.info("product detail load db success, productId={}, dbCost={}, totalCost={}",            productId, dbCost, System.currentTimeMillis() - start);
    return vo;}

补充后可以重点看几类日志:

text

product detail cache hitproduct detail cache missproduct not foundproduct detail load db success

然后统计:

  • 缓存命中率;
  • 哪些商品 ID miss 最多;
  • miss 是否集中在某个时间点;
  • miss 后数据库查询耗时;
  • 是否有大量不存在商品 ID。

如果发现某个热点商品在同一秒内出现大量 cache miss,基本就要重点看缓存击穿了。


五、用 ChatGPT 5.5 分析命中率数据

假设补充日志后,统计到下面的数据:

text

时间窗口:10:00:00 ~ 10:01:00
productId=10086总请求数:12000cache hit:10800cache miss:1200db query count:1200miss 集中时间:10:00:02 ~ 10:00:05
productId=10087总请求数:800cache hit:792cache miss:8
productId=999999999总请求数:3000cache hit:0cache miss:3000db query count:3000db result null:3000

可以继续问:

text

下面是商品详情接口的缓存命中统计,请帮我判断可能存在的问题。
要求:1. 分别分析 productId=10086、10087、999999999;2. 判断哪些更像缓存击穿,哪些更像缓存穿透;3. 给出对应优化方案;4. 不要只给结论,要说明判断依据。

可能得到这样的分析:

markdown

## productId=10086
现象:
- 请求量高;- miss 集中在 10:00:02 ~ 10:00:05;- miss 期间数据库查询次数达到 1200。
判断:
- 更像热点 key 缓存击穿。- 因为 miss 集中在短时间窗口,并且集中访问同一个热点商品。
优化方向:
- 对热点 key 使用互斥锁重建缓存;- 热点商品缓存适当延长过期时间;- 使用逻辑过期;- 活动开始前预热缓存。
## productId=10087
现象:
- 请求量不高;- cache hit 占比很高;- miss 数量少。
判断:
- 暂未体现明显缓存问题。- 少量 miss 可能是正常过期或首次加载。
## productId=999999999
现象:
- 请求量较高;- cache hit 为 0;- 数据库查询结果全部为 null。
判断:
- 更像缓存穿透。- 因为请求的数据在数据库中不存在,并且空结果没有写入缓存。
优化方向:
- 对空值进行短时间缓存;- 增加参数校验;- 使用布隆过滤器过滤不存在 ID;- 对异常请求做限流。

这种分析比较适合放进排查文档里,方便和测试、运维、业务一起沟通。


六、解决缓存穿透:缓存空值

当前代码中,如果商品不存在,直接返回:

java

if (product == null) {    return null;}

这会导致不存在的数据永远不进入缓存。

可以改成缓存空值:

java

private static final String EMPTY_CACHE_VALUE = "EMPTY";
public ProductDetailVO getProductDetail(Long productId) {    String cacheKey = "product:detail:" + productId;
    String cacheValue = redisTemplate.opsForValue().get(cacheKey);    if (StringUtils.hasText(cacheValue)) {        if (EMPTY_CACHE_VALUE.equals(cacheValue)) {            return null;        }        return JSON.parseObject(cacheValue, ProductDetailVO.class);    }
    Product product = productMapper.selectById(productId);    if (product == null) {        redisTemplate.opsForValue().set(                cacheKey,                EMPTY_CACHE_VALUE,                2,                TimeUnit.MINUTES        );        return null;    }
    ProductDetailVO vo = convert(product);
    redisTemplate.opsForValue().set(            cacheKey,            JSON.toJSONString(vo),            30,            TimeUnit.MINUTES    );
    return vo;}

这里需要注意两点。

1. 空值缓存时间不要太长

比如设置 1 到 5 分钟即可。

如果空值缓存太久,可能出现这种情况:

  1. 用户先查询商品 20001;
  2. 数据库中暂时没有;
  3. 系统缓存了空值;
  4. 后台新增商品 20001;
  5. 用户仍然读到空值缓存。

所以空值缓存一般只作为防穿透手段,不建议时间过长。

2. 仍然需要参数校验

缓存空值不是万能的。

如果商品 ID 明显非法,可以在入口直接拦截:

java

if (productId == null || productId <= 0) {    throw new BizException("商品 ID 不合法");}

对于恶意请求,还需要结合限流、黑名单、网关规则等手段。


七、解决缓存击穿:互斥锁重建缓存

对于热点商品,如果缓存失效后大量请求同时查数据库,可以加一个互斥锁。

核心思路是:

  • 第一个拿到锁的请求去查数据库并重建缓存;
  • 其他请求短暂等待后重试读取缓存;
  • 避免大量请求同时打到数据库。

示例代码:

java

public ProductDetailVO getProductDetail(Long productId) {    String cacheKey = "product:detail:" + productId;    String lockKey = "lock:product:detail:" + productId;
    String cacheValue = redisTemplate.opsForValue().get(cacheKey);    if (StringUtils.hasText(cacheValue)) {        if (EMPTY_CACHE_VALUE.equals(cacheValue)) {            return null;        }        return JSON.parseObject(cacheValue, ProductDetailVO.class);    }
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(            lockKey,            "1",            5,            TimeUnit.SECONDS    );
    if (Boolean.TRUE.equals(locked)) {        try {            // 双重检查,避免拿锁期间其他线程已经重建缓存            cacheValue = redisTemplate.opsForValue().get(cacheKey);            if (StringUtils.hasText(cacheValue)) {                if (EMPTY_CACHE_VALUE.equals(cacheValue)) {                    return null;                }                return JSON.parseObject(cacheValue, ProductDetailVO.class);            }
            Product product = productMapper.selectById(productId);            if (product == null) {                redisTemplate.opsForValue().set(                        cacheKey,                        EMPTY_CACHE_VALUE,                        2,                        TimeUnit.MINUTES                );                return null;            }
            ProductDetailVO vo = convert(product);            redisTemplate.opsForValue().set(                    cacheKey,                    JSON.toJSONString(vo),                    30,                    TimeUnit.MINUTES            );
            return vo;        } finally {            redisTemplate.delete(lockKey);        }    }
    // 没拿到锁,短暂等待后重试读缓存    try {        Thread.sleep(50);    } catch (InterruptedException e) {        Thread.currentThread().interrupt();    }
    cacheValue = redisTemplate.opsForValue().get(cacheKey);    if (StringUtils.hasText(cacheValue)) {        if (EMPTY_CACHE_VALUE.equals(cacheValue)) {            return null;        }        return JSON.parseObject(cacheValue, ProductDetailVO.class);    }
    // 兜底:仍未读到缓存,可以返回降级结果,或少量请求查库    return null;}

这段代码只是演示思路,实际项目中还要注意:

  • 锁过期时间不能太短;
  • 查询数据库耗时不能超过锁过期时间太多;
  • 删除锁时要避免误删别人的锁;
  • 高并发下不要让大量线程长时间 sleep;
  • 可以封装成通用缓存模板。

更严谨的锁释放方式,是给锁设置唯一值,删除时校验 value。
如果业务复杂,可以考虑 Redisson。


八、解决缓存雪崩:过期时间加随机值

当前缓存写法是固定 30 分钟:

java

redisTemplate.opsForValue().set(        cacheKey,        JSON.toJSONString(vo),        30,        TimeUnit.MINUTES);

如果大量商品在同一时间被加载,它们的过期时间也会接近。
30 分钟后,就可能集中失效。

可以给过期时间加随机值:

java

int baseExpireMinutes = 30;int randomMinutes = ThreadLocalRandom.current().nextInt(1, 10);
redisTemplate.opsForValue().set(        cacheKey,        JSON.toJSONString(vo),        baseExpireMinutes + randomMinutes,        TimeUnit.MINUTES);

这样可以把缓存过期时间打散,减少同一时间大量 key 失效的概率。

如果是活动商品,还可以在活动开始前做缓存预热:

java

public void warmUpActivityProducts(List<Long> productIds) {    for (Long productId : productIds) {        Product product = productMapper.selectById(productId);        if (product == null) {            continue;        }
        ProductDetailVO vo = convert(product);        String cacheKey = "product:detail:" + productId;
        redisTemplate.opsForValue().set(                cacheKey,                JSON.toJSONString(vo),                2,                TimeUnit.HOURS        );    }}

预热时也要注意控制速度,不要预热任务本身把数据库打满。


九、排查缓存和数据库不一致

缓存问题不只是命中率,也包括数据一致性。

比如商品价格更新后,用户看到的还是旧价格。

常见更新代码可能是这样:

java

public void updateProduct(ProductUpdateRequest request) {    Product product = new Product();    product.setId(request.getProductId());    product.setName(request.getName());    product.setPrice(request.getPrice());
    productMapper.updateById(product);
    String cacheKey = "product:detail:" + request.getProductId();    redisTemplate.delete(cacheKey);}

这属于比较常见的策略:先更新数据库,再删除缓存。

但仍然要关注几个问题:

1. 删除缓存失败怎么办

如果数据库更新成功,但 Redis 删除失败,旧缓存可能继续存在。

可以记录日志并进行补偿:

java

try {    redisTemplate.delete(cacheKey);} catch (Exception e) {    log.error("delete product cache failed, productId={}", request.getProductId(), e);    cacheDeleteRetryProducer.send(request.getProductId());}

2. 是否需要延迟双删

在一些并发读写场景下,可以考虑延迟双删:

java

public void updateProduct(ProductUpdateRequest request) {    String cacheKey = "product:detail:" + request.getProductId();
    redisTemplate.delete(cacheKey);
    productMapper.updateById(buildProduct(request));
    redisTemplate.delete(cacheKey);
    cacheDeleteDelayProducer.send(request.getProductId(), 1000);}

但延迟双删不是银弹。
它会增加复杂度,也不能保证所有场景绝对一致。

更重要的是先明确业务要求:

  • 商品详情允许几秒延迟吗?
  • 价格是否必须强一致?
  • 活动库存是否能走缓存?
  • 哪些字段可以缓存,哪些字段必须实时查?

不同业务答案不一样,方案也不一样。


十、让 ChatGPT 5.5 生成缓存问题排查清单

最后可以让 ChatGPT 5.5 帮忙输出一份 checklist。

提问示例:

text

请基于商品详情接口缓存问题,生成一份 Redis 缓存排查清单。
要求:1. 包含缓存穿透、击穿、雪崩、数据不一致;2. 每一项说明要看什么日志或指标;3. 每一项说明如何判断;4. 输出适合放到研发排查文档中。

可以整理成下面这样:

markdown

## Redis 缓存问题排查清单
### 1. 缓存命中率
- 看什么:cache hit、cache miss 统计。- 判断:如果 miss 比例突然升高,需要继续分析 miss 的 key 分布和时间分布。
### 2. 热点 key 是否击穿
- 看什么:同一个 key 是否在短时间内大量 miss。- 判断:如果某个热点 key 过期后大量请求同时查库,基本符合缓存击穿特征。
### 3. 是否存在缓存穿透
- 看什么:数据库返回 null 的请求数量。- 判断:如果大量请求查询不存在的数据,并且每次都查数据库,需要缓存空值或增加拦截。
### 4. 是否存在缓存雪崩
- 看什么:大量 key 是否在同一时间失效。- 判断:如果多个 key 同时 miss,并导致数据库 QPS 尖峰,需要检查过期时间分布。
### 5. Redis 本身是否慢
- 看什么:Redis slowlog、CPU、内存、网络、连接数。- 判断:如果 Redis 操作耗时升高,需要排查 Redis 负载,而不是只看业务代码。
### 6. 连接池是否不足
- 看什么:Redis 客户端连接池等待时间、活跃连接数、最大连接数。- 判断:如果业务线程等待连接时间较长,需要调整连接池或降低并发压力。
### 7. 缓存更新策略是否合理
- 看什么:更新数据库后是否删除缓存,删除失败是否有补偿。- 判断:如果数据库已更新但缓存未删除,可能导致读到旧数据。
### 8. 缓存 key 设计是否稳定
- 看什么:key 是否包含无关参数,是否存在大小写、空格、版本差异。- 判断:如果同一业务数据生成多个不同 key,会导致命中率下降。
### 9. 序列化是否异常
- 看什么:JSON 解析异常、字段兼容问题、类结构变更。- 判断:如果缓存存在但反序列化失败,可能被误判为缓存未命中。
### 10. 是否需要缓存预热
- 看什么:活动开始、首页推荐、热点商品等场景。- 判断:如果高峰前可预测热点数据,应考虑提前加载缓存。

这类清单适合团队复用。
后面遇到类似问题时,不用每次都从零开始排查。


十一、一个更稳妥的商品详情缓存模板

综合前面的内容,可以把商品详情缓存逻辑改成下面这种结构:

java

public ProductDetailVO getProductDetail(Long productId) {    if (productId == null || productId <= 0) {        throw new BizException("商品 ID 不合法");    }
    String cacheKey = "product:detail:" + productId;    String lockKey = "lock:product:detail:" + productId;
    // 1. 读缓存    ProductDetailVO cacheResult = getFromCache(cacheKey);    if (cacheResult != null) {        return cacheResult;    }
    // 2. 判断是否为空值缓存    String rawValue = redisTemplate.opsForValue().get(cacheKey);    if (EMPTY_CACHE_VALUE.equals(rawValue)) {        return null;    }
    // 3. 获取互斥锁    String lockValue = UUID.randomUUID().toString();    Boolean locked = redisTemplate.opsForValue().setIfAbsent(            lockKey,            lockValue,            5,            TimeUnit.SECONDS    );
    if (!Boolean.TRUE.equals(locked)) {        return retryReadCache(cacheKey);    }
    try {        // 4. 双重检查        ProductDetailVO doubleCheckResult = getFromCache(cacheKey);        if (doubleCheckResult != null) {            return doubleCheckResult;        }
        Product product = productMapper.selectById(productId);        if (product == null) {            redisTemplate.opsForValue().set(                    cacheKey,                    EMPTY_CACHE_VALUE,                    2,                    TimeUnit.MINUTES            );            return null;        }
        ProductDetailVO vo = convert(product);
        int expireMinutes = 30 + ThreadLocalRandom.current().nextInt(1, 10);        redisTemplate.opsForValue().set(                cacheKey,                JSON.toJSONString(vo),                expireMinutes,                TimeUnit.MINUTES        );
        return vo;    } finally {        releaseLock(lockKey, lockValue);    }}

这里省略了几个辅助方法,实际可以这样写:

java

private ProductDetailVO getFromCache(String cacheKey) {    String value = redisTemplate.opsForValue().get(cacheKey);    if (!StringUtils.hasText(value)) {        return null;    }
    if (EMPTY_CACHE_VALUE.equals(value)) {        return null;    }
    return JSON.parseObject(value, ProductDetailVO.class);}

java

private ProductDetailVO retryReadCache(String cacheKey) {    try {        Thread.sleep(50);    } catch (InterruptedException e) {        Thread.currentThread().interrupt();        return null;    }
    return getFromCache(cacheKey);}

java

private void releaseLock(String lockKey, String lockValue) {    String currentValue = redisTemplate.opsForValue().get(lockKey);    if (lockValue.equals(currentValue)) {        redisTemplate.delete(lockKey);    }}

不过需要提醒一点:
上面的代码适# 用 ChatGPT 5.5 辅助排查 Java 服务 CPU 飙高问题:从 top 到线程栈定位

前言

线上 Java 服务 CPU 飙高,是后端开发比较头疼的一类问题。

它和普通接口报错不太一样。接口报错通常还能从异常日志里看到堆栈,而 CPU 飙高时,日志里可能什么都没有,只能看到机器负载升高、接口响应变慢、告警不断触发。

常见现象包括:

  • 某台机器 CPU 使用率突然接近 100%;
  • 接口整体响应变慢;
  • 容器频繁被重启;
  • 日志没有明显异常;
  • 数据库、Redis 等外部依赖看起来正常;
  • 重启后短暂恢复,过一段时间又复现。

这类问题如果只凭经验猜,效率很低。
ChatGPT 5.5 在这里比较适合做辅助工作:帮我们整理排查步骤、解释线程栈、归纳可能原因、生成复盘模板。

本文以一个 Java 后端服务 CPU 飙高的场景为例,记录一次比较完整的排查思路。


一、问题场景:订单服务 CPU 突然打满

假设有一个订单服务 order-service,最近线上出现告警:

text

告警内容:order-service CPU 使用率超过 90%持续时间:5 分钟实例:order-service-2

机器上查看 CPU:

bash

top

可以看到:

text

PID     USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND23871   app       20   0   8.2g   2.1g   120m S  386.4  27.3   25:16.32 java

这说明 Java 进程 CPU 占用很高。

业务日志中只看到大量接口请求:

text

2026-01-22 10:15:31.122 INFO  [order-service] query order list, userId=100862026-01-22 10:15:31.125 INFO  [order-service] query order list, userId=100872026-01-22 10:15:31.128 INFO  [order-service] query order list, userId=10088

没有明显异常堆栈。

这时候不能简单认为是“请求量太大”。
CPU 飙高可能是正常流量导致,也可能是死循环、正则回溯、频繁 GC、序列化问题、锁竞争或者某段代码异常消耗 CPU。


二、先让 ChatGPT 5.5 整理排查路径

可以先把已知信息交给 ChatGPT 5.5:

text

你是一名 Java 后端线上问题排查助手。现在 order-service 出现 CPU 使用率超过 90% 的告警。
已知信息:1. Java 进程 CPU 占用 386%;2. 日志中没有明显异常;3. 接口响应变慢;4. 重启后短暂恢复,但过一段时间可能复现。
请帮我:1. 整理已确认事实;2. 区分可能原因和不能确认的结论;3. 给出排查优先级;4. 输出适合研发排查记录使用。

比较合理的输出会类似这样:

markdown

## 已确认事实
1. `order-service` 某实例 Java 进程 CPU 使用率明显升高。2. CPU 占用达到 386%,说明可能存在多个线程同时消耗 CPU。3. 当前日志中没有明显异常堆栈。4. 接口响应出现变慢。5. 重启可以短暂缓解,但不能说明问题已解决。
## 可能原因
1. 某些业务线程出现死循环或高频循环。2. 某段代码存在复杂计算或低效数据处理。3. 正则表达式出现灾难性回溯。4. 大量 JSON 序列化或反序列化消耗 CPU。5. GC 线程频繁工作导致 CPU 占用升高。6. 瞬时流量过高导致正常业务线程消耗 CPU。7. 线程池任务堆积后集中执行。8. 锁竞争或自旋导致 CPU 升高。
## 暂不能确认
1. 不能直接确认是流量问题。2. 不能直接确认是 GC 问题。3. 不能直接确认某个接口一定有问题。4. 不能通过重启恢复判断问题已经消失。
## 优先排查方向
1. 找到 Java 进程中最耗 CPU 的线程。2. 抓取线程栈。3. 将线程 ID 转换为十六进制后匹配堆栈。4. 判断高 CPU 线程正在执行什么代码。5. 结合接口日志和调用链确认触发入口。

这个阶段的重点不是让模型直接给答案,而是让它先帮我们把排查路径理顺。


三、定位最耗 CPU 的线程

已经知道 Java 进程 PID 是:

text

23871

接下来查看该进程下线程级别 CPU 占用:

bash

top -Hp 23871

可能看到:

text

PID     USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND23918   app       20   0   8.2g   2.1g   120m R  98.7  27.3   12:31.12 java23921   app       20   0   8.2g   2.1g   120m R  96.5  27.3   12:28.45 java23925   app       20   0   8.2g   2.1g   120m R  95.8  27.3   12:20.33 java23927   app       20   0   8.2g   2.1g   120m R  94.2  27.3   12:18.76 java

这里可以看到多个线程 CPU 都很高。

Java 线程栈中的线程 ID 通常是十六进制,需要把十进制线程 ID 转换一下。

例如:

bash

printf "%x\n" 23918

输出:

text

5d6e

再转换其他线程:

bash

printf "%x\n" 23921printf "%x\n" 23925printf "%x\n" 23927

得到:

text

5d715d755d77

然后抓取线程栈:

bash

jstack 23871 > jstack_23871.log

如果容器环境里没有 jstack,也可以尝试:

bash

jcmd 23871 Thread.print > thread_23871.log

接着在堆栈文件里搜索:

text

nid=0x5d6enid=0x5d71nid=0x5d75nid=0x5d77

四、把线程栈交给 ChatGPT 5.5 分析

假设搜索到的线程栈如下:

text

"pool-12-thread-3" #156 prio=5 os_prio=0 tid=0x00007f8c9401c000 nid=0x5d6e runnable [0x00007f8c6a7f8000]   java.lang.Thread.State: RUNNABLE        at java.util.regex.Pattern$Loop.match(Pattern.java:4895)        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4820)        at java.util.regex.Pattern$BranchConn.match(Pattern.java:4690)        at java.util.regex.Pattern$CharProperty.match(Pattern.java:3931)        at java.util.regex.Pattern$Branch.match(Pattern.java:4734)        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4789)        at java.util.regex.Pattern$Loop.match(Pattern.java:4895)        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4820)        at java.util.regex.Pattern.match(Pattern.java:3934)        at java.util.regex.Matcher.matches(Matcher.java:712)        at com.example.order.util.OrderNoValidator.validate(OrderNoValidator.java:38)        at com.example.order.service.OrderQueryService.queryList(OrderQueryService.java:91)        at com.example.order.controller.OrderController.list(OrderController.java:45)

这时可以这样问:

text

下面是 Java 服务 CPU 飙高时抓到的高 CPU 线程栈。
请帮我分析:1. 当前线程正在执行什么;2. 为什么可能导致 CPU 飙高;3. 还需要补充哪些信息;4. 给出修复建议;5. 不要只说“正则有问题”,需要解释判断依据。
【线程栈】粘贴线程栈

ChatGPT 5.5 可能会整理出:

markdown

## 线程栈分析
1. 当前线程处于 `RUNNABLE` 状态,说明正在占用 CPU 执行。2. 栈顶集中在 `java.util.regex.Pattern` 和 `Matcher.matches`。3. 业务代码入口位于 `OrderNoValidator.validate(OrderNoValidator.java:38)`。4. 调用链来自订单列表查询接口 `OrderController.list`。5. 高 CPU 线程可能正在执行订单号校验中的正则匹配。
## 为什么可能导致 CPU 飙高
1. Java 正则匹配本身是 CPU 密集型操作。2. 如果正则表达式存在嵌套量词、复杂分支或回溯路径,遇到特定输入可能产生大量回溯。3. 多个高 CPU 线程都卡在 `Pattern.match` 相关方法时,说明该路径可能被并发触发。4. 如果接口参数来自用户输入,异常长字符串或特殊字符串可能放大问题。
## 需要补充的信息
1. `OrderNoValidator.java:38` 的正则表达式。2. 触发请求中的订单号参数。3. 是否多个高 CPU 线程都在同一个正则调用点。4. 订单列表接口近期是否有异常搜索关键词。5. 是否存在爬虫或批量请求。

这个分析基本上就把重点指向了正则匹配。


五、检查业务代码中的正则

假设 OrderNoValidator 代码如下:

java

public class OrderNoValidator {
    private static final Pattern ORDER_NO_PATTERN =            Pattern.compile("^(\\d+)+$");
    public static boolean validate(String orderNo) {        if (orderNo == null) {            return false;        }        return ORDER_NO_PATTERN.matcher(orderNo).matches();    }}

这个正则看起来是想判断订单号是否全是数字。
但写法有问题:

text

^(\d+)+$

这里存在嵌套量词:

text

(\d+)+

当输入完全匹配时,可能还好;
但当输入是一个很长的数字串后面跟一个非数字字符时,比如:

text

111111111111111111111111111111111111111111111111111111a

正则引擎可能会尝试大量回溯,导致 CPU 飙高。

可以让 ChatGPT 5.5 解释这个问题:

text

下面这个 Java 正则是否可能导致灾难性回溯?
Pattern.compile("^(\\d+)+$")
如果可能,请说明原因,并给出更安全的写法。

比较合理的回答是:

markdown

该正则存在灾难性回溯风险。
原因:
1. `\d+` 本身表示一个或多个数字。2. 外层又使用 `+`,变成 `(\d+)+`。3. 同一段数字可以被拆分成多种匹配组合。4. 当字符串末尾出现不匹配字符时,正则引擎会尝试大量拆分方式。5. 输入越长,回溯次数可能越多,CPU 消耗会急剧增加。
更安全写法:
```javaPattern.compile("^\\d+$");

如果只允许固定长度订单号,例如 18 到 32 位:

java

Pattern.compile("^\\d{18,32}$");


因此,代码可以改成:
```javapublic class OrderNoValidator {
    private static final Pattern ORDER_NO_PATTERN =            Pattern.compile("^\\d{18,32}$");
    public static boolean validate(String orderNo) {        if (orderNo == null) {            return false;        }
        if (orderNo.length() < 18 || orderNo.length() > 32) {            return false;        }
        return ORDER_NO_PATTERN.matcher(orderNo).matches();    }}

这里增加长度判断也很重要。
不要让用户输入的任意长字符串直接进入正则匹配。


六、进一步确认是不是这个正则导致的

找到可疑点之后,还需要验证,而不是直接上线修改。

可以从几个角度确认。

1. 多个高 CPU 线程是否都在同一位置

如果多次 jstack 都看到类似堆栈:

text

java.util.regex.Patternjava.util.regex.Matcher.matchescom.example.order.util.OrderNoValidator.validate

说明这个位置高度可疑。

建议连续抓 3 次线程栈:

bash

jstack 23871 > jstack_1.logsleep 5jstack 23871 > jstack_2.logsleep 5jstack 23871 > jstack_3.log

如果同一批线程一直停留在正则匹配处,基本可以确认该代码路径消耗了大量 CPU。

2. 查询接口参数日志

检查订单列表接口是否收到异常参数:

text

orderNo=111111111111111111111111111111111111111111111111111111a

或者类似超长关键词:

text

keyword=999999999999999999999999999999999999999999999999999999999999x

如果参数明显异常,就要考虑:

  • 是否有人误操作;
  • 是否压测脚本传参异常;
  • 是否被爬虫或攻击流量触发;
  • 是否接口缺少参数长度限制。

3. 本地构造复现用例

可以写一个简单测试:

java

public class RegexTest {
    public static void main(String[] args) {        Pattern badPattern = Pattern.compile("^(\\d+)+$");        Pattern goodPattern = Pattern.compile("^\\d+$");
        String input = "111111111111111111111111111111111111111111111111111111111111a";
        long start1 = System.currentTimeMillis();        boolean r1 = badPattern.matcher(input).matches();        long end1 = System.currentTimeMillis();
        long start2 = System.currentTimeMillis();        boolean r2 = goodPattern.matcher(input).matches();        long end2 = System.currentTimeMillis();
        System.out.println("bad result=" + r1 + ", cost=" + (end1 - start1) + "ms");        System.out.println("good result=" + r2 + ", cost=" + (end2 - start2) + "ms");    }}

如果坏正则耗时明显高于好正则,就能进一步证明问题。


七、CPU 飙高不一定都是正则,也要排除 GC

有时 CPU 飙高并不是业务线程,而是 GC 线程占用 CPU。

可以用下面命令查看 GC 情况:

bash

jstat -gcutil 23871 1000 10

示例输出:

text

  S0     S1     E      O      M     CCS    YGC   YGCT    FGC    FGCT     GCT  0.00  98.23  85.12  92.34  87.20  80.12  1234  45.67   12     8.91     54.58

重点看:

  • YGC 是否快速增长;
  • FGC 是否频繁增长;
  • GCT 是否持续增加;
  • 老年代 O 是否长期很高;
  • Full GC 是否导致服务明显卡顿。

如果高 CPU 线程栈里看到的是:

text

"G1 Conc#0" ..."GC Thread#0" ..."VM Thread" ...

并且 jstat 显示频繁 GC,那么方向就不是正则,而是内存压力或对象创建过多。

可以让 ChatGPT 5.5 帮忙解释 GC 指标:

text

下面是 jstat -gcutil 的输出,请帮我判断是否存在频繁 GC 风险,并说明还需要补充哪些信息。

但要注意,GC 分析最好结合:

  • GC 日志;
  • 堆内存大小;
  • 对象分配速率;
  • Full GC 原因;
  • 内存 dump;
  • 最近上线变更。

单看一段 jstat 输出,不能直接下最终结论。


八、修复方案:参数限制 + 正则优化 + 降级保护

如果最终确认是正则导致 CPU 飙高,可以从几个层面修复。

1. 优化正则表达式

将:

java

Pattern.compile("^(\\d+)+$");

改为:

java

Pattern.compile("^\\d+$");

如果订单号长度固定,更推荐:

java

Pattern.compile("^\\d{18,32}$");

2. 增加参数长度限制

不要让超长参数进入业务逻辑:

java

if (orderNo != null && orderNo.length() > 32) {    throw new BizException("订单号长度不合法");}

对于搜索关键词也应该有限制:

java

if (keyword != null && keyword.length() > 50) {    throw new BizException("搜索关键词过长");}

3. 接口层增加参数校验

例如请求对象:

java

public class OrderQueryRequest {
    @Size(max = 32, message = "订单号长度不能超过32位")    private String orderNo;
    @Size(max = 50, message = "关键词长度不能超过50位")    private String keyword;
    private Integer status;
    private Integer pageNo;
    private Integer pageSize;}

Controller 中开启校验:

java

@GetMapping("/list")public Result<PageResult<OrderVO>> list(@Valid OrderQueryRequest request) {    return Result.success(orderQueryService.queryList(request));}

4. 网关层限制异常请求

如果发现大量异常参数来自同一来源,可以考虑:

  • IP 限流;
  • 用户维度限流;
  • 参数长度限制;
  • WAF 规则;
  • 爬虫识别;
  • 黑名单策略。

5. 增加监控和告警

建议补充以下监控:

  • 接口 QPS;
  • 接口 P95/P99 耗时;
  • JVM CPU 使用率;
  • 线程数;
  • GC 次数和耗时;
  • 请求参数异常次数;
  • 线程池队列长度。

九、让 ChatGPT 5.5 生成复盘文档

问题修复后,可以让模型帮忙生成一份复盘初稿。

提问示例:

text

请根据下面信息生成一份线上 CPU 飙高问题复盘文档。
要求:1. 包含问题现象、影响范围、排查过程、根因、修复方案、后续改进;2. 语气客观,不甩锅;3. 适合研发团队内部复盘。
【问题现象】order-service 某实例 CPU 超过 90%,接口响应变慢。
【排查过程】通过 top 定位 Java 进程,通过 top -Hp 定位高 CPU 线程,通过 jstack 匹配 nid,发现多个线程停留在 OrderNoValidator.validate 的正则匹配。
【根因】订单号校验正则使用 ^(\d+)+$,遇到超长异常输入时出现大量回溯。
【修复】改为 ^\d{18,32}$,增加参数长度校验和网关限流。

生成内容可以整理成:

markdown

## 问题现象
2026-01-22 10:15 左右,order-service 某实例出现 CPU 使用率持续超过 90% 的告警,部分订单列表接口响应变慢。
## 影响范围
影响订单列表查询接口的部分请求,主要集中在 order-service-2 实例。
## 排查过程
1. 通过 `top` 确认 Java 进程 CPU 占用异常。2. 通过 `top -Hp` 定位到多个高 CPU Java 线程。3. 将线程 ID 转换为十六进制后,在 `jstack` 文件中匹配对应线程。4. 发现多个线程处于 `RUNNABLE` 状态,且调用栈集中在 `java.util.regex.Pattern`。5. 继续向上追踪业务代码,定位到 `OrderNoValidator.validate` 方法。6. 检查代码发现订单号校验正则存在嵌套量词。7. 结合接口参数日志,确认异常长订单号参数触发了高耗时正则匹配。
## 根因分析
订单号校验使用了 `^(\d+)+$` 正则表达式。该表达式存在嵌套量词,在遇到长数字串后跟非数字字符的输入时,可能产生大量回溯,导致 CPU 被持续消耗。
## 修复方案
1. 将正则调整为 `^\d{18,32}$`。2. 在接口层增加订单号和关键词长度校验。3. 对异常参数请求增加日志记录。4. 在网关层增加参数长度限制和基础限流策略。
## 后续改进
1. 对项目中的正则表达式进行排查,重点关注嵌套量词。2. 完善 JVM CPU、线程数、GC、接口 P99 耗时监控。3. 对核心接口增加参数长度限制。4. 建立线上高 CPU 问题标准排查流程。

这类复盘文档不建议完全照搬模型输出,但可以作为初稿,节省整理时间。


十、CPU 飙高排查 checklist

最后整理一份比较实用的 checklist。

1. 确认进程

bash

top

确认是不是 Java 进程占用 CPU。

2. 查看线程级 CPU

bash

top -Hp <pid>

找到最耗 CPU 的线程 ID。

3. 线程 ID 转十六进制

bash

printf "%x\n" <tid>

4. 抓线程栈

bash

jstack <pid> > jstack.log

或:

bash

jcmd <pid> Thread.print > thread.log

5. 匹配 nid

在堆栈文件中搜索:

text

nid=0x十六进制线程ID

6. 判断线程在做什么

重点看:

  • 是否在业务代码循环;
  • 是否在正则匹配;
  • 是否在 JSON 序列化;
  • 是否在加解密;
  • 是否在 GC;
  • 是否在锁竞争;
  • 是否在某个线程池任务中。

7. 连续抓多次栈

不要只看一次:

bash

jstack <pid> > jstack_1.logsleep 5jstack <pid> > jstack_2.logsleep 5jstack <pid> > jstack_3.log

如果多次都卡在同一位置,可疑程度更高。

8. 结合业务日志和监控

需要同时看:

  • 请求量是否突增;
  • 是否近期上线;
  • 是否有异常参数;
  • 是否某个接口 P99 升高;
  • 是否 GC 异常;
  • 是否线程池队列堆积。

总结

Java 服务 CPU 飙高,不能只靠重启解决。重启只能缓解现象,不能定位根因。

比较稳妥的排查路径是:

  1. 用 top 找到高 CPU 进程;
  2. 用 top -Hp 找到高 CPU 线程;
  3. 将线程 ID 转成十六进制;
  4. 用 jstack 或 jcmd 抓线程栈;
  5. 根据 nid 匹配具体线程;
  6. 分析线程栈中的业务代码位置;
  7. 结合日志、参数、监控进行验证;
  8. 修复代码并补充防护措施。

ChatGPT 5.5 在这个过程中更适合做辅助角色:
它可以帮我们解释线程栈、整理排查步骤、生成验证清单和复盘文档,但最终结论仍然要靠真实日志、线程栈和监控数据来确认。

更多推荐