命令
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 命令可以通過參數來實現和 SETNX 、 SETEX 和 PSETEX 三個命令的效果,所以將來的 Redis 版本可能會廢棄並最終移除 SETNX 、 SETEX 和 PSETEX 這三個命令。
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"
setnx
和set 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
:數據庫中所有鍵值對,也被稱作數據庫的 keyspacedb.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條件,如下圖:
- 前文所述三個save,其saveParam的數組將會是下圖的樣子
- redisServer結構中維護了一個saveParam的數組,數組每個saveParam都存儲着一個save條件,如下圖:
-
自動觸發方式如何實現的呢?
- 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的不同選值有不同的同步策略。
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
支持同時開啓開啓兩種持久化方式,我們可以綜合使用AOF
和RDB
兩種持久化機制,用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-samples
,Redis
的 LRU
是取出配置的數目的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
天還不被使用的情況很少,再次只有 lruclock
第 2
輪繼續超過lru
屬性時,計算纔會出問題。
比如對象 A
記錄的 lru
是 1
天,而 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
按以下方式遞增:
- 提取
0
和1
之間的隨機數R
。 counter
- 初始值(默認爲5
),得到一個基礎差值,如果這個差值小於0
,則直接取0
,爲了方便計算,把這個差值記爲baseval
。- 概率
P
計算公式爲:1/(baseval * lfu_log_factor + 1)
。 - 如果
R < P
時,頻次進行遞增(counter++
)。
公式中的 lfu_log_factor
稱之爲對數因子,默認是 10
,可以通過參數來進行控制:
lfu_log_factor 10
下圖就是對數因子 lfu_log_factor
和頻次 counter
增長的關係圖:
圖片
可以看到,當對數因子 lfu_log_factor
爲 100
時,大概是 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
具體算法如下:
- 獲取當前時間戳,轉化爲分鐘 後取低
16
位(爲了方便後續計算,這個值記爲now
)。 - 取出對象內的
lru
屬性中的高16
位(爲了方便後續計算,這個值記爲ldt
)。 - 當
lru
>now
時,默認爲過了一個週期(16
位,最大65535
),則取差值65535-ldt+now
:當lru
<=now
時,取差值now-ldt
(爲了方便後續計算,這個差值記爲idle_time
)。 - 取出配置文件中的
lfu_decay_time
值,然後計算:idle_time / lfu_decay_time
(爲了方便後續計算,這個值記爲num_periods
)。 - 最後將
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,直到鎖丟失,獲取線程不持有該鎖
Redisson分佈式鎖存儲值格式
hset命令賦值,key爲鎖的名稱,field爲“客戶端唯一ID:線程ID”,value爲1
客戶端唯一id,就是uuid,value爲可重入鎖次數
建議
建議使用Redisson,源碼中加鎖/釋放鎖操作都是用lua腳本完成的,封裝的非常完善,開箱即用。
機制,failover策略不可靠
主從同步通常是異步的,並不能真正的容錯。
造成鎖不獨享的場景如下圖所示:
- 客戶端A申請從master實例獲取鎖key=test001,由於之前key=test001在master實例上不存在,所以客戶端A獲取鎖成功。
- master在通過異步主從同步將key=test001同步至slave之前掛掉了,此時slave經過failover升級爲master,但是此時slave上並無key=test001。
- 此時,客戶端B申請從redis獲取鎖key=test001,由於此時slave上不存在key=test001,同樣的,客戶端B獲取鎖成功。
- 最終的結果是,由於關鍵時刻的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 是爲了節約內存而開發的。和各種語言的數組類似,它是由連續的內存塊組成的,這樣一來,由於內存是連續的,就減少了很多內存碎片和指針的內存佔用,進而節約了內存。
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
- 內存到達上限,先要根據淘汰策略剔除一部分數據,再把新數據寫入
rdb
,aop 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
節點的過程, 其過程如下:
slave
發現自己的master
不可用;slave
將記錄集羣的currentEpoch
(選舉週期)加1,並廣播FAILOVER_AUTH_REQUEST
信息進行選舉;- 其他節點收到
FAILOVER_AUTH_REQUEST
信息後,只有其他的master
可以進行響應,master
收到消息後返回FAILOVER_AUTH_ACK
信息,對於同一個Epoch
,只能響應一次ack; slave
收集maste返回的ack消息slave
判斷收到的ack消息個數是否大於半數的master
個數,若是,則變成新的master
;- 廣播
Pong
消息通知其他集羣節點,自己已經成爲新的master
。
注意:從節點並不是在主節點一進入 FAIL 狀態就馬上嘗試發起選舉,而是有一定延遲,一定的延遲確保我們等待FAIL狀態在集羣中傳播,slave
如果立即嘗試選舉,其它masters
或許尚未意識到FAIL狀態,可能會拒絕投票。
- 延遲計算公式:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK
表示此slave
已經從master
複製數據的總量的rank
。Rank
越小代表已複製的數據越新。這種方式下,持有最新數據的slave
將會首先發起選舉(理論上)。
Redis集羣爲什麼至少需要三個master節點?
因爲新master
的選舉需要大於半數的集羣master
節點同意才能選舉成功,如果只有兩個master
節點,當其中一個掛了,是達不到選舉新master
的條件的。
Redis集羣爲什麼至少推薦節點數爲奇數?
奇數個master
節點可以在滿足選舉該條件的基礎上節省一個節點,比如三個master
節點和四個master
節點的集羣相比,大家如果都掛了一個master
節點都能選舉新master
節點,如果都掛了兩個master
節點都沒法選舉新master
節點了,所以奇數的master
節點更多的是從節省機器資源角度出發說的。
網絡不穩定是否會是否引起選舉?
真實世界的機房網絡往往並不是風平浪靜的,它們經常會發生各種各樣的小問題。比如網絡抖動就是非常常見的一種現象,突然之間部分連接變得不可訪問,然後很快又恢復正常。
爲解決這種問題,Redis Cluster
提供了一種選項cluster-node-timeout
,表示當某個節點持續 timeout
的時間失聯時,纔可以認定該節點出現故障,需要進行主從切換。如果沒有這個選項,網絡抖動會導致主從頻繁切換 (數據的重新複製)。
Redis主從數據同步
全量複製
- slave第一次啓動時,連接Master,發送PSYNC命令,格式爲
psync {runId} {offset}
{runId}
爲master的運行id;{offset}
爲slave自己的複製偏移量- 由於此時是slave第一次連接master,slave不知道master的runId,也不知道自己偏移量,這時候會傳一個問號和-1,告訴master節點是第一次同步。格式爲
psync ? -1
- 當master接收到
psync ? -1
時,就知道slave是要全量複製,就會將自己的runId
和offset
告知slave,回覆命令+fullresync {runId} {offset}
。同時,master會執行bgsave
命令來生成RDB文件,並使用緩衝區記錄此後的所有寫命令
- slave接受到master的回覆命令後,會保存master的
runId
和offset
- slave此時處於同步狀態,如果此時收到請求,當配置參數
slave-server-stale-data yes
時,會響應當前請求,no
則返回錯誤。
- master
bgsave
執行完畢,向Slave發送RDB文件,同時繼續緩衝此期間的寫命令。RDB文件發送完畢後,開始向Slave發送存儲在緩衝區的寫命令 - slave收到RDB文件,丟棄所有舊數據,開始載入RDB文件;並執行Master發來的所有的存儲在緩衝區裏的寫命令。
- 此後 master 每執行一個寫命令,就向Slave發送相同的寫命令。
增量複製
- 如果出現網絡閃斷或者命令丟失等異常情況時,當主從連接恢復後,由於從節點之前保存了自身已複製的偏移量和主節點的運行ID。因此會把它們當作psync參數發送給主節點,要求進行部分複製操作,格式爲
psync {runId} {offset}
- 主節點接到psync命令後首先覈對參數runId是否與自身一致,如果一致,說明之前複製的是當前主節點;之後根據參數offset在自身複製積壓緩衝區查找,如果偏移量之後的數據存在緩衝區中,則對從節點發送+CONTINUE響應,表示可以進行部分複製;否則進行全量複製。
- 主節點根據偏移量把複製積壓緩衝區裏的數據發送給從節點,保證主從複製進入正常狀態
主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。
先更新數據庫,再刪除緩存,會有什麼問題?
先更新數據庫,再刪除緩存。可能出現以下情況:
- 如果更新完數據庫,
Java
服務提交了事務,然後掛掉了,那Redis
還是會執行,這樣也會不一致。 - 如果更新數據庫成功,刪除緩存失敗了,那麼會導致數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。
先刪除緩存,再更新數據庫。
- 如果刪除緩存失敗,那就不更新數據庫,緩存和數據庫的數據都是舊數據,數據是一致的。
- 如果刪除緩存成功,而數據庫更新失敗了,那麼數據庫中是舊數據,緩存中是空的,數據不會不一致。因爲讀的時候緩存沒有,所以去讀了數據庫中的舊數據,然後更新到緩存中。
先刪除緩存,在寫數據庫成功之前,如果有讀請求發生,可能導致舊數據入緩存,引發數據不一致,怎麼處理?
分佈式鎖
Redission框架中的看門狗原理
參考:
Redis: 分佈式鎖的官方算法RedLock以及Java版本實現庫Redisson
Redis 緩存過期(maxmemory) 配置/算法 詳解
細說Redis分佈式鎖:setnx/redisson/redlock?瞭解一波?