当前位置:首页 > 问答 > 正文

Redis 空指针异常:Redis自增操作报空指针问题

Redis | 空指针异常:Redis自增操作报空指针问题

场景引入

最近在开发一个电商平台的秒杀功能时,遇到了一个让人头疼的问题:系统在高峰期频繁报出空指针异常(NullPointerException),而且偏偏出现在Redis的自增操作上,明明代码里已经做了判空处理,为什么还会出现这种问题?更奇怪的是,这个错误并不是每次都会出现,而是在并发量突然增大的时候才会偶尔蹦出来。

经过一番排查,终于找到了问题的根源,如果你也遇到过类似的情况,或者想提前避坑,不妨看看这篇文章。


问题现象

先来看一段典型的代码:

public Long increaseStock(String productId) {
    String key = "stock:" + productId;
    // 尝试从Redis中自增库存
    Long currentStock = redisTemplate.opsForValue().increment(key, 1);
    if (currentStock == null) {
        // 如果返回null,可能是key不存在,尝试初始化
        redisTemplate.opsForValue().setIfAbsent(key, 0L);
        currentStock = redisTemplate.opsForValue().increment(key, 1);
    }
    return currentStock;
}

看起来逻辑很严谨:先尝试自增,如果返回null,就初始化key再自增,在高并发场景下,这段代码仍然可能抛出空指针异常!

Redis 空指针异常:Redis自增操作报空指针问题

错误日志如下:

java.lang.NullPointerException: null
    at com.example.service.StockService.increaseStock(StockService.java:42)

问题原因

Redis的INCR命令特性

Redis的INCR命令在正常情况下不会返回null,但如果Redis连接异常(比如网络抖动、Redis服务短暂不可用),某些Redis客户端(如Lettuce或Jedis)可能会返回null,而不是抛出异常。

并发竞争条件

在高并发场景下,多个线程可能同时发现key不存在,并同时执行setIfAbsent,虽然Redis的SETNX是原子的,但setIfAbsent后立即increment的操作并不是原子的,仍然可能导致竞争问题。

Spring RedisTemplate的封装问题

RedisTemplate在某些情况下(如序列化配置不当)也可能导致返回null,尤其是在使用自定义序列化器时。

Redis 空指针异常:Redis自增操作报空指针问题


解决方案

方案1:使用Lua脚本保证原子性

最稳妥的方式是使用Lua脚本,将判断key是否存在 + 初始化 + 自增作为一个原子操作执行:

private static final String INCREMENT_SCRIPT =
    "local current = redis.call('incr', KEYS[1])\n" +
    "if current == 1 then\n" +  // 如果是第一次自增,说明key刚被创建
    "    redis.call('expire', KEYS[1], ARGV[1])\n" +  // 可选:设置过期时间
    "end\n" +
    "return current";
public Long safeIncrement(String key, long timeoutSeconds) {
    return redisTemplate.execute(
        new DefaultRedisScript<>(INCREMENT_SCRIPT, Long.class),
        Collections.singletonList(key),
        String.valueOf(timeoutSeconds)
    );
}

方案2:双重检查 + 锁

如果不想用Lua脚本,可以在代码层面加锁(比如分布式锁):

public Long safeIncrementWithLock(String key) {
    Long value = redisTemplate.opsForValue().increment(key, 1);
    if (value == null) {
        synchronized (this) {  // 分布式场景下可以用Redisson锁
            value = redisTemplate.opsForValue().increment(key, 1);
            if (value == null) {
                redisTemplate.opsForValue().setIfAbsent(key, 0L);
                value = redisTemplate.opsForValue().increment(key, 1);
            }
        }
    }
    return value;
}

方案3:初始化时预填充数据

如果可能,提前将所有可能的key初始化(比如在商品上架时写入Redis),避免运行时动态创建。


最佳实践

  1. 始终检查Redis返回值:即使文档说某个命令不会返回null,也要做好防御性编程。
  2. 优先使用Lua脚本:对于需要多个操作的场景,Lua脚本能保证原子性。
  3. 监控Redis连接:空指针可能是Redis连接问题的信号,建议监控连接池状态和网络延迟。
  4. 合理设置超时和重试:对于关键操作,可以结合Spring Retry等工具实现自动重试。

Redis的自增操作看似简单,但在高并发环境下可能会遇到各种边界情况,空指针异常往往只是表象,背后可能是竞争条件、连接问题或客户端行为不一致导致的,通过Lua脚本、双重检查或预初始化等方式,可以有效避免这类问题。

Redis 空指针异常:Redis自增操作报空指针问题

如果你的系统也依赖Redis的高并发操作,不妨检查一下相关代码,看看是否有类似的隐患!

发表评论