在高流量網頁應用程式中,有效管理使用者會話和提升網站效能至關重要。Redis 作為記憶體資料函式庫,提供優異的讀寫速度,非常適合用於會話管理和快取。本文將介紹如何使用 Redis 實作使用者登入狀態的維護、過期會話的清理,以及如何應用於電商網站的購物車管理和網頁快取,並提供 Python 程式碼範例。藉由 Redis 的高效能特性,可以有效降低資料函式庫負載,提升網站效能和使用者經驗。同時,文章也探討瞭如何結合 Redis 的 HASH 和 ZSET 結構,實作更精細的資料管理和快取策略。

Redis 網頁應用程式中的會話管理與清理機制

在開發高效能的網頁應用程式時,管理使用者會話是一項重要的任務。Redis 作為一個高效能的鍵值資料函式庫,提供了一系列強大的功能來幫助我們實作這一目標。本文將探討如何利用 Redis 來實作使用者登入狀態的管理以及會話清理機制。

更新 Token 與記錄使用者行為

當使用者存取網站時,我們需要更新其登入狀態並記錄其行為。以下是一個 Python 函式 update_token,它負責更新使用者的登入 token 並記錄其最近瀏覽的商品。

def update_token(conn, token, user, item=None):
    timestamp = time.time()
    conn.hset('login:', token, user)
    conn.zadd('recent:', {token: timestamp})
    if item:
        conn.zadd('viewed:' + token, {item: timestamp})
        conn.zremrangebyrank('viewed:' + token, 0, -26)

內容解密:

  1. 取得當前時間戳timestamp = time.time() 取得當前時間的時間戳,用於記錄使用者最近的活動時間。
  2. 更新登入狀態conn.hset('login:', token, user) 將使用者的登入 token 與其使用者 ID 對應起來,儲存在名為 login: 的雜湊表中。
  3. 記錄最近活動時間conn.zadd('recent:', {token: timestamp}) 將 token 和其對應的時間戳加入到名為 recent: 的有序集合中,用於追蹤最近活躍的使用者。
  4. 記錄瀏覽商品:如果提供了 item 引數,則將該商品加入到以 viewed: + token 為鍵的有序集合中,並記錄時間戳。同時,透過 conn.zremrangebyrank 移除舊的瀏覽記錄,保持最近的 25 筆資料。

清理過期會話

隨著時間的推移,Redis 中的資料會不斷增長,因此需要定期清理過期的會話資料。以下是一個清理函式 clean_sessions,它負責刪除過期的使用者會話。

QUIT = False
LIMIT = 10000000

def clean_sessions(conn):
    while not QUIT:
        size = conn.zcard('recent:')
        if size <= LIMIT:
            time.sleep(1)
            continue
        end_index = min(size - LIMIT, 100)
        tokens = conn.zrange('recent:', 0, end_index-1)
        session_keys = []
        for token in tokens:
            session_keys.append('viewed:' + token.decode('utf-8'))
        conn.delete(*session_keys)
        conn.hdel('login:', *tokens)
        conn.zrem('recent:', *tokens)

內容解密:

  1. 檢查會話數量size = conn.zcard('recent:') 取得當前 recent: 有序集合的大小,即活躍會話的數量。
  2. 判斷是否超出限制:如果會話數量未超出限制(LIMIT),則等待一秒後再次檢查。
  3. 清理過期會話:如果超出限制,則計算需要刪除的會話數量(最多 100 筆),並取出對應的 token。
  4. 刪除相關資料:刪除與這些 token 相關的瀏覽記錄、登入狀態以及最近活動記錄。

績效評估與擴充套件性

上述程式碼能夠支援每日五百萬獨立使用者存取。假設每天都有新的使用者,那麼在兩天內,我們需要開始刪除舊的會話資料。透過每秒執行一次清理函式,我們可以保持會話資料在可控範圍內。事實上,這個清理函式的效能遠超需求,能夠在網路環境下每秒清理超過一萬筆會話資料。

隨著網頁應用程式的不斷發展,使用者行為分析變得越來越重要。未來,我們可以進一步擴充套件這些功能,例如結合更多的人工智慧技術來分析使用者的瀏覽模式,或者利用 Redis 的其他特性來最佳化資料儲存和查詢效率。此外,如何在保證效能的同時提高資料的安全性和隱私保護,也將是未來需要重點關注的方向。

Redis 會話管理流程圖

  graph LR
    A[使用者存取] --> B[更新 Token]
    B --> C[記錄使用者行為]
    C --> D[檢查會話數量]
    D -->|超出限制|> E[清理過期會話]
    D -->|未超出限制|> F[等待]
    E --> G[刪除相關資料]
    F --> D

圖表翻譯: 此圖示展示了 Redis 會話管理的流程。首先,當使用者存取網站時,系統會更新其登入 token 並記錄其行為。接著,系統會檢查當前活躍會話的數量。如果數量超出預設限制,則觸發清理機制,刪除過期的會話資料;如果未超出限制,則等待一段時間後再次檢查。透過這種方式,系統能夠有效地管理使用者的會話狀態,並保持 Redis 資料函式庫的規模可控。

Redis 在電商網站中的應用:購物車管理與網頁快取

在現代電商網站中,如何有效管理使用者資料和提升網站效能是關鍵挑戰。本文將探討如何利用 Redis 來最佳化購物車管理和網頁快取,從而減少資料函式庫負擔並提升使用者經驗。

2.2 Redis 中的購物車管理

早在 90 年代中期,Netscape 率先使用 Cookie 技術來追蹤使用者的購物車內容。傳統的購物車實作方式需要在每次請求中傳遞 Cookie,不僅增加了請求負擔,也需要頻繁驗證 Cookie 的有效性。為了簡化流程並提升效能,我們將購物車資料儲存在 Redis 中。

購物車的 Redis 實作

我們的購物車系統根據 Redis 的 HASH 結構,將商品 ID 對應到使用者購買的數量。具體實作如下:

def add_to_cart(conn, session, item, count):
    if count <= 0:
        # 從購物車中移除商品
        conn.hrem('cart:' + session, item)
    else:
        # 將商品加入購物車
        conn.hset('cart:' + session, item, count)

#### 內容解密:

  1. 函式功能add_to_cart 函式負責更新使用者的購物車內容。
  2. 引數說明
    • conn:Redis 連線物件。
    • session:使用者的 session ID。
    • item:商品 ID。
    • count:商品數量。
  3. 邏輯解析
    • count 小於等於 0 時,函式呼叫 hrem 從購物車 HASH 中移除指定的商品。
    • count 大於 0 時,函式呼叫 hset 更新或新增商品數量。
  4. 技術考量:使用 Redis 的 HASH 結構可以高效地儲存和檢索購物車內容,避免了頻繁操作資料函式庫。

清理過期 Session 與購物車

為了保持資料的一致性,我們需要在清理過期 Session 的同時刪除對應的購物車資料。更新後的 clean_full_sessions 函式如下:

def clean_full_sessions(conn):
    while not QUIT:
        size = conn.zcard('recent:')
        if size <= LIMIT:
            time.sleep(1)
            continue
        end_index = min(size - LIMIT, 100)
        sessions = conn.zrange('recent:', 0, end_index-1)
        session_keys = []
        for sess in sessions:
            session_keys.append('viewed:' + sess)
            session_keys.append('cart:' + sess)
        # 批次刪除 Session 相關資料
        conn.delete(*session_keys)
        conn.hdel('login:', *sessions)
        conn.zrem('recent:', *sessions)

#### 內容解密:

  1. 函式功能clean_full_sessions 函式負責清理過期的使用者 Session 及其相關資料,包括購物車內容。
  2. 引數說明
    • conn:Redis 連線物件。
  3. 邏輯解析
    • 函式不斷檢查 Redis 中 recent: 有序集合的大小。
    • 當集合大小超過限制時,批次刪除最早的 Session ID 相關資料。
    • 同時清理 viewed:cart: 字首的鍵,以及 login: HASH 中的對應項。
  4. 技術考量:批次操作可以減少對 Redis 的請求次數,提升效能。

2.3 網頁快取最佳化

大多數電商網站的頁面內容並非完全動態生成,許多頁面在一段時間內保持不變。透過將這些頁面快取起來,可以顯著減少伺服器負載並加快頁面載入速度。

快取策略

我們的目標是對那些變化不頻繁的頁面進行快取處理。根據 Fake Web Retailer 的資料統計,約 95% 的頁面每天最多更新一次。因此,對這些頁面實施快取可以大幅度降低動態生成的負擔。

未來,我們可以進一步探索以下方向:

  1. 更精細的快取策略:根據不同頁面的更新頻率實施不同的快取策略。
  2. 使用者行為分析:利用 Redis 儲存的使用者行為資料進行更深入的分析,以最佳化推薦系統。
  3. 分散式系統最佳化:在分散式環境中進一步最佳化 Redis 的使用,以應對更高的並發需求。

透過持續最佳化和創新,我們可以進一步提升電商網站的效能和使用者滿意度。

2.3 網頁快取實作

在前面的章節中,我們已經將登入和訪客會話從關聯式資料函式庫和網頁瀏覽器遷移到 Redis,並且將購物車從關聯式資料函式庫遷移到 Redis。這些改進不僅提升了效能,也降低了對關聯式資料函式庫的負載,從而減少了我們的成本。

使用 Redis 進行網頁快取

網頁快取可以減少處理相同負載所需的伺服器數量,並且可以加快網站的回應速度。研究表明,減少使用者等待網頁載入的時間可以增加他們使用網站的意願,並改善他們對網站的評價。

所有標準的 Python 應用框架都提供了新增層的功能,這些層可以在請求處理過程中進行預處理或後處理。這些層通常被稱為中介層(middleware)或外掛程式(plugins)。讓我們建立一個這樣的中介層,它呼叫我們的 Redis 快取功能。

如果一個網頁請求無法被快取,我們將生成頁面並傳回內容。如果一個請求可以被快取,我們將嘗試從快取中取得並傳回頁面內容;否則,我們將生成頁面,將結果快取在 Redis 中最多 5 分鐘,然後傳回內容。我們的簡單快取方法如下所示:

def cache_request(conn, request, callback):
    if not can_cache(conn, request):
        return callback(request)
    page_key = 'cache:' + hash_request(request)
    content = conn.get(page_key)
    if not content:
        content = callback(request)
        conn.setex(page_key, content, 300)
    return content

內容解密:

  1. cache_request 函式的作用:該函式用於快取網頁請求的內容。它接收三個引數:Redis 連線物件 conn、請求物件 request,以及一個回呼函式 callback
  2. can_cache 檢查:首先檢查請求是否可以被快取。如果不能,則直接呼叫回呼函式生成頁面內容。
  3. 生成快取鍵:透過 hash_request 函式將請求轉換為一個簡單的字串鍵,用於後續查詢。
  4. 取得快取內容:嘗試從 Redis 中取得快取的內容。如果內容存在,則直接傳回;否則,生成內容並將其快取。
  5. 快取新生成的內容:如果內容可以被快取,則將新生成的內容存入 Redis,設定過期時間為 300 秒(5 分鐘)。
  6. 傳回內容:最終傳回頁面內容。

對於那 95% 可以被快取且經常被載入的內容,這段程式碼消除了在 5 分鐘內動態生成已檢視頁面的需求。根據內容的複雜程度,這一改變可以將資料密集型頁面的延遲從可能 20–50 毫秒降低到一次往返 Redis 的時間(對於本地連線不到 1 毫秒,在同一個資料中心內的其他電腦之間不到 5 毫秒)。

對於那些曾經需要從資料函式庫讀取資料的頁面,這進一步減少了頁面載入時間和資料函式庫負載。

2.4 資料函式庫列快取

到目前為止,在本章中,我們已經將登入和訪客會話從關聯式資料函式庫和網頁瀏覽器遷移到 Redis,將購物車從關聯式資料函式庫遷移到 Redis,並在 Redis 中快取了整個頁面。這幫助我們提高了效能,減少了對關聯式資料函式庫的負載,從而降低了成本。

資料函式庫列快取的必要性

我們向使用者顯示的個別產品頁面通常只會從資料函式庫載入一或兩列:已登入使用者的使用者資訊(透過 AJAX 呼叫保持使用我們的快取),以及有關該商品本身的資訊。即使對於我們可能不希望快取整個頁面的頁面(客戶帳戶頁面、某個使用者的過去訂單等),我們也可以改為快取關聯式資料函式庫中的個別列。

假設 Fake Web Retailer 決定開展一項新的促銷活動,既要清理一些舊庫存,又要讓人們回來花錢。為了實作這一點,我們將開始對某些商品進行每日特賣,直到它們售罄。在特賣的情況下,我們無法快取整個頁面,因為否則有人可能會看到一個有錯誤剩餘商品數量的頁面版本。雖然我們可以繼續從資料函式庫讀取商品列,但這可能會使我們的資料函式庫變得過度利用,這將增加我們的成本,因為我們需要再次擴充套件我們的資料函式庫。

資料函式庫列快取的實作

為了在重負載之前快取資料函式庫列,我們將編寫一個守護程式函式,該函式將持續執行,其目的是在 Redis 中快取特定的資料函式庫列,並按照可變的時間表更新它們。這些列本身將被儲存為 JSON 編碼的字典,作為普通的 Redis 值。我們將把欄位名稱和列值對應到字典的鍵和值。

import json
import redis

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

def cache_db_row(row_id, row_data):
    # 將資料轉換為 JSON 編碼的字典
    data_json = json.dumps(row_data)
    # 在 Redis 中設定該列
    conn.set(f"row:{row_id}", data_json)

# 示例資料
row_id = "inv:273"
row_data = {
    "qty": 629,
    "name": "GTab 7inch",
    "description": "..."
}

cache_db_row(row_id, row_data)

內容解密:

  1. cache_db_row 函式的作用:該函式用於將資料函式庫列快取到 Redis 中。它接收兩個引數:列 ID row_id 和列資料 row_data
  2. JSON 編碼:使用 json.dumps 將資料轉換為 JSON 編碼的字串,以便儲存在 Redis 中。
  3. 儲存在 Redis 中:使用 conn.set 將 JSON 編碼的字串儲存在 Redis 中,鍵為 row:<row_id>
  4. 示例資料:展示如何使用該函式快取一個示例商品列。

為了知道何時更新快取,我們將使用兩個 ZSET。第一個 ZSET 是 scheduleZSET,它將使用原始資料函式庫列中的列 ID 作為 ZSET 的成員。我們將使用時間戳作為排程分數,這將告訴我們下次應該何時將該列複製到 Redis。第二個 ZSET 是 delayZSET,它將使用相同的列 ID 作為成員,但分數將是每次快取更新之間的等待秒數。

為什麼選擇 JSON?

我們使用 JSON 而不是 XML、Google 的 Protocol Buffers、Thrift、BSON、MessagePack 或其他序列化格式是主觀的選擇。我們通常使用 JSON,因為它可讀性強,較為簡潔,並且在每種具有現有 Redis 使用者端的語言中都有快速的編碼和解碼函式庫可用(據我們所知)。如果您的情況需要使用其他格式,或者您更喜歡使用不同的格式,請隨意這樣做。

結構化儲存的限制

一些非關聯式資料函式庫的使用者有時期望能夠巢狀結構。具體來說,一些 Redis 的新使用者期望 HASH 能夠具有值是 ZSET 或 LIST 的能力。雖然在概念上這是可行的,但這裡有一個問題需要早期提出:Redis 的設計哲學是簡單性和高效性,因此不直接支援巢狀結構。

Redis 快取架構圖示

  graph LR;
    A[使用者請求] --> B{是否可快取};
    B -->|是| C[檢查 Redis 快取];
    B -->|否| D[生成頁面];
    C -->|有快取| E[傳回快取內容];
    C -->|無快取| F[生成頁面並快取];
    D --> G[傳回頁面內容];
    F --> G;
    E --> G;

圖表翻譯:

此圖示展示了根據 Redis 的網頁快取架構。當使用者發出請求時,首先檢查該請求是否可被快取。如果可被快取,則查詢 Redis 是否已有相應的快取內容。如果有,則直接傳回快取內容;如果沒有,則生成頁面並將其快取到 Redis 中,然後傳回頁面內容。如果請求不可被快取,則直接生成頁面並傳回。