专栏首页犀牛饲养员的技术笔记lua执行redis脚本找不到脚本的问题

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上。

比如,现在有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命令进行脚本数据缓存,并且返回一个哈希码作为脚本的调用句柄,每次调用脚本只需要发送哈希码来调用即可。

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

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

本文分享自微信公众号 - 犀牛饲养员的技术笔记(coder_start_up),作者:siwuxie18

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 看看斯坦福大学是如何教学生编程的

    最近在看斯坦福大学的<<programming abstractions>>课程,觉得其中的一节课讲得特别好,大概50分钟左右的一节课的视频, 仅仅一节课,就...

    用户7634691
  • ES分页看这篇就够了

    我们使用mysql的时候经常遇到分页查询的场景,在mysql中使用limit关键字来实现分页。比如下面的示例。

    用户7634691
  • 给你总结几个ES下最容易踩的坑

    我本人接触Elasticsearch(一下简称ES)有挺长一段时间了,本文结合自己的一些项目经验,给你总结几个实际项目中比较容易踩到的坑。希望读者能够避免犯这样...

    用户7634691
  • “wget: 无法解析主机地址”的解决方法

    码农UP2U
  • 使用PHP+Redis实现延迟任务,实现自动取消订单功能

    简单定时任务解决方案:使用redis的keyspace notifications(键失效后通知事件) 需要注意此功能是在redis 2.8版本以后推出的,因此...

    砸漏
  • java学习与应用(4.7)--redis、maven和说明

    redis一款NOSQL(not only sql)非关系型数据库(键值对数据库)(对关系型数据库进行弥补),数据之前没有关联,存储在内存中更快(关系型数据库存...

    嘘、小点声
  • Redis 和 StrictRedis的差异

    在编写redis运维工具的时候,遇到使用python的redis模块获取key的ttl值时返回为空值,但是redis数据库中是有具体的值的。

    用户1278550
  • CentOS快速安装Redis

    名山丶深处
  • CentOS快速安装Redis

    名山丶深处
  • day72_淘淘商城项目_05_匠心笔记

    taotao-portal-web工程中,动态展示内容信息。 前端团队:负责JS,html等开发。 后端团队:负责后台的开发并提供数据给前端。

    黑泽君

扫码关注云+社区

领取腾讯云代金券