vlambda博客
学习文章列表

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


写在前面



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



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

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

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应用层的代码是这样写的:

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}); }

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

问题来了


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


日志如下:

error:redis.clients.jedis.exceptions.JedisNoScriptException: NOSCRIPT No matching script. Please use EVAL...

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


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

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

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



比如,现在有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脚本找不到脚本的问题


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

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

luaSha = redisService.scriptLoad(luaScript, key);

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

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

解决方案


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


优化后的代码如下:

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}); }