日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis与Lua详解

發(fā)布時間:2024/4/13 数据库 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis与Lua详解 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

Lua

Lua語法

Lua 數(shù)據(jù)類型

Lua 是動態(tài)類型語言,變量不要類型定義,只需要為變量賦值。 值可以存儲在變量中,作為參數(shù)傳遞或結果返回。

Lua 中有 8 個基本類型分別為:nil、boolean、number、string、userdata、function、thread 和 table。

數(shù)據(jù)類型描述
nil這個最簡單,只有值nil屬于該類,表示一個無效值(在條件表達式中相當于false)。
boolean包含兩個值:false和true。
number表示雙精度類型的實浮點數(shù)
string字符串由一對雙引號或單引號來表示。可以用 2 個方括號 [[ ]]"來表示"一塊"字符串。字符串連接使用的是 ..
function由 C 或 Lua 編寫的函數(shù)
userdata表示任意存儲在變量中的C數(shù)據(jù)結構
thread表示執(zhí)行的獨立線路,用于執(zhí)行協(xié)同程序
tableLua 中的表(table)其實是一個"關聯(lián)數(shù)組"(associative arrays),數(shù)組的索引可以是數(shù)字、字符串或表類型。在 Lua 里,table 的創(chuàng)建是通過"構造表達式"來完成,最簡單構造表達式是{},用來創(chuàng)建一個空表。

通過type()方法可以獲取到變量的類型,返回值是string類型的。

type(X) print("a" .. 'b') --字符串連接

Lua字符串

字符串或串(String)是由數(shù)字、字母、下劃線組成的一串字符。

Lua 語言中字符串可以使用以下三種方式來表示:

  • 單引號間的一串字符。

  • 雙引號間的一串字符。

  • [[]] 間的一串字符。

字符串轉義

與其他語言一樣,使用\

字符串函數(shù)
-- 字符串全部轉為大寫字母。 string.upper(argument) -- 字符串全部轉為小寫字母。 string.lower(argument) --在字符串中替換。。 ----mainString 為要操作的字符串, findString 為被替換的字符,replaceString 要替換的字符,num 替換次數(shù)(可以忽略,則全部替換) string.gsub(mainString,findString,replaceString,num) --在一個指定的目標字符串中搜索指定的內容(第三個參數(shù)為索引),返回其具體位置。不存在則返回 nil。 string.find (str, substr, [init, [end]]) --字符串反轉 string.reverse(arg) --返回一個類似printf的格式化字符串 string.format(...) --char 將整型數(shù)字轉成字符并連接, byte 轉換字符為整數(shù)值(可以指定某個字符,默認第一個字符)。 string.char(...) 和 string.byte(arg[,int]) --計算字符串長度。 string.len(arg) --返回字符串string的n個拷貝(重復n次) string.rep(string, n) --鏈接兩個字符串 .. --返回一個迭代器函數(shù),每一次調用這個函數(shù),返回一個在字符串 str 找到的下一個符合 pattern 描述的子串。如果參數(shù) pattern 描述的字符串沒有找到,迭代函數(shù)返回nil。 string.gmatch(str, pattern) --只尋找源字串str中的第一個配對. 參數(shù)init可選, 指定搜尋過程的起點, 默認為1。 --在成功配對時, 函數(shù)將返回配對表達式中的所有捕獲結果; 如果沒有設置捕獲標記, 則返回整個配對字符串. 當沒有成功的配對時, 返回nil。 string.match(str, pattern, init) --截取字符串。j:截取結束位置,默認為 -1,最后一個字符。 string.sub(s, i [, j])

Lua 變量

變量在使用前,需要在代碼中進行聲明,即創(chuàng)建該變量。

Lua 變量有三種類型:全局變量局部變量表中的域

Lua 中的變量全是全局變量,那怕是語句塊或是函數(shù)里,除非用 local 顯式聲明為局部變量。(與JavaScript類似)

局部變量的作用域為從聲明位置開始到所在語句塊結束。

變量的默認值均為 nil。

在Lua 語言中,全局變量無須聲明即可使用,使用未經(jīng)初始化的全局變量不會導致錯誤。當使用未經(jīng)初始化的全局變量時,得到的結果為 nil

當把nil賦值給全局變量時,Lua會回收該全局變量。

賦值語句

a = "hello" .. "world" -- Lua 可以對多個變量同時賦值,變量列表和值列表的各個元素用逗號分開,賦值語句右邊的值會依次賦給左邊的變量。 a, b = 10, 2*x --> a=10; b=2*x -- 遇到賦值語句Lua會先計算右邊所有的值然后再執(zhí)行賦值操作,所以我們可以這樣進行交換變量的值: x, y = y, x -- swap 'x' for 'y' a[i], a[j] = a[j], a[i] -- swap 'a[i]' for 'a[j]'

當變量個數(shù)和值的個數(shù)不一致時,Lua會一直以變量個數(shù)為基礎采取以下策略:

  • a. 變量個數(shù) > 值的個數(shù) 按變量個數(shù)補足nil

  • b. 變量個數(shù) < 值的個數(shù) 多余的值會被忽略

*索引

對 table 的索引使用方括號 []。Lua 也提供了 . 操作。

t[i] t.i -- 當索引為字符串類型時的一種簡化寫法 gettable_event(t,i) -- 采用索引訪問本質上是一個類似這樣的函數(shù)調用

語句控制

循環(huán)語句

-- while(condition) dostatements end -- 數(shù)值循環(huán):var 從 exp1 變化到 exp2,每次變化以 exp3 為步長遞增 var,并執(zhí)行一次 "執(zhí)行體"。exp3 是可選的,如果不指定,默認為1。 for var=exp1,exp2,exp3 do <執(zhí)行體> end --泛型for 循環(huán)。i是數(shù)組索引值,v是對應索引的數(shù)組元素值。ipairs是Lua提供的一個迭代器函數(shù),用來迭代數(shù)組。 a = {"one", "two", "three"} for i, v in ipairs(a) doprint(i, v) end -- 類似其他語言 do ... while。 repeatstatements until( condition )--嵌套循環(huán)。前面幾個循環(huán)的互相嵌套 while(condition) dowhile(condition)dostatementsendstatements end

if

if(布爾表達式) then--[ 在布爾表達式為 true 時執(zhí)行的語句 --] end -------------------------- if(布爾表達式) then--[ 布爾表達式為 true 時執(zhí)行該語句塊 --] else--[ 布爾表達式為 false 時執(zhí)行該語句塊 --] end ------------------------ if( 布爾表達式 1) then--[ 布爾表達式 1 為 true 時執(zhí)行該語句塊 --]if(布爾表達式 2)then--[ 布爾表達式 2 為 true 時執(zhí)行該語句塊 --]end end

Lua認為false和nil為假,true 和非nil為真。要注意的是Lua中 0 為 true。

函數(shù)

通過關鍵字 function 聲明一個函數(shù)。return 語句返回結果。

optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)function_bodyreturn result_params_comma_separated end

說明:

  • optional_function_scope: 該參數(shù)是可選的制定函數(shù)是全局函數(shù)還是局部函數(shù),未設置該參數(shù)默認為全局函數(shù),如果你需要設置函數(shù)為局部函數(shù)需要使用關鍵字 local。
  • function_name: 指定函數(shù)名稱。
  • argument1, argument2, argument3..., argumentn: 函數(shù)參數(shù),多個參數(shù)以逗號隔開,函數(shù)也可以不帶參數(shù)。
  • function_body: 函數(shù)體,函數(shù)中需要執(zhí)行的代碼語句塊。
  • result_params_comma_separated: 函數(shù)返回值,Lua語言函數(shù)可以返回多個值,每個值以逗號隔開

*多值返回

function maximum (a)local mi = 1 -- 最大值索引local m = a[mi] -- 最大值for i,val in ipairs(a) doif val > m thenmi = im = valendendreturn m, mi endprint(maximum({8,10,23,12,5}))

*可變參數(shù)

Lua 函數(shù)可以接受可變數(shù)目的參數(shù),和 C 語言類似,在函數(shù)參數(shù)列表中使用三點 表示函數(shù)有可變的參數(shù)。

function add(...) local s = 0 for i, v in ipairs{...} do --> {...} 表示一個由所有變長參數(shù)構成的數(shù)組 。inpairs沒有使用括號,應該是語法編譯。s = s + v end return s end print(add(3,4,5,6,7)) --->25
  • 通常在遍歷變長參數(shù)的時候只需要使用 {…},然而變長參數(shù)可能會包含一些 nil,那么就可以用 select 函數(shù)來訪問變長參數(shù)了:select('#', …) 或者 select(n, …)

    • select('#', …) 返回可變參數(shù)的長度。
      • select(n, …) 用于返回從起點 n 開始到結束位置的所有參數(shù)列表。
  • 調用 select 時,必須傳入一個固定實參 selector(選擇開關) 和一系列變長參數(shù)。如果 selector 為數(shù)字 n,那么 select 返回參數(shù)列表中從索引 n 開始到結束位置的所有參數(shù)列表,否則只能為字符串 #,這樣 select 返回變長參數(shù)的總數(shù)。

Lua運算符

算術運算符

設定 A 的值為10,B 的值為 20:

操作符描述實例
+加法A + B 輸出結果 30
-減法A - B 輸出結果 -10
*乘法A * B 輸出結果 200
/除法B / A 輸出結果 2
%取余B % A 輸出結果 0
^乘冪A^2 輸出結果 100
-負號-A 輸出結果 -10

關系運算符

設定 A 的值為10,B 的值為 20:

操作符描述實例
==等于,檢測兩個值是否相等,相等返回 true,否則返回 false(A == B) 為 false。
~=不等于,檢測兩個值是否相等,不相等返回 true,否則返回 false(A ~= B) 為 true。
>大于,如果左邊的值大于右邊的值,返回 true,否則返回 false(A > B) 為 false。
<小于,如果左邊的值大于右邊的值,返回 false,否則返回 true(A < B) 為 true。
>=大于等于,如果左邊的值大于等于右邊的值,返回 true,否則返回 false(A >= B) 返回 false。
<=小于等于, 如果左邊的值小于等于右邊的值,返回 true,否則返回 false(A <= B) 返回 true。

邏輯運算符

設定 A 的值為 true,B 的值為 false:

操作符描述實例
and邏輯與操作符。 若 A 為 false,則返回 A,否則返回 B。(A and B) 為 false。
or邏輯或操作符。 若 A 為 true,則返回 A,否則返回 B。(A or B) 為 true。
not邏輯非操作符。與邏輯運算結果相反,如果條件為 true,邏輯非為 false。not(A and B) 為 true。

*其他運算符

操作符描述實例
..連接兩個字符串a…b ,其中 a 為 "Hello " , b 為 “World”, 輸出結果為 “Hello World”。
#一元運算符,返回字符串或表的長度。#“Hello” 返回 5

運算符優(yōu)先級

從高到低的順序:

^ not - (unary) * / % + - .. #################### < > <= >= ~= == and or

*除了 ^ 和 … 外所有的二元運算符都是左結合的。

a+i < b/2+1 --> (a+i) < ((b/2)+1) 5+x^2*8 --> 5+((x^2)*8) a < y and y <= z --> (a < y) and (y <= z) -x^2 --> -(x^2) x^y^z --> x^(y^z) ::::右結合的:::,注意順序

Lua數(shù)組

Lua 數(shù)組的索引鍵值可以使用整數(shù)表示,數(shù)組的大小不是固定的。

* 一維數(shù)組

array = {"Lua", "Tutorial"}

在 Lua 索引值是以 1 為起始,但你也可以指定 0 開始。

還可以以負數(shù)為數(shù)組索引值。數(shù)組的索引,不代表第幾個元素,而是類似map的key

array = {}for i= -2, 2 doarray[i] = i *2 endfor i = -2,2 doprint(array[i]) end --OUTPUT: -4 -2 0 2 4

多維數(shù)組

就是類似于value是一個map的map。

-- 初始化數(shù)組 array = {} for i=1,3 doarray[i] = {}for j=1,3 doarray[i][j] = i*jend end

Lua迭代器

泛型 for 迭代器

泛型 for 在自己內部保存迭代函數(shù),實際上它保存三個值:迭代函數(shù)、狀態(tài)常量、控制變量。

for k, v in pairs(t) doprint(k, v) end

泛型 for 的執(zhí)行過程:

  • 首先,初始化,計算 in 后面表達式的值,表達式應該返回泛型 for 需要的三個值:迭代函數(shù)狀態(tài)常量控制變量;與多值賦值一樣,如果表達式返回的結果個數(shù)不足三個會自動用 nil 補足,多出部分會被忽略。
  • 第二,將狀態(tài)常量和控制變量作為參數(shù)調用迭代函數(shù)(注意:對于 for 結構來說,狀態(tài)常量沒有用處,僅僅在初始化時獲取他的值并傳遞給迭代函數(shù))。
  • 第三,將迭代函數(shù)返回的值賦給變量列表
  • 第四,如果返回的第一個值為nil循環(huán)結束,否則執(zhí)行循環(huán)體。
  • 第五,回到第二步再次調用迭代函數(shù)

在Lua中我們常常使用函數(shù)來描述迭代器,每次調用該函數(shù)就返回集合的下一個元素。Lua 的迭代器包含以下兩種類型:

  • 無狀態(tài)的迭代器
  • 多狀態(tài)的迭代器

無狀態(tài)的迭代器

無狀態(tài)的迭代器是指不保留任何狀態(tài)的迭代器,因此在循環(huán)中我們可以利用無狀態(tài)迭代器避免創(chuàng)建閉包花費額外的代價。

每一次迭代,迭代函數(shù)都是用兩個變量(狀態(tài)常量和控制變量)的值作為參數(shù)被調用,一個無狀態(tài)的迭代器只利用這兩個值可以獲取下一個元素。

這種無狀態(tài)迭代器的典型的簡單的例子是 ipairs,它遍歷數(shù)組的每一個元素,元素的索引需要是數(shù)值。

--迭代函數(shù) function square(iteratorMaxCount,currentNumber)if currentNumber<iteratorMaxCountthencurrentNumber = currentNumber+1return currentNumber, currentNumber*currentNumberend endfor i,n in square,3,0 doprint(i,n) end--OUTPUT: 1 1 2 4 3 9

迭代的狀態(tài)包括被遍歷的表(循環(huán)過程中不會改變的狀態(tài)常量)和當前的索引下標(控制變量),ipairs 和迭代函數(shù)都很簡單,我們在 Lua 中可以這樣實現(xiàn):

--迭代函數(shù)。參數(shù)a:是狀態(tài)常量,i:控制變量 function iter (a, i)i = i + 1local v = a[i]if v thenreturn i, vend endfunction ipairs (a)return iter, a, 0 end

當 Lua 調用 ipairs(a) 開始循環(huán)時,他獲取三個值:迭代函數(shù) iter狀態(tài)常量 a控制變量初始值 0;然后 Lua 調用 iter(a,0) 返回 1, a[1](除非 a[1]=nil);第二次迭代調用 iter(a,1) 返回 2, a[2]……直到第一個 nil 元素。

多狀態(tài)的迭代器

迭代器需要保存多個狀態(tài)信息而不是簡單的狀態(tài)常量和控制變量,最簡單的方法是使用閉包,還有一種方法就是將所有的狀態(tài)信息封裝到 table 內,將 table 作為迭代器的狀態(tài)常量,因為這種情況下可以將所有的信息存放在 table 內,所以迭代函數(shù)通常不需要第二個參數(shù)。

array = {"Google", "Runoob"}function elementIterator (collection)local index = 0local count = #collection --collectioin 長度-- 閉包函數(shù)return function ()index = index + 1if index <= countthen-- 返回迭代器的當前元素return collection[index]endend endfor element in elementIterator(array) -- elementIterator(array) 調用的返回值(閉包) ,作為一個迭代函數(shù)。 doprint(element) end --OUTPUT: Google Runoob

Lua table(表)

table 是 Lua 的一種數(shù)據(jù)結構用來幫助我們創(chuàng)建不同的數(shù)據(jù)類型,如:數(shù)組、字典等。

Lua table 使用關聯(lián)型數(shù)組,你可以用任意類型的值來作數(shù)組的索引,但這個值不能是 nil

Lua table 是不固定大小的,你可以根據(jù)自己需要進行擴容。

Lua也是通過table來解決模塊(module)、包(package)和對象(Object)的。 例如string.format表示使用"format"來索引table string。

table(表)的構造

-- 初始化表 mytable = {}-- 指定值 mytable[1]= "Lua"-- 移除引用 mytable = nil -- lua 垃圾回收會釋放內存

Table 操作

序號方法用途
1table.concat (table [, sep [, start [, end]]]):concat是concatenate(連鎖, 連接)的縮寫. table.concat()函數(shù)列出參數(shù)中指定table的數(shù)組部分從start位置到end位置的所有元素, 元素間以指定的分隔符(sep)隔開。
2table.insert (table, [pos,] value):在table的數(shù)組部分指定位置(pos)插入值為value的一個元素. pos參數(shù)可選, 默認為數(shù)組部分末尾.
3table.maxn (table)指定table中所有正數(shù)key值中最大的key值. 如果不存在key值為正數(shù)的元素, 則返回0。(Lua5.2之后該方法已經(jīng)不存在了,本文使用了自定義函數(shù)實現(xiàn))
4table.remove (table [, pos])返回table數(shù)組部分位于pos位置的元素. 其后的元素會被前移. pos參數(shù)可選, 默認為table長度, 即從最后一個元素刪起。
5table.sort (table [, comp])對給定的table進行升序排序。

Lua其他

其他 Redis用不不涉及,就不整理了。

參考

https://www.runoob.com/lua/lua-strings.html

Redis

Redis中使用Lua的好處

  • 減少網(wǎng)絡開銷。可以將多個請求通過腳本的形式一次發(fā)送,減少網(wǎng)絡時延
  • 原子操作。redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他命令插入。因此在編寫腳本的過程中無需擔心會出現(xiàn)競態(tài)條件,無需使用事務。
  • 復用。客戶端發(fā)送的腳步會永久存在redis中,這樣,其他客戶端可以復用這一腳本而不需要使用代碼完成相同的邏輯。

Redis Lua腳本與事務

從定義上來說, Redis 中的腳本本身就是一種事務, 所以任何在事務里可以完成的事, 在腳本里面也能完成。 并且一般來說, 使用腳本要來得更簡單,并且速度更快。

使用事務時可能會遇上以下兩種錯誤:

  • 事務在執(zhí)行 EXEC 之前,入隊的命令可能會出錯。比如說,命令可能會產(chǎn)生語法錯誤(參數(shù)數(shù)量錯誤,參數(shù)名錯誤,等等),或者其他更嚴重的錯誤,比如內存不足(如果服務器使用 maxmemory 設置了最大內存限制的話)。
  • 命令可能在 EXEC 調用之后失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。

對于發(fā)生在 EXEC 執(zhí)行之前的錯誤,客戶端以前的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那么入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那么大部分客戶端都會停止并取消這個事務。

從 Redis 2.6.5 開始,服務器會對命令入隊失敗的情況進行記錄,并在客戶端調用 EXEC 命令時,拒絕執(zhí)行并自動放棄這個事務。

經(jīng)過測試lua中發(fā)生異常處理方式和redis 事務一致,可以說這兩個東西是一樣的,但是lua支持緩存,可以復用腳本,這個是原來的事務所沒有的

Redis 相關命令

EVAL

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

script 參數(shù)是一段 Lua 5.1 腳本程序,它會被運行在 Redis 服務器上下文中,這段腳本不必(也不應該)定義為一個 Lua 函數(shù)。

numkeys 參數(shù)用于指定鍵名參數(shù)的個數(shù)。

鍵名參數(shù) key [key ...] 從 EVAL 的第三個參數(shù)開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數(shù)可以在 Lua 中通過全局變量 KEYS 數(shù)組,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。

在命令的最后,那些不是鍵名參數(shù)的附加參數(shù) arg [arg ...] ,可以在 Lua 中通過全局變量 ARGV 數(shù)組訪問,訪問的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"

EVALSHA

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

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

EVALSHA 命令的表現(xiàn)如下:

  • 如果服務器還緩存了給定的 SHA1 校驗和所指定的腳本,那么執(zhí)行這個腳本
  • 如果服務器未緩存給定的 SHA1 校驗和所指定的腳本,那么它返回一個特殊的錯誤,提醒用戶使用 EVAL 代替 EVALSHA

以下是示例:

> set foo bar OK> eval "return redis.call('get','foo')" 0 "bar"> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar"> evalsha ffffffffffffffffffffffffffffffffffffffff 0 (error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).

客戶端庫的底層實現(xiàn)可以一直樂觀地使用 EVALSHA 來代替 EVAL ,并期望著要使用的腳本已經(jīng)保存在服務器上了,只有當 NOSCRIPT 錯誤發(fā)生時,才使用 EVAL 命令重新發(fā)送腳本,這樣就可以最大限度地節(jié)省帶寬。

這也說明了執(zhí)行 EVAL 命令時,使用正確的格式來傳遞鍵名參數(shù)和附加參數(shù)的重要性:因為如果將參數(shù)硬寫在腳本中,那么每次當參數(shù)改變的時候,都要重新發(fā)送腳本,即使腳本的主體并沒有改變,相反,通過使用正確的格式來傳遞鍵名參數(shù)和附加參數(shù),就可以在腳本主體不變的情況下,直接使用 EVALSHA 命令對腳本進行復用,免去了無謂的帶寬消耗。

SCRIPT FLUSH

清除Redis服務端所有 Lua 腳本緩存

script flush

SCRIPT EXISTS

給定一個或多個腳本的 SHA1 校驗和,返回一個包含 0或 1 的列表,表示校驗和所指定的腳本是否已經(jīng)被保存在緩存當中

SCRIPT EXISTS sha1 [sha1 …]

SCRIPT LOAD

將腳本 script 添加到Redis服務器的腳本緩存中,并不立即執(zhí)行這個腳本,而是會立即對輸入的腳本進行求值。并返回給定腳本的 SHA1 校驗和。如果給定的腳本已經(jīng)在緩存里面了,那么不執(zhí)行任何操作。

SCRIPT LOAD script

SCRIPT KILL

殺死當前正在運行的腳本

SCRIPT KILL

殺死當前正在運行的 Lua 腳本,當且僅當這個腳本沒有執(zhí)行過任何寫操作時,這個命令才生效。 這個命令主要用于終止運行時間過長的腳本,比如一個因為 BUG 而發(fā)生無限 loop 的腳本,諸如此類。

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

Lua中執(zhí)行redis命令

在 Lua 腳本中,可以使用兩個不同函數(shù)來執(zhí)行 Redis 命令,它們分別是:

  • redis.call()
  • redis.pcall()

這兩個函數(shù)的唯一區(qū)別在于它們使用不同的方式處理執(zhí)行命令所產(chǎn)生的錯誤

redis.call() 和 redis.pcall() 兩個函數(shù)的參數(shù)可以是任何格式良好(well formed)的 Redis 命令:

> eval "return redis.call('set','foo','bar')" 0 OK

上面這段腳本的確實現(xiàn)了將鍵 foo 的值設為 bar 的目的,但是,它違反了 EVAL 命令的語義,因為腳本里使用的所有鍵都應該由 KEYS 數(shù)組來傳遞,就像這樣:

> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK

要求使用正確的形式來傳遞鍵(key)是有原因的,因為不僅僅是 EVAL 這個命令,所有的 Redis 命令,在執(zhí)行之前都會被分析,籍此來確定命令會對哪些鍵進行操作。

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

在 Lua 數(shù)據(jù)類型和 Redis 數(shù)據(jù)類型之間轉換

當 Lua 通過 call() 或 pcall() 函數(shù)執(zhí)行 Redis 命令的時候,命令的返回值會被轉換成 Lua 數(shù)據(jù)結構。同樣地,當 Lua 腳本在 Redis 內置的解釋器里運行時,Lua 腳本的返回值也會被轉換成 Redis 協(xié)議(protocol),然后由 EVAL 將值返回給客戶端。

數(shù)據(jù)類型之間的轉換遵循這樣一個設計原則:如果將一個 Redis 值轉換成 Lua 值,之后再將轉換所得的 Lua 值轉換回 Redis 值,那么這個轉換所得的 Redis 值應該和最初時的 Redis 值一樣。

換句話說, Lua 類型和 Redis 類型之間存在著一一對應的轉換關系

以下列出的是詳細的轉換規(guī)則:

從 Redis 轉換到 Lua :

Redis integer reply -> Lua number # Redis 整數(shù)轉換成 Lua 數(shù)字 Redis bulk reply -> Lua string # Redis bulk 回復轉換成 Lua 字符串 Redis multi bulk reply -> Lua table (may have other Redis data types nested) #Redis 多條 bulk 回復轉換成 Lua 表,表內可能有其他別的 Redis 數(shù)據(jù)類型 Redis status reply -> Lua table with a single ok field containing the status #Redis 狀態(tài)回復轉換成 Lua 表,表內的 `ok` 域包含了狀態(tài)信息 Redis error reply -> Lua table with a single err field containing the error #Redis 錯誤回復轉換成 Lua 表,表內的 `err` 域包含了錯誤信息 Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type # Redis 的 Nil 回復和 Nil 多條回復轉換成 Lua 的布爾值 `false`

從 Lua 轉換到 Redis:

Lua number -> Redis integer reply # Lua 數(shù)字轉換成 Redis 整數(shù) Lua string -> Redis bulk reply # Lua 字符串轉換成 Redis bulk 回復 Lua table (array) -> Redis multi bulk reply # Lua 表(數(shù)組)轉換成 Redis 多條 bulk 回復 Lua table with a single ok field -> Redis status reply # 一個帶單個 `ok` 域的 Lua 表,轉換成 Redis 狀態(tài)回復 Lua table with a single err field -> Redis error reply # 一個帶單個 `err` 域的 Lua 表,轉換成 Redis 錯誤回復 Lua boolean false -> Redis Nil bulk reply # Lua 的布爾值 `false` 轉換成 Redis 的 Nil bulk 回復

從 Lua 轉換到 Redis 有一條額外的規(guī)則,這條規(guī)則沒有和它對應的從 Redis 轉換到 Lua 的規(guī)則:

Lua boolean true -> Redis integer reply with value of 1 # Lua 布爾值 `true` 轉換成 Redis 整數(shù)回復中的 `1`

示例:

#將 Lua 值轉換成 Redis 值 > eval "return 10" 0 (integer) 10> eval "return {1,2,{3,'Hello World!'}}" 0 1) (integer) 1 2) (integer) 2 3) 1) (integer) 32) "Hello World!" #將 Redis 值轉換成 Lua 值,然后再將 Lua 值轉換成 Redis 值的類型轉過程。 > eval "return redis.call('get','foo')" 0 "bar"

錯誤處理

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

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

redis> lpush foo a (integer) 1redis> eval "return redis.call('get', 'foo')" 0 (error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

和 redis.call() 不同, redis.pcall() 出錯時并不引發(fā)(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用于表示錯誤:

redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0 (error) ERR Operation against a key holding the wrong kind of value

純函數(shù)腳本

在編寫腳本方面,一個重要的要求就是,腳本應該被寫成純函數(shù)(pure function)。

也就是說,腳本應該具有以下屬性:

  • 對于同樣的數(shù)據(jù)集輸入,給定相同的參數(shù),腳本執(zhí)行的 Redis 寫命令總是相同的。腳本執(zhí)行的操作不能依賴于任何隱藏(非顯式)數(shù)據(jù),不能依賴于腳本在執(zhí)行過程中、或腳本在不同執(zhí)行時期之間可能變更的狀態(tài),并且它也不能依賴于任何來自 I/O 設備的外部輸入。

類似于冪等操作

為了確保腳本符合上面所說的屬性, Redis 做了以下工作:

  • Lua 沒有訪問系統(tǒng)時間或者其他內部狀態(tài)的命令
  • Redis 會返回一個錯誤,阻止這樣的腳本運行: 這些腳本在執(zhí)行隨機命令之后(比如 RANDOMKEY 、 [SRANDMEMBER key count] 或 TIME 等),還會執(zhí)行可以修改數(shù)據(jù)集的 Redis 命令。如果腳本只是執(zhí)行只讀操作,那么就沒有這一限制。注意,隨機命令并不一定就指那些帶 RAND 字眼的命令,任何帶有非確定性的命令都會被認為是隨機命令,比如 TIME 命令就是這方面的一個很好的例子。
  • 每當從 Lua 腳本中調用那些返回無序元素的命令時,執(zhí)行命令所得的數(shù)據(jù)在返回給 Lua 之前會先執(zhí)行一個靜默(slient)的字典序排序(lexicographical sorting)。舉個例子,因為 Redis 的 Set 保存的是無序的元素,所以在 Redis 命令行客戶端中直接執(zhí)行 SMEMBERS key ,返回的元素是無序的,但是,假如在腳本中執(zhí)行 redis.call("smembers", KEYS[1]) ,那么返回的總是排過序的元素。
  • 對 Lua 的偽隨機數(shù)生成函數(shù) math.random 和 math.randomseed 進行修改,使得每次在運行新腳本的時候,總是擁有同樣的 seed 值。這意味著,每次運行腳本時,只要不使用 math.randomseed ,那么 math.random 產(chǎn)生的隨機數(shù)序列總是相同的。

盡管有那么多的限制,但用戶還是可以用一個簡單的技巧寫出帶隨機行為的腳本(如果他們需要的話)。

假設現(xiàn)在我們要編寫一個 Redis 腳本,這個腳本從列表中彈出 N 個隨機數(shù)。一個 Ruby 寫的例子如下:

require 'rubygems' require 'redis'r = Redis.newRandomPushScript = <<EOFlocal i = tonumber(ARGV[1])local reswhile (i > 0) dores = redis.call('lpush',KEYS[1],math.random())i = i-1endreturn res EOFr.del(:mylist) puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])

這個程序每次運行都會生成帶有以下元素的列表:

> lrange mylist 0 -1 1) "0.74509509873814" 2) "0.87390407681181" 3) "0.36876626981831" 4) "0.6921941534114" 5) "0.7857992587545" 6) "0.57730350670279" 7) "0.87046522734243" 8) "0.09637165539729" 9) "0.74990198051087" 10) "0.17082803611217"

上面的 Ruby 程序每次都只生成同樣的列表,用途并不是太大。那么,該怎樣修改這個腳本,使得它仍然是一個純函數(shù)(符合 Redis 的要求),但是每次調用都可以產(chǎn)生不同的隨機元素呢?

一個簡單的辦法是,為腳本添加一個額外的參數(shù),讓這個參數(shù)作為 Lua 的隨機數(shù)生成器的 seed 值,這樣的話,只要給腳本傳入不同的 seed ,腳本就會生成不同的列表元素。

以下是修改后的腳本:

RandomPushScript = <<EOFlocal i = tonumber(ARGV[1])local resmath.randomseed(tonumber(ARGV[2]))while (i > 0) dores = redis.call('lpush',KEYS[1],math.random())i = i-1endreturn res EOFr.del(:mylist) puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))

盡管對于同樣的 seed ,上面的腳本產(chǎn)生的列表元素是一樣的(因為它是一個純函數(shù)),但是只要每次在執(zhí)行腳本的時候傳入不同的 seed ,我們就可以得到帶有不同隨機元素的列表。

Seed 會在復制(replication link)和寫 AOF 文件時作為一個參數(shù)來傳播,保證在載入 AOF 文件或附屬節(jié)點(slave)處理腳本時, seed 仍然可以及時得到更新。

注意,Redis 實現(xiàn)保證 math.random 和 math.randomseed 的輸出和運行 Redis 的系統(tǒng)架構無關,無論是 32 位還是 64 位系統(tǒng),無論是小端(little endian)還是大端(big endian)系統(tǒng),這兩個函數(shù)的輸出總是相同的。

全局變量保護

為了防止不必要的數(shù)據(jù)泄漏進 Lua 環(huán)境, Redis 腳本不允許創(chuàng)建全局變量。如果一個腳本需要在多次執(zhí)行之間維持某種狀態(tài),它應該使用 Redis key 來進行狀態(tài)保存。

企圖在腳本中訪問一個全局變量(不論這個變量是否存在)將引起腳本停止, EVAL 命令會返回一個錯誤:

redis 127.0.0.1:6379> eval 'a=10' 0 (error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

Lua 的 debug 工具,或者其他設施,比如打印(alter)用于實現(xiàn)全局保護的 meta table ,都可以用于實現(xiàn)全局變量保護。

實現(xiàn)全局變量保護并不難,不過有時候還是會不小心而為之。一旦用戶在腳本中混入了 Lua 全局狀態(tài),那么 AOF 持久化和復制(replication)都會無法保證,所以,請不要使用全局變量。

避免引入全局變量的一個訣竅是:將腳本中用到的所有變量都使用 local 關鍵字定義為局部變量。

使用腳本散發(fā) Redis 日志

在 Lua 腳本中,可以通過調用 redis.log 函數(shù)來寫 Redis 日志(log):

redis.log(loglevel, message)

其中, message 參數(shù)是一個字符串,而 loglevel 參數(shù)可以是以下任意一個值:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

上面的這些等級(level)和標準 Redis 日志的等級相對應。

對于腳本散發(fā)(emit)的日志,只有那些和當前 Redis 實例所設置的日志等級相同或更高級的日志才會被散發(fā)

以下是一個日志示例:

redis.log(redis.LOG_WARNING, "Something is wrong with this script.")

執(zhí)行上面的函數(shù)會產(chǎn)生這樣的信息:

[32343] 22 Mar 15:21:39 # Something is wrong with this script.

沙箱(sandbox)和最大執(zhí)行時間

腳本應該僅僅用于傳遞參數(shù)和對 Redis 數(shù)據(jù)進行處理,它不應該嘗試去訪問外部系統(tǒng)(比如文件系統(tǒng)),或者執(zhí)行任何系統(tǒng)調用。

除此之外,腳本還有一個最大執(zhí)行時間限制,它的默認值是 5 秒鐘,一般正常運作的腳本通常可以在幾分之幾毫秒之內完成,花不了那么多時間,這個限制主要是為了防止因編程錯誤而造成的無限循環(huán)而設置的。

最大執(zhí)行時間的長短由 lua-time-limit 選項來控制(以毫秒為單位),可以通過編輯 redis.conf 文件或者使用 CONFIG GET parameter 和 CONFIG SET parameter value 命令來修改它。

當一個腳本達到最大執(zhí)行時間的時候,它并不會自動被 Redis 結束,因為 Redis 必須保證腳本執(zhí)行的原子性,而中途停止腳本的運行意味著可能會留下未處理完的數(shù)據(jù)在數(shù)據(jù)集(data set)里面。

因此,當腳本運行的時間超過最大執(zhí)行時間后,以下動作會被執(zhí)行:

  • Redis 記錄一個腳本正在超時運行
  • Redis 開始重新接受其他客戶端的命令請求,但是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個命令會被處理,對于其他命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤。
  • 可以使用 SCRIPT KILL 命令將一個僅執(zhí)行只讀命令的腳本殺死,因為只讀命令并不修改數(shù)據(jù),因此殺死這個腳本并不破壞數(shù)據(jù)的完整性
  • 如果腳本已經(jīng)執(zhí)行過寫命令,那么唯一允許執(zhí)行的操作就是 SHUTDOWN NOSAVE ,它通過停止服務器來阻止當前數(shù)據(jù)集寫入磁盤

流水線(pipeline)上下文(context)中的 EVALSHA

在流水線請求的上下文中使用 EVALSHA 命令時,要特別小心,因為在流水線中,必須保證命令的執(zhí)行順序。

一旦在流水線中因為 EVALSHA 命令而發(fā)生 NOSCRIPT 錯誤,那么這個流水線就再也沒有辦法重新執(zhí)行了,否則的話,命令的執(zhí)行順序就會被打亂。

為了防止出現(xiàn)以上所說的問題,客戶端庫實現(xiàn)應該實施以下的其中一項措施:

  • 總是在流水線中使用 EVAL 命令
  • 檢查流水線中要用到的所有命令,找到其中的 EVAL 命令,并使用 [SCRIPT EXISTS sha1 sha1 …] 命令檢查要用到的腳本是不是全都已經(jīng)保存在緩存里面了。如果所需的全部腳本都可以在緩存里找到,那么就可以放心地將所有 EVAL 命令改成 EVALSHA 命令,否則的話,就要在流水線的頂端(top)將缺少的腳本用 SCRIPT LOAD script 命令加上去。

redis客戶端執(zhí)行Lua腳本

# --eval 后跟文件路徑 redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi

--eval而不是命令模式中的"eval",一定要有前端的--,腳本路徑后緊跟key [key …],相比命令行模式,少了numkeys這個key數(shù)量值。key [key …] 和 arg [arg …] 之間的 , ,英文逗號前后必須有空格,否則死活都報錯

參考

http://redisdoc.com/script/eval.html

總結

以上是生活随笔為你收集整理的Redis与Lua详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。