1、查看現在redis內存使用情況:使用 info memory 即可查看。
這裏有3個內容我們得注意 : ①:used_memory :物理使用的內存空間(1.97M)。
② :user_memory_rss :操作系統認爲Redis使用的內存空間。這裏是(2.59M)。
③:mem_fragmentstion_ratio:② / ① 的值
> 1 表示內存碎片化嚴重; < 1 說明redis內存存在硬盤化情況
2、要管理內存先看下我們要管理什麼內存,下面是內存的分類:
①:Redis 進程 本身所佔的內存,這部分內存不大,可以暫時不考慮。
②:對象內存 ,這部分是主要的內存。
③:緩存內存,包括客戶端輸入輸出緩存,AOF緩存(重寫的時候寫入命令),部分複製時的緩存(默認1M)
上述的3種內存之和就是 used_memory,那就有問題了,爲什麼會有 操作系統認爲的內存(used_memory_rss)大於真正的內存呢?這裏就牽扯到了redis的內存分配器了,我們可以看到 info memory 的最後一項:men_allocator :jemalloc 。jemalloc是默認的內存分配器,它分配內存的時候按照 小、大、巨大三個範圍分配,舉例說 我們要存儲 5KB的內容,jemalloc分配的內存塊大小可能就是8KB,這時3KB的內存就是內存碎片,這部分不能繼續分配給其他對象。
知道了爲什麼碎片的來源,我們就知道了 used_memory + 碎片內存 = used_memory_rss 。 mem_fragmentstion_ratio 的意義:這個值越大,說明內存空間碎片化越嚴重。
3、知道了有哪些內存,我們就得去考慮怎麼管理這些內存。
主要手段有兩種:控制內存上限 和 控制內存回收策略。
控制內存上限
控制內存上限很簡單,就是限制單點Redis 的最大使用內存量,這裏的最大內存指的是 真正使用的內存量,也就是 used_memory,因爲碎片內存的存在,所以實際消耗的內存量會大於 maxmemory。
設置 maxmemory 參數的值即可, 如動態的修改: config set maxmemory 4GB
內存回收
什麼時候觸發內存回收呢?
這裏有兩種情況:第一是在刪除過期key的時候;第二是內存達到 maxmemory 觸發內存回收策略。
刪除過期key的內存有兩種策略:
①:惰性刪除:過期的key不主動刪除,在客戶端使用這個key 的時候讀取key的過期時間,超時後刪除key並返回空值。這種策略節省了 CPU 的消耗,但是也容易造成內存泄漏,key長時間不用但是一直保存在內存中,所以有了下面的這種刪除過期key的策略。
②:定時任務刪除:默認每秒運行10次定時任務,每次定時任務都會隨機檢查20個鍵,刪除過期的key。定時任務還啓用自適應算法來根據鍵的過期時間比例和快慢兩種模式來回收鍵。
maxmemory-policy 控制的內存回收策略:
①:noeviction:默認策略,當達到內存上限的時候拒絕客戶端寫入,並報錯,但是讀命令還是能進行。
②:volatile-lru:根據LRU(Least Recently Used)刪除 過期 key,當過期key沒有時,回退到 noeviction 。
③:allkeys-lru :根據LRU 刪除 key,直到騰出足夠空間。
④:volatile-random:隨機刪除 過期 key ,直到騰出足夠空間;
⑤:allkeys-random:隨機刪除 key,直到騰出足夠空間
⑥:volatile-ttl:根據鍵值對象的 ttl 屬性 ,刪除最近要過期的key,如果沒有,則回退到 noevicion。
頻繁進行內存的回收成本非常大,主要包括查找符合的可回收鍵和刪除可回收鍵。這裏如果有從節點,還要考慮主節點的刪除會使從節點也進行同步,放大了主節點的寫操作。
4、瞭解了怎麼管理內存,我們下一步就得考慮怎麼優化內存了
①:針對key 和value 的縮減對象鍵值對象大小方法
簡單的來說就是讓key 和value 的長度小一些,在一個key能描述業務的情況下越短越好,如 user:{userId}:name 可以變成 u:{userId}:n 。 針對value 可以去掉不必要存儲的數據,其次可以將value 序列化存儲,採用高效的序列化方式,如protostuff ,kryo等。
②:使用共享對象池
Redis內部維持了一個 【0 ~ 9999】的整數對象池,當數據大量存在 【0 ~ 9999】 的數據的時候共享對象池能節省大量內存。但是這個共享對象池在 redis 使用 maxmemory + LRU 回收策略的時候失效,爲什麼呢?每隔 redis 對象都有一個屬性叫做 lru,它記錄了這個對象最後的使用時間,對象共享意味着 多個 引用共享同一個 對象,導致lru也被共享。如果沒有設置maxmemory,那麼就不會觸發 LRU回收,所以這時共享對象池也是可以使用的。共享對象池只有在 maxmemory + LRU 模式下才會失效。
③:字符串優化
字符串採用簡單動態字符串(SDS)的數據結構,內部存在預分配機制。
預分配機制通俗來說是個什麼回事呢? 在 對字符串操作的時候,具體 刪減字符串長度的時候不釋放刪減的空間,而是當做預分配的空間保留,對 字符串追加 的時候多分配一倍空間當做預分配空間(如 60字節的字符串追加60字節,最後至少240字節大小,這裏多出來的120字節就是預分配空間),那這樣的預分配有啥好處呢?好處就是減少了內存分配的次數。
對於字符串的預分配機制,我們應該小心它帶來的空間浪費。
儘量減少字符串的追加操作,如append 和 setrange,改爲直接使用set修改字符串。
字符串重構,如Json這樣的結構的數據改爲使用如使用 hash結構來存儲,但是使用hash又會遇到一些問題需要注意,下面也會寫到。
④:編碼優化
上圖是Redis 內部編碼的方式,這個編碼類型在數據寫入的時候即完成,隨着數據的變化編碼結構可能變化,但是編碼結構只能從小變大,並且不可逆。其實從編碼結構的變化也可以看做是由時間效率與空間效率的協調。
舉個例子,list 類型 最開始的編碼類型是ziplist,ziplist 的結構是帶有頭尾指針的連續內存空間,可以模擬爲雙向鏈表。存取的時間複雜度爲 O(N^2),在數據量小的時候採用ziplist編碼,這時因爲數據量小即使時間複雜度爲O(N^2)也有較高的時間效率,而ziplist的內存是非常小的,非常適合數據量小的時候使用,而當數據量大的時候,list編碼就會變成linkedlist來提高相應的時間效率,而犧牲空間效率(因爲鏈表的內存佔用肯定比ziplist大)。
對於hash,list,zset 三種我們可以通過指定 {hash / list / zset}-max-ziplist-value (value最大空間)和 {hash / list / zset}-max-ziplist-entries (元素個數)來控制 編碼從 ziplist 到後一種編碼的轉換時機。
而對於set 的編碼優化,對於 整數類型的數據存儲時 set使用intset的編碼存儲,它的內存和寫入效率都非常高。當set類型的長度大於 set-max-intset-entries 或者不是整數類型的時候使用hashtable編碼。所以用set來存儲整數類型非常合適,可以把 set-max-intset-entries 設置大一些。
⑤:控制鍵的數量
使用ziplist 的hash類型來存儲同等數量的 String類型的數據。