Redis除了基礎功能外,還能實作更進階的應用,像是自動補全、分散式鎖定、計數訊號量、任務佇列和延遲訊息傳遞等。這些功能可以有效提升應用程式的效能和可靠性。自動補全功能方面,Redis 提供了有序集合和集合搭配字首樹兩種實作方式,各有其優缺點。分散式鎖定則利用 SETNX 命令,確保多個客戶端不會同時修改同一份資料。計數訊號量則使用 INCR 和 DECR 命令控制平行存取資源的數量。任務佇列利用 Redis 的列表結構,實作後台任務的處理。延遲訊息傳遞則使用有序集合,根據時間戳記排序訊息,實作定時任務的功能。

使用Redis進行應用程式支援的進階技術

在前面的章節中,我們已經探討瞭如何使用Redis來支援應用程式的基本功能。本章節將進一步探討一些進階技術,包括如何使用Redis來實作自動補全功能、分散式鎖定、計數訊號量、任務佇列以及延遲訊息傳遞等。

自動補全功能的實作

自動補全功能是許多應用程式中非常實用的功能,尤其是在需要從大量資料中快速搜尋特定專案的情況下。Redis提供了兩種方法來實作字首匹配的自動補全功能。

方法一:使用有序集合實作自動補全

def autocomplete_on_prefix(conn, guild, prefix):
    start, count = 0, 5  # 假設我們每次取回5個結果
    while True:
        # 使用ZSCAN命令來掃描有序集合中的元素
        items = conn.zscan('guild:{}'.format(guild), start, match=prefix + '*', count=count)
        for item in items[1]:
            # 處理每個匹配的元素
            print(item)
        if items[0] == start:
            break
        start = items[0]

方法二:使用集合與字首樹實作自動補全

def add_to_autocomplete(conn, guild, item):
    # 將專案新增到字首樹中
    for i in range(1, len(item) + 1):
        prefix = item[:i]
        conn.sadd('autocomplete:{}'.format(prefix), item)
    conn.sadd('guild:{}'.format(guild), item)

def autocomplete_on_prefix(conn, guild, prefix):
    # 從字首樹中取得匹配的專案
    return conn.smembers('autocomplete:{}'.format(prefix))

分散式鎖定的實作

分散式鎖定是一種重要的技術,用於防止多個客戶端同時修改同一份資料,從而導致資料不一致的問題。Redis提供了SETNX命令來實作分散式鎖定。

簡單的分散式鎖定實作

def acquire_lock(conn, lock_name, acquire_timeout=10):
    # 使用SETNX命令來嘗試取得鎖定
    end = time.time() + acquire_timeout
    while time.time() < end:
        if conn.set(lock_name, 'locked', nx=True, ex=10):
            return True
        time.sleep(0.1)
    return False

def release_lock(conn, lock_name):
    # 釋放鎖定
    conn.delete(lock_name)

計數訊號量的實作

計數訊號量是一種用於控制平行存取資源的技術。Redis可以使用INCRDECR命令來實作計數訊號量。

簡單的計數訊號量實作

class CountingSemaphore:
    def __init__(self, conn, name, limit):
        self.conn = conn
        self.name = name
        self.limit = limit

    def acquire(self):
        # 嘗試取得訊號量
        if self.conn.incr(self.name) <= self.limit:
            return True
        # 如果取得失敗,則遞減計數器
        self.conn.decr(self.name)
        return False

    def release(self):
        # 釋放訊號量
        self.conn.decr(self.name)

任務佇列的實作

任務佇列是一種用於處理後台任務的技術。Redis可以使用列表來實作任務佇列。

簡單的任務佇列實作

def enqueue_task(conn, task_queue, task_data):
    # 將任務資料新增到佇列中
    conn.rpush(task_queue, task_data)

def dequeue_task(conn, task_queue):
    # 從佇列中取得任務資料
    return conn.blpop(task_queue, timeout=10)

延遲訊息傳遞的實作

延遲訊息傳遞是一種用於在特定時間執行任務的技術。Redis可以使用有序集合來實作延遲訊息傳遞。

簡單的延遲訊息傳遞實作

def send_delayed_message(conn, message_queue, message, delay):
    # 將訊息新增到有序集合中,時間戳記作為評分
    conn.zadd(message_queue, {message: time.time() + delay})

def process_delayed_messages(conn, message_queue):
    # 取得當前時間之前的所有訊息
    messages = conn.zrangebyscore(message_queue, 0, time.time())
    for message in messages:
        # 處理每個訊息
        print(message)
        # 從有序集合中刪除已處理的訊息
        conn.zrem(message_queue, message)
內容解密:

以上程式碼範例展示瞭如何使用Redis的不同資料結構來實作各種進階功能。自動補全功能使用了有序集合和字首樹, 分散式鎖定使用了SETNX命令,計數訊號量使用了INCRDECR命令,任務佇列使用了列表,延遲訊息傳遞使用了有序集合。這些範例展示了Redis在不同場景下的靈活性和強大功能。

隨著Redis的不斷發展和演進,未來可能會出現更多新的功能和特性。開發者可以透過不斷學習和實踐來掌握這些新技術,從而進一步提升應用程式的效能和可靠性。

參考資料

  • Redis官方檔案:https://redis.io/documentation
  • Redis Python客戶端:https://redis-py.readthedocs.io/en/latest/

圖表翻譯:

此圖示展示了使用Redis實作自動補全功能的流程圖。

  graph LR
    D[D]
    A[開始] --> B[輸入字首]
    B --> C[查詢Redis]
    C --> D{是否有匹配結果}
    D -->|是| E[傳回結果]
    D -->|否| F[傳回無結果]
    E --> G[結束]
    F --> G

圖表翻譯: 此圖示描述了自動補全功能的流程,包括輸入字首、查詢Redis、傳回結果等步驟。

自動完成(Autocomplete)功能實作

在網路世界中,自動完成是一種允許我們快速查詢所需內容而無需完全輸入的方法。它通常透過輸入的字元來查詢所有以這些字元開頭的詞語。某些自動完成工具甚至允許我們輸入短語的開頭並自動完成該短語。以 Google 搜尋為例,其搜尋框中的自動完成功能背後有大量的遠端資料支援,而瀏覽器歷史記錄和登入框等則由較小的本地資料函式庫支援。

本章節將構建兩種不同型別的自動完成功能。第一種使用列表來記憶使用者最近聯絡過的100位聯絡人,旨在最小化記憶體使用。第二種自動完成功能則提供更好的效能和可擴充套件性,但每個列表使用的記憶體較多。兩者的結構、方法和操作完成時間均有所不同。

最近聯絡人自動完成

此自動完成功能旨在為每個使用者維護一份最近聯絡過的使用者列表。為了增加遊戲的社交性並允許使用者快速找到並記住優秀的遊戲夥伴,Fake Game Company希望為客戶端建立一個聯絡人列表,以記錄每個使用者最近與之聊過的100個人。在客戶端,當使用者開始輸入想要與之聊天的使用者名稱時,自動完成功能將顯示螢幕名稱以輸入字元開頭的使用者列表。圖6.1展示了這種自動完成的範例。

圖6.1:最近聯絡人自動完成範例

此圖示顯示了使用者輸入「je」後,自動完成顯示的聯絡人列表,包括「Jean」、「Jeannie」和「Jeff」。

圖表翻譯: 圖6.1展示了一個根據使用者輸入自動顯示最近聯絡人的功能。當使用者輸入「je」時,系統顯示與之匹配的聯絡人名稱。

由於伺服器上的數百萬使用者每個人都有自己的最近100位聯絡人列表,我們需要在最小化記憶體使用的同時,仍能快速新增和移除列表中的使用者。Redis的LIST結構保持了專案的順序一致性,並且相比其他結構使用更少的記憶體,因此我們將使用LIST來儲存自動完成列表。

不過,由於LIST本身不支援在Redis內部執行自動完成操作,我們將在Python中執行實際的自動完成操作。這讓我們能夠利用Redis來儲存和更新這些列表,同時使用最少的記憶體,並將相對簡單的過濾工作交給Python處理。

處理最近聯絡人自動完成列表需要執行三個主要操作:

  1. 新增或更新聯絡人:將聯絡人設為最近聯絡的使用者。

    • 從列表中移除聯絡人(如果存在)。
    • 將聯絡人新增到列表的開頭。
    • 修剪列表以確保其不超過100個專案。
    def add_update_contact(conn, user, contact):
        ac_list = 'recent:' + user
        pipeline = conn.pipeline(True)
        pipeline.lrem(ac_list, contact)  # 移除列表中的聯絡人(如果存在)
        pipeline.lpush(ac_list, contact)  # 將聯絡人推入列表的左側(開頭)
        pipeline.ltrim(ac_list, 0, 99)    # 修剪列表以保持其不超過100個專案
        pipeline.execute()               # 執行所有操作
    

    #### 內容解密:

    • add_update_contact函式首先從列表中移除指定的聯絡人(如果存在),以避免重複。
    • 然後,它將聯絡人推入列表的開頭,以標記其為最近聯絡的使用者。
    • 使用LTRIM命令確保列表保持在100個專案以內,從而控制記憶體使用。
    • 所有這些操作都在一個MULTI/EXEC事務中執行,以確保操作的原子性,避免競爭條件。
  2. 移除聯絡人:如果使用者不希望再與某個聯絡人聯絡,可以將其從列表中移除。

    def remove_contact(conn, user, contact):
        conn.lrem('recent:' + user, contact)
    

    #### 內容解密:

    • remove_contact函式使用LREM命令從使用者的最近聯絡人列表中移除指定的聯絡人。
    • 這是一個簡單直接的操作,用於維護使用者的聯絡人列表。

自動完成功能的比較

  • 第一種自動完成使用LIST結構,主要優點是記憶體使用效率高,適合記憶體受限的環境。
  • 第二種自動完成將提供更好的效能和可擴充套件性,但每個列表使用的記憶體較多,適合對查詢效能要求較高的場景。

進一步的改進方向

  • 最佳化記憶體使用,特別是在使用者基數龐大的情況下。
  • 提升查詢效能,以支援更快速的自動完成體驗。
  • 考慮使用更進階的資料結構,如有序集合(Sorted Set),以提升自動完成功能的效率和可擴充套件性。

Redis 自動完成實作:從清單到有序集合

在前一章節中,我們討論瞭如何使用 Redis 的 LIST 結構來儲存最近聯絡人的清單,並實作了一個簡單的自動完成(autocomplete)功能。然而,這種方法在處理大型清單時可能會遇到效能問題。因此,本章節將介紹如何使用 Redis 的 ZSET 結構來實作更高效的自動完成功能。

簡單自動完成的限制

首先,讓我們回顧一下使用 LIST 結構實作的簡單自動完成功能。我們使用 fetch_autocomplete_list 函式來取得聯絡人清單,並在 Python 中進行篩選。

def fetch_autocomplete_list(conn, user, prefix):
    candidates = conn.lrange('recent:' + user, 0, -1)
    matches = []
    for candidate in candidates:
        if candidate.lower().startswith(prefix):
            matches.append(candidate)
    return matches

內容解密:

  • conn.lrange('recent:' + user, 0, -1):從 Redis 中取得指定使用者的最近聯絡人清單。
  • if candidate.lower().startswith(prefix):篩選出與指定字首匹配的聯絡人。
  • matches.append(candidate):將匹配的聯絡人加入結果清單中。

這種方法在清單較小或使用者數量有限的情況下可以正常運作。然而,當清單規模增大時,效能將會成為一大問題。

使用 ZSET 實作高效自動完成

為了克服 LIST 結構的限制,我們可以使用 Redis 的 ZSET 結構來實作自動完成。ZSET 是一種有序集合,可以根據成員的分數進行排序。在我們的案例中,我們將所有成員的分數設為 0,這樣 ZSET 就會根據成員名稱的字串順序進行排序。

ZSET 的優勢

  • 快速查詢:ZSET 提供了快速的成員查詢功能,可以快速判斷某個成員是否存在於集合中。
  • 排序功能:當所有成員的分數相同時,ZSET 會根據成員名稱的字串順序進行排序,這使得我們可以利用這個特性來實作自動完成。

實作 ZSET 自動完成

要實作根據 ZSET 的自動完成,我們需要將聯絡人名稱轉換為小寫字母,並將其加入 ZSET 中。然後,我們可以利用 ZSET 的排序特性來找出與指定字首匹配的成員。

尋找字首範圍

為了實作自動完成,我們需要找出與指定字首匹配的成員範圍。我們可以透過以下步驟來實作:

  1. 計算字首範圍:根據字首的最後一個字元,找出在字元序列中位於其前後的字元。
  2. 建構查詢範圍:利用上述字元,建構出查詢範圍的起始和結束成員。
valid_characters = '`abcdefghijklmnopqrstuvwxyz{'
def find_prefix_range(prefix):
    posn = bisect.bisect_left(valid_characters, prefix[-1:])
    suffix = valid_characters[(posn or 1) - 1]
    return prefix[:-1] + suffix + '{', prefix + '{'

內容解密:

  • bisect.bisect_left(valid_characters, prefix[-1:]):在 valid_characters 序列中找出字首最後一個字元的位置。
  • valid_characters[(posn or 1) - 1]:取得位於字首最後一個字元之前的字元。
  • prefix[:-1] + suffix + '{'prefix + '{':建構查詢範圍的起始和結束成員。

使用 ZSET 進行自動完成

有了查詢範圍後,我們就可以利用 ZRANGE 命令來取得與指定字首匹配的成員。

def autocomplete_on_prefix(conn, guild, prefix):
    start, end = find_prefix_range(prefix)
    identifier = str(uuid.uuid4())
    start += identifier
    end += identifier
    zset_name = 'members:' + guild
    
    # Add the range endpoints to the ZSET
    conn.zadd(zset_name, {start: 0, end: 0})
    
    # Find the ranks of the range endpoints
    start_rank = conn.zrank(zset_name, start)
    end_rank = conn.zrank(zset_name, end)
    
    # Fetch the range of items between the endpoints
    results = conn.zrange(zset_name, start_rank, end_rank - 1)
    
    # Remove the range endpoints from the ZSET
    conn.zrem(zset_name, start, end)
    
    return [item for item in results if '{' not in item]

內容解密:

  • conn.zadd(zset_name, {start: 0, end: 0}):將查詢範圍的起始和結束成員加入 ZSET 中。

  • conn.zrank(zset_name, start)conn.zrank(zset_name, end):取得查詢範圍的起始和結束成員在 ZSET 中的排名。

  • conn.zrange(zset_name, start_rank, end_rank - 1):取得查詢範圍內的成員。

  • conn.zrem(zset_name, start, end):從 ZSET 中移除查詢範圍的起始和結束成員。

  • [item for item in results if '{' not in item]:過濾掉包含 { 字元的成員。

  • 最佳化查詢效能:可以透過最佳化 ZSET 的查詢效能來進一步提高自動完成的效能。

  • 支援多語言:目前的實作僅支援英文名稱,未來可以擴充套件到支援多語言。

  • 整合機器學習:可以整合機器學習演算法來提高自動完成的準確度。

參考資料

以上內容完整介紹瞭如何使用 Redis 的 ZSET 結構來實作高效的自動完成功能,並提供了詳細的程式碼範例和解說。未來,我們可以進一步最佳化查詢效能、支援多語言以及整合機器學習演算法來提高自動完成的準確度和效率。