简介
Redis 从 2.6 版本开始引入使用 Lua 编程语言进行的服务器端脚本编程功能,这个功能可以让用户直接在 Redis 内部执行各种操作,从而达到简化代码并提高性能的作用。 P248
在不编写 C 代码的情况下添加新功能 P248
通过使用 Lua 对 Redis 进行脚本编程,我们可以避免一些减慢开发速度或者导致性能下降对常见陷阱。 P248
将 Lua 脚本载入 Redis P249
-
SCRIPT LOAD
命令可以将脚本载入 Redis ,这个命令接受一个字符串格式的 Lua 脚本为参数,它会把脚本存储起来等待之后使用,然后返回被存储脚本的 SHA1 校验和 -
EVALSHA
命令可以调用之前存储的脚本,这个命令接收脚本的 SHA1 校验和以及脚本所需的全部参数 -
EVAL
命令可以直接执行指定的脚本,这个命令接收脚本字符串以及脚本所需的全部参数。这个命令除了会执行脚本之外,还会将被执行的脚本缓存到 Redis 服务器里面
由于 Lua 的数据传入和传出限制, Lua 与 Redis 需要进行相互转换。因为脚本在返回各种不同类型的数据时可能会产生含糊不清的结果,所以我们应该尽量显式的返回字符串。 P250
我们可以在 官方文档 中找到 Redis 和 Lua 不同类型之间的转换表:
Redis 类型转换至 Lua 类型
Redis | Lua |
---|---|
integer reply | number |
bulk reply | string |
multi bulk reply | table (may have other Redis data types nested) |
status reply | table with a single ok field containing the status |
error reply | table with a single err field containing the error |
Nil bulk reply | false boolean type |
Nil multi bulk reply | false boolean type |
Lua 类型转换至 Redis 类型
Lua | Redis |
---|---|
number | integer reply (the number is converted into an integer) |
string | bulk reply |
table (array) | multi bulk reply (truncated to the first nil inside the Lua array if any) |
table with a single ok field | status reply |
table with a single err field | error reply |
boolean false | Nil bulk reply |
boolean true | integer reply with value of 1 |
创建新的状态消息 P251
- Lua 脚本跟单个 Redis 命令以及
MULTI
/EXEC
事务一样,都是原子操作 - 已经对结构进行了修改的 Lua 脚本将无法被中断
- 不执行任何写命令对只读脚本:可以在脚本对运行时间超过
lua-time-limit
选项指定的时间之后,执行SCRIPT KILL
命令杀死正在运行对脚本 - 有写命令的脚本:杀死脚本将导致 Redis 存储的数据进入一种不一致的状态。在这种情况下
- 不执行任何写命令对只读脚本:可以在脚本对运行时间超过
使用 Lua 重写锁和信号量 P254
如果我们事先不知道哪些键会被读取和写入,那么就应该使用 WATCH
/MULTI
/EXEC
事务或者锁,而不是 Lua 脚本。因此,在脚本里面对未被记录到 KEYS
参数中的键进行读取或者写入,可能会在程序迁移至 Redis 集群的时候出现不兼容或者故障。 P254
获取锁在目前已不需要使用 Lua 脚本实现,可以直接使用 SET
,并用 PX
和 NX
选项即可在键不存在的时候设置带过期时间的值。释放锁时为了保证释放的时自己获取的锁,需要使用 Lua 脚本实现。相关代码已在 实现自动补全、分布式锁和计数信号量 中实现。
移除 WATCH
/MULTI
/EXEC
事务 P258
一般来说,如果只有少数几个客户端尝试对被 WATCH
命令监视对数据进行修改,那么事务通常可以在不发生明显冲突或重试的情况下完成。但是,如果操作需要进行好几次通信往返,或者操作发生冲突的概率较高,又或者网络延迟较大,那么客户端可能需要重试很多次才能完成操作。 P258
使用 Lua 脚本替代事务不仅可以保证客户端尝试的执行都可以成功,还能降低通信开销,大幅提高 TPS 。同时由于 Lua 脚本没有多次通信往返,所以执行速度也会明显快于细粒度锁的版本。
Lua 脚本可以提供巨大的性能优势,并且能在一些情况下大幅地简化代码,但运行在 Redis 内部但 Lua 脚本只能访问位于 Lua 脚本之内或者 Redis 数据库之内的数据,而锁或者 WATCH
/MULTI
/EXEC
事务并没有这一限制。 P263
使用 Lua 对列表进行分片 P263
分片列表的构成 P263
为了能够对分片列表的两端执行推入操作和弹出操作,在构建分片列表时除了需要存储组成列表的各个分片之外,还需要记录列表第一个分片的 ID 以及最后一个分片的 ID 。当分片列表为空时,这两个字符串存储的分片 ID 将是相同的。 P263
组成分片列表的每个分片都会被命名为 <listname>:<shardid>
,并按照顺序进行分配。具体来说,如果程序总是从左端弹出元素,并从右端推入元素,那么最后一个分配的索引就会逐渐增大,并且新分片的 ID 也会变得越来越大。如果程序总是从右端弹出元素,并从左端推入元素,那么第一个分片的索引就会逐渐减少,并且新分片的 ID 也会变得越来越小。 P264
当分片列表包含多个列表时,位于分片两端的列表可能是被填满的,但位于两端之间的其他列表总是被填满的。 P264
将元素推入分片列表 P265
Lua 脚本根据命令 LPUSH
/RPUSH
找到列表的第一个分片或者最后一个分片,然后将元素推入分片对应的列表中,若分片已达个数上限(可以取配置中的 list-max-ziplist-entries
的值 - 1 作为上限),则会自动产生一个新的分片,继续推入,并更新第一个分片或者最后一个分片的分片 ID 。当推入操作执行完毕后,它会返回被推入元素的数量。 P265
从分片里面弹出元素 P266
Lua 脚本根据命令 LPOP
/RPOP
找到列表的第一个分片或者最后一个分片,然后在分片非空的情况下,从分片里面弹出一个元素,如果列表在执行弹出操作之后不再包含任何元素,那么程序就对记录着列表两端分片信息的字符串键进行修改(注意只有列表端分片为空时才修改对应的字符串键,而整个列表为空时,不做调整) P267
对分片列表执行阻塞弹出操作 P267
这一段书上讲得看不懂,也不知道为什么需要书中的花式操作才能完成。
个人觉得分片列表的阻塞弹出其实并不需要列表自身的阻塞弹出,我们可以不断执行上述 Lua 脚本实现的弹出元素的操作,若弹出成功,则直接返回,若弹出失败,则睡 1 ms 后继续执行弹出操作,直至弹出成功或者达到超时时间。这样我们对 Redis 对操作只在 Lua 脚本中,原子性保证了一定会弹出分片列表两端的元素。
本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action