Redis 在形式上看起來也差很少,分別是 multi/exec/discard。multi 指示事務的開始,exec 指示事務的執行,discard 指示事務的丟棄。由於 Redis 的單線程特性,它不用擔憂本身在執行隊列的時候被其它指令打攪,能夠保證他們能獲得的「原子性」執行。java
Redis 的事務根本不能算「原子性」,而僅僅是知足了事務的「隔離性」,隔離性中的串行化——當前執行的事務有着不被其它事務打斷的權利。redis
Redis 爲事務提供了一個 discard 指令,用於丟棄事務緩存隊列中的全部指令,在 exec 執行以前。數據庫
> get books (nil) > multi OK > incr books QUEUED > incr books QUEUED > discard OK > get books (nil)
咱們能夠看到 discard 以後,隊列中的全部指令都沒執行,就好像 multi 和 discard 中間的全部指令從未發生過同樣。緩存
Redis 提供了這種 watch 的機制,它就是一種樂觀鎖。有了 watch 咱們又多了一種能夠用來解決併發修改的方法服務器
watch 會在事務開始以前盯住 1 個或多個關鍵變量,當事務執行時,也就是服務器收到了 exec 指令要順序執行緩存的事務隊列時,Redis 會檢查關鍵變量自 watch 以後,是否被修改了 (包括當前事務所在的客戶端)。若是關鍵變量被人動過了,exec 指令就會返回 null 回覆告知客戶端事務執行失敗,這個時候客戶端通常會選擇重試。併發
注意事項spa
Redis 禁止在 multi 和 exec 之間執行 watch 指令,而必須在 multi 以前作好盯住關鍵變量,不然會出錯。線程
下面咱們再使用 Java 語言實現一遍。日誌
import java.util.List; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class TransactionDemo { public static void main(String[] args) { Jedis jedis = new Jedis(); String userId = "abc"; String key = keyFor(userId); jedis.setnx(key, String.valueOf(5)); # setnx 作初始化 System.out.println(doubleAccount(jedis, userId)); jedis.close(); } public static int doubleAccount(Jedis jedis, String userId) { String key = keyFor(userId); while (true) { jedis.watch(key); int value = Integer.parseInt(jedis.get(key)); value *= 2; // 加倍 Transaction tx = jedis.multi(); tx.set(key, String.valueOf(value)); List<Object> res = tx.exec(); if (res != null) { break; // 成功了 } } return Integer.parseInt(jedis.get(key)); // 從新獲取餘額 } public static String keyFor(String userId) { return String.format("account_%s", userId); } }
redis爲何不支持回滾?code
不支持回滾操做是由於redis是先執行指令而後作日誌,因此即便發生異常,沒有能夠用來執行回滾操做的日誌。因此這也回答了上篇文章的第二問,爲何傳統的數據庫都是先作日誌而後再作操做。