揭開面紗,追着 redis 七連問!

01 redis的通訊協議是什麼?

webp

redis的通訊協議是文本協議,是的,Redis服務器與客戶端通過RESP(REdis Serialization Protocol)協議通信,沒錯,文本協議確實是會浪費流量,不過它的優點在於直觀,非常的簡單,解析性能及其的好,我們不需要一個特殊的redis客戶端僅靠telnet或者是文本流就可以跟redis進行通訊。

1.客戶端的命令格式

(1)簡單字符串 Simple Strings, 以 "+"加號 開頭

(2)錯誤 Errors, 以"-"減號 開頭

(3)整數型 Integer, 以 ":" 冒號開頭

(4)大字符串類型 Bulk Strings, 以 "$"美元符號開頭

(5)數組類型 Arrays,以 "*"星號開

set hello abc 一個簡單的文本流就可以是redis的客戶端

webp

2.簡單總結

redis文檔認爲簡單的實現,快速的解析,直觀理解是採用RESP文本協議最重要的地方,有可能文本協議會造成一定量的流量浪費,但卻在性能上和操作上快速簡單,這中間也是一個權衡和協調的過程。

02  redis究竟有沒有ACID事務?

webp

要弄清楚redis有沒有事務,其實很簡單,上redis的官網查看文檔,發現:

webp

redis確實是有事務,不過按照傳統的事務定義ACID來看,redis是不是都具備了ACID的特性,ACID指的是 1.原子性 2.一致性 3.隔離性 4.持久性,我們將使用以上redis事務的命令來檢驗是否redis都具備了ACID的各個特徵。

1.原子性

事務具備原子性指的是,數據庫將事務中多個操作當作一個整體來執行,服務要麼執行事務中所有的操作,要不一個操作也不會執行。

1.1 事務隊列

首先弄清楚redis開始事務multi命令後,redis會爲這個事務生成一個隊列,每次操作的命令都會按照順序插入到這個隊列中,這個隊列裏面的命令不會被馬上執行,知道exec命令提交事務,所有隊列裏面的命令會被一次性,並且排他的進行執行。

webp

從上面的例子可以看出,當執行一個成功的事務,事務裏面的命令都是按照隊列裏面順序的並且排他的執行。但原子性又一個特點就是要麼全部成功,要不全部失敗,也就是我們傳統DB裏面說的回滾。

我們執行一個失敗的事務:

webp

可以發現,就算中間出現了失敗,set abc x這個操作也已經被執行了,並沒有進行回滾,從嚴格的意義上來將redis並不具備原子性。

1.2 爲何redis不支持回滾

這個其實跟redis的定位和設計有關係,先看看爲何我們的mysql可以支持回滾,這個還是跟寫log有關係,redis是完成操作之後纔會進行aof日誌記錄,aof日誌的定位只是記錄操作的指令記錄,而mysql有完善的redolog,並且是在事務進行commit之前就會寫完成redolog,binlog

webp

要知道mysql爲了能進行回滾是花了不少的代價,redis應用的場景更多是對抗高併發具備高性能,所以redis選擇更簡單,更快速無回滾的方式處理事務也是符合場景。

2.一致性

事務具備一致性指的是,如果數據庫在執行事務之前是一致的,那麼在事務執行之後,無論事務是否成功,數據庫也應該是一致的。

從redis來說可以從2個層面,一個是執行錯誤是否有確保一致性,另一個是宕機時,redis是否有確保一致性的機制。

2.1 執行錯誤是否有確保一致性

webp

依然去執行一個錯誤的事務,在事務執行的過程中會識別出來並進行錯誤處理,這些錯誤並不會對數據庫作出修改,也不會對事務的一致性產生影響。

2.2 宕機對一致性的影響

暫不考慮分佈式高可用的redis解決方案,先從單機看宕機恢復是否能滿意數據完整性約束。

無論是rdb還是aof持久化方案,可以使用rdb文件或aof文件進行恢復數據,從而將數據庫還原到一個一致的狀態。

2.3 再議一致性

上面 執行錯誤 和 宕機 對一致性的影響的觀點摘自黃健宏 《Redis設計與實現》,當在讀這章的時候還是有一些存疑的點,歸根到底redis並非關係型數據庫,如果僅僅就ACID的表述上來說,一致性就是從A狀態經過事務到達B狀態沒有破壞各種約束性,僅就redis而已不談實現的業務,那顯然就是滿意一致性。

但如果加上業務去談一致性,例如,A轉賬給B,A減少10塊錢,B增加10塊錢,因爲redis並不具備回滾,也就不具備傳統意義上的原子性,所以從redis也應該不具備傳統的一致性。

其實,這裏只是簡單討論下redis在傳統ACID上的概念怎麼進行對接,或許,有可能是我想多了,用傳統關係型數據庫的ACID去審覈redis是沒有意義的,redis本來就沒有意願去實現ACID的事務。

3.隔離性

隔離性指的是,數據庫中有多個事務併發的執行,各個事務之間不會相互影響,並且在併發狀態下執行的事務和串行執行的事務產生的結果是完全相同的。

redis 因爲是單線程操作,所以在隔離性上有天生的隔離機制,當redis執行事務時,redis的服務端保證在執行事務期間不會對事務進行中斷,所以,redis事務總是以串行的方式運行,事務也具備隔離性。

4.持久性

事務的持久性指的是,當一個事務執行完畢,執行這個事務所得到的結果被保存在持久化的存儲中,即使服務器在事務執行完成後停機了,執行的事務的結果也不會被丟失。

redis是否具備持久化,這個取決於redis的持久化模式

(1)純內存運行,不具備持久化,服務一旦停機,所有數據將丟失

(2)RDB模式,取決於RDB策略,只有在滿足策略纔會執行bgsave,異步執行並不能保證redis具備持久化

(3)aof模式,只有將appendfsync設置爲always,程序纔會在執行命令同步保存到磁盤,這個模式下,redis具備持久化

(將appendfsync設置爲always,只是在理論上持久化可行,但一般不會這麼操作)

5.簡單總結

(1)redis具備了一定的原子性,但不支持回滾

(2)redis不具備ACID中一致性的概念(或者說redis在設計就無視這點)

(3)redis具備隔離性

(4)redis通過一定策略可以保證持久性

redis和ACID純屬站在使用者的角度去思想,redis設計更多的是追求簡單與高性能,不會受制於傳統ACID的束縛。

03  redis的樂觀鎖watch是怎麼實現的?

當我們一提到樂觀鎖就會想起CAS(Compare And Set),CAS操作包含三個操作數—— 內存位置的值(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置更新爲新值。否則,處理器不做任何操作。

在redis的事務中使用watch實現,watch 會在事務開始之前盯住 1 個或多個關鍵變量,當事務執行時 也就是服務器收到了 exec 指令要順序執行緩存的事務隊列時, Redis 會檢查關鍵變量自 watch 之後,是否被修改了。

webp

1.java的AtomicXXX的樂觀鎖機制

在java中我們也經常的使用到一些樂觀鎖的參數,例如AtomicXXX,這些機制的背後是怎麼去實現的,是否redis也跟java的CAS實現機制是一樣,先來看看java的Atomic類,我們追一下源碼,可以看到它的背後其實是 Unsafe_CompareAndSwapObject

webp

可以看見compareAndSwapObject是native方法,需要在繼續追查

webp

2.cmpxchg

可以發現追查到最終cas,“比較並修改”,本來是兩個語意,但是最終確實一條cpu指令cmpxchg完成,cmpxchg是一條CPU指令的命令而不是多條cpu指令,所以它不會被多線程的調度所打斷,所以能夠保證CAS的操作是一個原子操作。當然cmpxchg的機制其實存在ABA還有多次重試的問題,這個不在這裏討論。

3.redis的watch機制

redis的watch也是使用cmpxchg嗎,兩者存在相似之處也用法上也有一些不同,redis的watch不存在aba問題,也沒有多次重試機制,其中有一個最重大的不同是:

redis事務執行其實是串行的,簡單追一下源碼: 摘錄出來的源碼可能有些凌亂,不錯可以簡單總結出來數據結構圖和簡單的流程圖,之後再看源碼就會清晰很多

webp

webp

存儲

webp

redisDb存放了一個watched_keys的dcit結構,每個被watch的key的值是一個鏈表結構,存放的是一組redis客戶端標誌。

流程

webp

每一次watch,multi,exec時都會去查詢這個watched_keys結構進行判斷,每次touch到被watch的key時都會標誌爲 CLIENT_DIRTY_CAS

因爲在redis中所有的事務都是串行的,假設有客戶端A和客戶端B都watch同一個key,當客戶端A進行touch修改或者A率先執行完,會把客戶端A從這個watched_keys的這個key的列表刪除然後把這個列表所有的客戶端都設置成CLIENT_DIRTY_CAS,當後面的客戶端B開始執行時,判斷到自己的狀態是CLIENT_DIRTY_CAS,便discardTransaction終止事務。

4.簡單總結

cmpxchg 的 實現主要是利用了cpu指令,看似兩個操作使用一條cpu指令完成,所以不會被多線程進行打斷。而redis的watch機制,更多是利用了redis本身單線程的機制,採用了watched_keys的數據結構和串行流程實現了樂觀鎖機制。

04  redis是如何持久化的?

webp

redis的持久化有兩種機制,一個是RDB,也就是快照,快照就是一次全量的備份,會把所有redis的內存數據進行二進制的序列化存儲到磁盤。另一種是aof日記,aof日誌記錄的是數據操作修改的指令記錄日誌,可以類比mysql的binlog,aof日期隨着時間的推移只會無限增量。

在對redis進行恢復時,rdb快照直接讀取磁盤既可以恢復,而aof需要對所有的操作指令進行重放進行恢復,這個過程有可能非常漫長。

webp

1.RDB

redis在進行RDB的快照生成有兩種方法,一種是save,由於redis是單進程單線程,直接使用save,redis會進行一個龐大的文件io操作,由於單進程單線程勢必會阻塞線上的業務,一般的話不會直接採用save,而是採用bgsave,之前一直說redis是單進程單線程,其實不然,在使用bgsave的時候,redis會fork一個子進程,快照的持久化就交給子進程去處理,而父進程繼續處理線上業務的請求。

1.1 fork機制

想要弄清楚RDB快照的生成原理就必須弄清楚fork機制,fork機制是linux操作系統的一個進程機制,當父進程fork出來一個子進程,子進程和夫進程擁有共同的內存數據結構,子進程剛剛產生時,它和父進程共享內存裏面的代碼段和數據段。

webp

一開始兩個進程都具備了相同的內存段,子進程在做數據持久化時,不會去修改現在的內存數據,而是會採用cow(copy on write)的方式將數據段頁面進行分離,當父進程修改了某一個數據段時,被共享的頁面就會複製一份分離出來,然後父進程再在新的數據段進行修改

webp

1.2 分裂

這個過程也成爲分裂的過程,本來父子進程都指向很多相同的內存塊,但是如果父進程對其中某個內存塊進行該修改,就會將其複製出來,進行分裂再在新的內存塊上面進行修改。

因爲子進程在fork的時候就可以固定內存,這個時間點的數據將不會產生變化,所以我們可以安心的產生快照不用擔心快照的內容收到父進程業務請求的影響,另外可以想象,如果在bgsave的過程中,redis沒有任何操作,父進程沒有接收到任何業務請求也沒有任何的背後例如過期移除等操作,父進程和子進程將會使用相同的內存塊。

2.AOF

AOF是redis操作指令的日誌存儲,類同於爲mysql的binlog,假設AOF從redis創建以來就一直執行,那麼AOF就記錄了所有的redis指令的記錄,如果要恢復redis,可以對AOF進行指令重放,便可修復整個redis實例,不過AOF日誌也有兩個比較大的問題,一個是AOF的日誌會隨着時間遞增,如果一個數據量大運行的時間久,AOF日誌量將變得異常龐大,另一個問題是AOF在做數據恢復時,由於重放的量非常龐大,恢復的時間將會非常的長。

AOF寫操作是在redis處理完業務邏輯之後,按照一定的策略纔會進行些aof日誌存盤,這點跟mysql的redolog和binlog有很大的不同,其實也因爲此原因,redis因爲處理邏輯在前而記錄操作日誌在後,也是導致redis無法進行回滾的一個原因。

2.1 bgrewriteao

針對上述的問題,redis在2.4之後也使用了bgrewriteaof對aof日誌進行瘦身,bgrewriteaof 命令用於異步執行一個AOF文件重寫操作。重寫會創建一個當前AOF文件的體積優化版本

2.2 RDB和AOF混合搭配模式

在對redis進行恢復的時候,如果我們採用了RDB的方式,因爲bgsave的策略,可能會導致我們丟失大量的數據。如果我們採用了AOF的模式,通過AOF操作日誌重放恢復,重放AOF日誌比RDB要長久很多。

webp

redis4.0之後,爲了解決這個問題,引入了新的持久化模式,混合持久化,將rdb的文件和局部增量的AOF文件相結合,rdb可以使用相隔較長的時間保存策略,aof不需要是全量日誌,只需要保存前一次rdb存儲開始到這段時間增量aof日誌即可,一般來說,這個日誌量是非常小的。

05  redis在內存使用上是如何開源節流?

webp

redis跟其他傳統數據庫不同,redis是一個純內存的數據庫,並且存儲了都是一些數據結構的數據,如果不對內存加以控制的話,redis很可能會因爲數據量過大導致系統的奔潰

1.ziplist

webp

當最開始嘗試開啓一個小數據量的hash結構和一個zset結構時,發現他們在redis裏面的真正結構類型是一個ziplist,ziplist是一個緊湊的數據結構,每一個元素之間都是連續的內存,如果在redis中,redis啓用的數據結構數據量很小時,redis就會切換到使用緊湊存儲的形式來進行壓縮存儲。

webp

例如,上面的例子,我們採用了hash結構進行存儲,hash結構是一個二維的結構,是一個典型的用空間換取時間的結構。但是如果使用的數據量很小,使用二維結構反而浪費了空間,在時間的性能上也並沒有得到太大的提升,還不如直接使用一維結構進行存儲,在查找的時候,雖然複雜度是O(n),但是因爲數據量少遍歷也非常快,增至比hash結構本身的查詢更快。

如果當集合對象的元素不斷的增加,或者某個value的值過大,這種小對象存儲也會升級生成標準的結構。redis也可以在配置中進行定義緊湊結構和標準結構的轉換參數:

webp

2.quicklist

webp

quicklist數據結構是redis在3.2才引入的一個雙向鏈表的數據結構,確實來說是一個ziplist的雙向鏈表。quicklist的每一個數據節點是一個ziplist,ziplist本身就是一個緊湊列表,假使,quicklist包含了5個ziplist的節點,每個ziplist列表又包含了5個數據,那麼在外部看來,這個quicklist就包含了25個數據項。

webp

quicklist的結構的設計簡單總結起來,是一個空間和時間的折中方案:

雙向鏈表可以在兩端進行push和pop操作,但是它在每一個節點除了保存自身的數據外,還要保存兩個指針,增加額外的內存開銷。其次是由於每個節點都是獨立的,在內存地址上並不連續,節點多了容易產生內存碎片。

ziplist本身是一塊連續的內存,存儲和查詢效率很高,但是,它不利於修改操作,每次數據變動時都會引發內存realloc,如果ziplist長度很長時,一次realloc會導致大批量數據拷貝。

所以,結合ziplist和雙向鏈表的優點,quciklist就孕育而生。

3.對象共享

redis在自己的對象系統中構建了一個引用計數方法,通過這個方法程序可以跟蹤對象的引用計數信息,除了可以在適當的時候進行釋放對象,還可以用來作爲對象共享。 舉個例子,假使健A創建了一個整數值100的字符串作爲值對象,這個時候鍵B也創建保存同樣整數值100的字符串對象作爲值對象,那麼在redis的操作時:

講數據庫鍵的指針指向一個現有的值對象

講被共享的值對象引用計數加一

webp

假使,我們的數據庫中指向整數值100的鍵不止鍵A和鍵B,而是有幾百個,那麼redis服務器中只需要一個字符串對象的內存就可以保存原本需要幾百個字符串對象的內存才能保存的數據。

06  redis是如何實現主從複製?

webp

幾個定義

(1)runID 服務器運行的ID

(2)offset 主服務器的複製偏移量和從服務器複製的偏移量

(3)replication backlog 主服務器的複製積壓緩衝區

在redis2.8之後,使用psync命令代替sync命令來執行復制的同步操作,psync命令具有完整重同步和部分重同步兩種模式:

(1)其中完整同步用於處理初次複製情況:完整重同步的執行步驟和sync命令執行步驟一致,都是通過讓主服務器創建併發送rdb文件,以及向從服務器發送保存在緩衝區的寫命令來進行同步。

(2)部分重同步是用於處理斷線後重複製情況:當從服務器在斷線後重新連接主服務器時,主服務可以講主從服務器連接斷開期間執行的寫命令發送給從服務器,從服務器只要接收並執行這些寫命令,就可以講數據庫更新至主服務器當前所處的狀態。

完整重同步:

webp

(1)slave發送psync給master,由於是第一次發送,不帶上runid和offset

(2)master接收到請求,發送master的runid和offset給從節點

(3)master生成保存rdb文件

(4)master發送rdb文件給slave

(5)在發送rdb這個操作的同時,寫操作會複製到緩衝區replication backlog buffer中,並從buffer區發送到slave

(6)slave將rdb文件的數據裝載,並更新自身數據

如果網絡的抖動或者是短時間的斷鏈也需要進行完整同步就會導致大量的開銷,這些開銷包括了,bgsave的時間,rdb文件傳輸的時間,slave重新加載rdb時間,如果slave有aof,還會導致aof重寫。這些都是大量的開銷所以在redis2.8之後也實現了部分重同步的機制。

部分重同步:

webp

(1)網絡發生錯誤,master和slave失去連接

(2)master依然向buffer緩衝區寫入數據

(3)slave重新連接上master

(4)slave向master發送自己目前的runid和offset

(5)master會判斷slave發送給自己的offset是否存在buffer隊列中,如果存在,則發送continue給slave,如果不存在,意味着可能錯誤了太多的數據,緩衝區已經被清空,這個時候就需要重新進行全量的複製

(6)master發送從offset偏移後的緩衝區數據給slave

(7)slave獲取數據更新自身數據

07  redis是怎麼制定過期刪除策略的?

當一個鍵處於過期的狀態,其實在redis中這個內存並不是實時就被從內存中進行摘除,而是redis通過一定的機制去把一些處於過期鍵進行移除,進而達到內存的釋放,那麼當一個鍵處於過期,redis會在什麼時候去刪除?幾時被刪除存在三種可能性,這三種可能性也代表了redis的三種不同的刪除策略。

定時刪除: 在設置鍵過去的時間同時,創建一個定時器,讓定時器在鍵過期時間來臨,立即執行對鍵的刪除操作。

惰性刪除: 放任鍵過期不管,但是每次從鍵空間獲取鍵時,都會檢查該鍵是否過期,如果過期的話,就刪除該鍵。

定期刪除: 每隔一段時間,程序都要對數據庫進行一次檢查,刪除裏面的過期鍵,至於要刪除多少過期鍵,由算法而定。

1.定時刪除

設置鍵的過期時間,創建定時器,一旦過期時間來臨,就立即對鍵進行操作操作,這種對內存是友好的,但是對cpu的時間是最不友好的,特別是在業務繁忙,過期鍵很多的時候,刪除過期鍵這個操作就會佔據很大一部分cpu的時間,要知道redis是單線程操作,在內存不緊張而cpu緊張的時候,將cpu的時間浪費在與業務無關的刪除過期鍵上面,會對redis的服務器的響應時間和吞吐量造成影響。 另外,創建一個定時器需要用到redis服務器中的時間事件,而當親時間事件的實現方式是無序鏈表,時間複雜度爲O(n),讓服務器大量創建定時器去實現定時刪除策略,會產生較大的性能影響,所以,定時刪除並不是一種好的刪除策略。

2.惰性刪除

與定時刪除相反,惰性刪除策略對cpu來說是最友好的,程序只有在取出鍵的時候纔會進行檢查,是一種被動的過程。與此同時,惰性刪除對內存來說又是最不友好的,一個鍵過期,只要不再被取出,這個過期鍵就不會被刪除,它佔用的內存也不會被釋放。 很明顯,惰性刪除也不是一個很好的策略,redis是非常依賴內存和驍好內存的,如果一些長期鍵長期沒有被訪問,就會造成大量的內存垃圾,甚至會操成內存的泄漏。

在對執行數據寫入時,通過expireIfNeeded函數對寫入的key進行過期判斷,其中expireIfNeeded在內部做了三件事情,分別是:

查看key是否過期

向slave節點傳播執行過去key的動作

刪除過期key

webp

3.定期刪除

上面兩種刪除策略,無論是定時刪除和惰性刪除,這兩種刪除方式在單一的使用上都存在明顯的缺陷,要麼佔用太多cpu時間,要麼浪費太多內存。定期刪除策略是前兩種策略的一個整合和折中

定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時間和頻率來減少刪除操作對cpu時間影響

通過合理的刪除執行的時長和頻率,來達到合理的刪除過期鍵


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