1. 程式人生 > 實用技巧 >【譯】StackExchange.Redis 中文文件(五)事務

【譯】StackExchange.Redis 中文文件(五)事務

事務

redis 的事務與SQL資料庫中的事務不同。 完整的文件在這裡,但換句話說:

redis 的事務由位於 MULTIEXEC(或 DISCARD,用於回滾)之間的命令塊組成。一旦遇到 MULTI,該連線上的命令將不會執行:它們會排隊(並且呼叫者將獲得對每個命令的回覆 QUEUED)。 當遇到 EXEC 時,一次性提交。如果看到的是 DISCARD 而不是 EXEC,則一切都將被丟棄。由於事務內的命令已排隊,因此你無法在事務內做決定。例如,在 SQL 資料庫中,你可以執行以下操作(虛擬碼-僅用於說明):

// 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 如何做?

這在 redis 事務中根本是不可能的:一旦事務開啟,你就無法獲取資料:你的操作已排隊。幸運的是,還有另外兩個命令可以幫助我們:WATCHUNWATCH

WATCH {key} 告訴 redis 我們對指定的 key 感興趣,以便進行事務操作。redis 將自動跟蹤該 key,並且任何更改都會使事務回滾: EXECDISCARD 的作用相同(呼叫方可以檢測到該錯誤並從頭開始重試)。 因此,你可以的是:WATCH 一個 key,以通常的方式檢查該 key 中的資料,然後 MULTI/EXEC 進行更改。如果在檢查資料時發現實際上不需要事務,則可以使用 UNWATCH

來忘記所有監視的 key。注意,在 EXECDISCARD 期間,被監視的 key 也會被重置:

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 操作內,如果其他人更改了 key(跟蹤所有其他連結對{custKey}的更改),事務將中止。

在 StackExchange.Redis 如何做?

由於 StackExchange.Redis 使用多路複用方法,因此使情況更加複雜。我們不能簡單地讓併發呼叫者發出 WATCH / UNWATCH / MULTI / EXEC / DISCARD:它們會混雜在一起。因此,提供了一個附加的抽象(使得事情變得更容易正確處理):* constraints*。 Constraints 基本上是預先包裝好的涉及 WATCH 的測試,某種測試以及對結果的檢查。如果所有約束都通過,則發出 MULTI/EXEC;否則發出 UNWATCH。所有這些都以防止命令與其他呼叫者混合在一起的方式完成。 因此我們的示例變為:

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 返回的物件只能訪問 async 方法:因為每個操作的結果只有在 Execute(或 ExecuteAsync)完成後才能知道。如果操作未開始,則所有 Task 都將標記為已取消,否則,在命令執行後,你可以正常獲取每個結果。

可用的 條件 集並不廣泛,但涵蓋了最常見的情況; 如果你還有其他條件需要檢視,請與我聯絡(或者更好的方法是:提交 pull-request)。

通過 When 進行內建操作

還應注意,redis 已預見到許多常見情況(特別是:key/hash 的存在,如上所示),並且存在單操作原子命令。這些可以通過 When 引數來訪問。因此,我們前面的示例也可以寫為:

var newId = CreateNewId();
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);

(這裡,When.NotExists 導致使用 HSETNX 命令,而不是 HSET

Lua

你還應該記住,redis 2.6及更高版本支援Lua指令碼,這是一種多功能工具,可在伺服器上作為單個原子單元執行多項操作。由於在 Lua 指令碼中沒有其他連線得到服務,因此它的行為很像事務,但是沒有 MULTI/EXEC 等複雜性。這也避免了呼叫者和伺服器之間的頻寬和等待時間等問題,但是代價是它在指令碼持續時間內壟斷伺服器。

在 redis 層(並假設 HSETNX 不存在)可以實現為:

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 });

(請注意,來自 ScriptEvaluateScriptEvaluateAsync 的響應是可變的,具體取決於你的指令碼;響應可以通過強制轉換來解釋:通常為 bool

原文地址:Transactions