@[TOC](第9章 Lua 脚本与排序)


前言

参考资料:《Redis设计与实现 第二版》;

第三部分为独立功能的实现,主要由以下模块组成:发布订阅事务Lua 脚本排序二进制位数组慢查询日志监视器

本篇将介绍 Redis 的Lua 脚本排序。Redis 从 2.6 版本开始可以使用一些命令操作 Lua 脚本,引入相关支持;Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,SORT 命令的一些可选性能让我们得到想要的排序;

与本章相关的 Redis 命令总结在下篇文章,欢迎点击收藏,本篇将不再重复:

《Redis常用命令及示例总结(API)》https://blog.51cto.com/dlhjw/4744855


1. Lua 脚本

  • Lua 脚本的执行的原子性的;

    1.1 Redis 创建并修改 Lua 环境的步骤

    1. 创建 Lua 环境:调用 Lua 的 C API 函数 lua_open;
    2. 载入函数库:包括基础库、表格库、字符串库、数学库、调试库、Lua CJSON 库、Struct 库、Lua cmsgpack 库等;
    3. 创建 redis 全局表格:表格包含对 Redis 进行操作的函数,如 redis.callredis.pcall 函数等;
    4. 使用 Redis 自制的随机函数替换 Lua 原有的随机函数math.randommath.randomseed 函数带有副作用,Redis 对其进行了替换,以避免在脚本中引入副作用;
    5. 创建排序辅助函数:有些命令在相同数据集上会产生不同结果,为了消除这些命令带来的不确定性,创建了 redis__compare_helper 排列辅助函数;
    6. 创建 redis.pcall 函数的错误报告辅助函数:创建 __redis__err__handler 错误处理函数,提供更详细的出错信息;
    7. 保护 Lua 全局环境:防止用户在执行脚本时将额外的全局变量添加到 Lua 环境中,但可以修改已存在的全局变量;
    8. 将 Lua 环境保存到服务器状态的 lua 属性里:然后等待服务器传来 lua 脚本;

保存 lua 环境

1.2 Lua 环境协作组件

  • Redis 服务器创建两个与 Lua 环境进行协作的组件;
  • 伪客户端
    伪客户端实现命令通信

  • lua_scripts 字典

    struct redisServer{
      //...
      //字典,键为某个 Lua 脚本的 SHA1 校验和,值为 SHA1 校验和对应的 Lua 脚本                                                                                                                                                                                                                                                                    
      dict *lua_scripts;
    }
    • 所有被 EVALSCRIPT LOAD 命令执行过的脚本会被保存到 lua_scripts 字典里;

1.3 EVAL 命令的实现

  • 执行命令:EVAL "return 'hello world'" 0,实现步骤如下:

    1. 定义脚本函数:给函数命令为 f_SHA1的校验和,函数体为脚本本身return 'hello world'
    2. 将脚本保存到 lua_scripts 字典

添加键值对后的 lua_scripts 字典

  1. 执行脚本函数:先进行设置钩子(hook)、传入参数等准备工作,再执行函数,将执行结果保存到客户端状态的输出缓冲区,执行垃圾回收等操作;

1.4 EVALSHA 命令的实现

  • 每个被 EVAL 命令成功执行过的 Lua 脚本,在 Lua 环境中有一个与该脚本对应的 Lua 函数,函数名为 f_SHA1的校验和
  • 因此即使不知道脚本内容,也可以通过 EVALSHA SHA1校验和 命令调用对应函数;

1.5 脚本管理命令的实现

  • SCRIPT FLUSH
    • 用于清除服务器中所有和 Lua 脚本有关的信息;
    • 会释放并重建 lua_scripts 字典,关闭现有 Lua 环境并重建一个新的 Lua 环境;
  • SCRIPT EXISTS
    • 根通过检查给定校验和 SHA1 是否在的于 lua_scripts 字典的键,判断对应的脚本是否存在服务器中;
  • SCRIPT LOAD
    • 所做工作与 EVAL 命令前两步一样:定义脚本函数,将脚本函数保存进 lua_scripts 字典;
  • SCRIPT KILL
    • 用于处理钩子(hook)超时的情况;
    • 如果脚本未执行写入操作,客户端可以使用该命令停止执行脚本;
    • 如果脚本已经进行过写入操作,客户端只能使用 SHUTDOWN nosave 命令来停止服务器,防止不合法的数据被写入到数据库中;

SCRIPT KILL命令的钩子情况

1.6 脚本复制

  • 当服务器运行在复制模式下时,具有写性质的脚本命令会被复制到从服务器里,如:EVALEVALSHASCRIPT FLUSHSCRIPTLOAD

1.6.1 EVAL、SCRIPT FLUSH、SCRIPTLOAD 命令的复制

  • 当主服务器执行完 EVALSCRIPT FLUSHSCRIPTLOAD 命令时,会直接将被执行的命令传播给所有从服务器;
    将从服务器传播给从服务器

    1.6.2 EVALSHA 命令的复制

  • EVALSHA 命令接受一个校验和 SHA1 作为参数传入;
  • 每个服服务器间的脚本校验和 SHA1 可能不同,导致在不同的服务器间执行 EVALSHA 命令情况可能也不同;
  • Redis 复制 EVALSHA 命令的方法:

    • 判断传播 EVALSHA 命令是否安全的方法:主服务器使用 repl_scriptcache_dict 字典记录哪些脚本传播给了所有从服务器。跟 lua_scripts 字典对比即可得知哪些校验和还未传播;
    struct redisServer{
        //...
        //字典,键是校验和 SHA1,值是 NULL
        dict *repl_scriptcache_dict;
    };
    • 清空 repl_scriptcache_dict 字典:每当主服务器添加一个新的从服务器时,主服务器会清空自己的 repl_scriptcache_dict 字典;
    • EVALSHA 命令转换成 EVAL 命令的方法:根据 sha1 校验和在 lua_scripts 字典中查找对应 Lua脚本 script;然后改写命令,用 script 替换 sha1;其他参数不变;
    • EVAL命令EVAL script numkeys key [key ...] arg [arg ...]
    • EVALSHA命令EVALSHA sha1 numkeys key [key ...] arg [arg ...]
    • 传播 EVALSHA 命令的方法:当主服务器成功执行完一个 EVALSHA 命令后,会根据该命令指定的 SHA1 校验和是否存在于 repl_scriptcache_dict 字典里来决定是否向从服务器传播 EVALSHA 命令还是 EVAL 命令;

传播 EVALSHA 命令的方法

2. 排序

2.1 SORT \<key> 命令的实现

  • 服务器执行 SORT numbers 命令的详细步骤:
  • 1)创建一个和 numbers 列表长度相同的数组,数组的每个项都是 redis.h/redisSortObject 结构;
  • 2)遍历数组,将各个数组项的 obj 指针指向 numbers 列表的各个项,构成 obj 指针和列表项的一对一关系;

obj 指针指向各个项

  • 3)遍历数组,将各个 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存到相应数组项 u.score 属性里;

设置数组的 u.score 属性

  • 4)根据数组项 u.score 属性的值,对数组进行数字值排序;

排序后的数组

  • 5)遍历数组,将各个数组项的 obj 指针所指向的列表项作为排序结果返回给客户端;

2.2 SORT 命令的可选项

  • ALPHA 选项可以对包含字符串的键进行排列;
  • ASC 选项执行升序排序;
  • DESC 选项执行降序排序;
    • 升序和降序都采用快速排序算法;
  • BY 选项可以指定某些字符串键,或者某个哈希键说包含的某些域来作为权重,对一个键排序;
  • LIMIT 选项可以让结果只返回其中部分已排序的元素;
  • GET 选项可以在键被排序后,根据被排序的元素,以及 GET 选项指定的模式,查找并返回某些键的值;
  • STORE选项可以将排序结果保存在指定的键里面;

2.3 多个选项的执行顺序

  • 选项的执行顺序:
    • 1)排序:会先使用 ALPHAASCDESCBY 这几个选项,对输入键进行排序,并得到一个排序结果集;
    • 2)限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制;
    • 3)获取外部键:使用 GET 选项,根据结果集的元素,以及 GET 指定的模式,查找并获取指定键的值,并用这些值构成新的结果集;
    • 4)保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上;
    • 5)向客户端返回排序结果集:遍历排序结果集,并依次向客户端返回排序结果集中的元素;
  • 除了 GET 选项之外,改变选项的摆放顺序不会影响 SORT 命令执行这些选项的顺序;

最后

::: hljs-center

新人制作,如有错误,欢迎指出,感激不尽!

:::

::: hljs-center

欢迎关注公众号,会分享一些更日常的东西!

:::

::: hljs-center

如需转载,请标注出处!

:::

::: hljs-center

公众号二维码

:::