正文
Redis lua脚本简要学习
背景
上周督促客户从Windows平台升级到了Linux平台.
redis一周相安无事.
但是这周一突然又出现了卡断和慢的情况.
只能继续进行分析.
分析思路
现场日志里面出现了大量的错误提示:
BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
不管用chatGPT后者是其他工具都可以查询出来:
Redis执行lua脚本时间太长了. 导致其他客户端无法获取结果集.
然后计划通过redis的slowlog 查询验证一下自己的想法
分析过程
我在自己的测试机器上面执行了 slowlog get 获取如下:
10) 1) (integer) 996
2) (integer) 1685314080
3) (integer) 10513
4) 1) "EVALSHA"
2) "7ae4b3e7f4bf3b0c3470baf33dd1b4941599b520"
3) "879"
发现最近十条慢命令全是id为: 7ae4b3e7f4bf3b0c3470baf33dd1b4941599b520 的lua脚本导致的.
督促现场抓紧获取指令:
现场反馈的图片与我这边一致
但是不太一样的是:
现场有四万个键值对耗时 接近 9 秒钟.
比我这边有非常大的差距.
我这边又让研发同事去另外一个大项目查询了下: 2万个参数. 执行时间为:58毫秒左右
389654
1685325926
57581
EVALSHA
7ae4b3e7f4bf3b0c3470baf33dd1b4941599b520
20855
基本上明确是此lua脚本导致的卡顿
问题再现以及缓解
网上查了一点简单资料, 准备做一下实验
在一个redis的客户端上面你执行命令
eval "while 0==0 do end" 0
然后在另外的redis客户端验证
redis-cli
get 1
get 2
会提示: "BUSY Redis is busy running a script."
然后执行: script KILL
再执行
get 1 就可以正常给出结果了.
说明问题基本上可以再现.
缓解方式: 如果不是一个很需要数据一致性的场景, 可以进行一下SCRIPT KILL
后期解决方式, 应该可以分组,每组不超过1万或者是1000 进行处理, 循环进行清理操作.
未解决的问题
翻找了一早上资料, 没有解决如何根据evalsha 的sha1值反向获取具体的lua脚本.
chatgpt给的结果就是垃圾, 一个get 一个list命令 script 根本没有这些参数.
没办法只能求助于开发:
private final static DefaultRedisScript SessionExistRedisScript = new DefaultRedisScript<>(
"local expires = ''\n" +
"for i=1, #KEYS do\n" +
" if(redis.call('EXISTS','caf-session:sessions:'..KEYS[i]) == 0)\n" +
" then\n" +
" expires = expires..';'..KEYS[i]\n" +
" end\n" +
"end\n" +
"return expires", String.class
);
问题思考
前端时间刚整理了因为Redis的scan导致机器卡顿宕机的问题
今天早上在看ergonomics导致full GC无法提供服务的现象.
我突然感觉redis因为单线程的处理逻辑, 如果大家都需要同步的获取redis的结果
那么每次redis执行吗命令都相当于在做一次minor GC
如果是一个slowlog. 那么就相当于所有连接着一个redis的所有应用服务器一起在做STW.的full GC
所以从时间链上分析. 任何single point 都需要非常严格的重视.
所以研发写的那一套lua脚本非常值得研究与修改.
lua脚本的优点
(1) 减少网络开销: 在Redis操作需求需要向Redis发送5次请求,
而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
(2) 原子操作: Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
(3) 复用: 客户端发送的脚本会永久存储在Redis中,
这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。
(4) 速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能;
对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。
关于lua脚本的继续学习
前端时间想学习 openresty 使用lua脚本.
但是非常可耻的立了flag没有学习深入下去.
所以发现还是遇到问题督促学习更好.
需要拥抱问题, 不能害怕问题, 通过问题来学习和提高自己.
lua脚本的基础知识
lua脚本是解释性的语言, 跟python和shell 都比较类似.
print("hello 济南小老虎!")
就是一个最简单的 hello world的脚本.
数据类型:
nil,布尔,浮点数,字符串,函数,线程,关联数组(table等)
循环判断
while( true )
do
print("测试循环")
os.execute("sleep " .. 2)
end
Redis里面eval等学习
eval 其实一个非常不安全的命令
很多一句话木马基本上都是基于这个吗精灵来做的
在js,php,等语言里面都有存在.
redis 里面 eval 是用于调用lua脚本的命令.
eval以及script脚本可以实现很多lua脚本的调用和使用.
学习和举例如下:
script load "while (true) do end"
会返回一个hash值如下:
127.0.0.1:6379> script load "while (true) do end"
"f926d8b19784f2c8c63b04288b32cb8de66070ba"
我理解此处就像是数据库绑定变量后可以避免hard parse 的方式一样.
下次执行时可以使用
evalsha f926d8b19784f2c8c63b04288b32cb8de66070ba 0
注意需要加最后一个0 用于指代参数个数
此时就可以执行命令了
等效率执行
eval "while (true) do end" 0
# 注意执行此命令会阻塞产品的使用, 必须尽早执行kill处理.
# 因为redis默认的lua脚本超时时间是 5 seconds 所以只会kill 5s 以上的script.
# 举例如下
# 我执行删除操作. 其实一直没有返回, 因为还没到五秒
127.0.0.1:6379> script kill
OK
(2.23s)
# 执行 lua脚本的返回情况如下:
(error) ERR Error running script (call to f_f926d8b19784f2c8c63b04288b32cb8de66070ba): @user_script:1: Script killed by user with SCRIPT KILL...
(5.00s)