Redis的Lua腳本編程的實(shí)現(xiàn)和應(yīng)用

[TOC]

相關(guān)命令

  1. EVAL
  2. SCRIPT_LOAD
  3. EVALSHA(執(zhí)行之前要求執(zhí)行過EVAL或者SCRIPT_LOAD)
  4. SCRIPT EXISTS
  5. SCRIPT FLUSH(慎用)
  6. SCRIPT KILL(LUA的寫操作務(wù)必謹(jǐn)慎乔妈,一旦有寫入這個將失效效)

簡介

  • Redis 服務(wù)器在啟動時绪氛, 會對內(nèi)嵌的 Lua 環(huán)境執(zhí)行一系列修改操作练慕, 從而確保內(nèi)嵌的 Lua 環(huán)境可以滿足 Redis 在功能性判呕、安全性等方面的需要。
  • Redis 服務(wù)器專門使用一個偽客戶端來執(zhí)行 Lua 腳本中包含的 Redis 命令贴膘。
  • Redis 使用腳本字典來保存所有被 EVAL 命令執(zhí)行過偶器, 或者被 SCRIPT_LOAD 命令載入過的 Lua 腳本捏题, 這些腳本可以用于實(shí)現(xiàn) SCRIPT_EXISTS 命令髓需, 以及實(shí)現(xiàn)腳本復(fù)制功能许师。
  • EVAL 命令為客戶端輸入的腳本在 Lua 環(huán)境中定義一個函數(shù), 并通過調(diào)用這個函數(shù)來執(zhí)行腳本授账。
  • EVALSHA 命令通過直接調(diào)用 Lua 環(huán)境中已定義的函數(shù)來執(zhí)行腳本枯跑。
  • SCRIPT_FLUSH 命令會清空服務(wù)器 lua_scripts 字典中保存的腳本惨驶, 并重置 Lua 環(huán)境白热。
  • SCRIPT_EXISTS 命令接受一個或多個 SHA1 校驗(yàn)和為參數(shù), 并通過檢查 lua_scripts 字典來確認(rèn)校驗(yàn)和對應(yīng)的腳本是否存在粗卜。
  • SCRIPT_LOAD 命令接受一個 Lua 腳本為參數(shù)屋确, 為該腳本在 Lua 環(huán)境中創(chuàng)建函數(shù), 并將腳本保存到 lua_scripts 字典中。
  • 服務(wù)器在執(zhí)行腳本之前攻臀, 會為 Lua 環(huán)境設(shè)置一個超時處理鉤子焕数, 當(dāng)腳本出現(xiàn)超時運(yùn)行情況時, 客戶端可以通過向服務(wù)器發(fā)送 SCRIPT_KILL 命令來讓鉤子停止正在執(zhí)行的腳本刨啸, 或者發(fā)送 SHUTDOWN nosave 命令來讓鉤子關(guān)閉整個服務(wù)器堡赔。
  • 主服務(wù)器復(fù)制 EVAL 、 SCRIPT_FLUSH 设联、 SCRIPT_LOAD 三個命令的方法和復(fù)制普通 Redis 命令一樣 —— 只要將相同的命令傳播給從服務(wù)器就可以了善已。
  • 主服務(wù)器在復(fù)制 EVALSHA 命令時, 必須確保所有從服務(wù)器都已經(jīng)載入了 EVALSHA 命令指定的 SHA1 校驗(yàn)和所對應(yīng)的 Lua 腳本离例, 如果不能確保這一點(diǎn)的話换团, 主服務(wù)器會將 EVALSHA 命令轉(zhuǎn)換成等效的 EVAL 命令, 并通過傳播 EVAL 命令來獲得相同的腳本執(zhí)行效果宫蛆。

啟動過程

  1. 創(chuàng)建并修改Lua環(huán)境
    1. 創(chuàng)建Lua環(huán)境-生成基本的Lua環(huán)境艘包,接下來對Lua環(huán)境做進(jìn)一步的修改

    2. 載入函數(shù)庫

      1. 基礎(chǔ)庫
      2. 表格庫:table library
      3. 字符串庫:string.find、string.format耀盗、string.len想虎、string.reverse
      4. 數(shù)學(xué)庫
      5. 調(diào)試庫
      6. Lua CJSON:用于處理UTF-8編碼的JSON格式,其中方法 cjson.decode叛拷、cjson.encode
      7. Struct庫:和c交互的庫
      8. Lua cmsgpack庫:用于處理MessagePack格式的數(shù)據(jù)磷醋,其中cmsgpack.pack行數(shù)將Lua值轉(zhuǎn)換為MessagePack數(shù)據(jù),而cmsgpack.unpack函數(shù)則將MessagePack數(shù)據(jù)轉(zhuǎn)換為Lua值
    3. 創(chuàng)建redis全局表格

      1. 創(chuàng)建redis表格(table)胡诗,并將它設(shè)置為全局變量
      2. redis.call邓线、redis.pcall、redis.log煌恢、redis.sha1hex(計算sha1校驗(yàn)和)
      3. 用于返回錯誤信息的:redis.error_reply骇陈、redis.status_reply
    4. 修改可能產(chǎn)生不一致數(shù)據(jù)的命令和方法:保證腳本在不同機(jī)器上產(chǎn)生相同的結(jié)果,Redis要求所有傳入服務(wù)器的Lua腳本瑰抵,以及Lua環(huán)境中的的所有函數(shù)都是無副作用的純函數(shù)你雌。

      1. 替換Lua原有的隨機(jī)函數(shù)
      2. 創(chuàng)建排序輔助函數(shù)
    5. 創(chuàng)建redis.pcall函數(shù)的錯誤報告輔助函數(shù)

    6. 保護(hù)Lua的全局環(huán)境

      1. 當(dāng)腳本創(chuàng)建一個全局變量時,服務(wù)器會報告一個錯誤(保證不會因?yàn)橥浭褂胠ocal關(guān)鍵字而將二外的全局變量添加到lua環(huán)境里面)
      2. 讀取一個不存在的全局變量也會報錯
      3. Redis沒有禁止在腳本里修改全局變量二汛,所以在執(zhí)行Lua腳本的時候婿崭,必須小心防止錯誤修改已存在的全局變量
    7. 將Lua環(huán)境保存到服務(wù)器狀態(tài)的lua屬性里

      1. 因?yàn)镽edis使用串行化的方式執(zhí)行命令,所以在任何特定時間里肴颊,最多只會有一個腳本能夠被放進(jìn)Lua環(huán)境里面執(zhí)行氓栈,因此整個Redis服務(wù)器只需要創(chuàng)建一個Lua環(huán)境即可
  2. 創(chuàng)建環(huán)境協(xié)作組件
    1. redis 偽客戶端:偽客戶端一直存在直到服務(wù)器關(guān)閉,執(zhí)行命令的過程:
      1. image
    2. 保存?zhèn)魅敕?wù)器的Lua腳本的腳本字典:實(shí)現(xiàn)SCRIPT EXISTS 命令婿着、實(shí)現(xiàn)腳本復(fù)制
      1. image

Redis Lua 的特點(diǎn)和注意事項(xiàng)

1. 特點(diǎn)

2. 注意事項(xiàng)

  1. Lua腳本的bug特別可怕授瘦,由于Redis的單線程特點(diǎn)醋界,一旦Lua腳本出現(xiàn)不會返回(不是返回值)得問題,那么這個腳本就會阻塞整個redis實(shí)例提完。
  2. Lua腳本應(yīng)該盡量短小實(shí)現(xiàn)關(guān)鍵步驟即可形纺。(原因同上)
  3. Lua腳本中不應(yīng)該出現(xiàn)常量Key,這樣會導(dǎo)致每次執(zhí)行時都會在腳本字典中新建一個條目徒欣,應(yīng)該使用全局變量數(shù)組KEYS和ARGV
  4. KEYS和ARGV的索引都從1開始
  5. 傳遞給lua腳本的的鍵和參數(shù):傳遞給lua腳本的鍵列表應(yīng)該包括可能會讀取或者寫入的所有鍵逐样。傳入全部的鍵使得在使用各種分片或者集群技術(shù)時,其他軟件可以在應(yīng)用層檢查所有的數(shù)據(jù)是不是都在同一個分片里面打肝。另外集群版redis也會對將要訪問的key進(jìn)行檢查官研,如果不在同一個服務(wù)器里面,那么redis將會返回一個錯誤闯睹。(決定使用集群版之前應(yīng)該考慮業(yè)務(wù)拆分)戏羽,參數(shù)列表無所謂。楼吃。
  6. lua腳本跟單個redis命令和事務(wù)段一樣都是原子的
  7. 已經(jīng)進(jìn)行了數(shù)據(jù)寫入的lua腳本將無法中斷始花,只能使用SHUTDOWN NOSAVE殺死Redis服務(wù)器,所以lua腳本一定要測試好孩锡。

典型應(yīng)用

1.分布式全局鎖(distlock)

Yii2下的實(shí)現(xiàn):


<?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(理解為多機(jī)版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)
    {
        //使用腳本最優(yōu)化性能,如果不用腳本則需要使用事務(wù)段
        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]));
    }
}

分析:

這個實(shí)現(xiàn)可以保證鎖的互斥性(避免多個客戶端同時獲取鎖)和超時性(避免資源一直處于鎖定狀態(tài))

SET resource_name my_random_value NX PX 30000

這個命令僅在不存在key的時候才能被執(zhí)行成功(NX選項(xiàng))躬窜,并且這個key有一個30秒的自動失效時間(PX屬性)浇垦。這個key的值是“my_random_value”(一個隨機(jī)值),這個值在所有的客戶端必須是唯一的荣挨,所有同一key的獲取者(競爭者)這個值都不能一樣(保證釋放資源的正確性)男韧。

但是這個例子是在支持故障轉(zhuǎn)移的主從結(jié)構(gòu)中會存在競態(tài),下邊是redis官方推薦的一個分布式鎖算法RedLock默垄,官方版分布式式鎖算法實(shí)現(xiàn)

2.計數(shù)器信號量(counter semaphore)

幾乎器也是一種鎖此虑,通常用于限制一項(xiàng)資源最多能夠同時被多少個進(jìn)程訪問。

計數(shù)器信號量實(shí)現(xiàn)的功能(使用有序集合和時間戳分?jǐn)?shù)處理計數(shù)器)

  • acquire
/*
** KEYS[1] 信號量鍵
** ARGV[1] 最小有效分?jǐn)?shù)
** ARGV[2] 信號量最大計數(shù)值
** ARGV[3] 當(dāng)前時間戳
** 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] 當(dāng)前時間戳
*/
static $refreshLuaScript = <<<LUA
--如果信號量仍然存在口锭,那么對它的時間戳進(jìn)行更新(通過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.改造事務(wù)段

4.對已有結(jié)構(gòu)進(jìn)行分片朦前,用來壓縮占用空間

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鹃操,隨后出現(xiàn)的幾起案子韭寸,更是在濱河造成了極大的恐慌,老刑警劉巖荆隘,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恩伺,死亡現(xiàn)場離奇詭異,居然都是意外死亡臭胜,警方通過查閱死者的電腦和手機(jī)莫其,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門癞尚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耸三,“玉大人乱陡,你說我怎么就攤上這事∫亲常” “怎么了憨颠?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長积锅。 經(jīng)常有香客問我爽彤,道長,這世上最難降的妖魔是什么缚陷? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任适篙,我火速辦了婚禮,結(jié)果婚禮上箫爷,老公的妹妹穿的比我還像新娘嚷节。我一直安慰自己,他們只是感情好虎锚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布硫痰。 她就那樣靜靜地躺著,像睡著了一般窜护。 火紅的嫁衣襯著肌膚如雪效斑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天柱徙,我揣著相機(jī)與錄音缓屠,去河邊找鬼。 笑死护侮,一個胖子當(dāng)著我的面吹牛藏研,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播概行,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蠢挡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凳忙?” 一聲冷哼從身側(cè)響起业踏,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涧卵,沒想到半個月后勤家,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柳恐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年伐脖,在試婚紗的時候發(fā)現(xiàn)自己被綠了热幔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡讼庇,死狀恐怖绎巨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蠕啄,我是刑警寧澤场勤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站歼跟,受9級特大地震影響和媳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哈街,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一留瞳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骚秦,春花似錦她倘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蒙揣,卻和暖如春靶溜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背懒震。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工罩息, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人个扰。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓瓷炮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親递宅。 傳聞我的和親對象是個殘疾皇子娘香,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容