前面的一篇文章《使用redisson的RMapCache的addAndGet方法报错ERR Error running script》中讲到的报错问题并提交了github issue,
https://github.com/redisson/redisson/issues/1628
提交issue后,最开始被认定为bug,后来经研发团队测试后确认是用法原因,反馈如下:
Dismissed.
In short, this is a setup error relates to the MapEncoder.
TL;DR
By default, redisson uses Jackson to encode the value and java's Long data type is not native to JSON as oppose to Integer, thus the encoded value is not a numeric value presented in Redis, instead, it is stored as a JSON string. On the other hand, the script invoked by addAndGet implicitly requires the value as a Lua numeric value. Therefore the script throws an error when the condition is not met.
The correct setup to use addAndGet should be as follows:
RMapCache<String, Long> map = redisson.getMapCache("testMap", StringCodec.INSTANCE);
map.put("test", 1L);
map.addAndGet("test", 1L);
出行本文提及的问题是因为redisson默认使用JsonJacksonCodec进行编码,在上面出错的代码中,如果先使用了fastPutIfAbsent、put等方法,在RMapCache不指定Codec的情况下默认使用JsonJacksonCodec,但是在addAndGet方法中,内部有一个方法:
org.redisson.RedissonMapCache.addAndGetOperationAsync(K, Number)
编码方式使用的是StringCodec,当RMapCache的值是Long类型的时候,JsonJacksonCodec和StringCodec的编码结果是不一样的,下面是测试代码:
@Test
public void codec() throws IOException{
JsonJacksonCodec ksonJacksonCodec = JsonJacksonCodec.INSTANCE;
Long l = 1L;
Integer i = 1;
ByteBuf buf = ksonJacksonCodec.getValueEncoder().encode(l);
System.out.println("JsonJacksonCodec编码Long后的ByteBuf:" + buf.toString());
System.out.println("解码结果:" + ksonJacksonCodec.getValueDecoder().decode(buf, null));
buf = ksonJacksonCodec.getValueEncoder().encode(i);
System.out.println("JsonJacksonCodec编码Integer后的ByteBuf:" + buf.toString());
System.out.println("解码结果:" + ksonJacksonCodec.getValueDecoder().decode(buf, null));
StringCodec stringCodec = StringCodec.INSTANCE;
buf = stringCodec.getValueEncoder().encode(l);
System.out.println("StringCodec编码Long后的ByteBuf:" + buf.toString());
System.out.println("解码结果:" + stringCodec.getValueDecoder().decode(buf, null));
buf = stringCodec.getValueEncoder().encode(i);
System.out.println("StringCodec编码Integer后的ByteBuf:" + buf.toString());
System.out.println("解码结果:" + stringCodec.getValueDecoder().decode(buf, null));
}
运行测试代码结果:
JsonJacksonCodec编码Long后的ByteBuf:PooledUnsafeDirectByteBuf(ridx: 0, widx: 20, cap: 256)
解码结果:1
JsonJacksonCodec编码Integer后的ByteBuf:PooledUnsafeDirectByteBuf(ridx: 0, widx: 1, cap: 256)
解码结果:1
StringCodec编码Long后的ByteBuf:PooledUnsafeDirectByteBuf(ridx: 0, widx: 1, cap: 256)
解码结果:1
StringCodec编码Integer后的ByteBuf:PooledUnsafeDirectByteBuf(ridx: 0, widx: 1, cap: 256)
解码结果:1
从运行结果来看redisson的JsonJacksonCodec对Long类型的数字1编码后占用20个字节,JsonJacksonCodec对Integer类型的数字1编码后占用1个字节,而StringCodec对Long和Integer类型的数字编码后都占一个字节,这还不能完全说明问题,我们来看看编码后存储到redis中的结果,下面是测试代码:
@Test
public void testCode(){
RBucket<Long> b1 = redissonClient.getBucket("testJsonJacksonCodecLong", JsonJacksonCodec.INSTANCE);
b1.set(1L);
RBucket<Integer> b2 = redissonClient.getBucket("testJsonJacksonCodecInteger", JsonJacksonCodec.INSTANCE);
b2.set(1);
RBucket<Long> b3 = redissonClient.getBucket("testStringCodecLong", StringCodec.INSTANCE);
b3.set(1L);
RBucket<Integer> b4 = redissonClient.getBucket("testStringCodecInteger", StringCodec.INSTANCE);
b4.set(1);
}
我们通过redis的命令行看下这两种不同编码的数据是什么样的:
127.0.0.1:6390> get testJsonJacksonCodecLong
"[\"java.lang.Long\",1]"
127.0.0.1:6390> get testJsonJacksonCodecInteger
"1"
127.0.0.1:6390> get testStringCodecLong
"1"
127.0.0.1:6390> get testStringCodecInteger
"1"
结论是:JsonJacksonCodec对Long类型数据编码后存储到redis中不再是数字字符串,而对Integer编码后任然是数字字符串;StringCodec对Long和Integer编码后存储的值都是数字字符串。
由于redisson的RMapCache的addAndGet方法内部使用的是redis的“HINCRBYFLOAT”命令,这个命令是这样描述的:
当以下任意一个条件发生时,返回一个错误:
域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型)
域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number)
所以通过上文的所有分析,当使用RMapCache的时候,如果在addAndGet方法之前调用了put、fastPutIfAbsent等方法,如果未指定编码器,默认使用Jackson进行编码,当值是Long类型时,再后续调用addAndGet方法将不能把field当前的值解释为双精度浮点数,也不能正确解码,所以会出错。解决这个问题的方法就是在获取RMapCache的时候指定编码方式为StringCodec,后续的addAndGet就能正确执行。