Redis?Cluster?實現(xiàn)多key事務操作的示例
在 Redis 集群模式下,MULTI/EXEC 事務直接報錯: “CROSSSLOT Keys in request don't hash to the same slot”。 那么,如何在保證數(shù)據(jù)一致性的前提下,同時操作多個 Key? 本文給出 生產(chǎn)級可行方案,從原子性到最終一致性全覆蓋。
如果你正在使用 Redis Cluster,一定遇到過這樣的困境:
- 想扣用戶余額,同時創(chuàng)建訂單、記錄日志;
- 寫了個
MULTI/EXEC,結果 Redis 報錯:key 不在同一個 slot; - 改用 Pipeline?但又怕中間被其他請求插隊,導致狀態(tài)不一致……
別急!Redis Cluster 雖然限制了跨 slot 的原子操作,但通過合理設計,我們依然能實現(xiàn)安全、可靠、高性能的多 Key 操作。
一、為什么 Redis Cluster 不支持跨節(jié)點事務?
Redis Cluster 將 key 空間劃分為 16384 個 slot,每個 key 通過 CRC16(key) % 16384 映射到一個 slot,由某個主節(jié)點負責。
而 MULTI/EXEC 事務要求所有 key 必須屬于同一個 slot,否則直接拒絕:
> MULTI > SET user:1001:name Alice > SET order:2001:status paid > EXEC (error) CROSSSLOT Keys in request don't hash to the same slot
原因很簡單: 事務需要在單個節(jié)點上原子執(zhí)行,而跨 slot 意味著涉及多個物理節(jié)點 —— Redis 無法保證分布式事務的 ACID。
?? 注意:Pipeline 也不是事務!它只是網(wǎng)絡優(yōu)化,命令之間仍可能被其他客戶端插入。
二、方案一:Hash Tag + Lua 腳本(強一致性,推薦?。?/h2>
這是 唯一能在 Redis Cluster 中實現(xiàn)原子多 Key 操作的方式。
? 核心思想:
- 利用 Hash Tag 規(guī)則,強制相關 key 落在同一 slot;
- 用 Lua 腳本 在單節(jié)點內完成所有操作,天然原子。
?? 實施步驟
1. Key 設計:使用{}包裹聚合 ID
Redis 規(guī)定:只有 {} 內的內容參與 slot 計算。
# 所有與用戶 1001 相關的 key
user:{1001}:balance → slot = CRC16("1001") % 16384
user:{1001}:orders → slot = CRC16("1001") % 16384
user:{1001}:log → slot = CRC16("1001") % 16384
? 這些 key 必然落在同一節(jié)點,可被原子操作。
2. 編寫 Lua 腳本(原子執(zhí)行)
-- 扣款 + 加訂單 + 記日志
local balance = tonumber(redis.call('GET', KEYS[1]) or 0)
if balance < tonumber(ARGV[2]) then
return redis.error_reply('INSUFFICIENT_BALANCE')
end
redis.call('DECRBY', KEYS[1], ARGV[2]) -- 扣余額
redis.call('SADD', KEYS[2], ARGV[1]) -- 加訂單
redis.call('RPUSH', KEYS[3], 'Order created') -- 記日志
return balance - tonumber(ARGV[2])
3. 客戶端調用(以 Jedis 為例)
String script = "..."; // 上述 Lua
List<String> keys = Arrays.asList(
"user:{1001}:balance",
"user:{1001}:orders",
"user:{1001}:log"
);
List<String> args = Arrays.asList("order_2026", "100");
Object result = jedis.eval(script, keys, args);
? 優(yōu)勢
- 強原子性:腳本執(zhí)行期間,其他命令無法插入;
- 高性能:一次網(wǎng)絡往返;
- 完全兼容 Cluster。
?? 注意事項
- 避免數(shù)據(jù)傾斜:不要用高頻 ID(如 user_id=1)作為 tag;
- 僅適用于“聚合根”模型:所有 key 應屬于同一業(yè)務實體(用戶、訂單、會話等)。
?? 最佳實踐:在領域建模階段,就將需原子操作的數(shù)據(jù)歸為同一聚合,并用
{aggregate_id}作為 Hash Tag。
三、方案二:應用層分步操作 + 補償機制(最終一致性)
當 key 無法歸到同一 slot(如跨用戶轉賬),且業(yè)務可接受最終一致性時,可采用 Saga 模式。
示例:用戶 A 轉賬給用戶 B
try {
// 1. 凍結 A 的資金(帶 TTL 防死鎖)
redis.setex("lock:A:100", 30, "100");
// 2. 扣 A 余額
redis.decrBy("user:A:balance", 100);
// 3. 加 B 余額
redis.incrBy("user:B:balance", 100);
// 4. 清除鎖
redis.del("lock:A:100");
} catch (Exception e) {
// 補償:回滾已執(zhí)行的操作
if (A余額已扣) redis.incrBy("user:A:balance", 100);
if (B余額已加) redis.decrBy("user:B:balance", 100);
redis.del("lock:A:100");
}
? 適用場景
- 跨聚合根操作(如 A→B 轉賬);
- 有明確補償邏輯(如退款、撤回);
- 可容忍短暫不一致。
? 劣勢
- 實現(xiàn)復雜(需冪等、重試、監(jiān)控);
- 無法保證強一致性。
四、方案三:異步隊列 + 冪等消費(高吞吐場景)
將多 Key 操作拆解為消息,交由 Kafka/RocketMQ 等可靠隊列處理:
graph LR A[發(fā)起操作] --> B[發(fā)消息到 MQ] B --> C[消費者1: 更新 key1] C --> D[消費者2: 更新 key2]
- 消費者需實現(xiàn)冪等(如用
SET key value NX防重); - 適合非實時場景:積分發(fā)放、通知推送、日志同步等。
五、不推薦方案:單獨部署非集群 Redis
- 為事務單獨維護一套 standalone Redis;
- 破壞架構統(tǒng)一性,增加運維成本;
- 喪失 Cluster 的高可用與擴展能力;
- 僅適用于極小規(guī)模、臨時過渡場景。
?? 總結:如何選擇?
| 場景 | 推薦方案 |
|---|---|
| 強一致性 + 多 Key 同業(yè)務實體 | ? Hash Tag + Lua 腳本 |
| 跨實體 Key + 可接受最終一致 | ? Saga 補償 或 異步隊列 |
| 簡單批量讀寫(同 Key) | ? MSET / MGET(內置原子命令) |
?? 核心原則: 在 Redis Cluster 中,*不要對抗 slot 機制,而要順應它*。 通過合理的數(shù)據(jù)建模(聚合根 + Hash Tag),你可以在享受 Cluster 高可用的同時,實現(xiàn)強一致的事務操作。
到此這篇關于Redis Cluster 實現(xiàn)多key事務操作的文章就介紹到這了,更多相關Redis Cluster 多key事務操作內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis?異常?read?error?on?connection?的解決方案
這篇文章主要介紹了Redis異常read?error?on?connection的解決方案,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下2022-08-08
Redis過期Key刪除策略和內存淘汰策略的實現(xiàn)
當內存使用達到上限,就無法存儲更多數(shù)據(jù)了,為了解決這個問題,Redis內部會有兩套內存回收的策略,過期Key刪除策略和內存淘汰策略,本文就來詳細的介紹一下這兩種方法,感興趣的可以了解一下2024-02-02

