天天看點

第十節: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() 在執行指令的過程中發生錯誤時,腳本會停止執行,并傳回一個腳本錯誤,錯誤的輸出資訊會說明錯誤造成的原因.

第十節:Redis 腳本、Lua文法學習、以及秒殺案例腳本分析
 B. redis.pcall() 出錯時并不引發(raise)錯誤,而是傳回一個帶 err 域的 Lua 表(table),用于表示錯誤
第十節:Redis 腳本、Lua文法學習、以及秒殺案例腳本分析

(3). 帶寬和EVALSHA

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

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

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

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

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

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

(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 關鍵字定義

為局部變量。

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

(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:

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

實操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 叢集所相容。

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

(2). evalsha

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

#格式
evalsha sha1 numkeys key [key ...] arg [arg ...]      
第十節:Redis 腳本、Lua文法學習、以及秒殺案例腳本分析

(3). script load

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

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

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

(4). script exists

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

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

(5). script kill

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

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

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

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

(6). script flush

 清除所有 Lua 腳本緩存

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

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

二. Lua文法學習

1. 介紹

 Lua 是一種輕量小巧的腳本語言,用标準C語言編寫并以源代碼形式開放, 其設計目的是為了嵌入應用程式中,進而為應用程式提供靈活的擴充和定制功能。常見的資料類型如下:

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

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

第十節:Redis 腳本、Lua文法學習、以及秒殺案例腳本分析
第十節: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 : 原創部落格請在轉載時保留原文連結或在文章開頭加上本人部落格位址,否則保留追究法律責任的權利。