發展歷程
1.0階段
常見文件存儲有兩種方式:磁盤,內存。
涉及到兩種常見問題:尋址,帶寬。
磁盤尋址在毫秒(ms)級,帶寬 xx G
內存尋址在納秒(ns)級,帶寬:很大
磁盤與內存,在尋址方面,慢了10w倍
磁盤有磁道,扇區。一個扇區512byte,容量很小,帶來一個問題,當文件比較大的時候,需要不停的訪問扇區,不停尋址,對應的文件索引也會非常龐大。
內存存儲,數據沒法持久化。
4k對齊:操作系統,無論讀取多大的文件,一次I/O,最少4k大小。也可以設置爲大於4k。
2.0階段
關係型數據庫出現:
數據庫將數據最小的存儲單元稱爲page(oracle爲block),一般爲8k,設置爲4k的整數倍,目的爲了滿足操作系統一次I/O大小。
在數據庫中,可以把數據文件,看做是分拆到 從0-N個page中。
索引:在正常的數據data page之後,再新開一個空間,其中的每個page存儲索引字段對應的信息,以及索引它所關聯的正常數據的data page信息。
當一個查詢條件,通過B/tree索引信息,命中某一個索引,如page1,則可以順勢找到元數據data page2,加載data page2進入內存。
當數據量非常大的時候,數據的贈刪改性能會降低,查詢性能方面,如果只有少量次查詢,查詢性能不會降低很明顯,但是如果高併發查詢時,受限於I/O的讀取容量,會造成大量的page讀取排隊堵塞,性能會下降的很明顯。我們希望速度能夠更快一些。
3.0階段
緩存:memcached,redis
Redis
結構圖:
client與Redis建立連接後,通過epoll多路複用實現單進程高吞吐,
二進制安全
存儲同樣的一個value:中,當shell使用utf-8時,Redis存儲的是3個字節,當shell編碼改成GBK之後,再次存儲”中“,Redis存儲的是2個字節。
Redis爲了二進制安全,以字節流讀取數據,而非字符流。
事務
如圖所示,client1和client2都連接了一個Redis實例。client1和client2都進行事務操作,client1進行更新key,client2進行查詢key後再進行刪除key。
我們都知道Redis對於業務處理這塊,是單進程的,那現在有兩個事務,Redis是如何實現的呢?
一個事務從開始到執行,有下面3個步驟:1.開始事務,2.命令入隊,3.執行事務。
從Redis接到第一個事務開啓命令MULTI,會對每一個事務維護一個命令的緩存隊列,直到Exec執行命令結束:
所以兩個事務,最終的執行順序,看哪個事務的exec命令先行到達。
注意點: Redis的事務,與關係型數據庫的事務有些不一樣。數據庫的事務是原子操作,多個命令如果有任何一個失敗,都會進行回滾。Redis的事務,當執行到某一行之後,如果發生的錯誤,已經執行的命令不會進行回滾,同時,後續的命令依舊會往下執行,所以Redis的事務並不是原子性操作,可以理解爲將一堆命令進行打包執行。
過期淘汰
主要有兩種過期淘汰機制:被動式,主動式
- 被動式(懶漢式):當客戶端發起一個key的請求時,Redis會檢測當前這個key是否已經過期。這種有一個很明顯的弊端,如果一個key幾十年沒有客戶端進行訪問,那這個key將會一直存在幾十年而不被清理。
- 主動式:Redis每秒10次執行:
a.隨機取20個key進行過期檢測
b.刪除已經過期的key
c.如果20個key中過期的大於25%,重複執行上述過程 - AOF文件中處理過期
爲了獲取正確的行爲而不犧牲一致性,當一個key過期時,DEL命令會被寫入AOF發給salves
持久化
Redis有兩種持久化方式:
- RDB(全量數據)
- AOF(增量數據)
RDB:
很好理解,RDB方式,即爲將當前Redis緩存中的所有數據,全部寫入磁盤。問題來了,該怎麼寫入呢?
引入一個問題:假設當前時間節點是早上10:00,Redis中有記錄a=3,b=4,現在需求是需要將這條進行持久化。
- 方案一:Redis主線程堵塞,拒絕外部一切響應,等將數據寫入磁盤後,再接收外部請求。
- 方案二:Redis主線不阻塞,繼續響應外部的讀/寫請求,新開一個子線程負責將數據寫入磁盤
從響應響應速度與吞吐量上考慮,肯定是優先第二種方法,但是如果選擇方案二,會帶來一個問題,如果子線程寫入磁盤需要30min,而主線程在這30min內,會key=a,和key=b的數據,分別進行了5次,8次更新,那麼在這30min內,當子線程真正開始持久化的時候,此時拿到的value,可以是這5次/8次更新中的任意一次的值。即:可能key=a的value保存的是第二次改動的結果,時間10:17,key=b保存的是第7次改動的結果,時間10:26。此時的RDB文件並非10:00時的數據。
那麼Redis是怎麼實現RDB的呢?
首先需要引入幾個概念:
- 虛擬內存地址
- 物理內存地址
- fork
- copy on write
我們都知道每個線程都有各自的線程空間,以Linux/Uinx系統爲例,export指令,可以讓父進程的數據對子進程可見,也即子線程持有父進程對象的引用在各個的線程空間,每個線程有自己的虛擬內存地址,通過os的地址映射到物理內存地址:
當Redis通過fork方式出來的子線程,便擁有了在某一個時間段,主進程持有數據的完整備份。你肯定要問了,主線程仍在響應客戶端讀寫請求,如果某一份數據,在還沒有寫入磁盤之前,主線程就將數據進行了修改,那子線程通過內存映射後獲取到的值,是否會是修改之後的數據呢?這個時候copy on write便排上用場了。顧名思義,寫的時候,進行復制,即當需要修改數據時,將數據進行復制。以上圖爲例,當主線程在某一時刻,需要將物理內存爲8的數據改成10,最終的地址映射關係將如下:
對應於Redis命令有兩種:
- save,阻塞,對應於方案一
- bgsave,非阻塞,對應於方法二
同時,Redis.conf 配置項中的save,實際對應於bgsave命令,接收兩個參數:時間間隔、變更行數
AOF
會將Redis的寫操作日誌記錄到文件中,append only,日誌不停進行追加。
下面設想一下這樣一個問題:
現在有一個小明,非常無聊,在接下來的一年內,不停的往Redis中執行這樣的操作,新增key1,刪除key1,再新增key1,再刪除key1,如此反覆。ok,現在有一個問題,一年之後,這個日誌文件將會有多大?後續將這個文件中每一條指令取出進行數據恢復,最終恢復完成,將需要花費多長時間?
從上面這個例子可以看出這種日誌持久化,非常明顯的優缺點:
優點:能最大限度的保留住每一次數據的變化,數據的完整性好
缺點:文件在不停的append,會導致文件不停膨脹,後續恢復效率很低。
Redis如何解決這個問題呢?
很明顯,需要解決兩個問題:1.日誌文件過大,2.指令重複無意義,如上面的那個例子,一年下來,實際數據,要麼新增一個key,要麼什麼數據都沒有。
Redis AOF重寫:
假設現有如下操作:
1.set key1 xx
2.set key2 xx
3.set key3 xx
…
10.set key10 xx
執行10次操作,往Redis中添加10條記錄,那麼AOF文件中級存在10條指令,此時Redis中存在10條數據。
當執行AOF重寫後,會直接讀取前Redis中的數據,即最終指令爲:set key1 xx key2 xx key3xx … key10 xx,將10條指令最終合併爲一條。
執行方式:
- 手動執行
bgrewriteaof
- 自動執行
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage
問題:
如果當前指令重寫,以fork子線程方式執行,那麼,如果在子線程執行重寫期間,父線程將某一條數據進行修改了,那麼子線程重寫完畢之後,這條數據的改修指令將不會被記錄進新的AOF文件。
爲解決數據狀態不一致的問題,在父線程fork出子線程之後,會維護一個指令緩衝區,在此期間,父線程繼續響應外部請求,同時會將這段時間內的日誌寫入緩衝區,當子線程重寫完成之後,父線程再將緩衝區內的指令,append進新的AOF文件。
AOF記錄頻次:
- no :使用os 的讀寫緩衝區,當緩衝區寫滿數據後,append 進AOF文件
- second(默認):每秒寫入
- always:每次改動都會寫入AOF
RDB,AOF混寫
Redis 4.0版本開始,支持RDB+AOF混寫模式,即將部分老數據,以RDB形式保存下來,後續指令再以指令形式寫入AOF。
AOF重寫命令執行時,會將當前數據保存爲RDB,期間寫指令進緩衝區,最終緩衝區寫入AOF。注意最終混寫生成的是一個AOF文件,文件裏面,前半部分是rdb的二進制數據,以“redis”字符起頭,文件後半部分是後續緩衝區裏的指令。重寫完成後,後續寫指令繼續append進該AOF文件。
優點:RDB恢復數據會很快,在很快恢復大量數據基礎上,只需要對少了增量數據進行指令修改數據即可,極大提升數據恢復效率。