分布式緩存之Redis


緩存大致可以分為兩類,一種是應用內緩存,比如Map(簡單的數據結構),以及EH Cache(Java第三方庫),另一種 就是緩存組件,比如Memached,Redis;Redis(remote dictionary server)是一個基於KEY-VALUE的高性能的 存儲系統,通過提供多種鍵值數據類型來適應不同場景下的緩存與存儲需求 

存儲結構

大家一定對字典類型的數據結構非常熟悉,比如map ,通過key value的方式存儲的結構。 redis的全稱是remote dictionary server(遠程字典服務器),它以字典結構存儲數據,並允許其他應用通過TCP協議讀寫字典中的內容。數 據結構如下

 

啟動停止redis

 Redis有哪些可執行文件

Redis-server                                        Redis服務器

Redis-cli                                               Redis命令行客戶端

Redis-benchmark          Redis性能測試工具

Redis-check-aof                                    Aof文件修復工具

Redis-check-dump                                Rdb文件檢查工具

Redis-sentinel                                        Sentinel服務器(2.8以后)

常用的命令是redis-server和redis-cli

\1.直接啟動

redis-server  ../redis.conf

服務器啟動后默認使用的是6379的端口,通過--port可以自定義端口;

redis-server --port 6380

以守護進程的方式啟動,需要修改redis.conf配置文件中deemonize yes

\2.停止redis

redis-cli SHUTDOWN

考慮到redis有可能正在將內存的數據同步到硬盤中,強行終止redis進程可能會導致數據丟失,正確停止redis的方式應該是向Redis發送SHUTDOWN命令

當redis收到SHUTDOWN命令后,先斷開所有客戶端連接,然后根據配置進行持久化,最終完成退出

數據類型

字符串類型(String)

字符串類型是redis中最基本的數據類型,它能存儲任何形式的字符串,包括二進制數據。你可以用它存儲用戶的 郵箱、json化的對象甚至是圖片。一個字符類型鍵允許存儲的最大容量是512M 

列表類型(list)

列表類型(list)可以存儲一個有序的字符串列表,常用的操作是向列表兩端添加元素或者獲得列表的某一個片段。

列表類型內部使用雙向鏈表實現,所以向列表兩端添加元素的時間復雜度為O(1), 獲取越接近兩端的元素速度就越 快。這意味着即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是很快的

 

 hasn類型

 

 集合類型

集合類型中,每個元素都是不同的,也就是不能有重復數據,同時集合類型中的數據是無序的。一個集合類型鍵可 以存儲至多232-1個 。集合類型和列表類型的最大的區別是有序性和唯一性 集合類型的常用操作是向集合中加入或刪除元素、判斷某個元素是否存在。由於集合類型在redis內部是使用的值 為空的散列表(hash table),所以這些操作的時間復雜度都是O(1).

 

 有序集合

 

 

有序集合類型,顧名思義,和前面講的集合類型的區別就是多了有序的功能
在集合類型的基礎上,有序集合類型為集合中的每個元素都關聯了一個分數,這使得我們不僅可以完成插入、刪除 和判斷元素是否存在等集合類型支持的操作,還能獲得分數最高(或最低)的前N個元素、獲得指定分數范圍內的元 素等與分數有關的操作。雖然集合中每個元素都是不同的,但是他們的分數卻可以相同

過期時間設置

在Redis中提供了Expire命令設置一個鍵的過期時間,到期以后Redis會自動刪除它,這個在實際使用過程中用的非常多。

EXPIRE命令的使用方法為

EXPIRE key seconds

其中seconds參數表示鍵的過期時間,單位為秒。

EXPIRE返回值為1表示設置成功,0標識設置失敗或鍵不存在

如果想知道一個鍵還有多久時間被刪除,可以使用TTL命令

TTL key

當鍵不存在時,TTL命令會返回-2
而對於沒有給指定鍵設置過期時間的,通過TTL命令會返回-1

如果想取消鍵的過期時間設置(使該鍵恢復成為永久的),可以使用PERSIST命令,如果該命令執行成功或者成功 清除了過期時間,則返回1 。 否則返回0(鍵不存在或者本身就是永久的) EXPIRE命令的seconds命令必須是整數,所以小單位是1秒,如果向要更精確的控制鍵的過期時間可以使用 PEXPIRE命令,當然實際過程中用秒的單位就夠了。 PEXPIRE命令的單位是毫秒。即PEXPIRE key 1000與EXPIRE key 1相等;對應的PTTL以毫秒單位獲取鍵的剩余有效時間
還有一個針對字符串獨有的過期時間設置方式:
setex(String key,int seconds,String value)

過期刪除的原理

Redis中的主鍵失效是如何實現的,即失效的主鍵是如何刪除的?實際上,Redis刪除失效主鍵的方法主要有兩種:

消極方法(passive way)

在主鍵被訪問時如果發現它已經失效,那么就刪除它

積極方法(active way)

周期性地從設置了失效時間的主鍵中選擇一部分失效的主鍵刪除

對於那些從未被查詢的key,即便他們已經過期,被動方式也無法清楚。

因此Redis會周期性地隨機測試一些key, 已過期的key將會被刪掉。Redis每秒會進行10次操作,具體的流程:

\1.隨機測試20個帶有timeout信息的key:

\2.刪除其中已經過期的key;

\3.如果超過25%的key被刪除,則重復執行步驟1;

這是一個簡單的概率算法(trivial probabilistic algorithm),基於假設我們隨機抽取的key空閑。

Redis發布訂閱

Redis提供了發布訂閱功能,可以用於消息的傳輸,Redis提供了一組命令可以讓開發者實現“發布/訂閱”模式 (publish/subscribe) . 該模式同樣可以實現進程間的消息傳遞,它的實現原理是:

發布/訂閱模式包含兩種角色,分別是發布者和訂閱者。訂閱者可以訂閱一個或多個頻道,而發布者可以向指定的 頻道發送消息,所有訂閱此頻道的訂閱者都會收到該消息

發布者發布消息的命令是PUBLISH,用法是

PUBLISH channel message

比如向channel.1發一條消息:hello

PUBLISH channel.1 "hello"

這樣就實現了消息的發送,該命令的返回值表示接受到這條消息的訂閱者數量。因為在執行這條命令的時候還沒有訂閱者訂閱該頻道,所以返回為0,另外值得注意的是消息發送出去不會持久化,如果發送之前沒有訂閱者,那么后續再有訂閱者訂閱該頻道,之前的消息就收不到了

訂閱者訂閱消息的命令是

SUBSCRIBE channel [channel ...]

該命令同時可以訂閱多個頻道,比如訂閱channel.1的頻道。SUBSCRIBE channel.1

執行SUBSCRIBE命令后客戶端就會進入訂閱狀態。

結構圖

channel分兩類,一個是普通channel,另一個是pattern channel(規則匹配),producer1發布了一條消息 

publish abc hello

redis server 發給abc這個普通channel上的所有訂閱者,同時abc也匹配上了pattern channel的名字,所以這條消息也同時發送給pattern channel *bc上的所有訂閱者

 

 

 Redis的數據是如何持久化的?

Redis支持兩種方式的持久化,一種是RDB方式,另一種是AOF(append-only-file)方式。前者會根據指定的規則“定時”將內存中的數據存儲在硬盤上,而后者在每次執行命令后將命令本身記錄下來。兩種持久化方式可以單獨使用其中一種,也可以將兩種方式結合使用

RDB方式

當符合一定條件時,Redis會單獨創建(fork)一個子進程來進行持久化,會先將數據寫入到一個臨時文件中,等到持久化過程都結束了,再用這個臨時文件替換上次持久化好的文件。整個過程中,主進程是不進行任何IO操作的,這就確保了極高的性能。如果需要進行大規模數據的恢復,且對於數據恢復的完整性不是非常的敏感,那RDB方式要比AOF方式更加的高效。RDB的缺點是后一次持久化后的數據可能丟失

Redis會在以下幾種情況下對數據進行快照
\1. 根據配置規則進行自動快照

\2. 用戶執行SAVE或者GBSAVE命令

\3. 執行FLUSHALL命令

\4. 執行復制(replication)時

根據配置規則進行自動快照

Redis允許用戶自定義快照條件,當符合快照條件時,Redis會自動執行快照操作。快照的條件可以由用戶在配置文 件中配置。配置格式如下
save
第一個參數是時間窗口,第二個是鍵的個數,也就是說,在第一個時間參數配置范圍內被更改的鍵的個數大於后面 的changes時,即符合快照條件。redis默認配置了三個規則

save 900 1

save 300 10

save 60 10000

每條快照規則占一行,每條規則之間是“或”的關系。 在900秒(15分)內有一個以上的鍵被更改則進行快照。

用戶執行SAVE或BGSAVE命令

除了讓Redis自動進行快照以外,當我們對服務進行重啟或者服務器遷移我們需要人工去干預備份。redis提供了兩 條命令來完成這個任務

\1. save命令

當執行save命令時,Redis同步做快照操作,在快照執行過程中會阻塞所有來自客戶端的請求。當redis內存中的數 據較多時,通過該命令將導致Redis較長時間的不響應。所以不建議在生產環境上使用這個命令,而是推薦使用 bgsave命令

\2. bgsave命令

bgsave命令可以在后台異步地進行快照操作,快照的同時服務器還可以繼續響應來自客戶端的請求。執行BGSAVE 后,Redis會立即返回ok表示開始執行快照操作。

通過LASTSAVE命令可以獲取近一次成功執行快照的時間; (自動快照采用的是異步快照操作) 

執行FLUSHALL命令

該命令在前面講過,會清除redis在內存中的所有數據。執行該命令后,只要redis中配置的快照規則不為空,也就 是save 的規則存在。redis就會執行一次快照操作。不管規則是什么樣的都會執行。如果沒有定義快照規則,就不 會執行快照操作 

執行復制時

該操作主要是在主從模式下,redis會在復制初始化時進行自動快照。

這里只需要了解當執行復制操作時,即使沒有定義自動快照規則,並且沒有手動執行過快照操作,它仍然會生成 RDB快照文件 

AOF方式

當使用Redis存儲非臨時數據時,一般需要打開AOF持久化來降低進程終止導致的數據丟失。AOF可以將Redis執行 的每一條寫命令追加到硬盤文件中,這一過程會降低Redis的性能,但大部分情況下這個影響是能夠接受的,另外 使用較快的硬盤可以提高AOF的性能 

開啟AOF

默認情況下Redis沒有開啟AOF(append only file)方式的持久化,可以通過appendonly參數啟用,在redis.conf中找到appendonly yes

開啟AOF持久化后每執行一條會更改Redis中的數據的命令后,Redis就會將該命令寫入硬盤中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通過dir參數設置的,默認的文件名是apendonly.aof,可以在redis.conf中的屬性appendfilename appendonlyh.aof修改

AOF的實現

set foo 1

set foo 2
set foo 3

get

redis 會將前3條命令寫入AOF文件中,通過vim的方式可以看到aof文件中的內容

我們會發現AOF文件的內容正是Redis發送的原始通信協議的內容,
從內容中我們發現Redis只記錄了3 條命令。然后這時有一個問題是前面2條命令其實是冗余的,
因為這兩條的執行結果都會被第三條命令覆 蓋。隨着執行的命令越來越多,
AOF文件的大小也會越來越大,其實內存中實際的數據可能沒有多少,
那這樣就會造成磁盤空間以及redis數據還原的過程比較長的問題。
因此我們希望Redis可以自動優化 AOF文件,就上面這個例子來說,
前面兩條是可以被刪除的。 而實際上Redis也考慮到了,
可以配置一 個條件,每當達到一定條件時Redis就會自動重寫AOF文件,
這個條件的配置問 auto-aof-rewritepercentage 100 auto-aof-rewrite-min-size 64mb

auto-aof-rewrite-percentage 表示的是當前的AOF文件大小超過上一次重寫時的AOF文件大小的百分之多少時會再次進行重寫,如果之前沒有重寫過,則以啟動時的AOF文件大小為依據

auto-aof-rewrite-min-size 表示限制了允許重寫的小AOF文件大小,通常在AOF文件很小的情況下即使其中有很 多冗余的命令我們也並不太關心。
另外,還可以通過BGREWRITEAOF 命令手動執行AOF,執行完以后冗余的命令已經被刪除了
在啟動時,Redis會逐個執行AOF文件中的命令來將硬盤中的數據載入到內存中,載入的速度相對於RDB會慢一些

AOF的重寫原理

Redis 可以在 AOF 文件體積變得過大時,自動地在后台對 AOF 進行重寫: 重寫后的新 AOF 文件包含了恢復當前 數據集所需的小命令集合。
重寫的流程是這樣,主進程會fork一個子進程出來進行AOF重寫,這個重寫過程並不是基於原有的aof文件來做 的,而是有點類似於快照的方式,全量遍歷內存中的數據,然后逐個序列到aof文件中。在fork子進程這個過程 中,服務端仍然可以對外提供服務,那這個時候重寫的aof文件的數據和redis內存數據不一致了怎么辦?不用擔 心,這個過程中,主進程的數據更新操作,會緩存到aof_rewrite_buf中,也就是單獨開辟一塊緩存來存儲重寫期間 收到的命令,當子進程重寫完以后再把緩存中的數據追加到新的aof文件。 當所有的數據全部追加到新的aof文件中后,把新的aof文件重命名為,此后所有的操作都會被寫入新的aof文件。
如果在rewrite過程中出現故障,不會影響原來aof文件的正常工作,只有當rewrite完成后才會切換文件。因此這個 rewrite過程是比較可靠的

Redis內存回收策略

Redis中提供了多種內存回收策略,當內存容量不足時,為了保證程序的運行,這時就不得不淘汰內存中的一些對 象,釋放這些對象占用的空間,那么選擇淘汰哪些對象呢?

其中,默認的策略為noeviction策略,當內存使用達到閾值的時候,所有引起申請內存的命令會報錯

allkeys-lru:從數據集(server.db[i].dict)中挑選近少使用的數據淘汰

適合的場景: 如果我們的應用對緩存的訪問都是相對熱點數據,那么可以選擇這個策略

allkeys-random:隨機移除某個key。

適合的場景:如果我們的應用對於緩存key的訪問概率相等,則可以使用這個策略

volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰。 volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選近少使用的數據淘汰。 volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰

適合場景:這種策略使得我們可以向Redis提示哪些key更適合被淘汰,我們可以自己控制 

實際上Redis實現的LRU並不是可靠的LRU,也就是名義上我們使用LRU算法淘汰內存數據,但是實際上被淘汰的鍵並不一定是真正的最少使用的數據,這里涉及到一個權衡的問題,如果需要在所有的數據中搜索最符合條件的數據,那么一定會增加系統的開銷,Redis是單線程的,所以耗時的操作會謹慎一些。為了在一定成本內實現相對的 LRU,早期的Redis版本是基於采樣的LRU,也就是放棄了從所有數據中搜索解改為采樣空間搜索優解。Redis3.0 版本之后,Redis作者對於基於采樣的LRU進行了一些優化,目的是在一定的成本內讓結果更靠近真實的LRU。 

Redis是單進程單線程?性能為什么這么快

Redis采用了一種非常簡單的做法,單線程來處理來自所有客戶端的開發請求,Redis把任務封閉在一個線程中從而避免了線程安全問題;redis為什么是單線程?

官方的解釋是,CPU並不是Redis的瓶頸所在,Redis的瓶頸主要在機器的內存和網絡的帶寬。那么Redis能不能處 理高並發請求呢?當然是可以的,至於怎么實現的,我們來具體了解一下。 【注意並發不等於並行,並發性I/O 流,意味着能夠讓一個計算單元來處理來自多個客戶端的流請求。並行性,意味着服務器能夠同時執行幾個事情, 具有多個計算單元】 

多路復用

Redis是跑在單線程中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待用戶輸入或輸出都是阻塞的,所以I/O操作在一般情況下往往不能直接返回,這會導致某一文件的I/O阻塞導致整個進程無法對其他客戶提供服務,而I/O多路復用就是為了解決這個問題而出現的。

了解多路復用之前,先簡單了解下幾種I/O模型

(1)同步阻塞IO(Blocking IO):即傳統的IO模型。

(2)同步非阻塞IO(Non-blocking IO):默認創建的socket都是阻塞的,非阻塞IO要求socket被設置為 NONBLOCK。
(3)IO多路復用(IO Multiplexing):即經典的Reactor設計模式,也稱為異步阻塞IO,Java中的Selector和 Linux中的epoll都是這種模型。

(4)異步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為異步非阻塞IO。

同步和異步、阻塞和非阻塞,到底是什么意思,感覺原理都差不多,我來簡單解釋一下
同步和異步,指的是用戶線程和內核的交互方式
阻塞和非阻塞,指用戶線程調用內核IO操作的方式是阻塞還是非阻塞
就像在Java中使用多線程做異步處理的概念,通過多線程去執行一個流程,主線程可以不用等待。而阻塞和非阻塞 我們可以理解為假如在同步流程或者異步流程中做IO操作,如果緩沖區數據還沒准備好,IO的這個過程會阻塞,這 個在之前講TCP協議的時候有講過.

在Redis中使用Lua腳本

我們在使用redis的時候,會面臨一些問題,比如

原子性問題

前面我們講過,redis雖然是單一線程的,當時仍然會存在線程安全問題,當然,這個線程安全問題不是來源安於 Redis服務器內部。而是Redis作為數據服務器,是提供給多個客戶端使用的。多個客戶端的操作就相當於同一個進 程下的多個線程,如果多個客戶端之間沒有做好數據的同步策略,就會產生數據不一致的問題。舉個簡單的例子:

多個客戶端的命令之間沒有做請求同步,導致實際執行順序可能會不一致,終的結果也就無法滿足原子性了。 

效率問題

redis本身的吞吐量是非常高的,因為它首先是基於內存的數據庫。在實際使用過程中,有一個非常重要的因素影 響redis的吞吐量,那就是網絡。我們在使用redis實現某些特定功能的時候,很可能需要多個命令或者多個數據類 型的交互才能完成,那么這種多次網絡請求對性能影響比較大。當然redis也做了一些優化,比如提供了pipeline管 道操作,但是它有一定的局限性,就是執行的多個命令和響應之間是不存在相互依賴關系的。所以我們需要一種機 制能夠編寫一些具有業務邏輯的命令,減少網絡請求 

Lua

Redis中內嵌了對Lua環境的支持,允許開發者使用Lua語言編寫腳本傳到Redis中執行,Redis客戶端可以使用Lua 腳本,直接在服務端原子的執行多個Redis命令。
使用腳本的好處:
\1. 減少網絡開銷,在Lua腳本中可以把多個命令放在同一個腳本中運行

\2. 原子操作,redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。換句話說,編寫腳本的過程中無 需擔心會出現競態條件

\3. 復用性,客戶端發送的腳本會永遠存儲在redis中,這意味着其他客戶端可以復用這一腳本來完成同樣的邏輯 Lua是一個高效的輕量級腳本語言(javascript、shell、sql、python、ruby…),用標准C語言編寫並以源代碼形式開 放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能;

Redis與Lua 

先初步的認識一下在redis中如何結合lua來完成一些簡單的操作 

在Lua腳本中調用Redis命令

在Lua腳本中調用Redis命令,可以使用redis.call函數調用。比如我們調用string類型的命令 redis.call(‘set’,’hello’,’world’)
local value=redis.call(‘get’,’hello’)

redis.call 函數的返回值就是redis命令的執行結果。前面我們介紹過redis的5中類型的數據返回的值的類型也都不 一樣。redis.call函數會將這5種類型的返回值轉化對應的Lua的數據類型

從Lua腳本中獲得返回值

在很多情況下我們都需要腳本可以有返回值,畢竟這個腳本也是一個我們所編寫的命令集,我們可以像調用其他 redis內置命令一樣調用我們自己寫的腳本,所以同樣redis會自動將腳本返回值的Lua數據類型轉化為Redis的返回 值類型。 在腳本中可以使用return 語句將值返回給redis客戶端,通過return語句來執行,如果沒有執行return, 默認返回為nil。 

EVAL命令的格式

[EVAL][腳本內容] [key參數的數量][key …] [arg …] 可以通過key和arg這兩個參數向腳本中傳遞數據,他們的值可以在腳本中分別使用KEYS和ARGV 這兩個類型的全 局變量訪問。比如我們通過腳本實現一個set命令,通過在redis客戶端中調用,那么執行的語句是:
lua腳本的內容為:

return redis.call(‘set’,KEYS[1],ARGV[1]) //KEYS和ARGV必須大寫

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua1 hello

注意:EVAL命令是根據 key參數的數量-也就是上面例子中的1來將后面所有參數分別存入腳本中KEYS和ARGV兩個 表類型的全局變量。當腳本不需要任何參數時也不能省略這個參數。如果沒有參數則為0

 

 

EVALSHA命令

考慮到我們通過eval執行lua腳本,腳本比較長的情況下,每次調用腳本都需要把整個腳本傳給redis,比較占用帶 寬。為了解決這個問題,redis提供了EVALSHA命令允許開發者通過腳本內容的SHA1摘要來執行腳本。該命令的用 法和EVAL一樣,只不過是將腳本內容替換成腳本內容的SHA1摘要

\1. Redis在執行EVAL命令時會計算腳本的SHA1摘要並記錄在腳本緩存中

\2. 執行EVALSHA命令時Redis會根據提供的摘要從腳本緩存中查找對應的腳本內容,如果找到了就執行腳本,否則 返回“NOSCRIPT No matching script,Please use EVAL”

通過以下案例來演示EVALSHA命令的效果

script load "return redis.call('get','lua1')" 將腳本加入緩存並生成sha1命令 evalsha"a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0

我們在調用eval命令之前,先執行evalsha命令,如果提示腳本不存在,則再調用eval命令

集群

先來簡單了解下redis中提供的集群策略, 雖然redis有持久化功能能夠保障redis服務器宕機也能恢復並且只有少量 的數據損失,但是由於所有數據在一台服務器上,如果這台服務器出現硬盤故障,那就算是有備份也仍然不可避免 數據丟失的問題。
在實際生產環境中,我們不可能只使用一台redis服務器作為我們的緩存服務器,必須要多台實現集群,避免出現 單點故障;

主從復制

復制的作用是把redis的數據庫復制多個副本部署在不同的服務器上,如果其中一台服務器出現故障,也能快速遷 移到其他服務器上提供服務。 復制功能可以實現當一台redis服務器的數據更新后,自動將新的數據同步到其他服 務器上
主從復制就是我們常見的master/slave模式, 主數據庫可以進行讀寫操作,當寫操作導致數據發生變化時會自動將 數據同步給從數據庫。而一般情況下,從數據庫是只讀的,並接收主數據庫同步過來的數據。 一個主數據庫可以有 多個從數據庫

 

 配置

在redis中配置master/slave是非常容易的,只需要在從數據庫的配置文件中加入slaveof主數據庫地址端口。而master數據庫不需要做任何改變

准備兩台服務器,分別安裝redis , server1  server2
 
\1.    在server2的redis.conf文件中增加 slaveof server1-ip 6379 、 同時將bindip注釋掉,允許所 有ip訪問
 
\2.  啟動server2
 
\3.  訪問server2的redis客戶端,輸入 INFO replication \4.  通過在master機器上輸入命令,比如set foo bar 、 在slave服務器就能看到該值已經同步過來了

原理

1.全量復制

Redis全量復制一般發生在slave初始化階段,這時slave需要將Master上的所有數據都復制一份。具體步驟

 

 

完成上面幾個步驟后就完成了slave服務器數據初始化的所有操作,savle服務器此時可以接收來自用戶的讀請求。
master/slave 復制策略是采用樂觀復制,也就是說可以容忍在一定時間內master/slave數據的內容是不同的,但是 兩者的數據會最終同步。具體來說,redis的主從同步過程本身是異步的,意味着master執行完客戶端請求的命令 后會立即返回結果給客戶端,然后異步的方式把命令同步給slave。 這一特征保證啟用master/slave后 master的性能不會受到影響。 但是另一方面,如果在這個數據不一致的窗口期間,master/slave因為網絡問題斷開連接,而這個時候,master 是無法得知某個命令最終同步給了多少個slave數據庫。不過redis提供了一個配置項來限制只有數據至少同步給多 少個slave的時候,master才是可寫的:
min-slaves-to-write 3 表示只有當3個或以上的slave連接到master,master才是可寫的
min-slaves-max-lag 10 表示允許slave最長失去連接的時間,如果10秒還沒收到slave的響應,則master認為該 slave以斷開

2.增量復制

從redis2.8開始,就支持主從復制的斷點續傳,如果主從復制過程中,網絡連接斷掉了,那么可以接着上次復制的地方,繼續復制下去,而不是從頭開始復制一份

master node會在內存中創建一個backlog,master和slave都會保存一個replica offset還有一個master id,offset 就是保存在backlog中的。如果master和slave網絡連接斷掉了,slave會讓master從上次的replica offset開始繼續 復制

但是如果沒有找到對應的offset,那么就會執行一次全量同步

3.無硬盤復制

 前面我們說過,Redis復制的工作原理基於RDB方式的持久化實現的,也就是master在后台保存RDB快照,slave接 收到rdb文件並載入,但是這種方式會存在一些問題 

\1. 當master禁用RDB時,如果執行了復制初始化操作,Redis依然會生成RDB快照,當master下次啟動時執行該 RDB文件的恢復,但是因為復制發生的時間點不確定,所以恢復的數據可能是任何時間點的。就會造成數據出現問 題

\2. 當硬盤性能比較慢的情況下(網絡硬盤),那初始化復制過程會對性能產生影響 因此2.8.18以后的版本,Redis引入了無硬盤復制選項,可以不需要通過RDB文件去同步,直接發送數據,通過以 下配置來開啟該功能
repl-diskless-sync yes master**在內存中直接創建rdb,然后發送給slave,不會在自己本地落地磁盤了

哨兵機制

在前面講的master/slave模式,在一個典型的一主多從的系統中,slave在整個體系中起到了數據冗余備份和讀寫 分離的作用。當master遇到異常終端后,需要從slave中選舉一個新的master繼續對外提供服務,這種機制在前面 提到過N次,比如在zk中通過leader選舉、kafka中可以基於zk的節點實現master選舉。所以在redis中也需要一種 機制去實現master的決策,redis並沒有提供自動master選舉功能,而是需要借助一個哨兵來進行監控 

什么是哨兵

顧名思義,哨兵的作用就是監控Redis系統的運行狀況,他的功能包括兩個

\1. 監控master和slave是否正常運行 

\2. master出現故障時自動將slave數據庫升級為master

哨兵是一個獨立的進程,使用哨兵后的架構圖

 

 

 

為了解決master選舉問題,又引出了一個單點問題,也就是哨兵的可用性如何解決,在一個一主多從的Redis系統 中,可以使用多個哨兵進行監控任務以保證系統足夠穩定。此時哨兵不僅會監控master和slave,同時還會互相監 控;這種方式稱為哨兵集群,哨兵集群需要解決故障發現、和master決策的協商機制問題

 

 

sentinel之間的相互感知

 sentinel節點之間會因為共同監視同一個master從而產生了關聯,一個新加入的sentinel節點需要和其他監視相同 master節點的sentinel相互感知,首先

\1. 需要相互感知的sentinel都向他們共同監視的master節點訂閱channel:sentinel:hello
\2. 新加入的sentinel節點向這個channel發布一條消息,包含自己本身的信息,這樣訂閱了這個channel的sentinel 就可以發現這個新的sentinel

\3. 新加入得sentinel和其他sentinel節點建立長連接

 

 

 master的故障發現

sentinel節點會定期向master節點發送心跳包來判斷存活狀態,一旦master節點沒有正確響應,sentinel會把 master設置為“主觀不可用狀態”,然后它會把“主觀不可用”發送給其他所有的sentinel節點去確認,當確認的 sentinel節點數大於>quorum時,則會認為master是“客觀不可用”,接着就開始進入選舉新的master流程;但是 這里又會遇到一個問題,就是sentinel中,本身是一個集群,如果多個節點同時發現master節點達到客觀不可用狀 態,那誰來決策選擇哪個節點作為maste呢?這個時候就需要從sentinel集群中選擇一個leader來做決策。而這里 用到了一致性算法Raft算法、它和Paxos算法類似,都是分布式一致性算法。但是它比Paxos算法要更容易理解; Raft和Paxos算法一樣,也是基於投票算法,只要保證過半數節點通過提議即可; 

動畫演示地址:http://thesecretlivesofdata.com/raft/ 

配置實現

通過在這個配置的基礎上增加哨兵機制。在其中任意一台服務器上創建一個sentinel.conf文件,文件內容 :

sentinel monitor name ip port quorum

其中name表示要監控的master的名字,這個名字是自己定義。 ip和port表示master的ip和端口號。 最后一個表示最低 通過票數,也就是說至少需要幾個哨兵節點統一才可以

port 6040 sentinel monitor mymaster 192.168.11.131 6379 1
sentinel down-after-milliseconds mymaster 5000 --表示如果5s內mymaster沒響應,就認為SDOWN sentinel failover-timeout mymaster 15000 --表示如果15秒后,mysater仍沒活過來,則啟動failover,從剩下的 slave中選一個升級為master

兩種方式啟動哨兵
redis-sentinel sentinel.conf

redis-server /path/to/sentinel.conf --sentinel

哨兵監控一個系統時,只需要配置監控master即可,哨兵會自動發現所有slave;

這時候,我們把master關閉,等待指定時間后(默認是30秒),會自動進行切換

+sdown表示哨兵主管認為master已經停止服務了,+odown表示哨兵客觀認為master停止服務了。接着哨兵開始進行故障恢復,挑選一個slave升級為master
+try-failover表示哨兵開始進行故障恢復 +failover-end 表示哨兵完成故障恢復 +slave表示列出新的master和slave服務器,我們仍然可以看到已經停掉的master,哨兵並沒有清楚已停止的服務 的實例,這是因為已經停止的服務器有可能會在某個時間進行恢復,恢復以后會以slave角色加入到整個集群中

Redis-Cluster

即使是使用哨兵,此時的Redis集群的每個數據庫依然存有集群中的所有數據,從而導致集群的總數據存儲量受限 於可用存儲內存最小的節點,形成了木桶效應。而因為Redis是基於內存存儲的,所以這一個問題在redis中就顯得 尤為突出了
在redis3.0之前,我們是通過在客戶端去做的分片,通過hash環的方式對key進行分片存儲。分片雖然能夠解決各 個節點的存儲壓力,但是導致維護成本高、增加、移除節點比較繁瑣。因此在redis3.0以后的版本最大的一個好處 就是支持集群功能,集群的特點在於擁有和單機實例一樣的性能,同時在網絡分區以后能夠提供一定的可訪問性以 及對主數據庫故障恢復的支持。
哨兵和集群是兩個獨立的功能,當不需要對數據進行分片使用哨兵就夠了,如果要進行水平擴容,集群是一個比較 好的方式

拓撲結構

一個Redis Cluster由多個Redis節點構成。不同節點組服務的數據沒有交集,也就是每個一節點組對應數據 sharding的一個分片。節點組內部分為主備兩類節點,對應master和slave節點。兩者數據准實時一致,通過異步 化的主備復制機制來保證。一個節點組有且只有一個master節點,同時可以有0到多個slave節點,在這個節點組中 只有master節點對用戶提供些服務,讀服務可以由master或者slave提供 

redis-cluster是基於gossip協議實現的無中心化節點的集群,因為去中心化的架構不存在統一的配置中心,各個節 點對整個集群狀態的認知來自於節點之間的信息交互。在Redis Cluster,這個信息交互是通過Redis Cluster Bus來 完成的 

Redis的數據分區

分布式數據庫首要解決把整個數據集按照分區規則映射到多個節點的問題,即把數據集划分到多個節點上,每個節 點負責整個數據的一個子集, Redis Cluster采用哈希分區規則,采用虛擬槽分區。

虛擬槽分區巧妙地使用了哈希空間,使用分散度良好的哈希函數把所有的數據映射到一個固定范圍內的整數集合, 整數定義為槽(slot)。比如Redis Cluster槽的范圍是0 ~ 16383。槽是集群內數據管理和遷移的基本單位。采用 大范圍的槽的主要目的是為了方便數據的拆分和集群的擴展,每個節點負責一定數量的槽。

計算公式:slot = CRC16(key)%16383。每一個節點負責維護一部分槽以及槽所映射的鍵值數據。

 

 

 HashTags

通過分片手段,可以將數據合理的划分到不同的節點上,這本來是一件好事。但是有的時候,我們希望對相關聯的 業務以原子方式進行操作。舉個簡單的例子
我們在單節點上執行MSET , 它是一個原子性的操作,所有給定的key會在同一時間內被設置,不可能出現某些指定 的key被更新另一些指定的key沒有改變的情況。但是在集群環境下,我們仍然可以執行MSET命令,但它的操作不 在是原子操作,會存在某些指定的key被更新,而另外一些指定的key沒有改變,原因是多個key可能會被分配到不 同的機器上。
所以,這里就會存在一個矛盾點,及要求key盡可能的分散在不同機器,又要求某些相關聯的key分配到相同機器。 這個也是在面試的時候會容易被問到的內容。怎么解決呢?
從前面的分析中我們了解到,分片其實就是一個hash的過程,對key做hash取模然后划分到不同的機器上。所以為 了解決這個問題,我們需要考慮如何讓相關聯的key得到的hash值都相同呢?如果key全部相同是不現實的,所以 怎么解決呢?在redis中引入了HashTag的概念,可以使得數據分布算法可以根據key的某一個部分進行計算,然后 讓相關的key落到同一個數據分片
舉個簡單的例子,加入對於用戶的信息進行存儲, user:user1:id、user:user1:name/ 那么通過hashtag的方式, user:{user1}:id、user:{user1}.name; 表示 當一個key包含 {} 的時候,就不對整個key做hash,而僅對 {} 包括的字符串做hash。

重定向客戶端

Redis Cluster並不會代理查詢,那么如果客戶端訪問了一個key並不存在的節點,這個節點是怎么處理的呢?比如 我想獲取key為msg的值,msg計算出來的槽編號為254,當前節點正好不負責編號為254的槽,那么就會返回客戶 端下面信息:
-MOVED 254 127.0.0.1:6381
表示客戶端想要的254槽由運行在IP為127.0.0.1,端口為6381的Master實例服務。如果根據key計算得出的槽恰好 由當前節點負責,則當期節點會立即返回結果

分片遷移

在一個穩定的Redis cluster下,每一個slot對應的節點是確定的,但是在某些情況下,節點和分片對應的關系會發 生變更
\1. 新加入master節點
\2. 某個節點宕機 也就是說當動態添加或減少node節點時,需要將16384個槽做個再分配,槽中的鍵值也要遷移。當然,這一過程, 在目前實現中,還處於半自動狀態,需要人工介入

新增一個主節點
新增一個節點D,redis cluster的這種做法是從各個節點的前面各拿取一部分slot到D上。大致就會變成這樣: 節點A覆蓋1365-5460
節點B覆蓋6827-10922 節點C覆蓋12288-16383
節點D覆蓋0-1364,5461-6826,10923-12287
刪除一個主節點
先將節點的數據移動到其他節點上,然后才能執行刪除

槽遷移的過程

槽遷移的過程中有一個不穩定狀態,這個不穩定狀態會有一些規則,這些規則定義客戶端的行為,從而使得Redis Cluster不必宕機的情況下可以執行槽的遷移。下面這張圖描述了我們遷移編號為1、2、3的槽的過程中,他們在 MasterA節點和MasterB節點中的狀態。

 

 

 

簡單的工作流程
\1. 向MasterB發送狀態變更命令,吧Master B對應的slot狀態設置為IMPORTING
\2. 向MasterA發送狀態變更命令,將Master對應的slot狀態設置為MIGRATING 當MasterA的狀態設置為MIGRANTING后,表示對應的slot正在遷移,為了保證slot數據的一致性,MasterA此時 對於slot內部數據提供讀寫服務的行為和通常狀態下是有區別的,

MIGRATING狀態 

 \1. 如果客戶端訪問的Key還沒有遷移出去,則正常處理這個key

\2. 如果key已經遷移或者根本就不存在這個key,則回復客戶端ASK信息讓它跳轉到MasterB去執行 

IMPORTING狀態

當MasterB的狀態設置為IMPORTING后,表示對應的slot正在向MasterB遷入,及時Master仍然能對外提供該slot 的讀寫服務,但和通常狀態下也是有區別的
\1. 當來自客戶端的正常訪問不是從ASK跳轉過來的,說明客戶端還不知道遷移正在進行,很有可能操作了一個目前 還沒遷移完成的並且還存在於MasterA上的key,如果此時這個key在A上已經被修改了,那么B和A的修改則會發生 沖突。所以對於MasterB上的slot上的所有非ASK跳轉過來的操作,MasterB都不會uu出去護理,而是通過MOVED 命令讓客戶端跳轉到MasterA上去執行

這樣的狀態控制保證了同一個key在遷移之前總是在源節點上執行,遷移后總是在目標節點上執行,防止出現兩邊 同時寫導致的沖突問題。而且遷移過程中新增的key一定會在目標節點上執行,源節點也不會新增key,是的整個遷 移過程既能對外正常提供服務,又能在一定的時間點完成slot的遷移。

Redis Java客戶端介紹

已有的客戶端支持

Redis Java客戶端有很多的開源產品比如Redission、Jedis、lettuce 

差異

Jedis是Redis的Java實現的客戶端,其API提供了比較全面的Redis命令的支持;
Redisson實現了分布式和可擴展的Java數據結構,和Jedis相比,功能較為簡單,不支持字符串操作,不支持排 序、事務、管道、分區等Redis特性。Redisson主要是促進使用者對Redis的關注分離,從而讓使用者能夠將精力更 集中地放在處理業務邏輯上。
lettuce是基於Netty構建的一個可伸縮的線程安全的Redis客戶端,支持同步、異步、響應式模式。多個線程可以 共享一個連接實例,而不必擔心多線程並發問題;

jedis-sentinel原理分析

原理

客戶端通過連接到哨兵集群,通過發送Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME 命令,從哨兵機器中 詢問master節點的信息,拿到master節點的ip和端口號以后,再到客戶端發起連接。連接以后,需要在客戶端建 立監聽機制,當master重新選舉之后,客戶端需要重新連接到新的master節點 

jedis-cluster原理分析

連接方式

Set<HostAndPort> hostAndPorts=new HashSet<>(); 
HostAndPort hostAndPort=new HostAndPort("192.168.11.153",7000);
HostAndPort hostAndPort1=new HostAndPort("192.168.11.153",7001);
HostAndPort hostAndPort2=new HostAndPort("192.168.11.154",7003);
HostAndPort hostAndPort3=new HostAndPort("192.168.11.157",7006);
hostAndPorts.add(hostAndPort);
hostAndPorts.add(hostAndPort1);
hostAndPorts.add(hostAndPort2);
hostAndPorts.add(hostAndPort3);
JedisCluster jedisCluster=new JedisCluster(hostAndPorts,6000);
jedisCluster.set("mic","hello");

原理分析

程序啟動初始化集群環境

1)、讀取配置文件中的節點配置,無論是主從,無論多少個,只拿第一個,獲取redis連接實例 2)、用獲取的redis連接實例執行clusterNodes()方法,實際執行redis服務端cluster nodes命令,獲取主從配置信 息
3)、解析主從配置信息,先把所有節點存放到nodes的map集合中,key為節點的ip:port,value為當前節點的 jedisPool

4)、解析主節點分配的slots區間段,把slot對應的索引值作為key,第三步中拿到的jedisPool作為value,存儲在 slots的map集合中 就實現了slot槽索引值與jedisPool的映射,這個jedisPool包含了master的節點信息,所以槽和幾點是對應的,與 redis服務端一致

從集群環境存取值

1)、把key作為參數,執行CRC16算法,獲取key對應的slot值
2)、通過該slot值,去slots的map集合中獲取jedisPool實例

3)、通過jedisPool實例獲取jedis實例,最終完成redis數據存取工作 

Redisson客戶端的操作方式

redis-cluster連接方式

Config config=new Config(); config.useClusterServers().setScanInterval(2000).        
addNodeAddress("redis://192.168.11.153:7000",                
          "redis://192.168.11.153:7001",                
          "redis://192.168.11.154:7003",
          "redis://192.168.11.157:7006");
RedissonClient redissonClient= Redisson.create(config);
RBucket<String> rBucket=redissonClient.getBucket("mic");
System.out.println(rBucket.get());

常規操作命令

getBucket-> 獲取字符串對象; 
getMap -> 獲取map對象
getSortedSet->獲取有序集合
getSet -> 獲取集合
getList ->獲取列表

分布式鎖的實現

關於鎖,其實我們或多或少都有接觸過一些,比如synchronized、 Lock這些,這類鎖的目的很簡單,在多線程環 境下,對共享資源的訪問造成的線程安全問題,通過鎖的機制來實現資源訪問互斥。那么什么是分布式鎖呢?或者 為什么我們需要通過Redis來構建分布式鎖,其實最根本原因就是Score(范圍),因為在分布式架構中,所有的應 用都是進程隔離的,在多進程訪問共享資源的時候我們需要滿足互斥性,就需要設定一個所有進程都能看得到的范 圍,而這個范圍就是Redis本身。所以我們才需要把鎖構建到Redis中。
Redis里面提供了一些比較具有能夠實現鎖特性的命令,比如SETEX(在鍵不存在的情況下為鍵設置值),那么我們可 以基於這個命令來去實現一些簡單的鎖的操作

Redisson實現分布式鎖

Redisson它除了常規的操作命令以外,還基於redis本身的特性去實現了很多功能的封裝,比如分布式鎖、原子操 作、布隆過濾器、隊列等等。我們可以直接利用這個api提供的功能去實現

Config config=new Config();
config.useSingleServer().setAddress("redis://192.168.11.152:6379");
RedissonClient redissonClient=Redisson.create(config);

RLock rLock=redissonClient.getLock("updateOrder");//最多等待100秒、上鎖10s以后自動解鎖
if(rLock.tryLock(100,10,TimeUnit.SECONDS)){
    System.out.println("獲取鎖成功");
}

管道模式

Redis服務是一種C/S模型,提供請求-響應式協議的TCP服務,所以當客戶端發起請求,服務端處理並返回結果到 客戶端,一般是以阻塞形式等待服務端的響應,但這在批量處理連接時延遲問題比較嚴重,所以Redis為了提升或 彌補這個問題,引入了管道技術:可以做到服務端未及時響應的時候,客戶端也可以繼續發送命令請求,做到客戶 端和服務端互不影響,服務端並最終返回所有服務端的響應,大大提高了C/S模型交互的響應速度上有了質的提高 

使用方法

Jedis jedis=new Jedis("192.168.11.152",6379); 
Pipeline pipeline=jedis.pipelined();

for(int i=0;i<1000;i++){    
  pipeline.incr("test");
}
pipeline.sync();

Redis的應用架構

對於讀多寫少的高並發場景,我們會經常使用緩存來進行優化。比如說支付寶的余額展示功能,實際上99%的時候 都是查詢,1%的請求是變更(除非是土豪,每秒鍾都有收入在不斷更改余額),所以,我們在這樣的場景下,可 以加入緩存,用戶->余額

 

 Redis緩存與數據一致性問題 

那么基於上面的這個出發點,問題就來了,當用戶的余額發生變化的時候,如何更新緩存中的數據,也就是說。
\1. 我是先更新緩存中的數據再更新數據庫的數據;
\2. 還是修改數據庫中的數據再更新緩存中的數據
這就是我們經常會在面試遇到的問題,數據庫的數據和緩存中的數據如何達到一致性?首先,可以肯定的是, redis中的數據和數據庫中的數據不可能保證事務性達到統一的,這個是毫無疑問的,所以在實際應用中,我們都 是基於當前的場景進行權衡降低出現不一致問題的出現概率

更新緩存還是讓緩存失效

更新緩存表示數據不但會寫入到數據庫,還會同步更新緩存; 而讓緩存失效是表示只更新數據庫中的數據,然后刪 除緩存中對應的key。那么這兩種方式怎么去選擇?這塊有一個衡量的指標。 \1. 如果更新緩存的代價很小,那么可以先更新緩存,這個代價很小的意思是我不需要很復雜的計算去獲得最新的 余額數字。
\2. 如果是更新緩存的代價很大,意味着需要通過多個接口調用和數據查詢才能獲得最新的結果,那么可以先淘汰 緩存。淘汰緩存以后后續的請求如果在緩存中找不到,自然去數據庫中檢索。

先操作數據庫還是先操作緩存?

當客戶端發起事務類型請求時,假設我們以讓緩存失效作為緩存的的處理方式,那么又會存在兩個情況,

\1. 先更新數據庫再讓緩存失效
\2. 先讓緩存失效,再更新數據庫

前面我們講過,更新數據庫和更新緩存這兩個操作,是無法保證原子性的,所以我們需要根據當前業務的場景的容 忍性來選擇。也就是如果出現不一致的情況下,哪一種更新方式對業務的影響最小,就先執行影響最小的方案 

最終一致性的解決方案

 

 關於緩存雪崩的解決方案 

當緩存大規模滲透在整個架構中以后,那么緩存本身的可用性講決定整個架構的穩定性。那么接下來我們來討論下 緩存在應用過程中可能會導致的問題。 

緩存雪崩

緩存雪崩是指設置緩存時采用了相同的過期時間,導致緩存在某一個時刻同時失效,或者緩存服務器宕機宕機導致 緩存全面失效,請求全部轉發到了DB層面,DB由於瞬間壓力增大而導致崩潰。緩存失效導致的雪崩效應對底層系 統的沖擊是很大的。 

解決方式

\1. 對緩存的訪問,如果發現從緩存中取不到值,那么通過加鎖或者隊列的方式保證緩存的單進程操作,從而避免 失效時並發請求全部落到底層的存儲系統上;但是這種方式會帶來性能上的損耗
\2. 將緩存失效的時間分散,降低每一個緩存過期時間的重復率

\3. 如果是因為緩存服務器故障導致的問題,一方面需要保證緩存服務器的高可用、另一方面,應用程序中可以采 用多級緩存

緩存穿透

 緩存穿透是指查詢一個根本不存在的數據,緩存和數據源都不會命中。出於容錯的考慮,如果從數據層查不到數據 則不寫入緩存,即數據源返回值為 null 時,不緩存 null。緩存穿透問題可能會使后端數據源負載加大,由於很多后 端數據源不具備高並發性,甚至可能造成后端數據源宕掉

解決方式

\1. 如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪 問數據庫,這種辦法最簡單粗暴。比如,”key” , “&&”。 在返回這個&&值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續等待繼續訪 問,還是放棄掉這次操作。如果繼續等待訪問,過一個時間輪詢點后,再次請求這個key,如果取到的值不再是 &&,則可以認為這時候key有值了,從而避免了透傳到數據庫,從而把大量的類似請求擋在了緩存之中。
\2. 根據緩存數據Key的設計規則,將不符合規則的key進行過濾 采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的BitSet中,不存在的數據將會被攔截掉,從而避免了 對底層存儲系統的查詢壓力

布隆過濾器

布隆過濾器是Burton Howard Bloom在1970年提出來的,一種空間效率極高的概率型算法和數據結構,主要用來 判斷一個元素是否在集合中存在。因為他是一個概率型的算法,所以會存在一定的誤差,如果傳入一個值去布隆過 濾器中檢索,可能會出現檢測存在的結果但是實際上可能是不存在的,但是肯定不會出現實際上不存在然后反饋存 在的結果。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter 通過極少的錯誤換取了存儲空間的極大節省。

bitmap

所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,通過Bit為單位來存儲數據,可以大大節省存儲空間. 所以我們可以通過一個int型的整數的32比特位來存儲32個10進制的數字,那么這樣所帶來的好處是內存占用少、 效率很高(不需要比較和位移)比如我們要存儲5(101)、3(11)四個數字,那么我們申請int型的內存空間,會有32 個比特位。這四個數字的二進制分別對應
從右往左開始數,比如第一個數字是5,對應的二進制數據是101, 那么從右往左數到第5位,把對應的二進制數據 存儲到32個比特位上。
第一個5就是 00000000000000000000000000101000

輸入3時候 00000000000000000000000000001100

布隆過濾器原理

布隆過濾器(Bloom Filter)的核心實現是一個超大的位數組和幾個哈希函數。假設位數組的長度為m,哈希函數的個數為k

 

 

以上圖為例,具體的操作流程:假設集合里面有3個元素{x, y, z},哈希函數的個數為3。首先將位數組進行初始化,將里面每個位都設置位0。對於集合里面的每一個元素,將元素依次通過3個哈希函數進行映射,每次映射都會產生一個哈希值,這個值對應位數組上面的一個點,然后將位數組對應的位置標記為1。查詢W元素是否存在集合中的時候,同樣的方法將W通過哈希映射到位數組上的3個點。如果3個點的其中有一個點不為1,則可以判斷該元素一定不存在集合中。反之,如果3個點都為1,則該元素可能存在集合中。注意:此處不能判斷該元素是否一定存在集合中,可能存在一定的誤判率。可以從圖中可以看到:假設某個元素通過映射對應下標為4,5,6這3個點。雖然這3個點都為1,但是很明顯這3個點是不同元素經過哈希得到的位置,因此這種情況說明元素雖然不在集合中,也可能對應的都是1,這是誤判率存在的原因。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM