Redis中Lua腳本的使用和設置超時

Redis提供瞭Lua腳本功能來讓用戶實現自己的原子命令,但也存在著風險,編寫不當的腳本可能阻塞線程導致整個Redis服務不可用。

本文將介紹Redis中Lua腳本的基本用法,以及腳本超時導致的問題和處理方式。

EVAL命令簡介

eval格式

Redis 提供瞭命令EVAL來執行Lua腳本,格式如下

EVAL script numkeys key [key …] arg [arg …]

其中 script 是將要執行的腳本內容,至於後面的腳本參數部分與本文無關,在此不做贅述。

特性

由於Redis對數據集單線程讀寫的特性,Lua腳本執行時會阻塞所有對數據集的讀寫操作,這給它帶來瞭下面兩個特性:

  • 原子性:可以通過Lua腳本實現對數據集的原子讀寫操作,這和Redis的事務功能MULTI / EXEC類似
  • 長時間阻塞風險:如果Lua腳本執行時間過長,導致整個Redis不可用

執行流程

eval "return 'hello world'" 0為例,腳本執行步驟如下

定義腳本函數

執行過的腳本可以根據hash值找到函數重新使用

Redis會根據傳入的腳本內容生成函數,函數名由 f_ + 腳本內容的sha1摘要組成。

function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
	return 'hello world'
end

函數保存到 Lua_scripts字典,便於 evalsha使用

執行腳本函數

  • 將KEYS和ARGV兩個參數數組傳入Lua執行環境
  • 裝載超時處理鉤子
  • 執行腳本
  • 移除超時鉤子
  • 結果保存到客戶端輸出緩沖區,等待服務器將結果返回客戶端
  • Lua環境垃圾回收

關於腳本超時

介紹完EVAL命令,下面來關註Lua腳本長時間阻塞的風險。

Redis的配置文件中提供瞭如下配置項來規定最大執行時長

  • Lua-time-limit 5000 Lua腳本最大執行時間,默認5秒

但這裡有個坑,當一個腳本達到最大執行時長的時候,Redis並不會強制停止腳本的運行,僅僅在日志裡打印個警告,告知有腳本超時。

Lua slow script detected: still in execution after 5000 milliseconds. You can try killing the script using the SCRIPT KILL command. Script SHA1 is: 2531e4edc1a1e2a9bac3c52e99466f9ccabf12c0

為什麼不能直接停掉呢?

因為 Redis 必須保證腳本執行的原子性,中途停止可能導致內存的數據集上隻修改瞭部分數據。

(隻讀的腳本應該是可以自動停的,沒自動停的原因我猜測是:腳本超時嚴重可以肯定出現瞭編碼錯誤,作者可能希望在測試中盡早發現這種問題,而不是靠自動停止導致bug被忽略?)

如果時長達到 Lua-time-limit 規定的最大執行時間,Redis隻會做這幾件事情:

日志記錄有腳本運行超時

開始允許接受其他客戶端請求,但僅限於 SCRIPT KILLSHUTDOWN NOSAVE 兩個命令

其他請求仍返回busy錯誤

SCRIPT KILL 命令

如果Lua隻是讀取數據而沒做修改的話,執行 SCRIPT KILL 就可以直接終止腳本執行,不用擔心數據被修改。

但是,如果腳本已經改寫瞭數據內容,SCRIPT KILL將報出以下錯誤,因為它破壞數據集的內容。

(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

SHUTDOWN NOSAVE 命令

如上所述,如果腳本已經執行瞭寫命令,SCRIPT KILL將無法執行。那我們就隻剩以下兩種選擇瞭:

  • 繼續等待腳本執行完成
  • 使用 SHUTDOWN NOSAVE 來直接停掉 Redis,並避免臟數據持久化到磁盤

最後,不知道你有沒有疑問,從開始執行腳本到 SHUTDOWN 之間的寫命令會把日志寫到AOF裡嗎?Lua腳本中的命令什麼時候會寫AOF裡?

講道理,既然 Redis 為瞭不破壞腳本的原子性而不讓SCRIPT KILL執行,那麼腳本中寫命令的 “提交” 也應當是原子執行的,而不是執行一句就向AOF裡寫一句。

“提交”:借用數據庫中 commit 的概念,這裡指寫入AOF文件中

下面就來驗證這個猜測:

先執行 tail -f appendonly.aof 實時查看AOF文件變化

再開一個redis-cli 命令行執行一個內容如下的Lua腳本

redis.call('set','a','aaaa') --先執行寫命令
local count = 1 
while( 999999999 > count ) -- 阻塞幾秒
do  
   count = count+1   
end
127.0.0.1:6379> eval "redis.call('set','a','aaaa') local count = 1 while( 999999999 > count ) do  count = count+1   end" 0
(nil)
(8.65s)

現象是,腳本剛開始執行,AOF文件毫無反應,一直等到8秒後腳本完成,命令才追加寫入到AOF中。

這就驗證瞭Redis腳本裡的寫命令是等到執行完成後再一次性寫入AOF的。

參考

Redis設計與實現

到此這篇關於Redis中Lua腳本的使用和設置超時 的文章就介紹到這瞭,更多相關Redis Lua 超時內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: