如何優(yōu)化 Redis 掃描性能
Redis 是一款強大而多才多藝的內(nèi)存數(shù)據(jù)存儲,被廣泛用于緩存、會話管理、實時分析等場景。Redis 的一個關(guān)鍵特性是其對邏輯數(shù)據(jù)庫的支持,使用戶能夠在單個 Redis 實例中對數(shù)據(jù)進行分區(qū)。這些邏輯數(shù)據(jù)庫提供了隔離和在鍵方面的不同命名空間,從而實現(xiàn)更有效的數(shù)據(jù)管理和組織。在本文中,我將展示如何利用邏輯數(shù)據(jù)庫來提升 Redis 查詢性能。
邏輯數(shù)據(jù)庫
Redis 支持多個邏輯數(shù)據(jù)庫,通常稱為“數(shù)據(jù)庫編號”或“DB”。每個邏輯數(shù)據(jù)庫都是相互隔離的,一個數(shù)據(jù)庫中存儲的數(shù)據(jù)無法直接從另一個數(shù)據(jù)庫中訪問。這種隔離提供了一種對數(shù)據(jù)進行邏輯分區(qū)的方式。在 Redis 中,鍵在數(shù)據(jù)庫內(nèi)是唯一的。因此,不同的數(shù)據(jù)庫為鍵提供了獨立的命名空間,允許在不發(fā)生沖突的情況下在不同的數(shù)據(jù)庫中使用相同的鍵。
帶有邏輯數(shù)據(jù)庫和共享資源(CPU 和內(nèi)存)的 Redis 實例
雖然邏輯數(shù)據(jù)庫提供了隔離,但它們?nèi)匀辉趩蝹€ Redis 實例內(nèi)共享相同的底層物理資源(內(nèi)存、CPU 等)。因此,對一個數(shù)據(jù)庫的大量使用可能潛在地影響其他數(shù)據(jù)庫的性能。
掃描性能
盡管 Redis 不是專為像傳統(tǒng)關(guān)系型數(shù)據(jù)庫那樣的復(fù)雜查詢而設(shè)計的,但在某些情況下,您可能需要獲取具有相同前綴的一組鍵。這是一個常見的需求,特別是在鍵按層次結(jié)構(gòu)組織或按公共標(biāo)識符分組的場景中。
讓我們深入探討一個性能查詢?nèi)Q于數(shù)據(jù)庫大小的場景。假設(shè)您正在使用 Redis 緩存最近訪問您網(wǎng)站的用戶的值,TTL(生存時間)為 24 小時。這些緩存的值存儲在前綴為 user_id 下。此外,您還有一個用于當(dāng)前正在使用您服務(wù)的用戶的 Active Users 緩存,前綴為 active_user_id,TTL 為 2 小時。現(xiàn)在,您有一個定期檢查有多少活躍用戶并使用 Active Users 緩存的過程。以下是性能如何受數(shù)據(jù)庫大小影響的一個示例。
隨著越來越多的用戶訪問您的網(wǎng)站并將其數(shù)據(jù)緩存在 Redis 中,前綴為 user_id 的數(shù)據(jù)庫大小將增長。令人驚訝的是,即使活躍用戶數(shù)量穩(wěn)定,掃描活躍用戶的速度也可能變慢。這是因為 SCAN 命令遍歷數(shù)據(jù)庫中的所有鍵,并之后應(yīng)用前綴模式。請參閱以下實現(xiàn)。我們有一個簡單的函數(shù),用于使用給定前綴向 Redis 數(shù)據(jù)庫填充隨機記錄。
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)
# 生成并將隨機數(shù)據(jù)加載到 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("數(shù)據(jù)加載到 Redis。")
在 Redis 中,SCAN 命令用于安全而高效地遍歷數(shù)據(jù)庫中的鍵。使用基于游標(biāo)的迭代方法與 SCAN 而不是一次性獲取所有鍵(KEYS <prefix>)的主要原因是確保該操作不會阻塞 Redis 服務(wù)器或在數(shù)據(jù)庫較大的情況下對其性能產(chǎn)生負(fù)面影響。
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} 的鍵數(shù)量: {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
現(xiàn)在我們根據(jù)數(shù)據(jù)庫中的 user_id 記錄數(shù)量檢查 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",
)
我們得到以下結(jié)果:
數(shù)據(jù)加載到 Redis。
DB=0 的鍵數(shù)量: 11
Keys: [b'active_user_id:aTtsr'], Duration: 0.004511117935180664s
數(shù)據(jù)加載到 Redis。
DB=0 的鍵數(shù)量: 1011
Keys: [b'active_user_id:aTtsr'], Duration: 0.051651954650878906s
數(shù)據(jù)加載到 Redis。
DB=0 的鍵數(shù)量: 100999
Keys: [b'active_user_id:aTtsr
'], Duration: 4.748287916183472s
隨著數(shù)據(jù)庫中 user_id 鍵的數(shù)量增加,執(zhí)行 active_user_id 查詢所需的時間也會成比例增加(從幾毫秒到幾秒)。這突顯了在設(shè)計和管理 Redis 數(shù)據(jù)庫時考慮數(shù)據(jù)庫大小和性能影響的重要性。
如果將 active_user_id 和 user_id 記錄保持在不同的邏輯數(shù)據(jù)庫中,那么 user_id 鍵的數(shù)量增加將不會影響 active_user_id 掃描。
數(shù)據(jù)加載到 Redis。
DB=0 的鍵數(shù)量: 1000990
DB=1 的鍵數(shù)量: 1
Keys: [b'active_user_id:DsHfN'], Duration: 0.003325939178466797s
正如您所見,將數(shù)據(jù)分隔到邏輯數(shù)據(jù)庫中是一種簡單而有效的設(shè)計策略,可用于提升 Redis 性能。
結(jié)論
Redis 的邏輯數(shù)據(jù)庫為在單個 Redis 實例中組織和管理數(shù)據(jù)提供了強大的機制。通過將數(shù)據(jù)劃分到獨立的邏輯數(shù)據(jù)庫中,用戶可以實現(xiàn)更好的隔離和更高效的數(shù)據(jù)訪問。然而,必須注意共享內(nèi)存和 CPU 利用率的潛在性能影響。