redis 和 memcached 的區(qū)別
1. redis支持更豐富的數(shù)據(jù)類型(支持更復(fù)雜的應(yīng)用場(chǎng)景):Redis不僅僅支持簡(jiǎn)單的k/v類型的數(shù)據(jù),同時(shí)還提供 list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)。memcache支持簡(jiǎn)單的數(shù)據(jù)類型,String。
2. Redis支持?jǐn)?shù)據(jù)的持久化,可以將內(nèi)存中的數(shù)據(jù)保持在磁盤中,重啟的時(shí)候可以再次加載進(jìn)行使用,而 Memecache把數(shù)據(jù)全部存在內(nèi)存之中。
3. 集群模式:memcached沒有原生的集群模式,需要依靠客戶端來實(shí)現(xiàn)往集群中分片寫入數(shù)據(jù);但是 redis 目前 是原生支持 cluster 模式的.
4. Memcached是多線程,非阻塞IO復(fù)用的網(wǎng)絡(luò)模型;Redis使用單線程的多路 IO復(fù)用模型
布隆過濾器基本使用
布隆過濾器有二個(gè)基本指令,bf.add 添加元素,bf.exists 查詢?cè)厥欠翊嬖冢挠梅ê?set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一個(gè)元素,如果想要一次添加多個(gè),就需要用到 bf.madd 指令。同樣如果需要一次查詢多個(gè)元素是否存在,就需要用到 bf.mexists 指令。
統(tǒng)計(jì)和查找
Redis 提供了位圖統(tǒng)計(jì)指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統(tǒng)計(jì)指定位置范圍內(nèi) 1 的個(gè)數(shù),bitpos 用來查找指定范圍內(nèi)出現(xiàn)的第一個(gè) 0 或 1。
比如我們可以通過 bitcount 統(tǒng)計(jì)用戶一共簽到了多少天,通過 bitpos 指令查找用戶從哪一天開始第一次簽到。如果指定了范圍參數(shù)[start, end],就可以統(tǒng)計(jì)在某個(gè)時(shí)間范圍內(nèi)用戶簽到了多少天,用戶自某天以后的哪天開始簽到。
Redis命令
字符串命令
Redis字符串可以存儲(chǔ)3種類型的值:
字節(jié)串(byte string)
整數(shù)
浮點(diǎn)數(shù)
整數(shù)和浮點(diǎn)數(shù) 可以有INCR和DECR等命令
字符串可以有 APPEND、SUBSTR、SETRANGE、SETBIT
列表
Redis的列表允許用戶從序列的兩端推入或者彈出元素,獲取列表元素。
RPUSH、LPUSH、RPOP、LPOP、LINDEX、LRANGE、LTRIM,
也可以阻塞執(zhí)行命令的客戶端:BLPOP BRPOP RPOPLPUSH BRPOPLPUSH,可以當(dāng)作消息隊(duì)列使用。
集合
以無序方式來存儲(chǔ)多個(gè)不相同的元素
SADD SREM SISMEMBER SCARD SMEMBERS SRANDMEMBER SPOP SMOVE
散列
多個(gè)鍵值對(duì)存儲(chǔ)到一個(gè)Redis鍵里面
HMGET HMSET HDEL HLEN
有序集合
有序集合存儲(chǔ)了成員與分值的映射,并且提供了分值處理命令,以及根據(jù)分值大小有序地獲取(fetch)或掃描(scan)成員和分值的命令。
ZADD ZREM ZCARD ZINCRBY ZCOUNT ZRANK ZSCORE ZRANGE
Redis事務(wù)
基本事務(wù)需要用到MULTI和EXEC命令。Redis接收到MULTI命令,會(huì)把之后發(fā)送的所有命令都放到一個(gè)隊(duì)列里面,直到收到EXEC命令位置,Redis會(huì)在不被打斷的情況下,一個(gè)接一個(gè)執(zhí)行存儲(chǔ)在隊(duì)列里面的所有命令。可以由pipeline犯法實(shí)現(xiàn)。
過期與刪除
PERSIST:移除過期時(shí)間 TTL:查看剩余過期時(shí)間 EXPIRE:指定秒數(shù)后過期
數(shù)據(jù)安全與性能保障
Redis提供了兩種不同的持久化方法來將數(shù)據(jù)存儲(chǔ)到硬盤里面。一種叫快照(snapshotting),可以將存在于某一時(shí)間的數(shù)據(jù)都寫入到硬盤里面。另一種叫做只追加文件(append-only file,AOF)它會(huì)在執(zhí)行命令時(shí),將執(zhí)行的寫命令復(fù)制到硬盤里面。
創(chuàng)建快照有以下幾種方式:
客戶端發(fā)送BGSAVE創(chuàng)建一個(gè)快照(windows不支持),Redis會(huì)調(diào)用fork來創(chuàng)建一個(gè)子進(jìn)程來將快照寫入硬盤。(在Unix和類Unix系統(tǒng)上,進(jìn)程創(chuàng)建子進(jìn)程時(shí)父子進(jìn)程共享相同內(nèi)存,知道父進(jìn)程或子進(jìn)程寫入內(nèi)存后,對(duì)被寫入內(nèi)存共享才會(huì)結(jié)束)
客戶端發(fā)送SAVE命令創(chuàng)建快照,會(huì)阻塞進(jìn)程。沒有足夠內(nèi)存執(zhí)行BGSAVE可以考慮
用戶設(shè)置了save配置選項(xiàng),如save 60 10000。從最近一次創(chuàng)建快照之后當(dāng)“60秒內(nèi)有10000次寫入”就會(huì)觸發(fā)一次BGSAVE
Redis通過SHUTDOWN命令接收到關(guān)閉服務(wù)器命令,或者接受到標(biāo)準(zhǔn)TERM信號(hào)時(shí),會(huì)執(zhí)行SAVE
Redis連接到另一個(gè)Redis,并且想對(duì)方發(fā)送SYNC命令來開始一次復(fù)制操作,如果主服務(wù)器沒有執(zhí)行BGSAVE或者沒有剛執(zhí)行完BGSAVE,那么主服務(wù)器就會(huì)執(zhí)行BGSAVE。
fork創(chuàng)建子進(jìn)程
子進(jìn)程剛剛產(chǎn)生時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段。這時(shí)你可以將父子進(jìn)程想像成一個(gè)連體嬰兒,共享身體。這是 Linux 操作系統(tǒng)的機(jī)制,為了節(jié)約內(nèi)存資源,所以盡可能讓它們共享起來。在進(jìn)程分離的一瞬間,內(nèi)存的增長(zhǎng)幾乎沒有明顯變化。
子進(jìn)程做數(shù)據(jù)持久化,它不會(huì)修改現(xiàn)有的內(nèi)存數(shù)據(jù)結(jié)構(gòu),它只是對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷讀取,然后序列化寫到磁盤中。但是父進(jìn)程不一樣,它必須持續(xù)服務(wù)客戶端請(qǐng)求,然后對(duì)內(nèi)存數(shù)據(jù)結(jié)構(gòu)進(jìn)行不間斷的修改。
這個(gè)時(shí)候就會(huì)使用操作系統(tǒng)的 COW (Copy On Write,寫時(shí)復(fù)制)機(jī)制來進(jìn)行數(shù)據(jù)段頁(yè)面的分離。數(shù)據(jù)段是由很多操作系統(tǒng)的頁(yè)面組合而成,當(dāng)父進(jìn)程對(duì)其中一個(gè)頁(yè)面的數(shù)據(jù)進(jìn)行修改時(shí),會(huì)將被共享的頁(yè)面復(fù)制一份分離出來,然后對(duì)這個(gè)復(fù)制的頁(yè)面進(jìn)行修改。這時(shí)子進(jìn)程相應(yīng)的頁(yè)面是沒有變化的,還是進(jìn)程產(chǎn)生時(shí)那一瞬間的數(shù)據(jù)。
隨著父進(jìn)程修改操作的持續(xù)進(jìn)行,越來越多的共享頁(yè)面被分離出來,內(nèi)存就會(huì)持續(xù)增長(zhǎng)。但是也不會(huì)超過原有數(shù)據(jù)內(nèi)存的 2 倍大小。另外一個(gè) Redis 實(shí)例里冷數(shù)據(jù)占的比例往往是比較高的,所以很少會(huì)出現(xiàn)所有的頁(yè)面都會(huì)被分離,被分離的往往只有其中一部分頁(yè)面。每個(gè)頁(yè)面的大小只有 4K,一個(gè) Redis 實(shí)例里面一般都會(huì)有成千上萬的頁(yè)面。
如果Redis進(jìn)程占用了20GB的內(nèi)存,在標(biāo)準(zhǔn)硬件上運(yùn)行BGSAVE所創(chuàng)建的子進(jìn)程將導(dǎo)致Redis停頓200-400毫秒。
AOF持久化
可以設(shè)置 appendonly yes 配置選項(xiàng)來打開 AOF持久化。
appendfsync有三個(gè)選項(xiàng) always(每個(gè)寫命令都同步) everysec(每秒)no(不顯式寫)
Redis會(huì)不斷將寫命令記錄到AOF,隨著不斷運(yùn)行體積會(huì)不斷增長(zhǎng),重啟之后需要重新執(zhí)行AOF文件記錄還原數(shù)據(jù)集,太大的AOF文件可能導(dǎo)致還原操作執(zhí)行很長(zhǎng)。可以使用BGREWRITEAOF命令移除AOF文件中的冗余命令重寫AOF文件。
復(fù)制(replication,MS)
從服務(wù)器指定 slaveof host port的配置文件,發(fā)送SLAVEOF no one命令終止復(fù)制主服務(wù)器。發(fā)送SLAVEOF host port命令開始復(fù)制新服務(wù)器。
復(fù)制的過程:
1. 主:等待命令進(jìn)入,從:連接(或重連)主服務(wù)器,發(fā)送SYNC命令
2. 主:執(zhí)行BGSAVE,并使用緩沖區(qū)記錄BGSAVE之后執(zhí)行的寫命令,從:根據(jù)配置決定是繼續(xù)使用現(xiàn)有數(shù)據(jù)還是返回錯(cuò)誤
3. 主:BGSAVE執(zhí)行完畢,快照發(fā)送給從服務(wù)器,并繼續(xù)用緩沖區(qū)記錄寫命令,從:丟棄舊數(shù)據(jù)載入快照文件
4. 主:快照文件發(fā)送完畢,發(fā)送緩沖區(qū)寫命令,從:解釋快照文件,正常執(zhí)行
5. 主:緩沖區(qū)寫命令發(fā)送完畢,每個(gè)寫命令同步給服務(wù)器,從:執(zhí)行所有寫命令
主從鏈(master/slave chaining)從服務(wù)器可以有自己的從服務(wù)器。從服務(wù)器1加載快照文件時(shí)中斷從服務(wù)器2的連接,從服務(wù)器2需要重新連接并重新同步。可以考慮多層主從鏈樹
為了驗(yàn)證主服務(wù)器是否已經(jīng)將寫數(shù)據(jù)發(fā)送至從服務(wù)器,用戶需要在向主服務(wù)器寫入數(shù)據(jù)后再寫一個(gè)唯一的虛構(gòu)值(unique dummy value),通過檢查虛構(gòu)值是否存在與從服務(wù)器來判斷寫數(shù)據(jù)是否已經(jīng)到達(dá)從服務(wù)器。
若要檢查是否寫入了硬盤中,可以檢查INFO命令的輸出結(jié)果中aof_pending_bio_fsync屬性值是否為0,如果是表示服務(wù)器已經(jīng)將所有已知的數(shù)據(jù)保存到硬盤。
發(fā)生故障后快照文件和AOF文件可以使用命令檢查(redis-check-aof redis-check-dump),修復(fù)AOF就是丟棄出錯(cuò)命令以及之后的所有命令。快照無法修復(fù)。
更換主從服務(wù)器,主服務(wù)器掛了,從服務(wù)器SAVE生成快照,發(fā)送給新主加載完成后,從服務(wù)器執(zhí)行SLAVEOF指定新主。(或者后面使用哨兵模式 Redis Sentinel)
事務(wù)
Redis的事務(wù)相關(guān)命令包括 WATCH MULTI/EXEC UNWATCH/DISCARD。
用戶使用WATCH監(jiān)視一個(gè)或多個(gè)鍵,接著使用MULTI開始一個(gè)新事務(wù),多個(gè)命令入隊(duì),可以發(fā)送DISCARD取消WATCH并清空所有已入隊(duì)命令。
也可以使用非事務(wù)型流水線(non-transactional pipeline)。執(zhí)行pipleline傳入true做參數(shù)表示使用事務(wù),傳入false表示無需事務(wù)。
分布式鎖
使用SETNX命令實(shí)現(xiàn)鎖的獲取功能,這個(gè)命令只會(huì)在鍵不存在的情況下為鍵設(shè)置值。
降低占用內(nèi)存
三種方式降低Redis占用:
短結(jié)構(gòu)(short structure)
分片結(jié)構(gòu)(shared structure)
打包存儲(chǔ)二進(jìn)制位和字節(jié)
短結(jié)構(gòu)
Redis為列表、集合、散列和有序集合提供了一組配置選項(xiàng),這些選項(xiàng)可以讓Redis以更節(jié)約空間的方式存儲(chǔ)長(zhǎng)度較短的結(jié)構(gòu)。Redis可以選擇使用一種名為壓縮列表(ziplist)和緊湊存儲(chǔ)方式來存儲(chǔ)這些結(jié)構(gòu)。壓縮列表是列表、散列、和有序集合這3種不同類型的對(duì)象的一種非結(jié)構(gòu)化(unstructured)表示,以序列化方式存儲(chǔ)數(shù)據(jù),這些序列化數(shù)據(jù)每次被讀取的時(shí)候都要進(jìn)行解碼,每次被寫入的時(shí)候都要進(jìn)行局部的重新編碼,并且可能需要對(duì)內(nèi)存里面的數(shù)據(jù)進(jìn)行移動(dòng)。
Redis用雙鏈表表示列表、散列表表示散列、散列表加上跳躍表(skiplist)表示有序集合。
鍵名盡量簡(jiǎn)短
分片結(jié)構(gòu)
使用 namespace:id 這樣的字符串鍵去存儲(chǔ)短字符串或者計(jì)數(shù)器,能夠有效降低存儲(chǔ)這些數(shù)據(jù)所需的內(nèi)存。
打包存儲(chǔ)二進(jìn)制位和字節(jié)
高效打包和更新Redis字符串的4個(gè)命令,分別是
GETRANGE、SETRANGE、GETBIT、SETBIT。
擴(kuò)展Redis
Redis Sentinel可以配合Redis的復(fù)制功能使用,對(duì)下線的主服務(wù)器進(jìn)行故障轉(zhuǎn)移。當(dāng)主服務(wù)器失效的時(shí)候,見識(shí)這個(gè)主服務(wù)器的所有Sentinel就會(huì)基于彼此共有的信息選出一個(gè)Sentinel,并從現(xiàn)有的從服務(wù)器當(dāng)中選出一個(gè)新的主服務(wù)器。當(dāng)被選中的從服務(wù)器轉(zhuǎn)換為主服務(wù)器之后,那個(gè)被選中的Sentinel就會(huì)讓剩余的其他服務(wù)器去復(fù)制這個(gè)新的主服務(wù)器(默認(rèn)Sentinel會(huì)一個(gè)一個(gè)遷移從服務(wù)器,可以通過配置選項(xiàng)進(jìn)行修改)
Redis的Lua腳本編程
使用EVAL和EVALSHA命令執(zhí)行Lua腳本
Lua腳本和單個(gè)Redis命令以及“MULTI/EXEC”事務(wù)一樣,都是原子操作
已經(jīng)對(duì)結(jié)構(gòu)進(jìn)行了修改的Lua腳本無法中斷
內(nèi)存淘汰策略
不同于之前的版本,redis5.0為我們提供了八個(gè)不同的內(nèi)存置換策略。很早之前提供了6種。
(1)volatile-lru:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選最近最少使用的數(shù)據(jù)淘汰。
(2)volatile-ttl:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選將要過期的數(shù)據(jù)淘汰。
(3)volatile-random:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰。
(4)volatile-lfu:從已設(shè)置過期時(shí)間的數(shù)據(jù)集挑選使用頻率最低的數(shù)據(jù)淘汰。
(5)allkeys-lru:從數(shù)據(jù)集中挑選最近最少使用的數(shù)據(jù)淘汰
(6)allkeys-lfu:從數(shù)據(jù)集中挑選使用頻率最低的數(shù)據(jù)淘汰。
(7)allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰
(8) no-eviction(驅(qū)逐):禁止驅(qū)逐數(shù)據(jù),這也是默認(rèn)策略。意思是當(dāng)內(nèi)存不足以容納新入數(shù)據(jù)時(shí),新寫入操作就會(huì)報(bào)錯(cuò),請(qǐng)求可以繼續(xù)進(jìn)行,線上任務(wù)也不能持續(xù)進(jìn)行,采用no-eviction策略可以保證數(shù)據(jù)不被丟失。
這八種大體上可以分為4中,lru、lfu、random、ttl。
LRU淘汰
LRU(Least recently used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進(jìn)行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。
在服務(wù)器配置中保存了 lru 計(jì)數(shù)器 server.lrulock,會(huì)定時(shí)(redis 定時(shí)程序 serverCorn())更新,server.lrulock 的值是根據(jù) server.unixtime 計(jì)算出來進(jìn)行排序的,然后選擇最近使用時(shí)間最久的數(shù)據(jù)進(jìn)行刪除。另外,從 struct redisObject 中可以發(fā)現(xiàn),每一個(gè) redis 對(duì)象都會(huì)設(shè)置相應(yīng)的 lru。每一次訪問數(shù)據(jù),會(huì)更新對(duì)應(yīng)redisObject.lru
在Redis中,LRU算法是一個(gè)近似算法,默認(rèn)情況下,Redis會(huì)隨機(jī)挑選5個(gè)鍵,并從中選擇一個(gè)最久未使用的key進(jìn)行淘汰。在配置文件中,按maxmemory-samples選項(xiàng)進(jìn)行配置,選項(xiàng)配置越大,消耗時(shí)間就越長(zhǎng),但結(jié)構(gòu)也就越精準(zhǔn)。
TTL淘汰
Redis 數(shù)據(jù)集數(shù)據(jù)結(jié)構(gòu)中保存了鍵值對(duì)過期時(shí)間的表,即 redisDb.expires。與 LRU 數(shù)據(jù)淘汰機(jī)制類似,TTL 數(shù)據(jù)淘汰機(jī)制中會(huì)先從過期時(shí)間的表中隨機(jī)挑選幾個(gè)鍵值對(duì),取出其中 ttl 最大的鍵值對(duì)淘汰。同樣,TTL淘汰策略并不是面向所有過期時(shí)間的表中最快過期的鍵值對(duì),而只是隨機(jī)挑選的幾個(gè)鍵值對(duì)。
隨機(jī)淘汰:
在隨機(jī)淘汰的場(chǎng)景下獲取待刪除的鍵值對(duì),隨機(jī)找hash桶再次hash指定位置的dictEntry即可。
Redis中的淘汰機(jī)制都是幾近于算法實(shí)現(xiàn)的,主要從性能和可靠性上做平衡,所以并不是完全可靠,所以開發(fā)者們?cè)诔浞至私釸edis淘汰策略之后還應(yīng)在平時(shí)多主動(dòng)設(shè)置或更新key的expire時(shí)間,主動(dòng)刪除沒有價(jià)值的數(shù)據(jù),提升Redis整體性能和空間。
多路復(fù)用
在 I/O 多路復(fù)用模型中,最重要的函數(shù)調(diào)用就是 select,該方法的能夠同時(shí)監(jiān)控多個(gè)文件描述符的可讀可寫情況,當(dāng)其中的某些文件描述符可讀或者可寫時(shí),select 方法就會(huì)返回可讀以及可寫的文件描述符個(gè)數(shù)。
關(guān)于 select 的具體使用方法,在網(wǎng)絡(luò)上資料很多,這里就不過多展開介紹了;
與此同時(shí)也有其它的 I/O 多路復(fù)用函數(shù) epoll/kqueue/evport,它們相比 select 性能更優(yōu)秀,同時(shí)也能支撐更多的服務(wù)。
Reactor 設(shè)計(jì)模式
Redis 服務(wù)采用 Reactor 的方式來實(shí)現(xiàn)文件事件處理器(每一個(gè)網(wǎng)絡(luò)連接其實(shí)都對(duì)應(yīng)一個(gè)文件描述符)
文件事件處理器使用 I/O 多路復(fù)用模塊同時(shí)監(jiān)聽多個(gè) FD,當(dāng) accept、read、write 和 close 文件事件產(chǎn)生時(shí),文件事件處理器就會(huì)回調(diào) FD 綁定的事件處理器。
雖然整個(gè)文件事件處理器是在單線程上運(yùn)行的,但是通過 I/O 多路復(fù)用模塊的引入,實(shí)現(xiàn)了同時(shí)對(duì)多個(gè) FD 讀寫的監(jiān)控,提高了網(wǎng)絡(luò)通信模型的性能,同時(shí)也可以保證整個(gè) Redis 服務(wù)實(shí)現(xiàn)的簡(jiǎn)單。
I/O 多路復(fù)用模塊
I/O 多路復(fù)用模塊封裝了底層的 select、epoll、avport 以及 kqueue 這些 I/O 多路復(fù)用函數(shù),為上層提供了相同的接口。
I/O 多路復(fù)用模型是利用select、poll、epoll可以同時(shí)監(jiān)察多個(gè)流的 I/O 事件的能力,在空閑的時(shí)候,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí),就從阻塞態(tài)中喚醒,于是程序就會(huì)輪詢一遍所有的流(epoll是只輪詢那些真正發(fā)出了事件的流),依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個(gè)網(wǎng)絡(luò)連接,“復(fù)用”指的是復(fù)用同一個(gè)線程。采用多路 I/O 復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請(qǐng)求(盡量減少網(wǎng)絡(luò)IO的時(shí)間消耗),且Redis在內(nèi)存中操作數(shù)據(jù)的速度非常快(內(nèi)存內(nèi)的操作不會(huì)成為這里的性能瓶頸),主要以上兩點(diǎn)造就了Redis具有很高的吞吐量。
Redis集群不需要sentinel哨兵也能完成節(jié)點(diǎn)移除和故障轉(zhuǎn)移的功能。需要將每個(gè)節(jié)點(diǎn)設(shè)置成集群模式,這種集群模式?jīng)]有中心節(jié)點(diǎn),可水平擴(kuò)展,據(jù)官方文檔稱可以線性擴(kuò)展到上萬個(gè)節(jié)點(diǎn)(官方推薦不超過1000個(gè)節(jié)點(diǎn))。redis集群的性能和高可用性均優(yōu)于之前版本的哨兵模式,且集群配置非常簡(jiǎn)單。
Redis無中心集群
Redis5之后提供了無中心的集群模式
Redis Cluseter 主要組件
key 分布模式,key空間分布被劃分為16384個(gè)slot,所以一個(gè)集群,主節(jié)點(diǎn)的個(gè)數(shù)最大為16384(一般建議master最大節(jié)點(diǎn)數(shù)為1000)
Cluster bus,每個(gè)節(jié)點(diǎn)有一個(gè)額外的TCP端口,這個(gè)端口用來和其他節(jié)點(diǎn)交換信息。這個(gè)端口一般是在與客戶端鏈接端口上面加10000,比如客戶端端口為6379,那么cluster bus的端口為16379.
cluster 拓?fù)洌琑edis cluster 是一個(gè)網(wǎng)狀的,每一個(gè)節(jié)點(diǎn)通過tcp與其他每個(gè)節(jié)點(diǎn)連接。假如n個(gè)節(jié)點(diǎn)的集群,每個(gè)節(jié)點(diǎn)有n-1個(gè)出的鏈接,n-1個(gè)進(jìn)的鏈接。這些鏈接會(huì)一直存活。假如一個(gè)節(jié)點(diǎn)發(fā)送了一個(gè)ping,很就沒收到pong,但還沒到時(shí)間把這個(gè)節(jié)點(diǎn)設(shè)為 unreachable,就會(huì)通過重連刷新鏈接。
Nodes handshake,如果一個(gè)節(jié)點(diǎn)發(fā)送MEET信息(METT 類似ping,但是強(qiáng)迫接受者,把它作為集群一員)。一個(gè)節(jié)點(diǎn)發(fā)送MEET信息,只有管理員通過命令行,運(yùn)行如下命令CLUSTER MEET ip port。如果這個(gè)節(jié)點(diǎn)已經(jīng)被一個(gè)節(jié)點(diǎn)信任,那么也會(huì)被其他節(jié)點(diǎn)信任。比如A 知道B,B知道C,B會(huì)發(fā)送gossip信息給A關(guān)于C的信息。A就會(huì)認(rèn)為C是集群一員,并與其建立連接。
失敗檢測(cè),集群失效檢測(cè)就是,當(dāng)某個(gè)master或者slave不能被大多數(shù)nodes可達(dá)時(shí),用于故障遷移并將合適的slave提升為master。當(dāng)slave提升未能有效實(shí)施時(shí),集群將處于error狀態(tài)且停止接收Client端查詢。
集群中的每個(gè)節(jié)點(diǎn)都會(huì)定期地向集群中的其他節(jié)點(diǎn)發(fā)送PING消息,以此交換各個(gè)節(jié)點(diǎn)狀態(tài)信息,檢測(cè)各個(gè)節(jié)點(diǎn)狀態(tài):在線狀態(tài)、疑似下線狀態(tài)PFAIL、已下線狀態(tài)FAIL。當(dāng)主節(jié)點(diǎn)A通過消息得知主節(jié)點(diǎn)B認(rèn)為主節(jié)點(diǎn)D進(jìn)入了疑似下線(PFAIL)狀態(tài)時(shí),主節(jié)點(diǎn)A會(huì)在自己的clusterState.nodes字典中找到主節(jié)點(diǎn)D所對(duì)應(yīng)的clusterNode結(jié)構(gòu),并將主節(jié)點(diǎn)B的下線報(bào)告(failure report)添加到clusterNode結(jié)構(gòu)的fail_reports鏈表中
如果集群里面,半數(shù)以上的主節(jié)點(diǎn)都將主節(jié)點(diǎn)D報(bào)告為疑似下線,那么主節(jié)點(diǎn)D將被標(biāo)記為已下線(FAIL)狀態(tài),將主節(jié)點(diǎn)D標(biāo)記為已下線的節(jié)點(diǎn)會(huì)向集群廣播主節(jié)點(diǎn)D的FAIL消息,
所有收到FAIL消息的節(jié)點(diǎn)都會(huì)立即更新nodes里面主節(jié)點(diǎn)D狀態(tài)標(biāo)記為已下線。
將node標(biāo)記為FAIL需要滿足以下兩個(gè)條件:
1.有半數(shù)以上的主節(jié)點(diǎn)將node標(biāo)記為PFAIL狀態(tài)。
2.當(dāng)前節(jié)點(diǎn)也將node標(biāo)記為PFAIL狀態(tài)。
多個(gè)從節(jié)點(diǎn)選主
選新主的過程基于Raft協(xié)議選舉方式來實(shí)現(xiàn)的
1)當(dāng)從節(jié)點(diǎn)發(fā)現(xiàn)自己的主節(jié)點(diǎn)進(jìn)行已下線狀態(tài)時(shí),從節(jié)點(diǎn)會(huì)廣播一條
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到這條消息,并且具有投票權(quán)的主節(jié)點(diǎn)向這個(gè)從節(jié)點(diǎn)投票
2)如果一個(gè)主節(jié)點(diǎn)具有投票權(quán),并且這個(gè)主節(jié)點(diǎn)尚未投票給其他從節(jié)點(diǎn),那么主節(jié)點(diǎn)將向要求投票的從節(jié)點(diǎn)返回一條,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個(gè)主節(jié)點(diǎn)支持從節(jié)點(diǎn)成為新的主節(jié)點(diǎn)
3)每個(gè)參與選舉的從節(jié)點(diǎn)都會(huì)接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根據(jù)自己收到了多少條這種消息來統(tǒng)計(jì)自己獲得了多少主節(jié)點(diǎn)的支持
4)如果集群里有N個(gè)具有投票權(quán)的主節(jié)點(diǎn),那么當(dāng)一個(gè)從節(jié)點(diǎn)收集到大于等于集群N/2+1張支持票時(shí),這個(gè)從節(jié)點(diǎn)就成為新的主節(jié)點(diǎn)
5)如果在一個(gè)配置紀(jì)元沒有從能夠收集到足夠的支持票數(shù),那么集群進(jìn)入一個(gè)新的配置紀(jì)元,并再次進(jìn)行選主,直到選出新的主節(jié)點(diǎn)為止
故障轉(zhuǎn)移
當(dāng)從節(jié)點(diǎn)發(fā)現(xiàn)自己的主節(jié)點(diǎn)變?yōu)橐严戮€(FAIL)狀態(tài)時(shí),便嘗試進(jìn)Failover,以期成為新的主。
以下是故障轉(zhuǎn)移的執(zhí)行步驟:
1)從下線主節(jié)點(diǎn)的所有從節(jié)點(diǎn)中選中一個(gè)從節(jié)點(diǎn)
2)被選中的從節(jié)點(diǎn)執(zhí)行SLAVEOF NO NOE命令,成為新的主節(jié)點(diǎn)
3)新的主節(jié)點(diǎn)會(huì)撤銷所有對(duì)已下線主節(jié)點(diǎn)的槽指派,并將這些槽全部指派給自己
4)新的主節(jié)點(diǎn)對(duì)集群進(jìn)行廣播PONG消息,告知其他節(jié)點(diǎn)已經(jīng)成為新的主節(jié)點(diǎn)
5)新的主節(jié)點(diǎn)開始接收和處理槽相關(guān)的請(qǐng)求
分布式鎖
1.互斥(必須):同一時(shí)刻,分布式部署的應(yīng)用中,同一個(gè)方法/資源只能被一臺(tái)機(jī)器上的一個(gè)線程占用。
2.鎖失效保護(hù)(必須):出現(xiàn)客戶端斷電等異常情況,鎖仍然能被其他客戶端獲取,防止死鎖。
3.可重入(可選):同一個(gè)線程在沒有釋放鎖之前,如果想再次操作,可以直接獲得鎖。
4.阻塞/非阻塞(可選):若沒有獲取到鎖,返回獲取失敗
5.高可用、高性能(可選):獲取釋放鎖最好是原子操作,獲取釋放鎖的性能要好
version1
lock:SETNX key value
unlock:DEL key [key ...]
指令含義參考:http://doc.redisfans.com/string/setnx.html
這是第一版最簡(jiǎn)單的方案,保證在沒有出現(xiàn)任何異常的時(shí)候多個(gè)客戶端可以使用分布式鎖。
但是問題來了,如下圖中所示,client2在獲取鎖之后突然掛了,這時(shí)候鎖k將無法釋放,其他client就永遠(yuǎn)拿不到這把鎖了。這就是需要解決的鎖失效保護(hù)問題。
version2
我們可以給鎖引入一個(gè)過期時(shí)間,這樣即使client2掛了,鎖過期之后其他client仍然能用。
EXPIRE key seconds
但此時(shí)同樣會(huì)存在一些問題:
1)誤刪
解決方法是每個(gè)client塞給鎖的value設(shè)定為唯一的隨機(jī)字符串,在刪除的時(shí)候先get一把,如果還是這個(gè)字符串的話才去刪。
2)過期時(shí)間需大于業(yè)務(wù)執(zhí)行時(shí)間,不然任務(wù)還沒搞完就被別人搶了
這個(gè)時(shí)候需要開啟另外一個(gè)線程專門去刷新鎖的過期時(shí)間。
version3
我們需要盡量保證獲取、釋放鎖的操作是原子性的,才能避免極端的異常情況。
原子性地加鎖
SET key uniquevalue NX EX 20
原子性地解鎖
我們可以使用原生的lua腳本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
public void unlock() {
// 使用lua腳本進(jìn)行原子刪除操作
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}