天天看点

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

文章目录

  • 一、Lua脚本
    • 1、创建并修改Lua环境
    • 2、Lua环境协作组件
      • 2.1 伪客户端
      • 2.2 lua_scripts字典
    • 3、EVAL命令的实现
      • 3.1 定义脚本函数
      • 3.2 将脚本保存到lua_scripts字典
      • 3.3 执行脚本函数
    • 4、EVALSHA命令的实现
    • 5、脚本管理命令的实现
      • 5.1 SCRIPT FLUSH
      • 5.2 SCRIPT EXISTS
      • 5.3 SCRIPT LOAD
      • 5.4 SCRIPT KILL
    • 6、脚本复制
      • 6.1 复制EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令
      • 6.2 复制EVALSHA命令
    • 7、重点回顾
  • 二、排序
    • 1、SORT\命令的实现
    • 2、ALPHA选项的实现
    • 3、ASC选项和DESC选项的实现
    • 4、BY选项的实现
    • 5、带有ALPHA选项的BY选项的实现
    • 6、LIMIT选项的实现
    • 7、GET选项的实现
    • 8、STORE选项的实现
    • 9、多个选项的执行顺序
      • 9.1 选项的执行顺序
    • 9.2 选项的摆放顺序
    • 10、重点回顾

一、Lua脚本

通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。

其中,使用EVAL命令可以直接对输入的脚本进行求值:

redis> EVAL "return 'hello world'" 0
"hello world"
           

而使用EVALSHA命令则可以根据脚本的SHA1校验和来对脚本进行求值,但这个命令要求校验和对应的脚本必须至少被EVAL命令执行过一次:

redis> EVAL "return 1+1" 0
(integer) 2
redis> EVALSHA "a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9" 0 // 上一个脚本的校验和
integer) 2 
           

1、创建并修改Lua环境

为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境(environ-ment),并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足Redis服务器的需要。

Redis服务器创建并修改Lua环境的整个过程由以下步骤组成:

  • 1)创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的。
  • 2)载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数据操作。
  • 3)创建全局表格redis,这个表格包含了对Redis进行操作的函数,比如用于在Lua脚本中执行Redis命令的redis.call函数。
  • 4)使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用。
  • 5)创建排序辅助函数,Lua环境使用这个辅佐函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性。
  • 6)创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息。
  • 7)对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到Lua环境中。
  • 8)将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的Lua脚本。
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

因为Redis使用串行化的方式来执行Redis命令,所以在任何特定时间里,最多都只会有一个脚本能够被放进Lua环境里面运行,因此,整个Redis服务器只需要创建一个Lua环境即可。

2、Lua环境协作组件

除了创建并修改Lua环境之外,Redis服务器还创建了两个用于与Lua环境进行协作的组件,它们分别是负责执行Lua脚本中的Redis命令的伪客户端,以及用于保存Lua脚本的lua_scripts字典。

2.1 伪客户端

因为执行Redis命令必须有相应的客户端状态,所以为了执行Lua脚本中包含的Redis命令,Redis服务器专门为Lua环境创建了一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有Redis命令。

Lua脚本使用redis.call函数或者redis.pcall函数执行一个Redis命令,需要完成以下步骤:

  • 1)Lua环境将redis.call函数或者redis.pcall函数想要执行的命令传给伪客户端。
  • 2)伪客户端将脚本想要执行的命令传给命令执行器。
  • 3)命令执行器执行伪客户端传给它的命令,并将命令的执行结果返回给伪客户端。
  • 4)伪客户端接收命令执行器返回的命令结果,并将这个命令结果返回给Lua环境。
  • 5)Lua环境在接收到命令结果之后,将该结果返回给redis.call函数或者redis.pcall函数。
  • 6)接收到结果的redis.call函数或者redis.pcall函数会将命令结果作为函数返回值返回给脚本中的调用者。
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
redis> EVAL "return redis.call('DBSIZE')" 0
(integer) 10086
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

2.2 lua_scripts字典

除了伪客户端之外,Redis服务器为Lua环境创建的另一个协作组件是lua_scripts字典,这个字典的键为某个Lua脚本的SHA1校验和(checksum),而字典的值则是SHA1校验和对应的Lua脚本:

struct redisServer {
    // ...
    dict *lua_scripts;
    // ...
};
           

Redis服务器会将所有被EVAL命令执行过的Lua脚本,以及所有被SCRIPT LOAD命令载入过的Lua脚本都保存到lua_scripts字典里面。

redis> SCRIPT LOAD "return 'hi'"
"2f31ba2bb6d6a0f42cc159d2e2dad55440778de3"
redis> SCRIPT LOAD "return 1+1"
"a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9"
redis> SCRIPT LOAD "return 2*2"
"4475bfb5919b5ad16424cb50f74d4724ae833e72"
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

lua_scripts字典有两个作用,一个是实现SCRIPT EXISTS命令,另一个是实现脚本复制功能。

3、EVAL命令的实现

EVAL命令的执行过程可以分为以下三个步骤:

  • 1)根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
  • 2)将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用。
  • 3)执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。

3.1 定义脚本函数

当客户端向服务器发送EVAL命令,要求执行某个Lua脚本的时候,服务器首先要做的就是在Lua环境中,为传入的脚本定义一个与这个脚本相对应的Lua函数,其中,Lua函数的名字由f_前缀加上脚本的SHA1校验和(四十个字符长)组成,而函数的体(body)则是脚本本身。

EVAL "return 'hello world'" 0
           

服务器将在Lua环境中定义以下函数:

function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
    return 'hello world'
end
           

因为客户端传入的脚本为return’hello world’,而这个脚本的SHA1校验和为5332031c6b470dc5a0dd9b4bf2030dea6d65de91,所以函数的名字为f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91,而函数的体则为return’hello world’。

使用函数来保存客户端传入的脚本有以下好处:

  • 执行脚本的步骤非常简单,只要调用与脚本相对应的函数即可。
  • 通过函数的局部性来让Lua环境保持清洁,减少了垃圾回收的工作量,并且避免了使用全局变量。
  • 如果某个脚本所对应的函数在Lua环境中被定义过至少一次,那么只要记得这个脚本的SHA1校验和,服务器就可以在不知道脚本本身的情况下,直接通过调用Lua函数来执行脚本。

3.2 将脚本保存到lua_scripts字典

EVAL命令要做的第二件事是将客户端传入的脚本保存到服务器的lua_scripts字典里面。

EVAL "return 'hello world'" 0
           

服务器将在lua_scripts字典中新添加一个键值对,其中键为Lua脚本的SHA1校验和:

5332031c6b470dc5a0dd9b4bf2030dea6d65de91
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

3.3 执行脚本函数

在为脚本定义函数,并且将脚本保存到lua_scripts字典之后,服务器还需要进行一些设置钩子、传入参数之类的准备动作,才能正式开始执行脚本。

整个准备和执行脚本的过程如下:

  • 1)将EVAL命令中传入的键名(key name)参数和脚本参数分别保存到KEYS数组和ARGV数组,然后将这两个数组作为全局变量传入到Lua环境里面。
  • 2)为Lua环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过SCRIPT KILL命令停止脚本,或者通过SHUTDOWN命令直接关闭服务器。
  • 3)执行脚本函数。
  • 4)移除之前装载的超时钩子。
  • 5)将执行脚本函数所得的结果保存到客户端状态的输出缓冲区里面,等待服务器将结果返回给客户端。
  • 6)对Lua环境执行垃圾回收操作。

4、EVALSHA命令的实现

只要脚本对应的函数曾经在Lua环境里面定义过,那么即使不知道脚本的内容本身,客户端也可以根据脚本的SHA1校验和来调用脚本对应的函数,从而达到执行脚本的目的,这就是EVALSHA命令的实现原理。

def EVALSHA(sha1):
    # 拼接出函数的名字
    # 例如:f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91 
    func_name = "f_" + sha1
    # 查看这个函数在Lua环境中是否存在
    if function_exists_in_lua_env(func_name):
        # 如果函数存在,那么执行它
        execute_lua_function(func_name)
    else:
        # 如果函数不存在,那么返回一个错误
        send_script_error("SCRIPT NOT FOUND")
           

当客户端执行以下EVALSHA命令时:

redis> EVALSHA "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"
           

服务器首先根据客户端输入的SHA1校验和,检查函数f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91是否存在于Lua环境中,得到的回应是该函数确实存在,于是服务器执行Lua环境中的f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91函数,并将结果"hello world"返回给客户端。

5、脚本管理命令的实现

除了EVAL命令和EVALSHA命令之外,Redis中与Lua脚本有关的命令还有四个,它们分别是SCRIPT FLUSH命令、SCRIPT EXISTS命令、SCRIPT LOAD命令、以及SCRIPT KILL命令。

5.1 SCRIPT FLUSH

SCRIPT FLUSH命令用于清除服务器中所有和Lua脚本有关的信息,这个命令会释放并重建lua_scripts字典,关闭现有的Lua环境并重新创建一个新的Lua环境。

def SCRIPT_FLUSH():
    # 释放脚本字典
    dictRelease(server.lua_scripts)
    # 重建脚本字典
    server.lua_scripts = dictCreate(...)
    # 关闭Lua环境
    lua_close(server.lua)
    # 初始化一个新的Lua环境
    server.lua = init_lua_env()
           

5.2 SCRIPT EXISTS

SCRIPT EXISTS命令根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器中。

SCRIPT EXISTS命令是通过检查给定的校验和是否存在于lua_scripts字典来实现的,以下是该命令的实现伪代码:

def SCRIPT_EXISTS(*sha1_list):
    # 结果列表
    result_list = []
    # 遍历输入的所有SHA1校验和
    for sha1 in sha1_list:
        # 检查校验和是否为lua_scripts字典的键
        # 如果是的话,那么表示校验和对应的脚本存在
        # 否则的话,脚本就不存在
        if sha1 in server.lua_scripts:
            # 存在用1表示
            result_list.append(1)
        else:
            # 不存在用0表示
            result_list.append(0)
    # 向客户端返回结果列表
    send_list_reply(result_list)
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

5.3 SCRIPT LOAD

SCRIPT LOAD命令所做的事情和EVAL命令执行脚本时所做的前两步完全一样:命令首先在Lua环境中为脚本创建相对应的函数,然后再将脚本保存到lua_scripts字典里面。

redis> SCRIPT LOAD "return 'hi'"
"2f31ba2bb6d6a0f42cc159d2e2dad55440778de3"
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

5.4 SCRIPT KILL

如果服务器设置了lua-time-limit配置选项,那么在每次执行Lua脚本之前,服务器都会在Lua环境里面设置一个超时处理钩子(hook)。

超时处理钩子在脚本运行期间,会定期检查脚本已经运行了多长时间,一旦钩子发现脚本的运行时间已经超过了lua-time-limit选项设置的时长,钩子将定期在脚本运行的间隙中,查看是否有SCRIPT KILL命令或者SHUTDOWN命令到达服务器。

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

6、脚本复制

与其他普通Redis命令一样,当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括EVAL命令、EVALSHA命令、SCRIPT FLUSH命令,以及SCRIPT LOAD命令。

6.1 复制EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令

Redis复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制其他普通Redis命令的方法一样,当主服务器执行完以上三个命令的其中一个时,主服务器会直接将被执行的命令传播(propagate)给所有从服务器。

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

6.2 复制EVALSHA命令

EVALSHA命令是所有与Lua脚本有关的命令中,复制操作最复杂的一个,因为主服务器与从服务器载入Lua脚本的情况可能有所不同,所以主服务器不能像复制EVAL命令、SCRIPT LOAD命令或者SCRIPT FLUSH命令那样,直接将EVALSHA命令传播给从服务器。对于一个在主服务器被成功执行的EVALSHA命令来说,相同的EVALSHA命令在从服务器执行时却可能会出现脚本未找到(not found)错误。

因为多个从服务器之间载入Lua脚本的情况也可能各有不同,所以即使一个EVALSHA命令可以在某个从服务器成功执行,也不代表这个EVALSHA命令就一定可以在另一个从服务器成功执行。

假设有主服务器master和从服务器slave1,并且slave1一直复制着master,所以master载入的所有Lua脚本,slave1也有载入(通过传播EVAL命令或者SCRIPT LOAD命令来实现)。如果客户端向master发送命令:

master> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
           

那么这个命令也会被传播到slave1上面,所以master和slave1都会成功载入SHA1校验和为5332031c6b470dc5a0dd9b4bf2030dea6d65de91的Lua脚本。

如果这时,一个新的从服务器slave2开始复制主服务器master,如果master不想办法将脚本:

"return 'hello world'"
           

传送给slave2的话,那么当客户端向主服务器发送命令:

master> EVALSHA "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"
           

master和slave1都将成功执行这个EVALSHA命令,而slave2却会发生脚本未找到错误。

为了防止以上假设的情况出现,Redis要求主服务器在传播EVALSHA命令的时候,必须确保EVALSHA命令要执行的脚本已经被所有从服务器载入过,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成一个等价的EVAL命令,然后通过传播EVAL命令来代替EVALSHA命令。

1.判断传播EVALSHA命令是否安全的方法

主服务器使用服务器状态的repl_scriptcache_dict字典记录自己已经将哪些脚本传播给了所有从服务器:

struct redisServer {
    // ...
    dict *repl_scriptcache_dict;
    // ...
};
           

repl_scriptcache_dict字典的键是一个个Lua脚本的SHA1校验和,而字典的值则全部都是NULL,当一个校验和出现在repl_scriptcache_dict字典时,说明这个校验和对应的Lua脚本已经传播给了所有从服务器,主服务器可以直接向从服务器传播包含这个SHA1校验和的EVALSHA命令,而不必担心从服务器会出现脚本未找到错误。

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

那么主服务器可以向从服务器传播以下三个EVALSHA命令,并且从服务器在执行这些EVALSHA命令的时候不会出现脚本未找到错误:

EVALSHA "2f31ba2bb6d6a0f42cc159d2e2dad55440778de3" ...
EVALSHA "a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9" ...
EVALSHA "4475bfb5919b5ad16424cb50f74d4724ae833e72" ...
           

另一方面,如果一个脚本的SHA1校验和存在于lua_scripts字典,但是却不存在于repl_scriptcache_dict字典,那么说明校验和对应的Lua脚本已经被主服务器载入,但是并没有传播给所有从服务器,如果我们尝试向从服务器传播包含这个SHA1校验和的EVALSHA命令,那么至少有一个从服务器会出现脚本未找到错误。

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

2.清空repl_scriptcache_dict字典

每当主服务器添加一个新的从服务器时,主服务器都会清空自己的repl_scriptcache_dict字典,这是因为随着新从服务器的出现,repl_scriptcache_dict字典里面记录的脚本已经不再被所有从服务器载入过,所以主服务器会清空repl_scriptcache_dict字典,强制自己重新向所有从服务器传播脚本,从而确保新的从服务器不会出现脚本未找到错误。

3.EVALSHA命令转换成EVAL命令的方法

通过使用EVALSHA命令指定的SHA1校验和,以及lua_scripts字典保存的Lua脚本,服务器总可以将一个EVALSHA命令:

EVALSHA <sha1> <numkeys> [key ...] [arg ...]
           

转换成一个等价的EVAL命令:

EVAL <script> <numkeys> [key ...] [arg ...]
           

具体的转换方法如下:

  • 1)根据SHA1校验和sha1,在lua_scripts字典中查找sha1对应的Lua脚本script。
  • 2)将原来的EVALSHA命令请求改写成EVAL命令请求,并且将校验和sha1改成脚本script,至于numkeys、key、arg等参数则保持不变。

如果一个SHA1值所对应的Lua脚本没有被所有从服务器载入过,那么主服务器可以将EVALSHA命令转换成等价的EVAL命令,然后通过传播等价的EVAL命令来代替原本想要传播的EVALSHA命令,以此来产生相同的脚本执行效果,并确保所有从服务器都不会出现脚本未找到错误。

另外,因为主服务器在传播完EVAL命令之后,会将被传播脚本的SHA1校验和(也即是原本EVALSHA命令指定的那个校验和)添加到repl_scriptcache_dict字典里面,如果之后EVALSHA命令再次指定这个SHA1校验和,主服务器就可以直接传播EVALSHA命令,而不必再次对EVALSHA命令进行转换。

4.传播EVALSHA命令的方法

当主服务器成功在本机执行完一个EVALSHA命令之后,它将根据EVALSHA命令指定的SHA1校验和是否存在于repl_scriptcache_dict字典来决定是向从服务器传播EVALSHA命令还是EVAL命令:

  • 1)如果EVALSHA命令指定的SHA1校验和存在于repl_scriptcache_dict字典,那么主服务器直接向从服务器传播EVALSHA命令。
  • 2)如果EVALSHA命令指定的SHA1校验和不存在于repl_scriptcache_dict字典,那么主服务器会将EVALSHA命令转换成等价的EVAL命令,然后传播这个等价的EVAL命令,并将EVALSHA命令指定的SHA1校验和添加到repl_scriptcache_dict字典里面。
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

7、重点回顾

  • Redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足Redis在功能性、安全性等方面的需要。
  • Redis服务器专门使用一个伪客户端来执行Lua脚本中包含的Redis命令。
  • Redis使用脚本字典来保存所有被EVAL命令执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
  • EVAL命令为客户端输入的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
  • EVALSHA命令通过直接调用Lua环境中已定义的函数来执行脚本。
  • SCRIPT FLUSH命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
  • SCRIPT EXISTS命令接受一个或多个SHA1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
  • SCRIPT LOAD命令接受一个Lua脚本为参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
  • 服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送SCRIPT KILL命令来让钩子停止正在执行的脚本,或者发送SHUTDOWN nosave命令来让钩子关闭整个服务器。·
  • 主服务器复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制普通Redis命令一样,只要将相同的命令传播给从服务器就可以了。
  • 主服务器在复制EVALSHA命令时,必须确保所有从服务器都已经载入了EVALSHA命令指定的SHA1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果。

二、排序

Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。

redis> RPUSH numbers 5 3 1 4 2
(integer) 5
# 按插入顺序排列的列表元素
redis> LRANGE numbers 0 -1
1) "5"
2) "3"
3) "1"
4) "4"
5) "2"
# 按值从小到大有序排列的列表元素
redis> SORT numbers
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
           

使用ALPHA选项,对一个包含字符串值的集合键进行排序

redis> SADD alphabet a b c d e f g
(integer) 7
# 乱序排列的集合元素
redis> SMEMBERS alphabet
1) "d"
2) "a"
3) "f"
4) "e"
5) "b"
6) "g"
7) "c"
# 排序后的集合元素
redis> SORT alphabet ALPHA
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
           

使用SORT命令和BY选项,以jack_number、peter_number、tom_number三个键的值为权重(weight),对有序集合test-result中的"jack"、“peter”、"tom"三个成员(member)进行排序:

redis> ZADD test-result 3.0 jack 3.5 peter 4.0 tom
(integer) 3
# 按元素的分值排列
redis> ZRANGE test-result 0 -1
1) "jack"
2) "peter"
3) "tom"
# 为各个元素设置序号
redis> MSET peter_number 1 tom_number 2 jack_number 3
OK
# 以序号为权重,对有序集合中的元素进行排序
redis> SORT test-result BY *_number
1) "peter"
2) "tom"
3) "jack"
           

1、SORT<key>命令的实现

SORT命令的最简单执行形式为:

SORT <key>
           

这个命令可以对一个包含数字值的键key进行排序。

redis> RPUSH numbers 3 1 2
(integer) 3
redis> SORT numbers
1) "1"
2) "2"
3) "3"
           

服务器执行SORT numbers命令的详细步骤如下:

  • 1)创建一个和numbers列表长度相同的数组,该数组的每个项都是一个redis.h/redisSortObject结构
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 2)遍历数组,将各个数组项的obj指针分别指向numbers列表的各个项,构成obj指针和列表项之间的一对一关系
    Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 3)遍历数组,将各个obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的u.score属性里面
    Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 4)根据数组项u.score属性的值,对数组进行数字值排序,排序后的数组项按u.score属性的值从小到大排列
    Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 5)遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端,程序首先访问数组的索引0,返回u.score值为1.0的列表项"1";然后访问数组的索引1,返回u.score值为2.0的列表项"2";最后访问数组的索引2,返回u.score值为3.0的列表项"3"。
typedef struct _redisSortObject {
    // 被排序键的值
    robj *obj;
    // 权重
    union {
        // 排序数字值时使用
        double score;
        // 排序带有BY选项的字符串值时使用
        robj *cmpobj;
    } u;
} redisSortObject;
           

2、ALPHA选项的实现

通过使用ALPHA选项,SORT命令可以对包含字符串值的键进行排序:

SORT <key> ALPHA
           
redis> SADD fruits apple banana cherry
(integer) 3
# 元素在集合中是乱序存放的
redis> SMEMBERS fruits
1) "apple"
2) "cherry"
3) "banana"
# 
对fruits键进行字符串排序
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
           

服务器执行SORT fruits ALPHA命令的详细步骤如下:

  • 1)创建一个redisSortObject结构数组,数组的长度等于fruits集合的大小。
  • 2)遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素
    Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 3)根据obj指针所指向的集合元素,对数组进行字符串排序,排序后的数组项按集合元素的字符串值从小到大排列:因为"apple"、“banana”、“cherry"三个字符串的大小顺序为"apple”<“banana”<“cherry”,所以排序后数组的第一项指向"apple"元素,第二项指向"banana"元素,第三项指向"cherry"元素
    Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
  • 4)遍历数组,依次将数组项的obj指针所指向的元素返回给客户端。

3、ASC选项和DESC选项的实现

在默认情况下,SORT命令执行升序排序,排序后的结果按值的大小从小到大排列,以下两个命令是完全等价的:

SORT <key>
SORT <key> ASC
           

相反地,在执行SORT命令时使用DESC选项,可以让命令执行降序排序,让排序后的结果按值的大小从大到小排列:

SORT <key> DESC
           
redis> RPUSH numbers 3 1 2
(integer) 3
redis> SORT numbers
1) "1"
2) "2"
3) "3"
redis> SORT numbers ASC
1) "1"
2) "2"
3) "3"
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序
redis> SORT numbers DESC
1) "3"
2) "2"
3) "1"
           
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

4、BY选项的实现

在默认情况下,SORT命令使用被排序键包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置。

通过使用BY选项,SORT命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序。

redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
           
redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
           

服务器执行SORT fruits BY*-price命令的详细步骤如下:

1)创建一个redisSortObject结构数组,数组的长度等于fruits集合的大小。

2)遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素。

3)遍历数组,根据各个数组项的obj指针所指向的集合元素,以及BY选项所给定的模式*-price,查找相应的权重键:

  • 对于"apple"元素,查找程序返回权重键"apple-price"。
  • 对于"banana"元素,查找程序返回权重键"banana-price"。
  • 对于"cherry"元素,查找程序返回权重键"cherry-price"。
Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

4)将各个权重键的值转换成一个double类型的浮点数,然后保存在相应数组项的u.score属性里面

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

5)以数组项u.score属性的值为权重,对数组进行排序,得到一个按u.score属性的值从小到大排序的数组

Redis设计与实现读书笔记八、Lua脚本的实现原理一、Lua脚本二、排序

6)遍历数组,依次将数组项的obj指针所指向的集合元素返回给客户端。

5、带有ALPHA选项的BY选项的实现

BY选项默认假设权重键保存的值为数字值,如果权重键保存的是字符串值的话,那么就需要在使用BY选项的同时,配合使用ALPHA选项。

redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> MSET apple-id "FRUIT-25" banana-id "FRUIT-79" cherry-id "FRUIT-13"
OK
           
redis> SORT fruits BY *-id ALPHA
1)"cherry"
2)"apple"
3)"banana"
           

6、LIMIT选项的实现

在默认情况下,SORT命令总会将排序后的所有元素都返回给客户端:

redis> SADD alphabet a b c d e f
(integer) 6
# 集合中的元素是乱序存放的
redis> SMEMBERS alphabet
1) "d"
2) "c"
3) "a"
4) "b"
5) "f"
6) "e"
# 对集合进行排序,并返回所有排序后的元素
redis> SORT alphabet ALPHA
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
           

但是,通过LIMIT选项,我们可以让SORT命令只返回其中一部分已排序的元素。

LIMIT选项的格式为LIMIT<offset><count>:

  • offset参数表示要跳过的已排序元素数量。
  • count参数表示跳过给定数量的已排序元素之后,要返回的已排序元素数量。
redis> SORT alphabet ALPHA LIMIT 0 4
1) "a"
2) "b"
3) "c"
4) "d"
           

7、GET选项的实现

在默认情况下,SORT命令在对键进行排序之后,总是返回被排序键本身所包含的元素。

redis> SADD students "peter" "jack" "tom"
(integer) 3
redis> SORT students ALPHA
1) "jack"
2) "peter"
3) "tom"
           

但是,通过使用GET选项,我们可以让SORT命令在对键进行排序之后,根据被排序的元素,以及GET选项所指定的模式,查找并返回某些键的值。

# 设置peter、jack、tom的全名
redis> SET peter-name "Peter White"
OK
redis> SET jack-name "Jack Snow"
OK
redis> SET tom-name "Tom Smith"
OK
# SORT命令首先对students集合进行排序,得到排序结果
# 1) "jack"
# 2) "peter"
# 3) "tom"
# 然后根据这些结果,获取并返回键jack-name、peter-name和tom-name的值
redis> SORT students ALPHA GET *-name
1) "Jack Snow"
2) "Peter White"
3) "Tom Smith"
           

8、STORE选项的实现

在默认情况下,SORT命令只向客户端返回排序结果,而不保存排序结果:

redis> SADD students "peter" "jack" "tom"
(integer) 3
redis> SORT students ALPHA
1) "jack"
2) "peter"
3) "tom"
           

但是,通过使用STORE选项,我们可以将排序结果保存在指定的键里面,并在有需要时重用这个排序结果:

redis> SORT students ALPHA STORE sorted_students
(integer) 3
redis> LRANGE sorted_students 0-1
1) "jack"
2) "peter"
3) "tom"
           

9、多个选项的执行顺序

9.1 选项的执行顺序

如果按照选项来划分的话,一个SORT命令的执行过程可以分为以下四步:

  • 1)排序:在这一步,命令会使用ALPHA、ASC或DESC、BY这几个选项,对输入键进行排序,并得到一个排序结果集。
  • 2)限制排序结果集的长度:在这一步,命令会使用LIMIT选项,对排序结果集的长度进行限制,只有LIMIT选项指定的那部分元素会被保留在排序结果集中。
  • 3)获取外部键:在这一步,命令会使用GET选项,根据排序结果集中的元素,以及GET选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集。
  • 4)保存排序结果集:在这一步,命令会使用STORE选项,将排序结果集保存到指定的键上面去。
  • 5)向客户端返回排序结果集:在最后这一步,命令遍历排序结果集,并依次向客户端返回排序结果集中的元素。
SORT <key> ALPHA DESC BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
           

9.2 选项的摆放顺序

另外要提醒的一点是,调用SORT命令时,除了GET选项之外,改变选项的摆放顺序并不会影响SORT命令执行这些选项的顺序。

SORT <key> ALPHA DESC BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
           
SORT <key> LIMIT <offset> <count> BY <by-pattern> ALPHA GET <get-pattern> STORE <store_key> DESC
           

都产生完全相同的排序数据集。

10、重点回顾

  • SORT命令通过将被排序键包含的元素载入到数组里面,然后对数组进行排序来完成对键进行排序的工作。
  • 在默认情况下,SORT命令假设被排序键包含的都是数字值,并且以数字值的方式来进行排序。
  • 如果SORT命令使用了ALPHA选项,那么SORT命令假设被排序键包含的都是字符串值,并且以字符串的方式来进行排序。
  • SORT命令的排序操作由快速排序算法实现。
  • SORT命令会根据用户是否使用了DESC选项来决定是使用升序对比还是降序对比来比较被排序的元素,升序对比会产生升序排序结果,被排序的元素按值的大小从小到大排列,降序对比会产生降序排序结果,被排序的元素按值的大小从大到小排列。
  • 当SORT命令使用了BY选项时,命令使用其他键的值作为权重来进行排序操作。
  • 当SORT命令使用了LIMIT选项时,命令只保留排序结果集中LIMIT选项指定的元素。
  • 当SORT命令使用了GET选项时,命令会根据排序结果集中的元素,以及GET选项给定的模式,查找并返回其他键的值,而不是返回被排序的元素。
  • 当SORT命令使用了STORE选项时,命令会将排序结果集保存在指定的键里面。
  • 当SORT命令同时使用多个选项时,命令先执行排序操作(可用的选项为ALPHA、ASC或DESC、BY),然后执行LIMIT选项,之后执行GET选项,再之后执行STORE选项,最后才将排序结果集返回给客户端。
  • 除了GET选项之外,调整选项的摆放位置不会影响SORT命令的排序结果。

继续阅读