Redis In Action 筆記(四):資料安全和效能優化

tsin發表於2019-06-23

文中的例子均使用python編寫

持久化資料到磁碟

  • RDB 快照(snapshotting)
  • AOF(append-only file)

    作用:用於恢復資料,儲存計算屬性等

    快照持久化

    選項

    save 60 1000 # 60秒有1000次寫入觸發
    stop-writes-on-bgsave-error no
    rdbcompression yes
    dbfilename dump.rdb  # 儲存的檔名
    dir ./    # 檔案路徑

    建立快照方法

  • 客戶端傳送BGSAVE命令(不支援windows)
  • 使用SAVE命令
  • 配置save選項:比如 save 60 10000,表示從最近一次建立快照之後算起,60秒內有10000次寫入,Redis就會觸發BGSAVE命令
  • Redis接到SHUTDOWN/TERM命令時,會執行一個SAVE命令
  • Redis之間複製的時候(參考4.2節)

    系統崩潰後,會丟失最近一次快照生成之後的資料,因此適用於丟失一部分資料也無所謂的情況。(不能接受資料丟失,則使用AOF)

    每GB的資料,大概耗時10-20ms,資料較大時會造成Redis停頓,可以考慮關閉自動儲存,手動傳送BGSAVE或SAVE來持久化

    SAVE命令不需要建立子程式,效率比BGSAVE高,可以寫一個指令碼在空閒時候生成快照(如果較長時間的資料丟失可以接受)

    AOF 持久化

    將被執行的命令寫到AOF檔案末尾,因此AOF檔案記錄了資料發生的變化,只要重新執行一次AOF檔案中的命令,就可以重建資料

    選項

    appendonly no  # 是否開啟AOF
    # 同步頻率選項:
    # 1. no(由作業系統決定),
    # 2. everysec(每秒,預設,和不開啟持久化效能相差無幾),
    # 3. always(每個命令,產生大量寫操作,固態硬碟慎用,會大大降低硬碟壽命)
    appendfsync everysec 
    
    # 重寫AOF的時候是否阻塞append操作
    no-appendfsync-on-rewrite no
    
    # 自動執行配置:檔案大於64mb且比上一次重寫之後至少大了一倍時執行
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb

    重寫/壓縮AOF檔案

    • 手動執行:傳送BGREWRITEAOF來移除冗餘命令,與BGSAVE原理相似,會建立一個子程式來處理
    • 自動執行:配置auto-aof-rewrite-percentage和auto-aof-rewrite-min-size來自動執行

總結:通過持久化,保證系統重啟或者系統崩潰的情況下仍然能保留資料。當系統負載量上升,資料完整性變得越來越重要,這時可考慮Redis的複製特性。

複製

選項配置

  • 確保主伺服器正確設定了dir和dbfilename選項,並且對Redis可寫
  • 設定主伺服器的選項:slaveof,比如slaveof host port
  • 可以手動傳送slaveof no one來終止複製操作,或者slaveof host port來開始從主伺服器複製

新版redis(2.8以後),當主從伺服器中途斷開時,採用增量複製,效率更高,參考:http://copyfuture.com/blogs-details/365aec...

主從鏈(略)

檢驗磁碟寫入

  • 檢驗是否同步成功:通過主伺服器構造一個唯一標識,檢驗是否同步到從伺服器
  • 從伺服器檢驗是否成功持久化到磁碟:對於每秒同步一次的AOF檔案,可以通過等待一秒或者檢驗aof_pending_bio_fsync等於0來判斷寫入成功

實現程式碼和詳解:

import redis
import time
mconn = redis.Redis(host='127.0.0.1', port=6379)
sconn = redis.Redis(host='127.0.0.1', port=6380)
# mconn 主伺服器連線物件
# sconn 從伺服器連線物件
def wait_for_sync(mconn, sconn):
    # 生成一個唯一標識
    identifier = str(uuid.uuid4())
    # 將這個唯一標識(令牌)新增到主伺服器
    mconn.zadd('sync:wait', identifier, time.time())

    # 等待主從複製成功
    # master_link_status = up 代表複製成功
    # 等同於 while sconn.info()['master_link_status'] = 'down'
    # 即同步為完成時,繼續等待,每隔1ms判斷一次
    while not sconn.info()['master_link_status'] != 'up':
        time.sleep(.001)

    # 從伺服器還沒有接收到主伺服器的同步時(無identifier),繼續等待
    while not sconn.zscore('sync:wait', identifier):
        time.sleep(.001)

    dealine = time.time() + 1.01 # 最多隻等待1s

    while time.time() < dealine:
        # 注意AOF開啟時,才有aof_pending_bio_fsync選項
        # 等於0說明沒有fsync掛起的任務,即寫入磁碟已完成
        if sconn.info()['aof_pending_bio_fsync'] == 0:
            break
        time.sleep(.001)

    # 從伺服器同步到磁碟後完成後,主伺服器刪除該唯一標識
    mconn.zrem('sync:wait', identifier)
    # 刪除15分鐘前可能沒有刪除的標識
    mconn.zremrangebyscore('sync.wait', time.time() - 900)

處理系統故障

檢驗快照檔案和AOF檔案

  • redis-check-aof [--fix] ,會刪除出錯命令及其之後的命令
  • redis-check-dump 出錯無法修復,最好多備份

更換故障伺服器

假設有A、B兩臺Redis伺服器,A為主,B為從,A機器出現故障,使用C作為新的伺服器。更換方法:向B傳送一個SAVE命令建立快照檔案,傳送給C,最後讓B成為C的從伺服器。此外,還要更新客戶端配置,讓程式讀寫正確的伺服器。

Redis事務

Redis處理事務的命令:MULTI、EXEC、DISCARD、WATCH、UNWATCH。
與傳統關係型資料庫的事務之區別:傳統關係型資料庫事務:BEGIN-->執行操作-->COMMIT確認操作-->出錯時可以ROLLBACK;Redis事務:MULTI開始事務-->新增多個命令-->EXEC執行,EXEC之前不會有任何實際操作。

例子

遊戲網站的商品買賣市場,玩家可以在市場裡銷售和購買商品。

資料結構

  • 使用者資訊
    hash,記錄使用者名稱和餘額

  • 存量
    set,記錄包含的商品編號

  • 市場
    zset,商品名.擁有者-->價格

Redis In Action 筆記(四):資料安全和效能優化

Redis In Action 筆記(四):資料安全和效能優化

實現邏輯

不同於傳統關係型資料庫,事務操作的時候會對資料進行加鎖,Redis事務操作只會在資料被其他客戶端搶先修改的情況下,通知執行了WATCH命令的客戶端,這時事務操作失敗,客戶端可以選擇重試或者中斷操作——這種做法稱之為樂觀鎖。

  • 連線

    import redis
    import time
    conn = redis.Redis(host='127.0.0.1', port=6379)
  • 將商品放到市場上銷售

    def list_item(conn, itemid, sellerid, price):
    inventory = "inventory:%s"%sellerid  # inventory key
    item = "%s.%s"%(itemid, sellerid)    # item key
    end = time.time() + 5
    pipe = conn.pipeline()
    while time.time() < end:
        try:
            pipe.watch(inventory) # 監視庫存變化                    
            if not pipe.sismember(inventory, itemid): # 如果庫存中沒有該商品
                pipe.unwatch()   # 取消監控                    
                return None
            pipe.multi()  # 開啟事務                            
            pipe.zadd("market:", item, price)  # 新增商品到市場      
            pipe.srem(inventory, itemid)       # 從庫存中刪除商品      
            pipe.execute()                     # 執行事務      
            return True
        // WATCH和EXEC之間所監控的inventory已經發生變化
       // 這時事務執行失敗,丟擲WatchError
        // 這裡不做任何處理,5s內會繼續while迴圈
        except redis.exceptions.WatchError:          
            pass                                     
    return False
  • 購買商品

    def purchase_item(conn, buyerid, itemid, sellerid, lprice):
    buyer = "users:%s"%buyerid            # 當前買家
    seller = "users:%s"%sellerid          # 當前賣家
    item = "%s.%s"%(itemid, sellerid)     # 市場market上商品的key
    inventory = "inventory:%s"%buyerid    # 買家使用者商品庫存
    end = time.time() + 10
    pipe = conn.pipeline()
    
    while time.time() < end:
        try:
            pipe.watch("market:", buyer)  # 監控當前市場和當前買家            
    
            price = pipe.zscore("market:", item)    # 商品價格    
            funds = int(pipe.hget(buyer, "funds"))  # 當前買家餘額   
            if price != lprice or price > funds:    # 當前價格是否發生變化或買家餘額不足  
                pipe.unwatch()                      # 取消監控   
                return None                         # 購買失敗
    
            pipe.multi()    # 開啟事務                           
            pipe.hincrby(seller, "funds", int(price))  # 賣家餘額增加
            pipe.hincrby(buyer, "funds", int(-price))  # 買家餘額減少
            pipe.sadd(inventory, itemid)               # 將新增該商品到買家庫存
            pipe.zrem("market:", item)                 # 刪除市場中的該商品
            pipe.execute()                             # 執行事務
            return True                                # 成功完成一次買賣過程     
    
        // WATCH失敗,即在WATCH和EXEC之間監控的KEY發生了改變
        // 10s內會繼續while迴圈重試
        except redis.exceptions.WatchError:            
            pass                                       
    return False  # 購買失敗

非事務型流水線(pipeline)

需要執行大量操作且不需要事務的時候(事務會消耗資源)
pipe = conn.pipeline()傳入True或者不傳入引數,表示事務型操作

將2.5節的update_token改造為非事務型流水線操作

  • 改造前

    # 需要2-5次通訊往返
    # 假如每次通訊耗時2毫秒,則執行一次update_token要4-10毫秒
    # 那麼每秒可以處理的請求數為100-250次
    def update_token(conn, token, user, item=None):
    timestamp = time.time()                            
    conn.hset('login:', token, user)        # 1           
    conn.zadd('recent:', token, timestamp)  # 2           
    if item:
        conn.zadd('viewed:' + token, item, timestamp)   # 3
        conn.zremrangebyrank('viewed:' + token, 0, -26) # 4
        conn.zincrby('viewed:', item, -1)               # 5
  • 改造後

    # 只要一次通訊
    # 通訊往返次數減少到原來的1/2-1/5
    # 每秒處理請求數可以到500次
    def update_token_pipeline(conn, token, user, item=None):
    timestamp = time.time()
    pipe = conn.pipeline(False)  # 非事務型流水線操作                        
    pipe.hset('login:', token, user)
    pipe.zadd('recent:', token, timestamp)
    if item:
        pipe.zadd('viewed:' + token, item, timestamp)
        pipe.zremrangebyrank('viewed:' + token, 0, -26)
        pipe.zincrby('viewed:', item, -1)
    pipe.execute()   # 執行新增的所有命令   

效能測試及注意事項

  • 測試命令:redis-benchmark -c 1 -q

    -q 表示簡化輸出結果
    -c 1 表示使用一個客戶端

結果大概如下所示:

.
.
.
PING (inline): 34246.57 requests per second
PING: 34843.21 requests per second
MSET (10 keys): 24213.08 requests per second
SET: 32467.53 requests per second
.
.
.

LRANGE (first 100 elements): 22988.51 requests per second
LRANGE (first 300 elements): 13888.89 requests per second
.
.
.
  • 結果分析及解決方法:
效能或錯誤 可能原因 解決方法
單個客戶端效能達到redis-benchmark的50%-60% 不使用pipeline時預期效能
單個客戶端效能達到redis-benchmark的25%-30% 對每個/每組命令都建立了新的連線 重用已有的Redis連線
客戶端錯誤:Cannot assign requested address 對每個/每組命令都建立了新的連線 重用已有的Redis連線

參考資料:

Was mich nicht umbringt, macht mich stärker

相關文章