Redis中使用Lua腳本


Redis中使用Lua腳本

一、簡介

  1. Redis中為什么引入Lua腳本?
    Redis是高性能的key-value內存數據庫,在部分場景下,是對關系數據庫的良好補充。
    Redis提供了非常豐富的指令集,官網上提供了200多個命令。但是某些特定領域,需要擴充若干指令原子性執行時,僅使用原生命令便無法完成。
    Redis 為這樣的用戶場景提供了 lua 腳本支持,用戶可以向服務器發送 lua 腳本來執行自定義動作,獲取腳本的響應數據。Redis 服務器會單線程原子性執行 lua 腳本,保證 lua 腳本在處理的過程中不會被任意其它請求打斷。
  2. Redis意識到上述問題后,在2.6版本推出了 lua 腳本功能,允許開發者使用Lua語言編寫腳本傳到Redis中執行。使用腳本的好處如下:
  • 減少網絡開銷。可以將多個請求通過腳本的形式一次發送,減少網絡時延。
  • 原子操作。Redis會將整個腳本作為一個整體執行,中間不會被其他請求插入。因此在腳本運行過程中無需擔心會出現競態條件,無需使用事務。
  • 復用。客戶端發送的腳本會永久存在redis中,這樣其他客戶端可以復用這一腳本,而不需要使用代碼完成相同的邏輯。
  1. 什么是Lua?
    Lua是一種輕量小巧的腳本語言,用標准C語言編寫並以源代碼形式開放。
    其設計目的就是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。因為廣泛的應用於:游戲開發、獨立應用腳本、Web 應用腳本、擴展和數據庫插件等。
    比如:Lua腳本用在很多游戲上,主要是Lua腳本可以嵌入到其他程序中運行,游戲升級的時候,可以直接升級腳本,而不用重新安裝游戲。
    Lua腳本的基本語法可參考:菜鳥教程

二、Redis中Lua的常用命令

命令不多,就下面這幾個:
- EVAL
- EVALSHA
- SCRIPT LOAD - SCRIPT EXISTS
- SCRIPT FLUSH
- SCRIPT KILL

2.1 EVAL命令

命令格式:EVAL script numkeys key [key …] arg [arg …]
- script參數是一段 Lua5.1 腳本程序。腳本不必(也不應該)定義為一個 Lua 函數
- numkeys指定后續參數有幾個key,即:key [key …]中key的個數。如沒有key,則為0
- key [key …] 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key)。在Lua腳本中通過KEYS[1], KEYS[2]獲取。
- arg [arg …] 附加參數。在Lua腳本中通過ARGV[1],ARGV[2]獲取。

// 例1:numkeys=1,keys數組只有1個元素key1,arg數組無元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"

// 例2:numkeys=0,keys數組無元素,arg數組元素中有1個元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"

// 例3:numkeys=2,keys數組有兩個元素key1和key2,arg數組元素中有兩個元素first和second 
//      其實{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua語法中“使用默認索引”的table表,
//      相當於java中的map中存放四條數據。Key分別為:1、2、3、4,而對應的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
//      舉此例子僅為說明eval命令中參數的如何使用。項目中編寫Lua腳本最好遵從key、arg的規范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 
1) "key1"
2) "key2"
3) "first"
4) "second"


// 例4:使用了redis為lua內置的redis.call函數
//      腳本內容為:先執行SET命令,在執行EXPIRE命令
//      numkeys=1,keys數組有一個元素userAge(代表redis的key)
//      arg數組元素中有兩個元素:10(代表userAge對應的value)和60(代表redis的存活時間)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44

通過上面的例4,我們可以發現,腳本中使用redis.call()去調用redis的命令。
在 Lua 腳本中,可以使用兩個不同函數來執行 Redis 命令,它們分別是: redis.call() 和 redis.pcall()
這兩個函數的唯一區別在於它們使用不同的方式處理執行命令所產生的錯誤,差別如下:

錯誤處理
當 redis.call() 在執行命令的過程中發生錯誤時,腳本會停止執行,並返回一個腳本錯誤,錯誤的輸出信息會說明錯誤造成的原因:

127.0.0.1:6379> lpush foo a
(integer) 1

127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

和 redis.call() 不同, redis.pcall() 出錯時並不引發(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用於表示錯誤:

127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

2.2 SCRIPT LOAD命令 和 EVALSHA命令

SCRIPT LOAD命令格式:SCRIPT LOAD script
EVALSHA命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]

這兩個命令放在一起講的原因是:EVALSHA 命令中的sha1參數,就是SCRIPT LOAD 命令執行的結果。

SCRIPT LOAD 將腳本 script 添加到Redis服務器的腳本緩存中,並不立即執行這個腳本,而是會立即對輸入的腳本進行求值。並返回給定腳本的 SHA1 校驗和。如果給定的腳本已經在緩存里面了,那么不執行任何操作。

在腳本被加入到緩存之后,在任何客戶端通過EVALSHA命令,可以使用腳本的 SHA1 校驗和來調用這個腳本。腳本可以在緩存中保留無限長的時間,直到執行SCRIPT FLUSH為止。

## SCRIPT LOAD加載腳本,並得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"

## EVALSHA使用sha1值,並拼裝和EVAL類似的numkeys和key數組、arg數組,調用腳本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 43

2.3 SCRIPT EXISTS 命令

命令格式:SCRIPT EXISTS sha1 [sha1 …]
作用:給定一個或多個腳本的 SHA1 校驗和,返回一個包含 0 和 1 的列表,表示校驗和所指定的腳本是否已經被保存在緩存當中

127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe346
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345 6aeea4b3e96171ef835a78178fceadf1a5dbe366
1) (integer) 1
2) (integer) 0

2.4 SCRIPT FLUSH 命令

命令格式:SCRIPT FLUSH
作用:清除Redis服務端所有 Lua 腳本緩存

127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0

2.5 SCRIPT KILL 命令

命令格式:SCRIPT KILL
作用:殺死當前正在運行的 Lua 腳本,當且僅當這個腳本沒有執行過任何寫操作時,這個命令才生效。 這個命令主要用於終止運行時間過長的腳本,比如一個因為 BUG 而發生無限 loop 的腳本,諸如此類。

假如當前正在運行的腳本已經執行過寫操作,那么即使執行SCRIPT KILL,也無法將它殺死,因為這是違反 Lua 腳本的原子性執行原則的。在這種情況下,唯一可行的辦法是使用SHUTDOWN NOSAVE命令,通過停止整個 Redis 進程來停止腳本的運行,並防止不完整(half-written)的信息被寫入數據庫中。

三、Redis執行Lua腳本文件

在第二章中介紹的命令,是在redis客戶端中使用命令進行操作。該章節介紹的是直接執行 Lua 的腳本文件。

3.1 編寫Lua腳本文件

local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
        redis.call('SET', KEYS[1], ARGV[2])
        return 1
else
        return 0
end

3.2 執行Lua腳本文件

執行命令: redis-cli -a 密碼 --eval Lua腳本路徑 key [key …] ,  arg [arg …] 
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 

此處敲黑板,注意啦!!!
"--eval"而不是命令模式中的"eval",一定要有前端的兩個-
腳本路徑后緊跟key [key …],相比命令行模式,少了numkeys這個key數量值
key [key …] 和 arg [arg …] 之間的“ , ”,英文逗號前后必須有空格,否則死活都報錯

## Redis客戶端執行
127.0.0.1:6379> set userName zhangsan 
OK
127.0.0.1:6379> get userName
"zhangsan"

## linux服務器執行
## 第一次執行:compareAndSet成功,返回1
## 第二次執行:compareAndSet失敗,返回0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 0

四、實例:使用Lua實現令牌桶算法

redis.replicate_commands();
-- 參數中傳遞的令牌key,基於選定的限流策略來定(唯一)
local key = KEYS[1]
-- 令牌桶填充 限流單位時間
local update_len = tonumber(ARGV[1])
-- 記錄 第一次訪問的時間戳
local key_time = key..'_FRT'
-- 獲取當前時間(這里的curr_time_arr 中第一個是 秒數,第二個是 秒數后毫秒數),由於我是按秒計算的,這里只要curr_time_arr[1](注意:redis數組下標是從1開始的)
-- 如果需要獲得毫秒數 則為 tonumber(arr[1]*1000 + arr[2])
local curr_time_arr = redis.call('TIME')
-- 當前時間秒數
local nowTime = tonumber(curr_time_arr[1])
-- 從redis中獲取當前key 第一次訪問的時間戳,無直接賦值0,有即value
local curr_key_time = tonumber(redis.call('get', key_time) or 0)
-- 獲取當前key對應令牌桶中的令牌數,無直接賦值-1,有即value
local token_count = tonumber(redis.call('get', key) or -1)
-- 當前令牌桶的容量,用戶自定義初始化大小
local token_size = tonumber(ARGV[2])
-- 令牌桶數量小於0 說明令牌桶沒有初始化
if token_count < 0 then
	redis.call('set',key_time,nowTime)
	redis.call('set',key,token_size -1)
	return token_size -1
else
	if token_count > 0 then -- 當前令牌桶中令牌數夠用
	    redis.call('set',key,token_count -1)
		return token_count -1   -- 返回剩余令牌數
	else    -- 當前令牌桶中令牌數已清空
       -- 判斷一下,當前時間秒數 與上次更新時間秒數  的間隔,是否大於規定時間間隔(update_len)
		if nowTime - curr_key_time > update_len then 
			redis.call('set',key,token_size -1)
			redis.call('set',key_time,nowTime)
			return token_size - 1
		else
			return -1
		end
	end
end

五、總結

  1. 通過上面一系列的介紹,對Lua腳本、Lua基礎語法有了一定了解,同時也學會在Redis中如何去使用Lua腳本去實現Redis命令無法實現的場景
  2. 回頭再思考文章開頭提到的Redis使用Lua腳本的幾個優點:減少網絡開銷、原子性、復用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM