成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Redis與Lua及Redis-py應(yīng)用Lua

YancyYe / 3398人閱讀

摘要:命令使用解釋器執(zhí)行腳本。命令根據(jù)給定的校驗(yàn)碼,執(zhí)行緩存在服務(wù)器中的腳本。命令用于校驗(yàn)指定的腳本是否已經(jīng)被保存在緩存當(dāng)中。殺死當(dāng)前正在運(yùn)行的腳本。全局變量保護(hù),為了防止不必要的數(shù)據(jù)泄漏進(jìn)環(huán)境,腳本不允許創(chuàng)建全局變量。

基本命令

Redis 腳本使用 Lua 解釋器來(lái)執(zhí)行腳本。 Reids 2.6 版本通過(guò)內(nèi)嵌支持 Lua 環(huán)境。執(zhí)行腳本的常用命令為 EVAL。

EVAL script numkeys key [key ...] arg [arg ...]
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

1 EVAL script numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
2 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
3 SCRIPT EXISTS script [script ...] 查看指定的腳本是否已經(jīng)被保存在緩存當(dāng)中。
4 SCRIPT FLUSH 從腳本緩存中移除所有腳本。
5 SCRIPT KILL 殺死當(dāng)前正在運(yùn)行的 Lua 腳本。
6 SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但并不立即執(zhí)行這個(gè)腳本。

Redis Eval 命令使用 Lua 解釋器執(zhí)行腳本。

EVAL script numkeys key [key ...] arg [arg ...]
參數(shù)說(shuō)明
script: 參數(shù)是一段 Lua 5.1 腳本程序。腳本不必(也不應(yīng)該)定義為一個(gè) Lua 函數(shù)。
numkeys: 用于指定鍵名參數(shù)的個(gè)數(shù)。
key [key ...]: 從 EVAL 的第三個(gè)參數(shù)開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數(shù)可以在 Lua 中通過(guò)全局變量 KEYS 數(shù)組,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
arg [arg ...]: 附加參數(shù),在 Lua 中通過(guò)全局變量 ARGV 數(shù)組訪問,訪問的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。

Redis Evalsha 命令根據(jù)給定的 sha1 校驗(yàn)碼,執(zhí)行緩存在服務(wù)器中的腳本。
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

redis 127.0.0.1:6379> SCRIPT LOAD "return "hello moto""
"232fd51614574cf0867b83d384a5e898cfd24e5a"
 
redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0
"hello moto"

Redis Script Exists 命令用于校驗(yàn)指定的腳本是否已經(jīng)被保存在緩存當(dāng)中。
SCRIPT EXISTS script [script ...]

redis 127.0.0.1:6379> SCRIPT LOAD "return "hello moto""    # 載入一個(gè)腳本
"232fd51614574cf0867b83d384a5e898cfd24e5a"
 
redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 1
 
redis 127.0.0.1:6379> SCRIPT FLUSH     # 清空緩存
OK
 
redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a
1) (integer) 0

SCRIPT FLUSH 從腳本緩存中移除所有腳本。
SCRIPT KILL 殺死當(dāng)前正在運(yùn)行的 Lua 腳本。
SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但并不立即執(zhí)行這個(gè)腳本。

詳細(xì)說(shuō)明

這是從一個(gè)Lua腳本中使用兩個(gè)不同的Lua函數(shù)來(lái)調(diào)用Redis的命令的例子:

redis.call()
redis.pcall()

redis.call() 與 redis.pcall()很類似, 他們唯一的區(qū)別是當(dāng)redis命令執(zhí)行結(jié)果返回錯(cuò)誤時(shí), redis.call()將返回給調(diào)用者一個(gè)錯(cuò)誤,而redis.pcall()會(huì)將捕獲的錯(cuò)誤以Lua表的形式返回
redis.call() 和 redis.pcall() 兩個(gè)函數(shù)的參數(shù)可以是任意的 Redis 命令:

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

需要注意的是,上面這段腳本的確實(shí)現(xiàn)了將鍵 foo 的值設(shè)為 bar 的目的,但是,它違反了 EVAL 命令的語(yǔ)義,因?yàn)槟_本里使用的所有鍵都應(yīng)該由 KEYS 數(shù)組來(lái)傳遞,就像這樣:

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

要求使用正確的形式來(lái)傳遞鍵(key)是有原因的,**因?yàn)椴粌H僅是 EVAL 這個(gè)命令,所有的 Redis 命令,在執(zhí)行之前都會(huì)被分析,籍此來(lái)確定命令會(huì)對(duì)哪些鍵進(jìn)行操作。
因此,對(duì)于 EVAL 命令來(lái)說(shuō),必須使用正確的形式來(lái)傳遞鍵,才能確保分析工作正確地執(zhí)行。 **

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

當(dāng) Lua 通過(guò) call() 或 pcall() 函數(shù)執(zhí)行 Redis 命令的時(shí)候,命令的返回值會(huì)被轉(zhuǎn)換成 Lua 數(shù)據(jù)結(jié)構(gòu)。 同樣地,當(dāng) Lua 腳本在 Redis 內(nèi)置的解釋器里運(yùn)行時(shí),Lua 腳本的返回值也會(huì)被轉(zhuǎn)換成 Redis 協(xié)議(protocol),然后由 EVAL 將值返回給客戶端。
下面兩點(diǎn)需要重點(diǎn)注意:
lua中整數(shù)和浮點(diǎn)數(shù)之間沒有什么區(qū)別。因此,我們始終將Lua的數(shù)字轉(zhuǎn)換成整數(shù)的回復(fù),這樣將舍去小數(shù)部分。如果你想從Lua返回一個(gè)浮點(diǎn)數(shù),你應(yīng)該將它作為一個(gè)字符串
有兩個(gè)輔助函數(shù)從Lua返回Redis的類型。

redis.error_reply(error_string) returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.

redis.status_reply(status_string) returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.

return {err="My Error"}
return redis.error_reply("My Error")
腳本的原子性

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

腳本緩存和 EVALSHA

EVAL 命令要求你在每次執(zhí)行腳本的時(shí)候都發(fā)送一次腳本主體(script body)。Redis 有一個(gè)內(nèi)部的腳本緩存機(jī)制,因此它不會(huì)每次都重新編譯腳本。
EVALSHA 命令,它的作用和 EVAL 一樣,都用于對(duì)腳本求值,但它接受的第一個(gè)參數(shù)不是腳本,而是腳本的 SHA1 校驗(yàn)和(sum)。
客戶端庫(kù)的底層實(shí)現(xiàn)可以一直樂觀地使用 EVALSHA 來(lái)代替 EVAL ,并期望著要使用的腳本已經(jīng)保存在服務(wù)器上了,只有當(dāng) NOSCRIPT 錯(cuò)誤發(fā)生時(shí),才使用 EVAL 命令重新發(fā)送腳本,這樣就可以最大限度地節(jié)省帶寬。
刷新腳本緩存的唯一辦法是顯式地調(diào)用 SCRIPT FLUSH 命令,這個(gè)命令會(huì)清空運(yùn)行過(guò)的所有腳本的緩存。通常只有在云計(jì)算環(huán)境中,才會(huì)執(zhí)行這個(gè)命令。

Redis對(duì)lua腳本做出的限制

不能訪問系統(tǒng)時(shí)間或者其他內(nèi)部狀態(tài)

Redis 會(huì)返回一個(gè)錯(cuò)誤,阻止這樣的腳本運(yùn)行: 這些腳本在執(zhí)行隨機(jī)命令之后(比如 RANDOMKEY 、 SRANDMEMBER 或 TIME 等),還會(huì)執(zhí)行可以修改數(shù)據(jù)集的 Redis 命令。如果腳本只是執(zhí)行只讀操作,那么就沒有這一限制。

每當(dāng)從 Lua 腳本中調(diào)用那些返回?zé)o序元素的命令時(shí),執(zhí)行命令所得的數(shù)據(jù)在返回給 Lua 之前會(huì)先執(zhí)行一個(gè)靜默(slient)的字典序排序(lexicographical sorting)。舉個(gè)例子,因?yàn)?Redis 的 Set 保存的是無(wú)序的元素,所以在 Redis 命令行客戶端中直接執(zhí)行 SMEMBERS ,返回的元素是無(wú)序的,但是,假如在腳本中執(zhí)行 redis.call(“smembers”, KEYS[1]) ,那么返回的總是排過(guò)序的元素。

對(duì) Lua 的偽隨機(jī)數(shù)生成函數(shù) math.random 和 math.randomseed 進(jìn)行修改,使得每次在運(yùn)行新腳本的時(shí)候,總是擁有同樣的 seed 值。這意味著,每次運(yùn)行腳本時(shí),只要不使用 math.randomseed ,那么 math.random 產(chǎn)生的隨機(jī)數(shù)序列總是相同的。

全局變量保護(hù),為了防止不必要的數(shù)據(jù)泄漏進(jìn) Lua 環(huán)境, Redis 腳本不允許創(chuàng)建全局變量。如果一個(gè)腳本需要在多次執(zhí)行之間維持某種狀態(tài),它應(yīng)該使用 Redis key 來(lái)進(jìn)行狀態(tài)保存。避免引入全局變量的一個(gè)訣竅是:將腳本中用到的所有變量都使用 local 關(guān)鍵字定義為局部變量。

可用庫(kù)

Redis Lua解釋器可用加載以下Lua庫(kù):
base
table
string
math
debug
struct 一個(gè)Lua裝箱/拆箱的庫(kù)
cjson 為L(zhǎng)ua提供極快的JSON處理
cmsgpack為L(zhǎng)ua提供了簡(jiǎn)單、快速的MessagePack操縱
bitop 為L(zhǎng)ua的位運(yùn)算模塊增加了按位操作數(shù)。
redis.sha1hex function. 對(duì)字符串執(zhí)行SHA1算法
每一個(gè)Redis實(shí)例都擁有以上的所有類庫(kù),以確保您使用腳本的環(huán)境都是一樣的。
struct, CJSON 和 cmsgpack 都是外部庫(kù), 所有其他庫(kù)都是標(biāo)準(zhǔn)。

redis 127.0.0.1:6379> eval "return cjson.encode({["foo"]= "bar"})" 0
"{"foo":"bar"}"
redis 127.0.0.1:6379> eval "return cjson.decode(ARGV[1])["foo"]" 0 "{"foo":"bar"}"
"bar"

127.0.0.1:6379> eval "return cmsgpack.pack({"foo", "bar", "baz"})" 0
"x93xa3fooxa3barxa3baz"
127.0.0.1:6379> eval "return cmsgpack.unpack(ARGV[1])" 0 "x93xa3fooxa3barxa3baz"
1) "foo"
2) "bar"
3) "baz"
使用腳本記錄Redis 日志

在 Lua 腳本中,可以通過(guò)調(diào)用 redis.log 函數(shù)來(lái)寫 Redis 日志(log):

redis.log(loglevel,message)

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

redis.LOG_DEBUG

redis.LOG_VERBOSE

redis.LOG_NOTICE

redis.LOG_WARNING
上面的這些等級(jí)(level)和標(biāo)準(zhǔn) Redis 日志的等級(jí)相對(duì)應(yīng)。

只有那些和當(dāng)前 Redis 實(shí)例所設(shè)置的日志等級(jí)相同或更高級(jí)的日志才會(huì)被散發(fā)。
以下是一個(gè)日志示例:

redis.log(redis.LOG_WARNING, "Something is wrong with this script.")
執(zhí)行上面的函數(shù)會(huì)產(chǎn)生這樣的信息:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
沙箱(sandbox)和最大執(zhí)行時(shí)間

腳本應(yīng)該僅僅用于傳遞參數(shù)和對(duì) Redis 數(shù)據(jù)進(jìn)行處理,它不應(yīng)該嘗試去訪問外部系統(tǒng)(比如文件系統(tǒng)),或者執(zhí)行任何系統(tǒng)調(diào)用。
除此之外,腳本還有一個(gè)最大執(zhí)行時(shí)間限制,它的默認(rèn)值是 5 秒鐘,一般正常運(yùn)作的腳本通??梢栽趲追种畮缀撩胫畠?nèi)完成,花不了那么多時(shí)間,這個(gè)限制主要是為了防止因編程錯(cuò)誤而造成的無(wú)限循環(huán)而設(shè)置的。
最大執(zhí)行時(shí)間的長(zhǎng)短由 lua-time-limit 選項(xiàng)來(lái)控制(以毫秒為單位),可以通過(guò)編輯 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令來(lái)修改它。

當(dāng)一個(gè)腳本達(dá)到最大執(zhí)行時(shí)間的時(shí)候,它并不會(huì)自動(dòng)被 Redis 結(jié)束,因?yàn)?Redis 必須保證腳本執(zhí)行的原子性,而中途停止腳本的運(yùn)行意味著可能會(huì)留下未處理完的數(shù)據(jù)在數(shù)據(jù)集(data set)里面。
因此,當(dāng)腳本運(yùn)行的時(shí)間超過(guò)最大執(zhí)行時(shí)間后,以下動(dòng)作會(huì)被執(zhí)行:
Redis 記錄一個(gè)腳本正在超時(shí)運(yùn)行
Redis 開始重新接受其他客戶端的命令請(qǐng)求,但是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個(gè)命令會(huì)被處理,對(duì)于其他命令請(qǐng)求, Redis 服務(wù)器只是簡(jiǎn)單地返回 BUSY 錯(cuò)誤。
可以使用 SCRIPT KILL 命令將一個(gè)僅執(zhí)行只讀命令的腳本殺死,因?yàn)橹蛔x命令并不修改數(shù)據(jù),因此殺死這個(gè)腳本并不破壞數(shù)據(jù)的完整性
如果腳本已經(jīng)執(zhí)行過(guò)寫命令,那么唯一允許執(zhí)行的操作就是 SHUTDOWN NOSAVE ,它通過(guò)停止服務(wù)器來(lái)阻止當(dāng)前數(shù)據(jù)集寫入磁盤

pipeline上下文(context)中的 EVALSHA

一旦在pipeline中因?yàn)?EVALSHA 命令而發(fā)生 NOSCRIPT 錯(cuò)誤,那么這個(gè)pipeline就再也沒有辦法重新執(zhí)行了,否則的話,命令的執(zhí)行順序就會(huì)被打亂。
為了防止出現(xiàn)以上所說(shuō)的問題,客戶端庫(kù)實(shí)現(xiàn)應(yīng)該實(shí)施以下的其中一項(xiàng)措施:

總是在pipeline中使用 EVAL 命令

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

案例1-實(shí)現(xiàn)訪問頻率限制:

實(shí)現(xiàn)訪問者 $ip 在一定的時(shí)間 $time 內(nèi)只能訪問 $limit 次.
非腳本實(shí)現(xiàn)

private boolean accessLimit(String ip, int limit, int time, Jedis jedis) {
    boolean result = true;

    String key = "rate.limit:" + ip;
    if (jedis.exists(key)) {
        long afterValue = jedis.incr(key);
        if (afterValue > limit) {
            result = false;
        }
    } else {
        Transaction transaction = jedis.multi();
        transaction.incr(key);
        transaction.expire(key, time);
        transaction.exec();
    }
    return result;
}

以上代碼有兩點(diǎn)缺陷

可能會(huì)出現(xiàn)競(jìng)態(tài)條件: 解決方法是用 WATCH 監(jiān)控 rate.limit:$IP 的變動(dòng), 但較為麻煩;

以上代碼在不使用 pipeline 的情況下最多需要向Redis請(qǐng)求5條指令, 傳輸過(guò)多.

Lua腳本實(shí)現(xiàn)
Redis 允許將 Lua 腳本傳到 Redis 服務(wù)器中執(zhí)行, 腳本內(nèi)可以調(diào)用大部分 Redis 命令, 且 Redis 保證腳本的 原子性 :
首先需要準(zhǔn)備Lua代碼: script.lua

local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]

local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
    if redis.call("INCR", key) > limit then
        return 0
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, expire_time)
    return 1
end

Java

private boolean accessLimit(String ip, int limit, int timeout, Jedis connection) throws IOException {
    List keys = Collections.singletonList(ip);
    List argv = Arrays.asList(String.valueOf(limit), String.valueOf(timeout));

    return 1 == (long) connection.eval(loadScriptString("script.lua"), keys, argv);
}

// 加載Lua代碼
private String loadScriptString(String fileName) throws IOException {
    Reader reader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));
    return CharStreams.toString(reader);
}

Lua 嵌入 Redis 優(yōu)勢(shì):

減少網(wǎng)絡(luò)開銷: 不使用 Lua 的代碼需要向 Redis 發(fā)送多次請(qǐng)求, 而腳本只需一次即可, 減少網(wǎng)絡(luò)傳輸;

原子操作: Redis 將整個(gè)腳本作為一個(gè)原子執(zhí)行, 無(wú)需擔(dān)心并發(fā), 也就無(wú)需事務(wù);

復(fù)用: 腳本會(huì)永久保存 Redis 中, 其他客戶端可繼續(xù)使用.

案例2-使用Lua腳本重新構(gòu)建帶有過(guò)期時(shí)間的分布式鎖.

案例來(lái)源: < Redis實(shí)戰(zhàn) > 第6、11章, 構(gòu)建步驟:

鎖申請(qǐng)

首先嘗試加鎖:

成功則為鎖設(shè)定過(guò)期時(shí)間; 返回;

失敗檢測(cè)鎖是否添加了過(guò)期時(shí)間;

wait.

鎖釋放

檢查當(dāng)前線程是否真的持有了該鎖:

持有: 則釋放; 返回成功;

失敗: 返回失敗.

非Lua實(shí)現(xiàn)

String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) {
    String identifier = UUID.randomUUID().toString();
    String key = "lock:" + lockName;

    long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut;
    while (System.currentTimeMillis() < acquireTimeEnd) {
        // 獲取鎖并設(shè)置過(guò)期時(shí)間
        if (connection.setnx(key, identifier) != 0) {
            connection.expire(key, lockTimeOut);
            return identifier;
        }
        // 檢查過(guò)期時(shí)間, 并在必要時(shí)對(duì)其更新
        else if (connection.ttl(key) == -1) {
            connection.expire(key, lockTimeOut);
        }

        try {
            Thread.sleep(10);
        } catch (InterruptedException ignored) {
        }
    }
    return null;
}

boolean releaseLock(Jedis connection, String lockName, String identifier) {
    String key = "lock:" + lockName;

    connection.watch(key);
    // 確保當(dāng)前線程還持有鎖
    if (identifier.equals(connection.get(key))) {
        Transaction transaction = connection.multi();
        transaction.del(key);
        return transaction.exec().isEmpty();
    }
    connection.unwatch();

    return false;
}

Lua腳本實(shí)現(xiàn)
Lua腳本: acquire

local key = KEYS[1]
local identifier = ARGV[1]
local lockTimeOut = ARGV[2]

-- 鎖定成功
if redis.call("SETNX", key, identifier) == 1 then
    redis.call("EXPIRE", key, lockTimeOut)
    return 1
elseif redis.call("TTL", key) == -1 then
    redis.call("EXPIRE", key, lockTimeOut)
end
return 0

Lua腳本: release

local key = KEYS[1]
local identifier = ARGV[1]

if redis.call("GET", key) == identifier then
    redis.call("DEL", key)
    return 1
end
return 0

參考:http://www.redis.cn/commands/...
http://www.redis.net.cn/tutor...
http://www.oschina.net/transl...
http://www.tuicool.com/articl...

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/38307.html

相關(guān)文章

  • Redis-py官方文檔翻譯

    摘要:采取兩種實(shí)現(xiàn)命令其一類盡量堅(jiān)持官方語(yǔ)法,但是以下除外沒有實(shí)現(xiàn),應(yīng)該是線程安全的原因。線程安全性是線程安全的。由于線程安全原因,不提供實(shí)現(xiàn),因?yàn)樗鼤?huì)導(dǎo)致數(shù)據(jù)庫(kù)的切換。 官網(wǎng):https://github.com/andymccurd...當(dāng)前版本:2.10.5注:這不是完整翻譯,只提取了關(guān)鍵信息。省略了部分內(nèi)容,如lua腳本支持。 pip install redis pip instal...

    Alfred 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<