redis 保存 array list 区别_为什么Redis的RDB备份不用多线程实现CopyOnWrite?
前言
這篇文章源于我昨天看到的一個有意思的問題。
快照持久化是個很耗時間的操作,而Redis采用fork一個子進程出來進行持久化。理論而言,fork出來的子進程會拷貝父進程所有的數據,這樣當Redis要持久化2G的內存數據的時候,子進程也會占據幾乎2G的內存。那么此時Redis相關的進程內存占用就會達到4G左右。這在數據體量比較小的時候還不嚴重,但是比如你的電腦內存是8G,目前備份快照數據本身體積是5G,那么按照上面的計算備份一定是無法進行的。所幸在Unix類操作系統上面做了如下的優化:在剛開始的時候父子進程共享相同的內存,直到父進程或者子進程進行內存的寫入后,對被寫入的內存共享才結束。這樣就會減少快照持久化時對內存的消耗。這就是COW技術,減少了快照生成時候的內存使用的同時節省了不少時間。而備份期間多用的內存正比于在此期間接收到的數據更改請求數目。
更具體地講,我們知道每個進程的虛擬空間是被劃分成正文段,數據段,堆,棧這四個部分,同時對應于每一個部分,操作系統會為之分配真實物理塊。當我們從父進程P1中fork出一個子進程P2時:
- 在沒有CopyOnWrite之前,我們要給子進程生成虛擬空間,并為虛擬空間地每一個部分分配對應地物理空間,接著要把父進程對應部分地物理空間地內容復制到子進程的空間中。這實際上是個既耗時又耗費空間地操作。
- 有了COW之后, fork子進程時,我們只為其生成虛擬空間,但是并不先為每個部分分配真實的物理空間,而是讓每個虛擬空間部分仍然指向父進程的物理空間。只有當父進程或子進程修改相應的共享內存空間時,才會為子進程分配物理空間并把父進程的物理空間內容進行復制。這就是所謂的寫時復制,即把內存的復制延遲到了內存寫入的時刻。
同時需要注意地是,父子進程共享的空間粒度是頁(在Linux中,頁的大小為4KB),父/子進程修改某個頁時,該頁的共享才結束,同時子進程分配該頁大小的物理空間復制父進程對應頁的內容。這樣,如果當子進程運行期間,父子進程都沒有修改數據,那么操作系統就節省了大量的內存復制時間和占用空間。
上面講的CopyOnWrite是操作系統在fork子進程時實現的。而題主問的是,我們能不能用多線程來實現COW進而來實現RDB生成呢?在回答這個問題之前,為了讓大家更明白多線程實現COW的事情,我們先以Java中的CopyOnWriteArrayList為例進行來看多線程實現COW是個什么操作。
首先我們看這么一段代碼。這段代碼在多線程下肯定是不安全的,為了讓它變得更安全,一個簡單的方法就是讀取和寫入時都加鎖,即同時要有讀鎖和寫鎖。但是我們都知道鎖是非常影響性能的,為了減少鎖的消耗,Java便推出了CopyOnWriteArrayList。
publicCopyOnWriteArrayList 相對于 ArrayList 線程安全,底層通過復制數組的方式來實現,其核心概念就是: 數據讀取時直接讀取,不需要鎖,數據寫入時,需要鎖,且對副本進行操作。那么當數據的操作以讀取為主時,我們便可以省去大量的讀鎖帶來的消耗。同時為了能讓多線程操作List時,一個線程的修改能被另一個線程立馬發現,CopyOnWriteList采用了Volatile關鍵詞來進行修飾,即每次數據讀取不從緩存里面讀取,而是直接從數據的內存地址中讀取。
我們以CopyOnWriteArrayList 的add()操作為例來看。
// 這個數組是核心的,因為用volatile修飾了總結而言,多線程實現COW實際上就是以空間換取時間使得數據讀取時不需要鎖。只是減少了讀鎖的開銷,但與常規的多線程操作共享數據的本質沒有什么區別。
好,最后我們回到題主的問題,使用多線程實現COW來實現RDB生成這個問題可以規約成使用多線程實現RDB生成問題。所以我們的問題核心在于解決能不能使用多線程來實現RDB生成。如果要這么做我們需要做出哪些額外的操作?
大家肯定會想RDB的生成過程本質不就是把內存中的數據序列化到硬盤文件中么?RDB生成時,子線程只需要進行數據讀取,主線程修改時加鎖修改。并且為了避免常規操作時鎖的過多開銷,我們可以只需要在RDB生成期間再加鎖,常規期間寫操作不需要加鎖。這樣總體而言帶來的開銷不會多很多,因為畢竟RDB生成是個低頻的操作。
但這里面其實有個很重要的概念就是”SnapShot“, 即RDB是Redis內存的某一個時刻的快照。比如,我6:15分開始生成RDB, 那么這個RDB保存的數據就是當時那一刻整個Redis內存中的數據狀態。使用多進程我們是很容易保證這一點的,但是使用多線程,我們是很難保證這個性質的。因為你可能在DUMP的過程中,主線程又修改了你還沒讀取的數據,又或者主線程修改了你剛剛已經序列化到文件中的某個數據。也就是說使用多線程進行生成RDB的時候,你并不知道自己生成的數據是到底哪個時刻的數據。你也并不知道修改期間哪些主線程的命令已經體現在了RDB文件中。
這個會產生大的影響么?單機版的Redis也許不大會,但是Redis集群中涉及到主從復制的時候就會產生很大的影響。
單機版Redis生成RDB無非就是想留個檔,那么具體RDB是哪一個時刻的,可能沒那么重要。更重要的是要生成RDB。而且這個RDB顯然越新越好,因為越新,Redis重啟后丟失的數據就越少。那么從這個角度而言,甚至說用多線程反而可能更好,因為多線程時可以讓一些生成RDB期間被修改的數據也體現在RDB中。
但是涉及到主從復制時就不可以了。主從復制時,Redis主節點會生成當時時刻的內存快照RDB文件,同時把RDB期間的所有的命令寫到緩存repl_backlog中,等從節點從主節點的RDB文件恢復數據之后,便從主節點的命令緩存中讀取所有的命令再進行執行一遍,以達到和主節點相同的狀態。那么用多線程生成RDB時,如果當主線程執行某個寫入命令時,從線程還未DUMP該數據,那么從線程生成的RDB就包含了該命令的執行結果。而子節點又恢復了數據之后,相當于子節點已經執行過了這個命令。那么當子節點從主節點的命令緩存中拉取命令來再執行一遍后,有些命令就會被重復執行。
看完覺得對你有幫助的話,那就記得關注我的專欄!
一畝三分地?zhuanlan.zhihu.com總結
以上是生活随笔為你收集整理的redis 保存 array list 区别_为什么Redis的RDB备份不用多线程实现CopyOnWrite?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 招商银行信用卡年费300怎么消除 招商银
- 下一篇: 飞利浦新款睡眠耳机 N7808 发布,佩