[TOC]
相关命令
- EVAL
- SCRIPT_LOAD
- EVALSHA(执行之前要求执行过EVAL或者SCRIPT_LOAD)
- SCRIPT EXISTS
- SCRIPT FLUSH(慎用)
- SCRIPT KILL(LUA的写操作务必谨慎,一旦有写入这个将失效效)
简介
- Redis 服务器在启动时, 会对内嵌的 Lua 环境执行一系列修改操作, 从而确保内嵌的 Lua 环境可以满足 Redis 在功能性、安全性等方面的需要。
- Redis 服务器专门使用一个伪客户端来执行 Lua 脚本中包含的 Redis 命令。
- Redis 使用脚本字典来保存所有被 EVAL 命令执行过, 或者被 SCRIPT_LOAD 命令载入过的 Lua 脚本, 这些脚本可以用于实现 SCRIPT_EXISTS 命令, 以及实现脚本复制功能。
- EVAL 命令为客户端输入的脚本在 Lua 环境中定义一个函数, 并通过调用这个函数来执行脚本。
- EVALSHA 命令通过直接调用 Lua 环境中已定义的函数来执行脚本。
- SCRIPT_FLUSH 命令会清空服务器 lua_scripts 字典中保存的脚本, 并重置 Lua 环境。
- SCRIPT_EXISTS 命令接受一个或多个 SHA1 校验和为参数, 并通过检查 lua_scripts 字典来确认校验和对应的脚本是否存在。
- SCRIPT_LOAD 命令接受一个 Lua 脚本为参数, 为该脚本在 Lua 环境中创建函数, 并将脚本保存到 lua_scripts 字典中。
- 服务器在执行脚本之前, 会为 Lua 环境设置一个超时处理钩子, 当脚本出现超时运行情况时, 客户端可以通过向服务器发送 SCRIPT_KILL 命令来让钩子停止正在执行的脚本, 或者发送 SHUTDOWN nosave 命令来让钩子关闭整个服务器。
- 主服务器复制 EVAL 、 SCRIPT_FLUSH 、 SCRIPT_LOAD 三个命令的方法和复制普通 Redis 命令一样 —— 只要将相同的命令传播给从服务器就可以了。
- 主服务器在复制 EVALSHA 命令时, 必须确保所有从服务器都已经载入了 EVALSHA 命令指定的 SHA1 校验和所对应的 Lua 脚本, 如果不能确保这一点的话, 主服务器会将 EVALSHA 命令转换成等效的 EVAL 命令, 并通过传播 EVAL 命令来获得相同的脚本执行效果。
启动过程
- 创建并修改Lua环境
-
创建Lua环境-生成基本的Lua环境,接下来对Lua环境做进一步的修改
-
载入函数库
- 基础库
- 表格库:table library
- 字符串库:string.find、string.format、string.len、string.reverse
- 数学库
- 调试库
- Lua CJSON:用于处理UTF-8编码的JSON格式,其中方法 cjson.decode、cjson.encode
- Struct库:和c交互的库
- Lua cmsgpack库:用于处理MessagePack格式的数据,其中cmsgpack.pack行数将Lua值转换为MessagePack数据,而cmsgpack.unpack函数则将MessagePack数据转换为Lua值
-
创建redis全局表格
- 创建redis表格(table),并将它设置为全局变量
- redis.call、redis.pcall、redis.log、redis.sha1hex(计算sha1校验和)
- 用于返回错误信息的:redis.error_reply、redis.status_reply
-
修改可能产生不一致数据的命令和方法:保证脚本在不同机器上产生相同的结果,Redis要求所有传入服务器的Lua脚本,以及Lua环境中的的所有函数都是无副作用的纯函数。
- 替换Lua原有的随机函数
- 创建排序辅助函数
-
创建redis.pcall函数的错误报告辅助函数
-
保护Lua的全局环境
- 当脚本创建一个全局变量时,服务器会报告一个错误(保证不会因为忘记使用local关键字而将二外的全局变量添加到lua环境里面)
- 读取一个不存在的全局变量也会报错
- Redis没有禁止在脚本里修改全局变量,所以在执行Lua脚本的时候,必须小心防止错误修改已存在的全局变量
-
将Lua环境保存到服务器状态的lua属性里
- 因为Redis使用串行化的方式执行命令,所以在任何特定时间里,最多只会有一个脚本能够被放进Lua环境里面执行,因此整个Redis服务器只需要创建一个Lua环境即可
-
- 创建环境协作组件
- redis 伪客户端:伪客户端一直存在直到服务器关闭,执行命令的过程:
- image
- 保存传入服务器的Lua脚本的脚本字典:实现SCRIPT EXISTS 命令、实现脚本复制
- image
- redis 伪客户端:伪客户端一直存在直到服务器关闭,执行命令的过程:
Redis Lua 的特点和注意事项
1. 特点
2. 注意事项
-
Lua脚本的bug特别可怕
,由于Redis的单线程特点,一旦Lua脚本出现不会返回(不是返回值)得问题,那么这个脚本就会阻塞整个redis实例。 - Lua脚本应该
尽量短小
实现关键步骤即可。(原因同上) - Lua脚本中
不应该出现常量Key
,这样会导致每次执行时都会在脚本字典中新建一个条目,应该使用全局变量数组KEYS和ARGV - KEYS和ARGV的索引都从1开始
- 传递给lua脚本的的键和参数:传递给lua脚本的键列表应该
包括可能会读取或者写入的所有键
。传入全部的键使得在使用各种分片或者集群技术时,其他软件可以在应用层检查所有的数据是不是都在同一个分片里面。另外集群版redis也会对将要访问的key进行检查,如果不在同一个服务器里面,那么redis将会返回一个错误。(决定使用集群版之前应该考虑业务拆分),参数列表无所谓。。 - lua脚本跟单个redis命令和事务段一样都是
原子的
- 已经进行了数据写入的lua脚本将无法中断,只能使用SHUTDOWN NOSAVE杀死Redis服务器,所以lua脚本一定要测试好。
典型应用
1.分布式全局锁(distlock)
Yii2下的实现:
<?php
namespace yii\redis;
use Yii;
use yii\base\InvalidConfigException;
use yii\di\Instance;
//使用了Yii2互斥锁接口
class Mutex extends \yii\mutex\Mutex
{
//锁过期时间,秒
public $expire = 30;
public $keyPrefix;
public $redis = 'redis';
private $_lockValues = [];
public function init()
{
parent::init();
$this->redis = Instance::ensure($this->redis, Connection::className());
if ($this->keyPrefix === null) {
$this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
}
}
protected function acquireLock($name, $timeout = 0)
{
$key = $this->calculateKey($name);
$value = Yii::$app->security->generateRandomString(20);
$waitTime = 0;
//使用setnx(理解为多机版sem_acquire)命令获取锁并自动重试(这个锁支持获取超时和自动过期)
while (!$this->redis->executeCommand('SET', [$key, $value, 'NX', 'PX', (int) ($this->expire * 1000)])) {
$waitTime++;
//超时则直接返回获取失败
if ($waitTime > $timeout) {
return false;
}
sleep(1);
}
$this->_lockValues[$name] = $value;
return true;
}
protected function releaseLock($name)
{
//使用脚本最优化性能,如果不用脚本则需要使用事务段
static $releaseLuaScript = <<<LUA
if redis.call("GET",KEYS[1])==ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
LUA;
if (!isset($this->_lockValues[$name]) || !$this->redis->executeCommand('EVAL', [
$releaseLuaScript,
1,
$this->calculateKey($name),
$this->_lockValues[$name]
])) {
return false;
} else {
unset($this->_lockValues[$name]);
return true;
}
}
protected function calculateKey($name)
{
return $this->keyPrefix . md5(json_encode([__CLASS__, $name]));
}
}
分析:
这个实现可以保证锁的互斥性(避免多个客户端同时获取锁)和超时性(避免资源一直处于锁定状态)
SET resource_name my_random_value NX PX 30000
这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样(保证释放资源的正确性)。
但是这个例子是在支持故障转移的主从结构中会存在竞态,下边是redis官方推荐的一个分布式锁算法RedLock,官方版分布式式锁算法实现
2.计数器信号量(counter semaphore)
几乎器也是一种锁,通常用于限制一项资源最多能够同时被多少个进程访问。
计数器信号量实现的功能(使用有序集合和时间戳分数处理计数器)
- acquire
/*
** KEYS[1] 信号量键
** ARGV[1] 最小有效分数
** ARGV[2] 信号量最大计数值
** ARGV[3] 当前时间戳
** ARGV[4] 客户端uniqueId
*/
static $acquireLuaScript = <<<LUA
--移除全部过期信号量
redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1])
if redis.call('zcard', KEYS[1]) < tonumber(ARGV[2]) then
redis.call('zadd', KEYS[1], ARGV[3], ARGV[4])
return ARGV[4]
end
LUA;
- reaease
zrem(key, clientId)
- refresh(有时需要)
/*
** KEYS[1] 信号量键
** ARGV[1] 客户端uniqueId
** ARGV[2] 当前时间戳
*/
static $refreshLuaScript = <<<LUA
--如果信号量仍然存在,那么对它的时间戳进行更新(通过zscore判断key存在与否)
if redis.call('zscore', KEYS[1], ARGV[1]) then
return redis.call('zadd', KEYS[1], ARGV[2], ARGV[1]) or true
end
LUA;
网友评论