Redis設計與實現 讀書筆記
阿新 • • 發佈:2022-03-25
Redis單執行緒
採用多路 I/O 複用技術可以讓單個執行緒高效的處理多個連線請求(儘量減少網路IO的時間消耗)
(1)為什麼不採用多程序或多執行緒處理? - 多執行緒處理可能涉及到鎖 - 多執行緒處理會涉及到執行緒切換而消耗CPU
(2)單執行緒處理的缺點? - 無法發揮多核CPU效能,不過可以通過在單機開多個Redis例項來完善
Redis的高併發和快速原因
- Redis是純記憶體資料庫,一般都是簡單的存取操作,執行緒佔用的時間很多,時間的花費主要集中在IO上,所以讀取速度快。
- 再說一下IO,Redis使用的是非阻塞IO,IO多路複用,使用了單執行緒來輪詢描述符,將資料庫的開、關、讀、寫都轉換成了事件,減少了執行緒切換時上下文的切換和競爭。
- Redis採用了單執行緒的模型,保證了每個操作的原子性,也減少了執行緒的上下文切換和競爭。
- Redis全程使用hash結構,讀取速度快,還有一些特殊的資料結構,對資料儲存進行了優化,如壓縮表,對短資料進行壓縮儲存,再如,跳錶,使用有序的資料結構加快讀取的速度。
- Redis採用自己實現的事件分離器,效率比較高,內部採用非阻塞的執行方式,吞吐能力比較大。
- 批量操作在傳送 EXEC 命令前被放入佇列快取。
- 收到 EXEC 命令後進入事務執行,事務中任意命令執行失敗,其餘命令依然被執行。也就是說 Redis 事務不保證原子性。
- 在事務執行過程中,其他客戶端提交的命令請求不會插入到事務執行命令序列中。
- 客戶端緩衝區:儲存客戶端連線的輸入輸出緩衝
- 複製積壓緩衝區:用於部分複製功能
- AOF緩衝區:用於在進行AOF重寫時,儲存最近的寫入命令
- redis是key-value資料庫,因此對每個鍵值對都會有一個dictEntry,儲存key和value的指標,next指向下一個dictEntry,與本鍵值對無關。
- key 不是直接以字串儲存,二十儲存在SDS結構中
- redisObject value的儲存資料結構,五種物件型別都是通過redisObject儲存
- jemalloc,記憶體分配器。dictEntry、redisObject和sds物件的記憶體都是通過記憶體分配器分配的。
- type 表示物件的型別,佔4個位元。目前包括REDIS_STRING(字串)、REDIS_LIST (列表)、REDIS_HASH(雜湊)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。通過命令type key命令獲取資料的type
- encoding 表示物件的內部編碼,佔4個位元。5種物件型別對應的編碼方式以及轉換在後文介紹。
- lru 記錄物件最後一次被命令程式訪問的時間,佔據的位元數不同的版本有所不同(如4.0版本佔24位元,2.6版本佔22位元)。
- 當建立新物件時,refcount初始化為1;
- 當有新程式使用該物件時,refcount加1;
- 當物件不再被一個新程式使用時,refcount減1;
- 當refcount變為0時,物件佔用的記憶體會被釋放
- 結構
- 獲取字串長度:SDS是O(1),C字串是O(n)。
- 緩衝區溢位:使用C字串的API時,如果字串長度增加(如strcat操作)而忘記重新分配記憶體,很容易造成緩衝區的溢位;而SDS由於記錄了長度,相應的API在可能造成緩衝區溢位時會自動重新分配記憶體,杜絕了緩衝區溢位。
- 修改字串時記憶體的重分配:對於C字串,如果要修改字串,必須要重新分配記憶體(先釋放再申請),因為如果沒有重新分配,字串長度增大時會造成記憶體緩衝區溢位,字串長度減小時會造成記憶體洩露。而對於SDS,由於可以記錄len和free,因此解除了字串長度和空間陣列長度之間的關聯,可以在此基礎上進行優化——空間預分配策略(即分配記憶體時比實際需要的多)使得字串長度增大時重新分配記憶體的概率大大減小;惰性空間釋放策略使得字串長度減小時重新分配記憶體的概率大大減小。
- 存取二進位制資料:SDS可以,C字串不可以。因為C字串以空字元作為字串結束的標識,而對於一些二進位制檔案(如圖片等),內容可能包括空字串,因此C字串無法正確存取;而SDS以字串長度len來作為字串結束標識,因此沒有這個問題。
1、字串 字串長度不能超過512MB,redis中最基礎的型別。鍵和其他型別的元素都是字串。 內部編碼應用場景:
- int:8個位元組的長整型。字串值是整型時,這個值使用long整型表示。
- embstr:<=39位元組的字串。embstr與raw都使用RedisObject和SDS儲存資料。區別在於:embstr的使用只分配一次記憶體空間(因此RedisObject和SDS是連續的),而raw需要分配兩次記憶體空間(分別為RedisObject和SDS分配空間)。因此與raw相比,embstr的好處在於建立時少分配一次空間、刪除時少釋放一次空間、物件的所有資料連在一起,尋找方便。而embstr的壞處也很明顯:如果字串的長度增加需要重新分配記憶體時,整個RedisObject和SDS都需要重新分配空間,因此Redis中的embstr實現為只讀。
- raw:大於39個位元組的字串
dictEntry dictEntry結構用於儲存鍵值對,結構定義如下: typedef struct dictEntry{ void *key; union{ void *val; uint64_tu64; int64_ts64; }v; struct dictEntry *next; }dictEntry; 其中,各個屬性的功能如下:
- key:鍵值對中的鍵;
- val:鍵值對中的值,使用union(即共用體)實現,儲存的內容既可能是一個指向值的指標,也可能是64位整型,或無符號64位整型;
- next:指向下一個dictEntry,用於解決雜湊衝突問題
- 在64位系統中,一個dictEntry物件佔24位元組(key/val/next各佔8位元組)。
- table屬性是一個指標,指向bucket;
- size屬性記錄了雜湊表的大小,即bucket的大小;
- used記錄了已使用的dictEntry的數量;
- sizemask屬性的值總是為size-1,這個屬性和雜湊值一起決定一個鍵在table中儲存的位置。
Redis持久化 RDB:快照形式是直接把記憶體中的資料儲存到一個 dump 的檔案中,定時儲存,儲存策略。 AOF:把所有的對 Redis 的伺服器進行修改的命令都存到一個檔案裡,命令的集合。Redis 預設是快照 RDB 的持久化方式。 Redis重啟後,優先使用AOF檔案來還原資料集,AOF儲存的資料集通常比RDB更完整。 RDB原理:redis在需要做持久化的時候,fork一個子程序,子程序將資料寫到磁碟的一個臨時RDB檔案中,在完成寫臨時檔案後,將原來的RDB替換掉,這樣的好處是可以copy-on-write。適合用於備份。 AOF原理:將每一個寫命令通過write函式追加到appendonly.aof檔案中,配置如下: appendfsync yes appendfsync always #每次有資料修改發生時都會寫入AOF檔案。 appendfsync everysec #每秒鐘同步一次,該策略為AOF的預設策略。 AOF 可以做到全程持久化,只需要在配置中開啟 appendonly yes。這樣 Redis 每執行一個修改資料的命令,都會把它新增到 AOF 檔案中,當 Redis 重啟時,將會讀取 AOF 檔案進行重放,恢復到 Redis 關閉前的最後時刻。 主從複製 主負責寫操作,從節點提供讀操作。 過程:
- 從節點執行 slaveof [masterIP] [masterPort],儲存主節點資訊。
- 從節點中的定時任務發現主節點資訊,建立和主節點的 Socket 連線。
- 從節點發送 Ping 訊號,主節點返回 Pong,兩邊能互相通訊。
- 連線建立後,主節點將所有資料傳送給從節點(資料同步)。
- 主節點把當前的資料同步給從節點後,便完成了複製的建立過程。接下來,主節點就會持續的把寫命令傳送給從節點,保證主從資料一致性。
上面是 Psync 的執行流程,從節點發送 psync[runId][offset] 命令,主節點有三種響應:
- FULLRESYNC:第一次連線,進行全量複製
- CONTINUE:進行部分複製
- ERR:不支援 psync 命令,進行全量複製
- 從節點發送 psync ? -1 命令(因為第一次傳送,不知道主節點的 runId,所以為?,因為是第一次複製,所以 offset=-1)。
- 主節點發現從節點是第一次複製,返回 FULLRESYNC {runId} {offset},runId 是主節點的 runId,offset 是主節點目前的 offset。
- 從節點接收主節點資訊後,儲存到 info 中。
- 主節點在傳送 FULLRESYNC 後,啟動 bgsave 命令,生成 RDB 檔案(資料持久化)。
- 主節點發送 RDB 檔案給從節點。到從節點載入資料完成這段期間主節點的寫命令放入緩衝區。
- 從節點清理自己的資料庫資料。
- 從節點載入 RDB 檔案,將資料儲存到自己的資料庫中。如果從節點開啟了 AOF,從節點會非同步重寫 AOF 檔案。
- 一旦主節點宕機,從節點晉升為主節點,同時需要修改應用方的主節點地址,還需要命令所有從節點去複製新的主節點,整個過程需要人工干預。
- 主節點的寫能力受到單機的限制。
- 主節點的儲存能力受到單機的限制。
- 原生複製的弊端在早期的版本中也會比較突出,比如:Redis 複製中斷後,從節點會發起 psync。此時如果同步不成功,則會進行全量同步,主庫執行全量備份的同時,可能會造成毫秒或秒級的卡頓。
Redis Sentinel(哨兵)主要功能包括主節點存活檢測、主從執行情況檢測、自動故障轉移、主從切換。 哨兵系統可以執行以下四個任務:
- 監控:不斷檢查主伺服器和從伺服器是否正常執行。
- 通知:當被監控的某個 Redis 伺服器出現問題,Sentinel 通過 API 指令碼向管理員或者其他應用程式發出通知。
- 自動故障轉移:當主節點不能正常工作時,Sentinel 會開始一次自動的故障轉移操作,它會將與失效主節點是主從關係的其中一個從節點升級為新的主節點,並且將其他的從節點指向新的主節點,這樣人工干預就可以免了。
- 配置提供者:在 Redis Sentinel 模式下,客戶端應用在初始化時連線的是 Sentinel 節點集合,從中獲取主節點的資訊。
- 當需要增加節點時,只需要把其他節點的某些雜湊槽挪到新節點就可以了;
- 當需要移除節點時,只需要把移除節點上的雜湊槽挪到其他節點就行了。
Pipeline redis客戶端執行一條命令的過程: 1 傳送命令 2 命令排隊 3 命令執行 4 返回結果 1-4的過程時間被稱為RTT 往返時間 redis提供批量操作命令mget、mset,可以有效地節約RTT(redis往返時間)。但是大部分命令都不支援批量操作。Pipeline能改善這個問題。 Pipeline能夠將命令進行組裝,一次RTT傳輸給Redis,再按這組命令的執行結果按順序返回給客戶端。 PS:pipeline不是原子操作,實際是多個命令 Bitmaps 本節將每個獨立使用者是否訪問過網站存放在Bitmaps中,將訪問的使用者記做1,沒有訪問的使用者記做0,用偏移量作為使用者的id。 setbit key offset value getbit key offset 統計bitcount key [startoffset] [endoffset] 運算bitop [and|or|not|xor] 目標key 運算key1 運算key2... GEO 1 新增位置經緯度 geoadd key 經度 緯度 地址資訊 [經度 緯度 地址資訊...] 2 獲取地理位置資訊 geopos key 地址資訊 [地址資訊...] 3 獲取兩個地理位置的距離 geolist key 地址資訊1 地址資訊2 單位(m:米,km:公里,mi:英里,ft:尺) 4 獲取指定位置範圍內的地理資訊位置集合 georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key] georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key] georadius和georadiusbymember兩個命令的作用是一樣的,都是以一個地 理位置為中心算出指定半徑內的其他地理資訊位置,不同的是georadius命令 的中心位置給出了具體的經緯度,georadiusbymember只需給出成員即可。 5 獲取geohash redis使用geohash將二維經緯度轉換為一維字串,字串越長表示位置更精準 6 刪除地址資訊 zrem key 地址資訊