前言
redission是广泛使用的缓存客户端,api设计巧妙,使用方便。作为程序员,在使用redission的时候不但要熟悉API的使用,而且还要熟悉代码底层原理,这样才能在使用的时候才能更加的顺手,程序运行更加稳定。本文先介绍redis执行lua时的缓存问题,然后探究redission对这一问题的解决方案,最后做个总结,希望对大家有帮助。
一、问题引入
问题现象
某redis生产集群内存占用较大,导致性能下降
排查与分析
使用info命令查看,used_memory_lua占比偏高,无论怎么清理业务key,内存占比都无法降下来。与业务确认,每次都使用了load加载lua,导致命令都被缓存。临时调用script flush 清除了lua脚本。从redis官网资料看,这个问题很早就有,那怎么才能更优雅的解决呢?
二、原因和解决方案
2.1 原因
从官网资料看,每次执行eval都会缓存的原因的是为了节省宽带和计算资源,redis会计算lua内容的sha值,sha值是唯一的,用来识别不同脚本。
业务每次执行都使用eval,参数不同,内容也就不同,使得redis server缓存了很多脚本,这种用法显然没有理解lua的正确用法。
2.2、解决方案
既然原因很清楚了,那么解决方法显而易见,就是提前加载,获取sha值,再次使用的时候直接传sha值。但是这种方案有几个点需要注意:
1.redis server重启问题,由于lua是存储在内存里的,每次重启都会清空脚本,客户端需要重新加载。 2.集群问题,针对cluster模式,参数还要添加路由key,确保lua用到的数据都在同一个实例。
从上面两点可以看出,封装这个脚本执行功能还是有一定的复杂度。虽然可以自己开发一套解决方案,但是为了避免重复造轮子,调研了下业界常用的组件如resttemplate,jedis,其中只有redission client提供了解决方案。那么redission的流程和原理是什么,有没有解决的那几个加载问题点呢。
2.3 redission执行lua原理
redission client将set,map等巧妙的封装成了对象结构,通过对象去操作数据,script也不例外,调用lua的示例代码如下(使用前需要配置useScriptCache为true):
RScript script = redissonClient.getScript(StringCodec.INSTANCE);
List<Object> res = script.eval(RScript.Mode.READ_ONLY, "return {'1','2','3.3333','foo',nil,'bar'}", RScript.ReturnType.MULTI, Collections.emptyList());
RScript实现了RScriptAsync接口,我们首先以该interface的eval为方法入口,一层一层的探究方法调用。发现调用到了CommandAsyncExecutor的evalWriteAsync方法,CommandAsyncService实现了该接口的具体方法。最后我们重点看下该方法的实现(已删掉部分不重要的代码):
如上图所示,redission通过执行的特定返回信息(NOSCRIPT前缀),来判断是否需要重新加载脚本,相比每次通过scriptExist判断,减少了网络请求。 请求的路由问题是判断redis为集群的时候通过调用getNodeSource拼接的,路由计算代码如下:
这样redission通过key计算路由前缀,通过异常信息重新加载lua,基本满足了业务需求,因此一般的场景可以使用redission执行lua脚本。
三、总结
本文通过一个线上redis内存案例引入了lua脚本使用的问题。redis执行lua脚本需要提前加载,防止内存占用过大和减少网络传输。这里面需要注意redis重启和集群路由问题。然后探究了redission对这一问题的解决方案,最后确定使用redission加载lua脚本。希望本文给初级程序员提供个解决问题思路,探究问题原因,对比解决方法,然后根据原理等确定最后的方案,做到心中有数。