RedisTemplate调lua踩了个坑原创
概述
RedisTemplate的Serializer将不同的数据类型进行序列化时,内置了一套逻辑,譬如Long类型的追回“L”,Byte的追加“B”。这就可能与redis期望的数据不一致,而引发错误
背景
要实现 次数/统计周期 的效果,可以通过incr和expire命令来实现:
1、根据访问者ip生成Redis key
2、执行incr命令,将key对应的数字+1
3、执行incr命令执行结果为1时【当前统计周期内第一次访问】,使用expire命令将key的过期时间设置为统计周期
4、根据incr的结果判断是否超出访问频率
看看上面的线程安全问题,会有啥影响
不过,上面的实现方案,在并发时,会有线程安全问题。【没get的小伙伴,可以留言讨论】
然后,查看了redis的文档时看到了建议的解法:
Lua脚本实现自增并过期
https://redis.io/commands/incr/
local current
current = redis.call("incr",KEYS[1])
if current == 1 then
redis.call("expire",KEYS[1],1)
end
问题描述
使用RedisTemplate执行上面的lua脚本是可以的。
但是有个问题:key的过期时间hard code在lua脚本中了。
统计需要根据情况动态调整的。
把 统计周期 作为参数传入时,却报错了:
ERR value is not an integer or out of range
show the code:
@SpringBootTest(classes = MainApplication.class)
@RunWith(SpringRunner.class)
public class RedisServiceImplTest {
@Autowired
private RedisRateLimitServiceImpl redisService;
@Test
public void callLua() throws InterruptedException {
String luaScript = "local current " +
"current = redis.call(\"incr\",KEYS[1]) " +
"if current == 1 then \n" +
" redis.call(\"expire\",KEYS[1],ARGV[1] ) \n" +
"end \n" +
"return current \n";
long expireTotalSeconds = 100;
for (int i = 0; i < 100; i++) {
Long currentTimes = redisService.callIncrLua(luaScript, Collections.singletonList("ipKey"), expireTotalSeconds);
int sleepSeconds = 10;
assertThat(currentTimes).isLessThanOrEqualTo(expireTotalSeconds / sleepSeconds + 1);
TimeUnit.SECONDS.sleep(sleepSeconds);
}
}
}
@Service
@Slf4j
public class RedisRateLimitServiceImpl implements RedisRateLimitService {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
@Override
public Long callIncrLua(String luaScript, List<String> keys, Object... args) {
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long currentTimes = redisTemplate.execute(script, keys, args);
String redisKey = keys.get(0);
log.info("redisKey {} currentTimes {} ", redisKey, currentTimes);
Long seconds = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS);
log.info("redisKey {} seconds {} ", redisKey, seconds);
return currentTimes;
}
}
为什么第一版的统计周期的值定义为long?
expire的入参是long,所以。。。
报错原因
RedisTemplate调lua时,传数字参数,数据类型需要是int。
将统计周期的数据类型改为int即可
原因分析
1、是不是redis调用Lua的玩法错了
不是的。直接在redis console中执行是Ok的
127.0.0.1:6379> script load 'local current current = redis.call("incr",KEYS[1]) if current == 1 then redis.call("expire",KEYS[1],ARGV[1]) end return current'
"0444141c52f5e7f61ade1c5c9463996e081c5a45"
127.0.0.1:6379> script exists 0444141c52f5e7f61ade1c5c9463996e081c5a45
1) (integer) 1
127.0.0.1:6379>
127.0.0.1:6379> EVALSHA 0444141c52f5e7f61ade1c5c9463996e081c5a45 1 ip1 1000
(integer) 1
127.0.0.1:6379> get ip1
"1"
127.0.0.1:6379> ttl ip1
(integer) 985
127.0.0.1:6379> ttl ip1
(integer) 982
127.0.0.1:6379>
没有问题
2、给Lua脚本传参的数据类型 错了?
应该是的。
过期时间Hard code时是Ok
报错了:ERR value is not an integer or out of range
直接通过redis的eval命令是Ok的。但是不管是int还是long,不都是数字?
3、redisTemplate在处理int、long时的逻辑不同?
是的。
ARGV的数据类型是long时,100传给redis时是100L【多了个L】
ARGV的数据类型是int时,100传给redis时是100【期望的值】
ASCII 49 48 48 76是什么?
4、redisTemplate处理long时为啥加了L?
与redisTemplate使用的Serializer有关。当前用的fastjson 1.2.83
out.write('L')
com.alibaba.fastjson.serializer.LongCodec#write
【我也很难,有的系统就是根据有没有L来区分是Long还是Int呢?】
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
/**
* https://github.com/alibaba/fastjson/wiki/%E5%9C%A8-Spring-%E4%B8%AD%E9%9B%86%E6%88%90-Fastjson#%E5%9C%A8-spring-data-redis-%E4%B8%AD%E9%9B%86%E6%88%90-fastjson
*/
GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
redisTemplate.setDefaultSerializer(genericFastJsonRedisSerializer);//设置默认的Serialize,包含 keySerializer & valueSerializer
/**
* 127.0.0.1:6379> keys *
* 1) "lua1"
* 2) "\"ip:127.0.0.1:1\""
* 3) "ip:127.0.0.1:1"
*
* 通过RedisTemplate执行命令:incr ip:127.0.0.1:1
* 如果KeySerializer为 new GenericFastJsonRedisSerializer()则redis中的key为"\"ip:127.0.0.1:1\""
* 如果KeySerializer为 new StringRedisSerializer(),则redis中的key为"ip:127.0.0.1:1"
*/
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
小结
1、redis执行Lua是ok的
2、不同的redis客户端在实现时,会有一些内置的区别
3、返回的错误信息也许就包含了答案。
譬如:ERR value is not an integer or out of range
100肯定没有out of range ,那只能是 not an integer了