Hello,Redis! 咱們相處已經不少年了,從模糊的認識到如今咱們已經深刻結合,你的好我一直都知道也一直都記住,可否在讓我多問問你的幾個問題,讓我更加深刻的去了解你。java
redis的通信協議是文本協議,是的,Redis服務器與客戶端經過RESP(REdis Serialization Protocol)協議通訊,沒錯,文本協議確實是會浪費流量,不過它的優勢在於直觀,很是的簡單,解析性能及其的好,咱們不須要一個特殊的redis客戶端僅靠telnet或者是文本流就能夠跟redis進行通信。mysql
客戶端的命令格式:linux
set hello abc
一個簡單的文本流就能夠是redis的客戶端
複製代碼
具體能夠見 redis.io/topics/prot… ,redis文檔認爲簡單的實現,快速的解析,直觀理解是採用RESP文本協議最重要的地方,有可能文本協議會形成必定量的流量浪費,但卻在性能上和操做上快速簡單,這中間也是一個權衡和協調的過程。redis
要弄清楚redis有沒有事務,其實很簡單,上redis的官網查看文檔,發現: 算法
事務具有原子性指的是,數據庫將事務中多個操做看成一個總體來執行,服務要麼執行事務中全部的操做,要不一個操做也不會執行。sql
首先弄清楚redis開始事務multi命令後,redis會爲這個事務生成一個隊列,每次操做的命令都會按照順序插入到這個隊列中,這個隊列裏面的命令不會被立刻執行,知道exec命令提交事務,全部隊列裏面的命令會被一次性,而且排他的進行執行。數據庫
從上面的例子能夠看出,當執行一個成功的事務,事務裏面的命令都是按照隊列裏面順序的而且排他的執行。但原子性又一個特色就是要麼所有成功,要不所有失敗,也就是咱們傳統DB裏面說的回滾。數組
當咱們執行一個失敗的事務:緩存
能夠發現,就算中間出現了失敗,set abc x這個操做也已經被執行了,並無進行回滾,從嚴格的意義上來將redis並不具有原子性。bash
這個其實跟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的持久化模式
(將appendfsync設置爲always,只是在理論上持久化可行,但通常不會這麼操做)
redis和ACID純屬站在使用者的角度去思想,redis設計更多的是追求簡單與高性能,不會受制於傳統ACID的束縛。
當咱們一提到樂觀鎖就會想起CAS(Compare And Set),CAS操做包含三個操做數—— 內存位置的值(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置更新爲新值。不然,處理器不作任何操做。
在redis的事務中使用watch實現,watch 會在事務開始以前盯住 1 個或多個關鍵變量,當事務執行時 也就是服務器收到了 exec 指令要順序執行緩存的事務隊列時, Redis 會檢查關鍵變量自 watch 以後,是否被修改了。
在java中咱們也常常的使用到一些樂觀鎖的參數,例如AtomicXXX,這些機制的背後是怎麼去實現的,是否redis也跟java的CAS實現機制是同樣,先來看看java的Atomic類,咱們追一下源碼,能夠看到它的背後實際上是 Unsafe_CompareAndSwapObject
能夠看見compareAndSwapObject是native方法,須要在繼續追查,能夠下載源碼或打開 hg.openjdk.java.net/jdk8u/
能夠發現追查到最終cas,「比較並修改」,原本是兩個語意,可是最終確實一條cpu指令cmpxchg完成,cmpxchg是一條CPU指令的命令而不是多條cpu指令,因此它不會被多線程的調度所打斷,因此可以保證CAS的操做是一個原子操做。固然cmpxchg的機制其實存在ABA還有屢次重試的問題,這個不在這裏討論。
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的數據結構和串行流程實現了樂觀鎖機制。
redis的持久化有兩種機制,一個是RDB,也就是快照,快照就是一次全量的備份,會把全部redis的內存數據進行二進制的序列化存儲到磁盤。另外一種是aof日記,aof日誌記錄的是數據操做修改的指令記錄日誌,能夠類比mysql的binlog,aof日期隨着時間的推移只會無限增量。
在對redis進行恢復時,rdb快照直接讀取磁盤既能夠恢復,而aof須要對全部的操做指令進行重放進行恢復,這個過程有可能很是漫長。
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是redis操做指令的日誌存儲,類同於爲mysql的binlog,假設AOF從redis建立以來就一直執行,那麼AOF就記錄了全部的redis指令的記錄,若是要恢復redis,能夠對AOF進行指令重放,即可修復整個redis實例,不過AOF日誌也有兩個比較大的問題,一個是AOF的日誌會隨着時間遞增,若是一個數據量大運行的時間久,AOF日誌量將變得異常龐大,另外一個問題是AOF在作數據恢復時,因爲重放的量很是龐大,恢復的時間將會很是的長。
AOF寫操做是在redis處理完業務邏輯以後,按照必定的策略纔會進行些aof日誌存盤,這點跟mysql的redolog和binlog有很大的不一樣,其實也由於此緣由,redis由於處理邏輯在前而記錄操做日誌在後,也是致使redis沒法進行回滾的一個緣由。
針對上述的問題,redis在2.4以後也使用了bgrewriteaof對aof日誌進行瘦身,bgrewriteaof 命令用於異步執行一個AOF文件重寫操做。重寫會建立一個當前AOF文件的體積優化版本。
在對redis進行恢復的時候,若是咱們採用了RDB的方式,由於bgsave的策略,可能會致使咱們丟失大量的數據。若是咱們採用了AOF的模式,經過AOF操做日誌重放恢復,重放AOF日誌比RDB要長久不少。
redis4.0以後,爲了解決這個問題,引入了新的持久化模式,混合持久化,將rdb的文件和局部增量的AOF文件相結合,rdb可使用相隔較長的時間保存策略,aof不須要是全量日誌,只須要保存前一次rdb存儲開始到這段時間增量aof日誌便可,通常來講,這個日誌量是很是小的。
redis跟其餘傳統數據庫不一樣,redis是一個純內存的數據庫,而且存儲了都是一些數據結構的數據,若是不對內存加以控制的話,redis極可能會由於數據量過大致使系統的奔潰
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
複製代碼
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的結構的設計簡單總結起來,是一個空間和時間的折中方案:
因此,結合ziplist和雙向鏈表的優勢,quciklist就孕育而生。
redis在本身的對象系統中構建了一個引用計數方法,經過這個方法程序能夠跟蹤對象的引用計數信息,除了能夠在適當的時候進行釋放對象,還能夠用來做爲對象共享。 舉個例子,假使健A建立了一個整數值100的字符串做爲值對象,這個時候鍵B也建立保存一樣整數值100的字符串對象做爲值對象,那麼在redis的操做時:
假使,咱們的數據庫中指向整數值100的鍵不止鍵A和鍵B,而是有幾百個,那麼redis服務器中只須要一個字符串對象的內存就能夠保存本來須要幾百個字符串對象的內存才能保存的數據。
幾個定義
在redis2.8以後,使用psync命令代替sync命令來執行復制的同步操做,psync命令具備完整重同步和部分重同步兩種模式:
完整重同步:
若是網絡的抖動或者是短期的斷鏈也須要進行完整同步就會致使大量的開銷,這些開銷包括了,bgsave的時間,rdb文件傳輸的時間,slave從新加載rdb時間,若是slave有aof,還會致使aof重寫。這些都是大量的開銷因此在redis2.8以後也實現了部分重同步的機制。
部分重同步:
當一個鍵處於過時的狀態,其實在redis中這個內存並非實時就被從內存中進行摘除,而是redis經過必定的機制去把一些處於過時鍵進行移除,進而達到內存的釋放,那麼當一個鍵處於過時,redis會在何時去刪除?幾時被刪除存在三種可能性,這三種可能性也表明了redis的三種不一樣的刪除策略。
設置鍵的過時時間,建立定時器,一旦過時時間來臨,就當即對鍵進行操做操做,這種對內存是友好的,可是對cpu的時間是最不友好的,特別是在業務繁忙,過時鍵不少的時候,刪除過時鍵這個操做就會佔據很大一部分cpu的時間,要知道redis是單線程操做,在內存不緊張而cpu緊張的時候,將cpu的時間浪費在與業務無關的刪除過時鍵上面,會對redis的服務器的響應時間和吞吐量形成影響。 另外,建立一個定時器須要用到redis服務器中的時間事件,而當親時間事件的實現方式是無序鏈表,時間複雜度爲O(n),讓服務器大量建立定時器去實現定時刪除策略,會產生較大的性能影響,因此,定時刪除並非一種好的刪除策略。
與定時刪除相反,惰性刪除策略對cpu來講是最友好的,程序只有在取出鍵的時候纔會進行檢查,是一種被動的過程。與此同時,惰性刪除對內存來講又是最不友好的,一個鍵過時,只要再也不被取出,這個過時鍵就不會被刪除,它佔用的內存也不會被釋放。 很明顯,惰性刪除也不是一個很好的策略,redis是很是依賴內存和驍好內存的,若是一些長期鍵長期沒有被訪問,就會形成大量的內存垃圾,甚至會操成內存的泄漏。
在對執行數據寫入時,經過expireIfNeeded函數對寫入的key進行過時判斷,其中expireIfNeeded在內部作了三件事情,分別是:
上面兩種刪除策略,不管是定時刪除和惰性刪除,這兩種刪除方式在單一的使用上都存在明顯的缺陷,要麼佔用太多cpu時間,要麼浪費太多內存。按期刪除策略是前兩種策略的一個整合和折中
能夠說redis可謂博大精深,簡單的七連問或者只是盲人摸象,或者此次只是摸到了一根象鼻子,或者還應該順着鼻子向下摸,下次可能摸到了一隻象耳朵,只要願意往下深刻去了解去摸索,而不僅應用不思考,總有一天會把redis這種大象給摸透了。
[ 注:部分章節參考和引用黃健宏 《Redis設計與實現》]