redis-----09-----redigo管道以及事务-命令事务

1 Redis 网络事件处理

redis 是单线程处理逻辑,网络事件处理以及命令处理都是在这个线程当中进行的。如果是只存在一个连接,那么不管你输入多少命令,操作都是具有原子性。
但是如果多个连接下,某个连接想按照原子性的执行多条redis命令,那么可能会被其它连接的命令插队,从而使这多条连接无法原子性执行。

简述Redis网络事件处理:
我们知道,网络双方连接是全双工的,每个连接下,双方各自有一对读写缓冲区(即下图的r、w)。连接1可能是我们程序的某个线程1的连接,连接2可能是另一个线程的连接,也有可能是其它程序的连接。

当连接1想要原子的执行多条命令,此时连接2刚好发送一条命令,redis服务器是个单线程的,会轮询这些连接去读取命令保存到redis服务器的队列当中,然后再执行。如果刚好轮询到这条命令,而连接1的命令未读取完,那么就不具备原子操作。
所以多条连接时,某个连接想原子的执行多条命令是不安全的,这里是针对执行多条命令,如果是执行单条命令,那么即使存在多条redis连接,也是原子性的,因为redis本身就是单线程。

在这里插入图片描述

如果想确保多条连接下,某个连接执行多条命令的原子性,那么就需要使用到redis事务。

对于redis具体如何实现原子性,可自行查看源码理解,不过个人猜测做法应该是:当检查到连接开启事务的操作,那么这多条命令先入队列,把这些命令全部入队完毕后,再轮询其它连接的命令。 当然中间可能出现入队错误或者入队超时等,但这些是redis的处理,反正大致猜想,具体有兴趣的可自行观看源码找答案(不过不建议,太浪费时间)。

2 Redis 事务

MULTI 开启事务,事务执行过程中,单个命令是入队列操作,直到调用 EXEC 才会一起执行。
下面来看一下事务的四个关键命令。

  • 1)MULTI:开启事务。
  • 2)EXEC:提交事务。
  • 3)DISCARD:取消事务。
  • 4)WATCH:检测key的变动,若在事务执行中,key变动则取消事务,若事务被取消则返回 nil 。在事务开启前调用,乐观锁实现(cas)。

3 应用

该应用主要说明,当在事务开启前使用watch观察某个key,开启事务后,当该key的值发生变化,那么事务会被取消并返回nil。

下面看到,tyy这个key在事务执行过程中并未用到,但是只要在事务执行过程中,有watch的key的值被改变(本人验证过,多个watch时,只要其中有一个值改变也会取消事务),事务就被取消,那么这个事务就有点冤枉了。所以以后写代码过程中需要注意。

# 若存在这两个key,先删除,防止影响下面的测试。
192.168.1.9:6379> del tyy lqq
(integer) 2

# 1. 首先设置一个key
192.168.1.9:6379> set tyy 1000
OK
# 2. 然后检测key的值是否有变动。
192.168.1.9:6379> watch tyy
OK
# 3. 确定一遍执行事务前的key的值。
192.168.1.9:6379> get tyy
"1000"
# 3. 开启事务。
192.168.1.9:6379> MULTI
OK
# 4. 执行事务。可以看到打印的QUEUED,代表入队列而为执行该命令。
192.168.1.9:6379(TX)> set lqq 2000
QUEUED

# 5. 开启另一个新的redis连接,输入命令,以改变watch观察的key的值有变动。
# 注意,即使本命令与第4步顺序换过来,结果也是一样,事务同样被取消。因为你已经开启了事务。
192.168.1.9:6379> set tyy 100
OK

# 6. 回到原来的连接,提交事务。
# 观察到事务中的命令并未成功执行,而是被取消了,因为EXEC返回nil了。也可以看到lqq这个key并不存在。
192.168.1.9:6379(TX)> EXEC
(nil)
192.168.1.9:6379> EXISTS lqq
(integer) 0
192.168.1.9:6379> 
# 7. 此时看tyy这个key肯定是被改变的。
192.168.1.9:6379> get tyy
"100"
192.168.1.9:6379> 

在这里插入图片描述

4 代码演示

package main

import (
	"fmt"
	"time"

	"github.com/garyburd/redigo/redis"
)

func main() {
	// 1. 连接到redis服务器。
	//c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", 6379))
	c, err := redis.Dial("tcp", "192.168.1.9:6379", redis.DialPassword("123456"))
	if err != nil {
		panic(err)
	}
	defer (func() {
		fmt.Println("connection close")
		c.Close()
	})()

	// 2. 事务的模拟。
	// 2.1)Send和Do里面的set和watch两条命令,只需要发送一次就能到达redis的缓冲区。具体看第08篇--管道使用技巧。
	c.Send("set", "score", 1000) // 这个只会先发送到redigo的缓冲区,Flush完才发送到redis的缓冲区。
	c.Do("watch", "score")

	// 2.2)先保存值,用于判断事务是否被取消。
	score, _ := redis.Int(c.Do("get", "score"))
	scoreCpy := score

	//这个睡眠主要是让我们有时间收到开启另一条连接改变watch score的值。
	if false {
		time.Sleep(time.Second * 20)
		// 如果开启事务,先开启上面的开关,把 false 修改为 true
		// 同时在另外一条连接修改 score 的值
	}

	// 2.3)开启事务。和2.1)同理,只需要发送一次就到redis的缓冲区。
	c.Send("multi")
	c.Send("set", "score", score*2)
	c.Do("exec")

	// 2.4)获取开启事务后的值。
	score, _ = redis.Int(c.Do("get", "score"))
	fmt.Println("score-old:", scoreCpy, "\tscore-new:", score)
}

// 上面看到,管道pipeline(2.1,2.3体现了管道) + 事务可以实现我们在go中执行多条命令的原子性。
// 但是管道本身不具有原子操作,只是单纯提高传输性能,而原子操作还是由事务和下节讲的lua脚本实现。
  • 1)首先,我们不睡眠,直接运行上面代码:
    可以看到事务是执行成功的,因为值变成两倍了,这符合我们的预期。但这里我想强调一点,就是在本连接的事务中改变watch的值,不算key的值被改变,事务仍然会成功执行。 如果是在其它连接中改变了这个watch的值,那么事务会被取消。
    在这里插入图片描述

  • 2)将上面睡眠打开即false改成true,然后再xshell开启一条新的连接,改变score的值。例如:

set score 100

打印结果看到,事务开启后,假设事务执行成功,那么score的值应变为1000的两倍或者100的两倍(假设事务不被取消,那么redis官方肯定会选择这两种结果之一进行返回),但是结果是100,说明事务被取消了。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值