Redis實戰(通俗易懂,超詳細攻略) V2.0版本

Redis內存數據庫

更新說明:此文第一版是2019年4月份寫得,當時也對Redis沒有太多的認識,寫了較爲基礎的一些東西,如今已到年末,對Redis的認識也多了很多,特此更新2.0版本。
新增的內容有:
1、排版優化
2、新增基礎數據類型的底層實現
3、新增高級數據類型
4、新增了Redis擴容
5、新增Redis混合備份的方式
6、優化Redis集羣主從通信的細節
7、對一些bug進行了優化
(目前更新進度80%,2019/12/11)

第二篇原創技術博客就貢獻給Redis,做緩存這塊,Redis內存數據庫極其擅長,記得在Redis之前還有個Memcached,但支持的數據類型較少只有一種,而Redis支持多達五種數據類型。可謂長江後浪推前浪,前浪死在沙灘上了。

本文旨在讓新手小白也能快速上手,所以從最基本的講起,如果您是中手或者高手,可以跳到響應章節查漏補缺。

1.什麼是Redis

Redis 是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。 它支持多種類型的數據結構,如
①字符串(strings),
②散列(hashes),
③列表(lists),
④ 集合(sets),
⑤有序集合(sorted sets),

與範圍查詢, bitmaps, hyperloglogs 和 地理空間(geospatial) 索引半徑查詢。 Redis 內置了 複製(replication),LUA腳本(Lua scripting), LRU驅動事件(LRU eviction),事務(transactions) 和不同級別的 磁盤持久化(persistence), 並通過 Redis哨兵(Sentinel)和自動 分區(Cluster)提供高可用性(high availability)。

2.爲什麼要用Redis

請看上一段,Redis是內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件

  • 數據庫
    Redis本質是內存數據庫,所以自然可以當做數據庫來使用,但要注意的是內存空間是極其有限的,可不像硬盤那樣浩瀚無垠,所以大多數情況下我們還是用關係型數據庫+Redis緩存的方式運用Redis
  • 緩存
    比如Mysql,可承擔的併發訪問量有多大呢?答案是幾百左右就會扛不住了,所以我們爲了支持更高的併發,會使用緩存,爲數據庫築起一道護盾,讓大多數請求都發生在緩存這一層。Redis是把數據存儲在內存上的,訪問數據速度相當快,很適合做緩存。
  • 消息中間件
    Redis支持發佈/訂閱消息,當然真正的MQ我們一般在Rabbit,Rocket,卡夫卡之間選一個,這並不是Redis的強項

3.Redis的使用方式

這一塊分爲兩個部分,第一部分是把Redis部署在linux上,我們在linux使用Redis。
第二部分是通過SpringBoot來操作使用Redis。

3.1 Linux上使用

3.1.1 redis啓動

1、備份redis.conf(此文件爲Redis配置文件,非常重要):拷貝一份redis.conf到其他目錄

2、修改redis.conf文件將裏面的daemonize no 改成 yes,讓服務在後臺啓動

3、啓動命令:執行 redis-server /myredis/redis.conf(後面那個是配置文件的位置)

4、用客戶端訪問: Redis-cli

多個端口可以 Redis-cli –p 6379

5、測試驗證: ping 若成功啓動會返回 pong!

3.1.2 redis關閉

單實例關閉:Redis-cli shutdown

也可以進入終端後再關閉 shutdown

多實例關閉,指定端口關閉:Redis-cli -p 6379 shutdown

3.1.3 Redis–key/value

Redis作爲Nosql數據庫,數據都以鍵值對的形式存儲,Value內置了5大數據類型,
在看Value操作之前,先看一看Key的操作

keys * 查詢當前庫的所有鍵

exists 判斷某個鍵是否存在

type 查看鍵的類型

del 刪除某個鍵

expire 爲鍵值設置過期時間,單位秒。

ttl 查看還有多少秒過期,-1表示永不過期 (-2表示已過期)

dbsize 查看當前數據庫的key的數量

Flushdb 清空當前庫(慎用!)

Flushall 通殺全部庫(刪庫跑路!!!忘了這個命令吧)

4 Redis五大數據類型

4.1 Redis五大數據類型–String

Redis 的字符串是動態字符串,是可以修改的字符串,內部結構實現上類似於 Java 的 ArrayList,採用預分配冗餘空間的方式來減少內存的頻繁分配,,內部爲當前字符串實際分配的空間 capacity 一般要高於實際字符串長度 len。當字符串長度小於 1M 時,擴容都是加倍現有的空間,如果超過 1M,擴容時一次只會多擴 1M 的空間。需要注意的是字符串最大長度爲 512M。

4.1.2 關於String底層實現

Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的結構是一個帶長度信息的字節數組。所以以後有人在面試的時候問你:請聊一聊SDS。就不要傻乎乎的問人家“Redis有名爲SDS的數據結構麼?”
在這裏插入圖片描述

上圖爲容量與長度的關係,是不是簡單明瞭。

SDS的結構:
struct SDS {
T capacity; // 數組容量
T len; // 數組長度
byte flags; // 特殊標識位,不理睬它
byte[] content; // 數組內容
}

SDS 結構使用了範型 T,爲什麼不直接用 int 呢,這是因爲當字符串比較短時,len 和 capacity 可以使用 byte 和 short 來表示,Redis 爲了對內存做極致的優化,不同長度的字符串使用不同的結構體來表示。所以別人才那麼快嘛,太精細了!
注意:創建字符串時 len 和 capacity 一樣長,不會多分配冗餘空間,這是因爲絕大多數場景下我們不會使用 append 操作來修改字符串。

4.1.3 String常用命令

get 查詢對應鍵值

set 添加鍵值對

append 將給定的 追加到原值的末尾

strlen 獲得值的長度

setnx 只有在 key 不存在時設置 key 的值

incr

將 key 中儲存的數字值增1

只能對數字值操作,如果爲空,新增值爲1

decr

將 key 中儲存的數字值減1

只能對數字值操作,如果爲空,新增值爲-1

incrby / decrby <步長>

將 key 中儲存的數字值增減。自定義步長。

mset …

同時設置一個或多個 key-value對

mget …

同時獲取一個或多個 value

msetnx …

同時設置一個或多個 key-value 對,當且僅當所有給定 key 都不存在。

getrange <起始位置> <結束位置>

獲得值的範圍,類似java中的substring

setrange <起始位置>

用 覆寫 所儲存的字符串值, 從<起始位置>開始。

setex <過期時間>

設置鍵值的同時,設置過期時間,單位秒。

getset

以新換舊,設置了新值同時獲得舊值。

4.2 Redis五大數據類型–list

單鍵多值

Redis 列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素導列表的頭部(左邊)或者尾部(右邊)。

它的底層實際是個雙向鏈表,對兩端的操作性能很高,通過索引下標的操作中間的節點性能會較差。操作的時間複雜度爲 O(1),但是索引定位很慢,時間複雜度爲 O(n)

4.2.1 底層實現原理

上面也講了底層是個雙向鏈表,但還是太模糊了,具體一點,其實底層用的是ZipList(壓縮鏈表)和QuickList(快速鏈表)。

首先在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是 ziplist,也即是壓縮列表。它將所有的元素緊挨着一起存儲,分配的是一塊連續的內存。(感覺好像數組啊,連續的內存空間)

當數據量比較多的時候纔會改成 quickList。那麼什麼是quickList呢?Redis 將鏈表和 ziplist 結合起來組成了 quickList。如下圖所示:在這裏插入圖片描述

4.2.1.1 壓縮鏈表
  • 壓縮鏈表結構:
    struct ziplist {
    int32 zlbytes; // 整個壓縮列表佔用字節數
    int32 zltail_offset; // 最後一個元素距離壓縮列表起始位置的偏移量,用於快速定位到最後一個節點
    int16 zllength; // 元素個數
    T[] entries; // 元素內容列表,挨個挨個緊湊存儲
    int8 zlend; // 標誌壓縮列表的結束,值恆爲 0xFF
    }
    在這裏插入圖片描述

    壓縮列表爲了支持雙向遍歷,所以纔會有 ztail_offset 這個字段,用來快速定位到最後一個元素,然後倒着遍歷。

  • Entry的數據結構:
    entry 塊隨着容納的元素類型不同,也會有不一樣的結構。
    struct entry {
    int prevlen; // 前一個 entry 的字節長度
    int encoding; // 元素類型編碼
    optional byte[] content; // 元素內容
    }

    encoding字段是Redis自己定義的N種編碼類型,羅列出來也無意義。只需要知道可以根據編碼字段來確定元素內容的類型即可。

  • 增加元素:
    因爲 ziplist 都是緊湊存儲,沒有冗餘空間 (對比一下 Redis 的字符串結構)。意味着插入一個新的元素就需要調用 realloc 擴展內存。取決於內存分配器算法和當前的 ziplist 內存大小,realloc 可能會重新分配新的內存空間,並將之前的內容一次性拷貝到新的地址,也可能在原有的地址上進行擴展,這時就不需要進行舊內容的內存拷貝。

如果 ziplist 佔據內存太大,重新分配內存和拷貝內存就會有很大的消耗。所以 ziplist 不適合存儲大型字符串,存儲的元素也不宜過多。

4.2.1.2 快速鏈表

先看下最基礎的雙向鏈表結構:
// 鏈表的節點
struct listNode {
listNode* prev;
listNode* next;
T value;
}
// 鏈表
struct list {
listNode *head;
listNode *tail;
long length;
}

鏈表的附加空間相對太高,prev 和 next 指針就要佔去 16 個字節 (64bit 系統的指針是 8 個字節),另外每個節點的內存都是單獨分配,會加劇內存的碎片化,影響內存管理效率

爲了解決上述問題,就引出了quicklist:
quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來緊湊存儲,多個 ziplist 之間使用雙向指針串接起來。
在這裏插入圖片描述zipList因爲是連續的內存空間,所以不需要prev 和 next 指針,空間佔用大幅減少。同樣,比起原來每個節點的一盤散沙,現在zipList是一坨一坨的沙磚,碎片化的問題也解決了。

  • 快速鏈表數據結構:
    struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向壓縮列表
    int32 size; // ziplist 的字節總數
    int16 count; // ziplist 中的元素數量
    int2 encoding; // 存儲形式 2bit,原生字節數組還是 LZF 壓縮存儲

    }
    struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素總數
    int nodes; // ziplist 節點的個數
    int compressDepth; // LZF 算法壓縮深度

    }

quicklist 內部默認單個 ziplist 長度爲 8k 字節,超出了這個字節數,就會新起一個 ziplist。ziplist 的長度由配置參數list-max-ziplist-size決定,是可以配置的。
爲了更進一步的壓縮,在原來zipList的基礎上,還有壓縮的zipList使用 LZF 算法壓縮,可以選擇壓縮深度
struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}
在這裏插入圖片描述quicklist 默認的壓縮深度是 0,也就是不壓縮。壓縮的實際深度由配置參數list-compress-depth決定。爲了支持快速的 push/pop 操作,quicklist 的首尾兩個 ziplist 不壓縮,此時深度就是 1。如果深度爲 2,就表示 quicklist 的首尾第一個 ziplist 以及首尾第二個 ziplist 都不壓縮。

4.2.2 常用命令

lpush/rpush …

從左邊/右邊插入一個或多個值。

lpop/rpop

從左邊/右邊吐出一個值。

值在鍵在,值光鍵亡。

rpoplpush

從列表右邊吐出一個值,插到列表左邊。

lrange

按照索引下標獲得元素(從左到右)

lindex

按照索引下標獲得元素(從左到右)

llen

獲得列表長度

linsert before

在的後面插入 插入值

lrem

從左邊刪除n個value(從左到右)

4.3 Redis五大數據類型–set

Redis set對外提供的功能與list類似是一個列表的功能,特殊之處在於set是可以自動排重的,當你需要存儲一個列表數據,又不希望出現重複數據時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是list所不能提供的。

Redis的Set是string類型的無序集合。它底層其實是一個value爲null的hash表,所以添加,刪除,查找的複雜度都是O(1)。

4.3.1 底層實現原理

Set的實現參考哈希的底層實現,Set就是Value都爲空的Hash。

4.3.2 常用命令

sadd …

將一個或多個 member 元素加入到集合 key 當中,已經存在於集合的 member 元素將被忽略。

smembers

取出該集合的所有值。

sismember

判斷集合是否爲含有該值,有返回1,沒有返回0

scard

返回該集合的元素個數。

srem …

刪除集合中的某個元素。

spop

隨機從該集合中吐出一個值。

srandmember

隨機從該集合中取出n個值。

不會從集合中刪除

sinter

返回兩個集合的交集元素。

sunion

返回兩個集合的並集元素。

sdiff

返回兩個集合的差集元素。

4.4 Redis五大數據類型–hash

Redis hash 是一個鍵值對集合。

Redis hash是一個string類型的field和value的映射表,hash特別適合用於存儲對象。

類似Java裏面的Map

4.4.1 底層實現原理

使用字典作爲存儲結構:

dict 結構內部包含兩個 hashtable,通常情況下只有一個 hashtable 是有值的。但是在 dict 擴容縮容時,需要分配新的 hashtable,然後進行漸進式搬遷,這時候兩個 hashtable 存儲的分別是舊的 hashtable 和新的 hashtable。待搬遷結束後,舊的 hashtable 被刪除,新的 hashtable 取而代之。
在這裏插入圖片描述所以,字典數據結構的精華就落在了 hashtable 結構上了。hashtable 的結構和 Java 的 HashMap 幾乎是一樣的,都是通過分桶的方式解決 hash 衝突。第一維是數組,第二維是鏈表。數組中存儲的是第二維鏈表的第一個元素的指針。

struct dictEntry {
void
key;
void
val;
dictEntry* next; // 鏈接下一個 entry
}
struct dictht {
dictEntry** table; // 二維
long size; // 第一維數組的長度
long used; // hash 表中的元素個數

}**

4.4.1.1 Redis擴容 漸進式Rehash

大字典的擴容是比較耗時間的,需要重新申請新的數組,然後將舊字典所有鏈表中的元素重新掛接到新的數組下面,這是一個O(n)級別的操作,作爲單線程的Redis表示很難承受這樣耗時的過程。步子邁大了會扯着蛋,所以Redis使用漸進式rehash小步搬遷。雖然慢一點,但是肯定可以搬完。
在這裏插入圖片描述搬遷操作埋伏在當前字典的後續指令中(來自客戶端的hset/hdel指令等),實際上在redis中每一個增刪改查命令中都會判斷數據庫字典中的哈希表是否正在進行漸進式rehash,如果是則幫助執行一次。但是有可能客戶端閒下來了,沒有了後續指令來觸發這個搬遷,那麼Redis就置之不理了麼?當然不會,優雅的Redis怎麼可能設計的這樣潦草。Redis還會在定時任務中對字典進行主動搬遷。

  • redis使用的Hash函數:
    hashtable 的性能好不好完全取決於 hash 函數的質量。hash 函數如果可以將 key 打散的比較均勻,那麼這個 hash 函數就是個好函數。Redis 的字典默認的 hash 函數是 siphash。siphash 算法即使在輸入 key 很小的情況下,也可以產生隨機性特別好的輸出,而且它的性能也非常突出。對於 Redis 這樣的單線程來說,字典數據結構如此普遍,字典操作也會非常頻繁,hash 函數自然也是越快越好。

  • 擴容條件:
    正常情況下,當 hash 表中元素的個數等於第一維數組的長度時,就會開始擴容,擴容的新數組是原數組大小的 2 倍。不過如果 Redis 正在做 bgsave,爲了減少內存頁的過多分離 (Copy On Write),Redis 儘量不去擴容 (dict_can_resize),但是如果 hash 表已經非常滿了,元素的個數已經達到了第一維數組長度的 5 倍 (dict_force_resize_ratio),說明 hash 表已經過於擁擠了,這個時候就會強制擴容。

  • 縮容條件:
    當 hash 表因爲元素的逐漸刪除變得越來越稀疏時,Redis 會對 hash 表進行縮容來減少 hash 表的第一維數組空間佔用。縮容的條件是元素個數低於數組長度的 10%。縮容不會考慮 Redis 是否正在做 bgsave。

4.4.2 常用命令

hset

給集合中的 鍵賦值

hget

從集合 取出 value

hmset …

批量設置hash的值

hexists key

查看哈希表 key 中,給定域 field 是否存在。

hkeys

列出該hash集合的所有field

hvals

列出該hash集合的所有value

hincrby

爲哈希表 key 中的域 field 的值加上增量 increment

hsetnx

將哈希表 key 中的域 field 的值設置爲 value ,當且僅當域 field 不存在 .

4.5 Redis五大數據類型–zset (sorted set)

Redis有序集合zset與普通集合set非常相似,是一個沒有重複元素的字符串集合。不同之處是有序集合的每個成員都關聯了一個評分(score) ,這個評分(score)被用來按照從最低分到最高分的方式排序集合中的成員。集合的成員是唯一的,但是評分可以是重複了 。

因爲元素是有序的, 所以你也可以很快的根據評分(score)或者次序(position)來獲取一個範圍的元素。訪問有序集合的中間元素也是非常快的,因此你能夠使用有序集合作爲一個沒有重複成員的智能列表。

4.5.1 底層原理

它的內部實現用的是一種叫着「跳躍鏈表」的數據結構。
因爲 zset 要支持隨機的插入和刪除,所以它不好使用數組來表示(數組的隨機插入刪除效率太低)。所以肯定要用鏈表。但zset的特點就是有一個Score值,Zset又是有序的,每次插入新元素要插入到適當的位置而不是無腦追加到末尾,也就是插入前肯定要定位。二分查找法對象只能是數組(因爲有索引下標),這時候跳躍鏈表(skipList)就閃亮登場了。
關於跳躍鏈表,《Redis深度歷險》這本書舉得例子挺好,我就直接粘過來了:

想想一個創業公司,剛開始只有幾個人,團隊成員之間人人平等,都是聯合創始人。隨着公司的成長,人數漸漸變多,團隊溝通成本隨之增加。這時候就會引入組長制,對團隊進行劃分。每個團隊會有一個組長。開會的時候分團隊進行,多個組長之間還會有自己的會議安排。公司規模進一步擴展,需要再增加一個層級 —— 部門,每個部門會從組長列表中推選出一個代表來作爲部長。部長們之間還會有自己的高層會議安排。
跳躍列表就是類似於這種層級制,最下面一層所有的元素都會串起來。然後每隔幾個元素挑選出一個代表來,再將這幾個代表使用另外一級指針串起來。然後在這些代表裏再挑出二級代表,再串起來。最終就形成了金字塔結構。 想想你老家在世界地圖中的位置:亞洲–>中國->安徽省->安慶市->樅陽縣->湯溝鎮->田間村->xxxx號,也是這樣一個類似的結構。

在這裏插入圖片描述

「跳躍列表」之所以「跳躍」,是因爲內部的元素可能「身兼數職」,比如上圖中間的這個元素,同時處於 L0、L1 和 L2 層,可以快速在不同層次之間進行「跳躍」。
定位插入點時,先在頂層進行定位,然後下潛到下一級定位,一直下潛到最底層找到合適的位置,將新元素插進去。你也許會問,那新插入的元素如何纔有機會「身兼數職」呢?
跳躍列表採取一個隨機策略來決定新元素可以兼職到第幾層。
首先 L0 層肯定是 100% 了,L1 層只有 50% 的概率,L2 層只有 25% 的概率,L3 層只有 12.5% 的概率,一直隨機到最頂層 L31 層。絕大多數元素都過不了幾層,只有極少數元素可以深入到頂層。列表中的元素越多,能夠深入的層次就越深,能進入到頂層的概率就會越大。

查找過程:
在這裏插入圖片描述

  • 隨機層數:
    對於每一個新插入的節點,都需要調用一個隨機算法給它分配一個合理的層數。直觀上期望的目標是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最頂層2^-63,因爲這裏每一層的晉升概率是 50%。
    不過 Redis 標準源碼中的晉升概率只有 25%,也就是代碼中的 ZSKIPLIST_P 的值。所以官方的跳躍列表更加的扁平化,層高相對較低,在單個層上需要遍歷的節點數量會稍多一點。
    也正是因爲層數一般不高,所以遍歷的時候從頂層開始往下遍歷會非常浪費。跳躍列表會記錄一下當前的最高層數maxLevel,遍歷時從這個 maxLevel 開始遍歷性能就會提高很多。

  • 插入過程:
    首先我們在搜索合適插入點的過程中將「搜索路徑」摸出來了,然後就可以開始創建新節點了,創建的時候需要給這個節點隨機分配一個層數,再將搜索路徑上的節點和這個新節點通過前向後向指針串起來。如果分配的新節點的高度高於當前跳躍列表的最大高度,就需要更新一下跳躍列表的最大高度。

  • 刪除過程:
    刪除過程和插入過程類似,都需先把這個「搜索路徑」找出來。然後對於每個層的相關節點都重排一下前向後向指針就可以了。同時還要注意更新一下最高層數maxLevel。

  • 更新過程:
    當我們調用 zadd 方法時,如果對應的 value 不存在,那就是插入過程。如果這個 value 已經存在了,只是調整一下 score 的值,那就需要走一個更新的流程。假設這個新的 score 值不會帶來排序位置上的改變,那麼就不需要調整位置,直接修改元素的 score 值就可以了。但是如果排序位置改變了,那就要調整位置。
    一個簡單的策略就是先刪除這個元素,再插入這個元素,需要經過兩次路徑搜索。Redis 就是這麼幹的。 不過 Redis 遇到 score 值改變了就直接刪除再插入,不會去判斷位置是否。

  • 特殊情況:如果Score都一樣
    在一個極端的情況下,zset 中所有的 score 值都是一樣的,zset 的查找性能會退化爲 O(n) 麼?Redis 作者自然考慮到了這一點,所以 zset 的排序元素不只看 score 值,如果 score 值相同還需要再比較 value 值 (字符串比較)。

4.5.2 常用命令

zadd …

將一個或多個 member 元素及其 score 值加入到有序集 key 當中。

zrange [WITHSCORES]

返回有序集 key 中,下標在 之間的元素

帶WITHSCORES,可以讓分數一起和值返回到結果集。

zrangebyscore key min max [withscores] [limit offset count]

返回有序集 key 中,所有 score 值介於 min 和 max 之間(包括等於 min 或 max )的成員。有序集成員按 score 值遞增(從小到大)次序排列。

zrevrangebyscore key max min [withscores] [limit offset count]

同上,改爲從大到小排列。

zincrby

爲元素的score加上增量

zrem

刪除該集合下,指定值的元素

zcount

統計該集合,分數區間內的元素個數

zrank

返回該值在集合中的排名,從0開始。




5 Redis的事務

5.1 事務基礎

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。事務可以一次執行多個命令, 並且帶有以下兩個重要的保證:

事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。

事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

EXEC 命令負責觸發並執行事務中的所有命令:

如果客戶端在使用 MULTI 開啓了一個事務之後,卻因爲斷線而沒有成功執行 EXEC ,那麼事務中的所有命令都不會被執行。
另一方面,如果客戶端成功在開啓事務之後執行 EXEC ,那麼事務中的所有命令都會被執行。

當使用 AOF 方式做持久化的時候, Redis 會使用單個 write(2) 命令將事務寫入到磁盤中。

然而,如果 Redis 服務器因爲某些原因被管理員殺死,或者遇上某種硬件故障,那麼可能只有部分事務命令會被成功寫入到磁盤中。

如果 Redis 在重新啓動時發現 AOF 文件出了這樣的問題,那麼它會退出,並彙報一個錯誤。

使用redis-check-aof程序可以修復這一問題:它會移除 AOF 文件中不完整事務的信息,確保服務器可以順利啓動。

從 2.2 版本開始,Redis 還可以通過樂觀鎖(optimistic lock)實現 CAS (check-and-set)操作。

MULTI 命令用於開啓一個事務,它總是返回 OK 。 MULTI 執行之後, 客戶端可以繼續向服務器發送任意多條命令, 這些命令不會立即被執行, 而是被放到一個隊列中, 當 EXEC命令被調用時, 所有隊列中的命令纔會被執行。

另一方面, 通過調用 DISCARD , 客戶端可以清空事務隊列, 並放棄執行事務。

以下是一個事務例子, 它原子地增加了 foo 和 bar 兩個鍵的值:

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

EXEC 命令的回覆是一個數組, 數組中的每個元素都是執行事務中的命令所產生的回覆。 其中, 回覆元素的先後順序和命令發送的先後順序一致。

當客戶端處於事務狀態時, 所有傳入的命令都會返回一個內容爲 QUEUED 的狀態回覆(status reply), 這些被入隊的命令將在 EXEC 命令被調用時執行。

5.2 事務中的錯誤

使用事務時可能會遇上以下兩種錯誤:

事務在執行 EXEC 之前,入隊的命令可能會出錯。比如說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其他更嚴重的錯誤,比如內存不足(如果服務器使用 maxmemory 設置了最大內存限制的話)。
命令可能在 EXEC 調用之後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。

對於發生在 EXEC 執行之前的錯誤,客戶端以前的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那麼入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那麼大部分客戶端都會停止並取消這個事務。

不過,從 Redis 2.6.5 開始,服務器會對命令入隊失敗的情況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。

在 Redis 2.6.5 以前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。 而新的處理方式則使得在流水線(pipeline)中包含事務變得簡單,因爲發送事務和讀取事務的回覆都只需要和服務器進行一次通訊。

至於那些在 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行。

從協議的角度來看這個問題,會更容易理解一些。 以下例子中, LPOP 命令的執行將出錯, 儘管調用它的語法是正確的:

> Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
> -ERR Operation against a key holding the wrong kind of value

EXEC 返回兩條bulk-string-reply: 第一條是 OK ,而第二條是 -ERR 。 至於怎樣用合適的方法來表示事務中的錯誤, 則是由客戶端自己決定的。

最重要的是記住這樣一條, 即使事務中有某條/某些命令執行失敗了, 事務隊列中的其他命令仍然會繼續執行 —— Redis 不會停止執行事務中的命令

以下例子展示的是另一種情況, 當命令在入隊時產生錯誤, 錯誤會立即被返回給客戶端:

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for ‘incr’ command

因爲調用 INCR 命令的參數格式不正確, 所以這個 INCR 命令入隊失敗。

5.5 樂觀鎖(watch)

watch 會在事務開始之前盯住 1 個或多個關鍵變量,當事務執行時,也就是服務器收到了 exec 指令要順序執行緩存的事務隊列時,Redis 會檢查關鍵變量自 watch 之後,是否被修改了 (包括當前事務所在的客戶端)。如果關鍵變量被人動過了,exec 指令就會返回 null 回覆告知客戶端事務執行失敗,這個時候客戶端一般會選擇重試。

> watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事務執行失敗
(nil)

5.4 事務總結

綜上所述,筆者認爲Redis是部分支持事務的,當語法沒有錯誤時,若遇到異常,會執行餘下事務,當語法有錯誤時,會整批報錯,不執行。

6 Redis的配置

在linux中 vim redis.conf可以看見一個900餘行的配置文件,下面介紹常用的30餘個配置:

1、redis默認不是以守護進程的方式運行,可以通過該配置項修改,使用yes啓用守護進程:

daemonize no

2、當redis以守護進程方式運行時,redis默認會把pid寫入/var/run/redis.pid文件,可以通過pidfile指定:

pidfile /var/run/redis.pid

3、指定redis監聽端口,默認端口號爲6379,作者在自己的一篇博文中解析了爲什麼選用6379作爲默認端口,因爲6379在手機按鍵上MERZ對應的號碼,而MERZ取自意大利女歌手Alessia Merz的名字:

port 6379

4、設置tcp的backlog,backlog是一個連接隊列,backlog隊列總和=未完成三次握手隊列+已完成三次握手隊列。在高併發環境下你需要一個高backlog值來避免慢客戶端連接問題。注意Linux內核會將這個值減小到/proc/sys/net/core/somaxconn 的值,所以需要確認增大somaxconn和tcp_max_syn_backlog兩個值來達到想要的

效果:

tcp-backlog 511

5、綁定的主機地址:

bind 127.0.0.1

6、當客戶端閒置多長時間後關閉連接,如果指定爲0,表示永不關閉:

timeout 300

7、設置檢測客戶端網絡中斷時間間隔,單位爲秒,如果設置爲0,則不檢測,建議設置爲60:

tcp-keepalive 0

8、指定日誌記錄級別,redis總共支持四個級別:debug、verbose、notice、warning,默認爲verbose:

loglevel verbose

9、日誌記錄方式,默認爲標準輸出,如果配置redis爲守護進程方式運行,而這裏又配置爲日誌記錄方式爲標準輸出,則日誌將會發送給/dev/null:

logfile stdout

10、設置數據庫數量,默認值爲16,默認當前數據庫爲0,可以使用select命令在連接上指定數據庫id:

databases 16

11、指定在多長時間內,有多少次更新操作,就將數據同步到數據文件,可以多個條件配合:

save
save 300 10:表示300秒內有10個更改就將數據同步到數據文件

12、指定存儲至本地數據庫時是否壓縮數據,默認爲yes,redis採用LZF壓縮,如果爲了節省CPU時間,可以關閉該選項,但會導致數據庫文件變得巨大:

rdbcompssion yes

13、指定本地數據庫文件名,默認值爲dump.rdb:

dbfilename dump.rdb

14、指定本地數據庫存放目錄:

dir ./

15、設置當本機爲slave服務時,設置master服務的IP地址及端口,在redis啓動時,它會自動從master進行數據同步:

slaveof

16、當master服務設置了密碼保護時,slave服務連接master的密碼:

masterauth

17、設置redis連接密碼,如果配置了連接密碼,客戶端在連接redis時需要通過auth 命令提供密碼,默認關閉:

requirepass foobared

18、設置同一時間最大客戶端連接數,默認無限制,redis可以同時打開的客戶端連接數爲redis進程可以打開的最大文件描述符數,如果設置maxclients 0,表示不作限制。當客 戶端連接數到達限制時,redis會關閉新的連接並向客戶端返回 max number of clients reached錯誤消息:

maxclients 128

19、指定redis最大內存限制,redis在啓動時會把數據加載到內存中,達到最大內存後,redis會先嚐試清除已到期或即將到期的key,當次方法處理後,仍然到達最大內存設置,將無法再進行寫入操作,但仍然可以進行讀取操作。Redis新的vm機制, 會把key存放內存,value會存放在swap區:

maxmemory

20、設置緩存過期策略,有6種選擇:(LRU算法最近最少使用)

volatile-lru:使用LRU算法移除key,只對設置了過期時間的key;

allkeys-lru:使用LRU算法移除key,作用對象所有key;

volatile-random:在過期集合key中隨機移除key,只對設置了過期時間的key;

allkeys-random:隨機移除key,作用對象爲所有key;

volarile-ttl:移除哪些ttl值最小即最近要過期的key;

noeviction:永不過期,針對寫操作,會返回錯誤信息。

maxmemory-policy noeviction

21、指定是否在每次更新操作後進行日誌記錄,redis在默認情況下是異步的把數據寫入磁盤,如果不開啓,可能會在斷電時導致一段時間內數據丟失。因爲redis本身同步數據文件是按上面save條件來同步的,所以有的數據會在一段時間內置存在於內存中。默認爲no:

appendonly no

22、指定更新日誌文件名,默認爲appendonly.aof:

appendfilename appendonly.aof

23、指定更新日誌條件,共有3個可選值:

no:表示等操作系統進行數據緩存同步到磁盤(快);

always:表示每次更新操作後手動調用fsync()將數據寫到磁盤(慢,安全);

everysec:表示每秒同步一次(折中,默認值)

appendfsync everysec

24、指定是否啓用虛擬內存機制,默認值爲no,簡單介紹一下,VM機制將數據分頁存放,由redis將訪問量較小的頁即冷數據 swap到磁盤上,訪問多的頁面由磁盤自動換出到內存中:

vm-enabled no

25、虛擬內存文件路徑,默認值爲/tmp/redis.swap,不可多個redis實例共享:

vm-swap-file /tmp/redis.swap

26、將所有大於vm-max-memory的數據存入虛擬內存,無論vm-max-memory設置多小,所有索引數據都是內存存儲的(redis的索引數據就是keys),也就是說,當vm-max-memory設置爲0的時候,其實是所有value都存在於磁盤。默認值爲 0:

vm-max-memory 0

27、redis swap文件分成了很多的page,一個對象可以保存在多個page上面,但一個page上不能被多個對象共享,vm-page-size是根據存儲的數據大小來設定的,作者建議如果儲存很多小對象,page大小最好設置爲32或者64bytes;如果存儲很多大對象,則可以使用更大的page,如果不確定,就使用默認值:

vm-page-size 32

28、設置swap文件中page數量,由於頁表(一種表示頁面空閒或使用的bitmap)是放在內存中的,在磁盤上每8個pages將消耗1byte的內存:

vm-pages 134217728

29、設置訪問swap文件的線程數,最好不要超過機器的核數,如果設置爲0,那麼所有對swap文件的操作都是串行的,可能會造成長時間的延遲。默認值爲4:

vm-max-threads 4

30、設置在客戶端應答時,是否把較小的包含併爲一個包發送,默認爲開啓:

glueoutputbuf yes

31、指定在超過一定數量或者最大的元素超過某一臨界值時,採用一種特殊的哈希算法:

hash-max-zipmap-entries 64

hash-max-zipmap-value 512

32、指定是否激活重置hash,默認開啓:

activerehashing yes

33、指定包含其他配置文件,可以在同一主機上多個redis實例之間使用同一份配置文件,而同時各個實例又擁有自己的特定配置文件:

include /path/to/local.conf

7 Redis 兩大持久化策略:RDB 與AOF

Redis 提供了不同級別的持久化方式:

RDB(RedisDataBase)持久化方式能夠在指定的時間間隔能對你的數據進行快照存儲.

AOF(AppendOnlyFile)持久化方式記錄每次對服務器寫的操作,當服務器重啓的時候會重新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操作到文件末尾.Redis還能對AOF文件進行後臺重寫,使得AOF文件的體積不至於過大.

如果你只希望你的數據在服務器運行的時候存在,你也可以不使用任何持久化方式.
你也可以同時開啓兩種持久化方式, 在這種情況下, 當redis重啓的時候會優先載入AOF文件來恢復原始的數據,因爲在通常情況下AOF文件保存的數據集要比RDB文件保存的數據集要完整.
最重要的事情是瞭解RDB和AOF持久化方式的不同,讓我們以RDB持久化方式開始:

7.1 RDB(RedisDataBase)

7.1.1 RDB優點

  • RDB是一個非常緊湊的文件,它保存了某個時間點得數據集,非常適用於數據集的備份,比如你可以在每個小時報保存一下過去24小時內的數據,同時每天保存過去30天的數據,這樣即使出了問題你也可以根據需求恢復到不同版本的數據集.
  • RDB是一個緊湊的單一文件,很方便傳送到另一個遠端數據中心或者亞馬遜的S3(可能加密),非常適用於災難恢復.
  • RDB在保存RDB文件時父進程唯一需要做的就是fork出一個子進程,接下來的工作全部由子進程來做,父進程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
    與AOF相比,在恢復大的數據集的時候,RDB方式會更快一些.

7.1.2 RDB缺點

  • 如果你希望在redis意外停止工作(例如電源中斷)的情況下丟失的數據最少的話,那麼RDB不適合你.雖然你可以配置不同的save時間點(例如每隔5分鐘並且對數據集有100個寫的操作),是Redis要完整的保存整個數據集是一個比較繁重的工作,你通常會每隔5分鐘或者更久做一次完整的保存,萬一在Redis意外宕機,你可能會丟失幾分鐘的數據.
  • RDB 需要經常fork子進程來保存數據集到硬盤上,當數據集比較大的時候,fork的過程是非常耗時的,可能會導致Redis在一些毫秒級內不能響應客戶端的請求.如果數據集巨大並且CPU性能不是很好的情況下,這種情況會持續1秒,AOF也需要fork,但是你可以調節重寫日誌文件的頻率來提高數據集的耐久度.

7.2 AOF(AppendOnlyFile)

7.2.1 AOF優點

  • 使用AOF 會讓你的Redis更加耐久: 你可以使用不同的fsync策略:無fsync,每秒fsync,每次寫的時候fsync.使用默認的每秒fsync策略,Redis的性能依然很好(fsync是由後臺線程進行處理的,主線程會盡力處理客戶端請求),一旦出現故障,你最多丟失1秒的數據.
  • AOF文件是一個只進行追加的日誌文件,所以不需要寫入seek,即使由於某些原因(磁盤空間已滿,寫的過程中宕機等等)未執行完整的寫入命令,你也也可使用redis-check-aof工具修復這些問題.
    Redis 可以在 AOF 文件體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。 整個重寫操作是絕對安全的,因爲 Redis 在創建新 AOF 文件的過程中,會繼續將命令追加到現有的 AOF 文件裏面,即使重寫過程中發生停機,現有的 AOF 文件也不會丟失。 而一旦新 AOF 文件創建完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,並開始對新 AOF 文件進行追加操作。
  • AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕鬆。 導出(export) AOF 文件也非常簡單: 舉個例子, 如果你不小心執行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那麼只要停止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 並重啓 Redis , 就可以將數據集恢復到 FLUSHALL 執行之前的狀態。

7.2.2 AOF缺點

  • 對於相同的數據集來說,AOF 文件的體積通常要大於 RDB 文件的體積。
  • 根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB 。 在一般情況下, 每秒 fsync 的性能依然非常高, 而關閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 可以提供更有保證的最大延遲時間(latency)。

7.3 關於新的混合式備份

7.3.1 開啓混合持久化

4.0版本的混合持久化默認關閉的,通過aof-use-rdb-preamble配置參數控制,yes則表示開啓,no表示禁用,默認是禁用的,可通過config set修改。
在這裏插入圖片描述

7.3.2 混合持久化過程

瞭解了AOF持久化過程和RDB持久化過程以後,混合持久化過程就相對簡單了。

混合持久化同樣也是通過bgrewriteaof完成的,不同的是當開啓混合持久化時,fork出的子進程先將共享的內存副本全量的以RDB方式寫入aof文件,然後在將重寫緩衝區的增量命令以AOF方式寫入到文件,寫入完成後通知主進程更新統計信息,並將新的含有RDB格式和AOF格式的AOF文件替換舊的的AOF文件。簡單的說:新的AOF文件前半段是RDB格式的全量數據後半段是AOF格式的增量數據

7.3.3 數據恢復過程

當我們開啓了混合持久化時,啓動redis依然優先加載aof文件,aof文件加載可能有兩種情況如下:

aof文件開頭是rdb的格式, 先加載 rdb內容再加載剩餘的 aof。
aof文件開頭不是rdb的格式,直接以aof格式加載整個文件。

8 Redis高可用主從複製集羣

8.1 同步方式

8.1.1 增量同步

Redis 同步的是指令流,主節點會將那些對自己的狀態產生修改性影響的指令記錄在本地的內存 buffer 中,然後異步將 buffer 中的指令同步到從節點,從節點一邊執行同步的指令流來達到和主節點一樣的狀態,一遍向主節點反饋自己同步到哪裏了 (偏移量)。
因爲內存的 buffer 是有限的,所以 Redis 主庫不能將所有的指令都記錄在內存 buffer 中。Redis 的複製內存 buffer 是一個定長的環形數組,如果數組內容滿了,就會從頭開始覆蓋前面的內容。
在這裏插入圖片描述如果因爲網絡狀況不好,從節點在短時間內無法和主節點進行同步,那麼當網絡狀況恢復時,Redis 的主節點中那些沒有同步的指令在 buffer 中有可能已經被後續的指令覆蓋掉了,從節點將無法直接通過指令流來進行同步,這個時候就需要用到更加複雜的同步機制 —— 快照同步。

8.1.2 快照同步

快照同步是一個非常耗費資源的操作,它首先需要在主庫上進行一次 bgsave 將當前內存的數據全部快照到磁盤文件中,然後再將快照文件的內容全部傳送到從節點。從節點將快照文件接受完畢後,立即執行一次全量加載,加載之前先要將當前內存的數據清空。加載完畢後通知主節點繼續進行增量同步。
在整個快照同步進行的過程中,主節點的複製 buffer 還在不停的往前移動,如果快照同步的時間過長或者複製 buffer 太小,都會導致同步期間的增量指令在複製 buffer 中被覆蓋,這樣就會導致快照同步完成後無法進行增量複製,然後會再次發起快照同步,如此極有可能會陷入快照同步的死循環。在這裏插入圖片描述當從節點剛剛加入到集羣時,它必須先要進行一次快照同步,同步完成後再繼續進行增量同步。

常用3招

  • 一主二僕
    在這裏插入圖片描述

    一個Master,兩個Slave,Slave只能讀不能寫;當Slave與Master斷開後需要重新slave of連接纔可建立之前的主從關係;Master掛掉後,Master關係依然存在,Master重啓即可恢復。

  • 薪火相傳
    在這裏插入圖片描述
    上一個Slave可以是下一個Slave的Master,Slave同樣可以接收其他slaves的連接和同步請求,那麼該slave作爲了

鏈條中下一個slave的Master,如此可以有效減輕Master的寫壓力。如果slave中途變更轉向,會清除之前的數據,重新

建立最新的。

  • 反客爲主

當Master掛掉後,Slave可鍵入命令 slaveof no one使當前redis停止與其他Master redis數據同步,轉成

Master redis。

四、複製原理
  • 1、Slave啓動成功連接到master後會發送一個sync命令;

  • 2、Master接到命令啓動後的存盤進程,同時收集所有接收到的用於修改數據集命令,在後臺進程執行完畢之後,master 將傳送整個數據文件到slave,以完成一次完全同步;

  • 3、全量複製:而slave服務在數據庫文件數據後,將其存盤並加載到內存中;

  • 4、增量複製:Master繼續將新的所有收集到的修改命令依次傳給slave,完成同步;

  • 5、但是隻要是重新連接master,一次完全同步(全量複製)將被自動執行。

五、哨兵模式(sentinel)
   反客爲主的自動版,能夠後臺監控Master庫是否故障,如果故障了根據投票數自動將slave庫轉換爲主庫。一組sentinel能

   同時監控多個Master。

   使用步驟:

   1、在Master對應redis.conf同目錄下新建sentinel.conf文件,名字絕對不能錯;

   2、配置哨兵,在sentinel.conf文件中填入內容:

         sentinel monitor 被監控數據庫名字(自己起名字) ip port 1

         說明:上面最後一個數字1,表示主機掛掉後slave投票看讓誰接替成爲主機,得票數多少後成爲主機。

  3、啓動哨兵模式:

        命令鍵入:redis-sentinel  /myredis/sentinel.conf

       注:上述sentinel.conf路徑按各自實際情況配置
六、複製的缺點

延時,由於所有的寫操作都是在Master上操作,然後同步更新到Slave上,所以從Master同步到Slave機器有一定的延遲,當系統很繁忙的時候,延遲問題會更加嚴重,Slave機器數量的增加也會使得這個問題更加嚴重。

9 Redis高級數據結構

9.1 位圖

其實不該把位圖放在“高級數據結構”子項的,因爲位圖不是特殊的數據結構,它的內容其實就是普通的字符串,也就是 byte 數組。我們可以使用普通的 get/set 直接獲取和設置整個位圖的內容,也可以使用位圖操作 getbit/setbit 等將 byte 數組看成「位數組」來處理。

位圖可以很好的節約空間,有的數據只有2種情況,比如記錄一個用戶是否登錄。可以用String可以記錄,記1爲登錄,0爲未登錄,可以01100111。。。這樣記錄,但很耗空間。若我們用位數組來存,一個頂八個,因爲即使是首個0,它也是八位的,已經夠我們記錄8天的登錄情況了!

  • 統計和查找
    Redis 提供了位圖統計指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統計指定位置範圍內 1 的個數,bitpos 用來查找指定範圍內出現的第一個 0 或 1。
    比如我們可以通過 bitcount 統計用戶一共簽到了多少天,通過 bitpos 指令查找用戶從哪一天開始第一次簽到。如果指定了範圍參數[start, end],就可以統計在某個時間範圍內用戶簽到了多少天,用戶自某天以後的哪天開始簽到。
    遺憾的是, start 和 end 參數是字節索引,也就是說指定的位範圍必須是 8 的倍數,而不能任意指定。這很奇怪。因爲這個設計,我們無法直接計算某個月內用戶簽到了多少天,而必須要將這個月所覆蓋的字節內容全部取出來 (getrange 可以取出字符串的子串) 然後在內存裏進行統計,這個非常繁瑣。

9.2 HyperLogLog

HyperLogLog 提供不精確的去重計數方案,雖然不精確但是也不是非常不精確,標準誤差是 0.81%,這樣的精確度已經可以滿足上面的 UV 統計需求了。

9.2.1 使用場景

記錄網站的不同客戶的訪問量(同一個用戶多次登錄也只算一次)

9.2.2 使用方法

HyperLogLog 提供了兩個指令 pfadd 和 pfcount,根據字面意義很好理解,一個是增加計數,一個是獲取計數。pfadd 用法和 set 集合的 sadd 是一樣的,來一個用戶 ID,就將用戶 ID 塞進去就是。pfcount 和 scard 用法是一樣的,直接獲取計數值。

HyperLogLog 除了上面的 pfadd 和 pfcount 之外,還提供了第三個指令 pfmerge,用於將多個 pf 計數值累加在一起形成一個新的 pf 值。
比如在網站中我們有兩個內容差不多的頁面,運營說需要這兩個頁面的數據進行合併。其中頁面的 UV 訪問量也需要合併,那這個時候 pfmerge 就可以派上用場了。

9.2.3 注意事項

它需要佔據一定 12k 的存儲空間,所以它不適合統計單個用戶相關的數據。如果你的用戶上億,可以算算,這個空間成本是非常驚人的。但是相比 set 存儲方案,HyperLogLog 所使用的空間那真是可以使用千斤對比四兩來形容了。

9.3 布隆過濾器

上一節我們學會了使用 HyperLogLog 數據結構來進行估數,它非常有價值,可以解決很多精確度不高的統計需求。
但是如果我們想知道某一個值是不是已經在 HyperLogLog 結構裏面了,它就無能爲力了,它只提供了 pfadd 和 pfcount 方法,沒有提供 pfcontains 這種方法。

布隆過濾器可以理解爲一個不怎麼精確的 set 結構,當你使用它的 contains 方法判斷某個對象是否存在時,它可能會誤判。但是布隆過濾器也不是特別不精確,只要參數設置的合理,它的精確度可以控制的相對足夠精確,只會有小小的誤判概率。

當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。打個比方,當它說不認識你時,肯定就不認識;當它說見過你時,可能根本就沒見過面,不過因爲你的臉跟它認識的人中某臉比較相似 (某些熟臉的係數組合),所以誤判以前見過你。

9.3.1 使用場景

我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,它每次推薦時要去重,去掉那些已經看過的內容。新聞客戶端推薦系統如何實現推送去重?

9.3.2 使用方法

Redis 官方提供的布隆過濾器到了 Redis 4.0 提供了插件功能之後才正式登場。布隆過濾器作爲一個插件加載到 Redis Server 中,給 Redis 提供了強大的布隆去重功能。

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

雖然在Redis中使用挺方便的,但作爲一個JAVA開發,在JAVA代碼中並沒有太好的辦法使用布隆過濾器,筆者在RedisTemplate並沒有找到布隆過濾器的操作方法。
只能引入jar包

<dependency>
		<groupId>com.redislabs</groupId>
		<artifactId>jrebloom</artifactId>
		<version>1.0.1</version>
</dependency>

在這裏插入圖片描述在這裏插入圖片描述

9.3.3 布隆過濾器的原理

每個布隆過濾器對應到 Redis 的數據結構裏面就是一個大型的位數組和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的 hash 值算得比較均勻。

向布隆過濾器中添加 key 時,會使用多個 hash 函數對 key 進行 hash 算得一個整數索引值然後對位數組長度進行取模運算得到一個位置,每個 hash 函數都會算得一個不同的位置。再把位數組的這幾個位置都置爲 1 就完成了 add 操作。
向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位數組中這幾個位置是否都位 1,只要有一個位爲 0,那麼說明布隆過濾器中這個 key 不存在。如果都是 1,這並不能說明這個 key 就一定存在,只是極有可能存在,因爲這些位被置爲 1 可能是因爲其它的 key 存在所致。如果這個位數組比較稀疏,這個概率就會很大,如果這個位數組比較擁擠,這個概率就會降低。具體的概率計算公式比較複雜。

使用時不要讓實際元素遠大於初始化大小,當實際元素開始超出初始化大小時,應該對布隆過濾器進行重建,重新分配一個 size 更大的過濾器,再將所有的歷史元素批量 add 進去 (這就要求我們在其它的存儲器中記錄所有的歷史元素)。因爲 error_rate 不會因爲數量超出就急劇增加,這就給我們重建過濾器提供了較爲寬鬆的時間。
在這裏插入圖片描述

9.3.4 其他應用

在爬蟲系統中,我們需要對 URL 進行去重,已經爬過的網頁就可以不用爬了。但是 URL 太多了,幾千萬幾個億,如果用一個集合裝下這些 URL 地址那是非常浪費空間的。這時候就可以考慮使用布隆過濾器。它可以大幅降低去重存儲消耗,只不過也會使得爬蟲系統錯過少量的頁面。
布隆過濾器在 NoSQL 數據庫領域使用非常廣泛,我們平時用到的 HBase、Cassandra 還有 LevelDB、RocksDB 內部都有布隆過濾器結構,布隆過濾器可以顯著降低數據庫的 IO 請求數量。當用戶來查詢某個 row 時,可以先通過內存中的布隆過濾器過濾掉大量不存在的 row 請求,然後再去磁盤進行查詢。
郵箱系統的垃圾郵件過濾功能也普遍用到了布隆過濾器,因爲用了這個過濾器,所以平時也會遇到某些正常的郵件被放進了垃圾郵件目錄中,這個就是誤判所致,概率很低。

10 SpringBoot上使用

11 Redis實現分佈式鎖

RedisTemplate
待填坑 2019.4.28

彩蛋:關於Redis爲什麼是單線程?

在這裏插入圖片描述

在這裏插入圖片描述

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