1. 程式人生 > >redis的幾個知識點

redis的幾個知識點

Redis的全稱是Remote Dictionary Server,即遠端字典服務,通常用作伺服器快取服務。

這裡通過Redis的幾個知識點來了解Redis。

Redis的通訊協議

Redis的通訊協議是文字協議,是的,Redis伺服器與客戶端通過RESP(Redis Serialization Protocol、Redis序列化協議)進行通訊。

雖然文字協議會浪費流量,不過它的優點在於直觀,非常得簡單,解析效能極其好,所以我們不需要一個特殊的Redis客戶端,僅靠Telnet或者是文字流就可以跟Redis進行通訊。

客戶端的命令格式

1.簡單字串Simple Strings,以加號【+】開頭。

2.錯誤Errors,以減號【-】開頭。

3.整數型Integer,以冒號【:】開頭。

4.大字串型別Bulk Strings,以美元符號【$】開頭。

5.陣列型別Arrays,以星號【*】開頭。

因為通訊協議是文字協議這一特性,一個簡單的文字流就可以是Redis的客戶端。

public static void set() throws UnknownHostException, IOException {
    Socket socket = new Socket("127.0.0.1", 8080);
    OutputStream os = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    // set hello
    os.write("*3\r\n".getBytes());
    os.write("$3\r\n".getBytes());
    os.write("set\r\n".getBytes());
    os.write("$5\r\n".getBytes());
    os.write("hello\r\n".getBytes());
    int num = 0;
    char ch;
    while ((num = br.read()) != -1) {
        ch = (char)num;
        System.out.println(ch);
    }
    socket.close();
}

這裡是簡單的總結,具體可以參考官方文件:https://redis.io/topics/protocol。

我的理解就是,Redis採用RESP文字協議最重要的三點:簡單的實現、快速的解析和直觀的理解。雖然文字協議可能會造成一定程度的流量浪費,但是在效能上和操作上卻快速簡單,這中間也是有一個權衡和協調的過程。

Reids的事務

Redis是有事務的,並且內建了一些事務相關的命令。

命令 描述
exec 執行所有事務塊內的命令。
watch 監視一個(或多個)key,如果在事務執行之前這個(或這些)key被其他命令所改動,那麼事務將被打斷。
discard 取消事務,放棄執行事務塊內的所有命令。
unwatch 取消watch命令對所有key的監視。
multi 標記一個事務塊的開始。

我們知道,傳統的資料庫的事務一般具有四個特性,即ACID(原子性、一致性、隔離性和永續性),然後我們可以使用上面的Redis事務相關命令來檢驗Redis是否都具備了這事務的四個特性。

原子性

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

1.事務操作佇列

Redis一旦開始執行事務開始命令multi之後,就會為這個事務生成一個佇列,每次操作的命令都會按照順序插入到這個佇列中。這個佇列中的操作命令不會被馬上執行,直到執行exec命令提交事務的時候,佇列裡面的所有操作命令才會被一次性且排他地執行。

從上面的例子中可以看出,當執行一個成功的事務,事務裡面的命令都是按照佇列裡面按順序並且排他地執行。

2.Reids不支援回滾

原子性的特性是要麼全部成功,要麼全部失敗,一旦事務中有一個操作失敗了,那麼之前成功的操作全部會回滾。

我們執行一個失敗的事務,就會發現Redis並不會回滾之前成功的操作,因此從嚴格意義上來說Redis並不具備原子性。

這和Redis的定位和設計有關係。

對比MySQL,MySQL支援回滾是因為MySQL有完整的Redo Log,並且是在事務進行Commit之前就會寫完Redo Log。

但是要知道MySQL為了能夠進行回滾花了不少的效能代價。

而Redis是完成操作之後才會進行AOF日誌記錄,而AOF日誌的定位只是記錄操作的指令記錄。實際上,Redis應用的場景更多的是對抗高併發的高效能,因此Redis選擇了更為簡單,更快速無回滾的方式處理事務也是符合場景的。

一致性

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

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

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

假設事務中有兩個操作,一個是將楊楊的餘額增加100,靜靜的餘額減少100,這兩個操作完成一個【靜靜給楊楊轉賬100】的事務。那麼,當操作一成功了,而操作二失敗了,成功的操作一併不會回滾,這就導致楊楊的餘額增加了100,而靜靜的餘額卻沒有減少100,資料也就不一致了。因此Redis對執行錯誤並沒有確保一致性的機制,可以說Redis並沒有支援事務的一致性。

2.宕機對一致性的影響

暫不考慮分散式高可用的Redis解決方案,先從單機看宕機恢復是否能滿足資料完成性約束。因為Redis支援將資料持久化到磁碟,宕機後可以從磁碟恢復資料。然而無論是RDB還是AOF方案去恢復資料,都只能恢復到宕機前的操作,就像前面的執行錯誤一樣,並不會將宕機前屬於同一個事務中成功的操作回滾。正是因為Redis並不支援回滾,也就不具備傳統意義上的原子性,因此也不具備傳統意義的一致性。

隔離性

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

Redis因為是單執行緒操作,所以在隔離性上有天生的隔離機制,當Redis執行事務的時候,Redis的服務端保證在執行事務期間不會對事務進行中斷,所以Redis事務總是以序列的方式執行,事務也具備隔離性。

永續性

事務的永續性是指,當一個事務執行完畢,執行這個事務所得到的結果會被永久地儲存在持久化的磁碟儲存中,即使伺服器在事務執行完畢之後宕機了,執行的事務的結果也不會丟失。而Redis是否具備持久化,取決於Redis的持久化模式(三種執行模式):

1.純粹的記憶體執行,不具備持久化功能。這樣的話,一旦服務宕機,所有資料都會丟失(記憶體中的資料會因為伺服器停止執行而全部清空)。

2.RDB模式執行。這一模式取決於RDB策略,只有在滿足策略的情況下才會執行Bgsave,非同步執行並不能保證Redis具備持久化。

3.AOF模式執行。這一模式下只有將appendfsync屬性設定為always,程式才會在執行命令的時候將資料同步儲存到磁碟。

Redis的持久化

Redis有兩種持久化機制,一個是RDB,也就是快照,快照就是一次全量的備份,會把所有Reids的記憶體資料進行二進位制的序列化儲存到磁碟;另一種是AOF日誌,AOF日誌記錄的是資料庫操作修改的指令記錄日誌,可以類比MySQL的BinLog,AOF日誌隨著時間的推移只會無限增量。

在對Redis進行恢復的時候,RDB快照直接讀取磁碟即可恢復,而AOF需要對所有的操作指令進行重放和恢復,這個過程可能非常漫長。

1.RDB持久化機制

Redis在進行RDB的快照生成有兩種方法,一種是save,一種是bgsave。由於Redis是單程序單執行緒,如果直接使用save的話,會進行一個龐大的檔案IO操作阻塞線上的業務,因此一般不會直接採用save而是採用bgsave。在使用basave的時候,Redis會fork一個子程序,快照的持久化就交給這個子程序去處理,而父程序繼續處理線上業務的請求。

fork機制是linux作業系統的一個程序機制,當父程序fork出來一個子程序,子程序和父程序擁有共同的記憶體資料結構,子程序剛剛產生時,它和父程序共享記憶體裡面的程式碼段和資料段。

一開始兩個程序都具備了相同的記憶體段,子程序在做資料持久化的時候,不會去修改現在的記憶體資料,而是會採用COW(Copy on Write)的方式將資料段頁面進行分離。當父程序修改了某一個數據段時,被共享的頁面就會複製一份分離出來,然後父程序再在新的資料端進行修改。這個過程也稱為分裂的過程,本來父子程序都指向很多相同的記憶體塊,但是如果父程序對其中某個記憶體塊進行修改,就會將其複製出來,進行分裂再在新的記憶體塊上進行修改。因為子程序在fork的時候就可以固定記憶體,這個時間點的資料將不會產生變化。所以我們可以安心地產生快照而不用擔心快照的內容受到父程序業務請求的影響。

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無法回滾。

另外,Redis在2.4的版本之後使用了bgrewriteaof對AOF日誌進行瘦身。bgrewriteaof命令用於非同步只能夠一個AOF檔案重寫操作,重寫會建立一個當前AOF檔案的體積優化版本。

3.RDB和AOF混合搭配持久化模式

在對Redis進行恢復的時候,如果我們採用了RDB機制,bgsave的策略可能會導致我們丟失大量的資料;如果我們採用AOF機制,通過AOF操作日誌重放恢復,重放AOF日誌恢復資料花費的時間比RDB要長久很多。

在Redis4.0的版本之後,為了解決這個問題,便引入了新的持久化模式,即混合持久化,將RDB的全量檔案和區域性增量的AOF檔案相結合。這樣,RDB就可以使用相隔時間較長的儲存策略,AOF也不需要是全量日誌,只需要儲存前一次RDB儲存開始到這段時間增量的AOF日誌即可,極大地減小了AOF的日誌量。

Redis在記憶體使用上的優化

Redis和其他傳統的資料庫不同,是一個純記憶體的資料庫,並且儲存的都是一些特定資料結構的資料,如果不對記憶體加以控制的話,Redis很可能會因為資料量過大而導致系統奔潰。

1.Ziplist

當最開始嘗試開啟一個小資料量的Hash結構和一個Set結構時,發現它們在Redis裡面的真正結構是一個Ziplist。Ziplist是一個緊湊的資料結構,每一個元素之間都是連續的記憶體,如果在Redis中Redis啟用的資料結構資料量很小時,Redis就會切換到使用緊湊儲存的形式來進行壓縮儲存。

例如在上面的例子中,我們採用了Hash結構進行儲存,Hash結構是一個二維的結構,是一個典型的用空間換取時間的結構。但是如果使用的資料量很小,使用二維結構反而浪費空間,在時間上的效能也並沒有得到太大的提升,還不如直接使用一維結構進行儲存。在查詢的時候,雖然複雜度是O(n),但是因為資料量少,遍歷也很快,甚至比Hash結構本身的查詢速度還快。如果當集合物件的元素不斷增加,或某個value的值過大,這種小物件儲存也會升級成標準的結構。

Redis可以在配置中進行緊湊結構和標準結構的轉換引數:

2.Quicklist

Quicklist資料結構是Redis在3.2才引入的一個雙向連結串列的資料結構,確實來說是一個Ziplist的雙向連結串列。Quicklist的每一個數據節點都是一個Ziplist,而Ziplist本身就是一個緊湊列表。假如Quicklist包含了5個Ziplist的節點,每個Ziplist列表又包含了5個數據,那麼在外部看來,這個Quicklist就包含了25個數據項。

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

雙向連結串列可以在兩端進行Push和Pop操作,但是它在每一個節點除了儲存自身的資料外,還要儲存兩個指標,增加額外的記憶體開銷。

其次是由於每個節點都是獨立的,在記憶體地址上並不連續,節點多了容易產生記憶體碎片。

另外,Ziplist本身是一塊連續的記憶體,儲存和查詢的效率很高。但是它不利於修改操作,每次的資料變動都會引發記憶體的Realloc。如果Ziplist的長度很長的話,一次Realloc會導致大批量的資料拷貝。

所以,結合Ziplist和雙向連結串列的優點,Quicklist就應運而生。

3.記憶體共享

Redis在自己的物件系統中構建了一個引用計數的方法,通過這個方法,程式可以跟蹤物件的引用計數資訊,除了可以在適當的時候進行物件釋放之外,還可以用來作為物件共享。

舉個例子,假如鍵A建立了一個整數值100的字串作為值物件,這個時候鍵B也建立儲存同樣整數值100的字串物件作為值物件。那麼在Reids操作的時候,會將資料庫鍵的指標指向一個現有的值的物件,並將共享的值物件的引用計數加1。這時候,即使資料庫中指向整數值100的鍵不止鍵A和鍵B,而是有幾百個的話,Redis也只需要一個字串物件的記憶體就可以儲存原本需要幾百個字串物件才能儲存的資料。

Redis的過期刪除策略

當一個鍵處於過期的狀態,其實在Redis中這個記憶體並不是實時就被從記憶體中摘除的,而是Redis會通過一定的機制去把一些處於過期的鍵進行移除,進而達到記憶體的釋放。

處於過期狀態的鍵被刪除的時間存在三種可能性,這三種可能性也就代表了Redis的三種不同的刪除策略:

1.定時刪除。定時刪除是在設定鍵過期時間的同時,建立一個定時器,讓定時器在鍵過期時間來臨的時候立即執行對鍵的刪除操作。

2.惰性刪除。惰性刪除是放任鍵過期不管,之後在每次從鍵空間獲取鍵的時候再去檢查該鍵是否過期,如果過期的話就刪除該鍵。

3.定期刪除。每隔一段時間,程式都要對資料庫進行一次檢查,刪除裡面的過期鍵,至於要刪除多少過期鍵,由演算法來決定。

定時刪除

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

定時刪除對記憶體是友好的,但是對CPU的時間是最不友好的,特別是在業務繁忙,過期鍵很多的時候,刪除過期鍵這個操作會佔據很大一部分CPU的時間。要知道,Redis是單執行緒操作的,在記憶體不緊張而CPU緊張的時候,將CPU的時間浪費在與業務無關的刪除過期鍵的操作上面,會對Reids的伺服器的響應時間和吞吐量造成影響。

另外,建立一個定時器需要用到Redis伺服器中的時間事件,而當前時間事件的實現方式是無序連結串列,時間複雜度為O(n),讓伺服器大量建立定時器去實現定時刪除策略,會產生較大的效能影響。所以,定時刪除並不是一種好的策略。

惰性刪除

惰性刪除與定時刪除相反,對CPU來說是最友好的,程式只有在取出鍵的時候才會進行檢查,是一種被動的策略。與此同時,惰性刪除對記憶體來說又是不友好的,如果一個過期鍵不再被取出的話,那這個鍵因為不會被檢查,也就永遠不會被刪除,它佔用的記憶體也就永遠不會被釋放。

很明顯,惰性刪除也不是一個很好的策略,因為Redis是非常依賴記憶體空間和較好的記憶體的,如果一些長期鍵長期沒有被訪問,就會造成大量的記憶體垃圾,甚至造成記憶體洩露。

惰性刪除的實現是在對執行資料寫入的時候,通過expireIfNeeded函式對寫入的key進行過期判斷,其中expireIfNeeded在內部做了三件事情,分別是:

1.檢視key是否過期。

2.向slave節點傳播執行刪除過期key的動作。

3.刪除過期key。

定期刪除

上面兩種刪除策略,無論是定時刪除還是惰性刪除,都在單一使用上存在明顯的缺陷,要麼佔用太多的CPU時間,要麼浪費太多的記憶體。而定期刪除則是前兩種刪除策略的一個整合和折中。

定期刪除策略是每隔一段時間執行一次刪除過期鍵的操作,並通過限制刪除操作執行的時間和頻率來減少刪除操作對CPU時間的影響。通過設定合理的刪除執行的時長和頻率來達到合理刪除過期鍵的目的。

Redis的主從複製

先說下幾個定義:

1.runID:伺服器執行的ID。

2.offset:主伺服器的複製偏移量和從伺服器複製的偏移量。

3.Replication Baclog:主伺服器的複製積壓緩衝區。

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

完整同步

完整同步用於處理初次複製情況,完整重同步的執行步驟與sync命令的執行步驟一直,都是通過讓主伺服器建立併發送RDB檔案,以及向從伺服器傳送儲存在緩衝區的寫命令來進行同步。

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的時間。如果salve有AOF(Append Only File),還會導致AOF重寫。

部分重同步

部分重同步是用於處理斷線後重新連線主伺服器時,主伺服器可以將主從伺服器連線斷開期間執行的寫命令傳送給從伺服器,從伺服器只要接受並執行這些寫命令,就可以將資料庫更新至主伺服器當前所處的狀態。

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獲取資料更新自身資料。

 

"總不能用一身的刺去擁抱無辜的人,認識就夠了,何必談餘生