Lua 与 Redis
|
Reids返回值类型 | Lua数据类型 |
---|---|
整数 | 数值 |
字符串 | 字符串 |
多行字符串 | 表(数组) |
状态回复 | 表(只有一个ok 字段存储状态信息) |
错误回复 | 表(只有一个err 字段存储错误信息) |
注: Lua 的
false
会转化为空结果.
redis-cli提供了EVAL
与EVALSHA
命令执行Lua脚本:
- EVAL
EVAL script numkeys key [key ...] arg [arg ...]
key和arg两类参数用于向脚本传递数据,他们的值可在脚本中使用KEYS
和ARGV
两个table访问:KEYS
表示要操作的键名,ARGV
表示非键名参数(并非强制). - EVALSHA
EVALSHA
命令允许通过脚本的SHA1来执行(节省带宽),Redis在执行EVAL
/SCRIPT LOAD
后会计算脚本SHA1缓存,EVALSHA
根据SHA1取出缓存脚本执行.
创建Lua环境
为了在 Redis 服务器中执行 Lua 脚本,Redis 内嵌了一个 Lua 环境,并对该环境进行了一系列修改,从而确保满足 Redis 的需要. 其创建步骤如下:
- 创建基础 Lua 环境,之后所有的修改都基于该环境进行;
- 载入函数库到 Lua 环境,使 Lua 脚本可以使用这些函数库进行数据操作: 如基础库(删除了
loadfile()
函数)、Table、String、Math、Debug等标准库,以及CJSON、 Struct(用于Lua值与C结构体转换)、 cmsgpack等扩展库(Redis 禁用Lua标准库中与文件或系统调用相关函数,只允许对 Redis 数据处理). - 创建全局表
redis
,其包含了对 Redis 操作的函数,如redis.call()
、redis.pcall()
等; - 替换随机函数: 为了确保相同脚本可在不同机器上产生相同结果,Redis 要求所有传入服务器的 Lua 脚本,以及 Lua 环境中的所有函数,都必须是无副作用的纯函数,因此Redis使用自制函数替换了 Math 库中原有的
math.random()
和math.randomseed()
. - 创建辅助排序函数: 对于 Lua 脚本来说,另一个可能产生数据不一致的地方是那些带有不确定性质的命令(如: 由于
set
集合无序,因此即使两个集合内元素相同,其输出结果也并不一样),这类命令包括SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS、KEYS 等.
Redis 会创建一个辅助排序函数__redis__compare_helper
,当执行完以上命令后,Redis会调用table.sort()
以__redis__compare_helper
作为辅助函数对命令返回值排序. - 创建错误处理函数: Redis创建一个
__redis__err__handler
错误处理函数,当调用redis.pcall()
执行 Redis 命令出错时,该函数将打印异常详细信息. - Lua全局环境保护: 确保传入脚本内不会将额外的全局变量导入到 Lua 环境内.
小心: Redis 并未禁止用户修改已存在的全局变量.
- 完成Redis的
lua
属性与Lua环境的关联:
整个 Redis 服务器只需创建一个 Lua 环境.
Lua环境协作组件
-
Redis创建两个用于与Lua环境协作的组件: 伪客户端- 负责执行 Lua 脚本中的 Redis 命令,
lua_scripts
字典- 保存 Lua 脚本:- 伪客户端
执行Reids命令必须有对应的客户端状态,因此执行 Lua 脚本内的 Redis 命令必须为 Lua 环境专门创建一个伪客户端,由该客户端处理 Lua 内所有命令:redis.call()
/redis.pcall()
执行一个Redis命令步骤如下: -
lua_scripts
字典
字典key为脚本 SHA1 校验和,value为 SHA1 对应脚本内容,所有被EVAL
和SCRIPT LOAD
载入过的脚本都被记录到lua_scripts
中,便于实现SCRIPT EXISTS
命令和脚本复制功能.
- 伪客户端
EVAL命令原理
EVAL
命令执行分为以下三个步骤:
-
定义Lua函数:
在 Lua 环境内定义 Lua函数 : 名为f_
前缀+脚本 SHA1 校验和,体为脚本内容本身. 优势:- 执行脚本步骤简单,调用函数即可;
- 函数的局部性可保持 Lua 环境清洁,减少垃圾回收工作量,且避免使用全局变量;
- 只要记住 SHA1 校验和,即可在不知脚本内容的情况下,直接调用 Lua 函数执行脚本(
EVALSHA
命令实现).
将脚本保存到
lua_scripts
字典;- 执行脚本函数:
执行刚刚在定义的函数,间接执行 Lua 脚本,其准备和执行过程如下:
1). 将EVAL
传入的键名和参数分别保存到KEYS
和ARGV
,然后将这两个数组作为全局变量传入到Lua环境;
2). 为Lua环境装载超时处理hook
(handler
),可在脚本出现运行超时时让通过SCRIPT KILL
停止脚本,或SHUTDOWN
关闭Redis;
3). 执行脚本函数;
4). 移除超时hook
;
5). 将执行结果保存到客户端输出缓冲区,等待将结果返回客户端;
6). 对Lua环境执行垃圾回收.
对于会产生随机结果但无法排序的命令(如只产生一个元素,如 SPOP、SRANDMEMBER、RANDOMKEY、TIME),Redis在这类命令执行后将脚本状态置为
lua_random_dirty
,此后只允许脚本调用只读命令,不允许修改数据库值.
实践
使用Lua脚本重新构建带有过期时间的分布式锁.
案例来源: <Redis实战> 第6、11章,构建步骤:
- 锁申请
- 首先尝试加锁:
- 成功则为锁设定过期时间; 返回;
- 失败检测锁是否添加了过期时间;
- wait.
- 首先尝试加锁:
- 锁释放
- 检查当前线程是否真的持有了该锁:
- 持有: 则释放; 返回成功;
- 失败: 返回失败.
- 检查当前线程是否真的持有了该锁:
非Lua实现
String acquireLockWithTimeOut(Jedis connection,String lockName,long acquireTimeOut,int lockTimeOut) {
String identifier = UUID.randomUUID().toString();
String key = "lock:" + lockName;
long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
while (System.currentTimeMillis() < acquireTimeEnd) {
// 获取锁并设置过期时间
if (connection.setnx(key,identifier) != 0) {
connection.expire(key,lockTimeOut);
return identifier;
}
// 检查过期时间,并在必要时对其更新
else if (connection.ttl(key) == -1) {
connection.expire(key,lockTimeOut);
}
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
}
return null;
}
boolean releaseLock(Jedis connection,String identifier) {
String key = "lock:" + lockName;
connection.watch(key);
// 确保当前线程还持有锁
if (identifier.equals(connection.get(key))) {
Transaction transaction = connection.multi();
transaction.del(key);
return transaction.exec().isEmpty();
}
connection.unwatch();
return false;
}
Lua脚本实现
- Lua脚本: acquire
local key = KEYS[1]
local identifier = ARGV[1]
local lockTimeOut = ARGV[2]
-- 锁定成功
if redis.call("SETNX",key,identifier) == 1 then
redis.call("EXPIRE",lockTimeOut)
return 1
elseif redis.call("TTL",key) == -1 then
redis.call("EXPIRE",lockTimeOut)
end
return 0
- Lua脚本: release
local key = KEYS[1]
local identifier = ARGV[1]
if redis.call("GET",key) == identifier then
redis.call("DEL",key)
return 1
end
return 0
- Pre工具: 脚本执行器
/** * @author jifang * @since 16/8/25 下午3:35. */
public class ScriptCaller {
private static final ConcurrentMap<String,String> SHA_CACHE = new ConcurrentHashMap<>();
private String script;
private ScriptCaller(String script) {
this.script = script;
}
public static ScriptCaller getInstance(String script) {
return new ScriptCaller(script);
}
public Object call(Jedis connection,List<String> keys,List<String> argv,boolean forceEval) {
if (!forceEval) {
String sha = SHA_CACHE.get(this.script);
if (Strings.isNullOrEmpty(sha)) {
// load 脚本得到 sha1 缓存
sha = connection.scriptLoad(this.script);
SHA_CACHE.put(this.script,sha);
}
return connection.evalsha(sha,argv);
}
return connection.eval(script,argv);
}
}
- Client
public class Client {
private ScriptCaller acquireCaller = ScriptCaller.getInstance(
"local key = KEYS[1]n" +
"local identifier = ARGV[1]n" +
"local lockTimeOut = ARGV[2]n" +
"n" +
"if redis.call("SETNX",identifier) == 1 thenn" +
" redis.call("EXPIRE",lockTimeOut)n" +
" return 1n" +
"elseif redis.call("TTL",key) == -1 thenn" +
" redis.call("EXPIRE",lockTimeOut)n" +
"endn" +
"return 0"
);
private ScriptCaller releaseCaller = ScriptCaller.getInstance(
"local key = KEYS[1]n" +
"local identifier = ARGV[1]n" +
"n" +
"if redis.call("GET",key) == identifier thenn" +
" redis.call("DEL",key)n" +
" return 1n" +
"endn" +
"return 0"
);
@Test
public void client() {
Jedis jedis = new Jedis("127.0.0.1",9736);
String identifier = acquireLockWithTimeOut(jedis,"ret1",200 * 1000,300);
System.out.println(releaseLock(jedis,identifier));
}
String acquireLockWithTimeOut(Jedis connection,int lockTimeOut) {
String identifier = UUID.randomUUID().toString();
List<String> keys = Collections.singletonList("lock:" + lockName);
List<String> argv = Arrays.asList(identifier,String.valueOf(lockTimeOut));
long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
boolean acquired = false;
while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) {
if (1 == (long) acquireCaller.call(connection,argv,false)) {
acquired = true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
}
}
return acquired ? identifier : null;
}
boolean releaseLock(Jedis connection,String identifier) {
List<String> keys = Collections.singletonList("lock:" + lockName);
List<String> argv = Collections.singletonList(identifier);
return 1 == (long) releaseCaller.call(connection,true);
}
}
- 参考 & 推荐
- 代码的未来
- Redis入门指南
- Redis实战
- Redis设计与实现
- 云风的Blog: Lua与虚拟机
- Lua简明教程- CoolShell
- Lua-newbie
- Lua-Users
- redis.io
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!