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

1.1、String
適用于簡單key-value存儲、setnx key value實現分布式鎖、計數器(原子性)、分布式全局唯一ID。
SDS(simple dynamic string)封裝char[],這是是Redis存儲的最小單元,一個SDS最大可以存儲512M信息。struct?sdshdr{
??unsigned?int?len;?//?標記char[]的長度
??unsigned?int?free;?//標記char[]中未使用的元素個數
??char?buf[];?//?存放元素的坑
}
RedisObject,核心有兩個作用:
說明是5種類型哪一種。 里面有指針用來指向 SDS。
set name sowhat的時候,其實Redis會創(chuàng)建兩個RedisObject對象,鍵的RedisObject 和 值的RedisOjbect 其中它們type = REDIS_STRING,而SDS分別存儲的就是 name 跟 sowhat 字符串咯。
SDS修改后大小 > 1M時 系統會多分配空間來進行 空間預分配。SDS是 惰性釋放空間的,你free了空間,可是系統把數據記錄下來下次想用時候可直接使用。不用新申請空間。
1.2、List

adlist.h 會發(fā)現底層就是個 雙端鏈表,該鏈表最大長度為2^32-1。常用就這幾個組合。lpush + lpop = stack 先進后出的棧? lpush + rpop = queue 先進先出的隊列? lpush + ltrim = capped collection 有限集合 lpush + brpop = message queue 消息隊列
1.3、Hash
1.3.1、dictEntry
真正的數據節(jié)點,包括key、value 和 next 節(jié)點。

1.3.2、dictht
1、數據 dictEntry 類型的數組,每個數組的item可能都指向一個鏈表。 2、數組長度 size。 3、sizemask 等于 size - 1。 4、當前 dictEntry 數組中包含總共多少節(jié)點。

1.3.3、dict
1、dictType 類型,包括一些自定義函數,這些函數使得key和value能夠存儲 2、rehashidx 其實是一個標志量,如果為 -1說明當前沒有擴容,如果不為 -1則記錄擴容位置。3、dictht數組,兩個Hash表。 4、iterators 記錄了當前字典正在進行中的迭代器


1.3.4、漸進式擴容
rehashindex來記錄轉移的情況,當全部轉移完成,將ht[1]改成ht[0]使用。
rehashidx,其為第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。1、如果是新增,則直接新增后第二個數組,因為如果新增到第一個數組,以后還是要移過來,沒必要浪費時間 2、如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。

1.4、Set
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 后就會發(fā)現 Zset用的就是可以跟二叉樹媲美的跳躍表來實現有序。跳表就是多層鏈表的結合體,跳表分為許多層(level),每一層都可以看作是數據的索引,這些索引的意義就是加快跳表查找數據速度。從上往下,從左往右進行查找?,F在找出值為37的節(jié)點為例,來對比說明跳表和普遍的鏈表。沒有跳表查詢 比如我查詢數據37,如果沒有上面的索引時候路線如下圖: 
有跳表查詢 有跳表查詢37的時候路線如下圖: 
應用場景:
積分排行榜、時間排序新聞、延時隊列。
1.6、Redis Geo

1.7、HyperLogLog
概率數據結構,它使用概率算法來統計集合的近似基數。而它算法的最本源則是伯努利過程 + 分桶 + 調和平均數。具體實現可看 ?HyperLogLog 講解。1.8、bitmap

用戶簽到
key = 年份:用戶id ?offset = (今天是一年中的第幾天) % (今年的天數)
統計活躍用戶
使用日期作為 key,然后用戶 id 為 offset 設置不同offset為0 1 即可。
1.9、Bloom Filter
不存在的一定不存在,存在的不一定存在。當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點(有效降低沖突概率),把它們置為1。檢索時,我們只要看看這些點是不是都是1就知道集合中有沒有它了:如果這些點有任何一個為0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
guava包玩耍一番。
1.10 發(fā)布訂閱
發(fā)布、訂閱模式的消息機制,其中消息訂閱者與發(fā)布者不直接通信,發(fā)布者向指定的頻道(channel)發(fā)布消息,訂閱該頻道的每個客戶端都可以接收到消息。不過比專業(yè)的MQ(RabbitMQ RocketMQ ActiveMQ Kafka)相比不值一提,這個功能就算球了。
2、持久化
2.1、RDB
1、壓縮后的二進制文,適用于備份、全量復制,用于災難恢復加載RDB恢復數據遠快于AOF方式,適合大規(guī)模的數據恢復。 2、如果業(yè)務對數據完整性和一致性要求不高,RDB是很好的選擇。數據恢復比AOF快。
1、RDB是周期間隔性的快照文件,數據的完整性和一致性不高,因為RDB可能在最后一次備份時宕機了。 2、備份時占用內存,因為Redis 在備份時會獨立fork一個子進程,將數據寫入到一個臨時文件(此時內存中的數據是原來的兩倍哦),最后再將臨時文件替換之前的備份文件。所以要考慮到大概兩倍的數據膨脹性。
1、 SAVE直接調用 rdbSave ,阻塞Redis 主進程,導致無法提供服務。2、BGSAVE則 fork 出一個子進程,子進程負責調用 rdbSave ,在保存完成后向主進程發(fā)送信號告知完成。在BGSAVE 執(zhí)行期間仍可以繼續(xù)處理客戶端的請求。3、Copy On Write 機制,備份的是開始那個時刻內存中的數據,只復制被修改內存頁數據,不是全部內存數據。 4、Copy On Write 時如果父子進程大量寫操作會導致分頁錯誤。

2.2、AOF
AOF是一秒一次去通過一個后臺的線程fsync操作,數據丟失不用怕。
1、對于相同數量的數據集而言,AOF文件通常要大于RDB文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。 2、根據同步策略的不同,AOF在運行效率上往往會慢于RDB??傊?,每秒同步策略的效率是比較高的。
aof_buf然后再同步到AO磁盤,如果實時寫入磁盤會帶來非常高的磁盤IO,影響整體性能。
1、在重寫期間,由于主進程依然在響應命令,為了保證最終備份的完整性;它 依然會寫入舊的AOF中,如果重寫失敗,能夠保證數據不丟失。2、為了把重寫期間響應的寫入信息也寫入到新的文件中,因此也會 為子進程保留一個buf,防止新寫的file丟失數據。3、重寫是直接把 當前內存的數據生成對應命令,并不需要讀取老的AOF文件進行分析、命令合并。4、無論是 RDB 還是 AOF 都是先寫入一個臨時文件,然后通過 rename完成文件的替換工作。
1、降低fork的頻率,比如可以手動來觸發(fā)RDB生成快照、與AOF重寫; 2、控制Redis最大使用內存,防止fork耗時過長; 3、配置牛逼點,合理配置Linux的內存分配策略,避免因為物理內存不足導致fork失敗。 4、Redis在執(zhí)行 BGSAVE和BGREWRITEAOF命令時,哈希表的負載因子>=5,而未執(zhí)行這兩個命令時>=1。目的是盡量減少寫操作,避免不必要的內存寫入操作。5、哈希表的擴展因子:哈希表已保存節(jié)點數量 / 哈希表大小。因子決定了是否擴展哈希表。
2.3、恢復

2.4、建議
?
3、Redis為什么那么快
3.1、 基于內存實現:
數據都存儲在內存里,相比磁盤IO操作快百倍,操作速率很快。
3.2、高效的數據結構:
Redis底層多種數據結構支持不同的數據類型,比如HyperLogLog它連2個字節(jié)都不想浪費。
3.3、豐富而合理的編碼:
1、String:自動存儲int類型,非int類型用raw編碼。 2、List:字符串長度且元素個數小于一定范圍使用 ziplist 編碼,否則轉化為 linkedlist 編碼。 3、Hash:hash 對象保存的鍵值對內的鍵和值字符串長度小于一定值及鍵值對。 4、Set:保存元素為整數及元素個數小于一定范圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼。 5、Zset:保存的元素個數小于定值且成員長度小于定值使用 ziplist 編碼,任意條件不滿足,則使用 skiplist 編碼。
3.4、合適的線程模型:
I/O 多路復用模型同時監(jiān)聽客戶端連接,多線程是需要上下文切換的,對于內存數據庫來說這點很致命。

3.5、 Redis6.0后引入多線程提速:
>> Redis運行執(zhí)行耗時,Redis的瓶頸主要在于網絡的 IO 消耗, 優(yōu)化主要有兩個方向:1、提高網絡 IO 性能,典型的實現比如使用 DPDK 來替代內核網絡棧的方式? 2、使用多線程充分利用多核,典型的實現比如 Memcached。
1、可以充分利用服務器 CPU 資源,目前主線程只能利用一個核 2、多線程任務可以分攤 Redis 同步 IO 讀寫負荷
Redis 6.0 版本 默認多線程是關閉的 io-threads-do-reads no Redis 6.0 版本 開啟多線程后 線程數也要 謹慎設置。 多線程可以使得性能翻倍,但是多線程只是用來處理網絡數據的讀寫和協議解析,執(zhí)行命令仍然是單線程順序執(zhí)行。
?
4、常見問題
4.1、緩存雪崩

Redis中大批量key在同一時間同時失效導致所有請求都打到了MySQL。而MySQL扛不住導致大面積崩塌。
1、緩存數據的過期時間加上個隨機值,防止同一時間大量數據過期現象發(fā)生。 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、緩存擊穿
現象:大并發(fā)集中對這一個熱點key進行訪問,當這個Key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請求數據庫。
設置熱點數據永遠不過期 加上互斥鎖也能搞定了
4.4、雙寫一致性
緩存跟數據庫均更新數據,如何保證數據一致性?安全問題:線程A更新數據庫->線程B更新數據庫->線程B更新緩存->線程A更新緩存。 導致臟讀。業(yè)務場景:讀少寫多場景,頻繁更新數據庫而緩存根本沒用。更何況如果緩存是疊加計算后結果更 浪費性能。
A 請求寫來更新緩存。 B 發(fā)現緩存不在去數據查詢舊值后寫入緩存。 A 將數據寫入數據庫,此時緩存跟數據庫不一致。
失效:應用程序先從cache取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。 命中:應用程序從cache中取數據,取到后返回。 更新: 先把數據存到數據庫中,成功后,再讓緩存失效。
4.5、腦裂
Hadoop 、Spark集群中都會出現這樣的情況,只是解決方法不同而已(用ZK配合強制殺死)。min-replicas-to-write?3??表示連接到master的最少slave數量
min-replicas-max-lag?10??表示slave連接到master的最大延遲時間
4.6、事務

1、redis事務就是一次性、順序性、排他性的執(zhí)行一個隊列中的一系列命令。 ? 2、Redis事務沒有隔離級別的概念:批量操作在發(fā)送 EXEC 命令前被放入隊列緩存,并不會被實際執(zhí)行,也就不存在事務內的查詢要看到事務里的更新,事務外查詢不能看到。 3、Redis不保證原子性:Redis中單條命令是原子性執(zhí)行的,但事務不保證原子性。 4、Redis編譯型錯誤事務中所有代碼均不執(zhí)行,指令使用錯誤。運行時異常是錯誤命令導致異常,其他命令可正常執(zhí)行。 5、watch指令類似于樂觀鎖,在事務提交時,如果watch監(jiān)控的多個KEY中任何KEY的值已經被其他客戶端更改,則使用EXEC執(zhí)行事務時,事務隊列將不會被執(zhí)行。
4.7、正確開發(fā)步驟
上線前:Redis 高可用,主從+哨兵,Redis cluster,避免全盤崩潰。上線時:本地 ehcache 緩存 + Hystrix 限流 + 降級,避免MySQL扛不住。上線后:Redis 持久化采用 RDB + AOF 來保證斷點后自動從磁盤上加載數據,快速恢復緩存數據。
?
5、分布式鎖
5.1、 Zookeeper實現分布式鎖
zookeeper知識:1、持久節(jié)點:客戶端斷開連接zk不刪除persistent類型節(jié)點 2、臨時節(jié)點:客戶端斷開連接zk刪除ephemeral類型節(jié)點 3、順序節(jié)點:節(jié)點后面會自動生成類似0000001的數字表示順序 4、節(jié)點變化的通知:客戶端注冊了監(jiān)聽節(jié)點變化的時候,會調用回調方法
只監(jiān)控它前面那個節(jié)點狀態(tài),從而避免羊群效應。關于模板代碼百度即可。
頻繁的創(chuàng)建刪除節(jié)點,加上注冊watch事件,對于zookeeper集群的壓力比較大,性能也比不上Redis實現的分布式鎖。
5.2、 Redis實現分布式鎖
SETNX 是SET if Not eXists的簡寫,日常指令是 SETNX key value,如果 key 不存在則set成功返回 1,如果這個key已經存在了返回0。
SETEX key seconds value 表達的意思是 將值 value 關聯到 key ,并將 key 的生存時間設為多少秒。如果 key 已經存在,setex命令將覆寫舊值。并且 setex是一個 原子性(atomic)操作。
一般就是用一個標識唯一性的字符串比如UUID 配合 SETNX 實現加鎖。
這里用到了LUA腳本,LUA可以保證是原子性的,思路就是判斷一下Key和入參是否相等,是的話就刪除,返回成功1,0就是失敗。
這個鎖是無法重入的,且自己實心的話各種邊邊角角都要考慮到,所以了解個大致思路流程即可,工程化還是用開源工具包就行。
5.3、 Redisson實現分布式鎖


?
6、Redis 過期策略和內存淘汰策略
6.1、Redis的過期策略
每個設置過期時間的key都需要創(chuàng)建一個定時器,到過期時間就會立即對key進行清除。該策略可以立即清除過期的數據,對內存很友好;但是會占用大量的CPU資源去處理過期的數據,從而影響緩存的響應時間和吞吐量。
只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節(jié)省CPU資源,卻對內存非常不友好。極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,占用大量內存。
每隔一定的時間,會掃描一定數量的數據庫的expires字典中一定數量的key,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和內存資源達到最優(yōu)的平衡效果。 expires字典會保存所有設置了過期時間的key的過期時間數據,其中 key 是指向鍵空間中的某個鍵的指針,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵。
惰性刪除 + 定期刪除。memcached采用的過期策略:惰性刪除。6.2、6種內存淘汰策略
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(驅逐):禁止驅逐數據,不刪除的意思。
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?eldest) ?{
????????//?當 map中的數據量大于指定的緩存?zhèn)€數的時候,就自動刪除最老的數據。
????????return?size()?>?CACHE_SIZE;
????}
}
6.2、總結
?
7、Redis 集群高可用
redis主從復制、Sentinel哨兵模式、Redis Cluster。
7.1、redis主從復制
增量同步 跟 全量同步 兩種機制。7.1.1、全量同步

1、slave連接master,發(fā)送 psync命令。2、master接收到 psync命名后,開始執(zhí)行bgsave命令生成RDB文件并使用緩沖區(qū)記錄此后執(zhí)行的所有寫命令。3、master發(fā)送快照文件到slave,并在發(fā)送期間繼續(xù)記錄被執(zhí)行的寫命令。4、slave收到快照文件后丟棄所有舊數據,載入收到的快照。 5、master快照發(fā)送完畢后開始向slave發(fā)送緩沖區(qū)中的寫命令。 6、slave完成對快照的載入,開始接收命令請求,并執(zhí)行來自master緩沖區(qū)的寫命令。
7.1.2、增量同步

7.1.3、Redis主從同步策略:
1、 主從剛剛連接的時候,進行全量同步;全同步結束后,進行增量同步。當然,如果有需要,slave 在任何時候都可以發(fā)起全量同步。redis 策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。2、slave在同步master數據時候如果slave丟失連接不用怕,slave在重新連接之后丟失重補。3、一般通過主從來實現讀寫分離,但是如果master掛掉后如何保證Redis的 HA呢?引入 Sentinel進行master的選擇。
7.2、高可用之哨兵模式

Redis-sentinel ?本身是一個獨立運行的進程,一般sentinel集群 節(jié)點數至少三個且奇數個,它能監(jiān)控多個master-slave集群,sentinel節(jié)點發(fā)現master宕機后能進行自動切換。Sentinel可以監(jiān)視任意多個主服務器以及主服務器屬下的從服務器,并在被監(jiān)視的主服務器下線時,自動執(zhí)行故障轉移操作。這里需注意
sentinel也有single-point-of-failure問題。大致羅列下哨兵用途:集群監(jiān)控:循環(huán)監(jiān)控master跟slave節(jié)點。 消息通知:當它發(fā)現有redis實例有故障的話,就會發(fā)送消息給管理員? 故障轉移:這里分為主觀下線(單獨一個哨兵發(fā)現master故障了)。客觀下線(多個哨兵進行抉擇發(fā)現達到quorum數時候開始進行切換)。 配置中心:如果發(fā)生了故障轉移,它會通知將master的新地址寫在配置中心告訴客戶端。
7.3、Redis Cluster

7.3.1、分區(qū)規(guī)則

節(jié)點取余:hash(key) % N一致性哈希:一致性哈希環(huán)虛擬槽哈希:CRC16[key] & 16383
虛擬槽分區(qū)方式,具題的實現細節(jié)如下:1、采用去中心化的思想,它使用虛擬槽solt分區(qū)覆蓋到所有節(jié)點上,取數據一樣的流程,節(jié)點之間使用輕量協議通信Gossip來減少帶寬占用所以性能很高,? 2、自動實現負載均衡與高可用,自動實現failover并且支持動態(tài)擴展,官方已經玩到可以1000個節(jié)點 實現的復雜度低。 3、每個Master也需要配置主從,并且內部也是采用哨兵模式,如果有半數節(jié)點發(fā)現某個異常節(jié)點會共同決定更改異常節(jié)點的狀態(tài)。 4、如果集群中的master沒有slave節(jié)點,則master掛掉后整個集群就會進入fail狀態(tài),因為集群的slot映射不完整。如果集群超過半數以上的master掛掉,集群都會進入fail狀態(tài)。 5、官方推薦 集群部署至少要3臺以上的master節(jié)點。
?
8、Redis 限流
緩存、降級和限流。那么何為限流呢?顧名思義,限流就是限制流量,就像你寬帶包了1個G的流量,用完了就沒了。通過限流,我們可以很好地控制系統的qps,從而達到保護系統的目的。1、基于Redis的setnx、zset
1.2、setnx
1.3、zset
2、漏桶算法

3、令牌桶算法

1、所有的請求在處理之前都需要拿到一個可用的令牌才會被處理。 2、根據限流大小,設置按照一定的速率往桶里添加令牌。 3、設置桶最大可容納值,當桶滿時新添加的令牌就被丟棄或者拒絕。 4、請求達到后首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業(yè)務邏輯,處理完業(yè)務邏輯之后,將令牌直接刪除。 5、令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之后將不會刪除令牌,以此保證足夠的限流。
1、自定義注解、aop、Redis + Lua 實現限流。 2、推薦 guava 的RateLimiter實現。
?
9、常見知識點
字符串模糊查詢時用 Keys可能導致線程阻塞,盡量用scan指令進行無阻塞的取出數據然后去重下即可。多個操作的情況下記得用 pipeLine把所有的命令一次發(fā)過去,避免頻繁的發(fā)送、接收帶來的網絡開銷,提升性能。bigkeys可以掃描redis中的大key,底層是使用scan命令去遍歷所有的鍵,對每個鍵根據其類型執(zhí)行STRLEN、LLEN、SCARD、HLEN、ZCARD這些命令獲取其長度或者元素個數。缺陷是線上試用并且個數多不一定空間大, 線上應用記得開啟Redis慢查詢日志哦,基本思路跟MySQL類似。 Redis中因為內存分配策略跟增刪數據是會導致 內存碎片,你可以重啟服務也可以執(zhí)行activedefrag yes進行內存重新整理來解決此問題。
1、Ratio >1 表明有內存碎片,越大表明越多嚴重。 2、Ratio?< 1 表明正在使用虛擬內存,虛擬內存其實就是硬盤,性能比內存低得多,這是應該增強機器的內存以提高性能。 3、一般來說,mem_fragmentation_ratio的數值在1 ~ 1.5之間是比較健康的。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
評論
圖片
表情
