文中的例子均使用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事務操作只會在資料被其他客戶端搶先修改的情況下,通知執行了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連線 |
參考資料: