大家好,今天分享一个在使用 redis lua 脚本过程中遇到的一个问题,问题不难,但是容易踩坑。
// 定义脚本资源
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(SCRIPT_PATH + scriptName)));
redisScript.setResultType(List.class);
// 预加载脚本(可选)
redisTemplate.getConnectionFactory().getClusterConnection().scriptLoad(redisScript.getScriptAsString().getBytes());
// 执行脚本
stringRedisTemplate.execute(redisScript, keys, args);
reidsTemplate 里持有一个 ScriptExecutor,最终的执行都代理给 ScriptExecutor,ScriptExecutor 会通过 evalsha 命令去 redis server 端执行脚本。
如果之前已经通过 script load 命令预加载了 lua 脚本,则 evalsha 会正常执行;如果没有事先加载脚本且第一次执行该脚本,则 evalsha 会返回 "NOSCRIPT No matching script. Please use EVAL." 异常,该异常会封装成 RedisSystemException 抛出。
捕获异常后,判断如果异常类型是 NonTransientDataAccessException,且异常信息里包含 "NOSCRIPT" 关键词,则再通过 eval 命令传递完整的脚本来执行一次,执行完之后会缓存脚本,以后每次调用只需通过 evalsha 命令传递 sha1 即可执行。
负责的项目中有一段 lua 脚本用来做短信发送频率的限流处理,服务部署到全新的一套环境后发现请求报错 "NOSCRIPT No matching script. Please use EVAL.",根据上述介绍,该错误表示 redis server 通过传递的 sha1 找不到相应的脚本。
该服务目前做法是没事先通过 script load 预加载脚本,是通过懒加载方式,由第一个请求去做加载操作。因为新的这套环境 redis 集群也是新搭建的,所以肯定是没缓存此脚本的,但是按照上述分析,第一个请求 evalsha 失败后是会执行 eval 的。
所以可以推断是异常类型不是 NonTransientDataAccessException,或者异常信息里没有包含 "NOSCRIPT" 关键词,导致异常直接抛出去了。
经过排查发现是前两周是接入了 sentinel-redis 流控功能引起的问题。
<dependency>
<groupId>com.xxx</groupId>
<artifactId>xxx-sentinel-spring-boot-starter-redis</artifactId>
<version>${xxx-sentinel.version}</version>
</dependency>
该模块会对每一个 redis 命令做拦截,然后通过 sentinel 流控 api 进行包裹。
实际命令是通过 method.invoke() 反射执行的。如果执行内部有异常,会抛出 InvocationTargetException。
综上,第一次执行 evalsha 命令抛出的 "NOSCRIPT" RedisSystemException 被包装成了 InvocationTargetException 异常,所以在此判断直接返回 false,导致异常直接抛出了,并没有执行后续的 eval 命令。
我们知道 redis 命令都是通过 RedisConnection 对象执行的,RedisConnection 是从 RedisConnectionFactory 中 get 的。
RedisConnectionFactory 一般有 jedis、lettuce 这两种实现。
通过 SpringBoot 自动装配装载进 Spring 容器的就是具体的 RedisConnectionFactory 实现。
所以我们可以通过拦截 RedisConnectionFactory 的 getConnection 方法得到 RedisConnection,然后对 RedisConnection 在进行一次代理,这样所有的 redis 命令就都能走到我们自己的拦截器里了。
回到主题,我们要怎么解决这个问题呢?
DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。
目前累计 4.3k star,欢迎大家试用,感谢你的 star,欢迎 pr,业务之余一起给开源贡献一份力量
gitee 地址:https://gitee.com/dromara/dynamic-tp
github 地址:https://github.com/dromara/dynamic-tp
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。