以下文章來源于sowhat1412 ,作者sowhat1412
高清思維導圖已同步Git:https://github.com/SoWhat1412/xmindfile
1、基本類型及底層實現
1.1、String
用途:
適用于簡單key-value存儲、setnx key value實現分布式鎖、計數器(原子性)、分布式全局唯一ID。
底層:C語言中String用char[]數組表示,源碼中用SDS
(simple dynamic string)封裝char[],這是是Redis存儲的最小單元
,一個SDS最大可以存儲512M信息。
struct sdshdr{
unsigned int len; // 標記char[]的長度
unsigned int free; //標記char[]中未使用的元素個數
char buf[]; // 存放元素的坑
}
Redis對SDS再次封裝生成了RedisObject
,核心有兩個作用:
- 說明是5種類型哪一種。
- 里面有指針用來指向 SDS。
當你執行set name sowhat
的時候,其實Redis會創建兩個RedisObject對象,鍵的RedisObject 和 值的RedisOjbect 其中它們type = REDIS_STRING,而SDS分別存儲的就是 name 跟 sowhat 字符串咯。
并且Redis底層對SDS有如下優化:
- SDS修改后大小 > 1M時 系統會多分配空間來進行
空間預分配
。- SDS是
惰性釋放空間
的,你free了空間,可是系統把數據記錄下來下次想用時候可直接使用。不用新申請空間。
1.2、List
查看源碼底層 adlist.h
會發現底層就是個 雙端鏈表,該鏈表最大長度為2^32-1。常用就這幾個組合。
lpush + lpop = stack 先進后出的棧
lpush + rpop = queue 先進先出的隊列
lpush + ltrim = capped collection 有限集合
lpush + brpop = message queue 消息隊列
一般可以用來做簡單的消息隊列,并且當數據量小的時候可能用到獨有的壓縮列表來提升性能。當然專業點還是要 RabbitMQ、ActiveMQ等
1.3、Hash
散列非常適用于將一些相關的數據存儲在一起,比如用戶的購物車。該類型在日常用途還是挺多的。
這里需要明確一點:Redis中只有一個K,一個V。其中 K 絕對是字符串對象,而 V 可以是String、List、Hash、Set、ZSet任意一種。
hash的底層主要是采用字典dict的結構,整體呈現層層封裝。從小到大如下:
1.3.1、dictEntry
真正的數據節點,包括key、value 和 next 節點。
1.3.2、dictht
1、數據 dictEntry 類型的數組,每個數組的item可能都指向一個鏈表。
2、數組長度 size。
3、sizemask 等于 size - 1。
4、當前 dictEntry 數組中包含總共多少節點。
1.3.3、dict
1、dictType 類型,包括一些自定義函數,這些函數使得key和value能夠存儲
2、rehashidx 其實是一個標志量,如果為
-1
說明當前沒有擴容,如果不為 -1
則記錄擴容位置。3、dictht數組,兩個Hash表。
4、iterators 記錄了當前字典正在進行中的迭代器
組合后結構就是如下:
1.3.4、漸進式擴容
為什么 dictht ht[2]是兩個呢?目的是在擴容的同時不影響前端的CURD,慢慢的把數據從ht[0]轉移到ht[1]中,同時rehashindex
來記錄轉移的情況,當全部轉移完成,將ht[1]改成ht[0]使用。
rehashidx = -1說明當前沒有擴容,rehashidx != -1則表示擴容到數組中的第幾個了。
擴容之后的數組大小為大于used2的2的n次方*的最小值,跟 HashMap 類似。然后挨個遍歷數組同時調整rehashidx的值,對每個dictEntry[i] 再挨個遍歷鏈表將數據 Hash 后重新映射到 dictht[1]里面。并且 dictht[0].use 跟 dictht[1].use 是動態變化的。
整個過程的重點在于rehashidx
,其為第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。
停止之后如果對該對象進行操作,那是什么樣子的呢?
1、如果是新增,則直接新增后第二個數組,因為如果新增到第一個數組,以后還是要移過來,沒必要浪費時間
2、如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。
1.4、Set
如果你明白Java中HashSet是HashMap的簡化版那么這個Set應該也理解了。都是一樣的套路而已。這里你可以認為是沒有Value的Dict。看源碼 t.set.c
就可以了解本質了。
int setTypeAdd(robj *subject, robj *value) {
long long llval;
if (subject->encoding == REDIS_ENCODING_HT) {
// 看到底層調用的還是dictAdd,只不過第三個參數= NULL
if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
incrRefCount(value);
return 1;
}
....
1.5、ZSet
范圍查找 的天敵就是 有序集合,看底層 redis.h
后就會發現 Zset用的就是可以跟二叉樹媲美的跳躍表
來實現有序。跳表就是多層鏈表的結合體,跳表分為許多層(level),每一層都可以看作是數據的索引,這些索引的意義就是加快跳表查找數據速度。
每一層的數據都是有序的,上一層數據是下一層數據的子集,并且第一層(level 1)包含了全部的數據;層次越高,跳躍性越大,包含的數據越少。并且隨便插入一個數據該數據是否會是跳表索引完全隨機的跟玩骰子一樣。
跳表包含一個表頭,它查找數據時,是從上往下,從左往右
進行查找。現在找出值為37的節點為例,來對比說明跳表和普遍的鏈表。
-
沒有跳表查詢 比如我查詢數據37,如果沒有上面的索引時候路線如下圖:圖片
-
有跳表查詢 有跳表查詢37的時候路線如下圖:圖片
應用場景:
積分排行榜、時間排序新聞、延時隊列。
1.6、Redis Geo
以前寫過Redis Geo核心原理解析,想看的直接跳轉即可。他的核心思想就是將地球近似為球體來看待,然后 GEO利用 GeoHash 將二維的經緯度轉換成字符串,來實現位置的劃分跟指定距離的查詢。
1.7、HyperLogLog
HyperLogLog :是一種概率
數據結構,它使用概率算法來統計集合的近似基數。而它算法的最本源則是伯努利過程 + 分桶 + 調和平均數
。具體實現可看 HyperLogLog 講解。
功能:誤差允許范圍內做基數統計 (基數就是指一個集合中不同值的個數) 的時候非常有用,每個HyperLogLog的鍵可以計算接近2^64不同元素的基數,而大小只需要12KB。錯誤率大概在0.81%。所以如果用做 UV 統計很合適。
HyperLogLog底層 一共分了 2^14 個桶,也就是 16384 個桶。每個(registers)桶中是一個 6 bit 的數組,這里有個騷操作就是一般人可能直接用一個字節當桶浪費2個bit空間,但是Redis底層只用6個然后通過前后拼接實現對內存用到了極致,最終就是 16384*6/8/1024 = 12KB。
1.8、bitmap
BitMap 原本的含義是用一個比特位來映射某個元素的狀態。由于一個比特位只能表示 0 和 1 兩種狀態,所以 BitMap 能映射的狀態有限,但是使用比特位的優勢是能大量的節省內存空間。
在 Redis 中BitMap 底層是基于字符串類型實現的,可以把 Bitmaps 想象成一個以比特位為單位的數組,數組的每個單元只能存儲0和1,數組的下標在 Bitmaps 中叫做偏移量,BitMap 的 offset 值上限 2^32 - 1。
- 用戶簽到
key = 年份:用戶id offset = (今天是一年中的第幾天) % (今年的天數)
- 統計活躍用戶
使用日期作為 key,然后用戶 id 為 offset 設置不同offset為0 1 即可。
PS : Redis 它的通訊協議是基于TCP的應用層協議 RESP(REdis Serialization Protocol)。
1.9、Bloom Filter
使用布隆過濾器得到的判斷結果:不存在的一定不存在,存在的不一定存在
。
布隆過濾器 原理:
當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點(有效降低沖突概率),把它們置為1。檢索時,我們只要看看這些點是不是都是1就知道集合中有沒有它了:如果這些點有任何一個為0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
想玩的話可以用Google的guava
包玩耍一番。
1.10 發布訂閱
redis提供了發布、訂閱
模式的消息機制,其中消息訂閱者與發布者不直接通信,發布者向指定的頻道(channel)發布消息,訂閱該頻道的每個客戶端都可以接收到消息。不過比專業的MQ(RabbitMQ RocketMQ ActiveMQ Kafka)相比不值一提,這個功能就算球了。
2、持久化
因為Redis數據在內存,斷電既丟,因此持久化到磁盤是必須得有的,Redis提供了RDB跟AOF兩種模式。
2.1、RDB
RDB 持久化機制,是對 Redis 中的數據執行周期性的持久化。更適合做冷備。優點:
1、壓縮后的二進制文,適用于備份、全量復制,用于災難恢復加載RDB恢復數據遠快于AOF方式,適合大規模的數據恢復。
2、如果業務對數據完整性和一致性要求不高,RDB是很好的選擇。數據恢復比AOF快。
缺點:
1、RDB是周期間隔性的快照文件,數據的完整性和一致性不高,因為RDB可能在最后一次備份時宕機了。
2、備份時占用內存,因為Redis 在備份時會獨立fork一個子進程,將數據寫入到一個臨時文件(此時內存中的數據是原來的兩倍哦),最后再將臨時文件替換之前的備份文件。所以要考慮到大概兩倍的數據膨脹性。
注意手動觸發及COW:
1、
SAVE
直接調用 rdbSave ,阻塞
Redis 主進程,導致無法提供服務。2、BGSAVE
則 fork 出一個子進程,子進程負責調用 rdbSave ,在保存完成后向主進程發送信號告知完成。在BGSAVE 執行期間仍可以繼續處理客戶端的請求。3、Copy On Write 機制,備份的是開始那個時刻內存中的數據,只復制被修改內存頁數據,不是全部內存數據。
4、Copy On Write 時如果父子進程大量寫操作會導致分頁錯誤。
2.2、AOF
AOF 機制對每條寫入命令作為日志,以 append-only 的模式寫入一個日志文件中,因為這個模式是只追加的方式,所以沒有任何磁盤尋址的開銷,所以很快,有點像 Mysql 中的binlog。AOF更適合做熱備。
優點:
AOF是一秒一次去通過一個后臺的線程fsync操作,數據丟失不用怕。
缺點:
1、對于相同數量的數據集而言,AOF文件通常要大于RDB文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。
2、根據同步策略的不同,AOF在運行效率上往往會慢于RDB。總之,每秒同步策略的效率是比較高的。
AOF整個流程分兩步:第一步是命令的實時寫入,不同級別可能有1秒數據損失。命令先追加到aof_buf
然后再同步到AO磁盤,如果實時寫入磁盤會帶來非常高的磁盤IO,影響整體性能。
第二步是對aof文件的重寫,目的是為了減少AOF文件的大小,可以自動觸發或者手動觸發(BGREWRITEAOF),是Fork出子進程操作,期間Redis服務仍可用。
1、在重寫期間,由于主進程依然在響應命令,為了保證最終備份的完整性;它
依然會寫入舊
的AOF中,如果重寫失敗,能夠保證數據不丟失。2、為了把重寫期間響應的寫入信息也寫入到新的文件中,因此也會
為子進程保留一個buf
,防止新寫的file丟失數據。3、重寫是直接把
當前內存的數據生成對應命令
,并不需要讀取老的AOF文件進行分析、命令合并。4、無論是 RDB 還是 AOF 都是先寫入一個臨時文件,然后通過
rename
完成文件的替換工作。
關于Fork的建議:
1、降低fork的頻率,比如可以手動來觸發RDB生成快照、與AOF重寫;
2、控制Redis最大使用內存,防止fork耗時過長;
3、配置牛逼點,合理配置Linux的內存分配策略,避免因為物理內存不足導致fork失敗。
4、Redis在執行
BGSAVE
和BGREWRITEAOF
命令時,哈希表的負載因子>=5,而未執行這兩個命令時>=1。目的是盡量減少寫操作,避免不必要的內存寫入操作。5、哈希表的擴展因子:哈希表已保存節點數量 / 哈希表大小。因子決定了是否擴展哈希表。
2.3、恢復
啟動時會先檢查AOF(數據更完整)文件是否存在,如果不存在就嘗試加載RDB。
2.4、建議
既然單獨用RDB會丟失很多數據。單獨用AOF,數據恢復沒RDB來的快,所以出現問題了第一時間用RDB恢復,然后AOF做數據補全才說王道。
3、Redis為什么那么快
3.1、 基于內存實現:
數據都存儲在內存里,相比磁盤IO操作快百倍,操作速率很快。
3.2、高效的數據結構:
Redis底層多種數據結構支持不同的數據類型,比如HyperLogLog它連2個字節都不想浪費。
3.3、豐富而合理的編碼:
Redis底層提供了 豐富而合理的編碼 ,五種數據類型根據長度及元素的個數適配不同的編碼格式。
1、String:自動存儲int類型,非int類型用raw編碼。
2、List:字符串長度且元素個數小于一定范圍使用 ziplist 編碼,否則轉化為 linkedlist 編碼。
3、Hash:hash 對象保存的鍵值對內的鍵和值字符串長度小于一定值及鍵值對。
4、Set:保存元素為整數及元素個數小于一定范圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼。
5、Zset:保存的元素個數小于定值且成員長度小于定值使用 ziplist 編碼,任意條件不滿足,則使用 skiplist 編碼。
3.4、合適的線程模型:
I/O 多路復用
模型同時監聽客戶端連接,多線程是需要上下文切換的,對于內存數據庫來說這點很致命。
3.5、 Redis6.0后引入多線程
提速:
要知道 讀寫網絡的read/write系統耗時 >>
Redis運行執行耗時,Redis的瓶頸主要在于網絡的 IO 消耗, 優化主要有兩個方向:
1、提高網絡 IO 性能,典型的實現比如使用 DPDK 來替代內核網絡棧的方式
2、使用多線程充分利用多核,典型的實現比如 Memcached。
協議棧優化的這種方式跟 Redis 關系不大,支持多線程是一種最有效最便捷的操作方式。所以Redis支持多線程主要就是兩個原因:
1、可以充分利用服務器 CPU 資源,目前主線程只能利用一個核
2、多線程任務可以分攤 Redis 同步 IO 讀寫負荷
關于多線程須知:
- Redis 6.0 版本 默認多線程是關閉的 io-threads-do-reads no
- Redis 6.0 版本 開啟多線程后 線程數也要 謹慎設置。
- 多線程可以使得性能翻倍,但是多線程只是用來處理網絡數據的讀寫和協議解析,執行命令仍然是單線程順序執行。
4、常見問題
4.1、緩存雪崩
雪崩定義:
Redis中大批量key在同一時間同時失效導致所有請求都打到了MySQL。而MySQL扛不住導致大面積崩塌。
雪崩解決方案:
1、緩存數據的過期時間加上個隨機值,防止同一時間大量數據過期現象發生。
2、如果緩存數據庫是分布式部署,將熱點數據均勻分布在不同搞得緩存數據庫中。
3、設置熱點數據永遠不過期。
4.2、緩存穿透
穿透定義:
緩存穿透 是 指緩存和數據庫中
都沒有
的數據,比如ID默認>0,黑客一直 請求ID= -12的數據那么就會導致數據庫壓力過大,嚴重會擊垮數據庫。
穿透解決方案:
1、后端接口層增加 用戶鑒權校驗,參數做校驗等。
2、單個IP每秒訪問次數超過閾值直接拉黑IP,關進小黑屋1天,在獲取IP代理池的時候我就被拉黑過。
3、從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫為key-null 失效時間可以為15秒防止惡意攻擊。
4、用Redis提供的 Bloom Filter 特性也OK。
4.3、緩存擊穿
擊穿定義:
現象:大并發集中對這一個熱點key進行訪問,當這個Key在失效的瞬間,持續的大并發就穿破緩存,直接請求數據庫。
擊穿解決:
設置熱點數據永遠不過期 加上互斥鎖也能搞定了
4.4、雙寫一致性
雙寫:緩存
跟數據庫
均更新數據,如何保證數據一致性?
1、先更新數據庫,再更新緩存
安全問題:線程A更新數據庫->線程B更新數據庫->線程B更新緩存->線程A更新緩存。
導致臟讀
。業務場景:讀少寫多場景,頻繁更新數據庫而緩存根本沒用。更何況如果緩存是疊加計算后結果更
浪費性能
。
2、先刪緩存,再更新數據庫
A 請求寫來更新緩存。
B 發現緩存不在去數據查詢舊值后寫入緩存。
A 將數據寫入數據庫,此時緩存跟數據庫不一致。
因此 FackBook 提出了 Cache Aside Pattern
失效:應用程序先從cache取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。
命中:應用程序從cache中取數據,取到后返回。
更新:
先把數據存到數據庫中,成功后,再讓緩存失效
。
4.5、腦裂
腦裂是指因為網絡原因,導致master節點、slave節點 和 sentinel集群處于不用的網絡分區,此時因為sentinel集群無法感知到master的存在,所以將slave節點提升為master節點 此時存在兩個不同的master節點就像一個大腦分裂成了兩個。其實在Hadoop
、Spark
集群中都會出現這樣的情況,只是解決方法不同而已(用ZK配合強制殺死)。
集群腦裂問題中,如果客戶端還在基于原來的master節點繼續寫入數據那么新的master節點將無法同步這些數據,當網絡問題解決后sentinel集群將原先的master節點降為slave節點,此時再從新的master中同步數據將造成大量的數據丟失。
Redis處理方案是redis的配置文件中存在兩個參數
min-replicas-to-write 3 表示連接到master的最少slave數量
min-replicas-max-lag 10 表示slave連接到master的最大延遲時間
如果連接到master的slave數量 < 第一個參數 且 ping的延遲時間 <= 第二個參數那么master就會拒絕寫請求,配置了這兩個參數后如果發生了集群腦裂則原先的master節點接收到客戶端的寫入請求會拒絕就可以減少數據同步之后的數據丟失。
4.6、事務
MySQL 中的事務還是挺多道道的還要,而在Redis中的事務只要有如下三步:
關于事務具體結論:
1、redis事務就是一次性、順序性、排他性的執行一個隊列中的一系列命令。
2、Redis事務沒有隔離級別的概念:批量操作在發送 EXEC 命令前被放入隊列緩存,并不會被實際執行,也就不存在事務內的查詢要看到事務里的更新,事務外查詢不能看到。
3、Redis不保證原子性:Redis中單條命令是原子性執行的,但事務不保證原子性。
4、Redis編譯型錯誤事務中所有代碼均不執行,指令使用錯誤。運行時異常是錯誤命令導致異常,其他命令可正常執行。
5、watch指令類似于樂觀鎖,在事務提交時,如果watch監控的多個KEY中任何KEY的值已經被其他客戶端更改,則使用EXEC執行事務時,事務隊列將不會被執行。
4.7、正確開發步驟
上線前
:Redis 高可用,主從+哨兵,Redis cluster,避免全盤崩潰。
上線時
:本地 ehcache 緩存 + Hystrix 限流 + 降級,避免MySQL扛不住。上線后
:Redis持久化采用 RDB + AOF 來保證斷點后自動從磁盤上加載數據,快速恢復緩存數據。
5、分布式鎖
日常開發中我們可以用 synchronized 、Lock 實現并發編程。但是Java中的鎖只能保證在同一個JVM進程內中執行。如果在分布式集群環境下用鎖呢?日常一般有兩種選擇方案。
5.1、 Zookeeper實現分布式鎖
你需要知道一點基本zookeeper
知識:
1、持久節點:客戶端斷開連接zk不刪除persistent類型節點 2、臨時節點:客戶端斷開連接zk刪除ephemeral類型節點 3、順序節點:節點后面會自動生成類似0000001的數字表示順序 4、節點變化的通知:客戶端注冊了監聽節點變化的時候,會調用回調方法
大致流程如下,其中注意每個節點只
監控它前面那個節點狀態,從而避免羊群效應
。關于模板代碼百度即可。
缺點:
頻繁的創建刪除節點,加上注冊watch事件,對于zookeeper集群的壓力比較大,性能也比不上Redis實現的分布式鎖。
5.2、 Redis實現分布式鎖
本身原理也比較簡單,Redis 自身就是一個單線程處理器,具備互斥的特性,通過setNX,exist等命令就可以完成簡單的分布式鎖,處理好超時釋放鎖的邏輯即可。
SETNX
SETNX 是SET if Not eXists的簡寫,日常指令是
SETNX key value
,如果 key 不存在則set成功返回 1,如果這個key已經存在了返回0。
SETEX
SETEX key seconds value 表達的意思是 將值 value 關聯到 key ,并將 key 的生存時間設為多少秒。如果 key 已經存在,setex命令將覆寫舊值。并且 setex是一個
原子性
(atomic)操作。
加鎖:
一般就是用一個標識唯一性的字符串比如UUID 配合 SETNX 實現加鎖。
解鎖:
這里用到了LUA腳本,LUA可以保證是原子性的,思路就是判斷一下Key和入參是否相等,是的話就刪除,返回成功1,0就是失敗。
缺點:
這個鎖是無法重入的,且自己實心的話各種邊邊角角都要考慮到,所以了解個大致思路流程即可,工程化還是用開源工具包就行。
5.3、 Redisson實現分布式鎖
Redisson 是在Redis基礎上的一個服務,采用了基于NIO的Netty框架,不僅能作為Redis底層驅動客戶端,還能將原生的RedisHash,List,Set,String,Geo,HyperLogLog等數據結構封裝為Java里大家最熟悉的映射(Map),列表(List),集(Set),通用對象桶(Object Bucket),地理空間對象桶(Geospatial Bucket),基數估計算法(HyperLogLog)等結構。
這里我們只是用到了關于分布式鎖的幾個指令,他的大致底層原理:
Redisson加鎖解鎖 大致流程圖如下:
6、Redis 過期策略和內存淘汰策略
6.1、Redis的過期策略
Redis中 過期策略 通常有以下三種:
1、定時過期:
每個設置過期時間的key都需要創建一個定時器,到過期時間就會立即對key進行清除。該策略可以立即清除過期的數據,對內存很友好;但是會占用大量的CPU資源去處理過期的數據,從而影響緩存的響應時間和吞吐量。
2、惰性過期:
只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對內存非常不友好。極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,占用大量內存。
3、定期過期:
每隔一定的時間,會掃描一定數量的數據庫的expires字典中一定數量的key,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和內存資源達到最優的平衡效果。
expires字典會保存所有設置了過期時間的key的過期時間數據,其中 key 是指向鍵空間中的某個鍵的指針,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵。
Redis采用的過期策略:惰性刪除
+ 定期刪除
。memcached采用的過期策略:惰性刪除
。
6.2、6種內存淘汰策略
Redis的內存淘汰策略是指在Redis的用于緩存的內存不足時,怎么處理需要新寫入且需要申請額外空間的數據。
1、volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
2、volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
3、volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
4、allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
5、allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰 6、no-enviction(驅逐):禁止驅逐數據,不刪除的意思。
面試常問常考的也就是LRU了,大家熟悉的LinkedHashMap
中也實現了LRU
算法的,實現如下:
class SelfLRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 傳遞進來最多能緩存多少數據
* @param cacheSize 緩存大小
*/
public SelfLRUCache(int cacheSize) {
// true 表示讓 linkedHashMap 按照訪問順序來進行排序,最近訪問的放在頭部,最老訪問的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當 map中的數據量大于指定的緩存個數的時候,就自動刪除最老的數據。
return size() > CACHE_SIZE;
}
}
6.2、總結
Redis的內存淘汰策略的選取并不會影響過期的key的處理。內存淘汰策略用于處理內存不足時的需要申請額外空間的數據,過期策略用于處理過期的緩存數據。
7、Redis 集群高可用
單機問題有機器故障、容量瓶頸、QPS瓶頸。在實際應用中,Redis的多機部署時候會涉及到redis主從復制
、Sentinel哨兵模式
、Redis Cluster
。
模式 | 優點 | 缺點 |
---|---|---|
單機版 | 架構簡單,部署方便 | 機器故障、容量瓶頸、QPS瓶頸 |
主從復制 | 高可靠性,讀寫分離 | 故障恢復復雜,主庫的寫跟存受單機限制 |
Sentinel 哨兵 | 集群部署簡單,HA | 原理繁瑣,slave存在資源浪費,不能解決讀寫分離問題 |
Redis Cluster | 數據動態存儲solt,可擴展,高可用 | 客戶端動態感知后端變更,批量操作支持查 |
7.1、redis主從復制
該模式下 具有高可用性且讀寫分離, 會采用 增量同步
跟 全量同步
兩種機制。
7.1.1、全量同步
Redis全量復制一般發生在Slave初始化階段,這時Slave需要將Master上的所有數據都復制一份:
1、slave連接master,發送
psync
命令。2、master接收到
psync
命名后,開始執行bgsave命令生成RDB文件并使用緩沖區記錄此后執行的所有寫命令。3、master發送快照文件到slave,并在發送期間繼續記錄被執行的寫命令。4、slave收到快照文件后丟棄所有舊數據,載入收到的快照。
5、master快照發送完畢后開始向slave發送緩沖區中的寫命令。
6、slave完成對快照的載入,開始接收命令請求,并執行來自master緩沖區的寫命令。
7.1.2、增量同步
也叫指令同步,就是從庫重放在主庫中進行的指令。Redis會把指令存放在一個環形隊列當中,因為內存容量有限,如果備機一直起不來,不可能把所有的內存都去存指令,也就是說,如果備機一直未同步,指令可能會被覆蓋掉。
Redis增量復制是指Slave初始化后開始正常工作時master發生的寫操作同步到slave的過程。增量復制的過程主要是master每執行一個寫命令就會向slave發送相同的寫命令。
7.1.3、Redis主從同步策略:
1、
主從剛剛連接的時候,進行全量同步;全同步結束后,進行增量同步
。當然,如果有需要,slave 在任何時候都可以發起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。2、slave在同步master數據時候如果slave丟失連接不用怕,slave在重新連接之后丟失重補
。3、一般通過主從來實現讀寫分離,但是如果master掛掉后如何保證Redis的 HA呢?引入
Sentinel
進行master的選擇。
7.2、高可用之哨兵模式
Redis-sentinel 本身是一個獨立運行的進程,一般sentinel集群 節點數至少三個且奇數個,它能監控多個master-slave集群,sentinel節點發現master宕機后能進行自動切換。Sentinel可以監視任意多個主服務器以及主服務器屬下的從服務器,并在被監視的主服務器下線時,自動執行故障轉移操作。這里需注意sentinel
也有single-point-of-failure
問題。大致羅列下哨兵用途:
集群監控:循環監控master跟slave節點。
消息通知:當它發現有redis實例有故障的話,就會發送消息給管理員
故障轉移:這里分為主觀下線(單獨一個哨兵發現master故障了)。客觀下線(多個哨兵進行抉擇發現達到quorum數時候開始進行切換)。
配置中心:如果發生了故障轉移,它會通知將master的新地址寫在配置中心告訴客戶端。
7.3、Redis Cluster
RedisCluster是Redis的分布式解決方案,在3.0版本后推出的方案,有效地解決了Redis分布式的需求。
7.3.1、分區規則
常見的分區規則
節點取余
:hash(key) % N一致性哈希
:一致性哈希環虛擬槽哈希
:CRC16[key] & 16383
RedisCluster采用了虛擬槽分區
方式,具題的實現細節如下:
1、采用去中心化的思想,它使用虛擬槽solt分區覆蓋到所有節點上,取數據一樣的流程,節點之間使用輕量協議通信Gossip來減少帶寬占用所以性能很高,
2、自動實現負載均衡與高可用,自動實現failover并且支持動態擴展,官方已經玩到可以1000個節點 實現的復雜度低。
3、每個Master也需要配置主從,并且內部也是采用哨兵模式,如果有半數節點發現某個異常節點會共同決定更改異常節點的狀態。
4、如果集群中的master沒有slave節點,則master掛掉后整個集群就會進入fail狀態,因為集群的slot映射不完整。如果集群超過半數以上的master掛掉,集群都會進入fail狀態。
5、官方推薦 集群部署至少要3臺以上的master節點。
8、Redis 限流
經常乘坐北京西二旗地鐵或者在北京西站乘坐的時候經常會遇到一種情況就是如果人很多,地鐵的工作人員拿個小牌前面一檔讓你等會兒再檢票,這就是實際生活應對人流量巨大的措施。
在開發高并發系統時,有三把利器用來保護系統:緩存
、降級
和限流
。那么何為限流呢?顧名思義,限流就是限制流量,就像你寬帶包了1個G的流量,用完了就沒了。通過限流,我們可以很好地控制系統的qps,從而達到保護系統的目的。
1、基于Redis的setnx、zset
1.2、setnx
比如我們需要在10秒內限定20個請求,那么我們在setnx的時候可以設置過期時間10,當請求的setnx數量達到20時候即達到了限流效果。
缺點:比如當統計1-10秒的時候,無法統計2-11秒之內,如果需要統計N秒內的M個請求,那么我們的Redis中需要保持N個key等等問題。
1.3、zset
其實限流涉及的最主要的就是滑動窗口,上面也提到1-10怎么變成2-11。其實也就是起始值和末端值都各+1即可。我們可以將請求打造成一個zset數組,當每一次請求進來的時候,value保持唯一,可以用UUID生成,而score可以用當前時間戳表示,因為score我們可以用來計算當前時間戳之內有多少的請求數量。而zset數據結構也提供了range方法讓我們可以很輕易的獲取到2個時間戳內有多少請求,
缺點:就是zset的數據結構會越來越大。
2、漏桶算法
漏桶算法思路:把水比作是請求,漏桶比作是系統處理能力極限,水先進入到漏桶里,漏桶里的水按一定速率流出,當流出的速率小于流入的速率時,由于漏桶容量有限,后續進入的水直接溢出(拒絕請求),以此實現限流。
3、令牌桶算法
令牌桶算法的原理:可以理解成醫院的掛號看病,只有拿到號以后才可以進行診病。
細節流程大致:
1、所有的請求在處理之前都需要拿到一個可用的令牌才會被處理。
2、根據限流大小,設置按照一定的速率往桶里添加令牌。
3、設置桶最大可容納值,當桶滿時新添加的令牌就被丟棄或者拒絕。
4、請求達到后首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之后,將令牌直接刪除。
5、令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之后將不會刪除令牌,以此保證足夠的限流。
工程化:
1、自定義注解、aop、Redis + Lua 實現限流。
2、推薦 guava 的RateLimiter實現。
9、常見知識點
- 字符串模糊查詢時用
Keys
可能導致線程阻塞,盡量用scan
指令進行無阻塞的取出數據然后去重下即可。 - 多個操作的情況下記得用
pipeLine
把所有的命令一次發過去,避免頻繁的發送、接收帶來的網絡開銷,提升性能。 - bigkeys可以掃描redis中的大key,底層是使用scan命令去遍歷所有的鍵,對每個鍵根據其類型執行STRLEN、LLEN、SCARD、HLEN、ZCARD這些命令獲取其長度或者元素個數。缺陷是線上試用并且個數多不一定空間大,
- 線上應用記得開啟Redis慢查詢日志哦,基本思路跟MySQL類似。
- Redis中因為內存分配策略跟增刪數據是會導致
內存碎片
,你可以重啟服務也可以執行activedefrag yes
進行內存重新整理來解決此問題。圖片
1、Ratio >1 表明有內存碎片,越大表明越多嚴重。
2、Ratio < 1 表明正在使用虛擬內存,虛擬內存其實就是硬盤,性能比內存低得多,這是應該增強機器的內存以提高性能。
3、一般來說,mem_fragmentation_ratio的數值在1 ~ 1.5之間是比較健康的。