天天看點

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指令的排序結果。

繼續閱讀