lua执行redis脚本找不到脚本的问题

2020-09-03 16:37:48 浏览数 (1)

写在前面

最近遇到了一个坑,给大家分享下。

有个项目,利用redis做统计功能。一向对性能追求极致的我怎么能随便写几条redis的统计语句就应付呢。于是我打算使用lua脚本把用到的几条redis指令封装一起,这样减少和redis的IO交互,还可以保证操作原子性。我为自己的聪明才智沾沾自喜。

脚本如下(下面并不是我项目中实际的脚本,做了一些修改,大家不用纠结语法和能否运行。不过不影响本文的分析):

代码语言:javascript复制
private final static String luaScript = 
            "redis.call('ZREMRANGEBYSCORE',KEYS[1],0,ARGV[1]);"  
            "redis.call('ZADD', KEYS[1], ARGV[3], ARGV[4]);"  
            "redis.call('EXPIRE',KEYS[1],ARGV[2]);"  
            "end;";

然后我的java应用层的代码是这样写的:

代码语言:javascript复制
private String luaSha;

private void runSha(String key, String expire, String score, string value) {
        if (luaSha == null) {
            luaSha = redisService.scriptLoad(luaScript, key);
        }
        redisService.evalsha(luaSha, 1, new String[]{key, expire, score, value});
    }

上面的代码我本地自测没有问题。于是自信满满的转给了测试小姐姐,我就开心的摸鱼去了。

问题来了

就在我专心致志的摸鱼的时候,测试小姐姐突然反馈,统计的结果和实际不符合,并且服务器上有一些错误日志。

日志如下:

代码语言:javascript复制
error:redis.clients.jedis.exceptions.JedisNoScriptException: NOSCRIPT No matching script. Please use EVAL
...

我看到日志的第一反应是,一定是redis配置问题,我本地测试过明明没有问题的。本着负责任的态度我还是去网上查了下这个报错。一查之后尴尬了,发现还真是自己考虑不周全。

要理解这个问题,先引出一个概念,就是redis集群里slot的概念。

使用redis-cluster集群部署Redis,redis-cluster把所有的物理节点映射到[0-16383]slot上。

比如,现在有3台Redis节点 ,分别给他们分配slot :

节点

集群slot

A

0~5000

B

5001~10000

C

10000~16383

有一个key要set到redis,先对key做hash计算然后mod 163838,比如结果是1000,那么这个key就会保存在A节点。读的时候也是一样的原理。

lua脚本有一种缓存机制。在redis集群中,为了避免重复发送脚本数据浪费网络资源,可以使用script load命令进行脚本数据缓存,并且返回一个哈希码作为脚本的调用句柄,每次调用脚本只需要发送哈希码来调用即可。

而这个脚本缓存有点像本地内存一样,需要每个节点都有缓存才可以,否则就会报上面的那个错误。那么节点上的缓存是什么加载的呢?就是下面这行代码:

代码语言:javascript复制
luaSha = redisService.scriptLoad(luaScript, key);

redis会首先根据key找到对应的slot,然后根据slot加载到对应节点上。

现在问题其实已经呼之欲出了,我们前面的java代码,只要luaSha != null就会去调用redis的evalhash执行脚本,但是因为key不是固定的(实际项目中这个key是用户id),所以有可能对应的节点上是没有脚本缓存的。

解决方案

了解了出错的原因,解决方案其实就很简单了。执行evalsha方法的时候,如果触发了JedisNoScriptException这个异常,就重新scriptLoad下脚本到缓存。这里还加了scriptExist再次检查下脚本是否存在,双重保险。

优化后的代码如下:

代码语言:javascript复制
private void runSha(String key, String expire, String score, string value) {
        if (luaSha == null) {
            luaSha = redisService.scriptLoad(luaScript, key);
        }
        try {
            redisService.evalsha(luaSha, 1, new String[]{key, expire, score, value});
        } catch (JedisNoScriptException e) {
            boolean scriptExist = redisService.scriptExist(luaSha, key);
            if (!scriptExist) {
                luaSha = redisService.scriptLoad(luaScript, key);
            }
            redisService.evalsha(luaSha, 1, new String[]{key, expire, score, value});
        } catch (Exception e) {
            log.error("redis eval sha error:", e);
        }

        redisService.evalsha(luaSha, 1, new String[]{key, expire, score, value});
    }

0 人点赞