RedisTemplate 使用及 Err 问题分析总结

介绍

有一个接口请求必须使用唯一 ID 进行请求,而该接口所属服务部署了多个。使用 Redistemplate 生成 dubbo 服务全局唯一 key 的时候,报了错误,并且这个错误不是 100% 复现。

提示无法将 value 进行自增,value 不是整数类型。

1
2
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

代码中利用 Redistemplate 生成唯一 ID 使用的方法:

1
2
3
4
@Autowired
private RedisTemplate redisTemplate;

Long uuid = redisTemplate.opsForValue().increment(REDIS_UUID,1);

最终定位问题在 RedisTemplate 序列化 的问题上,程序初始化的时候没有指定 value 的序列化方式,默认序列化的 value 并非整型类型,导致获取自增唯一 ID 异常。

基本配置与使用

先以 SpringBoot 2.2.0 项目为基础,介绍 RedisTemplate 的基础用法

  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 参数配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # Redis数据库索引(默认为0)
    spring.redis.database=0
    # Redis服务器地址
    spring.redis.host=localhost
    # Redis服务器连接端口
    spring.redis.port=6379
    # Redis服务器连接密码(默认为空)
    spring.redis.password=
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.jedis.pool.max-active=8
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.jedis.pool.max-wait=-1
    # 连接池中的最大空闲连接
    spring.redis.jedis.pool.max-idle=8
    # 连接池中的最小空闲连接
    spring.redis.jedis.pool.min-idle=0
    # 连接超时时间(毫秒)
    spring.redis.timeout=20
  3. 单元测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.example.demo;

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.util.Assert;

    @SpringBootTest
    class DemoApplicationTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test(){
    stringRedisTemplate.opsForValue().set("aaa","111");
    Assert.state("111".equals(stringRedisTemplate.opsForValue().get("aaa")),"not equals");
    }

    }

序列化方式

RedisTemplate 可以指定 key、value 的序列化方式,默认使用 JdkSerializationRedisSerializer。如果不指定键值对的序列化方式,可能会影响程序的正常运行。

RedisTemplate 可指定的序列化方式:

  • JdkSerializationRedisSerializer,JDK 提供的序列化功能,需要序列化的类需要实现 Serializable 接口

  • StringRedisSerializer,默认使用 UTF-8 编码格式将 String 转为字节数组

  • Jackson2JsonRedisSerializer,使用 Jackson 将对象序列化为 JSON,初始化的时候指定对象类型

  • GenericToStringSerializer,使用带类型转换的方式,将 String 转为字节数组

  • GenericJackson2JsonRedisSerializer,使用带类型转换的方式,将对象转为 JSON

  • OxmSerializer,XML 序列化

  • ByteArrayRedisSerializer,将 Byte 数组原样保存

序列化实例

  • JdkSerializationRedisSerializer,默认情况下,key,value 都是以二进制形式保存,保存的键值对不再是程序中定义好的类型。

  • 默认情况带有很多转义前缀

1
2
3
4
5
6
7
@Test
public void test1(){
// 默认的序列化方式
redisTemplate.opsForValue().set("default1",1);
redisTemplate.opsForValue().set("default2","1");
redisTemplate.opsForValue().set("default3","Answer");
}

upload successful


  • StringRedisSerializer
    • key、value 必须是 String 类型,否则会抛出 ClassCastException
1
2
3
4
5
6
7
8
9
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
   // 指定 key value 为 String 序列化方式
   redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.opsForValue().set("string1",1); // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
redisTemplate.opsForValue().set("string3","Answer");
User user = new User(18,"Answer");
redisTemplate.opsForValue().set("string4",user.toString());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
输出字符串
本地:9>get string3
"Answer"

输出对象,但是看不到属性信息
本地:9>get string4
"com.example.demo.User@5c77ba8f"


// 重写 User 的 toString 方法
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}

重写之后能看到属性信息
本地:9>get string4
"User{age=18, name='Answer'}"

  • Jackson2JsonRedisSerializer,实体类不重写 toString 也能够获取对应属性信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void test3(){
// 对象转 JSON 序列化方式,需要添加 jackson-databind
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class)
ObjectMapper om = new ObjectMapper();
final PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(validator,ObjectMapper.DefaultTyping.NON_FINAL);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.opsForValue().set("jackson1",1);
redisTemplate.opsForValue().set("jackson2","1");
redisTemplate.opsForValue().set("jackson3","Answer");
User user = new User(18,"Answer");
redisTemplate.opsForValue().set("jackson4",user);
}

String 类型 和 对象都能完整查看显示
本地:9>get jackson1
"1"
本地:9>get jackson2
""1""
本地:9>get jackson3
""Answer""
本地:9>get jackson4
"["com.example.demo.User",{"age":18,"name":"Answer"}]"

总结

  • 使用 Redistemplate 时候,注意键值对的序列化方式

  • 对象序列化建议使用 Jackson2JsonRedisSerializer,String 类型字符串使用 StringRedisSerializer

Spring Boot中使用Redis数据库