Redis 17_Lua脚本扩展Redis功能

一、Lua脚本概述

1.1 Lua脚本简介

Lua 由标准 C 编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。Lua 脚本可以很容易的被 C/C ++ 代码调用,也可以反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中可以被广泛应用。

Lua 脚本在游戏领域大放异彩,大家耳熟能详的《大话西游II》,《魔兽世界》都大量使用 Lua 脚本。Java 后端工程师接触过的 api 网关,比如 Openresty ,Kong 都可以看到 Lua 脚本的身影。

1.2 Redis中的Lua脚本支持

从 Redis 2.6.0 版本开始, Redis内置的 Lua 解释器,可以实现在 Redis 中运行 Lua 脚本。

使用 Lua 脚本的好处 :

  • 减少网络开销:将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其它命令插入。
  • 复用:客户端发送的脚本会永久存在 Redis 中,其它客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

Redis是一款高性能的键值存储数据库,提供了丰富的数据结构和功能,如字符串、列表、哈希、集合、有序集合等。然而,有时候我们需要执行一些复杂的操作,或者需要保证多个操作的原子性,这就需要用到Redis的Lua脚本功能。

Tips: 虽然Redis也支持事务,但Redis 的事务模式具备如下特点:

  • 保证隔离性;
  • 无法保证持久性;
  • 具备了一定的原子性,但不支持回滚;
  • 一致性的概念有分歧,假设在一致性的核心是约束的语意下,Redis 的事务可以保证一致性。

Redis内置了Lua脚本引擎,允许用户编写和执行Lua脚本。Lua是一种轻量级、高效的脚本语言,具有简洁的语法和强大的表达能力,非常适合用于编写Redis的扩展脚本。

Lua脚本在Redis服务器端执行,可以访问Redis提供的各种命令和数据结构。脚本执行是原子性的,保证了多个命令的执行是连续、不可中断的。

Redis会将执行过的Lua脚本缓存起来,当需要执行相同的脚本时,直接从缓存中获取,避免了重复解析和编译的开销。

1.3 Redis Lua脚本与事务

从定义上来说, Redis 中的脚本本身就是一种事务,所以任何在事务里可以完成的事,在脚本里面也能完成。并且一般来说,使用脚本要来得更简单,并且速度更快。

使用事务时可能会遇上以下两种错误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其它更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

Tips: 从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

经过测试 Lua 中发生异常处理方式和 redis 事务一致,可以说这两个东西是一样的,但是 Lua 支持缓存,可以复用脚本,这个是原来的事务所没有的

1.4 Redis Lua脚本的执行流程

对于Redis Lua脚本的执行流程,可以分为加载脚本、编译脚本和执行脚本三个阶段。

Tips: Redis Lua脚本的执行流程包括加载脚本、编译脚本和执行脚本三个阶段。加载脚本时会进行脚本缓存,编译脚本会将脚本转换为可执行的字节码,执行脚本具有原子性和事务性特点。

1、加载脚本

脚本缓存机制:

  • Redis脚本缓存的目的是为了提高脚本的执行效率,避免每次执行脚本都需要重新加载和编译。
  • 脚本缓存的实现方式是将脚本的SHA1散列值和脚本内容一起保存在Redis服务器的脚本缓存中。

脚本加载与缓存的关系:

  • 脚本加载的流程是将脚本传输给Redis服务器,并通过SHA1散列值判断脚本是否已经存在于缓存中。
  • 如果脚本已经存在于缓存中,则直接返回脚本的SHA1散列值。
  • 如果脚本不存在于缓存中,则将脚本进行缓存,并返回脚本的SHA1散列值。
  • 在使用脚本时,可以通过脚本的SHA1散列值来引用脚本,而不需要每次都传输脚本内容。

2、编译脚本

Lua脚本语法:

  • Lua脚本是基于Lua语言的一种脚本语言,具有自己的语法规则。
  • 常用的Lua语法元素包括变量、表达式、控制结构、函数等。

脚本编译过程:

  • 脚本编译的原理是将Lua脚本解析为一种中间表示形式(字节码)。
  • 在编译过程中,会检查脚本的语法错误,并将脚本转换为可执行的字节码。

3、执行脚本

脚本执行的原子性:

  • Redis Lua脚本具有原子性特点,即脚本中的所有操作要么全部执行成功,要么全部不执行。
  • 这是因为Redis在执行脚本时会将脚本作为一个整体进行执行,不会被其它操作中断。

脚本执行的事务性:

  • Redis事务是一种原子性的操作集合,可以将多个操作封装在一个事务中进行执行。
  • 在Lua脚本中,可以使用Redis事务的命令(如MULTI、EXEC、WATCH等)来实现事务性操作。

二、Redis 中 Lua 脚本常用命令

2.1 Redis 中 Lua 脚本常用命令列表

Lua命令 描述
EVAL script numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。
EVALSHA sha1 numkeys key [key ...] arg [arg ...] 执行 Lua 脚本。
SCRIPT LOAD script 将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。
SCRIPT EXISTS script [script ...] 查看指定的脚本是否已经被保存在缓存当中。
SCRIPT KILL 杀死当前正在运行的 Lua 脚本。
SCRIPT FLUSH 从脚本缓存中移除所有脚本。

2.2 EVAL 命令

命令格式:

1
EVAL script numkeys key [key ...] arg [arg ...]

说明:

  • script 是第一个参数,是一段 Lua 5.1脚本程序,它会被运行在Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。
  • numkeys 参数用于指定键名参数的个数,如果 numkeys > 0 则从第三个参数开始的 numkeys 个参数为redis key。
  • key [key ...],从 eval 的第三个参数开始算起,使用了 numkeys 个键(key),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用1为基址的形式访问(KEYS[1],KEYS[2]···)。
  • arg [arg ...],参数,在 Lua 脚本中通过 ARGV[1], ARGV[2] 获取。

简单实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
127.0.0.1:6379> eval "return ARGV[1]" 0 100 
"100"
red127.0.0.1:6379is> eval "return {ARGV[1],ARGV[2]}" 0 100 101
1) "100"
2) "101"
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

在 Lua 脚本中通过 redis.call() 来执行了 Redis 命令:

1
2
3
4
5
6
7
8
127.0.0.1:6379> set mystring 'hello world'
OK
127.0.0.1:6379> get mystring
"hello world"
127.0.0.1:6379> EVAL "return redis.call('GET',KEYS[1])" 1 mystring
"hello world"
127.0.0.1:6379> EVAL "return redis.call('GET','mystring')" 0
"hello world"

2.3 EVALSHA 命令

使用 EVAL 命令每次请求都需要传输 Lua 脚本主体(script body) ,若 Lua 脚本过长,不仅会消耗网络带宽,而且也会对 Redis 的性能造成一定的影响。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)。

EVALSHA 的思路是将 Lua 脚本先缓存起来,返回给客户端 Lua 脚本的 sha1 摘要。客户端存储脚本的 sha1 摘要,每次请求执行 EVALSHA 命令即可。

EVALSHA 命令的表现如下:

  • 如果服务器还缓存了给定的 SHA1 校验和所指定的脚本,那么执行这个脚本
  • 如果服务器未缓存给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用 EVAL 代替 EVALSHA

EVALSHA 命令基本语法如下:

1
127.0.0.1:6379> EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

示例:

1
2
3
4
5
6
7
127.0.0.1:6379> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
127.0.0.1:6379> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"

127.0.0.1:6379> evalsha ffffffffffffffffffffffffffffffffffffffff 0
(error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).

客户端库的底层实现可以一直乐观地使用 EVALSHA 来代替 EVAL ,并期望着要使用的脚本已经保存在服务器上了,只有当 NOSCRIPT 错误发生时,才使用 EVAL 命令重新发送脚本,这样就可以最大限度地节省带宽。

这也说明了执行 EVAL 命令时,使用正确的格式来传递键名参数和附加参数的重要性:因为如果将参数硬写在脚本中,那么每次当参数改变的时候,都要重新发送脚本,即使脚本的主体并没有改变,相反,通过使用正确的格式来传递键名参数和附加参数,就可以在脚本主体不变的情况下,直接使用 EVALSHA 命令对脚本进行复用,免去了无谓的带宽消耗。

2.4 其它Lua常用命令

1、SCRIPT LOAD

1
SCRIPT LOAD script 

该命令将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行 SHA1 求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

2、SCRIPT EXISTS

1
SCRIPT EXISTS sha1 [sha1 ] 

给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 或 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中。

3、SCRIPT KILL

1
SCRIPT KILL

杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

4、SCRIPT FLUSH

1
script flush

该命令清除Redis服务端所有 Lua 脚本缓存。

三、Redis 与 Lua 的交互

3.1 Lua中执行redis命令

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

  • redis.call()
  • redis.pcall()

redis.call()redis.pcall() 两个函数的参数可以是任何格式正的 Redis 命令:

1
2
127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK

上面这段脚本的确实现了将键 foo 的值设为 bar 的目的,但是,它违反了 EVAL 命令的语义,因为脚本里使用的所有键都应该由 KEYS 数组来传递,就像这样:

1
2
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是 EVAL 这个命令,所有的 Redis 命令,在执行之前都会被分析,以此来确定命令会对哪些键进行操作。

因此,对于 EVAL 命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。除此之外,使用正确的形式来传递键还有很多其它好处,它的一个特别重要的用途就是确保 Redis 集群可以将你的请求发送到正确的集群节点。不过,这条规矩并不是强制性的,从而使得用户有机会滥用(abuse) Redis 单实例配置(single instance configuration),代价是这样写出的脚本不能被 Redis 集群所兼容。

redis.call()redis.pcall() 的唯一区别在于它们对错误处理的不同

redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

1
2
3
4
5
127.0.0.1:6379> lpush foo a
(integer) 1

127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

redis.call() 不同,redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

1
2
redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

3.2 Lua 数据类型和 Redis 数据类型之间转换

当 Lua 通过 redis.call()redis.pcall() 函数执行 Redis 命令的时候,命令的返回值会被转换成 Lua 数据结构。同样地,当 Lua 脚本在 Redis 内置的解释器里运行时,Lua 脚本的返回值也会被转换成 Redis 协议(protocol),然后由 EVAL 将值返回给客户端。

数据类型之间的转换遵循这样一个设计原则:如果将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。换句话说,Lua 类型和 Redis 类型之间存在着一一对应的转换关系

以下列出的是详细的转换规则:

  • 从 Redis 转换到 Lua :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Redis integer reply -> Lua number       # Redis 整数转换成 Lua 数字
Redis bulk reply    -> Lua string       # Redis bulk 回复转换成 Lua 字符串

# Redis 多条 bulk 回复转换成 Lua 表,表内可能有其它别的 Redis 数据类型
Redis multi bulk reply  -> Lua table (may have other Redis data types nested)

# Redis 状态回复转换成 Lua 表,表内的 `ok` 域包含了状态信息
Redis status reply      -> Lua table with a single ok field containing the status            

# Redis 错误回复转换成 Lua 表,表内的 `err` 域包含了错误信息
Redis error reply       -> Lua table with a single err field containing the error

# Redis 的 Nil 回复和 Nil 多条回复转换成 Lua 的布尔值 `false`
Redis Nil bulk reply and Nil multi bulk reply   -> Lua false boolean type
  • 从 Lua 转换到 Redis:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Lua number          -> Redis integer reply # Lua 数字转换成 Redis 整数
Lua string          -> Redis bulk reply # Lua 字符串转换成 Redis bulk 回复
Lua table (array)   -> Redis multi bulk reply # Lua 表(数组)转换成 Redis 多条 bulk 回复

# 一个带单个 `ok` 域的 Lua 表,转换成 Redis 状态回复
Lua table with a single ok field    -> Redis status reply

# 一个带单个 `err` 域的 Lua 表,转换成 Redis 错误回复
Lua table with a single err field   -> Redis error reply

Lua boolean false   -> Redis Nil bulk reply # Lua 的布尔值 `false` 转换成 Redis 的 Nil bulk 回复

从 Lua 转换到 Redis 有一条额外的规则,这条规则没有和它对应的从 Redis 转换到 Lua 的规则

1
2
# Lua 布尔值 `true` 转换成 Redis 整数回复中的 `1`
Lua boolean true -> Redis integer reply with value of 1  

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 将 Lua 值转换成 Redis 值
> eval "return 10" 0
(integer) 10

> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
   2) "Hello World!"

# 将 Redis 值转换成 Lua 值,然后再将 Lua 值转换成 Redis 值的类型转过程。
> eval "return redis.call('get','foo')" 0
"bar"

3.3 编写Lua脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 示例1:计算列表中元素的总和
local sum = 0
local values = redis.call('LRANGE', KEYS[1], 0, -1)
for _, v in ipairs(values) do
    sum = sum + tonumber(v)
end
return sum

-- 示例2:实现分布式锁
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local lockTime = tonumber(ARGV[2])
local result = redis.call('SET', lockKey, lockValue, 'NX', 'PX', lockTime)
return result

-- 示例3:封装为函数
local function calculate_sum(values)
    local sum = 0
    for _, v in ipairs(values) do
        sum = sum + tonumber(v)
    end
    return sum
end

四、Lua脚本应用场景

4.1 复杂事务操作

在Redis中,事务可以保证一组命令的原子性执行,但有时候需要执行的操作比较复杂,难以用单个命令或者事务来实现。这时候就可以通过Lua脚本来编写复杂的事务逻辑,保证这些操作的原子性。

示例:分布式锁实现

1
2
3
4
5
6
-- 实现分布式锁
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local lockTime = tonumber(ARGV[2])
local result = redis.call('SET', lockKey, lockValue, 'NX', 'PX', lockTime)
return result

4.2 定制化数据处理

有时候需要对Redis中的数据进行一些非常规的处理,如数据过滤、转换、聚合等,这时候可以通过Lua脚本来实现定制化的数据处理逻辑。

示例:统计集合中满足条件的元素个数

1
2
3
4
5
6
7
8
9
-- 统计集合中满足条件的元素个数
local count = 0
local members = redis.call('SMEMBERS', KEYS[1])
for _, member in ipairs(members) do
    if tonumber(member) > tonumber(ARGV[1]) then
        count = count + 1
    end
end
return count

4.3 原子性操作

通过Lua脚本可以保证一系列操作的原子性,避免了因为执行多个命令而可能出现的并发问题,确保数据的一致性。

示例:计数器的原子操作

1
2
3
4
-- 计数器的原子操作
local counterKey = KEYS[1]
local increment = tonumber(ARGV[1])
return redis.call('INCRBY', counterKey, increment)

4.4 复杂数据结构操作

有时候需要对Redis中的复杂数据结构进行操作,如列表、哈希、有序集合等,这时候可以通过Lua脚本来编写复杂的数据操作逻辑,保证操作的原子性和一致性。

示例:哈希表中值的批量更新

1
2
3
4
5
6
7
-- 哈希表中值的批量更新
local hashKey = KEYS[1]
local values = cjson.decode(ARGV[1])
for field, value in pairs(values) do
    redis.call('HSET', hashKey, field, value)
end
return OK

4.5 使用Redis+Lua实现限流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- java端送入三个参数(1个key,2个param)string
-- limitKey(redi中key的值)
local key = KEYS[1];
 
-- nums(次数)
local nums = ARGV[1];
 
-- expire(秒S)
local expire = ARGV[2];
 
-- 对key-value中的 value +1的操作  返回一个结果
local afterval = redis.call('incr', key);
 
-- 第一次
if afterval == 1 then
    
    -- 失效时间(1S)  TLL 1S
    redis.call('expire', key, tonumber(expire))
 
    -- 第一次不会进行限制
    return 1; 
end
 
-- 不是第一次,进行判断
if afterval > tonumber(nums) then
    
    -- 限制了
    return 0;
end
return 1;

五、Redis 中的 Lua脚本最佳实践

Lua 脚本在 Redis 中的应用十分灵活,但为了确保脚本的可靠性、性能和安全性,我们需要遵循一些最佳实践。

5.1 参数传递

Lua脚本支持通过KEYS和ARGV参数来传递键名和参数值。在编写脚本时,应该充分利用这两个参数来传递脚本所需的数据,而不是在脚本中直接硬编码数据。

1
2
3
-- 示例:使用KEYS和ARGV参数传递数据
local key = KEYS[1]
local value = ARGV[1]

5.2 错误处理

在 Lua 脚本中,应该充分考虑可能出现的异常情况,并进行相应的错误处理。可以使用 redis.error_reply() 函数来返回错误信息,以便在调用脚本时进行处理。

1
2
3
4
-- 示例:错误处理
if some_condition then
    return redis.error_reply("Some error occurred")
end

5.3 脚本优化

为了提高 Lua 脚本的性能,应该尽量减少不必要的命令调用和循环次数,尽量简化脚本的逻辑。可以将多个操作合并为一个,减少通信开销,提高执行效率。

1
2
3
4
-- 示例:脚本优化
local counterKey = KEYS[1]
local increment = tonumber(ARGV[1])
return redis.call('INCRBY', counterKey, increment)

5.4 原子性保证

Lua 脚本在 Redis 中的执行是原子性的,但需要注意的是,脚本内部的多个命令执行并不能保证原子性,需要根据具体场景来保证一系列操作的原子性。

1
2
3
4
5
6
-- 示例:保证一系列操作的原子性
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local lockTime = tonumber(ARGV[2])
local result = redis.call('SET', lockKey, lockValue, 'NX', 'PX', lockTime)
return result

5.5 脚本复用

为了提高代码的复用性和可维护性,可以将常用的 Lua 脚本封装为函数,然后在需要的地方进行调用。

1
2
3
4
5
6
7
8
-- 示例:封装为函数
local function calculate_sum(values)
    local sum = 0
    for _, v in ipairs(values) do
        sum = sum + tonumber(v)
    end
    return sum
end

5.6 安全性考虑

在编写Lua脚本时,需要考虑脚本的安全性,避免出现可能导致安全漏洞的代码。尤其是在接收外部输入时,应该进行参数验证和过滤,防止注入攻击等安全问题。

1
2
3
4
5
-- 示例:安全性考虑
local input = ARGV[1]
if not input or type(input) ~= "string" then
    return redis.error_reply("Invalid input")
end

5.7 全局变量保护

为了防止不必要的数据泄漏进 Lua 环境,Redis Lua脚本不允许创建全局变量。如果一个 Lua脚本需要在多次执行之间维持某种状态,它应该使用 Redis key 来进行状态保存。

企图在脚本中访问一个全局变量(不论这个变量是否存在)将引起脚本停止,EVAL 命令会返回一个错误:

1
2
redis 127.0.0.1:6379> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

Lua 的 debug 工具,或者其它设施,比如打印(alter)用于实现全局保护的 meta table ,都可以用于实现全局变量保护。

实现全局变量保护并不难,不过有时候还是会不小心而为之。一旦用户在脚本中混入了 Lua 全局状态,那么 AOF 持久化和复制(replication)都会无法保证,所以,请不要使用全局变量。

避免引入全局变量的一个诀窍是:将脚本中用到的所有变量都使用 local 关键字定义为局部变量

5.8 Lua脚本中记录Redis日志

在 Lua 脚本中,可以通过调用 redis.log 函数来写 Redis 日志(log):

1
redis.log(loglevel, message)

其中, message 参数是一个字符串,而 loglevel 参数可以是以下任意一个值:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

上面的这些等级(level)和标准 Redis 日志的等级相对应。

对于Lua脚本写的日志,只有那些和当前 Redis 实例所设置的日志等级相同或更高级的日志才会被记录。

以下是一个日志示例:

1
redis.log(redis.LOG_WARNING, "Something is wrong with this script.")

执行上面的函数会产生这样的信息:

1
[32343] 22 Mar 15:21:39 # Something is wrong with this script.

5.9 沙箱(sandbox)和最大执行时间

脚本应该仅仅用于传递参数和对 Redis 数据进行处理,它不应该尝试去访问外部系统(比如文件系统),或者执行任何系统调用。

除此之外,脚本还有一个最大执行时间限制,它的默认值是 5 秒钟,一般正常运作的脚本通常可以在几分之几毫秒之内完成,花不了那么多时间,这个限制主要是为了防止因编程错误而造成的无限循环而设置的。

最大执行时间的长短由 lua-time-limit 选项来控制(以毫秒为单位),可以通过编辑 redis.conf 文件或者使用 CONFIG GET parameterCONFIG SET parameter value 命令来修改它。

当一个脚本达到最大执行时间的时候,它并不会自动被 Redis 结束,因为 Redis 必须保证脚本执行的原子性,而中途停止脚本的运行意味着可能会留下未处理完的数据在数据集(data set)里面。

因此,当脚本运行的时间超过最大执行时间后,以下动作会被执行:

  • Redis 记录一个脚本正在超时运行
  • Redis 开始重新接受其它客户端的命令请求,但是只有 SCRIPT KILLSHUTDOWN NOSAVE 两个命令会被处理,对于其它命令请求, Redis 服务器只是简单地返回 BUSY 错误。
  • 可以使用 SCRIPT KILL 命令将一个仅执行只读命令的脚本杀死,因为只读命令并不修改数据,因此杀死这个脚本并不破坏数据的完整性
  • 如果脚本已经执行过写命令,那么唯一允许执行的操作就是 SHUTDOWN NOSAVE ,它通过停止服务器来阻止当前数据集写入磁盘

5.10 流水线(pipeline)上下文(context)中的 EVALSHA

在流水线请求的上下文中使用 EVALSHA 命令时,要特别小心,因为在流水线中,必须保证命令的执行顺序。

一旦在流水线中因为 EVALSHA 命令而发生 NOSCRIPT 错误,那么这个流水线就再也没有办法重新执行了,否则的话,命令的执行顺序就会被打乱。

为了防止出现以上所说的问题,客户端库实现应该实施以下的其中一项措施:

  • 总是在流水线中使用 EVAL 命令
  • 检查流水线中要用到的所有命令,找到其中的 EVAL 命令,并使用 SCRIPT EXISTS sha1 sha1 … 命令检查要用到的脚本是不是全都已经保存在缓存里面了。如果所需的全部脚本都可以在缓存里找到,那么就可以放心地将所有 EVAL 命令改成 EVALSHA 命令,否则的话,就要在流水线的顶端(top)将缺少的脚本用 SCRIPT LOAD script 命令加上去。

5.11 redis客户端执行Lua脚本

1
2
# --eval 后跟文件路径
redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 
  • --eval 而不是命令模式中的 “eval”,一定要有前端的 --
  • 脚本路径后紧跟 key [key …]``,相比命令行模式,少了numkeys 这个key数量值。
  • key [key …]arg [arg …] 之间的 , 英文逗号前后必须有空格,否则死活都报错
Licensed under CC BY-NC-SA 4.0