热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Redis的Lua脚本编程的实现和应用

[TOC]相关命令EVALSCRIPT_LOADEVALSHA(执行之前要求执行过EVAL或者SCRIPT_LOAD)SCRIPTEXISTSSCRIPTFLUSH(慎用)SCRI

[TOC]

相关命令

  1. EVAL
  2. SCRIPT_LOAD
  3. EVALSHA(执行之前要求执行过EVAL或者SCRIPT_LOAD)
  4. SCRIPT EXISTS
  5. SCRIPT FLUSH(慎用)
  6. 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 命令来获得相同的脚本执行效果。

启动过程

  1. 创建并修改Lua环境
    1. 创建Lua环境-生成基本的Lua环境,接下来对Lua环境做进一步的修改

    2. 载入函数库

      1. 基础库
      2. 表格库:table library
      3. 字符串库:string.find、string.format、string.len、string.reverse
      4. 数学库
      5. 调试库
      6. Lua CJSON:用于处理UTF-8编码的JSON格式,其中方法 cjson.decode、cjson.encode
      7. Struct库:和c交互的库
      8. Lua cmsgpack库:用于处理MessagePack格式的数据,其中cmsgpack.pack行数将Lua值转换为MessagePack数据,而cmsgpack.unpack函数则将MessagePack数据转换为Lua值
    3. 创建redis全局表格

      1. 创建redis表格(table),并将它设置为全局变量
      2. redis.call、redis.pcall、redis.log、redis.sha1hex(计算sha1校验和)
      3. 用于返回错误信息的:redis.error_reply、redis.status_reply
    4. 修改可能产生不一致数据的命令和方法:保证脚本在不同机器上产生相同的结果,Redis要求所有传入服务器的Lua脚本,以及Lua环境中的的所有函数都是无副作用的纯函数。

      1. 替换Lua原有的随机函数
      2. 创建排序辅助函数
    5. 创建redis.pcall函数的错误报告辅助函数

    6. 保护Lua的全局环境

      1. 当脚本创建一个全局变量时,服务器会报告一个错误(保证不会因为忘记使用local关键字而将二外的全局变量添加到lua环境里面)
      2. 读取一个不存在的全局变量也会报错
      3. Redis没有禁止在脚本里修改全局变量,所以在执行Lua脚本的时候,必须小心防止错误修改已存在的全局变量
    7. 将Lua环境保存到服务器状态的lua属性里

      1. 因为Redis使用串行化的方式执行命令,所以在任何特定时间里,最多只会有一个脚本能够被放进Lua环境里面执行,因此整个Redis服务器只需要创建一个Lua环境即可
  2. 创建环境协作组件
    1. redis 伪客户端:伪客户端一直存在直到服务器关闭,执行命令的过程:
      1. 《Redis的Lua脚本编程的实现和应用》 image
    2. 保存传入服务器的Lua脚本的脚本字典:实现SCRIPT EXISTS 命令、实现脚本复制
      1. 《Redis的Lua脚本编程的实现和应用》 image

Redis Lua 的特点和注意事项

1. 特点

2. 注意事项

  1. Lua脚本的bug特别可怕,由于Redis的单线程特点,一旦Lua脚本出现不会返回(不是返回值)得问题,那么这个脚本就会阻塞整个redis实例。
  2. Lua脚本应该尽量短小实现关键步骤即可。(原因同上)
  3. Lua脚本中不应该出现常量Key,这样会导致每次执行时都会在脚本字典中新建一个条目,应该使用全局变量数组KEYS和ARGV
  4. KEYS和ARGV的索引都从1开始
  5. 传递给lua脚本的的键和参数:传递给lua脚本的键列表应该包括可能会读取或者写入的所有键。传入全部的键使得在使用各种分片或者集群技术时,其他软件可以在应用层检查所有的数据是不是都在同一个分片里面。另外集群版redis也会对将要访问的key进行检查,如果不在同一个服务器里面,那么redis将会返回一个错误。(决定使用集群版之前应该考虑业务拆分),参数列表无所谓。。
  6. lua脚本跟单个redis命令和事务段一样都是原子的
  7. 已经进行了数据写入的lua脚本将无法中断,只能使用SHUTDOWN NOSAVE杀死Redis服务器,所以lua脚本一定要测试好。

典型应用

1.分布式全局锁(distlock)

Yii2下的实现:


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 = <<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 = <<--移除全部过期信号量
redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1])
if redis.call('zcard', KEYS[1]) 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 = <<--如果信号量仍然存在,那么对它的时间戳进行更新(通过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;

3.改造事务段

4.对已有结构进行分片,用来压缩占用空间

原文链接


推荐阅读
author-avatar
手机用户2502913623
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有