如何優化 Redis 掃描性能

Redis 是一款強大而多才多藝的內存數據存儲,被廣泛用於緩存、會話管理、實時分析等場景。Redis 的一個關鍵特性是其對邏輯數據庫的支持,使用戶能夠在單個 Redis 實例中對數據進行分區。這些邏輯數據庫提供了隔離和在鍵方面的不同命名空間,從而實現更有效的數據管理和組織。在本文中,我將展示如何利用邏輯數據庫來提升 Redis 查詢性能。

邏輯數據庫

Redis 支持多個邏輯數據庫,通常稱爲 “數據庫編號” 或“DB”。每個邏輯數據庫都是相互隔離的,一個數據庫中存儲的數據無法直接從另一個數據庫中訪問。這種隔離提供了一種對數據進行邏輯分區的方式。在 Redis 中,鍵在數據庫內是唯一的。因此,不同的數據庫爲鍵提供了獨立的命名空間,允許在不發生衝突的情況下在不同的數據庫中使用相同的鍵。

帶有邏輯數據庫和共享資源(CPU 和內存)的 Redis 實例

雖然邏輯數據庫提供了隔離,但它們仍然在單個 Redis 實例內共享相同的底層物理資源(內存、CPU 等)。因此,對一個數據庫的大量使用可能潛在地影響其他數據庫的性能。

掃描性能

儘管 Redis 不是專爲像傳統關係型數據庫那樣的複雜查詢而設計的,但在某些情況下,您可能需要獲取具有相同前綴的一組鍵。這是一個常見的需求,特別是在鍵按層次結構組織或按公共標識符分組的場景中。

讓我們深入探討一個性能查詢取決於數據庫大小的場景。假設您正在使用 Redis 緩存最近訪問您網站的用戶的值,TTL(生存時間)爲 24 小時。這些緩存的值存儲在前綴爲 user_id 下。此外,您還有一個用於當前正在使用您服務的用戶的 Active Users 緩存,前綴爲 active_user_id,TTL 爲 2 小時。現在,您有一個定期檢查有多少活躍用戶並使用 Active Users 緩存的過程。以下是性能如何受數據庫大小影響的一個示例。

隨着越來越多的用戶訪問您的網站並將其數據緩存在 Redis 中,前綴爲 user_id 的數據庫大小將增長。令人驚訝的是,即使活躍用戶數量穩定,掃描活躍用戶的速度也可能變慢。這是因爲 SCAN 命令遍歷數據庫中的所有鍵,並之後應用前綴模式。請參閱以下實現。我們有一個簡單的函數,用於使用給定前綴向 Redis 數據庫填充隨機記錄。

import random
import redis
import string
def populate_db(host, port, db_number, key_prefix, n):
    r = redis.Redis(host=host, port=port, db=db_number)
    # 生成並將隨機數據加載到 Redis
    for i in range(n):
        suffix = ''.join(random.choices(string.ascii_letters, k=5))
        key = f"{key_prefix}{suffix}"
        value = ''.join(
            random.choices(string.ascii_letters + string.digits, k=5),
        )
        r.set(key, value)
    print("數據加載到 Redis。")

在 Redis 中,SCAN 命令用於安全而高效地遍歷數據庫中的鍵。使用基於遊標的迭代方法與 SCAN 而不是一次性獲取所有鍵(KEYS <prefix>)的主要原因是確保該操作不會阻塞 Redis 服務器或在數據庫較大的情況下對其性能產生負面影響。

import redis
import time
def scan_redis_by_pattern(host, port, db_number, pattern):
    r = redis.Redis(host=host, port=port, db=db_number)
    num_keys = r.dbsize()
    print(f"DB={db_number} 的鍵數量: {num_keys}")
    cursor = 0
    keys = []
    while True:
        cursor, partial_keys = r.scan(cursor, match=pattern)
        keys.extend(partial_keys)
        if cursor == 0:
            break
    return keys

現在我們根據數據庫中的 user_id 記錄數量檢查 active_user_id 查詢性能。

host = 'localhost'
port = 6379
pattern = 'active_user_id:*'
db_number = 0
# populate_db(host, port, db_number, "active_user_id:", 1)
for n in [10, 1000, 10000]:
    populate_db(host, port, db_number, "user_id:", n)
    start = time.time()
    keys = scan_redis_by_pattern(host, port, db_number, pattern)
    print(
        f"Keys: {keys}, Duration: {time.time() - start}s",
    )

我們得到以下結果:

數據加載到 Redis。
DB=0 的鍵數量: 11
Keys: [b'active_user_id:aTtsr'], Duration: 0.004511117935180664s
數據加載到 Redis。
DB=0 的鍵數量: 1011
Keys: [b'active_user_id:aTtsr'], Duration: 0.051651954650878906s
數據加載到 Redis。
DB=0 的鍵數量: 100999
Keys: [b'active_user_id:aTtsr
'], Duration: 4.748287916183472s

隨着數據庫中 user_id 鍵的數量增加,執行 active_user_id 查詢所需的時間也會成比例增加(從幾毫秒到幾秒)。這突顯了在設計和管理 Redis 數據庫時考慮數據庫大小和性能影響的重要性。

如果將 active_user_id 和 user_id 記錄保持在不同的邏輯數據庫中,那麼 user_id 鍵的數量增加將不會影響 active_user_id 掃描。

數據加載到 Redis。
DB=0 的鍵數量: 1000990
DB=1 的鍵數量: 1
Keys: [b'active_user_id:DsHfN'], Duration: 0.003325939178466797s

正如您所見,將數據分隔到邏輯數據庫中是一種簡單而有效的設計策略,可用於提升 Redis 性能。

結論

Redis 的邏輯數據庫爲在單個 Redis 實例中組織和管理數據提供了強大的機制。通過將數據劃分到獨立的邏輯數據庫中,用戶可以實現更好的隔離和更高效的數據訪問。然而,必須注意共享內存和 CPU 利用率的潛在性能影響。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g_yq_r3sA3N59ewimGwJqA