Redis問題收集

命令

set命令

  • EX second :設置鍵的過期時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value
  • PX millisecond :設置鍵的過期時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value
  • NX :只在鍵不存在時,纔對鍵進行設置操作。 SET key value NX 效果等同於 SETNX key value
  • XX :只在鍵已經存在時,纔對鍵進行設置操作。

因爲 SET 命令可以通過參數來實現和 SETNXSETEXPSETEX 三個命令的效果,所以將來的 Redis 版本可能會廢棄並最終移除 SETNXSETEXPSETEX 這三個命令。

redis命令方式設置鎖,可以使用setnx,incr命令,但是這兩個命令還要再設置expire過期時間,防止意外退出,鎖未刪除,所以上述兩種方式都不是原子性的,但是可以使用set nx ex來設置,這樣可以一次性實現設置鎖並設置過期時間

setex案例

18.16.200.68 dev:3>setex address 30 beijing
"OK"
18.16.200.68 dev:3>get address
"beijing"
18.16.200.68 dev:3>ttl address
"23"
18.16.200.68 dev:3>pttl address
"15595"

set ex 案例

18.16.200.68 dev:3>set aa bb ex 30
"OK"
18.16.200.68 dev:3>get aa
"bb"
18.16.200.68 dev:3>ttl aa
"24"
18.16.200.68 dev:3>pttl aa
"21944"

setnx案例

18.16.200.68 dev:3>get aa
null
18.16.200.68 dev:3>setnx aa bb
"1"
18.16.200.68 dev:3>get aa
"bb"
18.16.200.68 dev:3>setnx aa bbb
"0"
18.16.200.68 dev:3>get aa
"bb"

set nx案例

18.16.200.68 dev:3>get aaa
null
18.16.200.68 dev:3>set aaa bbb nx
"OK"
18.16.200.68 dev:3>get aaa
"bbb"
18.16.200.68 dev:3>set aaa ddd nx
null
18.16.200.68 dev:3>get aaa
"bbb"

setnxset nx這兩個可以用於判斷鎖,如果 key 不存在,將 key 設置爲 value
如果 key 已存在,則 SETNX 不做任何動作

set nx ex一次性設置鎖和過期時間

18.16.200.68 dev:3>get lock
null
18.16.200.68 dev:3>set lock true nx ex 30
"OK"
18.16.200.68 dev:3>ttl lock
"25"
18.16.200.68 dev:3>get lock
"true"
18.16.200.68 dev:3>set lock false nx
null
18.16.200.68 dev:3>get lock
"true"
18.16.200.68 dev:3>ttl lock
"5"

incr命令

18.16.200.68 dev:3>get n1
null
18.16.200.68 dev:3>incr n1
"1"
18.16.200.68 dev:3>get n1
"1"
18.16.200.68 dev:3>incr n1
"2"
18.16.200.68 dev:3>get n1
"2"

key 不存在,那麼 key 的值會先被初始化爲 0 ,然後再執行 INCR 操作進行加一。

該命令可用於鎖,其它用戶在執行 INCR 操作進行加一時,如果返回的數大於 1 ,說明這個鎖正在被使用當中。

watch,multi命令

watch命令用於監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷。

Multi 命令用於標記一個事務塊的開始,事務塊內的多條命令會按照先後順序被放進一個隊列當中,最後由 EXEC 命令原子性(atomic)地執行。

18.16.200.68 dev:3>set key 1
"OK"
18.16.200.68 dev:3>get key
"1"
18.16.200.68 dev:3>watch key
"OK"
18.16.200.68 dev:3>set key 2
"OK"
18.16.200.68 dev:3>multi 
"OK"
18.16.200.68 dev:3>set key 3
"QUEUED"
18.16.200.68 dev:3>get key
"QUEUED"
18.16.200.68 dev:3>exec

18.16.200.68 dev:3>get key
"2"

必須是事務執行之前,被監控key被修改,後續事務就無效

sadd命令

18.16.200.68 dev:3>sadd name hongda
"1"
18.16.200.68 dev:3>get name
"WRONGTYPE Operation against a key holding the wrong kind of value"
18.16.200.68 dev:3>sadd name da
"1"
18.16.200.68 dev:3>sadd name da2
"1"
18.16.200.68 dev:3>smembers name
1) "da2"
2) "da"
3) "hongda"

Redis Sadd 命令將一個或多個成員元素加入到集合中,已經存在於集合的成員元素將被忽略。

假如集合 key 不存在,則創建一個只包含添加的元素作成員的集合。

當集合 key 不是集合類型時,返回一個錯誤。

Redis過期字典

db.expires

熟悉 redis 的朋友都知道,每個數據庫維護了兩個字典:

  • db.dict:數據庫中所有鍵值對,也被稱作數據庫的 keyspace
  • db.expires:帶有生命週期的 key 及其對應的 TTL(存留時間),因此也被稱作 expire set

maxmemory-samples

 爲了保證性能,redis 中使用的 LRU 與 LFU 算法是一類近似實現。
 簡單來說就是:算法選擇被淘汰記錄時,不會遍歷所有記錄,而是以 隨機採樣 的方式選取部分記錄進行淘汰。
maxmemory-samples 選項控制該過程的採樣數量,增大該值會增加 CPU 開銷,但算法效果能更逼近實際的 LRU 與 LFU 。

lazyfree-lazy-eviction

 清理緩存就是爲了釋放內存,但這一過程會阻塞主線程,影響其他命令的執行。
 當刪除某個巨型記錄(比如:包含數百條記錄的 list)時,會引起性能問題,甚至導致系統假死。
 延遲釋放 機制會將巨型記錄的內存釋放,交由其他線程異步處理,從而提高系統的性能。
 開啓該選項後,可能出現使用內存超過 maxmemory 上限的情況。

Redis中ttl命令

18.16.200.68 dev:3>set name hongda
"OK"
18.16.200.68 dev:3>get name
"hongda"
18.16.200.68 dev:3>ttl name
"-1"
18.16.200.68 dev:3>expire name 10
"1"
18.16.200.68 dev:3>ttl name
"8"
18.16.200.68 dev:3>get name
"hongda"
18.16.200.68 dev:3>ttl name
"1"
18.16.200.68 dev:3>ttl name
"-2"
18.16.200.68 dev:3>get name
null

當 key 不存在時,返回 -2 。 當 key 存在但沒有設置剩餘生存時間時,返回 -1 。 否則,以秒爲單位,返回 key 的剩餘生存時間。

Redis如果沒有設置expire,是否默認永不過期

Redis無論有沒有設置expire,他都會遵循redis的配置好的刪除機制,在配置文件裏設置:
redis最大內存不足"時,數據清除策略,默認爲"volatile-lru"。

如果設置的清除策略是volatile-lru,即從設置了過期時間的key中使用LRU算法進行淘汰,

在這種清除策略下,如果沒有設置有效期,即使內存用完,redis 自動回收機制也是看設置了有效期的,不會動沒有設定有效期的,如果清理後內存還是滿的,就不再接受寫操作。

Redis的持久化

RDB快照

RDB(快照)持久化:保存某個時間點的全量數據快照,生成RDB文件在磁盤中。RDB文件是一個壓縮過的二進制文件,可以還原爲Redis的數據。

觸發和載入方式

  • 手動觸發方式

    • SAVE命令:阻塞Redis的服務器進程,直到RDB文件被創建完畢,阻塞期間服務器不能處理任何命令請求。
    • BGSAVE命令:Fork出一個子進程來創建RDB文件,不阻塞服務器進程。lastsave 指令可以查看最近的備份時間。
  • 載入方式

    • Redis沒有主動載入RDB文件的命令,RDB文件是在服務器啓動時自動載入,只要Redis服務器檢測到RDB文件的存在,即會載入。且載入過程,服務器也會是阻塞狀態
  • 自動觸發方式

    • 根據redis.conf配置裏的save m n定時觸發(用的是BGSAVE),m表示多少時間內,n表示修改次數。save可以設置多個條件,任意條件達到即會執行BGSAVE命令

      	save 900 1  //設置條件1,即服務器在900秒內,對數據庫進行了至少1次修改,即會觸發BGSAVE
      	save 300 10 //設置條件2,即服務器在300秒內,對數據庫進行了至少10次修改,即會觸發BGSAVE
      	save 60 1000  //設置條件3,即服務器在60秒內,對數據庫進行了至少1000次修改,即會觸發BGSAVE
      
    • redis如何保存自動觸發方式的save配置呢

      • redisServer結構中維護了一個saveParam的數組,數組每個saveParam都存儲着一個save條件,如下圖:
        • img
        • img
      • 前文所述三個save,其saveParam的數組將會是下圖的樣子
        • img
    • 自動觸發方式如何實現的呢

      • redisServer結構維護了一個dirty計數器和lastsave屬性。
      • dirty計數器記錄了上次SAVE或者BGSAVE之後,數據庫執行了多少次的增刪改,當服務器成功執行一個修改命令後,程序就會對該值+1,(對集合操作n個元素,dirty+n)。SAVE或者BGSAVE命令執行後,dirty計數器清零。
      • lastsave屬性是一個unix時間戳,記錄了服務器上次成功執行SAVE或者BGSAVE命令的時間。
      • Redis服務器有個週期性操作函數serverCron,默認每100毫秒執行一次,它其中一項工作就是檢查saveParam保存的條件,並根據dirty和lastsave字段判斷是否有哪一條條件已經被滿足。

快照期間,是否可以對數據進行改動

爲了快照而暫停寫操作,肯定是不能接受的。所以這個時候,Redis 就會藉助操作系統提供的寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。

簡單來說,bgsave 子進程是由主線程 fork 生成的,可以共享主線程的所有內存數據。bgsave 子進程運行後,開始讀取主線程的內存數據,並把它們寫入 RDB 文件。

此時,如果主線程對這些數據也都是讀操作(鍵值對 A),那麼,主線程和 bgsave 子進程相互不影響。但是,如果主線程要修改一塊數據(鍵值對 C),那麼,這塊數據就會被複制一份,生成該數據的副本(鍵值對 C’)。然後,主線程在這個數據副本上進行修改。同時,bgsave 子進程可以繼續把原來的數據(鍵值對 C)寫入 RDB 文件。

AOF日誌

AOF持久化的實現

AOF持久化功能的實現可以分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。

  • 命令追加
    • 當AOF持久化功能處於打開狀態時,服務器在執行完一個寫命令後,會以協議格式將被執行的寫命令追加到服務器狀態的 aof_buf緩存區的末尾。
  • AOF文件的寫入和同步
    • Redis的服務器進程就是一個事件循環。
    • 每次結束一個事件循環之前,都會調用flushAppendOnlyFile函數,考慮是否將緩衝區的內容寫入和保存到AOF文件裏面。
    • flushAppendOnlyFile函數根據配置項appendsync的不同選值有不同的同步策略。
    • img

AOF文件的載入

Redis讀取AOF文件並還原數據庫狀態的詳細步驟如下:

  • 服務器創建一個不帶網絡連接的僞客戶端(fake client)(因爲Redis的命令只能在客戶端上下文中執行);
  • 從AOF文件中分析並讀取出一條寫命令。
  • 一直執行步驟2和步驟3,直到AOF文件中的所有寫命令都被處理完畢爲止。

AOF重寫

體積過大的AOF文件很可能對Redis服務器、甚至整個宿主計算機造成影響,並且AOF文件的體積越大,使用AOF文件來進行數據還原所需的時間就越多。

爲了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫(rewrite)功能。

通過該功能,Redis服務器可以創建一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所保存的數據庫狀態相同,但新AOF文件不會包含任何浪費空間的冗餘命令,所以新AOF文件的體積通常會比舊AOF文件的體積要小得多。

我們稱新的AOF文件爲AOF重寫文件AOF重寫文件不是像AOF一樣記錄每一條的寫命令,也不是對AOF文件的簡單複製和壓縮。AOF重寫是通過讀取當前Redis數據庫狀態來實現的

AOF中,我們要保存四條寫命令,而在AOF重寫文件中,我們使用一條SADD animals "Dog" "Panda" "Tiger" "Lion" "Cat"來替代四條命令。

從數據庫中讀取鍵現在的值,然後用一條命令去記錄鍵值對,代替之前記錄這個鍵值對的多條命令,這就是AOF重寫功能的實現原理。(比如連續6條RPUSH命令會被整合成1條)

在實際中,爲了避免在執行命令時造成客戶端輸入緩衝區溢出,重寫程序在處理列表、哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數量,如果元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量(默認爲64)的值,那麼重寫程序將使用多條命令來記錄鍵的值,而不單單使用一條命令。例如如果SADD後面加入的元素爲90條,那麼會分成兩條SADD,第一條SADD 64個元素,第二條SADD 36個元素。

總結

RDB 快照

優點:文件結構緊湊,節省空間,易於傳輸,能夠快速恢復

缺點:生成快照的開銷只與數據庫大小相關,當數據庫較大時,生成快照耗時,無法頻繁進行該操作

AOF 日誌

優點:細粒度記錄對磁盤I/O壓力小,允許頻繁落盤,數據丟失的概率極低

缺點:恢復速度慢;記錄日誌開銷與更新頻率有關,頻繁更新會導致磁盤 I/O 壓力上升

RDB 和 AOF 到底該如何選擇

  • 不要僅僅使用 RDB,因爲那樣會導致你丟失很多數據;
  • 也不要僅僅使用 AOF,因爲那樣有兩個問題:第一,你通過 AOF 做冷備,沒有 RDB 做冷備來的恢復速度更快;第二,RDB 每次簡單粗暴生成數據快照,更加健壯,可以避免 AOF 這種複雜的備份和恢復機制的 bug
  • redis 支持同時開啓開啓兩種持久化方式,我們可以綜合使用 AOFRDB 兩種持久化機制,用 AOF 來保證數據不丟失,作爲數據恢復的第一選擇; 用 RDB 來做不同程度的冷備,在 AOF 文件都丟失或損壞不可用的時候,還可以使用 RDB 來進行快速的數據恢復。

RDB-AOF 混合持久化

Redis 用戶通常會因爲 RDB 持久化和 AOF 持久化之間不同的優缺點而陷入兩難的選擇當中:

  • RDB 持久化能夠快速地儲存和恢復數據, 但是在服務器停機時卻會丟失大量數據;
  • AOF 持久化能夠有效地提高數據的安全性, 但是在儲存和恢復數據方面卻要耗費大量的時間。

爲了讓用戶能夠同時擁有上述兩種持久化的優點, Redis 4.0 推出了一個能夠“魚和熊掌兼得”的持久化方案 —— RDB-AOF 混合持久化: 這種持久化能夠通過 AOF 重寫操作創建出一個同時包含 RDB 數據和 AOF 數據的 AOF 文件, 其中 RDB 數據位於 AOF 文件的開頭, 它們儲存了服務器開始執行重寫操作時的數據庫狀態: 至於那些在重寫操作執行之後執行的 Redis 命令, 則會繼續以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 數據之後。

RDB-AOF 混合持久化功能默認是處於關閉狀態的, 爲了啓用該功能, 用戶不僅需要開啓 AOF 持久化功能, 還需要將 aof-use-rdb-preamble 選項的值設置爲真

Redis的淘汰策略

淘汰策略

當達到內存使用上限maxmemory時,可指定的清理緩存所使用的策略有:

  • noeviction 當達到最大內存時直接返回錯誤,不覆蓋或逐出任何數據(默認的)
  • allkeys-lfu 淘汰整個 keyspace 中最不常用的 (LFU) 鍵 (4.0 或更高版本)
  • allkeys-lru 淘汰整個 keyspace 最近最少使用的 (LRU) 鍵
  • allkeys-random 淘汰整個 keyspace 中的隨機鍵
  • volatile-ttl 淘汰 expire set 中 TTL 最短的鍵
  • volatile-lfu 淘汰 expire set 中最不常用的鍵 (4.0 或更高版本)
  • volatile-lru 淘汰 expire set 中最近最少使用的 (LRU) 鍵
  • volatile-random 淘汰 expire set 中的隨機鍵

 當 expire set 爲空時,volatile-*noeviction 行爲一致。

查看Redis設置的內存大小

通過配置文件查看

通過在Redis安裝目錄下面的redis.conf配置文件中添加以下配置設置內存大小

通過命令查看並設置內存大小

λ redis-cli -h 18.16.200.82 -p 6379 -a shitou123 --raw
18.16.200.82:6379> config get maxmemory
maxmemory
0
18.16.200.82:6379> config set maxmemory 800mb
OK
18.16.200.82:6379> config get maxmemory
maxmemory
838860800

如果不設置最大內存大小或者設置最大內存大小爲0,在64位操作系統下不限制內存大小,在32位操作系統下最多使用3GB內存

查看Redis設置的淘汰策略

設置及查看redis淘汰策略

18.16.200.82:6379> config get maxmemory-policy
maxmemory-policy
noeviction
18.16.200.82:6379> config set maxmemory-policy volatile-lru
OK
18.16.200.82:6379> config get maxmemory-policy
maxmemory-policy
volatile-lru

LRU算法 (最近最少使用)

LRU(Least Recently Used),即最近最少使用,是一種緩存置換算法。 其核心思想是:可以記錄每個緩存記錄的最近訪問時間,最近未被訪問時間最長的數據會被首先淘汰。
其原理是維護一個雙向鏈表,key -> node,其中node保存鏈表前後節點關係及數據data。新插入的key時,放在頭部,並檢查是否超出總容量,如果超出則刪除最後的key;訪問key時,無論是查找還是更新,將該Key被調整到頭部。

Redis中實際使用的LRU算法

Redis 中的 LRU 不是嚴格意義上的LRU算法實現,是一種近似的 LRU 實現,主要是爲了節約內存佔用以及提升性能。Redis 有這樣一個配置 —— maxmemory-samplesRedisLRU 是取出配置的數目的key,然後從中選擇一個最近最不經常使用的 key 進行置換,默認的 5,如下:

maxmemory-samples 5

可以通過調整樣本數量來取得 LRU 置換算法的速度或是精確性方面的優勢。

Redis 不採用真正的 LRU 實現的原因是爲了節約內存使用。

Redis 如何管理熱度數據

前面我們講述字符串對象時,提到了 redisObject 對象中存在一個 lru屬性:

typedef struct redisObject {
        unsigned type:4;//對象類型(4位=0.5字節)
        unsigned encoding:4;//編碼(4位=0.5字節)
        unsigned lru:LRU_BITS;//記錄對象最後一次被應用程序訪問的時間(24位=3字節)
        int refcount;//引用計數。等於0時表示可以被垃圾回收(32位=4字節)
        void *ptr;//指向底層實際的數據存儲結構,如:SDS等(8字節)
        } robj;

lru 屬性是創建對象的時候寫入,對象被訪問到時也會進行更新。正常人的思路就是最後決定要不要刪除某一個鍵肯定是用當前時間戳減去 lru,差值最大的就優先被刪除。但是 Redis 裏面並不是這麼做的,Redis 中維護了一個全局屬性 lru_clock,這個屬性是通過一個全局函數 serverCron 每隔 100 毫秒執行一次來更新的,記錄的是當前 unix時間戳。

最後決定刪除的數據是通過 lru_clock 減去對象的 lru 屬性而得出的。那麼爲什麼Redis 要這麼做呢?直接取全局時間不是更準確嗎?

這是因爲這麼做可以避免每次更新對象的 lru 屬性的時候可以直接取全局屬性,而不需要去調用系統函數來獲取系統時間,從而提升效率(Redis當中有很多這種細節考慮來提升性能,可以說是對性能儘可能的優化到極致)。

不過這裏還有一個問題,我們看到,redisObject 對象中的 lru 屬性只有 24 位,24 位只能存儲 194 天的時間戳大小,一旦超過 194 天之後就會重新從 0 開始計算,所以這時候就可能會出現 redisObject 對象中的 lru 屬性大於全局的 lru_clock 屬性的情況。

正因爲如此,所以計算的時候也需要分爲 2 種情況:

  • 當全局 lruclock > lru,則使用 lruclock - lru 得到空閒時間。
  • 當全局 lruclock < lru,則使用 lruclock_max(即 194 天) -lru + lruclock 得到空閒時間。

需要注意的是,這種計算方式並不能保證抽樣的數據中一定能刪除空閒時間最長的。這是因爲首先超過 194 天還不被使用的情況很少,再次只有 lruclock2 輪繼續超過lru 屬性時,計算纔會出問題。

比如對象 A 記錄的 lru1 天,而 lruclock 第二輪都到 10 天了,這時候就會導致計算結果只有 10-1=9 天,實際上應該是 194+10-1=203天。但是這種情況可以說又是更少發生,所以說這種處理方式是可能存在刪除不準確的情況,但是本身這種算法就是一種近似的算法,所以並不會有太大影響。

LFU算法 (最不經常使用)

LFU算法是Redis4.0裏面新加的一種淘汰策略。它的全稱是Least Frequently Used,它的核心思想是根據key的最近被訪問的頻率進行淘汰,很少被訪問的優先被淘汰,被訪問的多的則被留下來。

LFU算法能更好的表示一個key被訪問的熱度。假如你使用的是LRU算法,一個key很久沒有被訪問到,只剛剛是偶爾被訪問了一次,那麼它就被認爲是熱點數據,不會被淘汰,而有些key將來是很有可能被訪問到的則被淘汰了。如果使用LFU算法則不會出現這種情況,因爲使用一次並不會使一個key成爲熱點數據。LFU原理使用計數器來對key進行排序,每次key被訪問的時候,計數器增大。計數器越大,可以約等於訪問越頻繁。具有相同引用計數的數據塊則按照時間排序。

LFU 全稱爲:Least Frequently Used。即:最不經常使用,這個主要針對的是使用頻率。這個屬性也是記錄在redisObject 中的 lru 屬性內。

當我們採用 LFU 回收策略時,lru 屬性的高 16 位用來記錄訪問時間(last decrement time:ldt,單位爲分鐘),低 8 位用來記錄訪問頻率(logistic counter:logc),簡稱 counter

訪問頻次遞增

LFU 計數器每個鍵只有 8 位,它能表示的最大值是 255,所以 Redis使用的是一種基於概率的對數器來實現 counter 的遞增。r

給定一箇舊的訪問頻次,當一個鍵被訪問時,counter 按以下方式遞增:

  1. 提取 01 之間的隨機數 R
  2. counter - 初始值(默認爲 5),得到一個基礎差值,如果這個差值小於 0,則直接取 0,爲了方便計算,把這個差值記爲 baseval
  3. 概率 P 計算公式爲:1/(baseval * lfu_log_factor + 1)
  4. 如果 R < P 時,頻次進行遞增(counter++)。

公式中的 lfu_log_factor 稱之爲對數因子,默認是 10 ,可以通過參數來進行控制:

lfu_log_factor 10

下圖就是對數因子 lfu_log_factor 和頻次 counter 增長的關係圖:

圖片圖片

可以看到,當對數因子 lfu_log_factor100 時,大概是 10M(1000萬) 次訪問纔會將訪問 counter 增長到 255,而默認的 10 也能支持到 1M(100萬) 次訪問 counter才能達到 255 上限,這在大部分場景都是足夠滿足需求的。

訪問頻次遞減

如果訪問頻次 counter 只是一直在遞增,那麼遲早會全部都到 255,也就是說 counter一直遞增不能完全反應一個 key 的熱度的,所以當某一個 key 一段時間不被訪問之後,counter 也需要對應減少。

counter 的減少速度由參數 lfu-decay-time 進行控制,默認是 1,單位是分鐘。默認值 1 表示:N 分鐘內沒有訪問,counter 就要減 N

lfu-decay-time 1

具體算法如下:

  1. 獲取當前時間戳,轉化爲分鐘 後取低 16 位(爲了方便後續計算,這個值記爲 now)。
  2. 取出對象內的 lru 屬性中的高 16 位(爲了方便後續計算,這個值記爲 ldt)。
  3. lru > now 時,默認爲過了一個週期(16 位,最大 65535),則取差值 65535-ldt+now:當 lru <= now 時,取差值 now-ldt(爲了方便後續計算,這個差值記爲idle_time)。
  4. 取出配置文件中的 lfu_decay_time 值,然後計算:idle_time / lfu_decay_time(爲了方便後續計算,這個值記爲num_periods)。
  5. 最後將counter減少:counter - num_periods

看起來這麼複雜,其實計算公式就是一句話:取出當前的時間戳和對象中的 lru 屬性進行對比,計算出當前多久沒有被訪問到,比如計算得到的結果是 100 分鐘沒有被訪問,然後再去除配置參數 lfu_decay_time,如果這個配置默認爲 1也即是 100/1=100,代表100 分鐘沒訪問,所以 counter 就減少 100

Redis分佈式鎖問題

Redis鎖必須設置過期時間

如果不設置過期時間,客戶端故障,鎖就永遠一直存在,資源永遠不能被再次獲取

Redis鎖中value設置隨機值

場景:客戶A獲取鎖,設置過期時間5s,但是因爲某些原因超時,超時期間客戶B也獲取了同樣key的鎖,

客戶A執行完,刪除鍵值爲key的鎖,但是其實該鎖爲客戶B的

解決方法:保證每個客戶端可以區分自己的鎖,比如即使key值相等,也可以通過設置value來區分,刪除鎖之前,可以先比對key,value,再進行刪除。

Redis鎖中的刪除操作使用lua腳本

上述的刪除操作,必須先獲取鎖,比對key,value,再進行刪除,那麼就必須調用到Redis的get,del命令,這樣明顯就不是原子性操作,不安全。

Redis鎖過期問題解決辦法 (看門狗)

設置過期時間,如果到過期時間,但是任務還未執行完畢,其他任務就會獲取鎖,這時就會有多個任務同時獲取到資源

現有的解決辦法是:

redisson在加鎖成功後,會註冊一個定時任務監聽這個鎖,每隔10秒就去查看這個鎖,如果還持有鎖,就對過期時間進行續期。默認過期時間30秒。這個機制也被叫做:“看門狗”。

看門狗功能是Redisson的,不是redis的,獲取鎖沒指定過期時間的,看門狗就會生效

默認情況下,lockWatchdogTimeout(可配)爲30s,會有task每10s循環判斷,如果該線程還持有鎖執行任務,就重置延時30s,直到鎖丟失,獲取線程不持有該鎖

舒服了,踩到一個關於分佈式鎖的非比尋常的BUG

Redisson分佈式鎖存儲值格式

hset命令賦值,key爲鎖的名稱,field爲“客戶端唯一ID:線程ID”,value爲1

客戶端唯一id,就是uuid,value爲可重入鎖次數

建議

建議使用Redisson,源碼中加鎖/釋放鎖操作都是用lua腳本完成的,封裝的非常完善,開箱即用。

機制,failover策略不可靠

主從同步通常是異步的,並不能真正的容錯。

造成鎖不獨享的場景如下圖所示:

20190817170800716.png

  1. 客戶端A申請從master實例獲取鎖key=test001,由於之前key=test001在master實例上不存在,所以客戶端A獲取鎖成功。
  2. master在通過異步主從同步將key=test001同步至slave之前掛掉了,此時slave經過failover升級爲master,但是此時slave上並無key=test001。
  3. 此時,客戶端B申請從redis獲取鎖key=test001,由於此時slave上不存在key=test001,同樣的,客戶端B獲取鎖成功。
  4. 最終的結果是,由於關鍵時刻的master宕機,造成兩個客戶端同時加鎖成功,這與分佈式鎖的獨享特性相互違背。

爲什麼Redis單線程效率高

Redis官方提供的數據是可以達到10w+QPS(每秒內查詢次數)

Redis中只有網絡請求模塊和數據操作模塊是單線程的。而其他的如持久化存儲模塊、集羣支撐模塊等是多線程的。

  • 基於內存操作
  • 單線程,避免了線程上下文頻繁切換,也避免了各種加鎖,釋放鎖問題
  • 採用網絡IO多路複用技術來提升Redis的網絡IO利用率

採用非阻塞IO,使用epoll作爲IO多路複用技術的實現,讓單個線程高效的處理多個連接請求(儘量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成爲這裏的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。

如果宿主機的cpu性能太高或太低,可以多起幾個Redis進程,因爲多起幾個進程可以利用cpu多核優勢。

缺點:因爲是單線程的,所以如果某個命令執行事件過長,會導致其他命令被阻塞。

Redis 的瓶頸並不在 CPU,而在內存和網絡。

內存不夠的話,可以加內存或者做數據結構優化和其他優化等,但網絡的性能優化纔是大頭,網絡 IO 的讀寫在 Redis 整個執行期間佔用了大部分的 CPU 時間,如果把網絡處理這部分做成多線程處理方式,那對整個 Redis 的性能會有很大的提升。

Redis不是一直號稱單線程效率也很高嗎,爲什麼又採用多線程了?

Jedis,Redisson,Lettuce三個Redis客戶端框架區別

Redis底層數據結構

  • Redis 字符串,是自己構建了一種名爲 簡單動態字符串(simple dynamic string , SDS)的抽象類型,並將 SDS 作爲 Redis 的默認字符串表示。
  • Redis List ,底層是 ZipList ,不滿足 ZipList 就使用雙向鏈表。ZipList 是爲了節約內存而開發的。和各種語言的數組類似,它是由連續的內存塊組成的,這樣一來,由於內存是連續的,就減少了很多內存碎片和指針的內存佔用,進而節約了內存。

選擇合適Redis數據結構,減少80%的內存佔用

Redis 5種數據結構 及使用場景分析

Redis 常用數據結構及其底層存儲實現總結

hash與String對比

hash類型數據比較少時,使用的時ziplist,比較省空間(相對於hash中設置key,value方式),但是相比String序列化對象不一定省空間,數據量大了就變成dict方式

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

在如下兩個條件之一滿足的時候,ziplist會轉成dict:

  • 當hash中的數據項(即field-value對)的數目超過512的時候,也就是ziplist數據項超過1024的時候(請參考t_hash.c中的hashTypeSet函數)。
  • 當hash中插入的任意一個value的長度超過了64的時候(請參考t_hash.c中的hashTypeTryConversion函數)。

Redis的hash之所以這樣設計,是因爲當ziplist變得很大的時候,它有如下幾個缺點:

  • 每次插入或修改引發的realloc操作會有更大的概率造成內存拷貝,從而降低性能。
  • 一旦發生內存拷貝,內存拷貝的成本也相應增加,因爲要拷貝更大的一塊數據。
  • 當ziplist數據項過多的時候,在它上面查找指定的數據項就會性能變得很低,因爲ziplist上的查找需要進行遍歷。

總之,ziplist本來就設計爲各個數據項挨在一起組成連續的內存空間,這種結構並不擅長做修改操作。一旦數據發生改動,就會引發內存realloc,可能導致內存拷貝。

hash比較好的就是可以hget直接獲取value值(hset直接設置value值)

Redis過期策略有哪些

惰性刪除:當讀/寫一個已經過期的 key 時,會觸發惰性刪除策略,直接刪除掉這個過期 key ,並按照 key 不存在去處理。惰性刪除,對內存不太好,已經過期的 key 會佔用太多的內存。

定期刪除:每隔一段時間,就會對 Redis 進行檢查,主動刪除一批已過期的 key。

Redis集羣作用

  • 自動將數據進行分片,每個master上放置一部分數據
  • 提供內置的高可用支持,部分master不可用時,還是可以繼續使用的。

redis cluster 架構下,每個 redis 要放開兩個端口號,比如一個是 6379,另外一個就是 加1w 的端口號,比如 16379

16379 端口號是用來進行節點間通信的,也就是 cluster bus 的東西,cluster bus 的通信,用來進行故障檢測、配置更新、故障轉移授權。cluster bus 用了另外一種二進制的協議,gossip 協議,用於節點間進行高效的數據交換,佔用更少的網絡帶寬和處理時間。

Redis 是怎麼進行水平擴容的? Redis 集羣數據分片的原理是什麼?

Redis 數據分片原理是哈希槽(hash slot)。

Redis 集羣有 16384 個哈希槽。每一個 Redis 集羣中的節點都承擔一個哈希槽的子集。

哈希槽讓在集羣中添加和移除節點非常容易。例如,如果我想添加一個新節點 D ,我需要從節點 A 、B、C 移動一些哈希槽到節點 D。同樣地,如果我想從集羣中移除節點 A ,我只需要移動 A 的哈希槽到 B 和 C。當節點 A 變成空的以後,我就可以從集羣中徹底刪除它。因爲從一個節點向另一個節點移動哈希槽並不需要停止操作,所以添加和移除節點,或者改變節點持有的哈希槽百分比,都不需要任何停機時間(downtime)。

一致性hash算法

一致性 Hash 算法將整個哈希值空間組織成一個虛擬的圓環, 我們對 key 進行哈希計算,使用哈希後的結果對 2 ^ 32 取模,hash 環上必定有一個點與這個整數對應。依此確定此數據在環上的位置,從此位置沿環順時針“行走”,第一臺遇到的服務器就是其應該定位到的服務器。 一致性 Hash 算法對於節點的增減都只需重定位環空間中的一小部分數據,具有較好的容錯性和可擴展性。 比如,集羣有四個節點 Node A 、B 、C 、D ,增加一臺節點 Node X。Node X 的位置在 Node B 到 Node C 直接,那麼受到影響的僅僅是 Node B 到 Node X 間的數據,它們要重新落到 Node X 上。 所以一致性哈希算法對於容錯性和擴展性有非常好的支持。

Redis變慢原因分析

你需要去查看一下 Redis 的慢日誌(slowlog)。

Redis 提供了慢日誌命令的統計功能,它記錄了有哪些命令在執行時耗時比較久。

查看 Redis 慢日誌之前,你需要設置慢日誌的閾值。例如,設置慢日誌的閾值爲 5 毫秒,並且保留最近 500 條慢日誌記錄:

# 命令執行耗時超過 5 毫秒,記錄慢日誌
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 條慢日誌
CONFIG SET slowlog-max-len 500

查看慢查詢日誌:

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693       # 慢日誌ID
   2) (integer) 1593763337  # 執行時間戳
   3) (integer) 5299        # 執行耗時(微秒)
   4) 1) "LRANGE"           # 具體執行的命令和參數
      2) "user_list:2000"
      3) "0"
      4) "-1"
2) 1) (integer) 32692
   2) (integer) 1593763337
   3) (integer) 5044
   4) 1) "GET"
      2) "user_info:1000"
...

變慢的原因:

  • redis操作內存數據,時間複雜度較高,需要花費更多的cpu資源
  • redis一次需要返回的數據過多,更多時間花費在數據協議組裝和網絡傳輸。
  • bigkey,一個key寫入的value太大,分配內存,釋放內存也比較耗時
  • 有規律的變慢,大概是redis設置爲主動過期,大量key集中到期,主線程刪除過期key
  • 內存到達上限,先要根據淘汰策略剔除一部分數據,再把新數據寫入
  • rdbaop rewrite期間延遲,主線程需要創建一個子線程進行數據持久化,創建子線程會調用操作系統的fork函數,fork會消耗大量cpu資源,在fork之前,整個redis會被阻塞,無法處理客戶端請求。
  • 操作系統是否開啓內存大頁機制,redis申請內存變大,申請內存耗時變長,導致每個寫請求延遲增加。
  • AOF刷盤機制設置爲always,即每次執行寫操作立刻刷盤
  • 設置了綁定cpu
  • 查看redis是否使用了swap,swap是使用磁盤,性能差。
  • redis設置爲開啓內存碎片整理,也會導致redis性能下降。
  • 網絡IO過載

Redis 爲什麼變慢了?一文講透如何排查 Redis 性能問題 | 萬字長文

Redis選舉

slave(從節點)發現自己的master(主節點)不可用時,變嘗試進行Failover,以便稱爲新的master。由於掛掉的master可能會有多個slave,從而存在多個slave競爭成爲master節點的過程, 其過程如下:

  1. slave發現自己的master不可用;
  2. slave將記錄集羣的currentEpoch(選舉週期)加1,並廣播FAILOVER_AUTH_REQUEST 信息進行選舉;
  3. 其他節點收到FAILOVER_AUTH_REQUEST信息後,只有其他的master可以進行響應,master收到消息後返回FAILOVER_AUTH_ACK信息,對於同一個Epoch,只能響應一次ack;
  4. slave收集maste返回的ack消息
  5. slave判斷收到的ack消息個數是否大於半數的master個數,若是,則變成新的master
  6. 廣播Pong消息通知其他集羣節點,自己已經成爲新的master

注意:從節點並不是在主節點一進入 FAIL 狀態就馬上嘗試發起選舉,而是有一定延遲,一定的延遲確保我們等待FAIL狀態在集羣中傳播,slave如果立即嘗試選舉,其它masters或許尚未意識到FAIL狀態,可能會拒絕投票。

  • 延遲計算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
  • SLAVE_RANK表示此slave已經從master複製數據的總量的rankRank越小代表已複製的數據越新。這種方式下,持有最新數據的slave將會首先發起選舉(理論上)。

Redis集羣爲什麼至少需要三個master節點?

因爲新master的選舉需要大於半數的集羣master節點同意才能選舉成功,如果只有兩個master節點,當其中一個掛了,是達不到選舉新master的條件的。

Redis集羣爲什麼至少推薦節點數爲奇數?

奇數個master節點可以在滿足選舉該條件的基礎上節省一個節點,比如三個master節點和四個master節點的集羣相比,大家如果都掛了一個master節點都能選舉新master節點,如果都掛了兩個master節點都沒法選舉新master節點了,所以奇數的master節點更多的是從節省機器資源角度出發說的。

網絡不穩定是否會是否引起選舉?

真實世界的機房網絡往往並不是風平浪靜的,它們經常會發生各種各樣的小問題。比如網絡抖動就是非常常見的一種現象,突然之間部分連接變得不可訪問,然後很快又恢復正常。

爲解決這種問題,Redis Cluster 提供了一種選項cluster-node-timeout,表示當某個節點持續 timeout 的時間失聯時,纔可以認定該節點出現故障,需要進行主從切換。如果沒有這個選項,網絡抖動會導致主從頻繁切換 (數據的重新複製)。

Redis主從數據同步

全量複製

  1. slave第一次啓動時,連接Master,發送PSYNC命令,格式爲psync {runId} {offset}
  • {runId} 爲master的運行id;{offset}爲slave自己的複製偏移量
  • 由於此時是slave第一次連接master,slave不知道master的runId,也不知道自己偏移量,這時候會傳一個問號和-1,告訴master節點是第一次同步。格式爲psync ? -1
  1. 當master接收到psync ? -1時,就知道slave是要全量複製,就會將自己的runIdoffset告知slave,回覆命令+fullresync {runId} {offset}。同時,master會執行bgsave命令來生成RDB文件,並使用緩衝區記錄此後的所有寫命令
  • slave接受到master的回覆命令後,會保存master的runIdoffset
  • slave此時處於同步狀態,如果此時收到請求,當配置參數slave-server-stale-data yes時,會響應當前請求,no則返回錯誤。
  1. master bgsave執行完畢,向Slave發送RDB文件,同時繼續緩衝此期間的寫命令。RDB文件發送完畢後,開始向Slave發送存儲在緩衝區的寫命令
  2. slave收到RDB文件,丟棄所有舊數據,開始載入RDB文件;並執行Master發來的所有的存儲在緩衝區裏的寫命令。
  3. 此後 master 每執行一個寫命令,就向Slave發送相同的寫命令。

增量複製

  1. 如果出現網絡閃斷或者命令丟失等異常情況時,當主從連接恢復後,由於從節點之前保存了自身已複製的偏移量和主節點的運行ID。因此會把它們當作psync參數發送給主節點,要求進行部分複製操作,格式爲psync {runId} {offset}
  2. 主節點接到psync命令後首先覈對參數runId是否與自身一致,如果一致,說明之前複製的是當前主節點;之後根據參數offset在自身複製積壓緩衝區查找,如果偏移量之後的數據存在緩衝區中,則對從節點發送+CONTINUE響應,表示可以進行部分複製;否則進行全量複製。
  3. 主節點根據偏移量把複製積壓緩衝區裏的數據發送給從節點,保證主從複製進入正常狀態

主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。

先更新數據庫,再刪除緩存,會有什麼問題?

這5個常問的Redis面試題你答得出來嗎?(詳細剖析)

先更新數據庫,再刪除緩存。可能出現以下情況:

  • 如果更新完數據庫, Java 服務提交了事務,然後掛掉了,那 Redis 還是會執行,這樣也會不一致。
  • 如果更新數據庫成功,刪除緩存失敗了,那麼會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。

先刪除緩存,再更新數據庫。

  • 如果刪除緩存失敗,那就不更新數據庫,緩存和數據庫的數據都是舊數據,數據是一致的。
  • 如果刪除緩存成功,而數據庫更新失敗了,那麼數據庫中是舊數據,緩存中是空的,數據不會不一致。因爲讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然後更新到緩存中。

先刪除緩存,在寫數據庫成功之前,如果有讀請求發生,可能導致舊數據入緩存,引發數據不一致,怎麼處理?

分佈式鎖

Redission框架中的看門狗原理

參考:

Redis: 分佈式鎖的官方算法RedLock以及Java版本實現庫Redisson

Redlock:Redis分佈式鎖最牛逼的實現

冷飯新炒:理解Redisson中分佈式鎖的實現

redis緩存淘汰策略LRU和LFU對比與分析

LRU和LFU算法以及其在Redis中的實現

redis的過期時間和過期刪除機制

Redis數據庫結構/鍵空間/過期字典/事務/鎖/持久化

Redis 緩存過期(maxmemory) 配置/算法 詳解

LRU和LFU算法以及其在Redis中的實現

Redis數據庫結構/鍵空間/過期字典/事務/鎖/持久化

細說Redis分佈式鎖:setnx/redisson/redlock?瞭解一波?

redis 分佈式鎖的 5個坑,真是又大又深

分佈式系統架構,回顧2020年常見面試知識點梳理(每次面試都會問到其中某一塊知識點)

Redis筆記-持久化策略

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章