1 lua 脚本
- redis中加载了一个 lua 虚拟机,用来执行 redis lua 脚本。redis lua 脚本的执行是原子性的,当某个脚本正在执行的时候,不会有其他命令或者脚本被执行。
- lua 脚本当中的命令会直接修改数据状态。
注意:如果项目中使用了 lua 脚本,不需要使用上一篇的命令事务。
redis的一些操作脚本的命令。
script load 'local key = KEYS[1];local s = redis.call("get", key);redis.call("set", key, s*2);return s*2'
输出结果:"8f7d021dcc386a422e0febe38befdc6084357610"
script exists "8f7d021dcc386a422e0febe38befdc6084357610"
输出结果:(integer) 1
script flush
输出结果:OK
script kill
输出结果:(error) NOTBUSY No scripts in execution right now.
对上面的命令在redis-cli中测试:
2 EVAL命令
如果想要在redis-cli直接使用命令测试,那么我们可以使用EVAL这个命令。这个命令主要是用来测试的比较多。语法为:
EVAL script numkeys key [key ...] arg [arg ...]
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
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 ...]
例如:
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
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>
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也同理的。
127.0.0.1:6379> set tyy 100
OK
127.0.0.1:6379>
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>
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() {
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()
})()
var data []byte
data, err = ioutil.ReadFile("double.lua")
if err != nil {
fmt.Println("load double.lua error")
return
}
script := redis.NewScript(1, string(data))
script.Load(c)
if true {
c.Send("set", "score", 1000)
rpy, _ := redis.Int(script.Do(c, "score"))
fmt.Println(rpy)
}
if false {
rpy, _ := redis.Int(script.Do(c, "lqq", 1000))
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 {
c.Send("set", "score", 1000)
rpy, _ := redis.Int(script.Do(c, "score"))
fmt.Println(rpy)
}
if true {
rpy, _ := redis.Int(script.Do(c, "lqq", 1000))
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才会支持。
|