写在前面
最近遇到了一个坑,给大家分享下。
有个项目,利用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});
}