深入淺出Redis(二)進階特性:事務,深入淺出redis
第一篇中介紹了Redis是一個強大的鍵-值倉儲,支援五種靈活的資料結構。事實上,Redis還支援其它的一些進階特性:事務、發布與訂閱、管道、指令碼等,本篇我們來看一下事務。
前一篇中我們提到,在Redis中每個命令都是原子性的,因為Redis內部的實現是單線程的。當然Redis也支援多個命令之間的事務,不過事務在Redis中相對來說很簡單,不像資料庫事務那樣涉及傳播層級、隔離等級等特性。
使用multi命令開始一個新的事務,exec命令提交,discard命令復原。如果把信用卡的可用額度存入balance,欠額存入debt,在消費的時候就必須在一個事務內同時更新這兩個鍵。
127.0.0.1:6379> set balance 100OK127.0.0.1:6379> set debt 0OK127.0.0.1:6379> multiOK127.0.0.1:6379> decrby balance 25QUEUED127.0.0.1:6379> incrby debt 25QUEUED127.0.0.1:6379> exec1) (integer) 752) (integer) 25127.0.0.1:6379> get balance"75"127.0.0.1:6379> get debt"25"
在multi命令後的其它命令,返回結果都是"QUEUED“,這些命令不會立即執行,只是簡單的在Server端緩衝起來了。在發出exec命令後,他們才會被一起執行;或者discard命令復原事務。
在Redis官方文檔有指出事務的兩個特點:上面的例子是信用卡扣減,但實際中在做扣減時我們需要檢查餘額是否足夠,所以一般會這麼做:
redis.multi()balance = redis.get('balance')if (balance < amtToSubtract) { redis.discard()} else { redis.decrby('balance', amtToSubtract) redis.incrby('debt', amtToSubtract) redis.exec()}
對於普通資料庫事務,上面的代碼沒問題,但對於Redis事務來說行不通,因為在exec命令之前,所有的命令都被Redis緩衝起來了,根本就拿不到balance的值。那類似這種需要基於已經存在的某個值的事務在Redis中如何?呢?答案是Watch命令:
redis.watch('balance')balance = redis.get('balance')if (balance < amtToSubtract) { redis.unwatch()} else { redis.multi() redis.decrby('balance', amtToSubtract) redis.incrby('debt', amtToSubtract) redis.exec()}
通俗點講,watch命令就是標記一個鍵,如果標記了一個鍵,在提交事務前如果該鍵被別人修改過,那事務就會失敗,這種情況通常可以在程式中重新再嘗試一次。像上面的例子,首先標記了鍵balance,然後檢查餘額是否足夠,不足就取消標幟,並不做扣減;足夠的話,就啟動事務進行更新操作,如果在此期間鍵balance被其它人修改,那在提交事務(執行exec)時就會報錯,程式中通常可以捕獲這類錯誤再重新執行一次,直到成功。
Redis事務失敗後不支援復原與資料庫事務很重要的一個區別是Redis事務在執行過程中出錯後不會復原。在exec命令後,Redis Server開始一個個的執行被緩衝的命令,如果其中某個命令執行出錯了,那之前的命令並不會被復原。
127.0.0.1:6379> set value 1OK127.0.0.1:6379> set value2 abcOK127.0.0.1:6379> multiOK127.0.0.1:6379> incr valueQUEUED127.0.0.1:6379> incr value2QUEUED127.0.0.1:6379> exec1) (integer) 22) (error) ERR value is not an integer or out of range127.0.0.1:6379> get value"2"127.0.0.1:6379>
exec提交事務後,在執行到incr value2時錯誤了(資料類型不正確),但事務對value的操作卻是生效的,這點可以從後面的get value的返回值看到。