本文記錄一些redis事務相關的原理。
1、基本概念
1)什么是redis的事務?
簡單理解,可以認為redis事務是一些列redis命令的集合,並且有如下兩個特點:
a)事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
b)事務是一個原子操作:事務中的命令要么全部被執行,要么全部都不執行。
2)事務的性質ACID
一般來說,事務有四個性質稱為ACID,分別是原子性,一致性,隔離性和持久性。
a)原子性atomicity:redis事務保證事務中的命令要么全部執行要不全部不執行。有些文章認為redis事務對於執行錯誤不回滾違背了原子性,是偏頗的。
b)一致性consistency:redis事務可以保證命令失敗的情況下得以回滾,數據能恢復到沒有執行之前的樣子,是保證一致性的,除非redis進程意外終結。
c)隔離性Isolation:redis事務是嚴格遵守隔離性的,原因是redis是單進程單線程模式,可以保證命令執行過程中不會被其他客戶端命令打斷。
d)持久性Durability:redis事務是不保證持久性的,這是因為redis持久化策略中不管是RDB還是AOF都是異步執行的,不保證持久性是出於對性能的考慮。
3)redis事務的錯誤
使用事務時可能會遇上以下兩種錯誤:
a)入隊錯誤:事務在執行 EXEC 之前,入隊的命令可能會出錯。比如說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其他更嚴重的錯誤,比如內存不足(如果服務器使用 maxmemory 設置了最大內存限制的話)。
b)執行錯誤:命令可能在 EXEC 調用之后失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。
注:第三種錯誤,redis進程終結,本文並沒有討論這種錯誤。
2、redis事務的用法
redis事務是通過MULTI,EXEC,DISCARD和WATCH四個原語實現的。
MULTI命令用於開啟一個事務,它總是返回OK。
MULTI執行之后,客戶端可以繼續向服務器發送任意多條命令,這些命令不會立即被執行,而是被放到一個隊列中,當EXEC命令被調用時,所有隊列中的命令才會被執行。
另一方面,通過調用DISCARD,客戶端可以清空事務隊列,並放棄執行事務。
下面給出幾種事務場景。
1)正常執行
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET key1 1 QUEUED 127.0.0.1:6379> HSET key2 field1 1 QUEUED 127.0.0.1:6379> SADD key3 1 QUEUED 127.0.0.1:6379> EXEC 1) OK 2) (integer) 1 3) (integer) 1
EXEC 命令的回復是一個數組,數組中的每個元素都是執行事務中的命令所產生的回復。 其中,回復元素的先后順序和命令發送的先后順序一致。
當客戶端處於事務狀態時,所有傳入的命令都會返回一個內容為 QUEUED 的狀態回復(status reply),這些被入隊的命令將在 EXEC命令被調用時執行。
2)放棄事務
當執行 DISCARD 命令時,事務會被放棄,事務隊列會被清空,並且客戶端會從事務狀態中退出:
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET key1 1 QUEUED 127.0.0.1:6379> DISCARD OK 127.0.0.1:6379> EXEC (error) ERR EXEC without MULTI
3)入隊錯誤回滾
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set key1 1 QUEUED 127.0.0.1:6379> HSET key2 1 (error) ERR wrong number of arguments for 'hset' command 127.0.0.1:6379> SADD key3 1 QUEUED 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors.
對於入隊錯誤,redis 2.6.5版本后,會記錄這種錯誤,並且在執行EXEC的時候,報錯並回滾事務中所有的命令,並且終止事務。
3)執行錯誤放過
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> HSET key1 field1 1 QUEUED 127.0.0.1:6379> HSET key2 field1 1 QUEUED 127.0.0.1:6379> EXEC 1) (error) WRONGTYPE Operation against a key holding the wrong kind of value 2) (integer) 1
當遇到執行錯誤時,redis放過這種錯誤,保證事務執行完成。
這里要注意此問題,與mysql中事務不同,在redis事務遇到執行錯誤的時候,不會進行回滾,而是簡單的放過了,並保證其他的命令正常執行。這個區別在實現業務的時候,需要自己保證邏輯符合預期。
3、使用WATCH
WATCH 命令可以為 Redis 事務提供 check-and-set (CAS)行為。
被 WATCH 的鍵會被監視,並會發覺這些鍵是否被改動過了。 如果有至少一個被監視的鍵在 EXEC 執行之前被修改了, 那么整個事務都會被取消, EXEC 返回空多條批量回復(null multi-bulk reply)來表示事務已經失敗。
127.0.0.1:6379> WATCH key1 OK 127.0.0.1:6379> set key1 2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set key1 3 QUEUED 127.0.0.1:6379> set key2 3 QUEUED 127.0.0.1:6379> EXEC (nil)
使用上面的代碼, 如果在 WATCH 執行之后, EXEC 執行之前, 有其他客戶端修改了 key1 的值, 那么當前客戶端的事務就會失敗。 程序需要做的, 就是不斷重試這個操作, 直到沒有發生碰撞為止。
這種形式的鎖被稱作樂觀鎖, 它是一種非常強大的鎖機制。 並且因為大多數情況下, 不同的客戶端會訪問不同的鍵, 碰撞的情況一般都很少, 所以通常並不需要進行重試。
4、python實現redis事務的demo
這里展示一個用python實現對key計數減一的原子操作。
# -*- coding:utf-8 -*- import redis from redis import WatchError from concurrent.futures import ProcessPoolExecutor r = redis.Redis(host='127.0.0.1', port=6379) # 減庫存函數, 循環直到減庫存完成 # 庫存充足, 減庫存成功, 返回True # 庫存不足, 減庫存失敗, 返回False def decr_stock(): # python中redis事務是通過pipeline的封裝實現的 with r.pipeline() as pipe: while True: try: # watch庫存鍵, multi后如果該key被其他客戶端改變, 事務操作會拋出WatchError異常 pipe.watch('stock:count') count = int(pipe.get('stock:count')) if count > 0: # 有庫存 # 事務開始 pipe.multi() pipe.decr('stock:count') # 把命令推送過去 # execute返回命令執行結果列表, 這里只有一個decr返回當前值 print pipe.execute()[0] return True else: return False except WatchError, ex: # 打印WatchError異常, 觀察被watch鎖住的情況 print ex pipe.unwatch() def worker(): while True: # 沒有庫存就退出 if not decr_stock(): break # 實驗開始 # 設置庫存為100 r.set("stock:count", 100) # 多進程模擬多個客戶端提交 with ProcessPoolExecutor(max_workers=2) as pool: for _ in range(10): pool.submit(worker)
觀察打印
/Users/didi/anaconda/bin/python /Users/didi/test/pythoneer/redis/transaction.py 99 98 97 Watched variable changed. 96 95 94 93 Watched variable changed. 92 Watched variable changed. 91 Watched variable changed. 90 Watched variable changed. 89 Watched variable changed. 88 Watched variable changed. Watched variable changed. 87 86 Watched variable changed. 85 Watched variable changed. 84 Watched variable changed. Watched variable changed. 83 82 Watched variable changed. 81 Watched variable changed. Watched variable changed. 80 79 Watched variable changed. Watched variable changed. 78 77 Watched variable changed. Watched variable changed. 76 75 Watched variable changed. Watched variable changed.74 Watched variable changed. 73 72 Watched variable changed. Watched variable changed. 71 70 Watched variable changed. 69 Watched variable changed. 68 Watched variable changed. 67 Watched variable changed. 66 Watched variable changed. Watched variable changed.65 64 Watched variable changed. 63 Watched variable changed. Watched variable changed. 62 Watched variable changed. 61 60 Watched variable changed. 59 Watched variable changed. Watched variable changed. 58 57 Watched variable changed. Watched variable changed. 56 Watched variable changed. 55 54 Watched variable changed. 53 Watched variable changed. 52 Watched variable changed. Watched variable changed. 51 50 Watched variable changed. 49 Watched variable changed. 48 Watched variable changed. 47 Watched variable changed. Watched variable changed.46 Watched variable changed. 45 Watched variable changed. 44 43 Watched variable changed. 42 Watched variable changed. Watched variable changed. 41 40 Watched variable changed. Watched variable changed. 39 Watched variable changed. 38 Watched variable changed. 37 Watched variable changed.36 Watched variable changed. 35 34 Watched variable changed. 33 Watched variable changed. Watched variable changed.32 Watched variable changed. 31 30 Watched variable changed. Watched variable changed. 29 Watched variable changed. 28 Watched variable changed.27 26 Watched variable changed. 25 Watched variable changed. 24 Watched variable changed. 23 Watched variable changed. 22Watched variable changed. Watched variable changed.21 20Watched variable changed. 19 Watched variable changed. 18 Watched variable changed. 17 Watched variable changed. 16 Watched variable changed. Watched variable changed. 15 Watched variable changed. 14 Watched variable changed. 13 12 Watched variable changed. Watched variable changed. 11 Watched variable changed. 10 Watched variable changed.9 8 Watched variable changed. 7 Watched variable changed. Watched variable changed. 6 5 Watched variable changed. Watched variable changed. 4 Watched variable changed. 3 2 Watched variable changed. 1 Watched variable changed. 0 Watched variable changed.