Redis脚本使用总结:
Redis 2.6.0开始支持对lua脚本的解析运行,使用内置的lua解析器,就可以对lua脚本进行求值运算。至于lua本身详细的总结介绍,请留意后续关于lua专门的文章总结,这里只做lua在Redis中的使用分析总结介绍,并在结合前面文章的例子说明。
· 关于lua
· 几个命令
· 类型转换
· 通信协议
· 例子验证
· 注意事项
1、关于lua
参考博文《Lua安装及使用总结》
http://blog.csdn.net/why_2012_gogo/article/details/51298226
2、几个命令
在Redis中,单单只提供lua的解析是不够的,还需要除了解析编译之外的lua脚本的求值和逻辑处理功能,所以下面的EVAL及相关的命令是必须要知道的:
命令 | 作用 |
EVAL | 对缓存在服务器中的脚本进行求值。 |
EVALSHA | 根据SHA1校验码,对缓存在服务器中的脚本进行求值运算。 |
SCRIPT EXISTS | 根据脚本的校验码,检测指定的脚本是否存在于脚本缓存中。 |
SCRIPT FLUSH | 清除所有脚本缓存。 |
SCRIPT KILL | 关闭或杀死当前运行的脚本。 |
SCRIPT LOAD | 将脚本加载入脚本缓存,但不立即运行。 |
EVAL与EVALSHA区别:
EVAL命令会在每次执行脚本的时候都发送一次脚本主体,它不会每次都重新编译,但是很多时候它付出了无必要的带宽来传递主体;而EVALSHA命令,它的作用与EVAL相同,但是它解决上面EVAL的带宽消耗,也就是它接受的第一个参数不是脚本而是脚本的SHA1校验和SUM。
NOTE:
A、如果服务器还存在给定的 SHA1 校验和所指定的脚本,那么就执行这个脚本;如果服务器不存在给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误:提醒用户使用 EVAL 代替 EVALSHA。
B、KEYS和ARGV:
EVAL的第一个参数是Lua脚本程序,它运行在 Redis 服务器中;
EVAL的第二个参数是参数的个数,后面的参数(从第三个参数),表示在脚本中所用到的那些 Redis 键,这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,…);
在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1]、ARGV[2],…)。
例如:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 mykey
3、类型转换
在Redis中使用lua,那么讨论的数据类型转换自然就是在Redis和lua之间转换了。当Lua通过call()或pcall()函数执行Redis命令时,命令的返回结果被转换为Lua数据结构;而Lua在Redis内置解析器中运行时,Lua脚本返回值被转换为Redis协议,然后由EVAL将处理的结果返回给客户端处理(需要时,可直接参考转换)。
A、Redis与Lua转换对照表(存在对应关系)
Redis | Lua |
integer | number |
bulk | string |
multi bulk | table |
status | table中状态信息ok |
error | table中状态信息err |
nil bulk/multi bulk | false |
B、Lua到Redis换转对照表(不存在对应关系)
Lua | Redis |
true | 1 |
|
|
|
|
Lua中的boolean值true到 Redis中的integer值为1;
NOTE:
Lua中整数和浮点数之间没有区别。所以,我们始终将Lua的数字转换成整数的回复,这样将舍去小数部分。如果就是希望Lua返回一个浮点数,那么应该将它作为一个字符串,比如ZSCORE命令。
4、通信协议
Redis基于C/S通信机制,也就是需要在客户端和Redis服务端交互通信,而大多的C/S通信是需要通信协议的约束,而Redis与外部的通信也不例外。
A、\r\n结尾
客户端与服务端交互的每一个命令及数据都必须以\r\n结尾,这只是规定没有需要特别的说明。
B、通用格式
*<number of arguments>
$<number of bytes of argument 1>
<argument data>
…
$<number of bytes of argument n>
<argument data>
…
例如:
"*2\r\n$3\r\nSET\r\n$5\r\nmykey\r\n"
5、例子验证
1)例子一:
同步Mysql数据到Redis的sql脚本为例说明,这里使用了管道技术,目的只为提高效率,对于管道的内容可查看资料,推荐如下:
博文:《Redis管道技术使用总结》,
博客地址:http://blog.csdn.net/why_2012_gogo/article/details/51260264
A、.sql脚本
SELECT CONCAT(
'*14\r\n',
'$',LENGTH(redis_cmd),'\r\n',redis_cmd,'\r\n',
'$',LENGTH(redis_key),'\r\n',redis_key,'\r\n',
'$',LENGTH(id_key),'\r\n',id_key,'\r\n','$',LENGTH(id_val),'\r\n',id_val,'\r\n',
'$',LENGTH(account_key),'\r\n',account_key,'\r\n','$',LENGTH(account_val),'\r\n',account_val,'\r\n',
'$',LENGTH(password_key),'\r\n',password_key,'\r\n','$',LENGTH(password_val),'\r\n',password_val,'\r\n',
'$',LENGTH(nickname_key),'\r\n',nickname_key,'\r\n','$',LENGTH(nickname_val),'\r\n',nickname_val,'\r\n',
'$',LENGTH(email_key),'\r\n',email_key,'\r\n','$',LENGTH(email_val),'\r\n',email_val,'\r\n',
'$',LENGTH(address_key),'\r\n',address_key,'\r\n','$',LENGTH(address_val),'\r\n',address_val,'\r'
)
FROM(
SELECT
'HMSET' AS redis_cmd,CONCAT(account,password,'_hash') AS redis_key,
'id' AS id_key,id AS id_val,
'account' AS account_key,account AS account_val,
'password' AS password_key,password AS password_val,
'nickname' AS nickname_key,nickname AS nickname_val,
'email' AS email_key,email AS email_val,
'address' AS address_key,address AS address_val
FROM t_user_info
) AS t
B、如何运行
$mysql -h 127.0.0.1 -uroot –Dcwteam --skip-column-names --raw <
/redis/sql/mysql_to_redis.sql | redis-cli -eval
/redis/lua/redis_table_json.lua --pipe
NOTE:
.sql->实现查询内容Redis协议化,并组装获得数据集;
.lua->使用cjson依赖库,实现查询结果json格式化;
-eval->执行并计算lua脚本求值;
--pipe->代表启用了管道技术支持;
C、.lua脚本
for k,v in pairs(ok) do
for key,val in pairs(v) do
if key%2 == 0 then
tmp[v[key-1]] = v[key];
end
end
ret[k]=tmp;
end
ngx.say(cjson.encode(ret));
NOTE:
最后一行通过ngx.say()函数,调用cjson依赖库的encode()将前面的json数组转为json格式数据并返回给Redis服务端。
2)例子二:
利用Redis的高效的I/O特点,实现固定时间内,限制客户端访问服务端次数,目的是为了防止客户端非法刷新或恶意攻击网站等用途。
A、.lua脚本
local cells = redis.call('incr',KEYS[1])
if cells == 1 then
redis.call('expire',KEYS[1],ARGV[1])
end
if cells > tonumber(ARGV[2]) then
return 0
end
return 1
NOTE:
KEYS->获取键名参数,这里指incr的值;
ARGV->获取非键名参数,这里指访问的次数;
call()和pcall()区别:
call()和pcall()很类似,唯一的区别是redis命令执行结果返回错误时,redis.call()将返回给调用者一个错误;而pcall()会将捕获的错误以Lua表的形式返回。
B、如何运行
$redis-cli --eval /redis/lua/cell_limiting.lua cell_limiting:127.0.0.1, 5 2
NOTE:
--eval代表通知redis-cli使用eval命令调用脚本;
/redis/lua/cell_limiting.lua为.lua文件的位置;
cell_limiting:127.0.0.1代表模拟的访问ip地址动作;
5 2 代表5秒之内只允许访问2次;
结果:
正如上图所示,如果5秒内访问的次数小于等于2次,则返回1,否则返回0,捕获到0这个状态之后,我们就可以做出对应的解决办法了。
6、注意事项
A、全局变量
为了防止数据泄漏进Lua环境,Redis 脚本不允许创建全局变量。如果一个脚本需要在多次执行之间维持某种状态,应该使用Redis key来进行状态保存。如果试图在脚本中访问一个全局变量(不论这个变量是否存在)将引起脚本停止。
NOTE:
为了防止这个问题,这里有个好的建议:将脚本中用到的所有变量,都使用local关键字显式的声明为局部变量,这也是个好的习惯。
B、脚本缓存
Redis保证所有被运行过的脚本都会被永久保存在脚本缓存当中,当EVAL 命令在一个 Redis实例上成功执行某个脚本后,针对这个脚本的所有EVALSHA命令都会成功执行。
另外,刷新脚本缓存的唯一办法是显式调用SCRIPTFLUSH 命令,这个命令会清空运行过的所有脚本的缓存,通常只有在云计算环境中,Redis 实例被改作其他客户或者别的应用程序的实例时,才会执行这个命令。
缓存可以长时间储存而不产生内存问题的原因是,它们的体积非常小,而且数量也非常少,即使脚本在概念上类似于实现一个新命令,或者在一个大规模程序里有成百上千的脚本,即使这些脚本会经常修改,储存这些脚本的内存仍然是微不足道的。实际上,用户会发现 Redis 不移除缓存中的脚本是一个好的设计。因为对于一个和 Redis 保持持久化连接的程序来说,执行过一次的脚本会一直保留在内存中,因此它可以在管道中使用 EVALSHA 命令而不必担心因为找不到所需的脚本而产生错误。
好了,到这里已经介绍了Redis实际使用中的方方面面,如有新的发现和总结会在后续文章迭代更新博文,谢谢。
技术讨论群:
489451956(新)