天天看點

Redis個人總結

1、Redis簡介

Redis 是一個高性能的key-value資料庫。

Redis特點

  • Redis支援資料的持久化,可以将記憶體中的資料儲存在磁盤中,重新開機的時候可以再次加載進行使用。
  • Redis不僅僅支援簡單的key-value類型的資料,同時還提供list,set,zset,hash等資料結構的存儲。
  • Redis支援資料的備份,即master-slave模式的資料備份。

Redis 優勢

  • 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
  • 豐富的資料類型 – Redis支援二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 資料類型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。
  • 豐富的特性 – Redis還支援 publish/subscribe, 通知, key 過期等等特性。

2、Redis資料結構

字元串string

Redis中的字元串使用SDS來實作的,它的結構基本和C中的字元串結構類似,隻是增加了free和len兩個int值來儲存char數組中的未使用空間和字元串長度。

優點:擷取字元串長度的時間複雜度從O(n)降到O(1);避免對字元串的修改時,進行頻繁的記憶體重配置設定;相容部分C字元串的函數。

缺點:占用較多的記憶體空間(SDS提供了釋放未使用空間的方法)。

對SDS進行字元串增長操作時,會使用空間預配置設定的政策,配置設定一塊未使用的空間,進而減少連續執行字元串增長操作所需的記憶體重配置設定次數。

常用指令:GET、SET、DEL

可以存儲的值: 字元串、整數、浮點數

清單list

Redis中使用雙向連結清單quicklist來實作清單資料結構。

同樣,quicklist中也增加了len字段來表示連結清單長度,減少擷取長度的時間複雜度。

常用指令:LPUSH、RPUSH、LPOP、RPOP、LINDEX、LRANGE、LLEN、LTRIM

阻塞式指令:BLPOP、BRPOP、RPOPLPUSH、BRPOPLPUSH

對于阻塞彈出和彈出并推入指令,最常見的使用場景就是消息傳遞和任務隊列。

通常情況下,預設會配置使用ziplist(壓縮清單)來代替清單,可以大大減少記憶體使用率。

集合set

Redis的集合用來儲存無序的、不重複的元素。使用intset和dict這兩種資料結構來實作。

常用指令:SADD、SREM、SISMEMBER、SCARD、SMEMBERS、SMOVE

多集合指令:SDIFF、SINTER、SUNION、SDIFFSTORE

當set中的元素都是整型且元素數目較少時,set優先使用intset(整數集合)作為底層資料結構,否則使用dict作為底層資料結構(dict的value是NULL)。

時間複雜度:intset是O(log n),dict是O(1)。但是intset能節省記憶體,且在元素數量較少的情況下,性能差距不大。

散列hash

Redis的散列使用dict(哈希表,具體實作細節後面詳述)來實作。

常用指令:HMSET、HMGET、HDEL、HLEN、HEXISTS、HKEYS、HVALS、HGETALL

有序集合zset

有序集合和散列一樣,都用于存放鍵值對:有序集合的鍵稱為member,值稱為score。是唯一一個既可以根據member通路元素,有可以根據score通路元素的結構。

Redis的有序集合使用skiplist(跳躍表)來實作。

Redis的skiplist和普通skiplist的主要差別是:為每個節點增加一個高度為1的後退指針,用于從表尾方向向表頭方向疊代;score值允許重複。

常用指令:ZADD、ZREM、ZCARD、ZCOUNT、ZRANK、ZSCORE、ZRANGE、ZINCRBY

3、擴充資料結構

Redis如何定位到key

Redis相當于一個巨大的hash結構,是以所有key的查找都是通過dict來實作的。

dict結構

和一般實作hash表的結構類似,redis的dict也采用了數組加連結清單的方式存儲hash表資料。ht對象儲存了數組的大小size(一定是2的指數),用于将哈希值映射到table位置的sizemask(總是等于size-1),還有已存放的對象個數used。

然而,redis中一個dict存在2個上述這種對象ht[2],用來實作rehash,并在存放了一個rehashidx來表示目前rehash的進度。

一般的,當rehash為-1(即沒有在rehash過程中),則隻需要在ht[0]中查找資料,此時也隻有ht[0]中存在資料,ht[1]為空。

如果查找資料時,發現正在rehash過程中,則先推動一次rehash(redis的rehash過程不是一次性完成的,是分步進行,每步最多執行10個數組的rehash工作),然後再從ht[0]中查找,如果存在,則直接傳回。否則,目前還在rehash時,再從ht[1]中繼續查找。

同理,插入和删除都會推動一步rehash動作。當rehash結束時,會将ht[1]直接複制給ht[0],然後将ht[1]清空。

優點:這種rehash方式,将rehash帶來的龐大計算量分攤到對字典的給個添加、删除和查找操作上,避免了服務在rehash時長時間卡頓。

缺點:占用了一部分的記憶體空間;在rehash時,進行添加、删除和查找操作會降低部分性能。

壓縮清單ziplist

壓縮清單是Redis為了節約記憶體鎖産生的。使用限制條件:單節點大小和節點個數。

quicklist存儲方式:連結清單的每個節點,都會帶有指向連結清單前後節點的2個指針,這樣大大浪費了記憶體空間。

Redis個人總結

ziplist存儲方式:是由一系列特殊編碼的連續記憶體塊組成的。zlbytes表示整個壓縮清單的占用位元組數;zltail表示壓縮清單尾節點到起始節點的偏移量,可以直接定位到尾結點;zllen表示壓縮清單的節點數量(隻能記錄小于65535,如果等于65535,則需要周遊整個清單才能計算出壓縮清單的長度);entry表示清單的各個節點;zlend表示壓縮清單的末端。

Redis個人總結

壓縮清單節點組成:previous_entry_length表示前一個節點的長度,如果長度能夠使用1個位元組儲存,則就使用一個位元組儲存,否則使用5個位元組儲存(第一個位元組會被填充為全1);encoding表示目前節點資料的類型和長度,值的最高兩位表示位元組數組的編碼(11表示整數,其餘表示位元組數組),整數類型固定使用一個位元組表示,位元組數組類型可分為1、2、5位元組表示;content表示節點資料,可以是一個位元組數組或者整數。

Redis個人總結

周遊:正向周遊使用壓縮清單節點中的encoding字段讀取目前節點長度,然後直接操作指針讀取下一個節點;反向周遊使用壓縮清單節點中的previous_entry_length字段讀取前一個節點的長度,然後操作指針讀取前一個節點。

連鎖更新:由于previous_entry_length表示前一個節點的長度,且該字段為1個或5個位元組儲存。是以當在連結清單中(非表尾)添加或者删除一個節點時,都會導緻該操作節點的後一個節點的previous_entry_length所占用的位元組數發生變化,進而導緻連結清單後面的節點都有可能需要重新配置設定previous_entry_length屬性的空間大小,形成了連鎖更新。由于使用壓縮清單的限制,是以性能上并不會造成太大的影響。(注:Redis為了性能問題,不會将5位元組的空間重新配置設定為1位元組的)

整數集合intset

由于使用dict需要花費較大的記憶體,Redis使用intset來儲存一定數量内的純整數集合。

intset結構:encoding表示編碼方式,即congtents中元素的最大編碼方式;length表示元素數量;contents表示元素數組,數組中的數值按值的大小從小到大有序的排列。

查詢值使用二分查找實作。

更新:當intset中加入一個新元素,且該新元素比現有所有元素的類型都要長時,intset需要先進行更新,然後才能将新元素添加到intset中。具體步驟如下:

  1. 根據新元素的類型, 擴充整數集合底層數組的空間大小, 并為新元素配置設定空間。
  2. 将底層數組現有的所有元素都轉換成與新元素相同的類型, 并将類型轉換後的元素放置到正确的位上, 而且在放置元素的過程中, 需要繼續維持底層數組的有序性質不變。
  3. 将新元素添加到底層數組裡面。(新元素要麼最大,要麼最小,是以位置隻有頭尾兩種)

    intset不支援降級。

4、資料安全

持久化

快照持久化:snapshotting。Redis調用fork建立子程序,然後子程序負責将快照資料寫入到硬碟中,父程序繼續處理請求。特點:從快照恢複速度快,快照大小比較固定,友善不同伺服器之前傳輸;快照寫入到硬碟的時間較長,下一次快照寫入之前都不能保證這段時間的資料安全性。

AOF持久化:append-only file。選項:always表示每隔寫指令都會寫入硬碟中,性能很差;everysec表示每秒同步一次AOF檔案,性能和no相差無幾,最多丢失1秒内的資料;no表示讓作業系統來決定何時進行同步,丢失不定數量的資料。特點:寫入到硬碟時間短,間隔短,最多隻丢失一定數量的資料;AOF檔案可能會過大(可以使用子程序來做重寫和壓縮AOF的工作)。

複制

開啟複制:隻需要從伺服器配置slaveof host port即可。不支援主主複制。

關閉服務:隻需要從伺服器配置slaveof no one即可。

從伺服器連接配接主伺服器時的步驟:

Redis個人總結

主從鍊:多個從伺服器連接配接一個主伺服器時,可能會讓主伺服器生成多個快照檔案,導緻主伺服器性能下降,是以推薦使用主從鍊來控制連接配接到主伺服器的數量。

事務

Redis事務以特殊指令MULTI開始,之後使用者傳入多個指令,最後以EXEC指令為結束。Redis在接收到EXEC指令之前不會執行任何實際的操作,是以使用者無法根據讀取到的資料來做決定。

很多Redis用戶端在執行事務時,會将MULTI,使用者指令和EXEC指令一起全部發送給Redis,這樣子可以提升執行的性能。

WATCH:該指令會對鍵進行監控,如果在使用者執行EXEC之前,有其他用戶端對該監視的鍵進行了寫操作,那麼使用者支援EXEC時将會傳回錯誤。

UNWATCH:在WATCH指令後,取消對該鍵的監視。

DISCARD:在MULTI和EXEC指令之間,取消WATCH指令。并清空所有事務隊列中的指令。

5、叢集

分片

為什麼要有分片:1、redis存儲上限由單台主機的記憶體大小限制;2、redis性能将由單台主機的cpu限制。分片技術可以通過使用多台主機來存儲更大的資料量,可以通過将計算任務分散到不同的主機來提升計算性能。

分片方式:

用戶端分片:由用戶端計算分片确定需要連接配接哪一個redis執行個體(Jedis)

代理分片:由代理去計算分片,連接配接相應的redis執行個體,将結果傳回給用戶端(twemproxy)

redis伺服器分片:用戶端随機通路一個redis執行個體,由這個redis執行個體将請求轉發給正确的redis執行個體,或者讓用戶端重定向到正确的redis執行個體。(Redis Cluster)

缺點:由于多個鍵可能被分片到不同的reids執行個體,無法支援多鍵操作,如集合交集SINTER;Redis事務中涉及到多個鍵時,也将不可以使用;在擴縮容時,操作比較複雜。

sentinel

sentinel是一種特殊的redis伺服器,它能監控多個master-slave叢集,發現master當機後能進行自動的切換。

一般的,會使用一個sentinel叢集來管理redis叢集,來增加穩定性。

sentinel管理master-slave資訊

sentinel在啟動時,會讀取配置檔案中的master資訊,并在主備切換後修改該配置檔案; slave節點的資訊可以從master節點中擷取(INFO指令)。

sentinel管理用戶端連接配接

用戶端周遊sentinel節點清單,擷取一個可用的sentinel節點;擷取該節點上配置的master資訊;用戶端驗證是否是master節點;正常連接配接到master節點;master發生變化時,sentinel向用戶端訂閱的頻道publish一條消息(釋出訂閱模式),讓用戶端重新擷取master連接配接。

sentinel互相發現

每個sentinel都會向master的hello管道中,每秒發送一次自己的配置資訊,來宣布它的存在。同樣也會訂閱hello管道的内容,如果有新的sentinel,則加入到自身維護的master監控清單中。如果新的sentinel的配置版本比自身的高,則使用該配置更新自己的master配置。

sentinel觸發failover

一個sentinel節點每隔一段時間都會向master發送心跳PING來确認master是否存活,如果master在一定時間範圍内沒有回複PONG或者回複了錯誤的消息,那麼這個sentinel就會主觀認為這個master不可用了。不過需要注意的是,這個時候sentinel并不會馬上進行failover主備切換,這個sentinel還需要參考sentinel叢集中其他sentinel的意見(使用Gossip協定來判斷),如果超過某個數量的sentinel也主觀地認為該master死了,那麼這個master就會被客觀地認為已經死了(ODOWN),此時将會觸發故障恢複流程(具體細節在後面詳述)。

RedisCluster

Redis個人總結

Redis Cluster是redis的分布式解決方案,是一個去中心化的多主叢集,每一個主都負責一部分資料(稱之為slot)。節點之間使用Gossip協定進行通信。

優點:

  1. 資料自動分片:每個節點負責一定數量的slot,每個key都會映射到一個slot上。
  2. 提供hash tag功能:将多個不同的key映射到相同的slot上,友善進行多key操作,隻要在key中包含"{}",那麼在計算hash時,隻會計算{}中的字元串。
  3. 自動故障恢複:自動檢測失效的節點,并選舉該主節點的其中一個從節點作為新的master。還支援手動故障恢複。
  4. 靈活擴縮容:靈活增加删除節點,自動完成slot的遷移。線性擴充到1000節點。

缺點:

  1. 由于使用Gossip協定進行通信,那麼叢集的資料将無法保證強一緻性,隻能保證最終一緻性。
  2. 是以讀寫分離将無法實作,所有從節點的讀都會重定向到key對應的slot主節點上。
  3. 隻支援單層複制,不支援主從鍊。
  4. 不支援節點自動發現,必須手動廣播meet消息。
  5. 雖然使用了hash tag對多鍵操作進行了支援,但是在slot遷移時仍然無法支援多鍵操作。
  6. PUBLISH指令會向所有節點進行廣播,加重了帶寬負擔。

查找key的流程:

  1. 通過hash算法計算出key所在的slot;
  2. 在節點的clusterState中找到該slot被負責的node節點。
  3. 如果不是本節點,則傳回MOVED重定向到指定節點。
  4. 在節點上查找該key,若找到,則傳回該key的結果。
  5. 如果未找到,則判斷該slot是否在MIGRATING,如果是,則ASK重定向到指定節點。
  6. 否則判斷該slot是否在IMPORTING,如果是且有ASKING标記,則在該節點查找。
  7. 否則傳回未找到。

Jedis連接配接Redis Cluster:

Jedis緩存了對每個master節點的連接配接池,和每個slot對應的連接配接池。是以Jedis在執行指令時,會做CRC16算法計算slot,并通過對應的連接配接池擷取連接配接。直接執行指令,如果該連接配接異常了,則會從所有連接配接中随機擷取一個,來重新擷取連接配接後,并重新整理連接配接池(預設重試5次)。

如果發生了重定向異常,如果傳回的是 moved,則重新整理連接配接池。如果是 ASK,則不重新整理連接配接池,在下次遞歸中直接使用 ASK 傳回的資訊進行調用。下次遞歸時,先執行 asking 指令打開新的用戶端連接配接,如果成功,則執行真正的指令。

和sentinel叢集不同的是,Redis Cluster并不會通知master的變化,是以Jedis在連接配接失敗或者被MOVED重定向時,再去更新slot對應的master的連接配接池資訊。

故障恢複:

故障發現的流程和sentinel類似,都是故障節點沒有在指定時間内回複給主節點PONG消息,或者回複了錯誤的消息。主節點就會向其他節點廣播pfail狀态,當半數以上主節點都判斷該節點故障時,就會向叢集中廣播fail消息,開啟故障恢複流程。

當故障的是帶有slot的主節點時,将會從該主節點的從節點清單中選出一個新的主節點。

選出新的主節點後,會将該從節點取消複制變為主節點,并将故障主節點負責的slot進行删除,将這些slot委派給自己。最後向叢集廣播PONG消息,通知其他節點目前從節點變為主節點和接管slot的資訊。

Redis Cluster的選舉過程和sentinel十分相似,隻是Redis Cluster中隻有master節點才有投票的權利,而且被選舉的也隻能是故障節點的從節點。

6、其他

鍵過期

Redis使用EXPIRE指令來給鍵設定過期時間。

Redis底層會給這個鍵設定一個逾時時間(一個絕對的unix時間戳,修改系統時間将會有影響)。

常用指令:EXPIRE、PERSIST、TTL。

過期删除政策:使用定期删除政策,定時任務每次檢查20個key,如果找到已經過期的key,則删除。而且當已經過期的key數量比較多時,則會進入到快速清理模式(定期删除時間間隔變短)。如果在使用者查詢或者設定值時,這個key若已經過期,則會直接删除該key。

slave不會主動觸發鍵過期,會等待master過期删除的DEL指令,來删除slave中的過期鍵;在master同步DEL指令之前,查詢slave上的過期鍵将會被告知是不存在的(其實是存在的)。

訂閱/釋出

頻道訂閱:SUBSCRIBE、UNSUBSCRIBE

底層實作:儲存了一個dict,dict中的鍵為channel,值為訂閱用戶端的連結清單;用戶端訂閱時,在連結清單中增加該用戶端,取消訂閱時,在連結清單中删除該用戶端。

模式訂閱:PSUBSCRIBE、PUNSUBSCRIBE

底層實作:帶有用戶端資訊和pattern的連結清單。

釋出:PUBLISH

底層實作:根據channel定位到dict中的用戶端連結清單,循環給這些用戶端發送消息。然後再周遊模式訂閱連結清單,檢視該channel是否比對pattern,如果是,則給用戶端資訊發送消息。

sentinel選舉

sentinel選舉使用Raft算法來實作,必須先保證該master處于ODOWN狀态才會觸發故障轉移。

Raft算法中有三個角色:Leader、Follower、Candidate。

正常運作狀态下沒有這些角色,當sentinel叢集運作過程中發生故障轉移時,才會使用這些角色,并讓自己成為Follower。

選舉流程:

  1. 如果一個sentinel認為master已經down的情況下(ODOWN),首先會判斷自己有沒有投過票,如果自己已經投過票給其他sentinel了,那麼在2被故障轉移逾時時間内,自己都會是Follower。
  2. 如果該sentinel沒有投過票,則它就成為Candidate。
  • 更新故障轉移狀态為start
  • 目前epoch加1,進入新的term
  • 向其他節點發送請求投票某節點down的指令,指令中攜帶自己的epoch。
  • 給自己投一票,即将自己的leader和leader_epoch改成自己和自己的epoch
  1. 其他sentinel收到Candidate的某節點down指令時,如果該sentinel已經投過票,判斷epoch是否比目前投票的epoch大,如果是,則投給該Candidate,否則忽略。否則投票給該Candidate。
  2. Candidate會不斷的統計自己的票數,直到發現票數超過一半且超過了配置的quorum。如果是,則成為Leader。否則,逾時後認為自己選舉失敗了,重新增加epoch開啟新一輪的投票。
  3. 成為Leader後,并不會像Raft那樣通知其他sentinel,而是從slave中選出master,master開始正常工作後。
  4. 隻要Candidate收到了新的master正常工作時,都會終止故障轉移流程。

慢日志

Redis自帶的慢日志記錄,可以通過slowlog-log-slower-than來設定執行時間超過多少微妙就會被認為是慢日志,slowlog-max-len來設定伺服器最多儲存多少條慢日志。

常用指令:SLOWLOG GET、SLOWLOG LEN、SLOWLOG RESET

7、應用

分布式鎖

Redis可以通過SETNX指令實作分布式鎖,SETNX是指當且僅當key不存在時,才會設定值。用戶端可以通過SETNX指令傳回成功與否來判斷是否擷取鎖成功。

為了避免用戶端當機,導緻該鎖沒有被及時清除而造成死鎖,建議給該key設定逾時時間。由于SETNX和EXPIRE需要使用事務來保證并發,是以Redis使用了SET指令代替:SET key value [EX seconds] [PX milliseconds] [NX|XX]。

隊列

可以使用清單資料結構的BLPOP和BRPOP等指令實作一個消息隊列。在可靠性需求不大的情況下,可以用來代替較重的MQ。

優先級隊列:BLPOP和BRPOP等指令支援傳入多個清單,會依次從清單中取值,知道取到值為止。是以可以使用多個不同優先級的隊列,将高優先級的清單放置在指令的左側即可。

8、緩存問題

緩存一緻性

問題:1、資料時效性要求很高;2、需要保證緩存中的資料與資料庫保持一緻;3、需要保證緩存節點和副本中的資料也保持一緻。

分析:主要問題在于,寫操作如何去處理redis和資料庫之間的一緻性,即緩存的更新政策。

解決方案:

  1. Cache Aside:在更新操作時,先寫入資料庫,然後将緩存中的值失效。這個政策使用最為普遍,但是也存在并發問題(一個是讀操作,但是沒有命中緩存,然後就到資料庫中取資料,此時來了一個寫操作,寫完資料庫後,讓緩存失效,然後,之前的那個讀操作再把老的資料放進去,是以,會造成髒資料)。但是由于寫操作比讀操作慢很多,是以這個場景出現的機率很小。
  2. Read/Write Through:讓緩存自己代理去更新資料庫。當緩存失效時,讀請求會導緻緩存服務主動加載資料,并将資料緩存和傳回給使用者。寫請求在緩存失效時,不操作緩存,直接修改資料庫。在命中緩存時,更新緩存,然後緩存服務自己更新資料庫。這種政策還是會存在并發問題,且實作方式比較複雜。
  3. Write Behind Caching:所有操作都隻寫緩存,不更新資料庫,緩存會異步地批量更新資料庫。缺點是資料不能保持一緻性,可能會存在丢失的情況。優點是性能高。

緩存并發

問題:緩存過期或者正在更新,同時有大量的并發請求該key。

分析:緩存過期時,大量請求落在DB上,可能導緻雪崩發生;緩存正在更新,大量請求擷取到的結果可能是更新前或者更新後的,導緻緩存一緻性問題。

解決方案:加入類似鎖的機制,在緩存過期或者更新的情況下,先嘗試擷取鎖,當更新或者從資料庫擷取完成後再釋放鎖,其他請求需要犧牲一些等待時間,即可從緩存中直接擷取資料。

緩存雪崩

問題:高并發時,多個熱點緩存同時更新或者過期導緻大量請求直接落在DB上,導緻DB崩潰。

分析:熱點緩存key在同一時間過期。

解決方案:設定過期時間時,最好将熱點緩存key在不同時間段過期。

緩存擊穿

問題:大量請求去查找一個不存在的key,導緻DB雪崩。

分析:處理不存在的key。

解決方案:

布隆過濾器:将所有key的哈希值都存放到足夠大的bitmap中,請求key時,先經過布隆過濾器。

設定指定值:為不存在的key設定一個指定的值,然後設定逾時時間。

大key優化

問題:一個key可能存放一個超大的字元串;一個key可能存放着很大的集合、散列、清單等。

分析:超大字元串讀取和寫入都會影響redis的性能;在一個集合、散列、清單中,大量的資料也會導緻redis的性能下降。

解決方案:對字元串或者集合、散列、清單進行分片(一般可以采用固定的質數來做hash),将單個資料結構的大小降下來。

繼續閱讀