本文主要是分享在實際工做中同事遇到的問題案例;活動組在作活動時,開發人員未考慮到接口併發場景,致使由於一些用戶在實際抽獎(土豪通常都是狂抽)過程當中對餘額產生了增長/減小的操做,致使緩存的餘額出現異常;經過我review代碼發現,開發者在更新緩存時:先get後set或者incrby,致使併發場景下get的值是一致的,因此緩存異常。php
那麼針對這種我進行了改進使用:redis+lua腳本實現原子性保證餘額數據正常。本文將跟你們一塊兒學習Redis使用lua腳本的應用:html
Redis是高性能的key-value內存數據庫,它幫助咱們解決了大部分業務問題;提供豐富的指令集合,據官網上統計有200多個命令。這些命令顯然已經知足了咱們的常規的業務場景需求。可是在某些特殊的場景下,業務須要原子性操做,redis原有的命令是沒法完成,因此須要額外開發實現原子操做。redis
由於這樣的問題,Redis爲開發者提供了lua
腳本的支持,用戶能夠向服務器發送lua腳原本執行自定義動做,以此獲取腳本的響應數據。Redis自己又是單線程執行lua腳本,保證了lua腳本在處理邏輯過程當中不會被任意其它請求打斷。數據庫
Lua是一種輕量小巧的腳本語言,用標準C語言編寫並以源代碼形式開放。json
其設計目的就是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。由於普遍的應用於:遊戲開發、獨立應用腳本、Web 應用腳本、擴展和數據庫插件等。數組
好比:Lua腳本用在不少遊戲上,主要是Lua腳本能夠嵌入到其餘程序中運行,遊戲升級的時候,能夠直接升級腳本,而不用從新安裝遊戲。緩存
小夥們能夠查看我lua從入門到實戰專欄
:安全
專欄已經在計劃中出教程了,雖然看似寫的內容比較簡單,可是須要注重細節地方;lua語言每每在項目中出問題基本上細節較多。
可以使用版本:從 Redis 2.6.0
版本開始起;可經過內置的 Lua 解釋器,可使用 EVAL
命令對 Lua 腳本進行執行。
時間複雜度:根據腳本的複雜度而定(腳本儘可能簡潔)。
使用Lua腳本的好處:
### 共有三條優點
① 支持原子性操做 - Redis會將整個腳本做爲一個總體執行,中間不會被其餘請求插入。所以在腳本運行過程當中無需擔憂會出現競態條件,無需使用事務
② 下降網絡開銷 - 將多個請求經過腳本的形式一次發送到服務器,減小了網絡的時延
③ 腳本複用 - 客戶端發送的腳本可支持永久存在redis中,這樣其餘客戶端能夠複用這一腳本,而不須要使用代碼完成相同的邏輯。
複製代碼
Eval命令的基本語法以下:
## 命令格式
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
### 參數說明
① script Lua 5.1版本以上腳本程序,它會被運行在Redis服務器上下文中,這段腳本沒必要(也不該該)定義爲一個 Lua函數。
② numkeys 指用於指定鍵名參數的個數
③ key [key ...] 指要操做的鍵名,能夠指定多個,在lua腳本中經過KEYS[1], KEYS[2]獲取
④ arg [arg ...] 指附加參數,在lua腳本中經過全局變量 ARGV 數組訪問;例如:ARGV[1], ARGV[2]
複製代碼
### 既有key鍵也有附加參數
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 value2
1) "key1"
2) "key2"
3) "value1"
4) "value2"
### 只有附加參數
127.0.0.1:6379> eval "return {ARGV[1],ARGV[2]}" 0 'hello!' 'my name is amumu'
1) "hello!"
2) "my name is amumu"
### 注意
{} 在lua裏是指數據類型table,一樣相似常說的數組格式
複製代碼
### lua腳本中,可以使用兩個不一樣函數來執行redis命令
① redis.call()
-- 正確的設置方式 設置amumu值爲1000 60s過時
127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 amumu 1000 60
(integer) 1
127.0.0.1:6379> get amumu -- 設置成功
"1000"
127.0.0.1:6379> ttl amumu -- 剩餘存活時間
(integer) 49
127.0.0.1:6379> ttl amumu -- 已通過期
(integer) -2
-- 出現報錯的狀況
127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 0 amumu 1000 60
(error) ERR Error running script (call to f_6aeea4b3e96171ef835a78178fceadf1a5dbe345): @user_script:1: @user_script: 1: Lua redis() command arguments must be strings or integers
② redis.pcall()
-- 正確的設置方式 獲取amumu緩存值
127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 1 amumu
"1000"
-- 出現報錯的狀況
127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 0 amumu
(error) @user_script: 1: Lua redis() command arguments must be strings or integers
複製代碼
從上面的報錯狀況能夠看出來:redis.call() 和 redis.pcall() 的惟一區別在於它們對錯誤處理的不一樣
redis.call()在執行命令的過程當中發生錯誤時,腳本會直接中止執行,並返回一個腳本錯誤,會告訴你形成錯誤的緣由
redis.pcall()執行中出錯時並不引起致命錯誤,而是返回一個帶err域的Lua表,展現結果
127.0.0.1:6379> eval 'local dt = redis.pcall("HGETALL", KEYS[1]); local res = {type(dt)}; for i, v in ipairs(dt) do res[#res+1] = i; res[#res+1] = v; end; return res' 0
1) "table"
複製代碼
## 在命令行裏使用
127.0.0.1:6379> redis-cli --eval lua_filenames key1 key2 , arg1 arg2 ...
### 各單位請注意
① eval命令的後面參數是lua腳本文件,須要完整的文件名;例如hello.lua
② 跟前兩種方式不同的地方,不須要指定numkeys個數,而是使用,(英文逗號)隔開;注意,先後有空格。
複製代碼
演示示例以下:
## test.lua文件
-- 獲取緩存key
local _key = KEYS[1]
-- 獲取設置的值
local _val = ARGV[1]
-- 獲取緩存已經存在的值
local result = redis.call('GET', _key);
result = result and result or ""
-- 定義返回的結果變量
local text = ''
if result == '' then
return text
else
text = result .. _val
redis.call('SET', _key, text)
end
return text
複製代碼
開始命令行運行lua腳本文件,以下圖:
-- 第一次設置緩存未有值 因此返回了null
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '歡迎關注個人lua專欄!'
-- 設置默認值
➜ ~ redis-cli set lua:test '你們好,我是阿沐!'
OK
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '歡迎關注個人lua專欄!'
你們好,我是阿沐!歡迎關注個人lua專欄!
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '熱衷於經過項目實戰經驗分享!'
你們好,我是阿沐!歡迎關注個人lua專欄!熱衷於經過項目實戰經驗分享!
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '請必定要仔細閱讀,注意點很重要!'
你們好,我是阿沐!歡迎關注個人lua專欄!熱衷於經過項目實戰經驗分享!請必定要仔細閱讀,注意點很重要!
### 注意
實際上咱們在正常開發過程,可能不會採用此方法,更多的仍是在,項目裏使用仍是以腳本方式寫入;
這裏只是告訴你們有多種執行方式
複製代碼
實戰實例:
<?php
$script = <<<EOF local _key = KEYS[1] local _val = ARGV[1] local result = redis.call('GET', _key); result = result and result or "" local text = '' if result == '' then return text else text = result .. _val redis.call('SET', _key, text) end return text EOF;
// 獲取傳過來的變量
$text = isset($argv[1]) ? $argv[1] : '';
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$result = $redis->eval($script, array("lua:test", $text), 1);
echo $result;
### 執行結果集
➜ ~ /usr/local/opt/php@7.2/bin/php index.php
➜ ~ redis-cli set lua:test '你們好,我是阿沐!'
OK
➜ Desktop /usr/local/opt/php@7.2/bin/php index.php '歡迎關注個人lua專欄!'
你們好,我是阿沐!歡迎關注個人lua專欄!
複製代碼
參數說明:
Redis::eval(string script, [array keys], int keys_nums)
### 解析參數
① ::eval 執行命令
② script 要執行的lua腳本
③ keys 是指key值
④ keys_nums 參數爲KEYS的個數,用來區分KEYS和ARGV
複製代碼
緣由以下:
① 生成環境下,若是使用evalsha會比eval發送更小的數據包,佔用更少的網絡資源;
② eval每次都須要把腳本完整發送給redis,而evalsha只須要傳遞一個sha1值便可完成
檢測指定sha1是否已經存在:
## 基本命令
-- 指定一個或多個腳本的sha1校驗和,返回一個結果集含有0和1的列表(tab),表示校驗和所指定的腳本是否已經被保存在緩存當中
script exists sha1 [sha1 ...]
## 說明:
① redis版本號:必須大於等於 2.6.0
② 時間複雜度: O(n),n爲給定的sha1校驗和的數量
③ 結果集: 一個列表返回;0-不存在緩存中;2-存在緩存中;列表值跟結果集一一對應
複製代碼
演示示例:
-- 檢測sha1是否存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 0
-- 設置sha1值
127.0.0.1:6379> script load "return redis.call('get', 'lua:test')"
"b3e2eb6aa7bdb29e60f32cd153612a2887164b70"
-- 這時已經存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 1
-- 獲取多個返回列表
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70' 'd1cb717b6f16ad4e798430f98c31bc449222b946'
1) (integer) 1
2) (integer) 0
複製代碼
根據sha1值使用evalsha執行腳本:
## 基礎執行命令
-- 根據給定的 sha1 校驗碼,執行緩存在服務器中的腳本
evalsha sha1 numkeys key [key ...] arg [arg ...]
### 參數說明:
① sha1 經過上面 script load 生成的 sha1 校驗碼
② numkeys 指定鍵名參數的個數
③ key redis的鍵名
④ arg 附加參數,就是附帶進入腳本的變量值
複製代碼
演示示例:(使用時要注意,並非全部的腳本都適合緩存,形成沒必要要的內存浪費)
➜ ~ redis-cli --raw evalsha b3e2eb6aa7bdb29e60f32cd153612a2887164b70 0
你們好,我是阿沐!歡迎關注個人lua專欄!
複製代碼
有的時候咱們腳本出問題了,可是並不知道究竟是由於那一行代碼或者變量不對致使腳本中斷;我想大部分開發都會急躁,更有甚至者調試了半天一直看不出問題,會口吐芬芳等等。其實,在實際開發過程當中,咱們找不到問題所在的時候,必定要多打日誌,咱們只有經過日誌才能更好地找到問題所在,而不是一味的抱怨,抱怨解決不了任何問題。
Lua
腳本中,能夠經過調用 redis.log
函數來將錯誤信息寫入 Redis 日誌(log),命令以下:
redis.log(loglevel, message)
### 參數說明
① loglevel 錯誤等級,跟咱們日常開發同樣,bug、提示、警告等等
② message 錯誤信息,跟咱們日常開發異常拋出信息一致
複製代碼
其中 loglevel 參數能夠是如下任意一個值:
redis.LOG_DEBUG -- 會打印生成大量信息,適用於開發/測試階段
redis.LOG_VERBOSE -- 包含不少不太有用的信息,可是不像debug級別那麼混亂
redis.LOG_NOTICE -- 適度冗長,適用於生產環境
redis.LOG_WARNING -- 僅記錄很是重要、關鍵的警告消息
複製代碼
注意:只有設置的錯誤等級大於等於redis實例日誌等級纔會被記錄下來
演示示例:
27.0.0.1:6379> eval 'redis.log(redis.LOG_WARNING, "Something is wrong with this script.")' 0
(nil)
-- 在redis的日誌文件中查看:
1174:M 30 May 18:09:20.347 # Something is wrong with this script.
複製代碼
PS:這個經過日誌來看腳本問題,仍是比較重要的,若是不能一眼看出你腳本問題,那麼請儘可能的保證你多打點日誌查問題。
### lua語言中如何實現原子腳本
package.path = package.path..";~/redis-lua/src/?.lua" --redis.lua所在目錄
local json_encode = require "cjson" .encode
local redis = require("redis")
local reds, err = redis.connect('127.0.0.1',6379)
--- lua腳本檢測當前緩存值是否已 溢出 未溢出累加 不然 計算應增長多少值
local _introduce_myself = [[ local _key = KEYS[1] local cnt = ARGV[1] local limit = ARGV[2] local currnt_cnt = redis.call('GET', _key) currnt_cnt = tonumber(currnt_cnt) or 0 limit = tonumber(limit) or 0 cnt = tonumber(cnt) or 0 local ret = {"num", 0 ,"score", 0} if currnt_cnt < limit then local res = currnt_cnt + cnt if res >= limit then local diff = limit - currnt_cnt redis.call('INCRBY', _key, diff) ret[2] = limit ret[4] = diff else redis.call('INCRBY', _key, cnt) ret[2] = cnt end end return ret ]]
-- 執行lua腳本
function execute_script()
local key = 'lua:test' -- 緩存key
local count = 500 -- 每次增長數量
local limit = 1000 -- 限制總數溢出狀況
-- 執行腳本 更改執行緩存值,保證不超過限制的最大值 溢出則丟棄
local res , err = reds:eval(_introduce_myself, 1, key, count, limit)
print(json_encode(res))
end
execute_script() -- 調用execute_script腳本函數
複製代碼
根據官方所說:lua腳本內部變量禁止產生隨機參數,若是在集羣環境下,存在多主多從節點;當master節點執行完腳本之後,slave節點會一樣執行該腳本。
一旦腳本內部含有隨機值這種,就可能致使主從數據不一致;因此lua腳本會嚴格限制全部的腳本都無反作用。
Redis 對 Lua 環境作了一些列相應的措施:
① 不提供訪問系統狀態狀態的庫
② 禁止使用 loadfile 函數
③ 禁止出現隨機性質命令
複製代碼
redisbook.readthedocs.io/en/latest/f… - 主要看腳本的安全性
各單位請注意:《Lua語言從入門到實戰》已經悄悄地進行中了!