佳木斯湛栽影视文化发展公司

主頁(yè) > 知識(shí)庫(kù) > 詳解Redis中Lua腳本的應(yīng)用和實(shí)踐

詳解Redis中Lua腳本的應(yīng)用和實(shí)踐

熱門(mén)標(biāo)簽:呼叫中心市場(chǎng)需求 地方門(mén)戶網(wǎng)站 百度競(jìng)價(jià)排名 鐵路電話系統(tǒng) 服務(wù)外包 AI電銷(xiāo) 網(wǎng)站排名優(yōu)化 Linux服務(wù)器

引言

前段時(shí)間組內(nèi)有個(gè)投票的產(chǎn)品,上線前考慮欠缺,導(dǎo)致被刷票嚴(yán)重。后來(lái),通過(guò)研究,發(fā)現(xiàn)可以通過(guò) redis lua 腳本實(shí)現(xiàn)限流,這里將 redis lua 腳本相關(guān)的知識(shí)分享出來(lái),講的不到位的地方還望斧正。

redis lua 腳本相關(guān)命令

這一小節(jié)的內(nèi)容是基本命令,可粗略閱讀后跳過(guò),等使用的時(shí)候再回來(lái)查詢

redis 自 2.6.0 加入了 lua 腳本相關(guān)的命令,EVAL、EVALSHA、SCRIPT EXISTSSCRIPT FLUSH、SCRIPT KILL、SCRIPT LOAD,自 3.2.0 加入了 lua 腳本的調(diào)試功能和命令SCRIPT DEBUG。這里對(duì)命令做下簡(jiǎn)單的介紹。

  • EVAL執(zhí)行一段lua腳本,每次都需要將完整的lua腳本傳遞給redis服務(wù)器。
  • SCRIPT LOAD將一段lua腳本緩存到redis中并返回一個(gè)tag串,并不會(huì)執(zhí)行。
  • EVALSHA執(zhí)行一個(gè)腳本,不過(guò)傳入?yún)?shù)是「2」中返回的tag,節(jié)省網(wǎng)絡(luò)帶寬
  • SCRIPT EXISTS判斷「2」返回的tag串是否存在服務(wù)器中。
  • SCRIPT FLUSH清除服務(wù)器上的所有緩存的腳本。
  • SCRIPT KILL殺死正在運(yùn)行的腳本。
  • SCRIPT DEBUG設(shè)置調(diào)試模式,可設(shè)置同步、異步、關(guān)閉,同步會(huì)阻塞所有請(qǐng)求。

生產(chǎn)環(huán)境中,推薦使用EVALSHA,相較于EVAL的每次發(fā)送腳本主體、浪費(fèi)帶寬,會(huì)更高效。這里要注意SCRIPT KILL,殺死正在運(yùn)行腳本的時(shí)候,如果腳本執(zhí)行過(guò)寫(xiě)操作了,這里會(huì)殺死失敗,因?yàn)檫@違反了 redis lua 腳本的原子性。調(diào)試盡量放在測(cè)試環(huán)境完成之后再發(fā)布到生產(chǎn)環(huán)境,在生產(chǎn)環(huán)境調(diào)試千萬(wàn)不要使用同步模式,原因下文會(huì)詳細(xì)討論。

Redis 中 lua 腳本的書(shū)寫(xiě)和調(diào)試

redis lua 腳本是對(duì)其現(xiàn)有命令的擴(kuò)充,單個(gè)命令不能完成、需要多個(gè)命令,但又要保證原子性的動(dòng)作可以用腳本來(lái)實(shí)現(xiàn)。腳本中的邏輯一般比較簡(jiǎn)單,不要加入太復(fù)雜的東西,因?yàn)?redis 是單線程的,當(dāng)腳本執(zhí)行的時(shí)候,其他命令、腳本需要等待直到當(dāng)前腳本執(zhí)行完成。因此,對(duì) lua 的語(yǔ)法也不需完全了解,了解基本的使用就足夠了,這里對(duì) lua 語(yǔ)法不做過(guò)多介紹,會(huì)穿插到腳本示例里面。

一個(gè)秒殺搶購(gòu)示例

假設(shè)有一個(gè)秒殺活動(dòng),商品庫(kù)存 100,每個(gè)用戶 uid 只能搶購(gòu)一次。設(shè)計(jì)搶購(gòu)流程如下:

  1. 先通過(guò) uid 判斷是否已經(jīng)搶過(guò),已經(jīng)搶過(guò)返回0結(jié)束。
  2. 判斷商品剩余庫(kù)存是否大于0,是的話進(jìn)入「3」,否的話返回0結(jié)束。
  3. 將用戶 uid 加入已購(gòu)用戶set中。
  4. 物品數(shù)量減一,返回成功1結(jié)束。
local goodsSurplus
local flag
-- 判斷用戶是否已搶過(guò)
local buyMembersKey  = tostring(KEYS[1])
local memberUid    = tonumber(ARGV[1])
local goodsSurplusKey = tostring(KEYS[2])
local hasBuy = redis.call("sIsMember", buyMembersKey, memberUid)

-- 已經(jīng)搶購(gòu)過(guò),返回0
if hasBuy ~= 0 then
 return 0
end

-- 準(zhǔn)備搶購(gòu)
goodsSurplus = redis.call("GET", goodsSurplusKey)
if goodsSurplus == false then
 return 0
end

-- 沒(méi)有剩余可搶購(gòu)物品
goodsSurplus = tonumber(goodsSurplus)
if goodsSurplus = 0 then
 return 0
end

flag = redis.call("SADD", buyMembersKey, memberUid)
flag = redis.call("DECR", goodsSurplusKey)

return 1

即使不了解 lua,相信你也可以將上面的腳本看個(gè)一二,其中--開(kāi)始的是單行注釋。local用來(lái)聲明局部變量,redis lua 腳本中的所有變量都應(yīng)該聲明為local xxx,避免在持久化、復(fù)制的時(shí)候產(chǎn)生各種問(wèn)題。KEYSARGV是兩個(gè)全局變量,就像 PHP 中的$argc、$argv一樣,腳本執(zhí)行時(shí)傳入的參數(shù)會(huì)寫(xiě)入這兩個(gè)變量,供我們?cè)谀_本中使用。redis.call用來(lái)執(zhí)行 redis 現(xiàn)有命令,傳參跟 redis 命令行執(zhí)行時(shí)傳入?yún)?shù)順序一致。

另外 redis lua 腳本中用到 lua table 的地方還比較多,這里要注意,lua 腳本中的 table 下標(biāo)是從 1 開(kāi)始的,比如KEYS、ARGV,這里跟其他語(yǔ)言不一樣,需要注意。

對(duì)于主要使用 PHP 這種弱類(lèi)型語(yǔ)言開(kāi)發(fā)同學(xué)來(lái)說(shuō),一定要注意變量的類(lèi)型,不同類(lèi)型比較的時(shí)候可能會(huì)出現(xiàn)類(lèi)似attempt to compare string with number的提示,這個(gè)時(shí)候使用 lua 的tonumber將字符串轉(zhuǎn)換為數(shù)字在進(jìn)行比較即可。比如我們使用GET去獲取一個(gè)值,然后跟 0 比較大小,就需要將獲取出來(lái)的字符串轉(zhuǎn)換為數(shù)字。

在調(diào)試之前呢,我們先看看效果,將上面的代碼保存到 lua 文件中/path/to/buy.lua,然后運(yùn)行redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984即可執(zhí)行腳本,執(zhí)行之后返回-1,因?yàn)槲覀兾丛O(shè)置商品數(shù)量,set goodsSurplus 5之后再次執(zhí)行,效果如下:

➜ ~ redis-cli set goodsSurplus 5
OK
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984
(integer) 0
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742983
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742982
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742981
(integer) 1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742980
(integer) -1
➜ ~ redis-cli --eval /path/to/buy.lua hadBuyUids goodsSurplus , 58247
(integer) -1

在命令行運(yùn)行腳本的時(shí)候,腳本后面?zhèn)魅氲氖菂?shù),通過(guò) , 分隔為兩組,前面是鍵,后面是值,這兩組分別寫(xiě)入KEYSARGV。分隔符一定要看清楚了,逗號(hào)前后都有空格,漏掉空格會(huì)讓腳本解析傳入?yún)?shù)異常。

debug 調(diào)試

上一小節(jié),我們寫(xiě)了很長(zhǎng)一段 redis lua 腳本,怎么調(diào)試呢,有沒(méi)有像 GDB 那樣的調(diào)試工具呢,答案是肯定的。redis 從 v3.2.0 開(kāi)始支持 lua debugger,可以加斷點(diǎn)、print 變量信息、展示正在執(zhí)行的代碼......我們結(jié)合上一小節(jié)的腳本,來(lái)詳細(xì)說(shuō)說(shuō) redis 中 lua 腳本的調(diào)試。

如何進(jìn)入調(diào)試模式

執(zhí)行redis-cli --ldb --eval /path/to/buy.lua hadBuyUids goodsSurplus , 5824742984,進(jìn)入調(diào)試模式,比之前執(zhí)行的時(shí)候多了參數(shù)--ldb,這個(gè)參數(shù)是開(kāi)啟 lua dubegger 的意思,這個(gè)模式下 redis 會(huì) fork 一個(gè)進(jìn)程進(jìn)入隔離環(huán)境,不會(huì)影響 redis 正常提供服務(wù),但調(diào)試期間,原始 redis 執(zhí)行命令、腳本的結(jié)果也不會(huì)體現(xiàn)到 fork 之后的隔離環(huán)境之中。因此呢,還有另外一種調(diào)試模式--ldb-sync-mode,也就是前面提到的同步模式,這個(gè)模式下,會(huì)阻塞 redis 上所有的命令、腳本,直到腳本退出,完全模擬了正式環(huán)境使用時(shí)候的情況,使用的時(shí)候務(wù)必注意這點(diǎn)。

調(diào)試命令詳解

這一小節(jié)的內(nèi)容是調(diào)試時(shí)候的詳細(xì)命令,可以粗略閱讀后跳過(guò),等使用的時(shí)候再回來(lái)查詢

幫助信息

[h]elp

調(diào)試模式下,輸入h或者help展示調(diào)試模式下的全部可用指令。

流程相關(guān)

[s]tep 、 [n]ext 、 [c]continue

執(zhí)行當(dāng)前行代碼,并停留在下一行,如下所示

* Stopped at 4, stop reason = step over
-> 4  local buyMembersKey  = tostring(KEYS[1])
lua debugger> n
* Stopped at 5, stop reason = step over
-> 5  local memberUid    = tonumber(ARGV[1])
lua debugger> n
* Stopped at 6, stop reason = step over
-> 6  local goodsSurplusKey = tostring(KEYS[2])
lua debugger> s
* Stopped at 7, stop reason = step over
-> 7  local hasBuy = redis.call("sIsMember", buyMembersKey, memberUid)

continue從當(dāng)前行開(kāi)始執(zhí)行代碼直到結(jié)束或者碰到斷點(diǎn)。

展示相關(guān)

[l]list 、 [l]list [line] 、 [l]list [line] [ctx] 、 [w]hole

展示當(dāng)前行附近的代碼,[line]是重新指定中心行,[ctx]是指定展示中心行周?chē)鷰仔写a。[w]hole是展示所有行代碼

打印相關(guān)

[p]rint 、 [p]rint var>

打印當(dāng)前所有局部變量,var>是打印指定變量,如下所示:

lua debugger> print
value> goodsSurplus = nil
value> flag = nil
value> buyMembersKey = "hadBuyUids"
value> memberUid = 58247
lua debugger> print buyMembersKey
value> "hadBuyUids"

斷點(diǎn)相關(guān)

[b]reak 、 [b]reak line> 、 [b]reak -line> 、 [b]reak 0

展示斷點(diǎn)、像指定行添加斷點(diǎn)、刪除指定行的斷點(diǎn)、刪除所有斷點(diǎn)

其他命令

[r]edis cmd> 、 [m]axlen [len] 、 [a]bort 、 [e]eval code> 、 [t]race
  • 在調(diào)試其中執(zhí)行 redis 命令
  • 設(shè)置展示內(nèi)容的最大長(zhǎng)度,0表示不限制
  • 退出調(diào)試模式,同步模式下(設(shè)置了參數(shù)--ldb-sync-mode)修改會(huì)保留。
  • 執(zhí)行一行 lua 代碼。
  • 展示執(zhí)行棧。

詳細(xì)說(shuō)下[m]axlen [len]命令,如下代碼:

local myTable = {}
local count = 0
while count  1000 do
  myTable[count] = count
  count = count + 1
end

return 1

在最后一行打印斷點(diǎn),執(zhí)行print可以看到,輸出了一長(zhǎng)串內(nèi)容,我們執(zhí)行maxlen 10之后,再次執(zhí)行print可以看到打印的內(nèi)容變少了,設(shè)置為maxlen 0之后,再次執(zhí)行可以看到所有的內(nèi)容全部展示了。

詳細(xì)說(shuō)下[t]race命令,代碼如下:

local function func1(num)
 num = num + 1
 return num
end

local function func2(num)
 num = func1(num)
 num = num + 1
 return num
end

func2(123)

執(zhí)行b 2在 func1 中打斷點(diǎn),然后執(zhí)行c,斷點(diǎn)地方停頓,再次執(zhí)行t,可以到如下信息:

lua debugger> t
In func1:
->#3   return num
From func2:
  7   num = func1(num)
From top level:
  12 func2(123)

請(qǐng)求限流

至此,算是對(duì) redis lua 腳本有了基本的認(rèn)識(shí),基本語(yǔ)法、調(diào)試也做了了解,接下來(lái)就實(shí)現(xiàn)一個(gè)請(qǐng)求限流器。流程和代碼如下:

--[[
 傳入?yún)?shù):
 業(yè)務(wù)標(biāo)識(shí)
 ip
 限制時(shí)間
 限制時(shí)間內(nèi)的訪問(wèn)次數(shù)
]]--
local busIdentify  = tostring(KEYS[1])
local ip      = tostring(KEYS[2])
local expireSeconds = tonumber(ARGV[1])
local limitTimes  = tonumber(ARGV[2])

local identify = busIdentify .. "_" .. ip

local times   = redis.call("GET", identify)

--[[
 獲取已經(jīng)記錄的時(shí)間
 獲取到繼續(xù)判斷是否超過(guò)限制
 超過(guò)限制返回0
 否則加1,返回1
]]--
if times ~= false then
 times = tonumber(times)
 if times >= limitTimes then
  return 0
 else
  redis.call("INCR", identify)
  return 1
 end
end

-- 不存在的話,設(shè)置為1并設(shè)置過(guò)期時(shí)間
local flag = redis.call("SETEX", identify, expireSeconds, 1)

return 1

將上面的 lua 腳本保存到/path/to/limit.lua,執(zhí)行redis-cli --eval /path/to/limit.lua limit_vgroup 192.168.1.19 , 10 3,表示 limit_vgroup 這個(gè)業(yè)務(wù),192.168.1.1 這個(gè) ip 每 10 秒鐘限制訪問(wèn)三次。

好了,至此,一個(gè)請(qǐng)求限流功能就完成了,連續(xù)執(zhí)行三次之后上面的程序會(huì)返回 0,過(guò) 10 秒鐘在執(zhí)行,又可以返回 1,這樣便達(dá)到了限流的目的。

有同學(xué)可能會(huì)說(shuō)了,這個(gè)請(qǐng)求限流功能還有值得優(yōu)化的地方,如果連續(xù)的兩個(gè)計(jì)數(shù)周期,第一個(gè)周期的最后請(qǐng)求 3 次,接著馬上到第二個(gè)周期了,又可以請(qǐng)求了,這個(gè)地方如何優(yōu)化呢,我們接著往下看。

請(qǐng)求限流優(yōu)化

上面的計(jì)數(shù)器法簡(jiǎn)單粗暴,但是存在臨界點(diǎn)的問(wèn)題。為了解決這個(gè)問(wèn)題,引入類(lèi)似滑動(dòng)窗口的概念,讓統(tǒng)計(jì)次數(shù)的周期是連續(xù)的,可以很好的解決臨界點(diǎn)的問(wèn)題,滑動(dòng)窗口原理如下圖所示:

建立一個(gè) redis list 結(jié)構(gòu),其長(zhǎng)度等價(jià)于訪問(wèn)次數(shù),每次請(qǐng)求時(shí),判斷 list 結(jié)構(gòu)長(zhǎng)度是否超過(guò)限制次數(shù),未超過(guò)的話,直接加到隊(duì)首返回成功,否則,判斷隊(duì)尾一條數(shù)據(jù)是否已經(jīng)超過(guò)限制時(shí)間,未超過(guò)直接返回失敗,超過(guò)刪除隊(duì)尾元素,將此次請(qǐng)求時(shí)間插入隊(duì)首,返回成功。

local busIdentify  = tostring(KEYS[1])
local ip      = tostring(KEYS[2])
local expireSeconds = tonumber(ARGV[1])
local limitTimes  = tonumber(ARGV[2])
-- 傳入額外參數(shù),請(qǐng)求時(shí)間戳
local timestamp   = tonumber(ARGV[3])
local lastTimestamp

local identify = busIdentify .. "_" .. ip
local times   = redis.call("LLEN", identify)
if times  limitTimes then
 redis.call("RPUSH", identify, timestamp)
 return 1
end

lastTimestamp = redis.call("LRANGE", identify, 0, 0)
lastTimestamp = tonumber(lastTimestamp[1])

if lastTimestamp + expireSeconds >= timestamp then
 return 0
end

redis.call("LPOP", identify)
redis.call("RPUSH", identify, timestamp)

return 1

上面的 lua 腳本保存到/path/to/limit_fun.lua,執(zhí)行redis-cli --eval /path/to/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999即可。

最開(kāi)始,我想著把時(shí)間戳計(jì)算redis.call("TIME")也放入 redis lua 腳本中,后來(lái)發(fā)現(xiàn)使用的時(shí)候 redis 會(huì)報(bào)錯(cuò),這是因?yàn)?redis 默認(rèn)情況復(fù)制 lua 腳本到備機(jī)和持久化中,如果腳本是一個(gè)非純函數(shù)(pure function),備庫(kù)中執(zhí)行的時(shí)候或者宕機(jī)恢復(fù)的時(shí)候可能產(chǎn)生不一致的情況,這里可以類(lèi)比 mysql 中基于 SQL 語(yǔ)句的復(fù)制模式。redis 在 3.2 版本中加入了redis.replicate_commands函數(shù)來(lái)解決這個(gè)問(wèn)題,在腳本第一行執(zhí)行這個(gè)函數(shù),redis 會(huì)將修改數(shù)據(jù)的命令收集起來(lái),然后用MULTI/EXEC包裹起來(lái),這種方式稱為script effects replication,這個(gè)類(lèi)似于 mysql 中的基于行的復(fù)制模式,將非純函數(shù)的值計(jì)算出來(lái),用來(lái)持久化和主從復(fù)制。我們這里將變動(dòng)參數(shù)提到調(diào)用方這里,調(diào)用者傳入時(shí)間戳來(lái)解決這個(gè)問(wèn)題。

另外,redis 從版本 5 開(kāi)始,默認(rèn)支持script effects replication,不需要在第一行調(diào)用開(kāi)啟函數(shù)了。如果是耗時(shí)計(jì)算,這樣當(dāng)然很好,同步、恢復(fù)的時(shí)候只需要計(jì)算一次后邊就不用計(jì)算了,但是如果是一個(gè)循環(huán)生成的數(shù)據(jù),可能在同步的時(shí)候會(huì)浪費(fèi)更多的帶寬,沒(méi)有腳本來(lái)的更直接,但這種情況應(yīng)該比較少。

至此,腳本優(yōu)化完成了,但我又想到一個(gè)問(wèn)題,我們的環(huán)境是單機(jī)環(huán)境,如果是分布式環(huán)境的話,腳本怎么執(zhí)行、何處理呢,接下來(lái)一節(jié),我們來(lái)討論下這個(gè)問(wèn)題。

集群環(huán)境中 lua 處理

redis 集群中,會(huì)將鍵分配的不同的槽位上,然后分配到對(duì)應(yīng)的機(jī)器上,當(dāng)操作的鍵為一個(gè)的時(shí)候,自然沒(méi)問(wèn)題,但如果操作的鍵為多個(gè)的時(shí)候,集群如何知道這個(gè)操作落到那個(gè)機(jī)器呢?比如簡(jiǎn)單的mget命令,mget test1 test2 test3,還有我們上面執(zhí)行腳本時(shí)候傳入多個(gè)參數(shù),帶著這個(gè)問(wèn)題我們繼續(xù)。

首先用 docker 啟動(dòng)一個(gè) redis 集群,docker pull grokzen/redis-cluster,拉取這個(gè)鏡像,然后執(zhí)行docker run -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 --name redis-cluster-script -e "IP=0.0.0.0" grokzen/redis-cluster啟動(dòng)這個(gè)容器,這個(gè)容器啟動(dòng)了一個(gè) redis 集群,3 主 3 從。

我們從任意一個(gè)節(jié)點(diǎn)進(jìn)入集群,比如redis-cli -c -p 7003,進(jìn)入后執(zhí)行cluster nodes可以看到集群的信息,我們鏈接的是從庫(kù),執(zhí)行set lua fun,有同學(xué)可能會(huì)問(wèn)了,從庫(kù)也可以執(zhí)行寫(xiě)嗎,沒(méi)問(wèn)題的,集群會(huì)計(jì)算出 lua 這個(gè)鍵屬于哪個(gè)槽位,然后定向到對(duì)應(yīng)的主庫(kù)。

執(zhí)行mset lua fascinating redis powerful,可以看到集群反回了錯(cuò)誤信息,告訴我們本次請(qǐng)求的鍵沒(méi)有落到同一個(gè)槽位上

(error) CROSSSLOT Keys in request don't hash to the same slot

同樣,還是上面的 lua 腳本,我們加上集群端口號(hào),執(zhí)行redis-cli -p 7000 --eval /tmp/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999,一樣返回上面的錯(cuò)誤。

針對(duì)這個(gè)問(wèn)題,redis官方為我們提供了hash tag這個(gè)方法來(lái)解決,什么意思呢,我們?nèi)℃I中的一段來(lái)計(jì)算 hash,計(jì)算落入那個(gè)槽中,這樣同一個(gè)功能不同的 key 就可以落入同一個(gè)槽位了,hash tag 是通過(guò){}這對(duì)括號(hào)括起來(lái)的字符串,比如上面的,我們改為mset lua{yes} fascinating redis{yes} powerful,就可以執(zhí)行成功了,我這里 mset 這個(gè)操作落到了 7002 端口的機(jī)器。

同理,我們對(duì)傳入腳本的鍵名做 hash tag 處理就可以了,這里要注意不僅傳入鍵名要有相同的 hash tag,里面實(shí)際操作的 key 也要有相同的 hash tag,不然會(huì)報(bào)錯(cuò)Lua script attempted to access a non local key in a cluster node,什么意思呢,就拿我們上面的例子來(lái)說(shuō),執(zhí)行的時(shí)候如下所示,可以看到 , 前面的兩個(gè)鍵都加了 hash tag —— yes,這樣沒(méi)問(wèn)題,因?yàn)槟_本里面只是用了一個(gè)拼接的 key —— limit_vgroup{yes}_192.168.1.19{yes}

redis-cli -c -p 7000 --eval /tmp/limit_fun.lua limit_vgroup{yes} 192.168.1.19{yes} , 10 3 1548660999

如果我們?cè)谀_本里面加上redis.call("GET", "yesyes")(別讓這個(gè)鍵跟我們拼接的鍵落在一個(gè)solt),可以看到就報(bào)了上面的錯(cuò)誤,所以在執(zhí)行腳本的時(shí)候,只要傳入?yún)?shù)鍵、腳本里面執(zhí)行 redis 命令時(shí)候的鍵有相同的 hash tag 即可。

另外,這里有個(gè) hash tag 規(guī)則:

鍵中包含{字符;建中包含{字符,并在{字符右邊;并且{,}之間有至少一個(gè)字符,之間的字符就用來(lái)做鍵的 hash tag。

所以,鍵limit_vgroup{yes}_192.168.1.19{yes}的 hash tag 是 yes。foo{}{bar}鍵的 hash tag就是它本身。foo{{bar}}鍵的 hash tag 是 {bar

使用 golang 連接使用 redis

這里我們使用 golang 實(shí)例展示下,通過(guò)ForEachMaster將 lua 腳本緩存到集群中的每個(gè) node,并保存返回的 sha 值,以后通過(guò) evalsha 去執(zhí)行代碼。

package main

import (
  "github.com/go-redis/redis"
  "fmt"
)

func createScript() *redis.Script {
  script := redis.NewScript(`
    local busIdentify  = tostring(KEYS[1])
    local ip      = tostring(KEYS[2])
    local expireSeconds = tonumber(ARGV[1])
    local limitTimes  = tonumber(ARGV[2])
    -- 傳入額外參數(shù),請(qǐng)求時(shí)間戳
    local timestamp   = tonumber(ARGV[3])
    local lastTimestamp

    local identify = busIdentify .. "_" .. ip
    local times   = redis.call("LLEN", identify)
    if times  limitTimes then
     redis.call("RPUSH", identify, timestamp)
     return 1
    end

    lastTimestamp = redis.call("LRANGE", identify, 0, 0)
    lastTimestamp = tonumber(lastTimestamp[1])

    if lastTimestamp + expireSeconds >= timestamp then
     return 0
    end

    redis.call("LPOP", identify)
    redis.call("RPUSH", identify, timestamp)

    return 1    
  `)

  return script
}

func scriptCacheToCluster(c *redis.ClusterClient) string {
  script := createScript()
  var ret string

  c.ForEachMaster(func(m *redis.Client) error {
    if result, err := script.Load(m).Result(); err != nil {
      panic("緩存腳本到主節(jié)點(diǎn)失敗")
    } else {
      ret = result
    }
    return nil
  })

  return ret

}

func main() {
  redisdb := redis.NewClusterClient(redis.ClusterOptions{
    Addrs: []string{
      ":7000",
      ":7001",
      ":7002",
      ":7003",
      ":7004",
      ":7005",
    },
  })
  // 將腳本緩存到所有節(jié)點(diǎn),執(zhí)行一次拿到結(jié)果即可
  sha := scriptCacheToCluster(redisdb)

  // 執(zhí)行緩存腳本
  ret := redisdb.EvalSha(sha, []string{
    "limit_vgroup{yes}",
    "192.168.1.19{yes}",
  }, 10, 3,1548660999)

  if result, err := ret.Result(); err != nil {
    fmt.Println("發(fā)生異常,返回值:", err.Error())
  } else {
    fmt.Println("返回值:", result)
  }

 // 示例錯(cuò)誤情況,sha 值不存在
  ret1 := redisdb.EvalSha(sha + "error", []string{
    "limit_vgroup{yes}",
    "192.168.1.19{yes}",
  }, 10, 3,1548660999)

  if result, err := ret1.Result(); err != nil {
    fmt.Println("發(fā)生異常,返回值:", err.Error())
  } else {
    fmt.Println("返回值:", result)
  }
}

執(zhí)行上面的代碼,返回值如下:

返回值: 0
發(fā)生異常,返回值: NOSCRIPT No matching script. Please use EVAL.

好了,目前為止,相信你對(duì) redis lua 腳本已經(jīng)有了很好的了解,可以實(shí)現(xiàn)一些自己想要的功能了,感謝大家的閱讀。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

您可能感興趣的文章:
  • 詳解利用redis + lua解決搶紅包高并發(fā)的問(wèn)題
  • 簡(jiǎn)介L(zhǎng)ua腳本與Redis數(shù)據(jù)庫(kù)的結(jié)合使用
  • Redis執(zhí)行Lua腳本的好處與示例代碼
  • redis中如何使用lua腳本讓你的靈活性提高5個(gè)逼格詳解
  • 利用Lua定制Redis命令的方法詳解
  • Redis如何使用lua腳本實(shí)例教程
  • Nginx利用Lua+Redis實(shí)現(xiàn)動(dòng)態(tài)封禁IP的方法
  • Redis和Lua使用過(guò)程中遇到的小問(wèn)題
  • 通過(guò)redis的腳本lua如何實(shí)現(xiàn)搶紅包功能

標(biāo)簽:蘭州 仙桃 衡水 崇左 湖南 黃山 湘潭 銅川

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《詳解Redis中Lua腳本的應(yīng)用和實(shí)踐》,本文關(guān)鍵詞  ;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 收縮
    • 微信客服
    • 微信二維碼
    • 電話咨詢

    • 400-1100-266
    申扎县| 神木县| 金坛市| 九寨沟县| 博湖县| 保亭| 通渭县| 阆中市| 寿宁县| 沂源县| 会泽县| 陇川县| 嘉义市| 荆门市| 汽车| 宁海县| 武平县| 板桥市| 巴彦县| 汤阴县| 登封市| 莱阳市| 思南县| 永川市| 丰原市| 永清县| 斗六市| 习水县| 乌鲁木齐市| 浮山县| 凤翔县| 四川省| 鹰潭市| 高邑县| 仙桃市| 咸丰县| 宁武县| 阳朔县| 伽师县| 曲沃县| 延吉市|