redis事務

 

本文記錄一些redis事務相關的原理。html

 

一、基本概念python

1)什麼是redis的事務?mysql

簡單理解,能夠認爲redis事務是一些列redis命令的集合,而且有以下兩個特色:redis

a)事務是一個單獨的隔離操做:事務中的全部命令都會序列化、按順序地執行。事務在執行的過程當中,不會被其餘客戶端發送來的命令請求所打斷。sql

b)事務是一個原子操做:事務中的命令要麼所有被執行,要麼所有都不執行。數組

 

2)事務的性質ACID服務器

通常來講,事務有四個性質稱爲ACID,分別是原子性,一致性,隔離性和持久性。異步

a)原子性atomicity:redis事務保證事務中的命令要麼所有執行要不所有不執行。有些文章認爲redis事務對於執行錯誤不回滾違背了原子性,是偏頗的。函數

b)一致性consistency:redis事務能夠保證命令失敗的狀況下得以回滾,數據能恢復到沒有執行以前的樣子,是保證一致性的,除非redis進程意外終結。post

c)隔離性Isolation:redis事務是嚴格遵照隔離性的,緣由是redis是單進程單線程模式,能夠保證命令執行過程當中不會被其餘客戶端命令打斷。

d)持久性Durability:redis事務是不保證持久性的,這是由於redis持久化策略中不論是RDB仍是AOF都是異步執行的,不保證持久性是出於對性能的考慮。

 

3)redis事務的錯誤

使用事務時可能會趕上如下兩種錯誤:

a)入隊錯誤:事務在執行 EXEC 以前,入隊的命令可能會出錯。好比說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其餘更嚴重的錯誤,好比內存不足(若是服務器使用 maxmemory 設置了最大內存限制的話)。

b)執行錯誤:命令可能在 EXEC 調用以後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,好比將列表命令用在了字符串鍵上面,諸如此類。

注:第三種錯誤,redis進程終結,本文並無討論這種錯誤。

 

二、redis事務的用法

redis事務是經過MULTIEXECDISCARD和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事務遇到執行錯誤的時候,不會進行回滾,而是簡單的放過了,並保證其餘的命令正常執行。這個區別在實現業務的時候,須要本身保證邏輯符合預期。

 

三、使用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 的值, 那麼當前客戶端的事務就會失敗。 程序須要作的, 就是不斷重試這個操做, 直到沒有發生碰撞爲止。

這種形式的鎖被稱做樂觀鎖, 它是一種很是強大的鎖機制。 而且由於大多數狀況下, 不一樣的客戶端會訪問不一樣的鍵, 碰撞的狀況通常都不多, 因此一般並不須要進行重試。

 

四、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.
複製代碼
相關文章
相關標籤/搜索