導(dǎo)言
大家好,我是南橘,從接觸java到現(xiàn)在也有差不多兩年時間了,兩年時間,從一名連java有幾種數(shù)據(jù)結(jié)構(gòu)都不懂超級小白,到現(xiàn)在懂了一點點的進(jìn)階小白,學(xué)到了不少的東西。知識越分享越值錢,我這段時間總結(jié)(包括從別的大佬那邊學(xué)習(xí),引用)了一些平常學(xué)習(xí)和面試中的重點(自我認(rèn)為),希望給大家?guī)硪恍椭?/p>
這篇文章的出現(xiàn),首先要感謝一個人三太子敖丙 ,就是他的文章讓我發(fā)現(xiàn),原來Redis的知識如此的多姿多彩。恩恩,他的文章,我是期期都看
這是這篇文章的思維導(dǎo)圖,因為用的是免費版的軟件,所以有不少水印,需要原版的可以問我要
1、Redis基礎(chǔ)知識
要學(xué)習(xí)Redis基礎(chǔ)知識,首先要知道Redis的五種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),我們先從這五種數(shù)據(jù)結(jié)構(gòu)的使用場景和一些工作中(面試中)容易出現(xiàn)的點來介紹
1、1 String 字符串類型
是redis中最基本的數(shù)據(jù)類型,一個key對應(yīng)一個value
適用情況:
1、緩存: 經(jīng)典使用場景,把常用信息,字符串,圖片或者視頻等信息放到redis中,redis作為緩存層,mysql做持久化層,降低mysql的讀寫壓力。
2.計數(shù)器:redis是單線程模型,一個命令執(zhí)行完才會執(zhí)行下一個,同時數(shù)據(jù)可以一步落地到其他的數(shù)據(jù)源。
3.session:通過redis實現(xiàn)session共享
1、2 Hash (哈希)
對于Java中的HashMap,本身是一種KV對結(jié)構(gòu),如 value={{field1,value1},......fieldN,valueN}},非常好理解
適用情況:
HashMap作為緩存,相比于string更節(jié)省空間的維護(hù)緩存信息,適合存儲如用戶信息,視頻信息等
底層用字典dict實現(xiàn)
1、3 List (鏈表)
Redis 的鏈表相當(dāng)于 Java 語言里面的 LinkedList
適用情況:
1、List在Redis中既可以做隊列也可以做棧使用,它的插入和刪除操作非常快,時間復(fù)雜度為 0(1),但是索引定位很慢,時間 復(fù)雜度為 O(n)。
2、可以作為微博的時間軸,有人發(fā)布微博,用lpush加入時間軸,展示新的列表信息。
3、可以實現(xiàn)阻塞隊列,左進(jìn)右出的隊列組完成設(shè)計
list底層使用quickList(快速鏈表)實現(xiàn)
在早期的設(shè)計中, 當(dāng)列表對象中元素的長度比較小或者數(shù)量比較少的時候,采用ziplist來存儲,當(dāng)列表對象中元素的長度比較大或者數(shù)量比較多的時候,則會轉(zhuǎn)而使用雙向列表linkedlist來存儲。
這兩種存儲方式都各有優(yōu)缺點
- 雙向鏈表linkedlist便于在表的兩端進(jìn)行push和pop操作,在插入節(jié)點上復(fù)雜度很低,但是它的內(nèi)存開銷比較大。首先,它在每個節(jié)點上除了要保存數(shù)據(jù)之外,還要額外保存兩個指針;其次,雙向鏈表的各個節(jié)點是單獨的內(nèi)存塊,地址不連續(xù),節(jié)點多了容易產(chǎn)生內(nèi)存碎片。
- ziplist存儲在一段連續(xù)的內(nèi)存上,所以存儲效率很高。但是,它不利于修改操作,插入和刪除操作需要頻繁的申請和釋放內(nèi)存。特別是當(dāng)ziplist長度很長的時候,一次realloc可能會導(dǎo)致大批量的數(shù)據(jù)拷貝。
3.2版本更新之后,list的底層實現(xiàn)變成了quickList
quickList是 zipList 和 linkedList 的混合體,它將 linkedList 按段切分,每一段使用 zipList 來緊湊存儲,多個 zipList 之間使用雙向指針串接起來。
1、4 Set 集合
Redis 的集合相當(dāng)于 Java 語言里面的 HashSet ,它內(nèi)部的鍵值對是無序的、唯一 的。
它的內(nèi)部實現(xiàn)相當(dāng)于一個特殊的字典,字典中所有的 value 都是一個值 NULL 當(dāng)集合中最后一個元素被移除之后,數(shù)據(jù)結(jié)構(gòu)被自動刪除,內(nèi)存被回收。
適用情況:
1、標(biāo)簽(tag),給用戶添加標(biāo)簽,或者用戶給消息添加標(biāo)簽,這樣有同一標(biāo)簽或者類似標(biāo)簽的可以給推薦關(guān)注的事或者關(guān)注的人。
2、點贊,或點踩,收藏等,可以放到set中實現(xiàn)
3、可以用來存儲在某活動中中獎的用戶 ID ,因為有去重功能,可以保證同 一個用戶不會中獎兩次。
1、5 Zset 有序集合
它類似于 Java SortedSet HashMap 的結(jié)合體, 方面它是個 set ,保證 了內(nèi)部 value 的唯性,另方面它可 以給每個 value 賦予一個 score ,代表 這個 value 的排序權(quán)重。它的內(nèi)部實現(xiàn) 用的是一種叫作“跳躍表”的數(shù)據(jù) 結(jié)構(gòu)。
還是和上期一樣,從一位大佬那邊借過來一張圖片給大家展示,什么是跳躍表
從這張圖片,我們可以看出來:跳躍表的底層是一個順序鏈表,每隔一個節(jié)點有一個上層的指針指向下一個節(jié)點,并層層向上遞歸。這樣設(shè)計成類似樹形的結(jié)構(gòu),可以使得對于鏈表的查找可以到達(dá)二分查找的時間復(fù)雜度。
skiplist他不要求上下兩層鏈表之間個數(shù)的嚴(yán)格對應(yīng)關(guān)系,他為每個節(jié)點隨機出一個層數(shù)。比如上圖第三個節(jié)點的隨機出的層數(shù)是4,那么就把它插入到四層的空間上,而第四個節(jié)點隨機出的層數(shù)是1,那它就只存在第一層空間上。
- 當(dāng)數(shù)據(jù)較少的時候,zset是由一個ziplist來實現(xiàn)的,就和list底層之前是一樣的
ziplist是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序存儲結(jié)構(gòu),類似于數(shù)組,ziplist在內(nèi)存中是連續(xù)存儲的,但是不同于數(shù)組,為了節(jié)省內(nèi)存 ziplist的每個元素所占的內(nèi)存大小可以不同
ziplist將一些必要的偏移量信息記錄在了每一個節(jié)點里,使之能跳到上一個節(jié)點或下一個節(jié)點。
- 當(dāng)數(shù)據(jù)較多的時候,zset是一個由dict 和一個 skiplist來實現(xiàn)的,dict用來查詢數(shù)據(jù)到分?jǐn)?shù)的對應(yīng)關(guān)系,而skiplist用來根據(jù)分?jǐn)?shù)查詢數(shù)據(jù)
除了這五大基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),Redis還有更加專業(yè)的數(shù)據(jù)結(jié)構(gòu)
HyperLogLog(基數(shù)統(tǒng)計的算法)、Geo(地理位置系列)、Pub\Sub(消息隊列)、Pipeline(管道)、BloomFiler(布隆過濾器),都在不同的地方有用到,有些我會在下文向大家介紹,還有一些深入的我沒有了解這么多,大家可以去敖丙的文章中進(jìn)一步了解
Pipeline
可以將多次IO往返時間縮減為一次,前提是pipleline執(zhí)行的指令之間沒有因果關(guān)系
管道(pipeline)可以一次性發(fā)送多條命令并在執(zhí)行完后一次性將結(jié)果返回,pipeline 通過減少客戶端與redis的通信次數(shù)來實現(xiàn)降低往返延時時間,而且Pipeline 實現(xiàn)的原理是隊列,而隊列的原理是時先進(jìn)先出,這樣就保證數(shù)據(jù)的順序性。
注意:pipeline機制可以優(yōu)化吞吐量,但無法提供原子性/事務(wù)保障
2、進(jìn)階知識
2、1 Redis實現(xiàn)分布式鎖
隨著互聯(lián)網(wǎng)技術(shù)的飛速發(fā)展,越來越多的單體架構(gòu)已經(jīng)轉(zhuǎn)型成了分布式架構(gòu),,分布式架構(gòu)確實能帶來性能和效率上的提升,但是也會帶來數(shù)據(jù)一致性的問題。
分布式鎖,就是解決分布式架構(gòu)中數(shù)據(jù)一致性的專用武器,分布式鎖需要滿足一下三個方面方可放心使用:
排他性:在同一時間只會有一個客戶端能獲取到鎖,其它客戶端無法同時獲取
避免死鎖:這把鎖在一段有限的時間之后,一定會被釋放(正常釋放或異常釋放)
高可用:獲取或釋放鎖的機制必須高可用且性能佳
目前,我所知道的分布式鎖大概有三種主流方式實現(xiàn),分別是zookpeer,redis,還有本地數(shù)據(jù)庫,今天我就介紹一下如何用redis實現(xiàn)分布式鎖。
基于Redis實現(xiàn)的鎖機制,主要是依賴redis自身的原子操作
setnx爭搶鎖,再用expire添加過期時間
你沒有看錯,就是這么簡單,如果害怕不妥,比如爭搶鎖的時候還沒有設(shè)置過期時間就突然宕機之類的問題,可以直接用jedis等封裝好的RedisTemplate把setnx和expire合成一條指令使用。
但是,這樣的分布式鎖不是絕對安全的
首先,單點故障的問題不可避免
其次,因為使用鎖的客戶端,和redis服務(wù)器,不在一起?。r間是有延遲的,我們只能依靠redis的TTL命令來查詢鎖的剩余時間,然后根據(jù)這個剩余時間來判斷鎖是否超時。
然而在通常的計算機系統(tǒng)中,很難獲取到一個可靠的時間。
- 系統(tǒng)可能因于時間服務(wù)器同步調(diào)整時間,
- 虛擬機可能調(diào)整時間,
- JVM GC可能導(dǎo)致時間停頓
怎么辦?
其實如果我們使用的場景不需要那么強的安全性精確性,這樣也足夠用了,至少我現(xiàn)在的公司夠用了(一個排名前列的第三方支付企業(yè))。但是,精益求精是程序員們的本質(zhì),RedLock的出現(xiàn)在一定程度上解決了這個問題。
我不是很了解RedLock,只能稍微給大家介紹一下,流程如下:
- 客戶端獲取當(dāng)前時間,生成一個隨機值作為鎖的值(目的是更加精確的獲得時間)
- 依次嘗試在所有5個redis上取得同一個鎖(使用類似單機redis鎖的方法, 使用同樣的key和同一個隨機值)
獲取鎖的操作本身需要設(shè)定一個比較小的超時時間(如5-50ms), 防止在一個掛掉的redis上浪費太多時間
如果一個redis不可用,要盡快開始嘗試下一個- 客戶端計算獲取鎖一共用了多長時間,通過用當(dāng)前時間減去第1步得到的時間
如果客戶端獲取了多數(shù)redis上的這個鎖(3到五個5),并且這時還沒有超過鎖的超時時間,
這個鎖就算是獲取成功了- 如果鎖獲取成功了,有效時間就按鎖超時時間-獲取鎖花費時間算
- 如果失敗,嘗試在所有redis上解除鎖
(解除鎖的操作是一段lua script,刪除一個key如果key的value是第1步生成的隨機值)
當(dāng)然,它也不能解決問題,但是, redis鎖只會在比較極端的情況下出錯,如果不是需要特別精確,只需要保證絕大多數(shù)可靠的時候,可以放心使用redis集群或者redlock。
2.2Redis集群
解決了分布式鎖的問題,但是還是沒有解決各種天災(zāi)人禍的問題,所以,這就需要Redis集群出馬了
集群同步機制
Redis中有主從機制,一個主節(jié)點對應(yīng)一個或多個從節(jié)點,主節(jié)點提供數(shù)據(jù)存取,從節(jié)點則是從主節(jié)點拉取數(shù)據(jù)備份,當(dāng)這個主節(jié)點掛掉后,就會有這個從節(jié)點選取一個來充當(dāng)主節(jié)點,從而保證集群不會掛掉。先講一下Redis主從同步的流程:
- 1.第一次同步時,從服務(wù)器向主服務(wù)器發(fā)送一次SYNC命令,主服務(wù)器收到之后做一次bgsave、并同時將后續(xù)修改操作記錄到內(nèi)存buffer,待完成后將RDB文件全量同步到復(fù)制節(jié)點
- 2.復(fù)制節(jié)點接收完成后將RDB鏡像加載到內(nèi)存中,加載完成后,再通知主節(jié)點
- 3.后續(xù)的增量數(shù)據(jù)通過AOF日志同步即可,有點類似數(shù)據(jù)庫的binlog
同時,在2.8版本之后,Redis可以自動判斷是需要全量同步還是增量同步,效率比較高,增量同步其實就是在完成全量同步后,開始新復(fù)制時向主服務(wù)器發(fā)送PSYNC(<runid> <offset>)命令(runid是上次復(fù)制的主服務(wù)器id,offset是從服務(wù)器的復(fù)制偏移量),主服務(wù)器會根據(jù)這個兩個參數(shù)來決定做哪種同步,判斷服務(wù)器id是否和本機相同,復(fù)制偏移量是否在緩沖區(qū)中。
集群的高可用性
- Redis Sentinal(哨兵模式)集群著眼于高可用,在master宕機時自動將slave提升為master,繼續(xù)提供服務(wù)
- Redis Cluster集群著眼于擴展性,在單個redis內(nèi)存不足時,使用Cluster進(jìn)行分片存儲
關(guān)于這些集群的東西一章內(nèi)容肯定寫不完,我會在以后的文章里向大家介紹
2、3異步隊列
Redis的本職工作是緩存,但是由于它多才多藝,成為隊列也不錯,有一些阻塞式的API讓其有能力做消息隊列;另外,做消息隊列的其他特性例如FIFO(先入先出)也很容易實現(xiàn),只需要一個List對象從頭取數(shù)據(jù),從尾部塞數(shù)據(jù)即可
- 在Redis中,如果讓List結(jié)構(gòu)作為隊列、rpush生產(chǎn)消息、lpop消費消息、當(dāng)lpop沒有消息的時候,可以當(dāng)sleep一會再重試,這就相當(dāng)于生產(chǎn)者消費模式模式了。同時List有個指令叫blpop,在沒有消息的時候,它會阻塞住直到消息到來。
- pub\sub主題訂閱者模式、可以實現(xiàn)1對N的消息隊列,實現(xiàn)生產(chǎn)一次,消費多次。但是,它也有不足之處,如果讓pub\sub主題訂閱者模式、消費者下線的情況下,消息會丟失、不如直接用MQ
2、4延時隊列
在Redis中,可以利用 sorted-set 來做延時隊列
zadd key score1 value1 score2 value2
- socre為執(zhí)行時間,key為隊列名,value為數(shù)據(jù)
- 消費隊列循環(huán)從sorted-set根據(jù)score獲?。▃rangebyscore)小于等于當(dāng)前時間的且score最小的一條數(shù)據(jù)輪詢處理
- 如果沒有取到數(shù)據(jù),睡一會再去獲取
但是,Redis的延時隊列無法返回ACK,所以需要自己實現(xiàn)
2、5 持久化
Redis有兩種持久化的方式,分別是RDB和AOF
RDB做鏡像全量持久化、AOF做增量持久化,因為RDB會耗費較長時間,不夠?qū)崟r,在停機的時候會導(dǎo)致大量有效數(shù)據(jù)丟失,所以需要AOF來配合使用,在redis實例重啟時,會使用RDB持久化文件重新構(gòu)建內(nèi)存,再使用AOF重放近期的操作指令來實現(xiàn)完整恢復(fù)重啟之前的狀態(tài)。
RDB機制
RDB持久化是指在指定的時間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤。也是默認(rèn)的持久化方式,這種方式是就是將內(nèi)存中數(shù)據(jù)以快照的方式寫入到二進(jìn)制文件中,默認(rèn)的文件名為dump.rdb。RDB提供了三種機制來觸發(fā)持久化
-
1、save觸發(fā)方式---客戶端發(fā)起save請求
執(zhí)行save命令期間,Redis不能處理其他命令,直到RDB過程完成為止
-
2、bgsave觸發(fā)方式--客戶端發(fā)起bgsave請求
執(zhí)行該命令時,Redis會在后臺異步進(jìn)行快照操作,快照同時還可以響應(yīng)客戶端請求
-
3、自動觸發(fā)
自動觸發(fā)是由我們的配置文件來完成的,在redis.conf文件中配置,大家可以去了解一下,這里就不寫那么多東西了
AOF機制
全量備份總是耗時的(隨機的傳說總是好的???),有時候我們提供一種更加高效的方式AOF,Redis會將每一個收到的寫命令都通過write函數(shù)追加到文件中,就是日志記錄。
和RDB一樣,AOF也有三種同步機制:
1、always:同步持久化 每次發(fā)生數(shù)據(jù)變更會被立即記錄到磁盤 性能較差但數(shù)據(jù)完整性比較好
2、everysec:每秒同步,異步操作,每秒記錄 如果一秒內(nèi)宕機,有數(shù)據(jù)丟失
3、no:從不同步
Redis本身的機制是AOF持久化開啟存在AOF文件時,優(yōu)先加載AOF文件。AOF文件不存在時候,加載RDB文件。加載AOF\RDB文件后,Redis啟動成功。AOF\RDB文件存在錯誤時,Redis啟動失敗并打印錯誤信息。
不要問AOF和RDB用哪個,我的經(jīng)驗就是,全都用。
RDB同步快,但是要損失最多五分鐘的內(nèi)容,AOF同步慢,但是每秒同步的情況下最多損失1s的內(nèi)容,損失的內(nèi)容也可以通過日志去找回。
機器斷電對數(shù)據(jù)丟失的影響
AOF日志中可以進(jìn)行sync屬性的配置,如果不要求性能,在每條寫指令時都sync一下磁盤,就不會丟失數(shù)據(jù),但在高性能要求下每次都sync是不現(xiàn)實的,一般都使用定時sync,比如1s一次,這個時候就最多丟失1s的數(shù)據(jù)。
結(jié)語
emmmm,第二篇文章也慢慢的寫完了,在寫文章的過程中,真的是發(fā)現(xiàn)自己懂得越多,不懂越多,而且因為公司周末要加班,所以這將Redis分成兩篇來完成,希望大家可以喜歡~~
同時需要思維導(dǎo)圖的話,可以聯(lián)系我,畢竟知識越分享越香!
在這里插入圖片描述