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才会支持。