redis-----10-----redigo管道以及事务-脚本事务

1 lua 脚本

  • redis中加载了一个 lua 虚拟机,用来执行 redis lua 脚本。redis lua 脚本的执行是原子性的,当某个脚本正在执行的时候,不会有其他命令或者脚本被执行。
  • lua 脚本当中的命令会直接修改数据状态。

注意:如果项目中使用了 lua 脚本,不需要使用上一篇的命令事务。

redis的一些操作脚本的命令。

# 从文件中读取 lua脚本内容 
# cat test1.lua | redis-cli script load --pipe

# 1. 加载 lua脚本字符串 生成 sha1,后续可以对这个sha1来操作这个脚本。
# 注意下面key在使用时,使用加上双引号或者说是string类型,例如"tyy"。否则会报错。
script load 'local key = KEYS[1];local s = redis.call("get", key);redis.call("set", key, s*2);return s*2' 
输出结果:"8f7d021dcc386a422e0febe38befdc6084357610"

# 2. 检查脚本缓存中,是否有该 sha1 散列值的lua脚本。返回1代表有,0没有。
script exists "8f7d021dcc386a422e0febe38befdc6084357610" 
输出结果:(integer) 1

# 3. 清除所有脚本缓存。
script flush 
输出结果:OK

# 4. 如果当前脚本运行时间过长,可以通过 script kill 杀死当前运行的脚本 。
# 因为此时没有脚本运行,所以会返回这个错误。
script kill 
输出结果:(error) NOTBUSY No scripts in execution right now.

对上面的命令在redis-cli中测试:
在这里插入图片描述

2 EVAL命令

如果想要在redis-cli直接使用命令测试,那么我们可以使用EVAL这个命令。这个命令主要是用来测试的比较多。语法为:

# 测试使用 
EVAL script numkeys key [key ...] arg [arg ...]
# 1. 首先set个值,防止下面脚本运行找不到tyy这个key。
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
# 2. 执行lua脚本。tyy需要加双引号,否则报错。
127.0.0.1:6379> eval 'local s = redis.call("get", "tyy");redis.call("set", "tyy", s*2);return s*2' 0
(integer) 200
# 再次执行看到值会一直变成原来两倍。
127.0.0.1:6379> eval 'local s = redis.call("get", "tyy");redis.call("set", "tyy", s*2);return s*2' 0
(integer) 400
127.0.0.1:6379>

3 EVALSHA命令

如果我们只使用EVAL的话,那么当脚本那串字符串非常长的话,字节数非常大,会导致传输耗时。所以我们可以通过使用EVALSHA,redis会返回sha1这个唯一密串,每一个sha1密串对应唯一的脚本,那么每次只会传输固定的字符串即可。
语法:
这个命令主要在线上使用。

# 线上使用 
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

例如:

# 1. 首先set个值,防止下面脚本运行找不到tyy这个key。
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
# 2. 先加载脚本生成密串。
127.0.0.1:6379> script load 'local s = redis.call("get", "tyy");redis.call("set", "tyy", s*2);return s*2'
"f659e760c03b56b787fd4bd0041776dfa1695e6a"
127.0.0.1:6379>
# 3. 通过密串执行lua脚本。
127.0.0.1:6379> EVALSHA "f659e760c03b56b787fd4bd0041776dfa1695e6a" 0
(integer) 200
127.0.0.1:6379> EVALSHA "f659e760c03b56b787fd4bd0041776dfa1695e6a" 0
(integer) 400
127.0.0.1:6379> EVALSHA "f659e760c03b56b787fd4bd0041776dfa1695e6a" 0
(integer) 800
127.0.0.1:6379>

这里额外演示添加numkeys参数的例子,EVAL也同理的。

# 1. 首先set个值,防止下面脚本运行找不到tyy这个key。
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
# 2. 先加载脚本生成密串。
# 这里注意一下,lua的数组下标是从1开始的,与C/C++、go等语言从0开始不一样。所以KEYS[xxx]时下标是1.
127.0.0.1:6379> script load 'local key = KEYS[1];local s = redis.call("get", key);redis.call("set", key, s*2);return s*2'
"33d40d5532db8f030d69accdf846323a72183171"
127.0.0.1:6379>
# 3. 通过密串执行lua脚本。
127.0.0.1:6379> EVALSHA "33d40d5532db8f030d69accdf846323a72183171" 1 tyy
(integer) 1600
127.0.0.1:6379> EVALSHA "33d40d5532db8f030d69accdf846323a72183171" 1 tyy
(integer) 3200
127.0.0.1:6379>

4 代码测试

go的测试代码:

package main

import (
	"fmt"
	"io/ioutil"

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

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

	// 2. 读取写好的lua脚本文件到字节切片中。
	var data []byte
	data, err = ioutil.ReadFile("double.lua")
	if err != nil {
		fmt.Println("load double.lua error")
		return
	}

	// 3. 加载脚本内容到script对象。
	script := redis.NewScript(1, string(data)) // 参数1的值1代表:代表我们有一个numkeys参数,具体看EVAL、EVALSHA命令的语法。

	// 4. 加载,相当于script load命令。此时脚本并未被执行。
	script.Load(c)

	if true {
		// 5. 先提前set这个key,否则lua脚本get一个不存在的key。
		c.Send("set", "score", 1000)
		// 6. 执行上面5的命令,然后执行脚本。
		// 注意,因为上面先Send,然后Do的时候也会Send一次,这次Send会把脚本命令也Send到redigo的缓冲区。所以上面的set score 1000必定会在脚本执行前运行。
		rpy, _ := redis.Int(script.Do(c, "score"))
		fmt.Println(rpy)
	}
	if false {
		// 5. 执行脚本。这个if的逻辑是优化后脚本的逻辑。
		rpy, _ := redis.Int(script.Do(c, "lqq", 1000)) // lqq代表我们上面NewScript的参数1指定的格式,因为上面指定了1个,所以这里只需要传1个。1000代表argv的参数。
		fmt.Println(rpy)
	}
}

lua脚本内容:

local key = KEYS[1]
local val = redis.call("get", key)
redis.call("set", key, val*2)
return val*2

结果:
在这里插入图片描述

这个例子主要强调以下两点:

  • 1)这个例子的lua脚本这样写是不安全的,因为get key的时候,这个key可能是不存在的。只不过我在代码中提前set好而已。
  • 2)特别强调这点,上面代表的第5点Send时这个命令,必定会在脚本的执行前运行。因为脚本的Send是由第6点的Do完成,所以第5点的Send是在脚本之前Send的,故set score 1000必在脚本之前执行。

完善上面的lua脚本。
lua内容:

local key = KEYS[1]
local default = ARGV[1]
if redis.call("exists", key) == 0 then
    redis.call("set", key, default)
end
local val = redis.call("get", key)
redis.call("set", key, val*2)
return val*2

代码的话,需要将上面的代码的true与false调换一下即可。
即:

if false {
	// 5. 先提前set这个key,否则lua脚本get一个不存在的key。
	c.Send("set", "score", 1000)
	// 6. 执行上面5的命令,然后执行脚本。
	// 注意,因为上面先Send,然后Do的时候也会Send一次,这次Send会把脚本命令也Send到redigo的缓冲区。所以上面的set score 1000必定会在脚本执行前运行。
	rpy, _ := redis.Int(script.Do(c, "score"))
	fmt.Println(rpy)
}
if true {
	// 5. 执行脚本。这个if的逻辑是优化后脚本的逻辑。
	rpy, _ := redis.Int(script.Do(c, "lqq", 1000)) // lqq代表我们上面NewScript的参数1指定的格式,因为上面指定了1个,所以这里只需要传1个。1000代表argv的参数。
	fmt.Println(rpy)
}

这样修改后,不管lua脚本中的local val = redis.call(“get”, key)这个key存不存在,我们程序执行脚本都能正常的执行。
结果:
在这里插入图片描述

5 应用

  • 1: 项目启动时,建立redis连接并验证后,先加载所有项目中使用的lua脚本(script load)。
  • 2: 项目中若需要热更新,通过(redis-cli)script flush;然后可以通过订阅发布功能通知所有服 务器重新加载lua脚本。
  • 3:若项目中lua脚本发生阻塞,可通过script kill暂停当前阻塞脚本的执行。

建议能用脚本用脚本,而不用命令事务(MUTIL、EXEC)。因为如果有watch key命令执行了,该key值在事务执行过程中被改变,那么事务就被取消了,这个情况的发生不太好,所以建议使用脚本执行批量操作的原子性(注意我这里指的的原子性是多条命令执行的原子性,等同于多线程加锁的意思,而下面指的是事务的原子性,两者区分一下)。

6 事务 ACID 特性分析

这个事务 ACID 在面试过程中会经常被问到,所以我们需要特别加深它的印象。

  • 1)A 原子性;事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。redis不支持回滚,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。所以redis不支持事务的原子性特性。
    例如MUTIL与EXEC中有3个命令,1成功执行,2执行失败,那么它仍会继续往下执行3。这很明显不支持事务的原子性的全部成功全部失败。

  • 2)C 一致性;事务使数据库从一个一致性状态到另外一个一致性状态。这里的一致性是指预期的一致性而不是异常后的一致性,所以redis也不满足事务的一致性的特性。
    例如同样上面的例子,MUTIL与EXEC中有3个命令,1成功执行,2执行失败,然后继续往下执行3并假设成功。正常情况下,我们预期的一致性应该期待它3个命令都能被成功执行,但是目前只有两个被成功执行,而2失败了。既然redis可能出现这种情况,那么所以redis也是不支持事务的一致性的特性。

  • 3)I 隔离性;事务的操作不被其他用户操作所打断。redis命令执行是串行的,redis事务天然具备隔离性。这里指的肯定是多个连接下,每次只会执行一个命令,那么根据redis单线程的特性,redis必定是具有隔离性的。

  • 4)D 持久性;redis只有在 aof 持久化策略的时候,并且需要在 redis.conf 中 appendfsync=always 才具备持久性。而实际项目中几乎不会使用 aof 持久化策略。

所以总结 事务 ACID 特性分析,redis对于事务 ACID 特性,支持I 隔离性,A 原子性、C 一致性不支持,而D 持久性只有配置了redis才会支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值