Redis事务介绍
事务:是一个单独的隔离操作,事务中的所有命令都会被序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
当使用 AOF
方式做持久化时,Redis会使用单个write命令将事务写入磁盘中,而不是将事务中的命令分别写入。
Redis事务相关命令
# 开启事务,标志着一个事务块的开始
MULTI
# 提交事务,即执行所有事务块中的命令
EXEC
# 取消事务,放弃执行事务块中的所有命令
DISCARD
# 监视一个或多个key,如果在事务执行前这些key被其他命令所改动,那么事务将被打断
WATCH key [key ...]
事务的使用说明:
MULTI
标志着一个事务块的开始,后续输入的多条指令就都属于Redis的事务块中,并且将按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。
所以,事实上Redis事务并非完全是原子性的,准确地说,Redis事务的原子性应该仅存在于 执行 EXEC
命令时,即使用 EXEC
命令提交事务时,事务中的全部命令要么提交,要么都不提交。
EXEC
命令的执行情况如下:
- 如果客户端在使用
MULTI
开启事务后,却因为断线而没有成功执行EXEC
,那么事务中的所有命令都不会被执行。 - 另一方面,如果客户端成功在开启事务后执行
EXEC
,那么事务中所有的命令都会被执行。
Redis事务的风险:如果Redis服务器因为某种原因被管理员杀死,或遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。
如果 Redis 在重启时发现 AOF
文件出现了上述的问题,那么它会退出,并汇报一个错误。使用redis-check-aof
程序可以修复这一问题:它会移除 AOF
文件中不完整的事务信息,确保服务器可以正常启动
Redis事务中的错误
再说说使用事务时可能遇到两种错误:
- 事务在执行
EXEC
之前,入队的命令可能出错,如命令的语法错误、内存不足(服务器使用了超过设置的maxmemory
最大内存限制)或其他更严重的错误等。 - 命令在
EXEC
调用后失败,如事务命令处理了错误类型的键,比如将列表命令用在了字符串键上等。
对于第一种错误:客户端以前的做法是检查命令入队所得的返回值;如命令入队返回 QUEUED
,那么入队成功,否则就是入队失败,如果有命令在入队时失败,大部分客户端都会停止并取消这个事务。
至于第二种错误:那些在 EXEC
命令执行之后产生的错误,并没有对它们进行特别处理:即使事务中有某个/某些命令在执行时产生了错误,事务中的其它命令仍会继续执行。
了解Redis不支持回滚的原因
下面是官方答复:
如果你有使用关系型数据库的经验,那么“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”,这种做法可能会让你觉得有些奇怪。
以下是这种做法的优点
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是用在了错误类型的键上面:也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速
有种观点认为 Redis 处理事务会产生bug,然而需要注意的是,在通常情况下,回滚并不能解决编程错误带来的问题。举个例子,如果你本身想通过 INCR
命令将键值加上 1,却不小心加上了2,或者对错误类型的键执行了 INCR
,回滚是没有办法处理这些情况的。
鉴于没有任何机制能够避免程序员自己造成的错误,并且这类错误通常不会在生产环境中出现,所以Redis选择了更简单、更快速的无回滚方式来处理事务 。
使用 CAS 操作实现乐观锁
WATCH
命令可以为 Redis 事务提供 check-and-set(CAS)行为。
被 WATCH
的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在 EXEC
执行之前被修改了,那么整个事务都会被取消,EXEC
返回空多条批量回复(null multi-bulk reply)来表示事务已经失败。
这种形式的锁被称为乐观锁,它是一种强大的锁机制。并且因为大多数情况下,不同客户端会访问不同的键,碰撞的情况一般很少,所以通常不需要重试。
手动实现Redis事务原子性
目标:规避Redis事务的风险,避免部分事务中的部分命令被写入,而部分命令被丢弃
实现原理:使用 Lua脚本
搭配 WATCH
命令一起实现Redis事务的原子性
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"log"
)
func main() {
// 创建Redis客户端
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.0.0:6379",
Password: "123456",
DB: 0,
})
// 初始化Pipeline
pipe := client.TxPipeline()
// 执行Lua脚本
script := `
local result = redis.call(KEYS[1], unpack(ARGV))
return result
`
// 封装的通用函数执行Lua脚本
_, err := pipe.Eval(context.Background(), script, []string{"SETEX"}, "key1", "20", "value1").Result()
if err != nil {
log.Fatal(err)
}
// 执行事务
_, err = pipe.Exec(context.Background())
if err != nil {
fmt.Println(err)
}
// 获取结果
value1, err := client.Get(context.Background(), "key1").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("key1:", value1)
// 关闭Redis客户端连接
err = client.Close()
if err != nil {
log.Fatal(err)
}
}