2007 年,一位程序員和朋友一起創建了一個網站。為了解決這個網站的負載問題,他自己定制了一個數據庫。于2009 年開發,稱之為Redis。這位意大利程序員是薩爾瓦托勒·桑菲利波(Salvatore Sanfilippo),他被稱為Redis之父,更廣為人知的名字是Antirez。
1.Redis簡介
REmote DIctionary Server(Redis) 是一個開源的使用 ANSI C 語言編寫、遵守 BSD 協議、支持網絡、可基于內存、分布式、可選持久性的鍵值對(Key-Value)存儲數據庫,并提供多種語言的 API。
Redis 通常被稱為數據結構服務器,因為值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等類型。
2.內存模型
首先可以進行Redis的內存模型學習,對Redis的使用有很大幫助,例如OOM時定位、內存使用量評估等。
通過info memory命令查看內存的使用情況。
主要參數:
- used_memory:從Redis角度使用了多少內存,即Redis分配器分配的內存總量(單位是字節),包括使用的虛擬內存(即swap);
- used_memory_rss:從操作系統角度實際使用量,即Redis進程占據操作系統的內存(單位是字節),包括進程運行本身需要的內存、內存碎片等,不包括虛擬內存。一般情況下used_memory_rss都要比used_memory大,因為Redis頻繁刪除讀寫等操作使得內存碎片較多,而虛擬內存的使用一般是非極端情況下是不怎么使用的;
- mem_fragmentation_ratio:即內存碎片比率,該值是used_memory_rss / used_memory的比值;mem_fragmentation_ratio一般大于1,且該值越大,內存碎片比例越大。如果mem_fragmentation_ratio<1,說明Redis使用了虛擬內存,由于虛擬內存的媒介是磁盤,比內存速度要慢很多,當這種情況出現時,應該及時排查,如果內存不足應該及時處理,如增加Redis節點、增加Redis服務器的內存、優化應用等。一般來說,mem_fragmentation_ratio在1.03左右是比較健康的狀態(對于jemalloc來說);
- mem_allocator:即Redis使用的內存分配器,一般默認是jemalloc。
2.1 Redis內存劃分
- 數據
作為數據庫,數據是最主要的部分,這部分占用的內存會統計在used_memory中。
- 進程本身運行需要的內存
這部分內存不是由jemalloc分配,因此不會統計在used_memory中。
- 緩沖內存
緩沖內存包括:
- 客戶端緩沖區:存儲客戶端連接的輸入輸出緩沖;
- 復制積壓緩沖區:用于部分復制功能;
- aof_buf:用于在進行AOF重寫時,保存最近的寫入命令。
這部分內存由jemalloc分配,因此會統計在used_memory中。
- 內存碎片
內存碎片是Redis在數據更改頻繁分配、回收物理內存過程中產生的。
內存碎片不會統計在used_memory中。
2.2 Redis數據存儲的細節
下面看一張經典的圖。
- jemalloc
無論是DictEntry對象,還是RedisObject、SDS對象,都需要內存分配器(如jemalloc)分配內存進行存儲。Redis在編譯時便會指定內存分配器;內存分配器可以是 libc 、jemalloc或者tcmalloc,默認是jemalloc。當Redis存儲數據時,會選擇大小最合適的內存塊進行存儲。
jemalloc劃分的內存單元如下圖所示:?
- dictEntry
每個dictEntry都保存著一個鍵值對,key值保存一個sds結構體,value值保存一個redisObject結構體。?
- redisObject
前面說到,Redis對象有5種類型;無論是哪種類型,Redis都不會直接存儲,而是通過RedisObject對象進行存儲。
RedisObject的每個字段的含義和作用如下:
- type
type字段表示對象的類型,占4個比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
- encoding
encoding表示對象的內部編碼,占4個比特(redis-3.0)。
- lru
lru記錄的是對象最后一次被命令程序訪問的時間,占據的比特數不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。
- refcount
refcount記錄的是該對象被引用的次數,類型為整型。refcount的作用,主要在于對象的引用計數和內存回收:
- 當創建新對象時,refcount初始化為1;
- 當有新程序使用該對象時,refcount加1;
- 當對象不再被一個新程序使用時,refcount減1;
- 當refcount變為0時,對象占用的內存會被釋放。
Redis中被多次使用的對象(refcount>1)稱為共享對象。Redis為了節省內存,當有一些對象重復出現時,新的程序不會創建新的對象,而是仍然使用原來的對象。這個被重復使用的對象,就是共享對象。目前共享對象僅支持整數值的字符串對象。
共享對象的具體實現
Redis的共享對象目前只支持整數值的字符串對象。之所以如此,實際上是對內存和CPU(時間)的平衡:共享對象雖然會降低內存消耗,但是判斷兩個對象是否相等卻需要消耗額外的時間。
對于整數值,判斷操作復雜度為O(1);
對于普通字符串,判斷復雜度為O(n);
而對于哈希、列表、集合和有序集合,判斷的復雜度為O(n^2)。
雖然共享對象只能是整數值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。
就目前的實現來說,Redis服務器在初始化時,會創建10000個字符串對象,值分別是09999的整數值;當Redis需要使用值為09999的字符串對象時,可以直接使用這些共享對象。10000這個數字可以通過調整參數REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變。
- ptr
ptr指針指向具體的數據,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
- SDS
Redis沒有直接使用C字符串(即以空字符‘\0’結尾的字符數組)作為默認的字符串表示,而是使用了SDS。SDS是簡單動態字符串(Simple Dynamic String)的縮寫。
)
通過SDS的結構可以看出,buf數組的長度=free+len+1(其中1表示字符串結尾的空字符);所以,一個SDS結構占據的空間為:free所占長度+len所占長度+ buf數組的長度=4+4+free+len+1=free+len+9。
為什么使用SDS而不直接使用C字符串?
SDS在C字符串的基礎上加入了free和len字段,帶來了很多好處:
- 獲取字符串長度:SDS是O(1),C字符串是O(n)。
- 緩沖區溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內存,很容易造成緩沖區的溢出;而SDS由于記錄了長度,相應的API在可能造成緩沖區溢出時會自動重新分配內存,杜絕了緩沖區溢出。
- 修改字符串時內存的重分配:對于C字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內存緩沖區溢出,字符串長度減小時會造成內存泄露。而對于SDS,由于可以記錄len和free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化——空間預分配策略(即分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率大大減小;惰性空間釋放策略使得字符串長度減小時重新分配內存的概率大大減小。
- 存取二進制數據:SDS可以,C字符串不可以。因為C字符串以空字符作為字符串結束的標識,而對于一些二進制文件(如圖片等),內容可能包括空字符串,因此C字符串無法正確存取;而SDS以字符串長度len來作為字符串結束標識,因此沒有這個問題。
3.持久化Persistence
持久化的功能:Redis是內存數據庫,數據都是存儲在內存中,為了避免進程退出導致數據的永久丟失,需要定期將Redis中的數據以某種形式(數據或命令)從內存保存到硬盤。當下次Redis重啟時,利用持久化文件實現數據恢復。除此之外,為了進行災難備份,可以將持久化文件拷貝到一個遠程位置。
Redis持久化分為RDB持久化和AOF持久化,前者將當前數據保存到硬盤,后者則是將每次執行的寫命令保存到硬盤(類似于MySQL的Binlog)。由于AOF持久化的實時性更好,即當進程意外退出時丟失的數據更少,因此AOF是目前主流的持久化方式,不過RDB持久化仍然有其用武之地。
3.1 RDB持久化
RDB(Redis Database)持久化方式能夠在指定的時間間隔能對你的數據進行快照存儲。一般通過bgsave命令會創建一個子進程,由子進程來負責創建RDB文件,父進程(即Redis主進程)則繼續處理請求。
圖片中的5個步驟所進行的操作如下:
- Redis父進程首先判斷:當前是否在執行save,或bgsave/bgrewriteaof(后面會詳細介紹該命令)的子進程,如果在執行則bgsave命令直接返回。bgsave/bgrewriteaof 的子進程不能同時執行,主要是基于性能方面的考慮:兩個并發的子進程同時執行大量的磁盤寫操作,可能引起嚴重的性能問題。
- 父進程執行fork操作創建子進程,這個過程中父進程是阻塞的,Redis不能執行來自客戶端的任何命令;
- 父進程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父進程,并可以響應其他命令;
- 子進程進程對內存數據生成快照文件;
- 子進程發送信號給父進程表示完成,父進程更新統計信息。
這里補充一下第4點是如何生成RDB文件的。一定有讀者也有疑問:在同步到磁盤和持續寫入這個過程是如何處理數據不一致的情況呢?生成快照RDB文件時是否會對業務產生影響?
- 通過 fork 創建的子進程能夠獲得和父進程完全相同的內存空間,父進程對內存的修改對于子進程是不可見的,兩者不會相互影響;
- 通過 fork 創建子進程時不會立刻觸發大量內存的拷貝,采用的是寫時拷貝COW (Copy On Write)。內核只為新生成的子進程創建虛擬空間結構,它們來復制于父進程的虛擬究竟結構,但是不為這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間;
3.2 AOF持久化
AOF(Append Only File)持久化方式記錄每次對服務器寫的操作,當服務器重啟的時候會重新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操作到文件末尾。Redis還能對AOF文件進行后臺重寫,使得AOF文件的體積不至于過大。
AOF的執行流程包括:
3.3 命令追加(append)
Redis先將寫命令追加到緩沖區aof_buf,而不是直接寫入文件,主要是為了避免每次有寫命令都直接寫入硬盤,導致硬盤IO成為Redis負載的瓶頸。
3.4 文件寫入(write)和文件同步(sync)
根據不同的同步策略將aof_buf中的內容同步到硬盤;
Linux 操作系統中為了提升性能,使用了頁緩存(page cache)。當我們將 aof_buf 的內容寫到磁盤上時,此時數據并沒有真正的落盤,而是在 page cache 中,為了將 page cache 中的數據真正落盤,需要執行 fsync / fdatasync 命令來強制刷盤。這邊的文件同步做的就是刷盤操作,或者叫文件刷盤可能更容易理解一些。
AOF緩存區的同步文件策略由參數appendfsync控制,各個值的含義如下:
- always:命令寫入aof_buf后立即調用系統write操作和系統fsync操作同步到AOF文件,fsync完成后線程返回。這種情況下,每次有寫命令都要同步到AOF文件,硬盤IO成為性能瓶頸,Redis只能支持大約幾百TPS寫入,嚴重降低了Redis的性能;即便是使用固態硬盤(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低SSD的壽命。可靠性較高,數據基本不丟失。
- no:命令寫入aof_buf后調用系統write操作,不對AOF文件做fsync同步;同步由操作系統負責,通常同步周期為30秒。這種情況下,文件同步的時間不可控,且緩沖區中堆積的數據會很多,數據安全性無法保證。
- everysec:命令寫入aof_buf后調用系統write操作,write完成后線程返回;fsync同步文件操作由專門的線程每秒調用一次。everysec是前述兩種策略的折中,是性能和數據安全性的平衡,因此是Redis的默認配置,也是我們推薦的配置。
有同學可能會疑問為什么always策略還是不能100%保障數據不丟失,例如在開啟AOF的情況下,有一條寫命令,Redis在寫命令執行完,寫aof_buf未成功的情況下宕機了?
不能,Redis就不能100%保證數據不丟失。
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)
分析得出,其實我們每次執行客戶端命令的時候操作并沒有寫到aof文件中,只是寫到了aof_buf內存當中,只有當下一個事件來臨時,才會去fsync到disk中,從redis的這種策略上我們也可以看出,redis和mysql在數據持久化之間的區別,redis的數據持久化僅僅就是一個附帶功能,并不是其主要功能。
結論:Redis即使在配制appendfsync=always的策略下,還是會丟失一個事件循環的數據。
3.5 文件重寫(rewrite)
定期重寫AOF文件,達到壓縮的目的。
AOF重寫是AOF持久化的一個機制,用來壓縮AOF文件,通過fork一個子進程,重新寫一個新的AOF文件,該次重寫不是讀取舊的AOF文件進行復制,而是讀取內存中的Redis數據庫,重寫一份AOF文件,有點類似于RDB的快照方式。
文件重寫之所以能夠壓縮AOF文件,原因在于:
- 過期的數據不再寫入文件
- 無效的命令不再寫入文件:如有些數據被重復設值(set mykey v1, set mykey v2)、有些數據被刪除了(sadd myset v1, del myset)等等
- 多條命令可以合并為一個:如sadd myset v1, sadd myset v2, sadd myset v3可以合并為sadd myset v1 v2 v3。不過為了防止單條命令過大造成客戶端緩沖區溢出,對于list、set、hash、zset類型的key,并不一定只使用一條命令;而是以某個常量為界將命令拆分為多條。這個常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定義,不可更改,3.0版本中值是64。
3.5.1?文件重寫時機
相關參數:
- aof_current_size:表示當前 AOF 文件空間
- aof_base_size:表示上一次重寫后 AOF 文件空間
- auto-aof-rewrite-min-size: 表示運行 AOF 重寫時文件的最小體積,默認為64MB
- auto-aof-rewrite-percentage: 表示當前 AOF 重寫時文件空間(aof_current_size)超過上一次重寫后 AOF 文件空間(aof_base_size)的比值多少后會重寫。
同時滿足下面兩個條件,則觸發 AOF 重寫機制:
- aof_current_size 大于 auto-aof-rewrite-min-size
- 當前 AOF 相比上一次 AOF 的增長率:(aof_current_size - aof_base_size)/aof_base_size 大于或等于 auto-aof-rewrite-percentage
3.5.2?文件重寫流程
?流程說明:
- 執行AOF重寫請求。
如果當前進程正在執行bgrewriteaof重寫,請求不執行。
如果當前進程正在執行bgsave操作,重寫命令延遲到bgsave完成之后再執行。
- 父進程執行fork創建子進程,開銷等同于bgsave過程。
3.1 主進程fork操作完成后,繼續響應其它命令。所有修改命令依然寫入AOF文件緩沖區并根據appendfsync策略同步到磁盤,保證原有AOF機制正確性。
3.2 由于fork操作運用寫時復制技術,子進程只能共享fork操作時的內存數據由于父進程依然響應命令,Redis使用“AOF”重寫緩沖區保存這部分新數據,防止新的AOF文件生成期間丟失這部分數據。
- 子進程依據內存快照,按照命令合并規則寫入到新的AOF文件。每次批量寫入硬盤數據量由配置aof-rewrite-incremental-fsync控制,默認為32MB,防止單次刷盤數據過多造成硬盤阻塞。
5.1 新AOF文件寫入完成后,子進程發送信號給父進程,父進程更新統計信息。
5.2 父進程把AOF重寫緩沖區的數據寫入到新的AOF文件。
5.3 使用新的AOF文件替換老的AOF文件,完成AOF重寫。
Redis 為什么考慮使用 AOF 而不是 WAL 呢?
很多數據庫都是采用的 Write Ahead Log(WAL)寫前日志,其特點就是先把修改的數據記錄到日志中,再進行寫數據的提交,可以方便通過日志進行數據恢復。
但是 Redis 采用的卻是 AOF(Append Only File)寫后日志,特點就是先執行寫命令,把數據寫入內存中,再記錄日志。
如果先讓系統執行命令,只有命令能執行成功,才會被記錄到日志中。因此,Redis 使用寫后日志這種形式,可以避免出現記錄錯誤命令的情況。
另外還有一個原因就是:AOF 是在命令執行后才記錄日志,所以不會阻塞當前的寫操作。
4.復制Replication
主從復制過程大體可以分為3個階段:連接建立階段(即準備階段)、數據同步階段、命令傳播階段;下面分別進行介紹。
4.1 連接建立階段
- 保存主節點信息
- 建立socket連接
- 發送ping命令
- 身份驗證
- 發送從節點端口信息
4.2 數據同步階段
執行流程:?
-
全量復制(完整重同步)
Redis通過psync命令進行全量復制的過程如下:
-
- 從節點判斷無法進行部分復制,向主節點發送全量復制的請求;或從節點發送部分復制的請求,但主節點判斷無法進行部分復制;具體判斷過程需要在講述了部分復制原理后再介紹;
- 主節點收到全量復制的命令后,執行bgsave,在后臺生成RDB文件,并使用一個緩沖區(稱為復制緩沖區)記錄從現在開始執行的所有寫命令;
- 主節點的bgsave執行完成后,將RDB文件發送給從節點;從節點首先清除自己的舊數據,然后載入接收的RDB文件,將數據庫狀態更新至主節點執行bgsave時的數據庫狀態;
- 主節點將前述復制緩沖區中的所有寫命令發送給從節點,從節點執行這些寫命令,將數據庫狀態更新至主節點的最新狀態;
- 如果從節點開啟了AOF,則會觸發bgrewriteaof的執行,從而保證AOF文件更新至主節點的最新狀態。
-
部分復制(部分重同步)
- 復制偏移量
主節點和從節點分別維護一個復制偏移量(offset),代表的是主節點向從節點傳遞的字節數;主節點每次向從節點傳播N個字節數據時,主節點的offset增加N;從節點每次收到主節點傳來的N個字節數據時,從節點的offset增加N。
- 復制積壓緩沖區
復制積壓緩沖區是由主節點維護的、固定長度的、先進先出(FIFO)隊列,默認大小1MB;當主節點開始有從節點時創建,其作用是備份主節點最近發送給從節點的數據。注意,無論主節點有一個還是多個從節點,都只需要一個復制積壓緩沖區。
- 服務器運行ID(runid)
每個Redis節點(無論主從),在啟動時都會自動生成一個隨機ID(每次啟動都不一樣),由40個隨機的十六進制字符組成;runid用來唯一識別一個Redis節點。
4.3 命令傳播階段
主->從:PING。
每隔指定的時間,主節點會向從節點發送PING命令,這個PING命令的作用,主要是為了讓從節點進行超時判斷。
從->主:REPLCONF ACK
在命令傳播階段,從節點會向主節點發送REPLCONF ACK命令,頻率是每秒1次;命令格式為:REPLCONF ACK {offset},其中offset指從節點保存的復制偏移量。
5.架構模式
5.1 哨兵模式
5.1.1?哨兵模式工作原理
- 每個 Sentinel 以每秒一次的頻率向它所知的 Master,Slave 以及其他 Sentinel 節點發送一個?
PING
?命令; - 如果一個實例(instance)距離最后一次有效回復 PING 命令的時間超過配置文件?
own-after-milliseconds
?選項所指定的值,則這個實例會被 Sentinel 標記為主觀下線; - 如果一個 Master 被標記為主觀下線,那么正在監視這個 Master 的所有 Sentinel 要以每秒一次的頻率確認 Master 是否真的進入主觀下線狀態;
- 當有足夠數量的 Sentinel(大于等于配置文件指定的值)在指定的時間范圍內確認?Master 的確進入了主觀下線狀態,則 Master 會被標記為客觀下線;
- 如果 Master 處于?ODOWN 狀態,則投票自動選出新的主節點。將剩余的從節點指向新的主節點繼續進行數據復制;
- 在正常情況下,每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有 Master,Slave 發送?
INFO
?命令;當 Master 被 Sentinel 標記為客觀下線時,Sentinel 向已下線的 Master 的所有 Slave 發送 INFO 命令的頻率會從 10 秒一次改為每秒一次; - 若沒有足夠數量的 Sentinel 同意 Master 已經下線,Master 的客觀下線狀態就會被移除。若 Master 重新向 Sentinel 的 PING 命令返回有效回復,Master 的主觀下線狀態就會被移除。
主要缺陷:單個節點的寫能力,存儲能力受到單機的限制,動態擴容困難復雜。
5.2 集群模式
為了解決哨兵模式存儲受單機的限制,這里引入分片概念。?
5.2.1?分片
Redis Cluster 采用虛擬哈希槽分區,所有的鍵根據哈希函數映射到 0 ~ 16383 整數槽內,計算公式:HASH_SLOT = CRC16(key) % 16384
。每一個節點負責維護一部分槽以及槽所映射的鍵值數據。
Redis Cluster 提供了靈活的節點擴容和縮容方案。在不影響集群對外服務的情況下,可以為集群添加節點進行擴容也可以下線部分節點進行縮容。可以說,槽是 Redis Cluster 管理數據的基本單位,集群伸縮就是槽和數據在節點之間的移動。
參考文獻
- Redis replication | Docs
- Redis persistence | Docs
- 最通俗易懂的 Redis 架構模式詳解 - 哈嘍沃德先生 - 博客園
- https://mp.weixin.qq.com/s/V2MKyMKtCO1skgWWXBnrgg
- Linux寫時拷貝技術(copy-on-write) - as_ - 博客園
- 深入學習Redis(1):Redis內存模型 - 編程迷思 - 博客園
- https://zhuanlan.zhihu.com/p/187596888
- Redis AOF重寫阻塞問題分析-騰訊云開發者社區-騰訊云
- Redis持久化——AOF(二) - 明王不動心 - 博客園