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閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容