Python–Redis實戰:第四章:資料安全與效能保障:第7節:非事務型流水線

Mark發表於2018-11-19

上一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第6節:Redis事務
下一篇文章: Python–Redis實戰:第四章:資料安全與效能保障:第8節:關於效能方面的注意事項

之前章節首次介紹multi和exec的時候討論過它們的”事務“性質:被multi和exec包裹的命令在執行時不會被其他客戶端打擾。而使用事務的其中一個好處就是底層的客戶端會通過使用流水線來提高事務執行的效能。本節將介紹如何在不使用事務的情況下,通過使用流水線來進一步提升命令的執行效能。

之前章節曾經介紹過一些可以接受多個引數的新增命令和更新命令,如mget、mset、hmget、hmset、rpush、lpush、sadd、zadd等。這些命令簡化了那些需要重複執行相同命令的操作,並且極大地提升了效能。儘管效果可能沒有以上提到的命令那麼顯著,但使用非事務型流水線同樣可以獲得相似的效能提升,並且可以讓使用者同時執行多個不同的命令。

在需要執行大量命令的情況下,即使命令實際上並不需要放在事務裡面執行,但是為了通過一次傳送所有命令來減少通訊次數並降低延遲值,使用者也可能會將命令包裹在multi和exec裡面執行。遺憾的是,multi和exec並不是免費的:它們也會消耗資源,並且可能會導致其他重要的命令被延遲執行。不過好訊息是,我們實際上可以在不使用multi和exec的情況下,獲得流水線代理的所有好處。之前章節使用了一下語句來在Python中執行multi和exec命令:

pipe=conn.pipeline()

如果使用者在執行pipeline()時傳入True作為引數,或者不傳入任何引數,那麼客戶端將使用multi和exec包裹起使用者要執行的所有命令。另一方面,如果使用者在執行pipeline()時傳入False為引數,那麼客戶端同樣會像執行事務那樣收集起使用者要執行的所有命令,只是不再使用multi和exec包裹這些命令。如果使用者需要向Redis傳送多個命令,並且對於這些命令來說,一個命令的執行結果並不會影響另一個命令的輸入,而且這些命令也不需要以實物的方式來執行的話,那麼我們可以通過向pipeline()方法傳入False來進一步提升Redis的整體效能。讓我們來看一個這方面的例子。

前面章節曾經編寫並更新過一個名為update_token()函式,它負責記錄使用者最近瀏覽過的商品以及使用者最近訪問過的頁面,並更新使用者的登入cookie。下面程式碼是之前展示過得更新版update_token()函式,這個函式每次執行都會呼叫2個或者5個Redis命令,使得客戶端和Redis之間產生2次或5次通訊往返。

import time


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)
        #移除舊商品,只記錄最新瀏覽的25件商品
        conn.zremrangebyrank(`viewed:`+token,0,-26)
        #更新給定商品的被瀏覽慈善
        conn.zincrby(`viewed:`,item,-1)

如果Redis和Web伺服器通過區域網進行連線,那麼他們之前的每次通訊往返大概需要耗費一兩毫秒,因此需要進行2次或者5次通訊往返的update_token()函式大概需要花費2~10毫秒來執行,按照這個速度計算,單個Web伺服器執行緒每秒可以處理100~500個請求,儘管這種速度已經非常可觀了,但是我們還可以在這個速度的基礎上更新一步:通過修改update_token()函式,讓它建立一個非事務型流水線,然後使用這個流水線來傳送所有請求,這樣我們就的帶了下面程式碼:

import time


def update_token_pipeline(conn,token,user,item=None):
    #獲取時間戳
    #設定流水線
    pipe=conn.pipeline(False)
    timestamp=time.time()
    #建立令牌和已登陸使用者之間的對映
    conn.hset(`login:`,token,user)
    #記錄令牌最後一次出現的時間
    conn.zadd(`recent:`,token,timestamp)
    if item:
        #把使用者瀏覽過的商品記錄起來
        conn.zadd(`viewed:`+token,item,timestamp)
        #移除舊商品,只記錄最新瀏覽的25件商品
        conn.zremrangebyrank(`viewed:`+token,0,-26)
        #更新給定商品的被瀏覽慈善
        conn.zincrby(`viewed:`,item,-1)
    pipe.execute()

通過將標準的Redis連線替換成流水線連線,程式可以將通訊往返的次數減少至原來的1/2到1/5,並將update_token_pipeline()函式的預期執行時間降低1~2毫秒。按照這個速度來計算的話,如果一個Web伺服器只需要執行update_token_pipeline()來更新商品的瀏覽資訊,那麼這個Web伺服器每秒可以處理500~1000個請求。從理論上來看,update_token_pipeline() 函式的效果非常棒,但是它的實際執行速度又是怎樣的呢?

為了回答這個問題,我們將對update_token()函式和update_token_pipeline()函式進行一些簡單的測試。我們將分別通過快速低延遲網路和慢速高延遲網路來訪問同一臺機器,並測試執行在機器上面的Redis每秒可以處理的請求數量。下面程式碼展示了效能測試函式,這個函式會在給定的時限內重複執行update_token()函式或者update_token_pipeline()函式,然後計算被測試的函式每秒執行了多少次。

import time


def benchmark_update_token(conn,duration):
    #測試會分別執行update_token函式和update_token_pipeline函式
    for function in (update_token,update_token_pipeline):
        #設定計數器以及測試結束的條件
        count=0
        start=time.time()
        end=start+duration
        while time.time()<end:
            count+=1
            #呼叫兩個函式的其中一個
            function(conn,`token`,`user`,`item`)
            #計算函式的執行時長
        delta=time.time()-start
        #列印測試結果
        print(function.__name__+":"+str(count)+","+str(delta)+","+str(count/delta))

下面展示了在不同寬頻以及不同延遲值的網路上執行效能測試函式所得到的資料。

在不同型別的網路上執行流水線和非流水線連線:對於高速網路,測試程式幾乎達到了單核處理器可以編碼/解碼Redis命令的極限;而對於低俗網路,測試程式的執行則受到網路頻寬和延遲值的影響

描述 頻寬 延遲值 每秒呼叫update_table()的次數 每秒呼叫update_table_pipeline()的次數
本地伺服器,Unix域套接字 大於1Gb(gigabit) 0.015ms 3761 6394
本地伺服器,本地連線 大於1Gb 0.015ms 3257 5991
遠端伺服器,共享交換機 1Gb 0.271ms 739 2841
遠端伺服器,通過VPN連線 1.8Mb(megabit) 48ms 3.67 18.2

根據上表資料顯示,高延遲網路使用流水線時的速度要比不使用流水線時的速度快5倍,低延遲網路使用流水線也可以帶來接近4倍的速度提升,而本地網路的測試結果實際上已經達到了Python在單核環境下使用Redis協議傳送和接受短命令序列的效能極限了。

現在我們已經知道如何在不使用事務的情況下,通過使用流水線來提示Redis的效能了,那麼除了流水線之外,還有其他可以提升Redis效能的常規方法嗎?

上一篇文章:Python–Redis實戰:第四章:資料安全與效能保障:第6節:Redis事務
下一篇文章: Python–Redis實戰:第四章:資料安全與效能保障:第8節:關於效能方面的注意事項

相關文章