1. 程式人生 > 其它 >redis-常見問題

redis-常見問題

一、redis可以用來做訊息佇列麼

redis可以做訊息佇列,可以利用list 和 streams 兩個方案比較如下圖所示

BRPOP:堵塞讀取,不需要一直輪詢獲取資料

BRPOPLPUSH:是讓消費者程式從一個 List 中讀取訊息,同時,Redis 會把這個訊息再插入到另一個 List(可以叫作備份 List)留存。這樣一來,如果消費者程式讀了訊息但沒能正常處理,等它重啟後,就可以從備份 List 中重新讀取訊息並進行處理了。(我理解如果消費成功還需要手動維護刪除消費成功的訊息,要不訊息堆積太多,也難也分辨出哪些是消費成功的 哪些是消費失敗的)而streams則是通過XACK確認訊息成功消費後,自動刪除,比較優雅

基於List,如果生產者訊息傳送很快,但是消費者處理訊息的速度比較慢,這就導致 List 中的訊息越積越多,給 Redis 的記憶體帶來很大壓力。所以不太建議用list

基於Stream,可以利用消費組下多個消費者來一起消費訊息

二、為什麼不建議用redis做訊息佇列

一、無法保證資料的完整性(容易丟失訊息

1、生產者在釋出訊息時異常:
a) 網路故障或其他問題導致釋出失敗(直接返回錯誤,訊息根本沒發出去)
b) 網路抖動導致釋出超時(可能傳送資料包成功,但讀取響應結果超時了,不知道結果如何)
情況a還好,訊息根本沒發出去,那麼重新發一次就好了。但是情況b沒辦法知道到底有沒有釋出成功,所以也只能再發一次。所以這兩種情況,生產者都需要重新發布訊息,直到成功為止(一般設定一個最大重試次數,超過最大次數依舊失敗的需要報警處理)。這就會導致消費者可能會收到重複訊息的問題,所以消費者需要保證在收到重複訊息時,依舊能保證業務的正確性(設計冪等邏輯),一般需要根據具體業務來做,例如使用訊息的唯一ID,或者版本號配合業務邏輯來處理。
2、消費者在處理訊息時異常:
也就是消費者把訊息拿出來了,但是還沒處理完,消費者就掛了。這種情況,需要消費者恢復時,依舊能處理之前沒有消費成功的訊息。使用List當作佇列時,也就是利用備份佇列來保證,代價是增加了維護這個備份佇列的成本。而Streams則是採用ack的方式,消費成功後告知中介軟體,這種方式處理起來更優雅,成熟的佇列中介軟體例如RabbitMQ、Kafka都是採用這種方式來保證消費者不丟訊息的。
3、訊息佇列中介軟體丟失訊息
上面2個層面都比較好處理,只要客戶端和服務端配合好,就能保證生產者和消費者都不丟訊息。但是,如果訊息佇列中介軟體本身就不可靠,也有可能會丟失訊息,畢竟生產者和消費這都依賴它,如果它不可靠,那麼生產者和消費者無論怎麼做,都無法保證資料不丟失。
a) 在用Redis當作佇列或儲存資料時,是有可能丟失資料的:一個場景是,如果開啟AOF並且是每秒寫盤,因為這個寫盤過程是非同步的,Redis宕機時會丟失1秒的資料

。而如果AOF改為同步寫盤,那麼寫入效能會下降。另一個場景是,如果採用主從叢集,如果寫入量比較大,從庫同步存在延遲,此時進行主從切換,也存在丟失資料的可能(從庫還未同步完成主庫發來的資料就被提成主庫)。總的來說,Redis不保證嚴格的資料完整性和主從切換時的一致性。我們在使用Redis時需要注意。

b) 而採用RabbitMQ和Kafka這些專業的佇列中介軟體時,就沒有這個問題了。這些元件一般是部署一個叢集,生產者在釋出訊息時,佇列中介軟體一般會採用寫多個節點+預寫磁碟的方式保證訊息的完整性,即便其中一個節點掛了,也能保證叢集的資料不丟失。當然,為了做到這些,方案肯定比Redis設計的要複雜(畢竟是專們針對佇列場景設計的)。

三、redis堵塞點

1. 和客戶端互動時的阻塞點

第一個阻塞點:集合全量查詢和聚合操作(集合的聚合統計操作,例如求交、並和差集,CPU密集型計算,可以使用 SCAN 命令,分批讀取資料,再在客戶端進行聚合計算

第二個阻塞點:bigkey 刪除操作(刪除操作的本質是要釋放鍵值對佔用的記憶體空間。釋放記憶體只是第一步,為了更加高效地管理記憶體空間,在應用程式釋放記憶體時,作業系統需要把釋放掉的記憶體塊插入一個空閒記憶體塊的連結串列,以便後續進行管理和再分配。這個過程本身需要一定時間,而且會阻塞當前釋放記憶體的應用程式,所以,如果一下子釋放了大量記憶體,空閒記憶體塊連結串列操作時間就會增加,相應地就會造成 Redis 主執行緒的阻塞。)

第三個阻塞點:清空資料庫。(涉及到刪除和釋放所有的鍵值對)

刪除操作並不需要給客戶端返回具體的資料結果,所以可以使用後臺子執行緒來非同步執行刪除操作。

2. 和磁碟互動時的阻塞點

第四個阻塞點:AOF 日誌同步寫(一個同步寫磁碟的操作的耗時大約是 1~2ms,如果有大量的寫操作需要記錄在 AOF 日誌中,並同步寫回的話,就會阻塞主執行緒了)

AOF 日誌同步寫”,為了保證資料可靠性,Redis 例項需要保證 AOF 日誌中的操作記錄已經落盤,這個操作雖然需要例項等待,但它並不會返回具體的資料結果給例項。所以,也可以啟動一個子執行緒來執行 AOF 日誌的同步寫,而不用讓主執行緒等待 AOF 日誌的寫完成。

3. 主從節點互動時的阻塞點

第五個阻塞點:載入 RDB 檔案(從庫在清空當前資料庫後,還需要把 RDB 檔案載入到記憶體,這個過程的快慢和 RDB 檔案的大小密切相關,RDB 檔案越大,載入過程越慢,可以把主庫的資料量大小控制在 2~4GB 左右,以保證 RDB 檔案能以較快的速度載入。

4. 切片叢集例項互動時的阻塞點

非同步的子執行緒機制

Redis 主執行緒啟動後,會使用作業系統提供的 pthread_create 函式建立 3 個子執行緒,分別由它們負責 AOF 日誌寫操作、鍵值對刪除以及檔案關閉的非同步執行。

主執行緒通過一個連結串列形式的任務佇列和子執行緒進行互動。當收到鍵值對刪除和清空資料庫的操作時,主執行緒會把這個操作封裝成一個任務,放入到任務佇列中,然後給客戶端返回一個完成資訊,表明刪除已經完成。但實際上,這個時候刪除還沒有執行,等到後臺子執行緒從任務佇列中讀取任務後,才開始實際刪除鍵值對,並釋放相應的記憶體空間(Lazy-free,只有達到一定條件了,在刪除key釋放記憶體時,才會真正放到非同步執行緒中執行,其他情況一律還是在主執行緒操作)。當 AOF 日誌配置成 everysec 選項後,主執行緒會把 AOF 寫日誌操作封裝成一個任務,也放到任務佇列中。後臺子執行緒讀取任務後,開始自行寫入 AOF 日誌,這樣主執行緒就不用一直等待 AOF 日誌寫完了。下面這張圖展示了 Redis 中的非同步子執行緒執行機制

四、如何應對redis變慢

三個因素,自身特性,作業系統,檔案系統

自身特性

1、慢查詢命令(1.用其他高效命令代替。比如說,如果需要返回一個 SET 中的所有成員時,不要使用 SMEMBERS 命令,而是要使用 SCAN 多次迭代返回,避免一次返回大量資料,造成執行緒阻塞。在使用SCAN命令時,不會漏key,但可能會得到重複的key。2.當你需要執行排序、交集、並集操作時,可以在客戶端完成,而不要用 SORT、SUNION、SINTER 這些命令,以免拖慢 Redis 例項。3.keys 會查詢所有鍵值對)

2、過期key操作(以前是同步刪除會造成堵塞,4.0以後是非同步刪除)

Redis 鍵值對的 key 可以設定過期時間。預設情況下,Redis 每 100 毫秒會刪除一些過期 key

具體的演算法如下:

1、取樣 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 個數的 key,並將其中過期的 key 全部刪除;

2、如果超過 25% 的 key 過期了,則重複刪除的過程,直到過期 key 的比例降至 25% 以下。

檔案系統

AOF 日誌提供了三種日誌寫回策略:no、everysec、always。這三種寫回策略依賴檔案系統的兩個系統呼叫完成,也就是 write 和 fsync。

當AOF寫回策略配置為 everysec 時,Redis 會使用後臺的子執行緒非同步完成 fsync 的操作(刷回磁碟)。

在使用 AOF 日誌時,為了避免日誌檔案不斷增大,Redis 會執行 AOF 重寫,Redis 使用子程序來進行 AOF 重寫。但是有一個潛在的風險點:由於 fsync 後臺子執行緒和 AOF 重寫子程序的存在,主 IO 執行緒一般不會被阻塞。但是,如果在重寫日誌時,AOF 重寫子程序的寫入量比較大,fsync 執行緒也會被阻塞,進而阻塞主執行緒(主執行緒會監控子執行緒是否處理完成),導致延遲增加。如下圖所示

解決方案:可以把配置項 no-appendfsync-on-rewrite 設定為 yes 這樣AOF 重寫日誌就不會呼叫fsync,直接返回即可,也就不會影響到主執行緒(風險點:宕機會丟失資料)

作業系統:

1、swap

觸發 swap 的原因主要是物理機器記憶體不足

解決方案:增加機器的記憶體或者使用 Redis 叢集。

2、記憶體大頁

redis採用寫時複製機制,一旦有資料要被修改,Redis 並不會直接修改記憶體中的資料,而是將這些資料拷貝一份,然後再進行修改。

如果採用了記憶體大頁,那麼,即使客戶端請求只修改 100B 的資料,Redis 也需要拷貝 2MB 的大頁。

解決方案:關閉記憶體大頁

9 個檢查點的 Checklist,

1、獲取 Redis 例項在當前環境下的基線效能。

2、是否用了慢查詢命令?如果是的話,就使用其他命令替代慢查詢命令,或者把聚合計算命令放在客戶端做。

3、是否對過期 key 設定了相同的過期時間?對於批量刪除的 key,可以在每個 key 的過期時間上加一個隨機數,避免同時刪除。

4、是否存在 bigkey? 對於 bigkey 的刪除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用非同步執行緒機制減少主執行緒阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代刪除;對於 bigkey 的集合查詢和聚合操作,可以使用 SCAN 命令在客戶端完成。

5、Redis AOF 配置級別是什麼?業務層面是否的確需要這一可靠性級別?如果我們需要高效能,同時也允許資料丟失,可以將配置項 no-appendfsync-on-rewrite 設定為 yes,避免 AOF 重寫和 fsync 競爭磁碟 IO 資源,導致 Redis 延遲增加。當然, 如果既需要高效能又需要高可靠性,最好使用高速固態盤作為 AOF 日誌的寫入盤。

6、Redis 例項的記憶體使用是否過大?發生 swap 了嗎?如果是的話,就增加機器記憶體,或者是使用 Redis 叢集,分攤單機 Redis 的鍵值對數量和記憶體壓力。同時,要避免出現 Redis 和其他記憶體需求大的應用共享機器的情況。

7、在 Redis 例項的執行環境中,是否啟用了透明大頁機制?如果是的話,直接關閉記憶體大頁機制就行了。

8、是否運行了 Redis 主從叢集?如果是的話,把主庫例項的資料量大小控制在 2~4GB,以免主從複製時,從庫因載入大的 RDB 檔案而阻塞。

9、是否使用了多核 CPU 或 NUMA 架構的機器執行 Redis 例項?使用多核 CPU 時,可以給 Redis 例項繫結物理核;使用 NUMA 架構時,注意把 Redis 例項和網路中斷處理程式執行在同一個 CPU Socket 上。

五、記憶體碎片

刪除資料後,為什麼記憶體佔用率還是很高?

這是因為,當資料刪除後,Redis 釋放的記憶體空間會由記憶體分配器管理,並不會立即返回給作業系統。所以,作業系統仍然會記錄著給 Redis 分配了大量記憶體。

而Redis 釋放的記憶體空間可能會有好多不連續的記憶體碎片,所以導致有很多空閒的記憶體,但是存不進去資料

記憶體碎片的形成有內因和外因兩個層面的原因。簡單來說,內因是作業系統的記憶體分配機制,外因是 Redis 的負載特徵

內因:記憶體分配器的分配策略

redis用的jemalloc,jemalloc 的分配策略之一,是按照一系列固定的大小劃分記憶體空間,例如 8 位元組、16 位元組、32 位元組、48 位元組,…, 2KB、4KB、8KB 等。當程式申請的記憶體最接近某個固定值時,jemalloc 會給它分配相應大小的空間。所以就導致有的元素申請的是是20位元組,但是分配器給分配的是32位元組,就會有12位元組的碎片

外因:鍵值對大小不一樣和刪改操作(會導致空間的擴容和釋放)

如何清理記憶體碎片

當有資料把一塊連續的記憶體空間分割成好幾塊不連續的空間時,作業系統就會把資料拷貝到別處。此時,資料拷貝需要能把這些資料原來佔用的空間都空出來,把原本不連續的記憶體空間變成連續的空間,如下圖所示

但是碎片清理是有代價的,作業系統需要把多份資料拷貝到新位置,把原有空間釋放出來,這會帶來時間開銷。因為 Redis 是單執行緒,在資料拷貝時,Redis 只能等著,這就導致 Redis 無法及時處理請求,效能就會降低

可以通過設定引數,來控制碎片清理的開始和結束時機,以及佔用的 CPU 比例,從而減少碎片清理對 Redis 本身請求處理的效能影響。

這兩個引數分別設定了觸發記憶體清理的一個條件,如果同時滿足這兩個條件,就開始清理。在清理的過程中,只要有一個條件不滿足了,就停止自動清理。

active-defrag-ignore-bytes 100mb:表示記憶體碎片的位元組數達到 100MB 時,開始清理;

active-defrag-threshold-lower 10:表示記憶體碎片空間佔作業系統分配給 Redis 的總空間比例達到 10% 時,開始清理。

自動記憶體碎片清理功能在執行時,還會監控清理操作佔用的 CPU 時間,而且還設定了兩個引數,分別用於控制清理操作佔用的 CPU 時間比例的上、下限,既保證清理工作能正常進行,又避免了降低 Redis 效能。這兩個引數具體如下:

active-defrag-cycle-min 25: 表示自動清理過程所用 CPU 時間的比例不低於 25%,保證清理能正常開展;

active-defrag-cycle-max 75:表示自動清理過程所用 CPU 時間的比例不高於 75%,一旦超過,就停止清理,從而避免在清理時,大量的記憶體拷貝阻塞 Redis,導致響應延遲升高。

六、緩衝區

緩衝區分成了客戶端的輸入和輸出緩衝區,以及主從叢集中主節點上的複製緩衝區和複製積壓緩衝區

客戶端輸入和輸出緩衝區主要使用兩類客戶端和 Redis 伺服器端互動,分別是普通客戶端,以及訂閱了 Redis 頻道的訂閱客戶端也就是pub/sub,普通客戶端是堵塞式傳送,訂閱不是堵塞式)溢位會導致網路連線關閉

為了避免客戶端和伺服器端的請求傳送和處理速度不匹配,伺服器端給每個連線的客戶端都設定了一個輸入緩衝區和輸出緩衝區,輸入緩衝區會先把客戶端傳送過來的命令暫存起來,Redis 主執行緒再從輸入緩衝區中讀取命令,進行處理。當 Redis 主執行緒處理完資料後,會把結果寫入到輸出緩衝區,再通過輸出緩衝區返回給客戶端,如下圖

主從叢集中的緩衝區分為複製緩衝區和複製積壓緩衝區:複製緩衝區是增量複製的時候用到了,如果溢位了,會導致主從複製失敗,主節點在把接收到的寫命令同步給從節點時,同時會把這些寫命令寫入複製積壓緩衝區。一旦從節點發生網路閃斷,再次和主節點恢復連線後,從節點就會從複製積壓緩衝區中,讀取斷連期間主節點接收到的寫命令,進而進行增量同步(主從會各自記住自己的偏移量),複製積壓緩衝區是個環形,不會溢位,會覆蓋之前的資料,如果之前的資料從庫還沒來得及同步,就會造成主從節點間重新開始執行全量複製。