隨著資料量不斷增長,Redis 的記憶體使用效率成為關鍵的效能瓶頸。本文介紹如何利用分片技術,將大型 HASH、SET 和 ZSET 分散到多個較小的結構中,有效降低記憶體佔用並提升查詢效能。文章以 Python 程式碼為例,詳細說明瞭如何設計分散式鍵計算函式、實作分散式 HASH 操作,並應用於 IP 位址到城市資訊的對映等實際案例。此外,文章還探討瞭如何使用分片 SET 實作獨立訪客統計,並分析了分片結構的優缺點以及未來的發展方向,例如最佳化分片策略、改進預估演算法以及擴充套件到其他應用場景。

9.2 分片結構(Sharded Structures)

在前一節中,我們討論瞭如何透過使用Redis中的壓縮列表(ziplist)和整數集合(intset)來減少記憶體使用。本文將介紹分片結構的概念,及其在HASH、SET和ZSET上的應用,以進一步最佳化記憶體使用和提升效能。

為何需要分片結構?

分片(Sharding)是一種將大規模資料分割成較小片段並分散儲存至不同位置的技術。這種技術在多個資料函式庫系統中被廣泛應用,以提升資料儲存和處理能力。在Redis中,透過對資料結構進行分片,我們可以在保持高效能的同時,進一步減少記憶體的使用。

分片結構的適用場景

對於LIST結構,由於其操作特性(如阻塞和非阻塞的PUSH和POP操作),在沒有Lua指令碼支援的情況下進行分片較為困難。因此,本文將重點介紹HASH、SET和ZSET的分片實作。

HASH的分片

HASH是一種常見的資料結構,用於儲存簡單的鍵值對。在某些應用場景中,單個HASH可能會包含大量的條目,例如在第5.3節中,我們曾經將IP地址對映到全球各地的城市資訊。該HASH包含了超過37萬個條目,顯然需要進行最佳化。

對HASH進行分片的關鍵步驟包括:

  1. 選擇分片鍵(Shard Key):利用HASH中的鍵作為分片的依據。
  2. 計算分片ID(Shard ID):對鍵進行雜湊計算,得到一個數值。根據預期的分片數量和總鍵數量,計算出每個分片應該包含的鍵數量,從而確定分片ID。

實作HASH分片

假設我們有一個包含城市ID和對應城市資訊的HASH,我們可以按照以下步驟進行分片:

  1. 對城市ID進行雜湊計算,得到一個整數值。
  2. 根據預定的分片數量,將雜湊值對映到對應的分片ID。
  3. 將城市ID和對應的城市資訊儲存到相應的分片HASH中,鍵格式為 city_info:<shard_id>
import hashlib

def shard_id(city_id, total_shards):
    # 對城市ID進行雜湊計算
    hash_value = int(hashlib.md5(city_id.encode()).hexdigest(), 16)
    # 計算分片ID
    return hash_value % total_shards

# 假設我們有100個分片
total_shards = 100
city_id = "12345"
shard_id = shard_id(city_id, total_shards)

# 將資料儲存到對應的分片HASH中
redis_client.hset(f"city_info:{shard_id}", city_id, "city_info_data")

#### 內容解密:

上述Python程式碼展示瞭如何根據城市ID計算分片ID並將資料儲存到對應的Redis HASH中。關鍵步驟包括:

  • 使用MD5雜湊函式對城市ID進行雜湊計算,得到一個整數雜湊值。
  • 透過取模運算,將雜湊值對映到預定的分片ID範圍內。
  • 使用Redis的HSET命令,將城市ID和對應的城市資訊儲存到相應的分片HASH中。

SET和ZSET的分片

對於SET和ZSET,分片的思路與HASH類別似,但需要考慮到它們各自的操作特性。例如,對於ZSET,由於某些操作(如ZRANGE、ZRANGEBYSCORE等)需要在所有分片上執行,因此在某些場景下,分片ZSET可能不如分片HASH或SET那樣直接有效。

然而,如果應用場景主要關注於查詢ZSET中排名靠前或靠後的元素,我們仍然可以透過分片來最佳化。例如,維護輔助的ZSET來跟蹤最高或最低得分的元素,並定期更新這些輔助ZSET,以保持高效查詢。

分片結構的優缺點

優點

  • 減少記憶體使用:透過將大結構分解為小結構,可以利用Redis的記憶體最佳化技術,如ziplist和intset。
  • 提升查詢效能:在某些場景下,分片可以減少單次查詢的延遲。

缺點

  • 實作複雜度增加:分片結構需要額外的邏輯來管理和維護,特別是在需要支援完整資料結構功能時。
  • 某些操作的效能下降:例如,對於ZSET,某些跨分片的操作可能會顯著降低效能。
  graph LR
    A[開始] --> B{計算分片ID}
    B -->|Shard ID|> C[儲存資料到對應分片]
    C --> D[結束]
    B -->|錯誤處理|> E[處理異常]

圖表翻譯:

此圖表展示了分片結構的基本流程。首先,系統會根據鍵計算出對應的分片ID。然後,將資料儲存到相應的分片中。過程中若出現錯誤,則進行相應的錯誤處理。該流程確保了資料能夠被正確地分散儲存和管理。

分散式結構最佳化:降低記憶體使用率的技術實踐

在現代的資料處理系統中,Redis 作為一種高效的記憶體資料函式庫,被廣泛應用於各種場景。然而,隨著資料量的增長,記憶體使用率成為了一個重要的最佳化方向。本文將探討如何透過分散式結構(Sharded structures)來最佳化 Redis 中的 HASH 結構,從而顯著降低記憶體使用率。

分散式鍵計算函式的設計與實作

為了實作 HASH 結構的分散式儲存,我們首先需要設計一個鍵計算函式,用於將原始鍵(key)對映到特定的分散式片段(shard)上。以下是一個實作範例:

def shard_key(base, key, total_elements, shard_size):
    if isinstance(key, (int)) or (isinstance(key, str) and key.isdigit()):
        shard_id = int(str(key), 10) // shard_size
    else:
        shards = 2 * total_elements // shard_size
        shard_id = binascii.crc32(key.encode('utf-8')) % shards
    return f"{base}:{shard_id}"

內容解密:

  1. 鍵型別的判斷與處理:函式首先判斷鍵(key)的型別。如果鍵是整數或數字字串,則直接用於計算 shard ID。這種情況下,我們假設鍵是連續分配的整數,從而可以根據鍵值直接計算 shard ID。
  2. CRC32 校驗和計算:對於非數值鍵,我們使用 CRC32 函式計算鍵的校驗和。CRC32 是一種快速且適合大多數情況的雜湊函式,能夠提供一個簡單的整數結果。
  3. Shard ID 的計算:對於數值鍵,直接根據 shard_size 進行整數除法運算以獲得 shard ID。對於非數值鍵,則根據總元素數量和 shard_size 計算出 shard 數量,並對 CRC32 結果進行模運算,以確保鍵被均勻分配到不同的 shard 中。
  4. 最終鍵的構建:將基礎鍵(base)和 shard ID 結合,生成最終的 shard 鍵,用於在 Redis 中定位資料。

分散式 HASH 操作的實作

利用上述 shard_key 函式,我們可以進一步實作類別似於 HSET 和 HGET 的分散式 HASH 操作函式:

def shard_hset(conn, base, key, value, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)
    return conn.hset(shard, key, value)

def shard_hget(conn, base, key, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)
    return conn.hget(shard, key)

內容解密:

  1. Shard 鍵的計算:在進行 HSET 或 HGET 操作之前,首先呼叫 shard_key 函式計算鍵對應的 shard 鍵。
  2. 資料儲存與檢索:利用計算出的 shard 鍵,在對應的 Redis HASH 結構中進行資料的儲存(HSET)或檢索(HGET)。

實際應用案例:IP 位址到城市資訊的對映

假設我們需要維護一個從 IP 位址到城市資訊的對映表,可以透過以下方式實作:

TOTAL_SIZE = 320000
SHARD_SIZE = 1024

def import_cities_to_redis(conn, filename):
    for row in csv.reader(open(filename)):
        # ... 資料處理邏輯
        city_id = # ... 取得城市 ID
        shard_hset(conn, 'cityid2city:', city_id, json.dumps([city, region, country]), TOTAL_SIZE, SHARD_SIZE)

def find_city_by_ip(conn, ip_address):
    # ... IP 位址處理邏輯
    city_id = # ... 取得城市 ID
    data = shard_hget(conn, 'cityid2city:', city_id, TOTAL_SIZE, SHARD_SIZE)
    return json.loads(data)

內容解密:

  1. 資料匯入:在資料匯入過程中,利用 shard_hset 將城市資訊儲存到分散式的 HASH 結構中。
  2. 資料查詢:查詢時,透過 shard_hget 從對應的 shard 中檢索城市資訊。

記憶體使用率最佳化效果評估

透過上述分散式 HASH 結構的最佳化,原本需要約 44 MB 記憶體儲存的城市資訊 HASH 表,被最佳化到約 12 MB,記憶體使用率降低了約 70%。這意味著在相同的記憶體資源下,可以儲存更多的資料。

練習:擴充套件更多操作功能

除了 HSET 和 HGET,您可以嘗試實作更多操作,如 HDEL、HINCRBY 和 HINCRBYFLOAT 的分散式版本,以進一步豐富分散式 HASH 結構的功能。

def shard_hdel(conn, base, key, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)
    return conn.hdel(shard, key)

def shard_hincrby(conn, base, key, amount, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)
    return conn.hincrby(shard, key, amount)

def shard_hincrbyfloat(conn, base, key, amount, total_elements, shard_size):
    shard = shard_key(base, key, total_elements, shard_size)
    return conn.hincrbyfloat(shard, key, amount)

內容解密:

  1. HDEL 操作:實作分散式的 HDEL 操作,用於刪除指定鍵的資料。
  2. HINCRBY 和 HINCRBYFLOAT 操作:實作分散式的計數器操作,用於對指定鍵的值進行增量操作,支援整數和浮點數型別。

這些擴充套件操作將進一步增強分散式 HASH 結構的靈活性和實用性,使其能夠支援更多型別的應用場景。

在Redis中實作分片SET以統計獨立訪客

隨著網路應用的發展,統計獨立訪客成為網站營運的重要指標。傳統的做法是在一天結束後進行統計,但這樣的做法延遲了資料的可用性。本章將介紹如何在Redis中使用分片SET(Sharded SETs)來實時統計獨立訪客數量。

為什麼使用分片SET?

在Redis中,SET是一種理想的資料結構,用於儲存唯一的元素。然而,當獨立訪客數量龐大時,單個SET可能會變得非常龐大,影響效能和記憶體使用。為瞭解決這個問題,我們可以採用分片技術,將大SET分散到多個小SET中,每個小SET負責儲存一部分資料。

分片SET的工作原理

  1. UUID處理:假設每個訪客都有一個唯一的UUID(Universally Unique Identifier)。為了節省空間,我們可以取UUID的前56位(即前15個十六進位制數字轉換為數字),這樣每個訪客對應一個唯一的數字ID。

  2. 分片函式:使用分片函式將訪客ID對映到不同的SET。這個函式會根據訪客ID和預期的訪客數量計算出一個分片鍵(Shard Key)。

  3. SADD操作:對對映到的SET執行SADD(Set Add)操作,將訪客ID新增到對應的SET中。如果新增成功(即該ID之前不存在於SET中),則表示這是一個新的獨立訪客。

實作分片SADD函式

def shard_sadd(conn, base, member, total_elements, shard_size):
    shard = shard_key(base, 'x'+str(member), total_elements, shard_size)
    return conn.sadd(shard, member)

內容解密:

  • shard_sadd函式封裝了分片SADD操作。
  • shard_key函式根據訪客ID和預期訪客數量計算分片鍵。
  • conn.sadd是Redis的SADD命令,用於將訪客ID新增到對應的分片SET中。

統計獨立訪客

為了實時統計獨立訪客數量,我們需要在每次訪客存取時執行以下步驟:

  1. 計算訪客ID:取訪客UUID的前56位作為數字ID。
  2. 確定日期:取得當天的日期,並建構當天的獨立訪客計數鍵。
  3. 執行分片SADD:使用shard_sadd函式將訪客ID新增到對應的分片SET中。
  4. 更新計數:如果SADD操作成功,表示這是一個新的獨立訪客,則對當天的獨立訪客計數進行增量操作。
SHARD_SIZE = 512
def count_visit(conn, session_id):
    today = date.today()
    key = 'unique:%s' % today.isoformat()
    expected = get_expected(conn, key, today)
    id = int(session_id.replace('-', '')[:15], 16)
    if shard_sadd(conn, key, id, expected, SHARD_SIZE):
        conn.incr(key)

內容解密:

  • count_visit函式負責統計獨立訪客。
  • SHARD_SIZE定義了分片的大小,這裡設為512,以保持SET的intset編碼。
  • get_expected函式預估當天的獨立訪客數量。

預估當天獨立訪客數量

為了動態調整分片數量,我們需要預估當天的獨立訪客數量。這個預估根據前一天的訪客數量,並假設當天的訪客數量至少是前一天的1.5倍。

DAILY_EXPECTED = 1000000
EXPECTED = {}
def get_expected(conn, key, today):
    # 實作細節省略

內容解密:

  • get_expected函式根據前一天的獨立訪客數量預估當天的數量。
  • DAILY_EXPECTED是初始的預期訪客數量。
  • EXPECTED字典用於快取已經計算出的預期訪客數量。
  1. 最佳化分片策略:根據實際訪客資料調整分片大小和策略,以進一步最佳化記憶體使用和效能。
  2. 改進預估演算法:利用機器學習等技術改進訪客數量預估演算法,提高預估的準確性。
  3. 擴充套件到其他場景:將分片SET的技術應用到其他需要統計唯一元素的場景中。

圖表翻譯:

此圖示展示了分片SET的工作原理和統計獨立訪客的流程。

  graph LR;
    B[B]
    E[E]
    A[訪客存取] --> B{計算訪客ID};
    B --> C[確定日期];
    C --> D[執行分片SADD];
    D --> E{是否新訪客};
    E -->|是| F[更新獨立訪客計數];
    E -->|否| G[結束];

圖表翻譯: 此圖表展示了從訪客存取到統計獨立訪客的整個流程。首先計算訪客ID,然後根據日期執行分片SADD操作。如果訪客ID是新的,則更新獨立訪客計數。

參考程式碼

以下是完整的參考程式碼,包含必要的註解和說明:

import redis
from datetime import date

# Redis連線
conn = redis.Redis(host='localhost', port=6379, db=0)

def shard_key(base, member, total_elements, shard_size):
    # 實作細節省略
    pass

def shard_sadd(conn, base, member, total_elements, shard_size):
    shard = shard_key(base, 'x'+str(member), total_elements, shard_size)
    return conn.sadd(shard, member)

SHARD_SIZE = 512

def count_visit(conn, session_id):
    today = date.today()
    key = 'unique:%s' % today.isoformat()
    expected = get_expected(conn, key, today)
    id = int(session_id.replace('-', '')[:15], 16)
    if shard_sadd(conn, key, id, expected, SHARD_SIZE):
        conn.incr(key)

DAILY_EXPECTED = 1000000
EXPECTED = {}

def get_expected(conn, key, today):
    # 實作細節省略
    pass

內容解密:

  • 程式碼展示瞭如何在Python中使用Redis實作分片SET和獨立訪客統計。
  • 關鍵函式包括shard_saddcount_visit
  • 透過合理設定SHARD_SIZE和最佳化get_expected函式,可以進一步提高系統的效能和準確性。