redis-5

redis学习第五章

脚本

  1. 介绍

redis在2.6版本中退出了脚本功能,允许开发者使用Lua语言编写脚本传到redis中执行。在Lua脚本中可以调用大部分的redis命令。使用脚本的好处:
1).减少网络开销。
2).原子操作:redis会将整个脚本作为一个整体执行,也就是说不必单行出现静态条件,也不用使用事务,事务可以完成的,脚本也可以完成。
3).复用:客户端发送的脚本会永远存储在redis中,这意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。

  1. 实例:访问频率控制

因为无需考虑事务,使用redis脚本实现访问频率限制就非常简单了。Lua如下:

local times = redis.call('incr',KEYS[1])

if times == 1 then
    -- KEYS[1] 键刚创建,所以为其设置生存时间
    redis.call('expire',KEYS[1],ARGV[1])
end

if times > tonumber(ARGV[2]) then
    return 0
end

return 1

如何运行测试该脚本呢?
首先把这段代码村委ratelimiting.lua,然后在命令行中输入:
$redis-cli --eval /path/to/ratelimiting.lua rate.limiting:127.0.0.1 , 10 3

其中–eval是告诉redis-cli读取并运行后面的Lua脚本
/path/to/ratelimiting.lua是lua脚本的位置
rate.limiting:127.0.0.1 , 10 3 是lua脚本的参数,其中“,”之前的是键,之后的是参数。分别用KEYS[1]和ARGV[1]、ARGV[2]获取
结合脚本可知,改脚本是限制访问频率为10s内三次。
注意: 参数和键之间的逗号“,”两边的空格不能省略,否则会出错。

  1. Lua语言

3.1).Lua语法

3.1.1).数据类型
Lua是一个动态类型的语言,一个变量可以存储任何类型的值。编写redis脚本会用到的类型如下表(Lua常用数据类型):

类型名 取值
空(nil) 空类型只包含一个值,即nil。nil表示空,所以没有赋值的变量或表的字段都是nil
布尔(boolean) 布尔类型包含true和false
数字(number) 整数和浮点都是使用数字类型来存储,如:1、0.2、3.5e20等
字符串(string) 该类型可以存储字符串,且与redis的键值一样都是二进制安全的。字符串可以使用单引号或双引号表示,两个字符是相同的。比如’a’,“b” 都是可以的。字符串中可以包含转义字,如\n、\r等
表(table) 表类型是lua语言中唯一的数据结构,既可以当数组又可以当字典(表类型的索引从1开始)
函数(function) 函数在Lua中是一等值(first-class value),可以存储在变量中、作为函数的参数或返回结果

3.1.2).变量

Lua的变量分为全局变量和局部变量。全局变量无需声明就可以直接使用,默认值是nil。如:
a = 1 – 为全局变量a赋值
print(b) --无需声明直接使用,默认值为nil
a = nil – 删除全局变量a的方法是将其赋值为nil。全局变量真没有声明和未声明之分,只有非nil和nil的区别。

在redis脚本中不能使用全局变量,值允许使用局部变量,以防止脚本之间相互影响。声明全局变量的方法为local变量名,就像这样:
local c --声明一个局部变量c,默认值是nil
local d = 1 – 声明一个局部变量d并赋值为1
local e, f --可以同时声明多个局部变量

声明一个存储函数的局部变量的方法为:
local say_hi = function ()
print ‘hi’
end

注意: 变量名只能是非数字开头,只能包含字母、数字和下划线,区分大小写。变量名不能与Lua关键字相同,保留关键字如下:
and break do else elseif
end false for function if
in local nil not or
repeat return then true until while

局部变量的作用域为从声明开始到所在层的语句块末尾,比如:
local x = 10
if true then
local x = x + 1
print(x)//11
do
local x = x + 1
print(x)//12
end
print(x)//11
end
print(x)//10

3.1.3).注释

Lua的注释分为单行和多行两种:
单行注释以–开始,到行位结束,一般习惯在–后面跟一个空格
多行注释以–[[开始,到]]结束,如:
–[[
这是一个注释
]]

3.1.4).赋值
Lua支持多重赋值:
local a,b = 1,2 --a=1 b=2
local c,d = 1,2,3 – c=1 d=2 3废弃
local e,f = 1 – e=1 f = nil

在执行多重赋值时,Lua会先计算所有表达式的值,比如:
local a = {1,2,3}
local i = 1
i,a[i] = i + 1 , 5
Lua计算所有表达式的值后,上面最后一个赋值语句变为i,a[1] = 2,5,所以赋值后i的值为2,a则为{5,2,3}
Lua中函数也会返回多个值。

3.1.5).操作符

Lua有以下5类操作符。

  1. 数学操作符。数学操作符包括常见的+、-、*、/、%(取模)、-(一元操作符,取负)和幂运算^。
    数学操作符的操作数如果是字符串会自动转换成数字,比如:
    print(‘1’ + 1) – 2
    print(‘10’ * 2) – 20

  2. 比较操作符
    == 比较两个操作数的类型和值是否都相等
    = 与==的结果相反
    <,>,<=,>= 小于,大于、小于等于、大于等于
    比较操作符的结果一定是布尔类型,比较操作符不同于数学操作符,不会对两边的操作数进行自动类型转换,也就是说:
    print(1 == ‘1’) --false,二者类型不同,不会进行自动类型转换
    print({‘a’} == {‘a’}) --false,对于表类型的值,比较的是二者的引用
    如果比较字符串和数字,可以手动进行类型转换,如下:
    print(1 == tonumber(‘1’)) – true
    print(‘1’ == tostring(1)) – true
    其中 tonumber函数还可以进行进制转换,比如 print(tonumber(‘F’,16)) – 将字符串’F’从16进制转换成10进制结果是15

  3. 逻辑操作符
    not 根据操作数的真和假相应地返回false和true
    and a and b 中如果a是真则返回b,否则返回a
    or a or b 中如果a是真则返回a,否则返回b

只要操作数不是nil或false,逻辑操作符就认为操作数是真,否则是假。特别需要注意的是即时是0或空字符串也被当做真。例如:
print(1 and 5) – 5
print(1 or 5) – 1
print(not 0) – false
print(’’ or 1) – ‘’

Lua的逻辑操作符支持短路,也就是说对于false and foo(),Lua不会调用foo函数,因为第一个操作符已经决定了无论foo函数返回的结果是什么,该表达式的值都是false,or操作符与之类似。

  1. 连接操作符
    连接操作符只有一个:..,用来连接两个字符串,比如:
    print(‘hello’..’ '..‘world’) – ‘hello world’
    连接操作符会自动把数值类型的值转换成字符串类型:
    print('this price is ’ .. 25) – ‘this price is 25’

  2. 取长度操作符
    取长度操作符是Lua5.1中新增加的操作符,同样只有一个,即#,用来获取字符串或表的长度。
    print(#‘hello’) --5
    各个运算符的优先级顺序(优先级依次降低)如下:

^
not # -(一元)
*  %
+ -
..
< > <= >= ~= ==
and
or
  1. if 语句
    Lua的if语句格式如下:
if 表达式 then
    语句块
elseif 条件表达式 then
    语句块
else
    语句块
end

Lua和JavaScript 一样每个语句都可以;结尾,但一般来说编写Lua时都会省略;
Lua也不强制要求缩进,所有语句也可以写在一行:

a = 1
b = 2
if a then
    b = 3
else
    b = 4
end
可以写成
a = 1 b = 2 if a then b = 3 else b = 4 end
甚至
a = 
1 b = 2 if a
then b =3 else b
= 4 end

但是为了增加可读性,在编写时一定要注意缩进。

  1. 循环语句
    Lua支持while、repeat和for循环语句。
    while语句的形式为:
while 条件表达式 do
    语句块
end

repeat语句的形式为:

repeat 
    语句块
until 条件表达式

for 语句有两种形式,一种是数字形式:

for 变量 = 初值,终值,步长 do
    语句块
end

其中步长可以省略,默认步长为1。例如:
使用for循环计算1~100的和:

local sum = 0
for i = 1,100 do
    sum = sum + i
end

for 语句的通用形式为:

for 变量1,变量2,...,变量N in 迭代器 do
    语句块
end

在编写redis脚本时我们常用通用行的for语句遍历表的值。

  1. 表类型
    表示Lua中唯一的数据结构,可以理解为关联数组,任何类型的值(除了空类型)都可以作为表的索引。表的定义方式为:
a = {} -- 将变量a赋值为一个空表
a['field'] = 'value' -- 将field字段赋值为value
print(a.field) -- 打印内容为'value',a.field是a['field']的语法糖

people = {    -- 也可以这样定义
    name = 'Bob'
    age = 29
}
print(people.name) -- 打印内容为'Bob'
当索引为整数时表和传统数组一样,例如:
a = {}
a[1] = 'Bob'
a[2] = 'Jeff'
或者
a = {'Bob','Jeff'}
print(a[1]) -- 打印内容为'Bob' 索引从1 开始

可以使用通用形式的for语句遍历数组,例如:

for index, value in ipairs(a) do
    print(index) -- index迭代数组a的索引
    print(value) -- value 迭代数组a的值
end

打印结果为:
1
Bob
2
Jeff

ipairs 是Lua内置的函数,实现类似迭代器的功能,当然还可以使用数字形式的for语句遍历数组,例如:

for i = 1,#a do
    print(i)
    print(a[i])
end

输出结果和上面相同。

Lua还提供了一个迭代器pairs,用来遍历非数组的表值,如:

people = {
    name = 'Bob'
    age = 29
}
for index,value in pairs(people) do
    print(index)
    print(value)
end

打印结果为
name
Bob
age
29

注意: pairs 和 ipairs 的区别在与前者会遍历所有值不为nil的索引,而后者只会从索引1开始递增遍历到最后一个值不为nil的整数索引。

  1. 函数

函数的定义为:

function (参数列表)
    函数体
end
或者还可以将其赋值给一个局部变量:
local square = function (num) -- 如果没参数括号也不能省略
    return num * num
end 
Lua还提供了语法糖来简化函数的定义:
local function square (num)
    return num * num
end
这段代码会被转义成
local square
square = function (num)
    return num * num
end

因为在赋值前声明了局部变量square,所以可以再函数内部引用自身,从而实现递归。
如果实参的个数小于形参的个数,则没有匹配到的形参的值为nil。相对于的,如果实参的个数大于形参的个数,则多出的实参会被忽略。
如果希望捕获多个实参(即实现可变参数个数),可以让最后一个形参为

local function square (...)
    local argv = {...}//将...转换成表argv
    for i = 1 , #argv do
        argv[i] = argv[i] * argv[i]
    end
    return unpack(argv)//unpack函数相当于return argv[1], argv[2], argv[3]
end

a, b, c = square(1, 2, 3)
    print(a)
    print(b)
    print(c)
输出结果为:
1
4
9

3.1.6). 标准库
Lua的标准库中提供了很多函数,比如前面介绍的迭代器,ipairs和pairs,类型转换函数tonumber和tostring,还有unpack函数都属于标准库的“Base”库。
Redis支持大部分Lua标准库:

库名 说明
Base 提供了一些基础函数
String 提供了用于字符串操作的函数
Table 提供了用于表操作的函数
Math 提供了数学计算函数
Debug 提供了用于调式的函数

Lua手册(www.lua.org/manual/5.1/manual.html#5)

  1. String库

String库的函数可以通过字符串类型的变量以面向对象的形式访问,如string.len(string_var)可以写成string_var:len()。

1.1).获取字符串长度

string.len(string)
//该函数的作用和操作符#类似,例如:
>print(string.len('hello'))
5
>print(#'hello')
5

1.2).转换大小写

string.lower(string)
string.upper(string)
//例如:
>print(string.lower('HELLO'))
hello
>print(string.upper('hello'))
HELLO

1.3).获取字符串

string.sub(string, start [, end]) 

可以获取一个字符串从索引start开始到end结束的子字符串,索引从1开始。索引可以是负数,-1代表最后一个元素。end默认为-1。

>print(string.sub('hello', 1))
hello
>print(string.sub('hello', 2))
ello
>print(string.sub('hello', 2, -2))
ell
>print(string.sub('hello, -2))
lo
  1. Table库

Table库中大部分函数都需要表的形式是数组形式。

2.1).将数组转换成字符串

table.concat(table [, sep [, i [, j]]])

该函数有点类似JS中的join函数,可以将一个数组转换成一个字符串,中间以sep参数指定字符串分割,默认为空字符串。i和j用来限制要转换的表元素的索引范围,默认分别是1和表的长度,不支持负索引:

>print(table.contact({1, 2, 3}))
123
>print(table.contact({1, 2, 3}, ',', 2))
2,3
>print(table.contact({1, 2, 3}, ',', 2, 2))
2

2.2).向数组中插入元素

table.insert(table, [pos ,] value)

向指定索引位置pos插入元素value,并将后面的元素顺序后移。默认pos的值是数组长度加一,即在数组尾部插入:

>a = {1, 2, 4}
>table.insert(a, 3, 3)
>table.insert(a, 5)
>print(table.concat(a, ', '))
1, 2, 3, 4, 5

2.3).从数组中弹出一个元素。

table.remove(table [, pos])

从指定的索引删除一个元素,并将后面的元素迁移,返回删除的元素值。默认pos的值是数组的长度,即弹出数组尾部的元素:

>table.remove(a)
>table.remove(a, 1)
>print(table.concat(a, ','))
2, 3, 4
  1. Math库
    Math库提供了常用的数字运算函数,如参数是字符串会自动尝试转换成数字。具体的函数列表:
函数定义 说明
math.abs(x) 求数字的绝对值
math.sin(x) 求三角函数sin的值
math.cos(x) 求三角函数cos的值
math.tan(x) 求三角函数tan的值
math.ceil(x) 向上取整
math.floor(x) 向下取整
math.max(x, …) 获得函数中最大的值
math.min(x, …) 获得函数中最小的值
max.pow(x, y) 获得x的y次方
max.sqrt(x) 获得x的平方根

此外还有随机数函数:

math.random([m [, n]])
math.randomseed(x)

math.random()函数用来生成一个随机数,根据参数不同其返回值范围也不同:
没有提供参数:返回范围在[0,1)的实数;
只提供了m参数:返回范围在[1, n]的整数;
同时提供了m和n参数:返回范围在[m, n]的整数。
math.random()函数生成的随机数是依据种子(seed)计算得来的伪随机数,意味着使用同一种子生成的随机数是相同的。可以使用math.randomseed()函数设置种子的值:

>math.randomseed(1)
>print(math.random(1,100))
1
>print(math.random(1,100))
14
>print(math.random(1,100))
76
>math.randomseed(1)//重置一下种子
>print(math.random(1,100))
1//重新得到相同的值
>print(math.random(1,100))
14
>print(math.random(1,100))
76

3.1.7).其他库
除了标准库以外,redis还通过cjson库和cmsgpack提供了对JSON和MessagePack的支持。redis自动加载了这两个库,在脚本中可以分别通过cjson和cmsgpack两个全局变量来访问对应的库。两者的用法如下:

local people = {
    name = 'Bob',
    age = 29
}
-- 使用cjson序列化字符串:
local json_people_str = cjson.encode(people)
-- 使用cmsgpack序列化成字符串
local msgpack_people_str = cmsgpack.pack(people)

-- 使用cjson将序列化后的字符串还原成表
local json_people_obj = cjson.decode(people)
print(json_people_obj.name)
-- 使用cmsgpack将序列化后的字符串还原成表
local msgpack_people_obj = cmsgpack.unpack(people)
print(msgpack_people_obj.name)
  1. Redis 与 Lus

4.1).在脚本中调用redis
在脚本中可以使用redis.call函数调用Redis命令:
redis.call(‘set’,‘foo’,‘bar’)
local value = redis.call(‘get’, ‘foo’) --获取到value 的值是bar
redis.call函数的返回值就是redis的命令执行结果(空结果比较特殊,Lua对应的数据类型是false)。如下表:

Redis返回值类型 Lua数据类型
整数回复 数字类型
字符串回复 字符串类型
多行字符串回复 表类型(数组形式)
状态回复 表类型(只有一个ok字段存储状态信息)
错误回复 表类型(只有一个err字段存储错误信息)

redis还提供了redis.pcall函数,功能与redis.call相同,唯一的区别是当命令执行错误时redis.pcall会记录错误并继续执行,而redis.call会直接返回错误,不继续执行。

4.2). 从脚本中返回值
在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句默认返回nil。因为我们可以像调用其他redis内置命令一样调用我们自己写的脚本,所以同样redis会自动将脚本返回值的Lua数据类型转换成Redis的返回值类型(其中Lua的false比较特殊,会被转换成空结果)。如下表

Lua数据类型 Redis返回值类型
数字类型 整数回复(Lua的数字类型会被自动转换成整数)
字符串类型 字符串回复
表类型 多行字符串回复
表类型(只有一个ok字段存储状态信息) 状态回复
表类型(只有一个err字段存储错误信息) 错误回复

4.3).脚本相关命令

4.3.1). EVAL命令

Redis提供了EVAL命令可以让开发者像调用其他Redis内置命令一样调用脚本。EVAL命令的格式是:EAVL脚本内容key参数的数量[key …] [arg …]。可以通过key 和 arg 这两类参数向脚本传递数据,他们的值可以在脚本中分别用KEYS 和 ARGV 两个表类型的全局变量访问。比如:
用脚本实现一个set命令
redis > EVAL “return redis.call(‘SET’, KEYS[1], ARGV[1])” 1 foo bar
OK
redis > GET foo
“bar”
其中要读写的键名应该作为key参数,其他的数据都作为arg参数。
注意: EAVL命令依据第二个参数将后面的所有参数分别存入脚本KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时,也不能省略这个参数(设为0)。

EVALSHA命令
考虑到脚本比较长的情况下,如果每次调用脚本都需要将整个脚本传给Redis会占用较多的贷款。为了解决这个问题,Redis提供了一个EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本,该命令的用法和EAVL一样。只不过是将脚本内容替换成脚本内容的摘要。

redis在执行EAVL命令时会先计算脚本的SHA1摘要并记录在脚本缓存中,执行费EAVLSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则返回错误。

一般在程序中使用EAVLSHA命令如下:
1).先计算脚本的SHA1摘要,并使用EVALSHA命令执行脚本
2).获得返回值,如果返回错误则使用EVAL命令重新执行脚本。

有些语言会代替开发者完成这一步,也就是说使用EVAL命令时,他们会先计算SHA1值,并尝试执行EAVLSHA命令,如果失败了则会直接使用EVAL命令。

4.3.2).在PHP中使用Lua脚本

实例1—同时获取多个散列类型的键值

 'tcp',
    'host' => '127.0.0.1',
    'port' => 6380
));

class HMGetAll extends Predis\Command\ScriptCommand
{
    //定义前多少个参数会被作为KEYS变量
    //false 表示所有的参数
    public function getKeysCount()	
    {
        return false;
    }

    //返回脚本内容 ,如果出现解析错误那么可能是<<getProfile()->defineCommand('hmgetall','HMGetAll');

//执行hmgetall命令
$value = $redis->hmgetall('user:1','user:2','user:3');
print($value)
array (size=3)
  0 => 
    array (size=2)
      0 => string 'name' (length=4)
      1 => string '1' (length=3)
  1 => 
    array (size=2)
      0 => string 'name' (length=4)
      1 => string '2' (length=4)
  2 => 
    array (size=2)
      0 => string 'name' (length=4)
      1 => string '3' (length=4)

4.3.3).获得并删除有序集合中分数最小的元素

 'tcp',
    'host' => '127.0.0.1',
    'port' => 6380
));

class ZGetMinScopen extends Predis\Command\ScriptCommand
{
    //定义前多少个参数会被作为KEYS变量
    //false 表示所有的参数
    public function getKeysCount()
    {
        return false;
    }

    //返回脚本内容
    public function getScript()
    {
        return <<getProfile()->defineCommand('zgetminscope','ZGetMinScopen');

//执行zgetminscope命令
$result = $redis->zgetminscope('memberScope');

var_dump($result);
string 'sgm' (length=3)

4.3.4).处理JSON
为学生添加课程成绩并且计算总分的Lua脚本:

 'tcp',
    'host' => '127.0.0.1',
    'port' => 6380
));

class HandleJson extends Predis\Command\ScriptCommand
{
    //定义前多少个参数会被作为KEYS变量
    //false 表示所有的参数
    function getKeysCount()
    {
        return false;
    }

    //返回脚本内容
    function getScript()
    {
        return 
        <<name = $name;
    }

    function addCourse($course,$score)
    {
        $this->course[$course] = $score;
    }
}

$student1 = new Student('sgm1');
$student2 = new Student('sgm2');
$student3 = new Student('sgm3');

$student1->addCourse('Math', '30');
$student1->addCourse('Chinese', '90');

$student2->addCourse('Math', '50');
$student2->addCourse('Chemistry', '70');

$student3->addCourse('English', '100');
$student3->addCourse('Math', '100');

$arr = [
    'user:1' => json_encode($student1, true),
    'user:2' => json_encode($student2, true),
    'user:3' => json_encode($student3, true),
    'user:4' => json_encode(array(
        'Math' => '1',
        'Chinese' => '2'
    ))
];

$redis->mset($arr);

$redis->getProfile()->defineCommand('handlejson', 'HandleJson');

$result = $redis->handlejson('user:1', 'user:2', 'user:3');

var_dump($result);
440

注意: Lua脚本不会直接返回数组或对象(目前我测试下来),所以如果直接返回user或者 return cjson.decode(user) 都会直接返回空对象。

技巧 因为在脚本中我们使用了unpack函数将KEYS表展开,所以执行脚本时我们可以传入任意数量的键参数。

  1. 深入脚本

5.1 KEYS与ARGV

通过脚本传递的参数分为KEYS和ARGV两类,前者表示要操作的键名,后者表示非简明参数。但是,这一要求并不是强制的,比如:eval “return redis.call(‘get’, KEYS[1])” 1 user:Bob 可以获得user:Bob的键值,同样还可以使用EVAL “return redis.call(‘get’, ‘user:’..ARGV[1])” 0 Bob 来完成同样的功能,此时我们虽然没有按照redis规定传递键名,但是还是获得了同样的结果。

虽然规则不是强制的,单不遵守规则依然有一定的代价。redis发布的3.0版本会带有集群(cluster)的功能,集群的作用是将数据库中的键分散到不同的节点上,这以为着在脚本执行前就需要知道脚本会操作哪些键以便找到对应的节点,所以如果脚本中的键名没有使用KEYS参数传递则无法兼容集群。

有时候键名是根据脚本某部分的执行结果生成的,这时就无法在执行前将键名明确标出,比如一个集合类型键存储用户ID列表,每个用户使用散列键存储,其中有一个字段是年龄:

local sum = 0
lcoal users = redis.call('SMEMBERS', KEYS[1])
for _, user_id in ipairs(users) do
    local user_age = redis.call('HGET', 'user:'..user_id, 'age')//用了KEYS中没有的键
    sum = sum + user_age
end
return sum / #users

这个脚本同样无法兼容集群功能(因为在第四行,访问了KEYS中没有的键),但是却十分实用,它避免了数据往返客户端和服务端的开销。为了兼容集群,可以再客户端获取集合中的用户ID,然后将用户ID组装成键名列表传给脚本并计算平均年龄。

5.2 沙盒与随机数

Redis脚本禁止使用Lua标准库中与文件或系统调用相关的函数,在脚本中只允许对redis的数据进行处理。并且redis还通过禁用脚本的全局变量的方式保证每个脚本都是相对隔离的,不会互相干扰。

使用沙盒不仅是为了保证服务器的安全性,而且还确保了脚本的执行结果只和脚本本身和执行时传递的参数有关,不依赖外界条件(如系统时间、系统中某个文件的内容、其他脚本执行结果等)。这是因为在执行复制和AOF持久化操作时记录的是脚本的内容而不是脚本调用的命令,所以必须保证在脚本内容和参数是一样的前提下脚本的执行结果必须是一样的。

除了沙盒外,为了确保执行的结果可以重现,redis还对随机数和会产生随机结果的命令进行了特殊处理。

对于随机数而言,redis替换了math.random和math.randomseed函数使得每次执行脚本时产生的随机数列都相同,如果希望获得不同的随机数序列,最简单的方法是由程序生成随机数并通过参数传递给脚本,或者采用更灵活的方法,即在程序中生成随机数种子。这样在脚本中调用math.random产生的随机数就不同了。

对于会产生随机结果但无法排序的命令(比如只会产生一个元素),redis会在这类命令执行后将该脚本状态标记为lua_random_dirty,此后只允许调用只读命令,不允许修改数据库的值,否则会返回错误。属于此类的redis命令有SPOP,SRANDMEMBER,RANDOMKEY和TIME

5.3 其他脚本相关命令

除了EVAL和EVALSHA外,redis还提供了其他4个脚本相关的命令,一般都会被客户端封装起来,开发者很少能使用到。

5.3.1 SCRIPT LOAD: 将脚本加入缓存

每次执行EVAL命令时redis都会将脚本的SHA1摘要加入到脚本缓存中,以便下次客户端可以使用EVALSHA命令调用该脚本。如果只是希望脚本加入缓存而不执行则可以使用SCRIPT LOAD命令,返回值是脚本的SHA1摘要:

redis > SCRIPT LOAD “return 1”
“e0e1f9fabfc9d4800c877a703b823ac0578ff8db”

5.3.2 SCRIPT EXISTS: 判断脚本是否已经被缓存
SCRIPT EXISTS命令可以同时查找1个或多个脚本的SHA1摘要是否被缓存:
redus > SCRIPT EXISTS e0e1f9fabfc9d4800c877a703b823ac0578ff8db 123
1). (integer) 1
2). (integer) 0

5.3.3 SCRIPT FLUSH: 清空脚本缓存
redis 将脚本的SHA摘要加入到缓存后会永久保留,不会删除,但可以手动使用该命令清空缓存:
redis > SCRIPT FLUSH
OK
5.3.4 SCRIPT KILL: 强制终止当前脚本的执行
如果想终止当前正在执行的脚本可以使用SCRIPT KILL命令。

5.4 原子性和执行时间
redius的脚本执行是原子的,即脚本执行期间redis不会执行其他脚本。所有的命令都必需等待脚本执行完成后才能执行。为防止某个脚本执行时间过长导致redis无法提供服务(比如进入死循环),reids提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,redis将开始接受其他命令但不会执行,以确保脚本的原子性,而是会返回"busy"错误。如:
首先在一个脚本中执行死循环脚本
redis A > EVAL “while true do end” 0
这个时候再B中发送命令
redis B > GET a

这时候实例B并不会返回结果,而是在5秒后返回一个busy错误:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
(1.60s)

在实例B中执行SCRIPT KILL命令可以终止当前脚本的运行:
redis B > SCRIPT KILL
OK

此时脚本被终止,并且实例A会返回错误。

注意 当该脚本对redis的数据进行了修改(如调用了SET、LPUSH或DEL等命令)则SCRIPT KILL命令不会终止脚本的运行以防脚本只执行了一部分。因为如果脚本只执行了一部分然后被终止,那么就会违背原子性要求,所以要么脚本的所有命令都执行,要么都不执行。所以这个时候只能用SHUTDOWN NOSAVE命令强行终止Redis。而SHUTDOWN NOSAVE 和 SHUTDOWN 命令的区别在与,前者不会进行持久化操作,这意味着所有发生在上一次快照后的数据库修改都会丢失。

你可能感兴趣的:(记录随笔)