1. 程式人生 > 其它 >Redis複習筆記-進階篇

Redis複習筆記-進階篇

Redis複習筆記-進階篇

釋出訂閱模式

訂閱頻道

訊息的生產者和消費者是不同的客戶端,在Redis中通過channel(頻道)模型進行關聯。訂閱者可以訂閱多個channel,訊息的釋出者可以給指定的channel釋出訊息,只要有訊息到達了channnel,所有訂閱了這個channel的訂閱者都會收到這條訊息。

subscribe channel-1 channel-2 channel-3
//一次訂閱多個頻道

publish channel-1 2673
//釋出者可以向指定頻道釋出訊息(並不支援一次向多個頻道傳送訊息,可以在業務程式碼中新增多條)

unsubscribe channel-1
//取消訂閱(不能在訂閱狀態下使用)

按規則(Pattern)訂閱頻道:

支援 ?和 * 佔位符。?代表一個字元,* 代表0個或者多個字元。

psubscribe *sport  
//適配所有以sport結尾的頻道名稱

一般來說,考慮到效能和持久化因素,不建議使用Redis的釋出訂閱功能來實現MQ。Redis的一些內部機制用到了釋出訂閱功能。


Redis事務

Redis單個命令是原子性的,但是為了確保多個命令作為一個不可分割的處理序列,就需要使用Redis事務。Redis事務具有三個特點:

  1. 按照進入佇列的順序執行
  2. 不會受到其他客戶端的請求的影響
  3. 事務不能巢狀,多個multi命令效果一樣
multi	//開啟事務
exec	//執行事務,在exec沒有被呼叫時,所有佇列中的命令都不會被執行
discard //取消事務,清空任務佇列,放棄執行
watch	//監視

為了防止事務過程中某個key的值被其他客戶端請求修改,帶來非預期的結果,在Redis中就提供了一個watch命令。用於多個客戶端更新變數的時候,跟原值作比較,只有它沒有被其他執行緒修改的情況下,才更新為新的值。它可以為Redis事務提供CAS樂觀鎖行為。

可以使用watch命令監視一個或者多個key,如果事務開啟之後,至少有一個被監視的key在exec執行之前被修改了,那麼整個事務都會被取消(key提前過期除外)。可以使用unwatch進行取消。

在multi命令之前先對要watch的key進行監視,然後開啟事務,exec命令開始執行,如果返回nil,則代表監視的值已經被其他客戶端進行了修改,事務取消。

事務可能遇到的問題

第一種,在執行exec之前發生錯誤:

一般是命令存在語法錯誤,編譯器出錯。事務會被拒絕執行,也就是佇列中所有的命令都不會得到執行。

第二種,在執行exec之後發生錯誤:

可能是型別出錯,但是隻有這條命令沒有被執行,對於其餘命令並沒有影響。

為什麼沒有回滾機制?

無論是哪種錯誤都不應該發生在生產環境內,回滾也不能解決程式碼的問題。


Lua 指令碼

一種輕量級指令碼語言,使用C來編寫,跟資料的儲存過程有點類似。

優勢:

  • 一次傳送多條命令,減少網路開銷
  • Redis會將整個指令碼作為一個整體執行,不會被其他請求打斷,保持原子性
  • 對於複雜的組合命令,可以放在檔案中,實現命令複用

呼叫Lua指令碼:

在Lua指令碼中呼叫Redis命令:

redis.call(command,key,[param1 , param2 ...])
//command 是命令
//key 是被操作的鍵
//param1,param2 代表給key的引數

例子:執行 set yang 21 就應該編寫命令:eval "return redis.call('set','yang','21') 0"

通常我們會把Lua指令碼放在檔案裡面,然後執行這個檔案。

Lua指令碼檔案:

快取Lua指令碼:

Lua指令碼如果比較長的時候,如果每次呼叫指令碼都需要將整個指令碼傳給Redis服務端,就會產生較大的網路開銷,為了解決這個問題,Redis可以快取Lua指令碼並生成SHA1摘要碼,後面可以直接通過摘要碼來執行Lua指令碼。

快取方式:

script load "return 'Hello World'"
//使用script load命令在服務端快取lua指令碼並生成一個摘要碼

evalsha "437fda89fwe89f8s982323jh1283s382" 0
//通過摘要碼執行快取的指令碼

指令碼超時:

由於Redis的指令執行本身是單執行緒的,如果執行Lua指令碼超時或者進入了死迴圈,就沒有辦法繼續提供服務了。

每個指令碼預設有一個超時時間為5s,與配置檔案中配置項:lua-time-limit 5000有關,超過5s,其他客戶端的命令便不會等待,直接返回"BUSY"錯誤。當然也不可以一直拒絕其他客戶端的命令。Redis還提供了命令可以使用:

script kill
//終止指令碼的執行,並不是所有指令碼都可以kill,那些對Redis的資料進行了修改(SET,DEL等)是不可以通過這種方式來停止指令碼執行的。如果使用了這種方式,則會返回UNKILLABLE錯誤。遇到這種情況,只能通過shutdown nosave命令,該操作不會進行持久化操作,意味著發生在上一次快照後的資料庫修改全部都會被丟失。

為什麼這麼設計?為什麼包含修改的指令碼不能中斷?

因為要保證指令碼執行的原子性,如果指令碼執行了一部分就被終止,那就違背了指令碼原子性的目標。


為什麼Redis這麼快?

根據實際測試,Redis的QPS在十萬左右,在高效能的伺服器上效能還能更強。

Redis這麼快的原因總結:

  • 純記憶體結構,採用了hashtable實現的KV結構的記憶體資料庫,時間複雜度為O(1)

  • 請求處理單執行緒:單執行緒指的是處理客戶端請求是單執行緒的,可以將它稱作主執行緒。4.0版本後還引入了一些執行緒處理其他的事情,比如清理髒資料,無用連線的釋放,大key的刪除等。單執行緒的好處是:1. 沒有建立執行緒,銷燬執行緒帶來的效能損耗 2. 避免了上下文切換導致的CPU消耗 3. 避免了執行緒之間帶來的競爭問題。

  • 多路複用機制:使用了多路複用處理併發連線

在Redis中單執行緒已經夠用了,Redis的瓶頸不在CPU上,更有可能是在記憶體或者網路頻寬上。也因為如此,不要在生產環境上執行長命令:比如:keys *,flushall,flushdb等,否則會導致請求被阻塞。

具體的多路複用以及單執行緒相關的底層知識點就不再這裡敘述了,體量太大了,理解即可。


記憶體回收

過期策略

  • 立即過期(主動淘汰):每個設定過期時間的key都需要建立一個定時器,到過期時間就會立即清除,該策略可以立即清除過期的資料,對記憶體很友好,但是會佔用大量的CPU資源去處理過期的資料,從而影響快取的響應時間和吞吐量。
  • 惰性過期(被動淘汰):只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對記憶體不友好,極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,佔用了大量記憶體。
  • 定期過期 :每隔一定的時間,會掃描一定數量的資料庫的expires字典中一定數量的key,並清除其中已過期的key。該策略是前兩者的一個折衷方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和記憶體資源達到最優的平衡效果。

如果所有的key都沒有設定過期屬性,Redis記憶體滿了怎麼辦?

淘汰策略

在記憶體使用達到最大記憶體極限時,需要使用淘汰演算法來決定清理掉哪些資料,以保證新資料的存入。

  • 最大記憶體設定:redis.conf 引數配置:#maxmemory<bytes> 如果不設定maxmemory或者設定為0,32位系統最多使用3GB記憶體,64位系統不限制記憶體。也可以通過config動態修改:config set maxmemory 2GB
  • 淘汰策略:通過配置 maxmemory-policy 進行決定使用哪種策略:

**LRU : **Least Recently Used:最近最少使用,判斷最近被使用的時間,目前最遠的資料優先被淘汰。

LFU :Least Frequently Used: 最不常用,按照使用頻率刪除,4.0版本增強。

random : 隨機刪除。

config set maxmemory-policy xxxxxx-xxx
//動態修改淘汰策略

如果沒有設定ttl或者沒有符合前提條件的key被淘汰,那麼volatile-lru,volatile-random,volatile-ttl相當於noeviction(不做記憶體回收)

建議使用volatile-lru,在保證正常服務的情況下,優先刪除最近最少使用的key。

LRU 淘汰原理

LRU是一個很常見的演算法,比如InnoDB的Buffer Pool 也用到了LRU。

**傳統的LRU : **通過連結串列+HashMap實現,設定連結串列長度,如果新增或者被訪問,就移動到頭節點,超過連結串列長度,末尾的節點被刪除。

**Redis LRU : **對傳統的LRU演算法進行了改良,通過隨機採用來調整演算法的精度。如果淘汰策略使用的是LRU,那麼就會從配置檔案中讀取配置的取樣個數:maxmemory_samples(預設是5個),然後隨機從資料庫中選擇讀到個數個key,淘汰其中熱度最低的key對應的快取資料。所以取樣數配置的越大,就越能精確的查詢到待淘汰的快取資料,但是也消耗更多的CPU計算,執行效率降低。

如何找出熱度最低的資料?

Redis中所有物件結構都有一個lru欄位,且使用了unsigned 的低24位,這個欄位用來記錄物件的熱度。物件被建立時會記錄lru值。在被訪問的時候會更新lru的值。但並不是獲取系統當前的時間戳,而是設定為全域性變數server.lrulock的值。

server.lrulock的值是怎麼來的?

Redis中有個定時處理的函式serverCron,預設每100毫秒呼叫函式updateCachedTime更新一次全域性變數sever.lrulock的值,它記錄的是當前unix的時間戳。

為什麼不獲取精確的時間而是放在全域性變數中呢?

這樣函式查詢key呼叫lookupKey中更新資料的lru熱度值時,就不用每次呼叫系統時間函式Time,可以調高執行效率。

評估熱度:

函式評估指定物件的lru熱度,方法就是物件的lru的值和全域性的server.lrulock的差值越大(越久沒有得到更新),該物件熱度越低。server.lrulock只有24位,按秒為單位來表示才能儲存194天,當超過24bit能表示的最大值時,就會從頭開始計算。在這種情況下,可能會出現物件的lru大於server.lrulock情況,那麼就將兩個相加而不相減來求最久的key。

為什麼不使用傳統的LRU實現呢?

需要額外的資料結構,消耗資源。而Redis LRU演算法在取樣數為10的時候,已經能接近傳統的LRU演算法了。

除了消耗資源之外,傳統的LRU還存在什麼問題?

因為傳統LRU,沒有做隨機取樣,所以有可能訪問頻率高但是最近一次訪問沒有訪問頻率低的最後一次訪問時間近。而結果就是將訪問頻率高的key刪除了。

LFU 淘汰原理

當Redis使用LFU淘汰策略時,原本用於記錄LRU熱度的欄位LRU_BITS這24 bits將被分為兩個部分:

  • 高16位用來記錄訪問時間(單位為分鐘,ldt: last decrement time)
  • 低8位用來記錄訪問頻率,簡稱counter(logc: logistic counter)

counter 是用基於概率的對數計數器實現的,8位可以表示百萬次的訪問頻率。物件被讀寫的時候,lfu的值會被更新。

這裡增長並不是訪問一次就加一,增長的速率由一個引數決定,lfu-log-factor 越大,counter的增長就越慢。而這個引數是通過配置檔案來決定的:

# lfu-log-factor 10

如果一段時間熱度高,就一直保持這個熱度也是不行的。體現不了整體頻率,所以,沒有訪問的時候,計數器需要遞減。減少的值由衰減因子:lfu-decay-time(分鐘)來控制,如果值為1,N分鐘沒有訪問,計數器就需要減少N。衰減因子越大,衰減就越滿。可以通過配置項進行配置衰減因子大小:

# lfu-decay-time 1

持久化機制

RDB 快照 (Redis DataBase)

RDB是Redis預設的持久化方案(如果開啟了AOF,優先使用AOF)。當滿足一定的條件的時候,會把當前記憶體種的資料寫入磁碟,生成一個快照檔案dump.rdb。Redis重啟的時候會載入這個檔案來恢復資料。

什麼時候寫入rdb檔案?

  • 自動觸發: 配置規則觸發,在redis.conf,SNAPSHOTTING,其中定義了觸發把資料儲存到磁碟的觸發頻率。

    save 900 1 #900秒內至少有一個key被修改
    save 300 10 #300秒內至少有10個key被修改
    save 60 10000 #60秒內至少有10000個key被修改
    
    #注意以上三個規則不衝突,同時生效
    

    如果不需要使用rdb方案,就將save註釋或者配置成空字串""。

    使用lastsave命令可以檢視最近一次成功生成快照的時間。

    除了配置觸發生成RDB,還有兩種自動的觸發方式:

    1. shutdown 觸發,保證伺服器正常關閉
    2. flushall ,rdb檔案是空的,沒什麼意義
  • 手動觸發:如果我們需要重啟伺服器服務或者遷移資料,這個時候就需要手動觸發RDB快照儲存,Redis提供了兩條命令:
    1. **save : ** 在生成快照的時候會阻塞伺服器,Redis不能處理其他命令,如果記憶體中的資料比較多,會造成Redis長時間的阻塞。生產環境不建議使用這個命令。
    2. **bgsave : ** Redis會在後臺非同步進行快照操作,同時還可以相應客戶端請求。具體操作是:Redis程序執行fork操作建立子程序(copy-on-write),RDB持久化過程由子程序負責,完成後自動結束。不會記錄fork之後產生的資料,阻塞只發生在fork階段,一般時間很短。

優勢和劣勢

優勢:

  1. RDB是一個十分緊湊的檔案,它儲存了redis在某個時間點上的資料集。這種檔案十分適合用於進行備份和災難恢復。
  2. 生成RDB檔案的時候,redis主程序會fork()一個子程序來處理所有的儲存工作,主程序不需要進行任何磁碟IO操作。
  3. RDB在恢復大資料集時的速度比AOF恢復的速度要快。

**劣勢: **

  1. RDB方式資料沒辦法做到實時持久化/秒級持久化。每次bgsave時執行都要執行fork操作建立子程序,頻繁執行成本過高。
  2. 在一定間隔時間做一次備份,所以如果redis意外宕機的話,就會丟失最後一次快照之後的所有修改。

如果資料相對來說比較重要,希望將損失降到最小,則可以使用AOF方式進行持久化!


AOF (Append Only File)

Redis預設不開啟。AOF採用日誌的形式來記錄每個寫操作,並追加到檔案中。開啟以後,執行更改Redis資料的命令時,就會把命令寫入到AOF檔案中。Redis重啟時會根據日誌檔案的內容把記錄的指令從前到後執行一次以完成對資料的恢復工作。

**AOF配置 : **

# 開關
appendonly no
# 檔名
appendfilename "appendonly.aof"

資料都是實時持久化到磁碟嗎?

由於作業系統的快取機制,AOF資料並沒有真正地寫入硬碟,而是進入了系統的硬碟快取。什麼時候把緩衝區的內容寫入到AOF檔案?

引數 說明
appendfsync everysec AOF 持久化策略(硬碟快取到磁碟),預設everysec
no 表示不執行fsync,由作業系統保證資料同步到磁碟,速度最快,但是不太安全
always 表示每次寫入都執行fsync,以保證資料同步到磁碟,效率很低。
everysec 表示每秒執行一次fsync,可能會丟失這1s的資料,兼顧安全行和效率。(通常選擇這個)

檔案越來越大,怎麼辦?

為了解決這個問題,Redis新增了重寫機制。當AOF檔案大小超過了所設定的閾值,Redis就會啟動AOF檔案的內容壓縮,只保留可以恢復資料的最小指令集。

也可以使用命令bgrewriteaof來重寫,AOF檔案重寫並不是對原檔案進行重新整理,而是直接讀取服務現有的鍵值對,然後用一條命令去代替之前記錄這個鍵值對的多條命令,生成一個新的檔案後去替換原來的AOF檔案。

# 重寫觸發機制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

重寫過程中,AOF檔案被更改了怎麼辦?

另外,在配置檔案中有兩個與AOF相關的引數:

優勢和劣勢

**優勢: **

  1. AOF持久化的方法提供了很多種的同步頻率,即使使用預設的同步頻率每秒同步一次,Redis最多也就丟失1s的資料。

**劣勢: **

  1. 對於具有相同資料的Redis,AOF檔案通常會比RDF檔案體積更大(RDB存的時資料快照)。
  2. 雖然AOF提供了多種同步頻率,預設情況下,每秒同步一次的頻率也需要比較高的效能。在高併發情況下,RDB比AOF具有更好的效能保證。