乐趣区

关于redis:springdataredis中同时使用setgetincrement的问题

spring-data-redis 中同时应用 set()、get()、increment()的问题

1. 问题形容

最近开发代码,应用 redis 时,遇到一个有意思的问题,问题代码如下:

class Test {

        /**
         * redis 操作句柄
         */
        @Autowired
        private RedisTemplate redisTemplate;
        
        public void test() {
            // 先 set key1,再 get key1 没有问题,然而再 increment 就会报错
            redisTemplate.opsForValue().set("key1", 1);
            Integer val1 = redisTemplate.opsForValue().get("key1");
            Long incr1 = redisTemplate.opsForValue().increment("key1");
            
            // 先 increment key2(key2 之前不存在),再 get key2 就会报错
            Long incr2 = redisTemplate.opsForValue().increment("key2");
            Integer val2 = redisTemplate.opsForValue().get("key2");
        }
}

如代码正文形容:

  • 问题 1:先 set key1,再 get key1 没有问题,然而再 increment 就会报错,报错内容如下:
org.springframework.dao.InvalidDataAccessApiUsageException: ERR value is not an integer or out of range; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range
    at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:69) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:42) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
  • 问题 2:先 increment key2(key2 之前不存在),再 get key2 就会报错,报错内容如下:
org.springframework.data.redis.serializer.SerializationException: 反序列化对象失败; nested exception is com.caucho.hessian.io.HessianProtocolException: unknown code for readObject at 0x31 (1)
    at tech.joymo.framework.redis.sserializer.HessianSerializer.deserialize(HessianSerializer.java:61) ~[joymo-framework-redis-1.0-20210202.122946-5.jar:1.0-SNAPSHOT]
    at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:335) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:61) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]
    at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53) ~[spring-data-redis-2.3.6.RELEASE.jar:2.3.6.RELEASE]

2. 问题剖析

剖析下面的报错内容可知,两个问题都是 redis 存储的数据格式有问题导致,然而为什么会有这种问题呢?

查阅 spring 文档 (Spring Data Redis 10.8. Serializers) 可知,Spring 对 Redis 罕用序列化的策略有两种

Multiple implementations are available (including two that have been already mentioned in this documentation):

  • JdkSerializationRedisSerializer, which is used by default for RedisCache and RedisTemplate.
  • the StringRedisSerializer.

其中 RedisTemplate 的默认序列化策略是 JdkSerializationRedisSerializer,而StringRedisTemplate 的序列化策略是StringRedisSerializer

RedisTemplate 是应用的 JdkSerializationRedisSerializer 序列化,序列化后的值蕴含了对象信息,版本号,类信息等,是一串字符串,所以无奈进行数值自增操作。

而 StringRedisTemplate 序列化策略是字符串的值间接转为字节数组,所以存储到 redis 中是数值,所以能够进行自增操作。

所以,因为代码中注入的是 RedisTemplate 实现,应用了 JdkSerializationRedisSerializer 作为序列化办法,所以 set 和 get 的时候都会进行序列化和反序列化,而 increment 操作不会进行序列化,所以导致上述两个问题。

3. 解决办法

解决办法很简略,JdkSerializationRedisSerializer序列化导致的问题,那么将序列化换成 StringRedisSerializer 即可。代码如下,间接注入 RedisTemplate<String, String>StringRedisTemplate都能够取得 StringRedisTemplate 的实例,从而解决问题。

留神:StringRedisTemplate 的实例 value 只能为 String

class Test {

        /**
         * redis 操作句柄
         */
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
    
        /**
         * redis 操作句柄
         */
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        public void test() {
            // 先 set key1,再 get key1 没有问题,然而再 increment 就会报错
            redisTemplate.opsForValue().set("key1", "1");
            String val1 = redisTemplate.opsForValue().get("key1");
            String incr1 = redisTemplate.opsForValue().increment("key1");
            
            // 先 increment key2(key2 之前不存在),再 get key2 就会报错
            String incr2 = redisTemplate.opsForValue().increment("key2");
            String val2 = redisTemplate.opsForValue().get("key2");
        }
}

或者应用 redisTemplate 前设置序列化策略

留神:设置 value 的序列化工具的区别:

  1. new StringRedisSerializer(),value 只能为 String(如代码 1)
  2. new GenericToStringSerializer<>(Integer.class),依据入参的不同,能够设置不同类型的 value,但最初保留到 redis 时会转为 String,取出时,最初会转为设置的类型,较为不便(如代码 2)
  3. 留神 1 和 2 获取值是都必须要类型转换,这是因为不设置泛型时,默认出参类型为 Object,所以为了代码清晰明确,倡议设置 key 和 value 的类型,而且 idea 也不会高亮揭示(很丑)(如代码 3)

代码 1:

class Test {

        /**
         * redis 操作句柄
         */
        @Autowired
        private RedisTemplate redisTemplate;

        public void test() {redisTemplate.setValueSerializer(new StringRedisSerializer());
            // 先 set key1,再 get key1 没有问题,然而再 increment 就会报错
            redisTemplate.opsForValue().set("key1", "1");
            String val1 = (String) redisTemplate.opsForValue().get("key1");
            Long incr1 = redisTemplate.opsForValue().increment("key1");

            // 先 increment key2(key2 之前不存在),再 get key2 就会报错
            Long incr2 = redisTemplate.opsForValue().increment("key2");
            String val2 = (String) redisTemplate.opsForValue().get("key2");
        }
    }

代码 2:

    class Test {

        /**
         * redis 操作句柄
         */
        @Autowired
        private RedisTemplate redisTemplate;

        public void test() {redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
            // 先 set key1,再 get key1 没有问题,然而再 increment 就会报错
            redisTemplate.opsForValue().set("key1", 1);
            Integer val1 = (Integer) redisTemplate.opsForValue().get("key1");
            Long incr1 = redisTemplate.opsForValue().increment("key1");

            // 先 increment key2(key2 之前不存在),再 get key2 就会报错
            Long incr2 = redisTemplate.opsForValue().increment("key2");
            Integer val2 = (Integer) redisTemplate.opsForValue().get("key2");
        }
    }

代码 3:

class Test {

        /**
         * redis 操作句柄
         */
        @Autowired
        private RedisTemplate<String, Integer> redisTemplate;

        public void test() {redisTemplate.setValueSerializer(new StringRedisSerializer());
            // 先 set key1,再 get key1 没有问题,然而再 increment 就会报错
            redisTemplate.opsForValue().set("key1", 1);
            Integer val1 = redisTemplate.opsForValue().get("key1");
            Long incr1 = redisTemplate.opsForValue().increment("key1");

            // 先 increment key2(key2 之前不存在),再 get key2 就会报错
            Long incr2 = redisTemplate.opsForValue().increment("key2");
            Integer val2 = redisTemplate.opsForValue().get("key2");
        }
    }
退出移动版