[TOC]
相關(guān)命令
- EVAL
- SCRIPT_LOAD
- EVALSHA(執(zhí)行之前要求執(zhí)行過EVAL或者SCRIPT_LOAD)
- SCRIPT EXISTS
- SCRIPT FLUSH(慎用)
- 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í)行效果宫蛆。
啟動過程
- 創(chuàng)建并修改Lua環(huán)境
創(chuàng)建Lua環(huán)境-生成基本的Lua環(huán)境艘包,接下來對Lua環(huán)境做進(jìn)一步的修改
-
載入函數(shù)庫
- 基礎(chǔ)庫
- 表格庫:table library
- 字符串庫:string.find、string.format耀盗、string.len想虎、string.reverse
- 數(shù)學(xué)庫
- 調(diào)試庫
- Lua CJSON:用于處理UTF-8編碼的JSON格式,其中方法 cjson.decode叛拷、cjson.encode
- Struct庫:和c交互的庫
- Lua cmsgpack庫:用于處理MessagePack格式的數(shù)據(jù)磷醋,其中cmsgpack.pack行數(shù)將Lua值轉(zhuǎn)換為MessagePack數(shù)據(jù),而cmsgpack.unpack函數(shù)則將MessagePack數(shù)據(jù)轉(zhuǎn)換為Lua值
-
創(chuàng)建redis全局表格
- 創(chuàng)建redis表格(table)胡诗,并將它設(shè)置為全局變量
- redis.call邓线、redis.pcall、redis.log煌恢、redis.sha1hex(計算sha1校驗(yàn)和)
- 用于返回錯誤信息的:redis.error_reply骇陈、redis.status_reply
-
修改可能產(chǎn)生不一致數(shù)據(jù)的命令和方法:保證腳本在不同機(jī)器上產(chǎn)生相同的結(jié)果,Redis要求所有傳入服務(wù)器的Lua腳本瑰抵,以及Lua環(huán)境中的的所有函數(shù)都是無副作用的純函數(shù)你雌。
- 替換Lua原有的隨機(jī)函數(shù)
- 創(chuàng)建排序輔助函數(shù)
創(chuàng)建redis.pcall函數(shù)的錯誤報告輔助函數(shù)
-
保護(hù)Lua的全局環(huán)境
- 當(dāng)腳本創(chuàng)建一個全局變量時,服務(wù)器會報告一個錯誤(保證不會因?yàn)橥浭褂胠ocal關(guān)鍵字而將二外的全局變量添加到lua環(huán)境里面)
- 讀取一個不存在的全局變量也會報錯
- Redis沒有禁止在腳本里修改全局變量二汛,所以在執(zhí)行Lua腳本的時候婿崭,必須小心防止錯誤修改已存在的全局變量
-
將Lua環(huán)境保存到服務(wù)器狀態(tài)的lua屬性里
- 因?yàn)镽edis使用串行化的方式執(zhí)行命令,所以在任何特定時間里肴颊,最多只會有一個腳本能夠被放進(jìn)Lua環(huán)境里面執(zhí)行氓栈,因此整個Redis服務(wù)器只需要創(chuàng)建一個Lua環(huán)境即可
- 創(chuàng)建環(huán)境協(xié)作組件
- redis 偽客戶端:偽客戶端一直存在直到服務(wù)器關(guān)閉,執(zhí)行命令的過程:
- 保存?zhèn)魅敕?wù)器的Lua腳本的腳本字典:實(shí)現(xiàn)SCRIPT EXISTS 命令婿着、實(shí)現(xiàn)腳本復(fù)制
- redis 偽客戶端:偽客戶端一直存在直到服務(wù)器關(guān)閉,執(zhí)行命令的過程:
Redis Lua 的特點(diǎn)和注意事項(xiàng)
1. 特點(diǎn)
2. 注意事項(xiàng)
-
Lua腳本的bug特別可怕
授瘦,由于Redis的單線程特點(diǎn)醋界,一旦Lua腳本出現(xiàn)不會返回(不是返回值)得問題,那么這個腳本就會阻塞整個redis實(shí)例提完。 - Lua腳本應(yīng)該
盡量短小
實(shí)現(xiàn)關(guān)鍵步驟即可形纺。(原因同上) - Lua腳本中
不應(yīng)該出現(xiàn)常量Key
,這樣會導(dǎo)致每次執(zhí)行時都會在腳本字典中新建一個條目徒欣,應(yīng)該使用全局變量數(shù)組KEYS和ARGV - KEYS和ARGV的索引都從1開始
- 傳遞給lua腳本的的鍵和參數(shù):傳遞給lua腳本的鍵列表應(yīng)該
包括可能會讀取或者寫入的所有鍵
逐样。傳入全部的鍵使得在使用各種分片或者集群技術(shù)時,其他軟件可以在應(yīng)用層檢查所有的數(shù)據(jù)是不是都在同一個分片里面打肝。另外集群版redis也會對將要訪問的key進(jìn)行檢查官研,如果不在同一個服務(wù)器里面,那么redis將會返回一個錯誤闯睹。(決定使用集群版之前應(yīng)該考慮業(yè)務(wù)拆分)戏羽,參數(shù)列表無所謂。楼吃。 - lua腳本跟單個redis命令和事務(wù)段一樣都是
原子的
- 已經(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;