这篇Redis文章,图灵看了都说好
作者:lunnzhang,騰訊 CDG 后臺開發(fā)工程師。
2007 年,他和朋友一起創(chuàng)建了一個網(wǎng)站。為了解決這個網(wǎng)站的負(fù)載問題,他自己定制了一個數(shù)據(jù)庫。2009 年開發(fā)的,這個是 Redis。這位意大利程序員是薩爾瓦托勒·桑菲利波(Salvatore Sanfilippo),他被稱為Redis之父,更廣為人知的名字是Antirez。
一、Redis簡介
REmote DIctionary Server(Redis) 是一個開源的使用 ANSI C 語言編寫、遵守 BSD 協(xié)議、支持網(wǎng)絡(luò)、可基于內(nèi)存、分布式、可選持久性的鍵值對(Key-Value)存儲數(shù)據(jù)庫,并提供多種語言的 API。
Redis 通常被稱為數(shù)據(jù)結(jié)構(gòu)服務(wù)器,因為值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等類型。
二、內(nèi)存模型
首先可以進(jìn)行Redis的內(nèi)存模型學(xué)習(xí),對Redis的使用有很大幫助,例如OOM時定位、內(nèi)存使用量評估等。
通過info memory命令查看內(nèi)存的使用情況。
主要參數(shù):
used_memory:從Redis角度使用了多少內(nèi)存,即Redis分配器分配的內(nèi)存總量(單位是字節(jié)),包括使用的虛擬內(nèi)存(即swap);
used_memory_rss:從操作系統(tǒng)角度實際使用量,即Redis進(jìn)程占據(jù)操作系統(tǒng)的內(nèi)存(單位是字節(jié)),包括進(jìn)程運(yùn)行本身需要的內(nèi)存、內(nèi)存碎片等,不包括虛擬內(nèi)存。一般情況下used_memory_rss都要比used_memory大,因為Redis頻繁刪除讀寫等操作使得內(nèi)存碎片較多,而虛擬內(nèi)存的使用一般是非極端情況下是不怎么使用的;
mem_fragmentation_ratio:即內(nèi)存碎片比率,該值是used_memory_rss / used_memory的比值;mem_fragmentation_ratio一般大于1,且該值越大,內(nèi)存碎片比例越大。如果mem_fragmentation_ratio<1,說明Redis使用了虛擬內(nèi)存,由于虛擬內(nèi)存的媒介是磁盤,比內(nèi)存速度要慢很多,當(dāng)這種情況出現(xiàn)時,應(yīng)該及時排查,如果內(nèi)存不足應(yīng)該及時處理,如增加Redis節(jié)點(diǎn)、增加Redis服務(wù)器的內(nèi)存、優(yōu)化應(yīng)用等。一般來說,mem_fragmentation_ratio在1.03左右是比較健康的狀態(tài)(對于jemalloc來說);
mem_allocator:即Redis使用的內(nèi)存分配器,一般默認(rèn)是jemalloc。
Redis內(nèi)存劃分
數(shù)據(jù)
作為數(shù)據(jù)庫,數(shù)據(jù)是最主要的部分,這部分占用的內(nèi)存會統(tǒng)計在used_memory中。
進(jìn)程本身運(yùn)行需要的內(nèi)存
這部分內(nèi)存不是由jemalloc分配,因此不會統(tǒng)計在used_memory中。
緩沖內(nèi)存
緩沖內(nèi)存包括:
客戶端緩沖區(qū):存儲客戶端連接的輸入輸出緩沖;
復(fù)制積壓緩沖區(qū):用于部分復(fù)制功能;
aof_buf:用于在進(jìn)行AOF重寫時,保存最近的寫入命令。
這部分內(nèi)存由jemalloc分配,因此會統(tǒng)計在used_memory中。
內(nèi)存碎片
內(nèi)存碎片是Redis在數(shù)據(jù)更改頻繁分配、回收物理內(nèi)存過程中產(chǎn)生的。
內(nèi)存碎片不會統(tǒng)計在used_memory中。
Redis數(shù)據(jù)存儲的細(xì)節(jié)
下面看一張經(jīng)典的圖。
jemalloc
無論是DictEntry對象,還是RedisObject、SDS對象,都需要內(nèi)存分配器(如jemalloc)分配內(nèi)存進(jìn)行存儲。Redis在編譯時便會指定內(nèi)存分配器;內(nèi)存分配器可以是 libc 、jemalloc或者tcmalloc,默認(rèn)是jemalloc。當(dāng)Redis存儲數(shù)據(jù)時,會選擇大小最合適的內(nèi)存塊進(jìn)行存儲。
jemalloc劃分的內(nèi)存單元如下圖所示:
dictEntry
每個dictEntry都保存著一個鍵值對,key值保存一個sds結(jié)構(gòu)體,value值保存一個redisObject結(jié)構(gòu)體。
redisObject
前面說到,Redis對象有5種類型;無論是哪種類型,Redis都不會直接存儲,而是通過RedisObject對象進(jìn)行存儲。
RedisObject的每個字段的含義和作用如下:
type
type字段表示對象的類型,占4個比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
encoding
encoding表示對象的內(nèi)部編碼,占4個比特(redis-3.0)。
lru
lru記錄的是對象最后一次被命令程序訪問的時間,占據(jù)的比特數(shù)不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。
refcount
refcount記錄的是該對象被引用的次數(shù),類型為整型。refcount的作用,主要在于對象的引用計數(shù)和內(nèi)存回收:
當(dāng)創(chuàng)建新對象時,refcount初始化為1;
當(dāng)有新程序使用該對象時,refcount加1;
當(dāng)對象不再被一個新程序使用時,refcount減1;
當(dāng)refcount變?yōu)?時,對象占用的內(nèi)存會被釋放。
Redis中被多次使用的對象(refcount>1)稱為共享對象。Redis為了節(jié)省內(nèi)存,當(dāng)有一些對象重復(fù)出現(xiàn)時,新的程序不會創(chuàng)建新的對象,而是仍然使用原來的對象。這個被重復(fù)使用的對象,就是共享對象。目前共享對象僅支持整數(shù)值的字符串對象。
共享對象的具體實現(xiàn)
Redis的共享對象目前只支持整數(shù)值的字符串對象。之所以如此,實際上是對內(nèi)存和CPU(時間)的平衡:共享對象雖然會降低內(nèi)存消耗,但是判斷兩個對象是否相等卻需要消耗額外的時間。
對于整數(shù)值,判斷操作復(fù)雜度為O(1);
對于普通字符串,判斷復(fù)雜度為O(n);
而對于哈希、列表、集合和有序集合,判斷的復(fù)雜度為O(n^2)。
雖然共享對象只能是整數(shù)值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。
就目前的實現(xiàn)來說,Redis服務(wù)器在初始化時,會創(chuàng)建10000個字符串對象,值分別是0~9999的整數(shù)值;當(dāng)Redis需要使用值為0~9999的字符串對象時,可以直接使用這些共享對象。10000這個數(shù)字可以通過調(diào)整參數(shù)REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進(jìn)行改變。
ptr
ptr指針指向具體的數(shù)據(jù),如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
SDS
Redis沒有直接使用C字符串(即以空字符‘\0’結(jié)尾的字符數(shù)組)作為默認(rèn)的字符串表示,而是使用了SDS。SDS是簡單動態(tài)字符串(Simple Dynamic String)的縮寫。
通過SDS的結(jié)構(gòu)可以看出,buf數(shù)組的長度=free+len+1(其中1表示字符串結(jié)尾的空字符);所以,一個SDS結(jié)構(gòu)占據(jù)的空間為:free所占長度+len所占長度+ buf數(shù)組的長度=4+4+free+len+1=free+len+9。
為什么使用SDS而不直接使用C字符串?
SDS在C字符串的基礎(chǔ)上加入了free和len字段,帶來了很多好處:
獲取字符串長度:SDS是O(1),C字符串是O(n)。
緩沖區(qū)溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內(nèi)存,很容易造成緩沖區(qū)的溢出;而SDS由于記錄了長度,相應(yīng)的API在可能造成緩沖區(qū)溢出時會自動重新分配內(nèi)存,杜絕了緩沖區(qū)溢出。
修改字符串時內(nèi)存的重分配:對于C字符串,如果要修改字符串,必須要重新分配內(nèi)存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內(nèi)存緩沖區(qū)溢出,字符串長度減小時會造成內(nèi)存泄露。而對于SDS,由于可以記錄len和free,因此解除了字符串長度和空間數(shù)組長度之間的關(guān)聯(lián),可以在此基礎(chǔ)上進(jìn)行優(yōu)化——空間預(yù)分配策略(即分配內(nèi)存時比實際需要的多)使得字符串長度增大時重新分配內(nèi)存的概率大大減小;惰性空間釋放策略使得字符串長度減小時重新分配內(nèi)存的概率大大減小。
存取二進(jìn)制數(shù)據(jù):SDS可以,C字符串不可以。因為C字符串以空字符作為字符串結(jié)束的標(biāo)識,而對于一些二進(jìn)制文件(如圖片等),內(nèi)容可能包括空字符串,因此C字符串無法正確存取;而SDS以字符串長度len來作為字符串結(jié)束標(biāo)識,因此沒有這個問題。
三、持久化Persistence
持久化的功能:Redis是內(nèi)存數(shù)據(jù)庫,數(shù)據(jù)都是存儲在內(nèi)存中,為了避免進(jìn)程退出導(dǎo)致數(shù)據(jù)的永久丟失,需要定期將Redis中的數(shù)據(jù)以某種形式(數(shù)據(jù)或命令)從內(nèi)存保存到硬盤。當(dāng)下次Redis重啟時,利用持久化文件實現(xiàn)數(shù)據(jù)恢復(fù)。除此之外,為了進(jìn)行災(zāi)難備份,可以將持久化文件拷貝到一個遠(yuǎn)程位置。
Redis持久化分為RDB持久化和AOF持久化,前者將當(dāng)前數(shù)據(jù)保存到硬盤,后者則是將每次執(zhí)行的寫命令保存到硬盤(類似于MySQL的Binlog)。由于AOF持久化的實時性更好,即當(dāng)進(jìn)程意外退出時丟失的數(shù)據(jù)更少,因此AOF是目前主流的持久化方式,不過RDB持久化仍然有其用武之地。
RDB持久化
RDB(Redis Database)持久化方式能夠在指定的時間間隔能對你的數(shù)據(jù)進(jìn)行快照存儲。一般通過bgsave命令會創(chuàng)建一個子進(jìn)程,由子進(jìn)程來負(fù)責(zé)創(chuàng)建RDB文件,父進(jìn)程(即Redis主進(jìn)程)則繼續(xù)處理請求。
圖片中的5個步驟所進(jìn)行的操作如下:
Redis父進(jìn)程首先判斷:當(dāng)前是否在執(zhí)行save,或bgsave/bgrewriteaof(后面會詳細(xì)介紹該命令)的子進(jìn)程,如果在執(zhí)行則bgsave命令直接返回。bgsave/bgrewriteaof 的子進(jìn)程不能同時執(zhí)行,主要是基于性能方面的考慮:兩個并發(fā)的子進(jìn)程同時執(zhí)行大量的磁盤寫操作,可能引起嚴(yán)重的性能問題。
父進(jìn)程執(zhí)行fork操作創(chuàng)建子進(jìn)程,這個過程中父進(jìn)程是阻塞的,Redis不能執(zhí)行來自客戶端的任何命令;
父進(jìn)程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父進(jìn)程,并可以響應(yīng)其他命令;
子進(jìn)程進(jìn)程對內(nèi)存數(shù)據(jù)生成快照文件;
子進(jìn)程發(fā)送信號給父進(jìn)程表示完成,父進(jìn)程更新統(tǒng)計信息。
這里補(bǔ)充一下第4點(diǎn)是如何生成RDB文件的。一定有讀者也有疑問:在同步到磁盤和持續(xù)寫入這個過程是如何處理數(shù)據(jù)不一致的情況呢?生成快照RDB文件時是否會對業(yè)務(wù)產(chǎn)生影響?
通過 fork 創(chuàng)建的子進(jìn)程能夠獲得和父進(jìn)程完全相同的內(nèi)存空間,父進(jìn)程對內(nèi)存的修改對于子進(jìn)程是不可見的,兩者不會相互影響;
通過 fork 創(chuàng)建子進(jìn)程時不會立刻觸發(fā)大量內(nèi)存的拷貝,采用的是寫時拷貝COW (Copy On Write)。內(nèi)核只為新生成的子進(jìn)程創(chuàng)建虛擬空間結(jié)構(gòu),它們來復(fù)制于父進(jìn)程的虛擬究竟結(jié)構(gòu),但是不為這些段分配物理內(nèi)存,它們共享父進(jìn)程的物理空間,當(dāng)父子進(jìn)程中有更改相應(yīng)段的行為發(fā)生時,再為子進(jìn)程相應(yīng)的段分配物理空間;
AOF持久化
AOF(Append Only File)持久化方式記錄每次對服務(wù)器寫的操作,當(dāng)服務(wù)器重啟的時候會重新執(zhí)行這些命令來恢復(fù)原始的數(shù)據(jù),AOF命令以redis協(xié)議追加保存每次寫的操作到文件末尾。Redis還能對AOF文件進(jìn)行后臺重寫,使得AOF文件的體積不至于過大。
AOF的執(zhí)行流程包括:
命令追加(append)
Redis先將寫命令追加到緩沖區(qū)aof_buf,而不是直接寫入文件,主要是為了避免每次有寫命令都直接寫入硬盤,導(dǎo)致硬盤IO成為Redis負(fù)載的瓶頸。
文件寫入(write)和文件同步(sync)
根據(jù)不同的同步策略將aof_buf中的內(nèi)容同步到硬盤;
Linux 操作系統(tǒng)中為了提升性能,使用了頁緩存(page cache)。當(dāng)我們將 aof_buf 的內(nèi)容寫到磁盤上時,此時數(shù)據(jù)并沒有真正的落盤,而是在 page cache 中,為了將 page cache 中的數(shù)據(jù)真正落盤,需要執(zhí)行 fsync / fdatasync 命令來強(qiáng)制刷盤。這邊的文件同步做的就是刷盤操作,或者叫文件刷盤可能更容易理解一些。
AOF緩存區(qū)的同步文件策略由參數(shù)appendfsync控制,各個值的含義如下:
always:命令寫入aof_buf后立即調(diào)用系統(tǒng)write操作和系統(tǒng)fsync操作同步到AOF文件,fsync完成后線程返回。這種情況下,每次有寫命令都要同步到AOF文件,硬盤IO成為性能瓶頸,Redis只能支持大約幾百TPS寫入,嚴(yán)重降低了Redis的性能;即便是使用固態(tài)硬盤(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。可靠性較高,數(shù)據(jù)基本不丟失。
no:命令寫入aof_buf后調(diào)用系統(tǒng)write操作,不對AOF文件做fsync同步;同步由操作系統(tǒng)負(fù)責(zé),通常同步周期為30秒。這種情況下,文件同步的時間不可控,且緩沖區(qū)中堆積的數(shù)據(jù)會很多,數(shù)據(jù)安全性無法保證。
everysec:命令寫入aof_buf后調(diào)用系統(tǒng)write操作,write完成后線程返回;fsync同步文件操作由專門的線程每秒調(diào)用一次。everysec是前述兩種策略的折中,是性能和數(shù)據(jù)安全性的平衡,因此是Redis的默認(rèn)配置,也是我們推薦的配置。
有同學(xué)可能會疑問為什么always策略還是不能100%保障數(shù)據(jù)不丟失,例如在開啟AOF的情況下,有一條寫命令,Redis在寫命令執(zhí)行完,寫aof_buf未成功的情況下宕機(jī)了?
不能,Redis就不能100%保證數(shù)據(jù)不丟失。
void?flushAppendOnlyFile(int?force)?{ssize_t?nwritten;int?sync_in_progress?=?0;mstime_t?latency;if?(sdslen(server.aof_buf)?==?0)?return;if?(server.aof_fsync?==?AOF_FSYNC_EVERYSEC)sync_in_progress?=?bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC)?!=?0;if?(server.aof_fsync?==?AOF_FSYNC_EVERYSEC?&&?!force)?{/*?With?this?append?fsync?policy?we?do?background?fsyncing.*?If?the?fsync?is?still?in?progress?we?can?try?to?delay*?the?write?for?a?couple?of?seconds.?*/if?(sync_in_progress)?{if?(server.aof_flush_postponed_start?==?0)?{/*?No?previous?write?postponing,?remember?that?we?are*?postponing?the?flush?and?return.?*/server.aof_flush_postponed_start?=?server.unixtime;return;}?else?if?(server.unixtime?-?server.aof_flush_postponed_start?<?2)?{/*?We?were?already?waiting?for?fsync?to?finish,?but?for?less*?than?two?seconds?this?is?still?ok.?Postpone?again.?*/return;}/*?Otherwise?fall?trough,?and?go?write?since?we?can't?wait*?over?two?seconds.?*/server.aof_delayed_fsync++;redisLog(REDIS_NOTICE,"Asynchronous?AOF?fsync?is?taking?too?long?(disk?is?busy?).?Writing?the?AOF?buffer?without?waiting?for?fsync?to?complete,?this?may?slow?down?Redis.");}}/*?We?want?to?perform?a?single?write.?This?should?be?guaranteed?atomic*?at?least?if?the?filesystem?we?are?writing?is?a?real?physical?one.*?While?this?will?save?us?against?the?server?being?killed?I?don't?think*?there?is?much?to?do?about?the?whole?server?stopping?for?power?problems*?or?alike?*/latencyStartMonitor(latency);nwritten?=?write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));latencyEndMonitor(latency);/*?We?want?to?capture?different?events?for?delayed?writes:*?when?the?delay?happens?with?a?pending?fsync,?or?with?a?saving?child*?active,?and?when?the?above?two?conditions?are?missing.*?We?also?use?an?additional?event?name?to?save?all?samples?which?is*?useful?for?graphing?/?monitoring?purposes.?*/if?(sync_in_progress)?{latencyAddSampleIfNeeded("aof-write-pending-fsync",latency);}?else?if?(server.aof_child_pid?!=?-1?||?server.rdb_child_pid?!=?-1)?{latencyAddSampleIfNeeded("aof-write-active-child",latency);}?else?{latencyAddSampleIfNeeded("aof-write-alone",latency);}latencyAddSampleIfNeeded("aof-write",latency);/*?We?performed?the?write?so?reset?the?postponed?flush?sentinel?to?zero.?*/server.aof_flush_postponed_start?=?0;if?(nwritten?!=?(signed)sdslen(server.aof_buf))?{static?time_t?last_write_error_log?=?0;int?can_log?=?0;/*?Limit?logging?rate?to?1?line?per?AOF_WRITE_LOG_ERROR_RATE?seconds.?*/if?((server.unixtime?-?last_write_error_log)?>?AOF_WRITE_LOG_ERROR_RATE)?{can_log?=?1;last_write_error_log?=?server.unixtime;}/*?Log?the?AOF?write?error?and?record?the?error?code.?*/if?(nwritten?==?-1)?{if?(can_log)?{redisLog(REDIS_WARNING,"Error?writing?to?the?AOF?file:?%s",strerror(errno));server.aof_last_write_errno?=?errno;}}?else?{if?(can_log)?{redisLog(REDIS_WARNING,"Short?write?while?writing?to?""the?AOF?file:?(nwritten=%lld,?""expected=%lld)",(long?long)nwritten,(long?long)sdslen(server.aof_buf));}if?(ftruncate(server.aof_fd,?server.aof_current_size)?==?-1)?{if?(can_log)?{redisLog(REDIS_WARNING,?"Could?not?remove?short?write?""from?the?append-only?file.??Redis?may?refuse?""to?load?the?AOF?the?next?time?it?starts.??""ftruncate:?%s",?strerror(errno));}}?else?{/*?If?the?ftruncate()?succeeded?we?can?set?nwritten?to*?-1?since?there?is?no?longer?partial?data?into?the?AOF.?*/nwritten?=?-1;}server.aof_last_write_errno?=?ENOSPC;}/*?Handle?the?AOF?write?error.?*/if?(server.aof_fsync?==?AOF_FSYNC_ALWAYS)?{/*?We?can't?recover?when?the?fsync?policy?is?ALWAYS?since?the*?reply?for?the?client?is?already?in?the?output?buffers,?and?we*?have?the?contract?with?the?user?that?on?acknowledged?write?data*?is?synced?on?disk.?*/redisLog(REDIS_WARNING,"Can't?recover?from?AOF?write?error?when?the?AOF?fsync?policy?is?'always'.?Exiting...");exit(1);}?else?{/*?Recover?from?failed?write?leaving?data?into?the?buffer.?However*?set?an?error?to?stop?accepting?writes?as?long?as?the?error*?condition?is?not?cleared.?*/server.aof_last_write_status?=?REDIS_ERR;/*?Trim?the?sds?buffer?if?there?was?a?partial?write,?and?there*?was?no?way?to?undo?it?with?ftruncate(2).?*/if?(nwritten?>?0)?{server.aof_current_size?+=?nwritten;sdsrange(server.aof_buf,nwritten,-1);}return;?/*?We'll?try?again?on?the?next?call...?*/}}?else?{/*?Successful?write(2).?If?AOF?was?in?error?state,?restore?the*?OK?state?and?log?the?event.?*/if?(server.aof_last_write_status?==?REDIS_ERR)?{redisLog(REDIS_WARNING,"AOF?write?error?looks?solved,?Redis?can?write?again.");server.aof_last_write_status?=?REDIS_OK;}}server.aof_current_size?+=?nwritten;/*?Re-use?AOF?buffer?when?it?is?small?enough.?The?maximum?comes?from?the*?arena?size?of?4k?minus?some?overhead?(but?is?otherwise?arbitrary).?*/if?((sdslen(server.aof_buf)+sdsavail(server.aof_buf))?<?4000)?{sdsclear(server.aof_buf);}?else?{sdsfree(server.aof_buf);server.aof_buf?=?sdsempty();}/*?Don't?fsync?if?no-appendfsync-on-rewrite?is?set?to?yes?and?there?are*?children?doing?I/O?in?the?background.?*/if?(server.aof_no_fsync_on_rewrite?&&(server.aof_child_pid?!=?-1?||?server.rdb_child_pid?!=?-1))return;/*?Perform?the?fsync?if?needed.?*/if?(server.aof_fsync?==?AOF_FSYNC_ALWAYS)?{/*?aof_fsync?is?defined?as?fdatasync()?for?Linux?in?order?to?avoid*?flushing?metadata.?*/latencyStartMonitor(latency);aof_fsync(server.aof_fd);?/*?Let's?try?to?get?this?data?on?the?disk?*/latencyEndMonitor(latency);latencyAddSampleIfNeeded("aof-fsync-always",latency);server.aof_last_fsync?=?server.unixtime;}?else?if?((server.aof_fsync?==?AOF_FSYNC_EVERYSEC?&&server.unixtime?>?server.aof_last_fsync))?{if?(!sync_in_progress)?aof_background_fsync(server.aof_fd);server.aof_last_fsync?=?server.unixtime;} }那么從上面redis-3.0的源碼及上下文
if?(server.aof_fsync?==?AOF_FSYNC_ALWAYS)分析得出,其實我們每次執(zhí)行客戶端命令的時候操作并沒有寫到aof文件中,只是寫到了aof_buf內(nèi)存當(dāng)中,只有當(dāng)下一個事件來臨時,才會去fsync到disk中,從redis的這種策略上我們也可以看出,redis和mysql在數(shù)據(jù)持久化之間的區(qū)別,redis的數(shù)據(jù)持久化僅僅就是一個附帶功能,并不是其主要功能。
結(jié)論:Redis即使在配制appendfsync=always的策略下,還是會丟失一個事件循環(huán)的數(shù)據(jù)。
文件重寫(rewrite)
定期重寫AOF文件,達(dá)到壓縮的目的。
AOF重寫是AOF持久化的一個機(jī)制,用來壓縮AOF文件,通過fork一個子進(jìn)程,重新寫一個新的AOF文件,該次重寫不是讀取舊的AOF文件進(jìn)行復(fù)制,而是讀取內(nèi)存中的Redis數(shù)據(jù)庫,重寫一份AOF文件,有點(diǎn)類似于RDB的快照方式。
文件重寫之所以能夠壓縮AOF文件,原因在于:
過期的數(shù)據(jù)不再寫入文件
無效的命令不再寫入文件:如有些數(shù)據(jù)被重復(fù)設(shè)值(set mykey v1, set mykey v2)、有些數(shù)據(jù)被刪除了(sadd myset v1, del myset)等等
多條命令可以合并為一個:如sadd myset v1, sadd myset v2, sadd myset v3可以合并為sadd myset v1 v2 v3。不過為了防止單條命令過大造成客戶端緩沖區(qū)溢出,對于list、set、hash、zset類型的key,并不一定只使用一條命令;而是以某個常量為界將命令拆分為多條。這個常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定義,不可更改,3.0版本中值是64。
文件重寫時機(jī)
相關(guān)參數(shù):
aof_current_size:表示當(dāng)前 AOF 文件空間
aof_base_size:表示上一次重寫后 AOF 文件空間
auto-aof-rewrite-min-size: 表示運(yùn)行 AOF 重寫時文件的最小體積,默認(rèn)為64MB
auto-aof-rewrite-percentage: 表示當(dāng)前 AOF 重寫時文件空間(aof_current_size)超過上一次重寫后 AOF 文件空間(aof_base_size)的比值多少后會重寫。
同時滿足下面兩個條件,則觸發(fā) AOF 重寫機(jī)制:
aof_current_size 大于 auto-aof-rewrite-min-size
當(dāng)前 AOF 相比上一次 AOF 的增長率:(aof_current_size - aof_base_size)/aof_base_size 大于或等于 auto-aof-rewrite-percentage
文件重寫流程
流程說明:
執(zhí)行AOF重寫請求。
如果當(dāng)前進(jìn)程正在執(zhí)行bgrewriteaof重寫,請求不執(zhí)行。
如果當(dāng)前進(jìn)程正在執(zhí)行bgsave操作,重寫命令延遲到bgsave完成之后再執(zhí)行。
父進(jìn)程執(zhí)行fork創(chuàng)建子進(jìn)程,開銷等同于bgsave過程。
3.1 主進(jìn)程fork操作完成后,繼續(xù)響應(yīng)其它命令。所有修改命令依然寫入AOF文件緩沖區(qū)并根據(jù)appendfsync策略同步到磁盤,保證原有AOF機(jī)制正確性。
3.2 由于fork操作運(yùn)用寫時復(fù)制技術(shù),子進(jìn)程只能共享fork操作時的內(nèi)存數(shù)據(jù)由于父進(jìn)程依然響應(yīng)命令,Redis使用“AOF”重寫緩沖區(qū)保存這部分新數(shù)據(jù),防止新的AOF文件生成期間丟失這部分?jǐn)?shù)據(jù)。
子進(jìn)程依據(jù)內(nèi)存快照,按照命令合并規(guī)則寫入到新的AOF文件。每次批量寫入硬盤數(shù)據(jù)量由配置aof-rewrite-incremental-fsync控制,默認(rèn)為32MB,防止單次刷盤數(shù)據(jù)過多造成硬盤阻塞。
5.1 新AOF文件寫入完成后,子進(jìn)程發(fā)送信號給父進(jìn)程,父進(jìn)程更新統(tǒng)計信息。
5.2 父進(jìn)程把AOF重寫緩沖區(qū)的數(shù)據(jù)寫入到新的AOF文件。
5.3 使用新的AOF文件替換老的AOF文件,完成AOF重寫。
Redis 為什么考慮使用 AOF 而不是 WAL 呢??
很多數(shù)據(jù)庫都是采用的 Write Ahead Log(WAL)寫前日志,其特點(diǎn)就是先把修改的數(shù)據(jù)記錄到日志中,再進(jìn)行寫數(shù)據(jù)的提交,可以方便通過日志進(jìn)行數(shù)據(jù)恢復(fù)。
但是 Redis 采用的卻是 AOF(Append Only File)寫后日志,特點(diǎn)就是先執(zhí)行寫命令,把數(shù)據(jù)寫入內(nèi)存中,再記錄日志。
如果先讓系統(tǒng)執(zhí)行命令,只有命令能執(zhí)行成功,才會被記錄到日志中。因此,Redis 使用寫后日志這種形式,可以避免出現(xiàn)記錄錯誤命令的情況。
另外還有一個原因就是:AOF 是在命令執(zhí)行后才記錄日志,所以不會阻塞當(dāng)前的寫操作。
四、復(fù)制Replication
主從復(fù)制過程大體可以分為3個階段:連接建立階段(即準(zhǔn)備階段)、數(shù)據(jù)同步階段、命令傳播階段;下面分別進(jìn)行介紹。
連接建立階段
保存主節(jié)點(diǎn)信息
建立socket連接
發(fā)送ping命令
身份驗證
發(fā)送從節(jié)點(diǎn)端口信息
數(shù)據(jù)同步階段
執(zhí)行流程:
全量復(fù)制(完整重同步)
Redis通過psync命令進(jìn)行全量復(fù)制的過程如下:
從節(jié)點(diǎn)判斷無法進(jìn)行部分復(fù)制,向主節(jié)點(diǎn)發(fā)送全量復(fù)制的請求;或從節(jié)點(diǎn)發(fā)送部分復(fù)制的請求,但主節(jié)點(diǎn)判斷無法進(jìn)行部分復(fù)制;具體判斷過程需要在講述了部分復(fù)制原理后再介紹;
主節(jié)點(diǎn)收到全量復(fù)制的命令后,執(zhí)行bgsave,在后臺生成RDB文件,并使用一個緩沖區(qū)(稱為復(fù)制緩沖區(qū))記錄從現(xiàn)在開始執(zhí)行的所有寫命令;
主節(jié)點(diǎn)的bgsave執(zhí)行完成后,將RDB文件發(fā)送給從節(jié)點(diǎn);從節(jié)點(diǎn)首先清除自己的舊數(shù)據(jù),然后載入接收的RDB文件,將數(shù)據(jù)庫狀態(tài)更新至主節(jié)點(diǎn)執(zhí)行bgsave時的數(shù)據(jù)庫狀態(tài);
主節(jié)點(diǎn)將前述復(fù)制緩沖區(qū)中的所有寫命令發(fā)送給從節(jié)點(diǎn),從節(jié)點(diǎn)執(zhí)行這些寫命令,將數(shù)據(jù)庫狀態(tài)更新至主節(jié)點(diǎn)的最新狀態(tài);
如果從節(jié)點(diǎn)開啟了AOF,則會觸發(fā)bgrewriteaof的執(zhí)行,從而保證AOF文件更新至主節(jié)點(diǎn)的最新狀態(tài)。
部分復(fù)制(部分重同步)
復(fù)制偏移量
主節(jié)點(diǎn)和從節(jié)點(diǎn)分別維護(hù)一個復(fù)制偏移量(offset),代表的是主節(jié)點(diǎn)向從節(jié)點(diǎn)傳遞的字節(jié)數(shù);主節(jié)點(diǎn)每次向從節(jié)點(diǎn)傳播N個字節(jié)數(shù)據(jù)時,主節(jié)點(diǎn)的offset增加N;從節(jié)點(diǎn)每次收到主節(jié)點(diǎn)傳來的N個字節(jié)數(shù)據(jù)時,從節(jié)點(diǎn)的offset增加N。
復(fù)制積壓緩沖區(qū)
復(fù)制積壓緩沖區(qū)是由主節(jié)點(diǎn)維護(hù)的、固定長度的、先進(jìn)先出(FIFO)隊列,默認(rèn)大小1MB;當(dāng)主節(jié)點(diǎn)開始有從節(jié)點(diǎn)時創(chuàng)建,其作用是備份主節(jié)點(diǎn)最近發(fā)送給從節(jié)點(diǎn)的數(shù)據(jù)。注意,無論主節(jié)點(diǎn)有一個還是多個從節(jié)點(diǎn),都只需要一個復(fù)制積壓緩沖區(qū)。
服務(wù)器運(yùn)行ID(runid)
每個Redis節(jié)點(diǎn)(無論主從),在啟動時都會自動生成一個隨機(jī)ID(每次啟動都不一樣),由40個隨機(jī)的十六進(jìn)制字符組成;runid用來唯一識別一個Redis節(jié)點(diǎn)。
命令傳播階段
主->從:PING。
每隔指定的時間,主節(jié)點(diǎn)會向從節(jié)點(diǎn)發(fā)送PING命令,這個PING命令的作用,主要是為了讓從節(jié)點(diǎn)進(jìn)行超時判斷。
從->主:REPLCONF ACK
在命令傳播階段,從節(jié)點(diǎn)會向主節(jié)點(diǎn)發(fā)送REPLCONF ACK命令,頻率是每秒1次;命令格式為:REPLCONF ACK {offset},其中offset指從節(jié)點(diǎn)保存的復(fù)制偏移量。
五、架構(gòu)模式
哨兵模式
哨兵模式工作原理
每個 Sentinel 以每秒一次的頻率向它所知的 Master,Slave 以及其他 Sentinel 節(jié)點(diǎn)發(fā)送一個 PING 命令;
如果一個實例(instance)距離最后一次有效回復(fù) PING 命令的時間超過配置文件 own-after-milliseconds 選項所指定的值,則這個實例會被 Sentinel 標(biāo)記為主觀下線;
如果一個 Master 被標(biāo)記為主觀下線,那么正在監(jiān)視這個 Master 的所有 Sentinel 要以每秒一次的頻率確認(rèn) Master 是否真的進(jìn)入主觀下線狀態(tài);
當(dāng)有足夠數(shù)量的 Sentinel(大于等于配置文件指定的值)在指定的時間范圍內(nèi)確認(rèn) Master 的確進(jìn)入了主觀下線狀態(tài),則 Master 會被標(biāo)記為客觀下線;
如果 Master 處于 ODOWN 狀態(tài),則投票自動選出新的主節(jié)點(diǎn)。將剩余的從節(jié)點(diǎn)指向新的主節(jié)點(diǎn)繼續(xù)進(jìn)行數(shù)據(jù)復(fù)制;
在正常情況下,每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有 Master,Slave 發(fā)送 INFO 命令;當(dāng) Master 被 Sentinel 標(biāo)記為客觀下線時,Sentinel 向已下線的 Master 的所有 Slave 發(fā)送 INFO 命令的頻率會從 10 秒一次改為每秒一次;
若沒有足夠數(shù)量的 Sentinel 同意 Master 已經(jīng)下線,Master 的客觀下線狀態(tài)就會被移除。若 Master 重新向 Sentinel 的 PING 命令返回有效回復(fù),Master 的主觀下線狀態(tài)就會被移除。
https://redis.io/topics/replication
https://redis.io/topics/persistence
https://www.cnblogs.com/mrhelloworld/p/redis-architecture.html
https://mp.weixin.qq.com/s/V2MKyMKtCO1skgWWXBnrgg
https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
https://www.cnblogs.com/kismetv/p/8654978.html
https://zhuanlan.zhihu.com/p/187596888
https://cloud.tencent.com/developer/article/1633077
https://www.cnblogs.com/yangmingxianshen/p/8373205.html
主要缺陷:單個節(jié)點(diǎn)的寫能力,存儲能力受到單機(jī)的限制,動態(tài)擴(kuò)容困難復(fù)雜。
集群模式
為了解決哨兵模式存儲受單機(jī)的限制,這里引入分片概念。
分片
Redis Cluster 采用虛擬哈希槽分區(qū),所有的鍵根據(jù)哈希函數(shù)映射到 0 ~ 16383 整數(shù)槽內(nèi),計算公式:HASH_SLOT = CRC16(key) % 16384。每一個節(jié)點(diǎn)負(fù)責(zé)維護(hù)一部分槽以及槽所映射的鍵值數(shù)據(jù)。
Redis Cluster 提供了靈活的節(jié)點(diǎn)擴(kuò)容和縮容方案。在不影響集群對外服務(wù)的情況下,可以為集群添加節(jié)點(diǎn)進(jìn)行擴(kuò)容也可以下線部分節(jié)點(diǎn)進(jìn)行縮容。可以說,槽是 Redis Cluster 管理數(shù)據(jù)的基本單位,集群伸縮就是槽和數(shù)據(jù)在節(jié)點(diǎn)之間的移動。
參考文獻(xiàn)
近期好文:
四種代碼潔癖類型,程序員看了直呼內(nèi)行
一文深入理解 Kubernetes
從 0 到 1 實現(xiàn)瀏覽器端沙盒運(yùn)行環(huán)境
視頻號最新視頻
同學(xué),你打拳這么厲害,是拳擊教練么?
總結(jié)
以上是生活随笔為你收集整理的这篇Redis文章,图灵看了都说好的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 腾讯云EMR基于YARN针对云原生容器化
- 下一篇: Nginx 最全操作总结