Redis 中的事务机制,go-redis 应该如何正确使用事务

Redis 中的事务

Redis 中的事务是一种将多个命令打包,一次性按顺序执行的机制。这种机制只能减少网络往返时间,不能保证事务的原子性。事务执行期间即使一个语句发生错误,下一条语句也会执行,而不会结束事务并回滚

Redis 中的事务在执行期间会经历三个阶段:

  1. 开启事务
  2. 命令入队
  3. 执行事务/放弃事务
开启事务

通过 MULTI 命令开启事务。正常情况下,执行 MULTI 命令后客户端会返回 “OK” 结果,表示开启事务成功

需要注意,MULTI 命令不能嵌套使用,如果已经开启事务再执行 MULTI 命令,就会报错。虽然会报错,但这个行为并不会终止已经开启的事务

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> MULTI
(error) ERR MULTI calls can not be nested
命令入队

开启事务之后,就可以执行所有常规 Redis 命令(除去能影响 Redis 状态和只能在事务外执行的语句),输入命令后,此条命令就会入对,客户端会返回 “QUEUED” 表示入队成功

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> GET k1
QUEUED

在之后,Redis 会按输入顺序执行这些入队的命令

执行事务/放弃事务

执行事务的命令是 EXEC

在执行这个命令后,Redis 就会真正开始执行事务队列中的命令,并依次返回结果

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> GET k1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> GET k2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "v1"
3) OK
4) "v2"

放弃事务的命令是 DISCARD

执行这个命令后,会清空事务队列中所以命令并退出事务模式

127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get *
(nil)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 v1
QUEUED
127.0.0.1:6379> SET key2 v2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> get key2
(nil)
事务期间的错误

事务执行期间出现的错误有以下三类:

  1. 执行时错误
  2. 入队时错误,不会终止整个事务
  3. 入轨时错误,会终止整个事务

执行时错误

这种错误发生在 Redis 不执行命令就发现不了错误的情况

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET num 1
QUEUED
127.0.0.1:6379> SADD num 2
QUEUED
127.0.0.1:6379> SET num 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get num
"3"

在上面示例中,set num 1,已经把键 num 的值设置为了字符串类型,sadd num 1 又把值设置为集合类型,所以会报错

但是即使是报错了,可以看到后面的 set num 3 这条语句依然执行了,这也说明了 Redis 中的事务并不能保证一致性和原子性

入队时错误,不会终止整个事务

这种错误只会发生在特定的命令中,比如 WATCH、MULTI

这些命令在入队时即使发生错误,也不会终止整个事务

127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> MULTI
(error) ERR MULTI calls can not be nested
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK

入队时错误,会终止整个事务

这种错误会出现在命令不存在、参数个数不对等语法错误,在入队时 Redis 语法检测如果发现了错误,就会及时返回错误提示,并终止这个事务

127.0.0.1:6379> SET k1 v1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v2
QUEUED
127.0.0.1:6379> SET k1
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> GET k1
"v1"

在事务开启之前,设置了 k1 v1 键值对,在开启事务后把更新语句放入事务队列中,但因为下面的 set k1 语句错误(参数个数不够)导致语法检测发现错误,之后执行 exec 执行事务时就会报错,事务队列中的语句也会全部清空不执行

WATCH 命令

WATCH 命令的实现类似于乐观锁,它用于在并发情况下,保证事务的原子性。通过 WATCH 命令,客户端可以监控一个或一组键,如果这些键在事务执行期间被其他客户端修改,那么整个事务就会终止执行

WATCH 命令基本用法为:

WATCH key [key...]

需要注意,WATCH 命令必须在开启事务之前执行。开启事务后执行会报错,但不会终止事务
在这里插入图片描述

在上面的例子中,客户端 1先设置了 k1 v1 键值对,随后通过 watch 命令监视 k1、开启事务,在命令入队期间,客户端 2更新了 k1 的值

在客户端 2执行完成后,客户端 1提交事务,会因为 k1 的值被修改而终止事务,set k1 v2 这条语句并没有生效

与 WATCH 命令对应的有 UNWATCH 命令,它用于清除之前监控的所有键值对

Go 操作 Reids

在 go-redis 中,TxPipeline 方法可以开启一个管道,类似于 Redis 中的 MULTI 命令,开启一个事务

Pipeliner.Exec()方法类似于 Redis 中的 EXEC 命令,表示执行事务,把事务队列中的命令拿出来一一执行

错误写法

假如现在有一个转账业务,在看这篇文章之前,你可能会这样写

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
)

func main() {
	// 上下文对象 充当事务队列
	ctx := context.Background()

	// 创建 Redis 客户端
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis 所在地址和端口号
		Password: "",               // 密码
		DB:       0,                // 选择数据库
	})

	// 现在 user1 要向 user2 转账 50
	// Redis 中对应键值对为 user1:50  user2:0  表示 user1 的余额为 50 user2 的余额为 0

	// 使用 MULTI 开启事务
	pipe := client.TxPipeline()

	// 此处省略逻辑判断,比如判断 user1 余额是否充足等
	// 如果满足条件
	pipe.IncrBy(ctx, "user1", -50) // 减去 user1 的余额
	pipe.IncrBy(ctx, "user2", 50)  // 加上 user2 的余额

	// 执行事务
	_, err := pipe.Exec(ctx)
	if err != nil {
		fmt.Println("事务执行失败:", err)
		return
	}

	fmt.Println("事务执行成功")
}

把对 user1 和 user2 的余额操作放在同一个事务中,满足条件后一起执行。这样看起来没有什么问题,但是上面也说了,Reeids 中的事务机制只是逐个执行事务队列中的命令,并不能保证原子性

对于上面的代码,对 Redis 的操作就等同于

MULTI
INCRBY user1 -50
INCRBY user2 50
EXEC

由于不能保证原子性,在并发场景下,经过逻辑判断到执行完事务期间,可能造成多次增加余额和多次减去余额,造成数据不一致

所以这样写并没有什么意义,不能保证增加余额和减去余额同时成功或同时失败

正确写法

正确写法应该是使用 WATCH 命令来实现乐观锁,保证事务原子性

WATCH 命令可以监视一个或多个键,如果在事务执行之前这些键被其他客户端修改了,事务将被打断并返回一个错误

在并发场景下,当多个客户端需要对同一个数据进行操作时,为了避免数据不一致或者冲突,可以使用 WATCH 命令来监视这些数据。如果在事务执行之前有其他客户端修改了被监视的数据,事务就会失败,这时候可以根据业务逻辑选择重试事务或者执行其他操作

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
)

func main() {
	// 上下文对象 充当事务队列
	ctx := context.Background()

	// 创建 Redis 客户端
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis 所在地址和端口号
		Password: "",               // 密码
		DB:       0,                // 选择数据库
	})

	// 现在 user1 要向 user2 转账 50
	// Redis 中对应键值对为 user1:50  user2:0  表示 user1 的余额为 50 user2 的余额为 0

	// 使用 WATCH 监视 user1 和 user2
	watchKeys := []string{"user1", "user2"}
	err := client.Watch(ctx, func(tx *redis.Tx) error {
		// 使用 MULTI 开启事务
		pipe := tx.Pipeline()

		// 此处省略逻辑判断,比如判断 user1 余额是否充足等
		// 如果满足条件
		pipe.IncrBy(ctx, "user1", -50) // 减去 user1 的余额
		pipe.IncrBy(ctx, "user2", 50)  // 加上 user2 的余额

		// 执行事务(一般需要设置重试次数,事务执行失败后重新执行直到成功)
		_, err := pipe.Exec(ctx)
		if err != nil {
			fmt.Println("事务执行失败:", err)
			return err
		}

		fmt.Println("事务执行成功")
		return nil
	}, watchKeys...)

	if err != nil {
		fmt.Println("事务执行失败:", err)
		return
	}
}

在这段代码中,使用 client.Watch 方法来监视 user1 和 user2 这两个键。在回调函数中开启事务并执行转账逻辑。如果在监视期间这两个键被其他客户端修改,事务将会失败。一般来说,在事务失败后,需要重新执行这个事务直到执行成功

这样使用 WATCH 命令后,保证了增加和减去余额的同时成功或失败,保证了事务的原子性

总结

Redis 中的事务只是一种顺序执行命令的机制,不能保证原子性,与 Redis 事务相关的命令有:

  • multi:开启事务
  • exec:执行事务
  • discard:放弃事务
  • watch:监控键
  • unwatch:取消监控

在数据一致性要求高的场景,要使用 watch 命令保证事务的原子性

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
使用Gin和Redis进行事务操作时,你可以通过以下步骤来实现: 1. 创建Redis连接:首先,你需要使用Go语言的Redis客户端库连接到Redis数据库。可以使用第三方库如`go-redis`或`redigo`来实现。创建一个Redis连接池,以便在需要时可以重用连接。 2. 创建Gin路由:使用Gin框架创建HTTP路由和处理程序。你可以定义不同的路由来处理各种请求。 3. 开启事务:在处理程序使用Redis的MULTI命令来开启一个事务。MULTI命令将后续的Redis命令添加到事务队列,而不是立即执行它们。 4. 执行Redis命令:在事务使用Redis的各种命令(如SET、GET、INCR等)来执行你需要的操作。这些命令会被添加到事务队列。 5. 执行事务:在事务的所有命令都添加完成后,使用Redis的EXEC命令来执行整个事务Redis会按照添加的顺序依次执行队列的命令,并返回每个命令的结果。 6. 处理事务结果:根据EXEC命令的返回结果,你可以判断事务是否执行成功。如果成功,你可以继续处理其他业务逻辑。如果失败,你可以回滚事务或进行其他错误处理。 需要注意的是,在Redis事务,一旦EXEC命令被调用,Redis会将所有命令作为一个原子操作执行。这意味着要么所有命令都成功执行,要么都不执行。如果在事务执行期间发生了错误,你可以使用Redis的DISCARD命令来放弃执行事务的结果,并回滚到事务开始之前的状态。 希望这些步骤能帮助你在Gin和Redis实现事务操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值