Skip to content

小功能大用处(上)

慢查询分析

许多存储系统(例如 MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。
所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息(例如: 发生时间,耗时,命令的详细信息)记录下来,Redis 也提供了类似的功能。

Redis 客户端执行一条命令分为如下 4 个部分:

(1) 发送命令
(2) 命令排队
(3) 命令执行
(4) 返回结果

需要注意,慢查询只统计步骤(3)的时间,所以没有慢查询并不代表客户端没有超时问题

慢查询的两个配置参数

对于慢查询功能,需要明确两件事:

  • 预设阈值怎么设置?
  • 慢查询记录存放在哪?

Redis 提供了 slowlog-log-slower-than 和 slowlog-max-len 配置来解决这两个问题。
从字面意思就可以看出,slowlog-log-slower-than 就是那个预设阈值,它的单位是微秒,默认值是 10000,假如执行一条 “很慢” 的命令(例如 keys*),如果它的执行时间超过了 1000 微秒,那么它将被记录在慢查询日志中

运维提示:

如果 slowlog-log-slower-than=0 会记录所有的命令,slowlog-log-slower-than<0 对于任何命令都不会进行记录

从字面意思看,slowlog-max-len 只是说明了慢查询日志最多存储多少条,并没有说明存放在哪里?
实际上 Redis 使用了一个列表来存储慢查询日志,slowlog-max-len 就是列表的最大长度。
一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,例如 slowlog-max-len 设置为 5,当有第 6 条慢查询插入的话,那么队头的第一条数据就出列,第 6 条慢查询就会入列

在 Redis 中有两种修改配置的方法,一种是修改配置文件,另一种是使用 config set 命令动态修改。
例如下面使用 config set 命令将 slowlog-log-slower-than 设置为 20000 微秒,slowlog-max-len 设置为 1000:

1
2
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000 config rewrite

如果要 Redis 将配置持久化到本地配置文件,需要执行 config rewrite 命令

虽然慢查询日志是存放在 Redis 内存列表中的,但是 Redis 并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理。下面介绍这几个命令:

(1) 获取慢查询日志

slowlog get [n]

下面操作返回当前 Redis 的慢查询,参数 n 可以指定条数

每个慢查询日志由 4 个属性组成,分别是慢查询日志的标识 id、发生时间戳、命令耗时、执行命令和参数

(2) 获取慢查询日志列表当前的长度

slowlog len

例如,当前 Redis 中有 45 条慢查询:

1
2
slowlog len
(integer) 45

(3) 慢查询日志重置

slowlog reset

实际是对列表做清理操作,例如:

1
2
3
4
5
6
slowlog len
(integer) 45
slowlog reset
OK
slowlog len
0

最佳实践

慢查询功能可以有效地帮助我们找到 Redis 可能存在的瓶颈,但在实际使用过程中要注意以下几点:

(1) slowlog-max-len 配置建议: 线上建议调大慢查询列表,记录慢查询时 Redis 会对长命令做截断操作,并不会占用大量内存。
增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为 1000 以上

(2) slowlog-log-slower-than 配置建议: 默认值超过 10 毫秒判定为慢查询,需要根据 Redis 并发量调整该值。
由于 Redis 采用单线程响应命令,对于高流量的场景,如果命令执行时间在 1 毫秒以上,那么 Redis 最多可支持 OPS 不到 1000.
因此对于高 OPS 场景的 Redis 建议设置为 1 毫秒

(3) 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。
因此客户端执行命令的时间会大于命令实际执行时间。
因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。

(4) 由于慢查询日志是一饿先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行 slow get 命令将慢查询日志持久化到其他存储中(例如 MySQL),然后可以制作可视化界面进行查询

Redis Shell

Redis 提供了 redis-cli、redis-server、redis-benchmark 等 Shell 工具。
它们虽然比较简单,但是麻烦虽小五脏俱全,有时可以很巧妙地解决一些问题

redis-cli 详解

可以执行 redis-cli --help 命令来进行查看,下面将对一些重要参数的含义以及使用场景进行说明

-r

-r (repeat) 选项代表嫁给你命令执行多次,例如下面操作将会执行三次 ping 命令:

1
2
3
4
redis-cli -r 3 ping
PONG
PONG
PONG

-i

-i (interval) 选项代表每隔几秒执行一次命令,但是 -i 选项必须和 -r 选项一起使用,下面的操作会每隔 1 秒执行一次 ping 命令,一共执行 5 次

1
2
3
4
5
6
redis-cli -r 5 -i 1 ping
PONG
PONG
PONG
PONG
PONG

例如下面的操作利用 -r 和 -i 选项,每隔 1 秒输出内存的使用量,一共输出 100 次

1
2
3
4
5
6
7
8
9
redis-cli -r 100 -i 1 info | grep used_memory_human
used_memory_human:1.02M
used_memory_human:1.02M
used_memory_human:1.02M
used_memory_human:1.02M
used_memory_human:1.02M
used_memory_human:1.02M
used_memory_human:1.02M
...

-x

-x 选项代表从标准输入(stdin)读取数据作为 redis-cli 的最后一个参数,例如下面的操作会将字符串 world 作为 set hello 的值:

1
2
echo "world" | redis-cli -x set heloo
OK

-c

-c(cluster)选项是连接 Redis Cluster 节点时需要使用的,-c 选项可以防止 moved 和 Ask 异常

-a

如果 Redis 配置了密码,可以用 -a(auth) 选项,有了这个选项就不需要手动输入 auth 命令

--scan 和 --pattern

--scan 选项和 --pattern 选项用于扫描指定模式的键,相当于使用 scan 命令

--slave

--slave 选项是把当前客户端模拟成当前 Redis 节点的从节点,可以用来获取当前 Redis 节点的更新操作。
合理的利用这个选项可以记录当前连接 Redis 节点的一些更新操作,这些更新操作很可能是实际开发业务时需要的数据。

下面开启第一个客户端,使用 --slave 选项,看到同步已完成:

1
2
3
redis-cli --slave
SYNC with master, discarding 7288 bytes of bulk transfer...
SYNC done. Logging commands from master.

再开启另一个客户度做一些更新操作:

··· redis-cli 127.0.0.1:6379> set hello world OK 127.0.0.1:6379> set a b OK 127.0.0.1:6379> incr count (integer) 1 127.0.0.1:6379> get hello "world" ···

第一个客户端会收到 Redis 节点的更新操作:

··· redis-cli --slave SYNC with master, discarding 7288 bytes of bulk transfer... SYNC done. Logging commands from master. "PING" "PING" "PING" "PING" "PING" "SELECT","0" "set","hello","world" "set","a","b" "PING" "incr","count" "PING" "PING" ···

注意:
PING 命令是由于主从复制产生的

--rdb

--rdb 选项会请求 Redis 实例生成并发送 RDB 持久化文件、保存在本地。
可使用它做持久化文件的定期备份

--pipe

--pipe 选项用于将命令封装成 Redis 通信协议定义的数据格式,批量发送给 Redis 执行

--bigkeys

--bigkeys 选项使用 scan 命令对 Redis 的键进行采样,从中找到内存占用比较大的键值,这些键可能是系统的瓶颈

--eval

--eval 选项用于执行指定 Lua 脚本

--latency

latency 有三个选项,分别是 --latency、--latency-history、--latency-dist。
它们都可以检测网络延迟,对于 Redis 的开发和运维非常有帮助

(1) --latency

该选项可以测试客户端到目标 Redis 的网络延迟,例如当前拓扑结构如下图所示。

客户端 B 和 Redis 在机房 B,客户端 A 在机房 A,机房 A 和机房 B 是跨地区的。

客户端 B:

1
2
redis-cli -h {machineB} --latency
min: 0, max: 1, avg: 0.07(4211 samples)

客户端 A:

1
2
redis-cli -h {machineB} --latency
min: 0, max: 2, avg: 1.04(2096 samples)

可以看到客户端 A 由于距离 Redis 比较远,平均网络延迟会稍微高一些

(2) --latency-history

--latency 的执行结果只有一条,如果想以分时段的形式了解延迟信息,可以使用 --latency-history 选项

1
2
3
redis-cli -h 10.10.xx.xx --latency-history
min: 0, max: 1, avg: 0.28(1330 samples) -- 15.01 seconds renage...
min: 0, max: 1, avg 0.05(1364 samples) -- 15.01 seconda range

可以看到延时信息每 15 秒输出一次,可以通过 -i 参数控制间隔时间

(3) --latency-dist

该选项会使用统计图表的形式从控制台输出延迟统计信息

--stat

--stat 选项可以实时获取 Redis 的重要统计信息,虽然 info 命令中的统计信息更全,但是能实时看到一些增量的数据(例如 requests) 对于 Redis 的运维还是有一定帮助的,如下所示:

1
2
3
4
5
6
7
redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections
40         2.04M    2       0       209 (+0)            15
40         2.04M    2       0       210 (+1)            15
40         2.04M    2       0       211 (+1)            15
40         2.04M    2       0       212 (+1)            15

--raw 和 --no-raw

--no-raw 选项是要求命令的返回结果是原始的格式,--raw 恰恰相反,返回格式化后的结果

在 Redis 中设置一个中文的 value:

1
2
redis-cli set hello "你好"
OK

如果正常执行 get 或者使用 --no-raw 选项,那么返回的结果是二进制格式:

1
2
3
4
5
➜  ~ redis-cli get hello
"\xe4\xbd\xa0\xe5\xa5\xbd"
➜  ~ redis-cli --no-raw get hello
"\xe4\xbd\xa0\xe5\xa5\xbd"
➜  ~

如果使用了 --raw 选项,将会返回中文:

1
2
➜  ~ redis-cli --raw get hello
你好

redis-server 详解

redis-server 除了启动 Redis 外,还有一个 --test-memory 选项。
redis-server-test-memory 可以用来检测当前操作系统能否稳定地分配指定容量的内存给 Redis,通过这种检测可以有效避免因为内粗那问题造成 Redis 崩溃,例如下面操作检测当前操作系统能否提供 1G 的内存给 Redis:

1
redis-server --test-memory 1024

整个内存检测的时间比较长。
当输出passed this test 时说明内存检测完毕,最后会提示 --test-memory 只是简单检测,如果有之一可以使用更加专业的内存检测工具:

通常无需每次开启 Redis 实例时都执行 --test-memory 选项,该功能更偏向于调试和测试,例如,想快速占满机器内存做一些极端条件的测试,这个功能是一个不错的选择

redis-benchmark 详解

redis-benchmark 可以为 Redis 做基准性能测试,它提供了很多选项帮助开发和运维人员测试 Redis 的相关性能

-c

-c(clients)选项代表客户端的并发数量(默认是 50)

-n <requests>

-n(num) 选项代表客户端请求总量(默认是 100000)

例如 redis-benchmark -c 100 -n 20000 代表 100 个客户端同时请求 Redis,一共执行 20000 次。
redis-benchmark 会对各类数据结构的命令进行测试,并给出性能指标:

1
2
3
4
5
6
7
8
===== GET =====
  20000 requests completed in 0.27 seconds
    100 parallel clients
    3 bytes payload
    keep alive: 1
  99.11% <= 1 milliseconds
  100.00% <= 1 milliseconds
  73529.41 requests per second

例如上面一共执行了 20000 次 get 操作,在 0.27 秒完成,每个请求数据量是 3 个字节,99.11% 的命令执行时间小于 1 毫秒,Redis 每秒可以处理 73529.41 次 get 请求

-q

-q 选项仅仅显示 redis-benchmark 的 requests per second 信息,例如

1
2
3
4
5
6
7
redis-benchmark -c 100 -n 200000 -q
PING_INLINE: 74349.45 requests per second
PING_BULK: 68728.52 requests per second
SET: 71174.38 requests per second...
LEARNGE_500(first 450 elements): 11299.44 requests per second
LEANGE_600 (first 600 elements): 9319.67 requests per second
MSET (10 keys): 70671.38 requests per second

-r

在一个空的 Redis 上执行了 redis-benchmark 会发现只有 3 个键:

1
2
3
4
5
6
dbsize
(integer) 3
keys*
1) "counter:__rand_int__"
2) "mylist"
3) "key:__rand_int__"

如果想向 Redis 插入更多的键,可以执行使用 -r(random) 选项,可以向 Redis 插入更多随机的键

redis-benchmark -c 100 -n 20000 -r 10000

-r 选项会在 key、counter 键上加一个 12 位的后缀,-r 10000 代表只对后四位做随机处理(-r 不是随机数的个数)。
例如上面操作后,key 的数量和结果结构如下:

1
2
3
4
5
6
7
8
dbsize
(integer) 18641
scan 0
1) "14336"
2) 1) "key:000000004580"
   2) "key:000000004519"
   ...
   10) "key:000000002113"

-P

-P 选项代表每个请求 pipeline 的数据量(默认为 1)

-k <boolean>

-k 选项代表客户端是否使用 keepalive,1 为使用,0 为不使用,默认值为 1

-t

-t 选项可以对指定命令进行基准测试

1
2
3
redis-benchmark -t get,set -q
SET: 98619.32 requests per second
GET: 97560.98 requests per second

--csv

--csv 选项会将结果按照 csv 格式输出,便于后续处理,如导出到 Excel 等

1
2
3
redis-benchmark -t get,set --csv
"SET","81300.81"
"GET","79051.38"

Pipeline

Pipeline 概念

Redis 客户端执行一条命令分为如下四个过程:

1) 发送命令
2) 命令排队
3) 命令执行
4) 返回结果

其中 1) + 4) 称为 Round Trip Time(RTT,往返时间)

Redis 提供了批量操作命令(例如 mget、mest 等),有效地节约 RTT。
但大部分命令是不支持批量操作的,例如要执行 n 次 hgetall 命令,并没有 mhgetall 命令存在,需要消耗 n 次 RTT。
Redis 的客户端和服务器可能部署在不同的机器上。
例如客户端在北京,Redis 服务器在上海,两地直线距离约为 1300 公里,那么 1 次 RTT 时间 = 1300 * 2/ (300000 * 2 / 3) = 13 毫秒(光在真空中传输速度为每秒 30 万公里,这里假设光纤为光速 2/3),那么客户端在 1 秒内大约只能执行 80 次左右的命令,这个和 Redis 的高并发吞吐背道而驰

Pipeline(流水线)机制能改善上面这类问题,它能将一组 Redis 命令进行组装,通过一次 RTT 传输给 Redis,再将这组 Redis 命令的执行结果按顺序返回给客户端

Pipeline 并不是什么新的技术或机制,很多技术上都使用过。
而且 RTT 在不同网络环境下会有不同,例如同机房和同机器会比较快,跨机房跨地区会比较慢。
Redis 命令真正执行的时间通常在微秒级别,所以才会有 Redis 性能瓶颈是网络这样的说法

redis-cli 的 --pipe 选项实际上就是使用 Pipeline 机制

大部分开发人员更倾向于使用高级语言客户端中的 Pipeline,目前大部分 Redis 客户端都支持 Pipeline

性能测试

  • Pipeline 执行速度一般比逐条执行要快
  • 客户端和服务端的网络延时越大,Pipeline 的效果越明显

原生批量命令与 Pipeline 对比

  • 原生批量命令是原子的,Pipeline 是非原子的
  • 原生批量命令是一个命令对应多个 key,Pipeline 支持多个命令。
  • 原生批量命令是 Redis 服务端支持实现的,而 Pipeline 需要服务端和客户端的共同实现

最佳实践

Pipeline 虽然好用,但是每次 Pipeline 组装的命令个数不能没有节制,否则一次组装 Pipeline 数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的 Pipeline 拆分成多次较小的 Pipeline 来完成

Pipeline 只能操作一个 Redis 实例,但是即使在分布式 Redis 场景中,也可以作为批量操作的重要优化手段

事务与Lua

为了保证多条命令组合的原子性,Redis 提供了简单的事务功能以及集成 Lua 脚本来解决这个问题。

事务

熟悉关系型数据库的开发者应该对事务比较了解,简单地说,事务表示一组动作,要么全部执行,要么全部不执行。
例如在你社交网站上用户 A 关注了用户 B,那么需要在用户 A 的关注表中加入用户 B,并且在用户 B 的粉丝表中添加用户 A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。

Redis 提供了简单的事务功能,将一组需要一起执行的命令放到 multi 和 exec 两个命令之间。
multi 命令代表事务开始,exec 命令代表事务结束,它们之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题

1
2
3
4
5
6
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED

可以看到 sadd 命令此时的返回结果是 QUEUED,代表命令并没有真正执行,而是暂时保存在 Redis 中。
如果此时另一个客户端执行 sismember user:a:follow user:b 返回结果应该为 0

只有当 exec 执行后,用户 A 关注用户 B 的行为才算完成,如下所示返回的两个结果对应 sadd 命令

1
2
3
4
5
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1

如果要停止事务的执行,可以使用 discard 命令代替 exec 命令即可

命令错误

例如下面操作错将 set 写成了 sett,属于语法错误,会造成整个事务无法执行,key 和 counter 值为发生变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
127.0.0.1:6379> mget key counter
1) "hello"
2) "100"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sett key world
(error) ERR unknown command `sett`, with args beginning with: `key`, `world`,
127.0.0.1:6379> incr counter
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> mget key counter
1) "hello"
2) "100"

运行时错误

例如用户 B 在添加粉丝列表时,误把 sadd 命令写成了 zadd 命令,这种就是运行时命令,因为语法是正确的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> zadd user:b:fans 1 user:a
QUEUED
127.0.0.1:6379> exec
1) (integer) 0
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1

可以看到 Redis 并不支持回滚功能,dadd user:a:follow user:b 命令已经执行成功,开发人员需要自己修复这类问题

Lua 用法简述

Lua 语言是在 1993 年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由 C 语言实现的,虽然简单小巧但是功能强大,所以许多应用都选它作为脚本语言,
尤其是在游戏领域,例如大名鼎鼎的暴雪公司将 Lua 语言引入到 "魔兽世界" 这款游戏中,Rovio 公司将 Lua 语言作为 "愤怒的小鸟" 这款火爆游戏的关卡升级引擎,Web 服务器 Nginx 将 Lua 语法作为扩展,增强自身功能。
Redis 将 Lua 作为脚本语言可帮助开发者定制自己的 Redis 命令,在这之前,必须修改源码。
在介绍如何在 Redis 中使用 Lua 脚本之前,有必要对 Lua 语言的使用做一个基本的介绍

数据类型及其逻辑处理

Lua 语言提供了如下几种数据类型: booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单。
下面将结合例子对 Lua 的基本数据类型和逻辑处理进行说明

(1) 字符串

下面定义一个字符串类型的数据:

1
local strings val = "world"

其中,local 代表 val 是一个局部变量,如果没有 local 代笔是全局变量。
print 函数可以打印出变量的值,例如下面代码将打印 world,其中 "--" 是 Lua 语言的注释

1
2
-- 结果是 "world"
print(hello)

(2) 数组

在 Lua 中,如果要使用类似数组的功能,可以用 tables 类型,下面代码使用定义了一个 tables 类型的变量 myArray,但和大多数编程语言不同的是,Lua 的数组下标从 1 开始计算:

1
2
3
local tables myArray = {"redis", "jedis", true, 88.0}
--true
print(myArray[3])

如果想遍历这个数组,可以使用 for 和 while,这些关键字和许多编程语言是一致的

(a) for

下面代码会计算 1 到 100 的和,关键字 for 以 end 作为结束符

1
2
3
4
5
6
7
local int sum = 0
for i = 1, 100
do
    sum = sum + i
end
-- 输出结果为 5050
print(sum)

要遍历 myArray,首先需要知道 tables 的长度,只需要在变量前加一个 # 号即可:

1
2
3
4
for i = 1, #myArray
do
    print(myArray[i])
end

除此之外,Lua 还提供了内置函数 ipairs,使用 for index, value ipairs(tables) 可以遍历出所有的索引下标和值:

1
2
3
4
5
for index,value in ipairs(myArray)
do
    print(index)
    print(value)
end

(b) while

下面代码同样会计算 1 到 100 的和,只不过使用的是 while 循环,while 循环同样以 end 作为结束符

1
2
3
4
5
6
7
8
9
local int sum = 0
local int i = 0
while i <= 100
do
    sum = sum + i
    i = i + 1
end
-- 输出结果为 5050
print(sum)

(c) if else

要确定数组中是否包含了 jedis,有则打印 true,注意 if 以 end 结尾,if 后紧跟 then:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
local tables myArray = {"redis", "jedis", true, 88.0}
for i = 1, #myArray
do
    if myArray[i] == "jedis"
    then
        print("true")
        break
    else
        --do nothing
    end
end

(3) 哈希

如果要使用类似哈希的功能,同样可以使用 tables 类型,例如下面代码定义了一个 tables,每个元素包含了 key 和 value,其中 strings1..string2 是将两个字符串进行连接

1
2
3
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_a age is"..user_1["age"])

如果要遍历 user_1,可以使用 Lua 的内置函数 pairs:

1
2
3
4
for key, value in pairs(user_1)
do 
    print(key..value)
end

函数定义

在 Lua 中,函数以 function 开头,以 end 结尾,funcName 是函数名,中间部分是函数体:

1
2
3
function funcName()
...
end

contact 函数将两个字符串拼接:

1
2
3
4
5
function contact(str1, str2)
    return str1..str2
end
-- "hello world"
print(contact("hello", "world"))

Redis 与 Lua

在 Redis 中使用 Lua

在 Redis 中执行 Lua 脚本有两种方法: eval 和 evalsha

(1) eval

eval 脚本内容 key个数 key列表 参数列表

下面例子使用了 key 列表和参数列表来为 Lua 脚本提供更多的灵活性

1
2
127.0.0.1:6379> eval 'return "hello "..KEYS[1]..ARGV[1]' 1 redis world
"hello redisworld"

此时 KEYS[1]="redis"ARGV[1]="world",蓑衣最终的返回结果是 "hello redisworld"

如果 Lua 脚本较长,还可以使用 redis-cli --eval 直接执行文件

eval 命令和 --eval 参数本质是一样的,客户端如果想执行 Lua 脚本,首先在客户端写好 Lua 脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端

(2) evalsha

除了使用 eval,Redis 还提供了 evalsha 命令来执行 Lua 脚本。
首先要将 Lua 脚本加载到 Redis 服务端,得到该脚本的 SHA1 校验和,evalsha 命令使用 SHA1 作为参数可以直接执行对应 Lua 脚本,避免每次发送 Lua 脚本的开销。
这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用

Lua 的 Redis API

Lua 可以使用 redis.call 函数实现对 Redis 的访问,例如下面代码是 Lua 使用 redis.call 调用了 Redis 的 set 和 get 操作:

1
2
redis.call("set", "hello", "world")
redis.call("get", "hello")

放在 Redis 的执行效果如下:

1
2
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"

除此之外 Lua 还可以使用 redis.pcall 函数实现对 Redis 的调用,redis.call 和 redis.pcall 的不同在于,如果 redis.call 执行失败,那么脚本执行结束会直接返回错误,而 redis.pcall 会忽略错误继续执行脚本,所以在实际开发中要根据具体的应用场景进行函数的选择

开发提示

Lua 可以使用 redis.log 函数将 Lua 脚本的日志输出到 Redis 的日志文件中,但是一定要控制日志级别

案例

Lua 脚本功能为 Redis 开发和运维人员带来如下三个好处:

  • Lua 脚本在 Redis 中是原子执行的,执行过程中不会插入其他命令。
  • Lua 脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在 Redis 内存中,实现复用的效果
  • Lua 脚本可以将多余命令一次性打包,有效地减少网络开销

下面以一个例子说明 Lua 脚本的使用,当前列表记录着热门用户的 id,假设这个列表有 5 个元素,如下所示:

1
2
3
4
5
6
127.0.0.1:6379> lrange hot:user:list 0 -1
1) "user:1:ratio"
2) "user:8:ratio"
3) "user:3:ratio"
4) "user:99:ratio"
5) "user:72:ratio"

user:{id}:ratio 代表用户的热度,它本身又是一个字符串类型的键

1
2
3
4
5
6
127.0.0.1:6379> mget user:1:ratio  user:8:ratio  user:3:ratio  user:99:ratio  user:72:ratio
1) "986"
2) "762"
3) "556"
4) "400"
5) "101"

现要求将列表内所有的键对应热度做加 1 操作,并且保证是原子执行,此功能可以利用 Lua 脚本来实现

1) 将列表中所有元素取出,赋值给你 mylist:

1
local mylist = redis.call("lrange", KEYS[1], 0, -1)

2) 定义局部变量 count=0,这个 count 就是最后 incr 的总次数

1
local count = 0

3) 遍历 mylist 中所有元素,每次做完 count 自增,最后返回 count:

1
2
3
4
5
6
for index, key in ipairs(mylist)
do
    redis.call("incr", key)
    count = count + 1
end
return count

将上述脚本写入 lrange_and_mincr.lua 文件,并执行如下操作,返回结果为 5

1
2
3
4
5
6
7
8
local mylist = redis.call("lrange", KEYS[1], 0, -1)
local count = 0
for index, key in ipairs(mylist)
do
    redis.call("incr", key)
    count = count + 1
end
return count
1
2
redis-cli --eval lrange_and_mincr.lua hot:user:list
(integer) 5

执行后所有用户的热度自增 1:

1
2
3
4
5
6
127.0.0.1:6379> mget user:1:ratio  user:8:ratio  user:3:ratio  user:99:ratio  user:72:ratio
1) "987"
2) "763"
3) "557"
4) "401"
5) "102"

Redis 如何管理 Lua 脚本

Redis 提供了 4 个命令实现对 Lua 脚本的管理,下面分别介绍

(1) script load

script load script

次命令用于将 Lua 脚本加载到 Redis 内存中

(2) script exists

script exists sha1 [sha1 ...]

此命令用于判断 sha1 是否已经加载到 Redis 内存中:

1
2
127.0.0.1:6379> script exists 23333
1) (integer) 0

返回结果代表 sha1[sha1 ...] 被加载到 Redis 内存的个数

(3) script flush

script flush

此命令用于清除 Redis 内存已经加载的所有 Lua 脚本

(4) script kill

script kill

此命令用于杀掉正在执行的 Lua 脚本。
如果 Lua 脚本比较耗时,甚至 Lua 脚本存在问题,那么此时 Lua 脚本的执行会阻塞 Redis,直到脚本执行完毕或者外部进行干预将其结束。