Redis:從應用到底層,一文幫你搞定

Python實戰社羣

Java實戰社羣

長按識別下方二維碼,按需求添加

掃碼關注添加客服

進Python社羣▲

掃碼關注添加客服

進Java社羣

作者丨sowhat1412

來源丨sowhat1412(ID:sowhat9094)

高清思維導圖已同步Git:https://github.com/SoWhat1412/xmindfile,關注公衆號sowhat1412獲取海量資源

總感覺哪裏不對,但是又說不上來

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,核心有兩個作用:

  1. 說明是5種類型哪一種。

  2. 裏面有指針用來指向 SDS。

當你執行set name sowhat的時候,其實Redis會創建兩個RedisObject對象,鍵的RedisObject 和 值的RedisOjbect 其中它們type = REDIS_STRING,而SDS分別存儲的就是 name 跟 sowhat 字符串咯。

並且Redis底層對SDS有如下優化:

  1. SDS修改後大小 > 1M時 系統會多分配空間來進行空間預分配

  2. 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則表示擴容到數組中的第幾個了。

擴容之後的數組大小爲大於used*2的2的n次方的最小值,跟 HashMap 類似。然後挨個遍歷數組同時調整rehashidx的值,對每個dictEntry[i] 再挨個遍歷鏈表將數據 Hash 後重新映射到 dictht[1]裏面。並且 dictht[0].usedictht[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的節點爲例,來對比說明跳錶和普遍的鏈表。

  1. 沒有跳錶查詢 比如我查詢數據37,如果沒有上面的索引時候路線如下圖:

  2. 有跳錶查詢 有跳錶查詢37的時候路線如下圖:應用場景:

積分排行榜、時間排序新聞、延時隊列。

1.6、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

  1. 用戶簽到

key = 年份:用戶id  offset = (今天是一年中的第幾天) % (今年的天數)

  1. 統計活躍用戶

使用日期作爲 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在執行BGSAVEBGREWRITEAOF命令時,哈希表的負載因子>=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 讀寫負荷

關於多線程須知:

  1. Redis 6.0 版本 默認多線程是關閉的 io-threads-do-reads no

  2. Redis 6.0 版本 開啓多線程後 線程數也要 謹慎設置。

  3. 多線程可以使得性能翻倍,但是多線程只是用來處理網絡數據的讀寫和協議解析,執行命令仍然是單線程順序執行

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節點就像一個大腦分裂成了兩個。其實在HadoopSpark集羣中都會出現這樣的情況,只是解決方法不同而已(用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、分區規則

常見的分區規則

  1. 節點取餘:hash(key) % N

  2. 一致性哈希:一致性哈希環

  3. 虛擬槽哈希: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、推薦 guavaRateLimiter實現。

9、常見知識點

  1. 字符串模糊查詢時用Keys可能導致線程阻塞,儘量用scan指令進行無阻塞的取出數據然後去重下即可。

  2. 多個操作的情況下記得用pipeLine把所有的命令一次發過去,避免頻繁的發送、接收帶來的網絡開銷,提升性能。

  3. bigkeys可以掃描redis中的大key,底層是使用scan命令去遍歷所有的鍵,對每個鍵根據其類型執行STRLEN、LLEN、SCARD、HLEN、ZCARD這些命令獲取其長度或者元素個數。缺陷是線上試用並且個數多不一定空間大,

  4. 線上應用記得開啓Redis慢查詢日誌哦,基本思路跟MySQL類似。

  5. Redis中因爲內存分配策略跟增刪數據是會導致內存碎片,你可以重啓服務也可以執行activedefrag yes進行內存重新整理來解決此問題。

1、Ratio >1 表明有內存碎片,越大表明越多嚴重。

2、Ratio < 1 表明正在使用虛擬內存,虛擬內存其實就是硬盤,性能比內存低得多,這是應該增強機器的內存以提高性能。

3、一般來說,mem_fragmentation_ratio的數值在1 ~ 1.5之間是比較健康的。

10、End

關於Redis先吹逼這麼多(本來想寫秒殺的,怕寫太長,估計能看到這就算是認真閱讀者了)

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣

近期精彩內容推薦:  

 爲何說IT科技公司應該留住35歲員工?

 工友們!大家好,今天你摸魚了嗎?

 緩存穿透,雪崩,擊穿以及解決方案分析

 圖文詳解:如何給女朋友解釋什麼是微服務?


在看點這裏好文分享給更多人↓↓

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