前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >lua执行redis脚本找不到脚本的问题

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

作者头像
用户7634691
发布2020-09-03 16:37:48
2.8K0
发布2020-09-03 16:37:48
举报

写在前面

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

有个项目,利用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});
    }
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题来了
  • 解决方案
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档