从源码发现Redisson的小问题

2,041 阅读2分钟

1、场景

redis版本:

org.springframework.boot spring-boot-starter-data-redis

redis框架:

org.redisson redisson-spring-boot-starter 3.11.2

redis操作工具类:

    public boolean setIfAbsent(String key, Object value, long time) {
        key = append(key);
        boolean flag = true;
        try {
            if (time > 0) {
                flag = redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
            } else {
                flag = setIfAbsent(key, value);
            }
        }  catch (Exception e) {
            throw new DispatchCenterException(REDIS_SET_IF_ABSENT_ERR, "key=" + key, e);
        }
        return flag;
    }

实际场景中我们使用的redission作为操作redis的框架,同时封装了对应的工具类,当我们使用时setIfAbset时控制台总是会出现空指针问题。问题截图如下:

image.png

通过异常堆栈可以看到是redis的问题,但是看代码并没有看出问题所以只能debug流程跟踪异常信息。在跟踪时就发现了最终的异常信息。

2、原因

    public boolean setIfAbsent(String key, Object value, long time) {
        key = append(key);
        //获取setIfAbsent的响应值时,封装的工具类中使用的是boolean,而不是Boolean
        boolean flag = true;
        try {
            if (time > 0) {
                flag = redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
            } else {
                flag = setIfAbsent(key, value);
            }
        }  catch (Exception e) {
            throw new DispatchCenterException(REDIS_SET_IF_ABSENT_ERR, "key=" + key, e);
        }
        return flag;
    }
    
   //看源码可以知道setIfAbsent响应值的类型是Boolean
   public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
        byte[] rawKey = this.rawKey(key);
        byte[] rawValue = this.rawValue(value);
        Expiration expiration = Expiration.from(timeout, unit);
        return (Boolean)this.execute((connection) -> {
            return connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent());
        }, true);
    }


    .
    .
    .

    public T getNow() {
        return this.promise.getNow();
    }


	//跟踪到最终生成响应参数的源码,可以看到通过一定的逻辑的判断给出最后的返回值,
	//但是这里通过三目表达式可以看到最终可能会返回null。
    public V getNow() {
        Object result = this.result;
        return !(result instanceof DefaultPromise.CauseHolder) && result != SUCCESS && result != UNCANCELLABLE ? result : null;
    }

场景一

幂等正常的redis请求,可以看到redis最终执行结束后result返回1,并且最终返回值的逻辑判断可以看到直接返回result。

image.png

image.png

场景二

幂等出现的场景,此时result是一个Object,并且是一个不可查看的Object,通过第二张图可以看到SUCESS的对象指向的内存地址和result指向的内存地址都是同一个对象,并且所有的逻辑判断最终结果为false,因此最终返回值为null,这样也就导致了如果接收参数不设置为Boolean而是设置为boolean,那么返回null之后进行拆箱时会出现空指针问题,不知道这是不是Redission的一个bug,因为正常的逻辑setIfAbsent如果redis中已经存在该key,那么redission应该返回false而不是返回一个null。

image.png

image.png

image.png

3、解决方案

将封装的redis工具类中的boolean改成包装类Boolean。

    public Boolean setIfAbsent(String key, Object value, long time) {
        key = append(key);
        Boolean flag = true;
        try {
            if (time > 0) {
                flag = redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
            } else {
                flag = setIfAbsent(key, value);
            }
        }  catch (Exception e) {
            throw new DispatchCenterException(REDIS_SET_IF_ABSENT_ERR, "key=" + key, e);
        }
        return Boolean.TRUE.equals(flag);
    }

4、总结

不知道这算不算是Redission的一个小bug,按照正常理解如果redis存在key,那么使用setIfAbsent时应该返回false或者0,返回null让我怎么都想不通.在GitHub提个issue问了,等待回复吧。