目錄
數據類型
字符串:
List:
HASH:
Set:
Zset:
BitMap:(這個及以下是后來新增的數據結構)
HyperLogLog:
GEO:
Stream:
主要數據結構
SDS
壓縮列表
HASH
整數結合
跳表
quickList
listpack?編輯
持久化
AOF(Append only FIle)
AOF寫回策略
AOF重寫
aof后臺重寫
rdb快照
混合持久化
?布隆過濾器
分布式鎖
數據類型
五種常用數據類型:字符串,List,HASH,Set,Zset(也叫sorted Set)
字符串:
key-Value形式儲存,key可以理解為字符串的名字,Value是實際字符串的內容
底層有SDS實現(SDS:簡單動態字符串),個人理解是基本字符串的擴展。短的時候是embstr,長了是raw。簡單理解就是,Redis額外開辟了一或二個額外空間來儲存字符串相關的內容,比如字符串的長度,字符串的實際地址等。
基本的使用語句是set,get,setNX等。
字符串可以用來當做分布式鎖:很多個客戶端共同使用一個Redis,在進行一個操作的時候,設置一個字符串:set lock_key unique_value NX PX 10000 后面的PX 10000 是超過這個時間自動釋放鎖(防止死鎖)NX是not exit 不存在這個字符串時才會加鎖。
字符串也可以用作緩存:儲存數據就是了;也可以用作計數器:字符串有自增指令INCR和INCRBY,可以使字符串的值加1或加多,并且由于是單線程實現的,不需要考慮競態問題
List:
Key-value儲存形式,是一個雙端隊列
內部結構listPack
基本語法LPUSH,LPOP,RPUSH,RPOP等,并且有阻塞獲取的語法BRPOP:阻塞,右邊,取
可以用作簡單的消息隊列,但有些問題:List實現的消息隊列不能實現持久化;不能多個消費者處理同一個消息;需要手動維護每個消息的唯一id
HASH:
Key-field-value儲存形式, key是HASH的名稱,filed可以類比哈希表的key,Value可以類比哈希表的Value
內部儲存結構是listPack和哈希表
基本語法是HSET、HMSET等(M指的是Multi,一次處理多個)
Set:
Key-filed儲存形式
內部實現時整數集合和哈希表
和普通的set一樣,每個元素唯一,但是是無序存儲。
基本語法是SADD,SREM,SCARD(cardinality,集合的勢),SMEMBERS(獲取集合中的全部元素)。并且SET支持SUNION,SINTER,SDIFF(并,交,差)集合運算
Zset:
key-score-value儲存形式,集合中的元素按照score的順序排序。
內部實現時listPack和跳表
基本語法是ZADD,ZREM等。支持交、并操作(不支持差了)
BitMap:(這個及以下是后來新增的數據結構)
每個元素都是二進制的(相當于維護了一個二進制的數組,但不需要實現設定大小)
基本語法SETBIT,GETBIT等,支持二進制運算:與&,或|,異或^,反~
主要用于制作布隆過濾器或記錄一些二進制的狀態(比如簽到等)
HyperLogLog:
用處就是可以用一個相當小(12k)的內存來統計具體有多少個元素(能用,但是有誤差0.81%)
常用指令PFADD(添加),PFCOUNT(計數),PFMERGE(合并)
比如向這個結構中PFADD100w個數據,可以用PFCOUNT快速得到大致的數量
GEO:
地圖存儲,底層用的是ZSET
Stream:
主要解決list實現消息隊列時不能持久化,不能多個消費者處理的問題
基本語法XADD,XREAD等。可以定義XGROUP(消費者組),用消費者組讀取內容(一組可以有多個消費者)。組內消費者不能讀取同一條數據,但不同組的消費者可以處理同一條內容。
處理完成后向Stream返回XACK確認消息已經處理完畢。沒處理完的數據可以用SPENDING獲取,這樣當Redis意外關閉時未處理的消息也不會丟失。
但是Stream存儲的消息中間件可能會丟失,并且大小受內存大小的限制。如果有高要求的話還是用kafka等專業消息隊列比較好
主要數據結構
我們之前說的,上面的數據類型都有一個key,以及對應的value。(key是數據的名稱,value就是對應的數據)。而Redi是用一個哈希表來保存這些全部的數據的。在下圖中可以看到,Redi維護了一個dictEntry,就是哈希表中儲存的鍵值對。每個對有一個key指針和value指針,key指針指向一個String,value指針指向對應的具體數據類型。
Redis中存儲的都是Redis對象。有一個type標志這個對象是哪一種數據類型(比如Zset),encoding表示底層數據結構。比如SDS會有embstr和raw兩種選擇。最后的就是一個指針,指向真正的數據
SDS
就是設計了一個頭結構,記錄了字符串的長度和已經分配的內存空間大小。這樣獲取長度的復雜度為O(1),并且執行字符串拼接的時候不用再考慮內存分配(C的問題),最后,由于已經標明了字符串的長度,因此不需要再為字符串添加‘/0’的終止符,使得字符串現在不止可以儲存文本,也可以儲存二進制數據了
壓縮列表
結構的會儲存結構整體占用的字節數,列表尾的偏移量,節點數和結束點。(不再是普通鏈表的分塊存儲,這樣提高了CPU緩存的命中率)
每個節點會儲存上一個節點的長度,本節點的數據類型和長度,實際數據內容。
壓縮列表的兩個問題就是:查詢中間節點需要的復雜度是O(n);連鎖更新問題(主要是在改變節點內容的時候需要改變下一個節點的prevlen值,可能會出現改一個之后后面的很多個都要改的情況。)
HASH
和java7及以前的邏輯是基本上一樣的。不過是擴容的時候不是一次性復制,而是用到哪個bin,就把哪個bin的內容復制過來
整數結合
就是一塊連續的內存空間,三個量:編碼方式,元素個數和保存元素的數組。通過二分查找實現有序數組進而實現唯一。不過每個數字占的大小有限制(比如都要小于16位),如果有一個數超過這個限制,那么所有的數都要大小升級,數組整體擴容
跳表
很多層,每層維護一個鏈表。目的是為了實現鏈表的更快查詢,大致結構長這個樣子。鏈表的層數要事先設計好。節點按照一定的概率向上升(這個概率要人為給定)
跳表的主要優勢是他結構簡單易于實現,范圍查詢更快,插入刪除需要的操作更少,每個節點儲存的指針更少(跳表平均1/(1-p),平衡樹兩個)
quickList
添加元素的時候,不再直接新建一個節點,而是先看這個節點對應的壓縮鏈表能否容納這個節點(就是看列表中是否還有足夠的空閑內存和可容納節點個數),如果可以的話就放進去,不能的話才新建一個節點
listpack
和壓縮鏈表相比,實際上就是把pre(上一個節點的長度)刪掉,換成了當前節點的長度。相當于壓縮鏈表的改進版。
持久化
Redis的持久化(其實就是將內容寫入到磁盤)有AOF和RDB兩種策略
AOF(Append only FIle)
就是寫一個aof文件,這個文件保存在磁盤中。具體來說有以下處理過程:
Redis接受到寫命令之后,先在內存中修改對應的數據,然后把這個修改數據的命令儲存在aof緩沖區中,等待合適的時機把緩沖區中的內容寫入到aof文件中。
AOF寫回策略
所謂的合適的時機就是aof的刷盤策略,總共有三種:always、everysec和no。第一種就是每次有修改命令,都把內容寫入到內核空間的page cache中同時寫入到磁盤中。everysec是每一秒寫一次,no是交給操作系統決定什么時候寫入磁盤。(其實就是aof的三種刷盤策略,主要是標明主進程什么時候調用操作系統的fsync方法進行刷盤,aof緩沖區中的內容都是寫完之后直接調用i/o系統的Write方法寫入到page cache中)
AOF重寫
如果aof文件太大,那么就會觸發aof文件的重寫。主進程先fork一個子進程(就是復制了一份頁面給子進程),子進程根據頁表內容讀取內存并寫入到新的aof文件中(實際上是生成一個寫這條內容的指令,并將這條指令保存下來)。這期間父進程和子進程是共享物理內存的,當父進程對某一塊內存進行修改的時候,才會將這塊內存原有的內容復制給子進程,父進程修改內存(所謂的寫時復制)。這樣就保證了子進程寫的內容都是父進程fork時內存的樣子。
aof后臺重寫
當子進程生成新的aof文件時,主線程會正常執行內容(并不會阻塞),這些正常執行的操作將不止被緩存在aof緩沖區中,還會被寫在aof重寫緩沖區中。子進程將頁表中的內容寫好后,會告訴主進程已經重寫好(發送一條信號)。主進程將aof重寫緩沖區中的內容追加到新的aof文件中,然后將老aof文件覆蓋。
rdb快照
有兩種策略:save和bgsave。save是主進程完成快照生成工作,此時Redis會被阻塞,bgsave是fork一個子進程生成對應的快照。
其實邏輯跟aof重寫差不多,只不過aof重寫的時候是生成語句并保存,rdb重寫是記錄對應的數據內容,用的時候直接讀取就可以了。并且rdb快照生成由于是全量復制,所以會很慢,需要平衡生成快照的時機和丟失內容多少的關系。(aof也是全量掃描,但有的內容可能在寫的時候可以由一個可重復指令完成,因此某些情況下會比rdb快)
混合持久化
在aof重寫時,不再生成讀取內容生成指令,而是直接按照rdb的模式復制,在處理aof重寫緩存的時候才是aof的記錄語句的形式。
過期刪除和內存淘汰
這兩個操作并不相同:過期刪除指的是我們可以對一個Redis對象設定存活時間,超出這個時間后刪除對象時過期刪除,內存淘汰則是當Redis的儲存空間不足時,需要對一些對象進行刪除
過期刪除:
我們知道在設定一個Redis對象的時候,可以設置他的過期時間,那么如何對過期對象進行刪除就有三種策略:
- 定時刪除。這種策略指的是在設定一個可以過期的Redis對象時,同時也啟動一個定時事件,事件到期后由一個專門的線程來對他進行刪除。這種方法思路很簡單,但實際使用會消耗太多資源,比如CPU時間等,并且也可能會導致一些熱點數據同時大量被刪除,因此這種刪除策略并沒有被采用
- 定期刪除。這種策略說的是每過一段時間檢查一部分的對象(一般是20個),刪除過期的對象,如果說過期對象大于五個,就繼續隨機抽取然后刪除,直到超時或者過期對象少于五個
- 惰性刪除。這種指的是完全不檢查,等到用的時候再看有沒有過期,過期的話刪除。這種策略的問題也十分明顯:會有大量的過期對象沒有被刪除,占用空間
Redis使用的是定期和惰性結合,一方面會定期檢查,另一方面使用對象的時候如果過期就刪除
Redis實現了一個HashMap來儲存每個可過期對象的對象地址和對應的創建時間,使用的時候比較這個創建時間。需要指明的是,這個HashMap和我們可使用的hash并不一樣:我們使用的hash數據結構要求key是字符串,這個HashMap存的是地址,并且這個HashMap是Redis底層自己實現的,我們用不了
內存淘汰
當內存不夠用的時候,就需要淘汰一些不常用的對象。淘汰手段一般是LRU和LFU。LRU指的是找到最久沒有使用的,LFU指的是使用次數最少的。Redis對這兩個都做了優化
- LRU:每個對象儲存最近被使用的時間,刪除的時候隨機采樣五個,刪除最久沒用的內個
- LFU:這個相對復雜一點,Redis給每個對象增加了一個LFU字段,這個字段記錄了上次使用的時間和一個標志使用頻次的數,這個頻次的數初始為5,對象每次被使用時會先減1,然后根據上次使用時間和現在的時間算一個權重出來,頻次再加上這個權重作為最終的結果
具體使用哪個Redis提供了一些參數,修改這些參數就行了
主從復制
具體應用:
?布隆過濾器
可以把他理解成一個只能添加和查找元素是否存在的數據結構(某種意義上的Hashmap?)
他的特點是可以實現快速查找,但查找只能保證他認為不在的不在,他認為在的也不一定在
布隆過濾器底層數據結構是一個長二值數組。同時設置了幾個hash函數。
????????每次插入元素時,就計算這個元素對應的hash值,并將關于數組長度取模后的對應位置的值設為1。
? ? ? ? 在查詢的時候,計算這個元素在數組中對應的幾個位置(也是根據hash值取模得到的),如果全為1,那么認為他存在(當然也可能不在)。
應用場景:解決redis緩存穿透
實際使用:Redis其實并沒有實現所謂的BitMap數據結構,他是依賴字符串實現的。在我的項目中,布隆過濾器是這樣設計的:
首先根據公式計算出需要的哈希函數數量和數組長度:n是預估的數量個數,p是誤判率
計算得到的m就是需要的布隆過濾器長度(指定的字符串所占的比特數),k是需要的哈希函數個數,取整。
選擇一個哈希函數(我選的是一個,不是k個!)比如md5,計算當前元素的哈希值。將計算的到的哈希值拆成k分,每份再計算一個hashCode(),對長度取模就得到一個數組,長度為k。加入的時候將這個數組每一位上的值設置為true。查找的時候就看對應位有沒有false,只要有一個就說明當前元素沒有被加入過。
分布式鎖
分布式鎖的實現也是字符串。具體來說,每個進程(或者線程)公用一個Redis的時候就可以設計分布式鎖,使得只有一個線程可以使用資源。
分布式鎖主要依賴字符串的setIfAbsent和get方法。當一個線程嘗試獲取鎖的時候,就會在Redis中嘗試設置一個字符串,設置成功的話就相當于成功獲取了鎖,否則就是獲取鎖失敗,任務結束之后刪除字符串就相當于釋放了鎖。
單一Redis節點需要考慮這幾個問題:
- 某個線程在刪除字符串時,要確保這個字符串是他自己設置的
- 要保證刪除操作是原子的
- 釋放鎖之前鎖不能自動過期
對于第一個問題,我們考慮這個場景:兩個線程都想要獲取鎖,線程A獲得鎖,但超時了,鎖自動釋放,線程B新建了鎖,線程A執行完任務刪除了鎖,但這時他刪除的是線程B新建的鎖。解決思路就是用字符串的key當做鎖名,value當做線程對鎖的持有證
第二個問題,要先檢查鎖存在才能釋放,這中間存在時間,這段時間里可能會出現鎖過期然后另外一個線程新建鎖的問題,因此要讓Redis一次性檢查并且釋放。具體實現思路就是使用lua腳本。Spring中,用字符串寫好腳本然后提交execute。
第三個問題,采用看門狗機制,對所有的任務線程設計一個定時任務線程池,新建任務線程的時候就把自己線程的信息傳遞給看門狗,看門狗定期用收到的信息執行鎖延時任務:只要線程還在運行,就延長鎖。任務線程結束之后再中斷這個定期任務。(注意這里要用到線程的ThreadLocal來記錄每個線程獨立的信息,比如他的鎖名,鎖持續時間等等)
如果多Redis節點的話需要考慮redLock,但我這里是單節點就沒有實現