更新時間:2022-02-15 09:34:28 來源:動力節(jié)點 瀏覽1528次
redis 中的事務(wù)由放置在MULTI和EXEC(或DISCARD用于回滾)之間的命令塊組成。一旦MULTI 遇到 a ,該連接上的命令就不會被執(zhí)行- 它們被排隊(并且調(diào)用者得到QUEUED 每個的回復(fù))。當EXEC遇到 an 時,它們都被應(yīng)用在一個單元中(即沒有其他連接在操作之間獲得時間)。如果DISCARD看到 a 而不是 a EXEC,則所有內(nèi)容都將被丟棄。因為事務(wù)內(nèi)部的命令是排隊的,所以你不能在事務(wù)內(nèi)部做出決定 。例如,在 SQL 數(shù)據(jù)庫中,您可能會執(zhí)行以下操作(偽代碼 - 僅用于說明):
// assign a new unique id only if they don't already
// have one, in a transaction to ensure no thread-races
var newId = CreateNewUniqueID(); // optimistic
using(var tran = conn.BeginTran())
{
var cust = GetCustomer(conn, custId, tran);
var uniqueId = cust.UniqueID;
if(uniqueId == null)
{
cust.UniqueId = newId;
SaveCustomer(conn, cust, tran);
}
tran.Complete();
}
這在 redis 事務(wù)中根本不可能:一旦事務(wù)打開,您就無法獲取數(shù)據(jù)- 您的操作已排隊。幸運的是,還有另外兩個命令可以幫助我們:WATCH和UNWATCH.
WATCH {key}告訴 Redis 我們對指定的鍵感興趣以用于事務(wù)。Redis 會自動跟蹤這個鍵,任何更改都會使我們的事務(wù)回滾 -EXEC與DISCARD(調(diào)用者可以檢測到這一點并從頭開始重試)相同。所以你可以做的是:WATCH一個鍵,以正常方式從那個鍵中檢查數(shù)據(jù),然后MULTI/EXEC你的更改。如果,當您檢查數(shù)據(jù)時,您發(fā)現(xiàn)您實際上并不需要該事務(wù),您可以使用UNWATCH忘記所有被監(jiān)視的鍵??。請注意,監(jiān)視的鍵也會在EXEC和期間重置DISCARD。所以在 Redis 層,這在概念上是:
WATCH {custKey}
HEXISTS {custKey} "UniqueId"
-- (check the reply, then either:)
MULTI
HSET {custKey} "UniqueId" {newId}
EXEC
-- (or, if we find there was already an unique-id:)
UNWATCH
這可能看起來很奇怪 - 有一個MULTI/EXEC只跨越一個操作 - 但重要的是我們現(xiàn)在也在跟蹤{custKey}所有其他連接的更改 - 如果其他任何人更改密鑰,事務(wù)將被中止。
StackExchange.Redis 使用多路復(fù)用器方法這一事實使情況變得更加復(fù)雜。我們不能簡單地讓并發(fā)調(diào)用者發(fā)出WATCH/ UNWATCH/ MULTI/ EXEC/ DISCARD:它們都會混在一起。所以提供了一個額外的抽象——另外讓事情變得更簡單:約束。約束基本上是預(yù)先準備好的測試,包括WATCH、某種測試和對結(jié)果的檢查。如果所有約束都通過,則發(fā)出MULTI/ ;EXEC否則UNWATCH發(fā)出。這一切都是以防止命令與其他調(diào)用者混合在一起的方式完成的。所以我們的例子變成了:
var newId = CreateNewId();
var tran = db.CreateTransaction();
tran.AddCondition(Condition.HashNotExists(custKey, "UniqueID"));
tran.HashSetAsync(custKey, "UniqueID", newId);
bool committed = tran.Execute();
// ^^^ if true: it was applied; if false: it was rolled back
請注意,從返回的對象CreateTransaction只能訪問異步方法 - 因為每個操作的結(jié)果要等到Execute(或ExecuteAsync)完成之后才能知道。如果未應(yīng)用操作,所有Tasks 將被標記為取消 - 否則,在命令執(zhí)行后,您可以正常獲取每個結(jié)果。
可用條件的集合并不廣泛,但涵蓋了最常見的場景;如果您想查看其他條件,請與我聯(lián)系(或更好:提交拉取請求)。
還需要注意的是,Redis 已經(jīng)預(yù)料到了許多常見的場景(特別是:key/hash 的存在,就像上面一樣),并且存在單操作原子命令。這些是通過When參數(shù)訪問的——所以我們前面的例子也可以寫成:
var newId = CreateNewId();
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);
(這里是使用命令的When.NotExists原因,而不是)HSETNXHSET
您還應(yīng)該記住,Redis 2.6 及更高版本支持 Lua 腳本,這是一種通用工具,用于在服務(wù)器上作為單個原子單元執(zhí)行多個操作。由于在 Lua 腳本期間沒有為其他連接提供服務(wù),它的行為很像一個事務(wù),但沒有MULTI/EXEC等的復(fù)雜性。這也避免了調(diào)用者和服務(wù)器之間的帶寬和延遲等問題,但權(quán)衡是它壟斷了腳本期間的服務(wù)器。
在 Redis 層(假設(shè)HSETNX不存在),這可以實現(xiàn)為:
EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId}
這可以通過以下方式在 StackExchange.Redis 中使用:
var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end",
new RedisKey[] { custKey }, new RedisValue[] { newId });
(請注意,來自ScriptEvaluateand的響應(yīng)ScriptEvaluateAsync是可變的,具體取決于您的確切腳本;響應(yīng)可以通過強制轉(zhuǎn)換來解釋 - 在這種情況下為 a bool)
通過上述相信大家對Redis分布式事務(wù)已經(jīng)有所了解,大家如果對此比較感性趣,想了解更多相關(guān)知識,不妨來關(guān)注一下動力節(jié)點的Redis教程,里面的課程內(nèi)容從淺到深,通俗易懂,適合沒有基礎(chǔ)的朋友學習,希望對大家能夠有所幫助。