Redis實戰 Redis命令 數據安全與性能保障 降低佔用內存 擴展Redis Redis的Lua腳本編程 多路複用 Redis無中心集羣 分佈式鎖

redis 和 memcached 的區別

1. redis支持更豐富的數據類型(支持更復雜的應用場景):Redis不僅僅支持簡單的k/v類型的數據,同時還提供 list,set,zset,hash等數據結構的存儲。memcache支持簡單的數據類型,String。

2. Redis支持數據的持久化,可以將內存中的數據保持在磁盤中,重啓的時候可以再次加載進行使用,而 Memecache把數據全部存在內存之中。

3. 集羣模式:memcached沒有原生的集羣模式,需要依靠客戶端來實現往集羣中分片寫入數據;但是 redis 目前 是原生支持 cluster 模式的.

4. Memcached是多線程,非阻塞IO複用的網絡模型;Redis使用單線程的多路 IO複用模型

布隆過濾器基本使用

布隆過濾器有二個基本指令,bf.add 添加元素,bf.exists 查詢元素是否存在,它的用法和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一個元素,如果想要一次添加多個,就需要用到 bf.madd 指令。同樣如果需要一次查詢多個元素是否存在,就需要用到 bf.mexists 指令。

統計和查找

Redis 提供了位圖統計指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統計指定位置範圍內 1 的個數,bitpos 用來查找指定範圍內出現的第一個 0 或 1。

比如我們可以通過 bitcount 統計用戶一共簽到了多少天,通過 bitpos 指令查找用戶從哪一天開始第一次簽到。如果指定了範圍參數[start, end],就可以統計在某個時間範圍內用戶簽到了多少天,用戶自某天以後的哪天開始簽到。

Redis命令

字符串命令

Redis字符串可以存儲3種類型的值:

  • 字節串(byte string)

  • 整數

  • 浮點數

整數和浮點數 可以有INCR和DECR等命令

字符串可以有 APPEND、SUBSTR、SETRANGE、SETBIT

列表

Redis的列表允許用戶從序列的兩端推入或者彈出元素,獲取列表元素。

RPUSH、LPUSH、RPOP、LPOP、LINDEX、LRANGE、LTRIM,

也可以阻塞執行命令的客戶端:BLPOP BRPOP RPOPLPUSH BRPOPLPUSH,可以當作消息隊列使用。

集合

以無序方式來存儲多個不相同的元素

SADD SREM SISMEMBER SCARD SMEMBERS SRANDMEMBER SPOP SMOVE

散列

多個鍵值對存儲到一個Redis鍵裏面

HMGET HMSET HDEL HLEN

有序集合

有序集合存儲了成員與分值的映射,並且提供了分值處理命令,以及根據分值大小有序地獲取(fetch)或掃描(scan)成員和分值的命令。

ZADD ZREM ZCARD ZINCRBY ZCOUNT ZRANK ZSCORE ZRANGE

Redis事務

基本事務需要用到MULTI和EXEC命令。Redis接收到MULTI命令,會把之後發送的所有命令都放到一個隊列裏面,直到收到EXEC命令位置,Redis會在不被打斷的情況下,一個接一個執行存儲在隊列裏面的所有命令。可以由pipeline犯法實現。

過期與刪除

PERSIST:移除過期時間 TTL:查看剩餘過期時間 EXPIRE:指定秒數後過期

數據安全與性能保障

Redis提供了兩種不同的持久化方法來將數據存儲到硬盤裏面。一種叫快照(snapshotting),可以將存在於某一時間的數據都寫入到硬盤裏面。另一種叫做只追加文件(append-only file,AOF)它會在執行命令時,將執行的寫命令複製到硬盤裏面。

創建快照有以下幾種方式:

  • 客戶端發送BGSAVE創建一個快照(windows不支持),Redis會調用fork來創建一個子進程來將快照寫入硬盤。(在Unix和類Unix系統上,進程創建子進程時父子進程共享相同內存,知道父進程或子進程寫入內存後,對被寫入內存共享纔會結束)

  • 客戶端發送SAVE命令創建快照,會阻塞進程。沒有足夠內存執行BGSAVE可以考慮

  • 用戶設置了save配置選項,如save 60 10000。從最近一次創建快照之後當“60秒內有10000次寫入”就會觸發一次BGSAVE

  • Redis通過SHUTDOWN命令接收到關閉服務器命令,或者接受到標準TERM信號時,會執行SAVE

  • Redis連接到另一個Redis,並且想對方發送SYNC命令來開始一次複製操作,如果主服務器沒有執行BGSAVE或者沒有剛執行完BGSAVE,那麼主服務器就會執行BGSAVE。

fork創建子進程

子進程剛剛產生時,它和父進程共享內存裏面的代碼段和數據段。這時你可以將父子進程想像成一個連體嬰兒,共享身體。這是 Linux 操作系統的機制,爲了節約內存資源,所以儘可能讓它們共享起來。在進程分離的一瞬間,內存的增長几乎沒有明顯變化。

子進程做數據持久化,它不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,然後序列化寫到磁盤中。但是父進程不一樣,它必須持續服務客戶端請求,然後對內存數據結構進行不間斷的修改。

這個時候就會使用操作系統的 COW (Copy On Write,寫時複製)機制來進行數據段頁面的分離。數據段是由很多操作系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面複製一份分離出來,然後對這個複製的頁面進行修改。這時子進程相應的頁面是沒有變化的,還是進程產生時那一瞬間的數據。

隨着父進程修改操作的持續進行,越來越多的共享頁面被分離出來,內存就會持續增長。但是也不會超過原有數據內存的 2 倍大小。另外一個 Redis 實例裏冷數據佔的比例往往是比較高的,所以很少會出現所有的頁面都會被分離,被分離的往往只有其中一部分頁面。每個頁面的大小隻有 4K,一個 Redis 實例裏面一般都會有成千上萬的頁面。

如果Redis進程佔用了20GB的內存,在標準硬件上運行BGSAVE所創建的子進程將導致Redis停頓200-400毫秒。

AOF持久化

可以設置 appendonly yes 配置選項來打開 AOF持久化。

appendfsync有三個選項 always(每個寫命令都同步) everysec(每秒)no(不顯式寫)

Redis會不斷將寫命令記錄到AOF,隨着不斷運行體積會不斷增長,重啓之後需要重新執行AOF文件記錄還原數據集,太大的AOF文件可能導致還原操作執行很長。可以使用BGREWRITEAOF命令移除AOF文件中的冗餘命令重寫AOF文件。

複製(replication,MS)

從服務器指定 slaveof host port的配置文件,發送SLAVEOF no one命令終止複製主服務器。發送SLAVEOF host port命令開始複製新服務器。

複製的過程:

1. 主:等待命令進入,從:連接(或重連)主服務器,發送SYNC命令

2. 主:執行BGSAVE,並使用緩衝區記錄BGSAVE之後執行的寫命令,從:根據配置決定是繼續使用現有數據還是返回錯誤

3. 主:BGSAVE執行完畢,快照發送給從服務器,並繼續用緩衝區記錄寫命令,從:丟棄舊數據載入快照文件

4. 主:快照文件發送完畢,發送緩衝區寫命令,從:解釋快照文件,正常執行

5. 主:緩衝區寫命令發送完畢,每個寫命令同步給服務器,從:執行所有寫命令

主從鏈(master/slave chaining)從服務器可以有自己的從服務器。從服務器1加載快照文件時中斷從服務器2的連接,從服務器2需要重新連接並重新同步。可以考慮多層主從鏈樹

爲了驗證主服務器是否已經將寫數據發送至從服務器,用戶需要在向主服務器寫入數據後再寫一個唯一的虛構值(unique dummy value),通過檢查虛構值是否存在與從服務器來判斷寫數據是否已經到達從服務器。

若要檢查是否寫入了硬盤中,可以檢查INFO命令的輸出結果中aof_pending_bio_fsync屬性值是否爲0,如果是表示服務器已經將所有已知的數據保存到硬盤。

發生故障後快照文件和AOF文件可以使用命令檢查(redis-check-aof redis-check-dump),修復AOF就是丟棄出錯命令以及之後的所有命令。快照無法修復。

更換主從服務器,主服務器掛了,從服務器SAVE生成快照,發送給新主加載完成後,從服務器執行SLAVEOF指定新主。(或者後面使用哨兵模式 Redis Sentinel)

事務

Redis的事務相關命令包括 WATCH MULTI/EXEC UNWATCH/DISCARD。

用戶使用WATCH監視一個或多個鍵,接着使用MULTI開始一個新事務,多個命令入隊,可以發送DISCARD取消WATCH並清空所有已入隊命令。

也可以使用非事務型流水線(non-transactional pipeline)。執行pipleline傳入true做參數表示使用事務,傳入false表示無需事務。

分佈式鎖

使用SETNX命令實現鎖的獲取功能,這個命令只會在鍵不存在的情況下爲鍵設置值。

降低佔用內存

三種方式降低Redis佔用:

  • 短結構(short structure)

  • 分片結構(shared structure)

  • 打包存儲二進制位和字節

短結構

Redis爲列表、集合、散列和有序集合提供了一組配置選項,這些選項可以讓Redis以更節約空間的方式存儲長度較短的結構。Redis可以選擇使用一種名爲壓縮列表(ziplist)和緊湊存儲方式來存儲這些結構。壓縮列表是列表、散列、和有序集合這3種不同類型的對象的一種非結構化(unstructured)表示,以序列化方式存儲數據,這些序列化數據每次被讀取的時候都要進行解碼,每次被寫入的時候都要進行局部的重新編碼,並且可能需要對內存裏面的數據進行移動。

Redis用雙鏈表表示列表、散列表表示散列、散列表加上跳躍表(skiplist)表示有序集合。

鍵名儘量簡短

分片結構

使用 namespace:id 這樣的字符串鍵去存儲短字符串或者計數器,能夠有效降低存儲這些數據所需的內存。

打包存儲二進制位和字節

高效打包和更新Redis字符串的4個命令,分別是

GETRANGE、SETRANGE、GETBIT、SETBIT。

擴展Redis

Redis Sentinel可以配合Redis的複製功能使用,對下線的主服務器進行故障轉移。當主服務器失效的時候,見識這個主服務器的所有Sentinel就會基於彼此共有的信息選出一個Sentinel,並從現有的從服務器當中選出一個新的主服務器。當被選中的從服務器轉換爲主服務器之後,那個被選中的Sentinel就會讓剩餘的其他服務器去複製這個新的主服務器(默認Sentinel會一個一個遷移從服務器,可以通過配置選項進行修改)

Redis的Lua腳本編程

使用EVAL和EVALSHA命令執行Lua腳本

Lua腳本和單個Redis命令以及“MULTI/EXEC”事務一樣,都是原子操作

已經對結構進行了修改的Lua腳本無法中斷

內存淘汰策略

不同於之前的版本,redis5.0爲我們提供了八個不同的內存置換策略。很早之前提供了6種。

(1)volatile-lru:從已設置過期時間的數據集中挑選最近最少使用的數據淘汰。

(2)volatile-ttl:從已設置過期時間的數據集中挑選將要過期的數據淘汰。

(3)volatile-random:從已設置過期時間的數據集中任意選擇數據淘汰。

(4)volatile-lfu:從已設置過期時間的數據集挑選使用頻率最低的數據淘汰。

(5)allkeys-lru:從數據集中挑選最近最少使用的數據淘汰

(6)allkeys-lfu:從數據集中挑選使用頻率最低的數據淘汰。

(7)allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰

(8) no-eviction(驅逐):禁止驅逐數據,這也是默認策略。意思是當內存不足以容納新入數據時,新寫入操作就會報錯,請求可以繼續進行,線上任務也不能持續進行,採用no-eviction策略可以保證數據不被丟失。

這八種大體上可以分爲4中,lru、lfu、random、ttl。

LRU淘汰

LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那麼將來被訪問的機率也更高”。

在服務器配置中保存了 lru 計數器 server.lrulock,會定時(redis 定時程序 serverCorn())更新,server.lrulock 的值是根據 server.unixtime 計算出來進行排序的,然後選擇最近使用時間最久的數據進行刪除。另外,從 struct redisObject 中可以發現,每一個 redis 對象都會設置相應的 lru。每一次訪問數據,會更新對應redisObject.lru

在Redis中,LRU算法是一個近似算法,默認情況下,Redis會隨機挑選5個鍵,並從中選擇一個最久未使用的key進行淘汰。在配置文件中,按maxmemory-samples選項進行配置,選項配置越大,消耗時間就越長,但結構也就越精準。

TTL淘汰

Redis 數據集數據結構中保存了鍵值對過期時間的表,即 redisDb.expires。與 LRU 數據淘汰機制類似,TTL 數據淘汰機制中會先從過期時間的表中隨機挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣,TTL淘汰策略並不是面向所有過期時間的表中最快過期的鍵值對,而只是隨機挑選的幾個鍵值對。

隨機淘汰:

在隨機淘汰的場景下獲取待刪除的鍵值對,隨機找hash桶再次hash指定位置的dictEntry即可。

Redis中的淘汰機制都是幾近於算法實現的,主要從性能和可靠性上做平衡,所以並不是完全可靠,所以開發者們在充分了解Redis淘汰策略之後還應在平時多主動設置或更新key的expire時間,主動刪除沒有價值的數據,提升Redis整體性能和空間。

多路複用

在 I/O 多路複用模型中,最重要的函數調用就是 select,該方法的能夠同時監控多個文件描述符的可讀可寫情況,當其中的某些文件描述符可讀或者可寫時,select 方法就會返回可讀以及可寫的文件描述符個數。

關於 select 的具體使用方法,在網絡上資料很多,這裏就不過多展開介紹了;

與此同時也有其它的 I/O 多路複用函數 epoll/kqueue/evport,它們相比 select 性能更優秀,同時也能支撐更多的服務。

Reactor 設計模式

Redis 服務採用 Reactor 的方式來實現文件事件處理器(每一個網絡連接其實都對應一個文件描述符)

文件事件處理器使用 I/O 多路複用模塊同時監聽多個 FD,當 accept、read、write 和 close 文件事件產生時,文件事件處理器就會回調 FD 綁定的事件處理器。

雖然整個文件事件處理器是在單線程上運行的,但是通過 I/O 多路複用模塊的引入,實現了同時對多個 FD 讀寫的監控,提高了網絡通信模型的性能,同時也可以保證整個 Redis 服務實現的簡單。

I/O 多路複用模塊

I/O 多路複用模塊封裝了底層的 select、epoll、avport 以及 kqueue 這些 I/O 多路複用函數,爲上層提供了相同的接口。

I/O 多路複用模型是利用select、poll、epoll可以同時監察多個流的 I/O 事件的能力,在空閒的時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll是隻輪詢那些真正發出了事件的流),依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這裏“多路”指的是多個網絡連接,“複用”指的是複用同一個線程。採用多路 I/O 複用技術可以讓單個線程高效的處理多個連接請求(儘量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成爲這裏的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。

Redis集羣不需要sentinel哨兵也能完成節點移除和故障轉移的功能。需要將每個節點設置成集羣模式,這種集羣模式沒有中心節點,可水平擴展,據官方文檔稱可以線性擴展到上萬個節點(官方推薦不超過1000個節點)。redis集羣的性能和高可用性均優於之前版本的哨兵模式,且集羣配置非常簡單。

Redis無中心集羣

Redis5之後提供了無中心的集羣模式

Redis Cluseter 主要組件

key 分佈模式,key空間分佈被劃分爲16384個slot,所以一個集羣,主節點的個數最大爲16384(一般建議master最大節點數爲1000)

Cluster bus,每個節點有一個額外的TCP端口,這個端口用來和其他節點交換信息。這個端口一般是在與客戶端鏈接端口上面加10000,比如客戶端端口爲6379,那麼cluster bus的端口爲16379.

cluster 拓撲,Redis cluster 是一個網狀的,每一個節點通過tcp與其他每個節點連接。假如n個節點的集羣,每個節點有n-1個出的鏈接,n-1個進的鏈接。這些鏈接會一直存活。假如一個節點發送了一個ping,很就沒收到pong,但還沒到時間把這個節點設爲 unreachable,就會通過重連刷新鏈接。

Nodes handshake,如果一個節點發送MEET信息(METT 類似ping,但是強迫接受者,把它作爲集羣一員)。一個節點發送MEET信息,只有管理員通過命令行,運行如下命令CLUSTER MEET ip port。如果這個節點已經被一個節點信任,那麼也會被其他節點信任。比如A 知道B,B知道C,B會發送gossip信息給A關於C的信息。A就會認爲C是集羣一員,並與其建立連接。

失敗檢測,集羣失效檢測就是,當某個master或者slave不能被大多數nodes可達時,用於故障遷移並將合適的slave提升爲master。當slave提升未能有效實施時,集羣將處於error狀態且停止接收Client端查詢。

集羣中的每個節點都會定期地向集羣中的其他節點發送PING消息,以此交換各個節點狀態信息,檢測各個節點狀態:在線狀態、疑似下線狀態PFAIL、已下線狀態FAIL。當主節點A通過消息得知主節點B認爲主節點D進入了疑似下線(PFAIL)狀態時,主節點A會在自己的clusterState.nodes字典中找到主節點D所對應的clusterNode結構,並將主節點B的下線報告(failure report)添加到clusterNode結構的fail_reports鏈表中

如果集羣裏面,半數以上的主節點都將主節點D報告爲疑似下線,那麼主節點D將被標記爲已下線(FAIL)狀態,將主節點D標記爲已下線的節點會向集羣廣播主節點D的FAIL消息,

所有收到FAIL消息的節點都會立即更新nodes裏面主節點D狀態標記爲已下線。

將node標記爲FAIL需要滿足以下兩個條件:

1.有半數以上的主節點將node標記爲PFAIL狀態。

2.當前節點也將node標記爲PFAIL狀態。

多個從節點選主

選新主的過程基於Raft協議選舉方式來實現的

1)當從節點發現自己的主節點進行已下線狀態時,從節點會廣播一條

CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到這條消息,並且具有投票權的主節點向這個從節點投票

2)如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支持從節點成爲新的主節點

3)每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,並根據自己收到了多少條這種消息來統計自己獲得了多少主節點的支持

4)如果集羣裏有N個具有投票權的主節點,那麼當一個從節點收集到大於等於集羣N/2+1張支持票時,這個從節點就成爲新的主節點

5)如果在一個配置紀元沒有從能夠收集到足夠的支持票數,那麼集羣進入一個新的配置紀元,並再次進行選主,直到選出新的主節點爲止

故障轉移

當從節點發現自己的主節點變爲已下線(FAIL)狀態時,便嘗試進Failover,以期成爲新的主。

以下是故障轉移的執行步驟:

1)從下線主節點的所有從節點中選中一個從節點

2)被選中的從節點執行SLAVEOF NO NOE命令,成爲新的主節點

3)新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽全部指派給自己

4)新的主節點對集羣進行廣播PONG消息,告知其他節點已經成爲新的主節點

5)新的主節點開始接收和處理槽相關的請求

分佈式鎖

1.互斥(必須):同一時刻,分佈式部署的應用中,同一個方法/資源只能被一臺機器上的一個線程佔用。

2.鎖失效保護(必須):出現客戶端斷電等異常情況,鎖仍然能被其他客戶端獲取,防止死鎖。

3.可重入(可選):同一個線程在沒有釋放鎖之前,如果想再次操作,可以直接獲得鎖。

4.阻塞/非阻塞(可選):若沒有獲取到鎖,返回獲取失敗

5.高可用、高性能(可選):獲取釋放鎖最好是原子操作,獲取釋放鎖的性能要好

version1

lock:SETNX key value

unlock:DEL key [key ...]

指令含義參考:http://doc.redisfans.com/string/setnx.html

這是第一版最簡單的方案,保證在沒有出現任何異常的時候多個客戶端可以使用分佈式鎖。

但是問題來了,如下圖中所示,client2在獲取鎖之後突然掛了,這時候鎖k將無法釋放,其他client就永遠拿不到這把鎖了。這就是需要解決的鎖失效保護問題。

version2

我們可以給鎖引入一個過期時間,這樣即使client2掛了,鎖過期之後其他client仍然能用。

EXPIRE key seconds

但此時同樣會存在一些問題:

1)誤刪

解決方法是每個client塞給鎖的value設定爲唯一的隨機字符串,在刪除的時候先get一把,如果還是這個字符串的話纔去刪。

2)過期時間需大於業務執行時間,不然任務還沒搞完就被別人搶了

這個時候需要開啓另外一個線程專門去刷新鎖的過期時間。

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腳本進行原子刪除操作

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

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章