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