第十節:Redis 腳本、Lua語法學習、以及秒殺案例腳本分析


一. Redis腳本

1. 簡介

 從 Redis 2.6.0 版本開始,通過內置的 Lua 解釋器,可以使用 EVAL 命令對 Lua 腳本進行求值。在lua腳本中可以通過兩個不同的函數調用redis命令,分別是:redis.call()    redis.pcall()

(1). 腳本的原子性

 Redis 使用單個 Lua 解釋器去運行所有腳本,並且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很類似。在其他別的客戶端看來,腳本的效果(effect)要么是不可見的(not visible),要么就是已完成的(already completed)。

 另一方面,這也意味着,執行一個運行緩慢的腳本並不是一個好主意。寫一個跑得很快很順溜的腳本並不難,因為腳本的運行開銷(overhead)非常少,但是當你不得不使用一些跑得比較慢的腳本時,請小心,因為當這些蝸牛腳本在慢吞吞地運行的時候,其他客戶端會因為服務器正忙而無法執行命令。

(2). 錯誤處理

 redis.call() 和 redis.pcall() 的唯一區別在於它們對錯誤處理的不同。

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

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

(3). 帶寬和EVALSHA

 A. EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體(script body)。Redis 有一個內部的緩存機制,因此它不會每次都重新編譯腳本,不過在很多場合,付出無謂的帶寬來傳送腳本主體並不是最佳選擇。

 B.為了減少帶寬的消耗, Redis 實現了 EVALSHA 命令,它的作用和 EVAL 一樣,都用於對腳本求值,但它接受的第一個參數不是腳本,而是腳本的 SHA1 校驗和(sum)。

 C. 客戶端庫的底層實現可以一直樂觀地使用 EVALSHA 來代替 EVAL ,並期望着要使用的腳本已經保存在服務器上了,只有當 NOSCRIPT 錯誤發生時,才使用 EVAL 命令重新發送腳本,這樣就可以最大限度地節省帶寬。

 D. 這也說明了執行 EVAL 命令時,使用正確的格式來傳遞鍵名參數和附加參數的重要性:因為如果將參數硬寫在腳本中,那么每次當參數改變的時候,都要重新發送腳本,即使腳本的主體並沒有改變,相反,通過使用正確的格式來傳遞鍵名參數和附加參數,就可以在腳本

主體不變的情況下,直接使用 EVALSHA 命令對腳本進行復用,免去了無謂的帶寬消耗。

(4). 腳本緩存

 A. Redis 保證所有被運行過的腳本都會被永久保存在腳本緩存當中,這意味着,當 EVAL命令在一個 Redis 實例上成功執行某個腳本之后,隨后針對這個腳本的所有 EVALSHA 命令都會成功執行。

 B. 刷新腳本緩存的唯一辦法是顯式地調用 SCRIPT FLUSH 命令,這個命令會清空運行過的所有腳本的緩存。通常只有在雲計算環境中,Redis 實例被改作其他客戶或者別的應用程序的實例時,才會執行這個命令。

 C. 緩存可以長時間儲存而不產生內存問題的原因是,它們的體積非常小,而且數量也非常少,即使腳本在概念上類似於實現一個新命令,即使在一個大規模的程序里有成百上千的腳本,即使這些腳本會經常修改,即便如此,儲存這些腳本的內存仍然是微不足道的。

 D. 事實上,用戶會發現 Redis 不移除緩存中的腳本實際上是一個好主意。比如說,對於一個和 Redis 保持持久化鏈接(persistent connection)的程序來說,它可以確信,執行過一次的腳本會一直保留在內存當中,因此它可以在流水線中使用 EVALSHA 命令而不必擔心因為找不到所需的腳本而產生錯誤(稍候我們會看到在流水線中執行腳本的相關問題)。

(5). 全局變量保護

 為了防止不必要的數據泄漏進 Lua 環境, Redis 腳本不允許創建全局變量。如果一個腳本需要在多次執行之間維持某種狀態,它應該使用 Redis key 來進行狀態保存。

 實現全局變量保護並不難,不過有時候還是會不小心而為之。一旦用戶在腳本中混入了Lua 全局狀態,那么 AOF 持久化和復制(replication)都會無法保證,所以,請不要使用全局變量。避免引入全局變量的一個訣竅是:將腳本中用到的所有變量都使用 local 關鍵字定義
為局部變量。

(6). 純函數腳本、內置Lua庫、redis日志、沙箱和最大執行時間

 詳見redis的幫助文檔了。

2.  腳本指令

(1). eval

 執行lua腳本

#格式
eval script numkeys key [key ...] arg [arg ...]
#參數說明
#script:
是一段 Lua 5.1 腳本程序,它會被運行在 Redis 服務器上下文中,這段腳本不必(也不應該)定義為一個 Lua 函數。
#numkeys:用於指定鍵名參數的個數。
#key:鍵名參數,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數可以在 Lua 中通過全局變量 KEYS 數組,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
#arg:全局變量,可以在 Lua 中通過全局變量 ARGV 數組訪問,訪問的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)
 
        

實操1:

實操2: 在lua腳本中可以通過兩個不同的函數調用redis命令,分別是:redis.call()  和  redis.pcall()

#寫法1
eval "return redis.call('set','name1','ypf1')" 0
#寫法2 (推薦!!)
eval "return redis.call('set',KEYS[1],'ypf2')" 1 name2

剖析:

 寫法1違反了EVAL 命令的語義,因為腳本里使用的所有鍵都應該由 KEYS 數組來傳遞。

 要求使用正確的形式來傳遞鍵(key)是有原因的,因為不僅僅是 EVAL 這個命令,所有的 Redis 命令,在執行之前都會被分析,以此來確定命令會對哪些鍵進行操作。因此,對於 EVAL 命令來說,必須使用正確的形式來傳遞鍵,才能確保分析工作正確地執行。除此之外,使用正確的形式來傳遞鍵還有很多其他好處,它的一個特別重要的用途就是確保 Redis 集群可以將你的請求發送到正確的集群節點。(對 Redis 集群的工作還在進行當中,但是腳本功能被設計成可以與集群功能保持兼容。)不過,這條規矩並不是強制性的,從而使得用戶有機會濫用(abuse) Redis 單實例配置(single instance configuration),代價是這樣寫出的腳本不能被 Redis 集群所兼容。

(2). evalsha

 根據給定的 sha1 校驗碼,對緩存在服務器中的腳本進行求值

#格式
evalsha sha1 numkeys key [key ...] arg [arg ...]

(3). script load

 將腳本 script 添加到腳本緩存中,但並不立即執行這個腳本。

 EVAL 命令也會將腳本添加到腳本緩存中,但是它會立即對輸入的腳本進行求值。如果給定的腳本已經在緩存里面了,那么不做動作。在腳本被加入到緩存之后,通過 EVALSHA 命令,可以使用腳本的 SHA1 校驗和來調用這個腳本。腳本可以在緩存中保留無限長的時間,直到執行 SCRIPT FLUSH 為止。

(4). script exists

 判斷腳本是否已經添加到緩存中去了,1代表已經添加,0代表沒有添加。

(5). script kill

 殺死當前正在運行的 Lua 腳本,當且僅當這個腳本沒有執行過任何寫操作時,這個命令才生效。

 這個命令主要用於終止運行時間過長的腳本,比如一個因為 BUG 而發生無限 loop 的腳本,諸如此類。SCRIPT KILL 執行之后,當前正在運行的腳本會被殺死,執行這個腳本的客戶端會從EVAL 命令的阻塞當中退出,並收到一個錯誤作為返回值。

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

(6). script flush

 清除所有 Lua 腳本緩存

 

參考redis文檔。.....................

 

二. Lua語法學習

1. 介紹

 Lua 是一種輕量小巧的腳本語言,用標准C語言編寫並以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。常見的數據類型如下:

redis和lua之間的數據類型存在一 一對應關系:

 

 

2. 好處

 (1). 減少網絡開銷:本來多次網絡請求的操作,可以用一個請求完成,原先多次次請求的邏輯都放在redis服務器上完成,使用腳本,減少了網絡往返時延。

 (2). 原子操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。

 (3). 復用:客戶端發送的腳本會永久存儲在Redis中,意味着其他客戶端可以復用這一腳本而不需要使用代碼完成同樣的邏輯。

 (4).替代redis的事務功能:redis自帶的事務功能很雞肋,報錯不支持回滾,而redis的lua腳本幾乎實現了常規的事務功能,支持報錯回滾操作,官方推薦如果要使用redis的事務功能可以用redis lua替代。

官網原話

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

注:lua整合一系列redis操作, 是為了保證原子性, 即redis在處理這個lua腳本期間不能執行其它操作, 但是lua腳本自身假設中間某條指令出錯,並不會回滾的,會繼續往下執行或者報錯了

3. 基本語法

 (1). 基本結構,類似於js,前面聲明方法,后面調用方法。

 (2). 獲取傳過來的參數:ARGV[1]、ARGV[2] 依次類推,獲取傳過來的Key,用KEYS[1]來獲取。

 (3). 調用redis的api,用redis.call( )方法調用。

 (4). int類型轉換 tonumber

參考代碼:

local function seckillLimit()
--(1).獲取相關參數
-- 限制請求數量
local tLimits=tonumber(ARGV[1]);
-- 限制秒數
local tSeconds =tonumber(ARGV[2]);
-- 受限商品key
local limitKey = ARGV[3];
--(2).執行判斷業務
local myLimitCount = redis.call('INCR',limitKey);

-- 僅當第一個請求進來設置過期時間
if (myLimitCount ==1) 
then
redis.call('expire',limitKey,tSeconds) --設置緩存過期
end;   --對應的是if的結束

-- 超過限制數量,返回失敗
if (myLimitCount > tLimits) 
then
return 0;  --失敗
end;   --對應的是if的結束

end;   --對應的是整個代碼塊的結束


--1. 單品限流調用
local status1 = seckillLimit();
if status1 == 0 then
return 2;   --失敗
end

 

詳細語法參考菜鳥教程:https://www.runoob.com/lua/lua-tutorial.html 

 

三. 秒殺案例腳本分析

 

 詳見:https://www.cnblogs.com/yaopengfei/p/13826478.html

 

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

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



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