追著 redis 進行七連問 Hello Redis 有幾個問題想請教你 Hello,Redis! 我們相處已經很多年了,從模糊的認識到現在我們已經深入結合,你的好我一直都知道也一直都記住,能否在讓我多問問你的幾個問題,讓我更加深入的去瞭解你。 1. redis的通訊協議是什麼 redis的通訊協議 ...
追著 redis 進行七連問
Hello Redis 有幾個問題想請教你
Hello,Redis! 我們相處已經很多年了,從模糊的認識到現在我們已經深入結合,你的好我一直都知道也一直都記住,能否在讓我多問問你的幾個問題,讓我更加深入的去瞭解你。
1. redis的通訊協議是什麼
redis的通訊協議是文本協議,是的,Redis伺服器與客戶端通過RESP(REdis Serialization Protocol)協議通信,沒錯,文本協議確實是會浪費流量,不過它的優點在於直觀,非常的簡單,解析性能及其的好,我們不需要一個特殊的redis客戶端僅靠telnet或者是文本流就可以跟redis進行通訊。
客戶端的命令格式:
- 簡單字元串 Simple Strings, 以 "+"加號 開頭
- 錯誤 Errors, 以"-"減號 開頭
- 整數型 Integer, 以 ":" 冒號開頭
- 大字元串類型 Bulk Strings, 以 "$"美元符號開頭
- 數組類型 Arrays,以 "*"星號開頭
set hello abc
一個簡單的文本流就可以是redis的客戶端
複製代碼
簡單總結
具體可以見 redis.io/topics/prot… ,redis文檔認為簡單的實現,快速的解析,直觀理解是採用RESP文本協議最重要的地方,有可能文本協議會造成一定量的流量浪費,但卻在性能上和操作上快速簡單,這中間也是一個權衡和協調的過程。
2. redis究竟有沒有ACID事務
要弄清楚redis有沒有事務,其實很簡單,上redis的官網查看文檔,發現:
redis確實是有事務,不過按照傳統的事務定義ACID來看,redis是不是都具備了ACID的特性,ACID指的是 1.原子性 2.一致性 3.隔離性 4.持久性,我們將使用以上redis事務的命令來檢驗是否redis都具備了ACID的各個特征。
原子性
事務具備原子性指的是,資料庫將事務中多個操作當作一個整體來執行,服務要麼執行事務中所有的操作,要不一個操作也不會執行。
事務隊列
首先弄清楚redis開始事務multi命令後,redis會為這個事務生成一個隊列,每次操作的命令都會按照順序插入到這個隊列中,這個隊列裡面的命令不會被馬上執行,知道exec命令提交事務,所有隊列裡面的命令會被一次性,並且排他的進行執行。
對應 ->
從上面的例子可以看出,當執行一個成功的事務,事務裡面的命令都是按照隊列裡面順序的並且排他的執行。但原子性又一個特點就是要麼全部成功,要不全部失敗,也就是我們傳統DB裡面說的回滾。
當我們執行一個失敗的事務:
可以發現,就算中間出現了失敗,set abc x這個操作也已經被執行了,並沒有進行回滾,從嚴格的意義上來將redis並不具備原子性。
為何redis不支持回滾
這個其實跟redis的定位和設計有關係,先看看為何我們的mysql可以支持回滾,這個還是跟寫log有關係,redis是完成操作之後才會進行aof日誌記錄,aof日誌的定位只是記錄操作的指令記錄,而mysql有完善的redolog,並且是在事務進行commit之前就會寫完成redolog,binlog
要知道mysql為了能進行回滾是花了不少的代價,redis應用的場景更多是對抗高併發具備高性能,所以redis選擇更簡單,更快速無回滾的方式處理事務也是符合場景。
一致性
事務具備一致性指的是,如果資料庫在執行事務之前是一致的,那麼在事務執行之後,無論事務是否成功,資料庫也應該是一致的。
從redis來說可以從2個層面,一個是執行錯誤是否有確保一致性,另一個是宕機時,redis是否有確保一致性的機制。
執行錯誤是否有確保一致性
依然去執行一個錯誤的事務,在事務執行的過程中會識別出來併進行錯誤處理,這些錯誤並不會對資料庫作出修改,也不會對事務的一致性產生影響。
宕機對一致性的影響
暫不考慮分散式高可用的redis解決方案,先從單機看宕機恢復是否能滿意數據完整性約束。
無論是rdb還是aof持久化方案,可以使用rdb文件或aof文件進行恢複數據,從而將資料庫還原到一個一致的狀態。
再議一致性 ❓❓
上面 執行錯誤 和 宕機 對一致性的影響的觀點摘自黃健巨集 《Redis設計與實現》,當在讀這章的時候還是有一些存疑的點,歸根到底redis並非關係型資料庫,如果僅僅就ACID的表述上來說,一致性就是從A狀態經過事務到達B狀態沒有破壞各種約束性,僅就redis而已不談實現的業務,那顯然就是滿意一致性。
但如果加上業務去談一致性,例如,A轉賬給B,A減少10塊錢,B增加10塊錢,因為redis並不具備回滾,也就不具備傳統意義上的原子性,所以從redis也應該不具備傳統的一致性。
其實,這裡只是簡單討論下redis在傳統ACID上的概念怎麼進行對接,或許,有可能是我想多了,用傳統關係型資料庫的ACID去審核redis是沒有意義的,redis本來就沒有意願去實現ACID的事務。
隔離性
隔離性指的是,資料庫中有多個事務併發的執行,各個事務之間不會相互影響,並且在併發狀態下執行的事務和串列執行的事務產生的結果是完全相同的。
redis 因為是單線程操作,所以在隔離性上有天生的隔離機制,當redis執行事務時,redis的服務端保證在執行事務期間不會對事務進行中斷,所以,redis事務總是以串列的方式運行,事務也具備隔離性。
持久性
事務的持久性指的是,當一個事務執行完畢,執行這個事務所得到的結果被保存在持久化的存儲中,即使伺服器在事務執行完成後停機了,執行的事務的結果也不會被丟失。
redis是否具備持久化,這個取決於redis的持久化模式
- 純記憶體運行,不具備持久化,服務一旦停機,所有數據將丟失
- RDB模式,取決於RDB策略,只有在滿足策略才會執行bgsave,非同步執行並不能保證redis具備持久化
- aof模式,只有將appendfsync設置為always,程式才會在執行命令同步保存到磁碟,這個模式下,redis具備持久化
(將appendfsync設置為always,只是在理論上持久化可行,但一般不會這麼操作)
簡單總結
- redis具備了一定的原子性,但不支持回滾
- redis不具備ACID中一致性的概念(或者說redis在設計就無視這點)
- redis具備隔離性
- redis通過一定策略可以保證持久性
redis和ACID純屬站在使用者的角度去思想,redis設計更多的是追求簡單與高性能,不會受制於傳統ACID的束縛。
3. redis的樂觀鎖watch是怎麼實現的?
當我們一提到樂觀鎖就會想起CAS(Compare And Set),CAS操作包含三個操作數—— 記憶體位置的值(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置更新為新值。否則,處理器不做任何操作。
在redis的事務中使用watch實現,watch 會在事務開始之前盯住 1 個或多個關鍵變數,當事務執行時 也就是伺服器收到了 exec 指令要順序執行緩存的事務隊列時, Redis 會檢查關鍵變數自 watch 之後,是否被修改了。
java的AtomicXXX的樂觀鎖機制
在java中我們也經常的使用到一些樂觀鎖的參數,例如AtomicXXX,這些機制的背後是怎麼去實現的,是否redis也跟java的CAS實現機制是一樣,先來看看java的Atomic類,我們追一下源碼,可以看到它的背後其實是 Unsafe_CompareAndSwapObject
可以看見compareAndSwapObject是native方法,需要在繼續追查,可以下載源碼或打開hg.openjdk.java.net/jdk8u/
cmpxchg
可以發現追查到最終cas,“比較並修改”,本來是兩個語意,但是最終確實一條cpu指令cmpxchg完成,cmpxchg是一條CPU指令的命令而不是多條cpu指令,所以它不會被多線程的調度所打斷,所以能夠保證CAS的操作是一個原子操作。當然cmpxchg的機制其實存在ABA還有多次重試的問題,這個不在這裡討論。
redis的watch機制
redis的watch也是使用cmpxchg嗎,兩者存在相似之處也用法上也有一些不同,redis的watch不存在aba問題,也沒有多次重試機制,其中有一個最重大的不同是:
redis事務執行其實是串列的,簡單追一下源碼: 摘錄出來的源碼可能有些凌亂,不錯可以簡單總結出來數據結構圖和簡單的流程圖,之後再看源碼就會清晰很多
存儲
redisDb存放了一個watched_keys的dcit結構,每個被watch的key的值是一個鏈表結構,存放的是一組redis客戶端標誌。
流程
每一次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終止事務。
簡單總結
cmpxchg 的 實現主要是利用了cpu指令,看似兩個操作使用一條cpu指令完成,所以不會被多線程進行打斷。而redis的watch機制,更多是利用了redis本身單線程的機制,採用了watched_keys的數據結構和串列流程實現了樂觀鎖機制。
4. redis是如何持久化的
redis的持久化有兩種機制,一個是RDB,也就是快照,快照就是一次全量的備份,會把所有redis的記憶體數據進行二進位的序列化存儲到磁碟。另一種是aof日記,aof日誌記錄的是數據操作修改的指令記錄日誌,可以類比mysql的binlog,aof日期隨著時間的推移只會無限增量。
在對redis進行恢復時,rdb快照直接讀取磁碟既可以恢復,而aof需要對所有的操作指令進行重放進行恢復,這個過程有可能非常漫長。
RDB
redis在進行RDB的快照生成有兩種方法,一種是save,由於redis是單進程單線程,直接使用save,redis會進行一個龐大的文件io操作,由於單進程單線程勢必會阻塞線上的業務,一般的話不會直接採用save,而是採用bgsave,之前一直說redis是單進程單線程,其實不然,在使用bgsave的時候,redis會fork一個子進程,快照的持久化就交給子進程去處理,而父進程繼續處理線上業務的請求。
fork機制
想要弄清楚RDB快照的生成原理就必須弄清楚fork機制,fork機制是linux操作系統的一個進程機制,當父進程fork出來一個子進程,子進程和夫進程擁有共同的記憶體數據結構,子進程剛剛產生時,它和父進程共用記憶體裡面的代碼段和數據段。
一開始兩個進程都具備了相同的記憶體段,子進程在做數據持久化時,不會去修改現在的記憶體數據,而是會採用cow(copy on write)的方式將數據段頁面進行分離,當父進程修改了某一個數據段時,被共用的頁面就會複製一份分離出來,然後父進程再在新的數據段進行修改。
分裂
這個過程也成為分裂的過程,本來父子進程都指向很多相同的記憶體塊,但是如果父進程對其中某個記憶體塊進行該修改,就會將其複製出來,進行分裂再在新的記憶體塊上面進行修改。
因為子進程在fork的時候就可以固定記憶體,這個時間點的數據將不會產生變化,所以我們可以安心的產生快照不用擔心快照的內容收到父進程業務請求的影響,另外可以想象,如果在bgsave的過程中,redis沒有任何操作,父進程沒有接收到任何業務請求也沒有任何的背後例如過期移除等操作,父進程和子進程將會使用相同的記憶體塊。
AOF
AOF是redis操作指令的日誌存儲,類同於為mysql的binlog,假設AOF從redis創建以來就一直執行,那麼AOF就記錄了所有的redis指令的記錄,如果要恢復redis,可以對AOF進行指令重放,便可修複整個redis實例,不過AOF日誌也有兩個比較大的問題,一個是AOF的日誌會隨著時間遞增,如果一個數據量大運行的時間久,AOF日誌量將變得異常龐大,另一個問題是AOF在做數據恢復時,由於重放的量非常龐大,恢復的時間將會非常的長。
AOF寫操作是在redis處理完業務邏輯之後,按照一定的策略才會進行些aof日誌存檔,這點跟mysql的redolog和binlog有很大的不同,其實也因為此原因,redis因為處理邏輯在前而記錄操作日誌在後,也是導致redis無法進行回滾的一個原因。
bgrewriteaof
針對上述的問題,redis在2.4之後也使用了bgrewriteaof對aof日誌進行瘦身,bgrewriteaof 命令用於非同步執行一個AOF文件重寫操作。重寫會創建一個當前AOF文件的體積優化版本。
RDB和AOF混合搭配模式
在對redis進行恢復的時候,如果我們採用了RDB的方式,因為bgsave的策略,可能會導致我們丟失大量的數據。如果我們採用了AOF的模式,通過AOF操作日誌重放恢復,重放AOF日誌比RDB要長久很多。
redis4.0之後,為瞭解決這個問題,引入了新的持久化模式,混合持久化,將rdb的文件和局部增量的AOF文件相結合,rdb可以使用相隔較長的時間保存策略,aof不需要是全量日誌,只需要保存前一次rdb存儲開始到這段時間增量aof日誌即可,一般來說,這個日誌量是非常小的。
5. redis在記憶體使用上是如何開源節流
redis跟其他傳統資料庫不同,redis是一個純記憶體的資料庫,並且存儲了都是一些數據結構的數據,如果不對記憶體加以控制的話,redis很可能會因為數據量過大導致系統的奔潰
ziplist
127.0.0.1:6379> hset hash_test abc 1
(integer) 1
127.0.0.1:6379> object encoding hash_test
"ziplist"
127.0.0.1:6379> zadd z_test 10 key
(integer) 1
127.0.0.1:6379> object encoding z_test
"ziplist"
複製代碼
當最開始嘗試開啟一個小數據量的hash結構和一個zset結構時,發現他們在redis裡面的真正結構類型是一個ziplist,ziplist是一個緊湊的數據結構,每一個元素之間都是連續的記憶體,如果在redis中,redis啟用的數據結構數據量很小時,redis就會切換到使用緊湊存儲的形式來進行壓縮存儲。
例如,上面的例子,我們採用了hash結構進行存儲,hash結構是一個二維的結構,是一個典型的用空間換取時間的結構。但是如果使用的數據量很小,使用二維結構反而浪費了空間,在時間的性能上也並沒有得到太大的提升,還不如直接使用一維結構進行存儲,在查找的時候,雖然複雜度是O(n),但是因為數據量少遍歷也非常快,增至比hash結構本身的查詢更快。
如果當集合對象的元素不斷的增加,或者某個value的值過大,這種小對象存儲也會升級生成標準的結構。redis也可以在配置中進行定義緊湊結構和標準結構的轉換參數:
hash-max-ziplist-entries 512 # hash的元素個數超過512就必須用標準結構存儲
hash-max-ziplist-value 64 # hash的任意元素的key/value的長度超過 64 就必須用標準結構存儲
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512
複製代碼
quicklist
127.0.0.1:6379> rpush key v1
(integer) 1
127.0.0.1:6379> object encoding key
"quicklist"
複製代碼
quicklist數據結構是redis在3.2才引入的一個雙向鏈表的數據結構,確實來說是一個ziplist的雙向鏈表。quicklist的每一個數據節點是一個ziplist,ziplist本身就是一個緊湊列表,假使,quicklist包含了5個ziplist的節點,每個ziplist列表又包含了5個數據,那麼在外部看來,這個quicklist就包含了25個數據項。
quicklist的結構的設計簡單總結起來,是一個空間和時間的折中方案:
- 雙向鏈表可以在兩端進行push和pop操作,但是它在每一個節點除了保存自身的數據外,還要保存兩個指針,增加額外的記憶體開銷。其次是由於每個節點都是獨立的,在記憶體地址上並不連續,節點多了容易產生記憶體碎片。
- ziplist本身是一塊連續的記憶體,存儲和查詢效率很高,但是,它不利於修改操作,每次數據變動時都會引發記憶體realloc,如果ziplist長度很長時,一次realloc會導致大批量數據拷貝。
所以,結合ziplist和雙向鏈表的優點,quciklist就孕育而生。
對象共用
redis在自己的對象系統中構建了一個引用計數方法,通過這個方法程式可以跟蹤對象的引用計數信息,除了可以在適當的時候進行釋放對象,還可以用來作為對象共用。 舉個例子,假使健A創建了一個整數值100的字元串作為值對象,這個時候鍵B也創建保存同樣整數值100的字元串對象作為值對象,那麼在redis的操作時:
- 講資料庫鍵的指針指向一個現有的值對象
- 講被共用的值對象引用計數加一
假使,我們的資料庫中指向整數值100的鍵不止鍵A和鍵B,而是有幾百個,那麼redis伺服器中只需要一個字元串對象的記憶體就可以保存原本需要幾百個字元串對象的記憶體才能保存的數據。
6. redis是如何實現主從複製
幾個定義
- runID 伺服器運行的ID
- offset 主伺服器的複製偏移量和從伺服器複製的偏移量
- replication backlog 主伺服器的複製積壓緩衝區
在redis2.8之後,使用psync命令代替sync命令來執行複製的同步操作,psync命令具有完整重同步和部分重同步兩種模式:
- 其中完整同步用於處理初次複製情況:完整重同步的執行步驟和sync命令執行步驟一致,都是通過讓主伺服器創建併發送rdb文件,以及向從伺服器發送保存在緩衝區的寫命令來進行同步。
- 部分重同步是用於處理斷線後重覆制情況:當從伺服器在斷線後重新連接主伺服器時,主服務可以講主從伺服器連接斷開期間執行的寫命令發送給從伺服器,從伺服器只要接收並執行這些寫命令,就可以講資料庫更新至主伺服器當前所處的狀態。
完整重同步:
- slave發送psync給master,由於是第一次發送,不帶上runid和offset
- master接收到請求,發送master的runid和offset給從節點
- master生成保存rdb文件
- master發送rdb文件給slave
- 在發送rdb這個操作的同時,寫操作會複製到緩衝區replication backlog buffer中,並從buffer區發送到slave
- slave將rdb文件的數據裝載,並更新自身數據
如果網路的抖動或者是短時間的斷鏈也需要進行完整同步就會導致大量的開銷,這些開銷包括了,bgsave的時間,rdb文件傳輸的時間,slave重新載入rdb時間,如果slave有aof,還會導致aof重寫。這些都是大量的開銷所以在redis2.8之後也實現了部分重同步的機制。
部分重同步:
- 網路發生錯誤,master和slave失去連接
- master依然向buffer緩衝區寫入數據
- slave重新連接上master
- slave向master發送自己目前的runid和offset
- master會判斷slave發送給自己的offset是否存在buffer隊列中,如果存在,則發送continue給slave,如果不存在,意味著可能錯誤了太多的數據,緩衝區已經被清空,這個時候就需要重新進行全量的複製
- master發送從offset偏移後的緩衝區數據給slave
- slave獲取數據更新自身數據
7. redis是怎麼制定過期刪除策略的
當一個鍵處於過期的狀態,其實在redis中這個記憶體並不是實時就被從記憶體中進行摘除,而是redis通過一定的機制去把一些處於過期鍵進行移除,進而達到記憶體的釋放,那麼當一個鍵處於過期,redis會在什麼時候去刪除?幾時被刪除存在三種可能性,這三種可能性也代表了redis的三種不同的刪除策略。
- 定時刪除: 在設置鍵過去的時間同時,創建一個定時器,讓定時器在鍵過期時間來臨,立即執行對鍵的刪除操作。
- 惰性刪除: 放任鍵過期不管,但是每次從鍵空間獲取鍵時,都會檢查該鍵是否過期,如果過期的話,就刪除該鍵。
- 定期刪除: 每隔一段時間,程式都要對資料庫進行一次檢查,刪除裡面的過期鍵,至於要刪除多少過期鍵,由演算法而定。
定時刪除
設置鍵的過期時間,創建定時器,一旦過期時間來臨,就立即對鍵進行操作操作,這種對記憶體是友好的,但是對cpu的時間是最不友好的,特別是在業務繁忙,過期鍵很多的時候,刪除過期鍵這個操作就會占據很大一部分cpu的時間,要知道redis是單線程操作,在記憶體不緊張而cpu緊張的時候,將cpu的時間浪費在與業務無關的刪除過期鍵上面,會對redis的伺服器的響應時間和吞吐量造成影響。 另外,創建一個定時器需要用到redis伺服器中的時間事件,而當親時間事件的實現方式是無序鏈表,時間複雜度為O(n),讓伺服器大量創建定時器去實現定時刪除策略,會產生較大的性能影響,所以,定時刪除並不是一種好的刪除策略。
惰性刪除
與定時刪除相反,惰性刪除策略對cpu來說是最友好的,程式只有在取出鍵的時候才會進行檢查,是一種被動的過程。與此同時,惰性刪除對記憶體來說又是最不友好的,一個鍵過期,只要不再被取出,這個過期鍵就不會被刪除,它占用的記憶體也不會被釋放。 很明顯,惰性刪除也不是一個很好的策略,redis是非常依賴記憶體和驍好記憶體的,如果一些長期鍵長期沒有被訪問,就會造成大量的記憶體垃圾,甚至會操成記憶體的泄漏。
在對執行數據寫入時,通過expireIfNeeded函數對寫入的key進行過期判斷,其中expireIfNeeded在內部做了三件事情,分別是:
- 查看key是否過期
- 向slave節點傳播執行過去key的動作
- 刪除過期key
定期刪除
上面兩種刪除策略,無論是定時刪除和惰性刪除,這兩種刪除方式在單一的使用上都存在明顯的缺陷,要麼占用太多cpu時間,要麼浪費太多記憶體。定期刪除策略是前兩種策略的一個整合和折中
- 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時間和頻率來減少刪除操作對cpu時間影響
- 通過合理的刪除執行的時長和頻率,來達到合理的刪除過期鍵
總結
可以說redis可謂博大精深,簡單的七連問或者只是盲人摸象,或者這次只是摸到了一根象鼻子,或者還應該順著鼻子向下摸,下次可能摸到了一隻象耳朵,只要願意往下深入去瞭解去摸索,而不只應用不思考,總有一天會把redis這種大象給摸透了。
源:https://juejin.im/post/5d29ac845188252cc75e2d5c