性能文章>RedisTemplate调lua踩了个坑>

RedisTemplate调lua踩了个坑原创

1年前
300644

概述

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了

 

点赞收藏
bit01
请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
4
4