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