Redis實(shí)戰(zhàn)

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);

}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379