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

[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. image
    2. 保存传入服务器的Lua脚本的脚本字典:实现SCRIPT EXISTS 命令、实现脚本复制
      1. 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下的实现:


<?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;

3.改造事务段

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

原文链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容