参考链接
- https://www.liwenzhou.com/posts/Go/redis/#autoid-1-3-1
- https://www.redis.com.cn/redis-transaction.html
Redis 事务
Redis 是单线程执行命令的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。
但是,
Multi/exec
能够确保在multi/exec
两个语句之间的命令之间没有其他客户端正在执行命令。
MULTI、EXEC、DISCARD、WATCH 这四个指令构成了 redis 事务处理的基础。
- MULTI 用来组装一个事务;
- EXEC 用来执行一个事务;
- DISCARD 用来取消一个事务;
- WATCH 用来监视一些 key,一旦这些 key 在事务执行之前被改变,则取消事务的执行。
在 Redis 中,通过使用MULTI命令启动事务,然后需要传递应在事务中执行的命令列表,之后整个事务由EXEC命令执行。
RDM Redis Console
Connecting...
Connected.
6379-1:0>MULTI
"OK"
6379-1:0>SET A 1
"QUEUED"
6379-1:0>INCR A
"QUEUED"
6379-1:0>EXEC
1) "OK"
2) "OK"
3) "OK"
4) "2"
5) "OK"
6379-1:0>GET A
"2"
6379-1:0>
在上面的例子中,我们看到了 QUEUED 的字样,这表示我们在用 MULTI 组装事务时,每一个命令都会进入到内存队列中缓存起来,如果出现 QUEUED 则表示我们这个命令成功插入了缓存队列,在将来执行 EXEC 时,这些被 QUEUED 的命令都会被组装成一个事务来执行。
对于事务的执行来说,如果 redis 开启了 AOF 持久化的话,那么一旦事务被成功执行,事务中的命令就会通过 write 命令一次性写到磁盘中去
如果在向磁盘中写的过程中恰好出现断电、硬件故障等问题,那么就可能出现只有部分命令进行了 AOF 持久化,这时 AOF 文件就会出现不完整的情况,这时,我们可以使用 redis-check-aof 工具来修复这一问题,这个工具会将 AOF 文件中不完整的信息移除,确保 AOF 文件完整可用。
Redis 事务错误
有关事务,大家经常会遇到的是两类错误:
- 调用 EXEC 之前的错误
- 调用 EXEC 之后的错误
调用 EXEC 之前的错误
有可能是由于语法有误导致的,也可能时由于内存不足导致的。
只要出现某个命令无法成功写入缓冲队列的情况,redis 都会进行记录,在客户端调用 EXEC 时,redis 会拒绝执行这一事务。
(这是 2.6.5 版本之后的策略。在 2.6.5 之前的版本中,redis 会忽略那些入队失败的命令,只执行那些入队成功的命令)。我们来看一个这样的例子:
RDM Redis Console
Connecting...
Connected.
6379-1:0>MULTI
"OK"
6379-1:0>GET A
"QUEUED"
6379-1:0>INCR A
"QUEUED"
6379-1:0>ERROR CODE ! # 这个命令报错!
"ERR unknown command 'ERROR', with args beginning with: 'CODE' '!' "
6379-1:0>EXEC # 提交事务就拒绝执行了!
"EXECABORT Transaction discarded because of previous errors."
6379-1:0>GET A
"2"
6379-1:0>
调用 EXEC 之后的错误
调用 EXEC 之后的错误 指的是:
当 EXEC 执行后,才出现的错误!比如给某个字符串类型的数据,使用了集合命令,这个在EXEC 之前是不会报错的!在事务中会正常被
QUEUED
而对于调用 EXEC 之后的错误,redis 则采取了完全不同的策略,即 redis 不会理睬这些错误,而是继续向下执行事务中的其他命令。
这是因为,对于应用层面的错误,并不是 redis 自身需要考虑和处理的问题,所以一个事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。
来看一个例子:
RDM Redis Console
Connecting...
Connected.
6379-1:0>MULTI
"OK"
6379-1:0>set age 1
"QUEUED"
6379-1:0>sadd age 2 # 对字符串类型使用了 集合的命令!但是依然被 QUEUED
"QUEUED"
6379-1:0>set a 2
"QUEUED"
6379-1:0>EXEC
1) "OK"
2) "OK"
3) "OK"
4) "WRONGTYPE Operation against a key holding the wrong kind of value"
5) "OK"
6) "OK"
7) "OK"
6379-1:0>GET A
"2"
6379-1:0>
可以看到! set a 2
这个指令,虽然他的前一个命令报错了!但是它还是被正确执行了!
Watch
指令WATCH,这是一个很好用的指令,它可以帮我们实现类似于“乐观锁”的效果,即CAS(check and set)
。
WATCH 本身的作用是监视 key 是否被改动过,而且支持同时监视多个 key,只要还没真正触发事务,WATCH 都会尽职尽责的监视,一旦发现某个 key 被修改了,在执行 EXEC 时就会返回 nil,表示事务无法触发。
上面的操作是模拟两个客户端!
- 客户端A监听了一个age,想开启一个事务对其修改!
- 然后另一个客户端B 在 A 客户端事务提交之前,对 A 的值进行了修改!
- 那么A提交事务的时候,就返回了nil,事务出发失败!
这个场景的话,可以想象我们的游戏摆摊交易系统!当玩家交易的时候,多个玩家对一个商品浏览,那么每一个客户端都监听了这个商品,当其中一个购买了,相当于修改了商品的值!那么其他玩家就购买失败了!
go-redis 事务
在go-redis库中使用事务,需要用 TxPipeline 或 TxPipelined 方法!!
https://pkg.go.dev/github.com/go-redis/redis#Client.TxPipeline
TxPipeline 就像 Pipeline
,不过其内部包装了 MULTI/ EXEC
命令。
// TxPipeline demo
pipe := rdb.TxPipeline()
incr := pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
_, err := pipe.Exec(ctx)
fmt.Println(incr.Val(), err)
// TxPipelined demo
var incr2 *redis.IntCmd
_, err = rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
incr2 = pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
return nil
})
fmt.Println(incr2.Val(), err)
上面代码相当于在一个RTT下执行了下面的redis命令:
MULTI
INCR pipeline_counter
EXPIRE pipeline_counts 3600
EXEC
go-redis Watch
Watch方法接收一个函数和一个或多个key作为参数。
Watch(fn func(*Tx) error, keys ...string) error
func WatchTx() {
err := rdb.Watch(doSomeTxThing, "count")
if err != nil {
panic(any("Watch error " + err.Error()))
}
}
func doSomeTxThing(tx *redis.Tx) error {
// 1. watch count !
n, err := tx.Get("count").Int()
if err != nil && err != redis.Nil {
return err
}
time.Sleep(5 * time.Second)
// 2.在事务中对 count 进行修改!
_, err = tx.TxPipelined(func(pipe redis.Pipeliner) error {
pipe.Set("count", n+1, time.Hour)
return nil
})
return err
}
上面的代码操作:
- 开启了一个 Watch 监听 key “count”,然后监听期间,对其进行事务的操作!
- 此时如果有其他客户端,改动了监听值,那么事务就会报错!
Go redis 提供了一个 redis.TxFailedErr
来检查事务是否失败!
评论区