redis的lua脚本快速入门
本文主要阐述四个方面:
使用redis的lua脚本可以保证多个命令的原子性
介绍redis 基本命令及缓存机制
在redisTemplate中如何使用lua脚本
介绍lua的序列化和反序列化
01 原子性
redis使用单个lua解释器去运行所有的脚本,并且保证脚步执行会以原子性的执行。
每个脚本都有一个最大执行时间限制,默认值是5s。最大执行时间的长短由配置文件redis.conf的lua-time-limit选项来控制,或直接使用config get和config set命令来修改。
当一个脚本执行达到最大执行时间,redis不会主动结束它,它会进行下面几个步骤:
①redis记录一个脚本正在超时运行
②redis开始重新接受其它客户端请求,但只接受执行script kill命令和shutdown nosave两个命令,若客户端执行其它命令,redis会返回busy错误。
③如果脚本只执行过读操作,使用script kill命令可以立即停止此脚本;如果脚本执行过写操作,只允许shutdown save/nosave命令,通过停止服务器来阻止当前数据写入磁盘。(此时服务器关闭,数据不会被保存)
02 redis 执行lua语法格式
从redis 2.6之后,redis内置Lua脚本解释器,并提供eval命令来执行Lua脚本求值
2.1 格式
eval script numkeys keys args
// 参数说明
// eval: redis提供执行lua脚本的命令
// script:lua脚本
// numkeys:指定键名参数集(keys)的个数
// keys:键名参数集,通过全局变量KEYS数组表示,下标从1开始。
// args:键值参数集,通过全局变量ARGV数组表示,下标从1开始
2.2 示例
EVAL命令的语义要求字面量不要直接写在lua脚本中,推荐使用变量来定义lua脚本,并将字面量放在键名参数集keys和键值参数集args中,通过全局变量KEYS和ARGV来获取,这样做的好处是可缓存!在lua脚本中,可以使用两个函数来执行redis命令,分别是:redis.call()和redis.pcall(),示例如下:
eval "return redis.call('set',KEYS[1],ARGA[1])" 1 name sym
03 类型转换
在lua脚本中执行call()或pcall()的redis命令,命令的返回值会被转换成Lua的数据结构。
Lua脚本内置的redis解释器,当lua返回值也会被转换成redis的数据结构,由eval命令将结果返回给客户端。
3.1 redis转换成Lua
redis类型 |
lua类型 |
描述 |
redis_integer |
lua_number |
redis整数转为lua数字 |
redis_bulk |
lua_string |
redis bulk回复转为lua字符串 |
redis multi bulk |
lua_table |
redis 多条bulk回复转为Lua 表 |
redis status |
lua_table |
redis状态回复转为lua表,表内ok域包含状态信息 |
redis error |
lua_table |
redis错误回复转为lua表,表内的err域包含错误信息 |
redis nil、redis multi nil |
lua_boolean_false |
redis的nil回复和nil多条回复转为lua的布尔值false |
3.2 Lua转换成Redis
Lua类型 |
redis类型 |
描述 |
lua_number |
redis_integer |
lua数字转为redis整数 |
lua_string |
redis_bulk |
lua字符串转为redis bulk回复 |
lua_array lua_table |
redis_mulit bulk |
lua表(数组)转为redis多条bulk回复 |
lua_table_ok |
redis stauts |
一个带单个ok域的lua表,转为redis状态回复 |
lua_table_err |
redis error |
一个带单个err域的lua表,转为redis错误回复 |
lua_boolean_err |
redis nil |
lua布尔值false转为redis的nil回复 |
04 脚本命令
4.1 命令
redis提供几个script脚本命令,用于对脚本子系统进行控制
script flush:清除所有的脚本缓存 script load:将脚本装入脚本缓存,不立即运行并返回校验和 script exists:根据脚本的校验和,判断脚本是否存在脚本缓存中 script kill:杀死当前正在运行的的脚本(防止脚本运行缓存,占用内存)
4.2 缓存和evalsha
redis有一个内部的脚本缓存机制,它不会每次都重新编译脚本,反倒是它会将所有运行过的脚本永久保存在脚本缓存中(因为redis发现脚本体积非常小,即使量很大,甚至经常修改,储存这些脚本的内存也是微不足道的)。
清空脚本缓存只有唯一一个方式,就是执行script flush命令。
使用eval命令执行脚本时,每次都要发送脚本主体,如果脚本足够复杂,这会付出无谓的网络带宽。redis基于对lua的缓存,它实现了evalsha命令。
evalsha命令和eval命令效果一样,都是解释lua脚本执行,但是evalsha命令的第一个参数不是脚本主体,而是脚本的SHAI校验和,这个校验和可以通过script load命令得到。evalsha命令执行过程分两步:
①如果redis服务器保存了给定SHA1校验和所指定的脚本,就会执行该脚本
②如果redis服务器没保存给定SHA1校验和所指定的脚本,它就会返回一个特殊错误,告知客户端使用eval命令去执行
4.3 全局变量保护
redis的lua脚本不允许创建全局变量,如果脚本需要在多次执行之间维持某种状态,可以借助外部redis key来保存状态,每次脚本执行前,获取redis相对应的key赋值给局部变量。在lua脚本中创建或访问一个全局变量,都会引起脚本停止,eval命令会返回一个错误
redis的全局变量保护并不是百分百成功,有时候会在脚本中混入lua全局状态,可能会引发AOF持久化和主从复制都无法得到保证。redis建议不要在脚本中使用全局变量,可以使用local关键字定义脚本中的变量!
05 redisTemplate执行脚本
5.1应用程序加载脚本
前提条件是需要引入spring-data-redis的jar
DefaultRedisScript脚本注入并设置设置参数
// 这种方式可以java bean注入的方式进行
@Bean public RedisScript<List> defaultRedisScript() {
DefaultRedisScript<List> defaultRedisScript = new DefaultRedisScript<>();
// 第一种方式
ResourceScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("lua/test.lua"));
defaultRedisScript.setScriptSource(scriptSource);
// 第二种方式
// ClassPathResource resource = new ClassPathResource("lua/test.lua");
// defaultRedisScript.setLocation(resource);
// 第三种方式
// String luaScript = "";
// defaultRedisScript.setScriptText(luaScript);
// 注意从源码中可以得知:result type只能是 Long, Boolean, List,
// or deserialized value type其中的一种
defaultRedisScript.setResultType(List.class);
return defaultRedisScript;
}
5.2 lua/test.lua脚本文件
-- 注意:这里是从1开始
local uvKey = KEYS[1];
local pvKey = KEYS[2];
-- 这里是一些参数
local count = ARGV[1];
local result = {};
local uvValue = redis.call('GETSET', uvKey, count);
local pvValue = redis.call('GETSET', pvKey, count);
table.insert(result,uvValue);
table.insert(result,pvValue);
retutn result;
5.3 执行脚本文件
private RedisTemplate<String, String> redisTemplate;
private RedisScript<List> redisScript;
publica void statVisitor(){
List result= redisTemplate.execute(redisScript,
Arrays.asList("uv", "pv"),String.valueOf(4));
}
06 lua的序列化和反序列化
6.1 序列化的格式
序列化之前
local text = {A=2, B={3, c="cValue", {x='xValue', y=1.5}}}
序列化之后
{['A']=2,['B']={3,{['y']=1.5,['x']='xValue'},['c']='strBb'}}
6.2 代码实现
反序列化(String反序列化成table)
local function unserialize(luaString)
local t = type(luaString)
if t == "nil" or luaString == "" then
return nil
elseif t == "number" or t == "string" or t == "boolean" then
luaString = tostring(luaString)
else
error("can not unserialize a " .. t .. " type.")
end
luaString = "return " .. luaString
local func = loadstring(luaString)
if func == nil then
return nil
end
return func()
end
序列化(Table序列化成String)
local function serialize(objTable)
local luaString = ""
local t = type(objTable)
if t == "number" then
luaString = luaString .. objTable
elseif t == "boolean" then
luaString = luaString .. tostring(objTable)
elseif t == "string" then
luaString = luaString .. string.format("%q", objTable)
elseif t == "table" then
luaString = luaString .. "{\n"
for k, v in pairs(obj) do
luaString = luaString .. "[" .. serialize(k) .. "]=" .. serialize(v) .. ",\n"
end
local metatable = getmetatable(obj)
if metatable ~= nil and type(metatable.__index) == "table" then
for k, v in pairs(metatable.__index) do
luaString = luaString .. "[" .. serialize(k) .. "]=" .. serialize(v) .. ",\n"
end
end
luaString = luaString .. "}"
elseif t == "nil" then
return nil
else
error("can not serialize a " .. t .. " type.")
end
return luaString
end
由于个人技术有限,如果有理解不到位或错误的地方,请点击“看一看”,同时留下评论,我会根据你们的建议进行修改。
---------------------TK-------------------
封面图片来源:互联网
长按下图二维码,即刻关注【天空奇点】
分享每天点滴知识,成就更好的你