redis常用結構和奇淫技巧

Redis可以做什麼

  1. 記錄帖子的點贊數、評論數和點擊數 (hash)。

  2. 記錄用戶的帖子 ID 列表 (排序),便於快速顯示用戶的帖子列表 (zset)。

  3. 記錄帖子的標題、摘要、作者和封面信息,用於列表頁展示 (hash)。

  4. 記錄帖子的點贊用戶 ID 列表,評論 ID 列表,用於顯示和去重計數 (zset)。

  5. 緩存近期熱帖內容 (帖子內容空間佔用比較大),減少數據庫壓力 (hash)。

  6. 記錄帖子的相關文章 ID,根據內容推薦相關帖子 (list)。

  7. 如果帖子 ID 是整數自增的,可以使用 Redis 來分配帖子 ID(計數器)。

  8. 收藏集和帖子之間的關係 (zset)。

  9. 記錄熱榜帖子 ID 列表,總熱榜和分類熱榜 (zset)。

  10. 緩存用戶行爲歷史,進行惡意行爲過濾 (zset,hash)。

  11. 數據推送去重Bloom filter

  12. pv,uv統計


Redis5種基本數據對象


他們是:字符串+列表+集合+哈希+有序集合

String字符串
比如緩存用戶信息,需要使用JSON序列化成字符串存入緩存。取用戶信息時又會經過一次反序列化的過程。

redis是動態字符串,最大512M,小於1M時加倍現有空間,大於1M時加倍1M的空間

set key userinfo;
get key;


批量對多個字符串讀寫,節省網絡耗時

mset key1 value1 key2 value2
mget key1 key2

設置key的過期時間-涉及到過期策略

##  設置5秒過期時間
set key1 value1
expire key1 5
##  合併set+expire
setex key2 5 value2
##  如果存在key3就不執行
setnx  key3 value3

計數功能
超過sigined long的最大值最小值,會報錯

## 自增key1的值
set key1 50;
incr key1; 
## 合併指令
incrby key1 50;


list列表-相當於LinkedList
插入刪除塊,查找慢
作用:常用作異步隊列使用。將需要延後處理的任務序列化成字符串放入redis,另一個線程從列表輪詢進行執行。

隊列-右邊進左邊出

rpush key1 value1 value2 value3
lpop key1
"value1"
lpop key1
"value2"
llen key1

棧-右邊進右邊出

rpush key1 value1 value2 value3
rpop key1
"value3"
lpop key1
"value2"

ltrim截取
##截取這個區間的值(0表示第一個數 -1表示最後一個數)
ltrim start_index end_index   

快速例表:ziplist<——>ziplist<——>ziplist<——>ziplist<——>ziplist

hash字典-相當於hashmap
redis字典只能是字符串,與hashmap的重哈希方式不一樣,redis爲了提高性能,採用了漸進式的rehash策略
漸進式的rehash會保留新舊兩個hash結構,然後在後續定時任務以及hash指令中,逐漸將舊內容遷移到新hash結構。
hash結構也可以存儲用戶信息,不同於String串一次性序列化整個對象,hash字典只序列化某些字段實現部分獲取。

hset object key1 value1;
hset object key2 value2;
hgetall object;
hlen object;
hget object key1;
hincry object key1 1; //字典中單個key也可單獨自增

set集合-相當於hashSet
用來存儲中獎用戶的id,因爲有去重的功能,保證一個用戶不會中將兩次

sadd key1 value1;
sadd key1 value2;
spop key1;
smembers key1          //查詢鍵爲key1的所有值
"value1"
sismember key1 value1; //查詢某value是否存在
scard key1             //查詢鍵爲key1值得個數

zset有序集合-相當於sortedSet
它是一個set保證value得唯一性,而且它可以給每個value賦一個排序權重。
存儲一對多得數據,並且對多的一方按照權重進行排序

存儲粉絲列表,value值是用戶ID,score是關注時間。這樣可以對粉絲列表按關注時間進行排序。
存儲班級學生成績,value值是學生ID,score是他的考試成績。對成績按分數進行排序

zadd banji stu1 56;
zadd banji stu2 69;
zadd banji stu3 80;
zrem banji 80; //刪除value
zrange banji 0 -1; //按score進行排序
zrevrange banji 0 -1; //按score逆序排序
zrangebyscore banji 60 90; //根據分值區間
zcard banji;   //相當於count

容器型數據結構通用規則
list/set/hash/zset

create if not exists
drop if no elements

redis其它常用功能點

分佈式鎖

分佈式鎖本質上要實現的目標就是在 Redis 裏面佔一個“茅坑”,當別的進程也要來佔時,發現已經有人蹲在那裏了,就只好放棄或者稍後再試。

佔坑一般是使用 setnx(set if not exists) 指令,只允許被一個客戶端佔坑,先來先佔, 用完了,再調用 del 指令釋放茅坑。

setnx aobing
expire aobing
del aobing

但是引入後會有問題,原子性,怎麼解決,就比如setnx成功,設置失效時間expire的時候失敗,怎麼辦?

當時出現了賊多的第三方插件,作者爲了解決這個亂象,就在Redis 2.8 版本中作者加入了 set 指令的擴展參數:

set  aobing ture  ex 5 nx
del  aobing

但是這樣還是有問題超時問題,可重入問題等等,這個時候,第三方的一些插件就橫空出世了,Redission ,Jedis,他們的底層我就不過多描述了,都是通過lua腳本去保證的,大致邏輯跟我們代碼實現是差不多的。

就比如去刪除的時候,去校驗是否當前線程鎖定的,就把比較和刪除這樣一些動作都放到一起了:

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

延時隊列

我們平時習慣於使用 Rabbitmq 、RocketMQ和 Kafka 作爲消息隊列中間件,來給應用程序之間增加異步消息傳遞功能。這兩個中間件都是專業的消息隊列中間件,特性之多超出了大多數人的理解能力。

使用過 Rabbitmq 的同學知道它使用起來有多複雜,發消息之前要創建 Exchange,再創建 Queue,還要將 Queue 和 Exchange 通過某種規則綁定起來,發消息的時候要指定 routing-key,還要控制頭部信息。消費者在消費消息之前也要進行上面一系列的繁瑣過程。

但是絕大多數情況下,雖然我們的消息隊列只有一組消費者,但還是需要經歷上面這些繁瑣的過程。

有了 Redis,它就可以讓我們解脫出來,對於那些只有一組消費者的消息隊列,使用 Redis 就可以非常輕鬆的搞定。

Redis 的消息隊列不是專業的消息隊列,它沒有非常多的高級特性,沒有 ack 保證,如果對消息的可靠性有着極致的追求,那麼它就不適合使用。

> rpush notify-queue apple banana pear
(integer) 3
> llen notify-queue
(integer) 3
> lpop notify-queue
"apple"
> llen notify-queue
(integer) 2
> lpop notify-queue
"banana"
> llen notify-queue
(integer) 1
> lpop notify-queue
"pear"
> llen notify-queue
(integer) 0
> lpop notify-queue
(nil)

但是這樣有問題大家發現沒有?隊列會空是吧,那怎麼解決呢?

客戶端是通過隊列的 pop 操作來獲取消息,然後進行處理,處理完了再接着獲取消息,再進行處理。

如此循環往復,這便是作爲隊列消費者的客戶端的生命週期。

可是如果隊列空了,客戶端就會陷入 pop 的死循環,不停地 pop,沒有數據,接着再 pop,又沒有數據,這就是浪費生命的空輪詢。

空輪詢不但拉高了客戶端的 CPU,redis 的 QPS 也會被拉高,如果這樣空輪詢的客戶端有幾十來個,Redis 的慢查詢可能會顯著增多。

解決方式很簡單,讓線程睡一秒 Thread.sleep(1000)

Redis在我開發的一些簡易後臺我確實有用到,因爲我覺得沒必要接入消息隊列中間件,大家平時開發小系統可以試試。

位圖bitmap

在我們平時開發過程中,會有一些 bool 型數據需要存取,比如用戶一年的簽到記錄,簽了是 1,沒簽是 0,要記錄 365 天。如果使用普通的 key/value,每個用戶要記錄 365 個,當用戶上億的時候,需要的存儲空間是驚人的。

爲了解決這個問題,Redis 提供了位圖數據結構,這樣每天的簽到記錄只佔據一個位,365 天就是 365 個位,46 個字節 (一個稍長一點的字符串) 就可以完全容納下,這就大大節約了存儲空間。

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

當我們要統計月活的時候,因爲需要去重,需要使用 set 來記錄所有活躍用戶的 id,這非常浪費內存。

這時就可以考慮使用位圖來標記用戶的活躍狀態。每個用戶會都在這個位圖的一個確定位置上,0 表示不活躍,1 表示活躍。然後到月底遍歷一次位圖就可以得到月度活躍用戶數。

這個類型不僅僅可以用來讓我們改二進制改字符串值,最經典的就是用戶連續簽到。

key 可以設置爲 前綴:用戶id:年月    譬如 setbit sign:123:1909 0 1

代表用戶ID=123簽到,簽到的時間是19年9月份,0代表該月第一天,1代表簽到了

第二天沒有簽到,無需處理,系統默認爲0

第三天簽到  setbit sign:123:1909 2 1

可以查看一下目前的簽到情況,顯示第一天和第三天簽到了,前8天目前共簽到了2天

127.0.0.1:6379> setbit sign:123:1909 0 1
0
127.0.0.1:6379> setbit sign:123:1909 2 1
0
127.0.0.1:6379> getbit sign:123:1909 0
1
127.0.0.1:6379> getbit sign:123:1909 1
0
127.0.0.1:6379> getbit sign:123:1909 2
1
127.0.0.1:6379> getbit sign:123:1909 3
0
127.0.0.1:6379> bitcount sign:123:1909 0 0
2

HyperLogLog

如果統計 PV 那非常好辦,給每個網頁一個獨立的 Redis 計數器就可以了,這個計數器的 key 後綴加上當天的日期。這樣來一個請求,incrby 一次,最終就可以統計出所有的 PV 數據。

但是 UV 不一樣,它要去重,同一個用戶一天之內的多次訪問請求只能計數一次。

這就要求每一個網頁請求都需要帶上用戶的 ID,無論是登陸用戶還是未登陸用戶都需要一個唯一 ID 來標識。

你也許已經想到了一個簡單的方案,那就是爲每一個頁面一個獨立的 set 集合來存儲所有當天訪問過此頁面的用戶 ID。

當一個請求過來時,我們使用 sadd 將用戶 ID 塞進去就可以了。

通過 scard 可以取出這個集合的大小,這個數字就是這個頁面的 UV 數據。沒錯,這是一個非常簡單的方案。

但是,如果你的頁面訪問量非常大,比如一個爆款頁面幾千萬的 UV,你需要一個很大的 set 集合來統計,這就非常浪費空間。

如果這樣的頁面很多,那所需要的存儲空間是驚人的。爲這樣一個去重功能就耗費這樣多的存儲空間,值得麼?其實老闆需要的數據又不需要太精確,105w 和 106w 這兩個數字對於老闆們來說並沒有多大區別,So,有沒有更好的解決方案呢?

HyperLogLog 提供了兩個指令 pfadd 和 pfcount,根據字面意義很好理解,一個是增加計數,一個是獲取計數。

pfadd 用法和 set 集合的 sadd 是一樣的,來一個用戶 ID,就將用戶 ID 塞進去就是,pfcount 和 scard 用法是一樣的,直接獲取計數值。

127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 1
127.0.0.1:6379> pfadd codehole user2
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 2
127.0.0.1:6379> pfadd codehole user3
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 3
127.0.0.1:6379> pfadd codehole user4
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 4
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10

pfadd 這個 pf 是什麼意思?

 

它是 HyperLogLog 這個數據結構的發明人 Philippe Flajolet 的首字母縮寫。

他底層有點複雜,他是怎麼做到這麼小的結構,存儲這麼多數據的?也是很取巧大家有空可以看下我之前的文章。

布隆過濾器

HyperLogLog 數據結構來進行估數,它非常有價值,可以解決很多精確度不高的統計需求。

但是如果我們想知道某一個值是不是已經在 HyperLogLog 結構裏面了,它就無能爲力了,它只提供了 pfadd 和 pfcount 方法,沒有提供 pfcontains 這種方法。

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

你會想到服務器記錄了用戶看過的所有歷史記錄,當推薦系統推薦新聞時會從每個用戶的歷史記錄裏進行篩選,過濾掉那些已經存在的記錄。問題是當用戶量很大,每個用戶看過的新聞又很多的情況下,這種方式,推薦系統的去重工作在性能上跟的上麼?

127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

布隆過濾器的initial_size估計的過大,會浪費存儲空間,估計的過小,就會影響準確率,用戶在使用之前一定要儘可能地精確估計好元素數量,還需要加上一定的冗餘空間以避免實際元素可能會意外高出估計值很多。

布隆過濾器的error_rate越小,需要的存儲空間就越大,對於不需要過於精確的場合,error_rate設置稍大一點也無傷大雅。比如在新聞去重上而言,誤判率高一點只會讓小部分文章不能讓合適的人看到,文章的整體閱讀量不會因爲這點誤判率就帶來巨大的改變。

在爬蟲系統中,我們需要對 URL 進行去重,已經爬過的網頁就可以不用爬了。但是 URL 太多了,幾千萬幾個億,如果用一個集合裝下這些 URL 地址那是非常浪費空間的。這時候就可以考慮使用布隆過濾器。它可以大幅降低去重存儲消耗,只不過也會使得爬蟲系統錯過少量的頁面。

布隆過濾器在 NoSQL 數據庫領域使用非常廣泛,我們平時用到的 HBase、Cassandra 還有 LevelDB、RocksDB 內部都有布隆過濾器結構,布隆過濾器可以顯著降低數據庫的 IO 請求數量。當用戶來查詢某個 row 時,可以先通過內存中的布隆過濾器過濾掉大量不存在的 row 請求,然後再去磁盤進行查詢。

郵箱系統的垃圾郵件過濾功能也普遍用到了布隆過濾器,因爲用了這個過濾器,所以平時也會遇到某些正常的郵件被放進了垃圾郵件目錄中,這個就是誤判所致,概率很低。

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