This page looks best with JavaScript enabled

Redis Transaction 使用

 ·  ☕ 2 min read

Redis transaction

工作時有需要做一個
Set if key exists
發現 redis 只提供一個 SETNX,Set if key doesn’t exist
順便來研究一下 redis 的 transaction

MULTI

https://redis.io/topics/transactions
在 redis 裡面做 transaction 基本上就是使用 MULTI

MULTI  
SET key0 value0  
SET key1 value1
EXEC 

Note: redis transaction 跟一般的 db transaction 有點不一樣,可以保證中間不被插入其他指令,照順序執行,但做到一半出錯不會 rollback,且甚至會繼續往下做
例如

SET a 1
SET b ssss
SET c 1
MULTI
INCR a
INCR b
INCR c
EXEC

1) (integer) 2
2) (error) ERR value is not an integer or out of range
3) (integer) 2

Redis 為何沒有 rollback:
https://redislabs.com/blog/you-dont-need-transaction-rollbacks-in-redis/

Redis persistency:
https://redis.io/topics/persistence?_ga=2.166846334.1799612137.1620217522-2107259978.1617716900
transaction 做到一半真的機器停住的話,可能真的只有一半指令進去,AOF的話就可以把多的那部分去掉,RDB的話應該是直接掉資料了(?

我們 Server 使用的是 https://github.com/go-redis/redis
裡面提供了 TxPipeline 就是接到 MULTI 上面

go-redis 提供的 Pipeline 沒有用 MULTI EXEC 包起來,中間有可能被切開,就是一次傳很多指令去 redis 減少 round trip 時間

使用方法也滿簡單的

1
2
3
4
pipe := client.TxPipeline()
pipe.Incr(key)
pipe.Expire(key, time.Minute*15)
_, err = pipe.Exec()

不過還是沒辦法達到 set if key exists

如下面的範例,因為 exists 要等到 pipe.Exec() 之後才會有正確的值進去

1
2
3
4
5
6
pipe := client.TxPipeline()
pipe.Set(key, 1, 1*time.Second)
exists := pipe.Exists(key)
fmt.Println(exists.Val()) // 0
pipe.Exec()
fmt.Println(exists.Val()) // 1

估狗了一下發現只好用 WATCH 來 workaround

WATCH 可以監視一個 key
之後使用 MULTI EXEC 如果發現這個 key 被動過,就會 EXEC 失敗

WATCH key0
MULTI
SET xxx xxxxx
EXEC // 如果 key0 在 EXEC 之前被改過,就會失敗

所以要實現 set if key exists
就變成

// sudo code
WATCH key0
exists = EXISTS key0
if exists == True {
    MULTI
    SET xxx xxxxx
    EXEC // 如果 key0 在 EXEC 之前被改過,就會失敗
}

但也不全然是只要存在就 set,因為當 key0 被改的時候也會不做

下面是 Go 裡面的做法
參考:https://www.tizi365.com/archives/309.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
key := "key"
fn := func(tx *redis.Tx) error {
    // Do things
    exists := client.Exists(key).Val()
    if exists != 1 {
        return nil
    }
    
    // client.HSet(key, "B", 666) // Modifying key causes transaction failure
    
    // call Pupeline if key is not changed
    _, err := tx.Pipelined(func(pipe redis.Pipeliner) error {
        pipe.HSet(key, "A", 2)
        return nil
    })
    return err
}

client.HSet(key, "A", 1)
fmt.Println("Before", client.HGet(key, "A").Val())
err := model.userRedis.Watch(fn, key)
fmt.Println(err) // 失敗的話會印出 redis: transaction failed 
fmt.Println("After", client.HGet(key, "A").Val())
return
Share on

Marko Peng
WRITTEN BY
Marko Peng
Good man