Redis - 事务

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事务中的错误

  再说说使用事务时可能遇到两种错误:

  1. 事务在执行 EXEC 之前,入队的命令可能出错,如命令的语法错误、内存不足(服务器使用了超过设置的 maxmemory 最大内存限制)或其他更严重的错误等。
  2. 命令在 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)
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值