Redis的9種數(shù)據(jù)類型用法解讀
在具體描述這幾種數(shù)據(jù)類型之前,我們先通過一張圖了解下 Redis 內(nèi)部內(nèi)存管理中是如何描述這些不同數(shù)據(jù)類型的:
首先Redis內(nèi)部使用一個redisObject對象來表示所有的key和value,redisObject最主要的信息如上圖所示:type代表一個value對象具體是何種數(shù)據(jù)類型,encoding是不同數(shù)據(jù)類型在redis內(nèi)部的存儲方式,
比如:type=string代表value存儲的是一個普通字符串,那么對應(yīng)的encoding可以是raw或者是int,如果是int則代表實際redis內(nèi)部是按數(shù)值型類存儲和表示這個字符串的,當(dāng)然前提是這個字符串本身可以用數(shù)值表示,比如:"123" "456"這樣的字符串。
這需要特殊說明一下vm字段,只有打開了Redis的虛擬內(nèi)存功能,此字段才會真正的分配內(nèi)存,該功能默認(rèn)是關(guān)閉狀態(tài)的,該功能會在后面具體描述。
通過上圖我們可以發(fā)現(xiàn)Redis使用redisObject來表示所有的key/value數(shù)據(jù)是比較浪費內(nèi)存的,當(dāng)然這些內(nèi)存管理成本的付出主要也是為了給Redis不同數(shù)據(jù)類型提供一個統(tǒng)一的管理接口,實際作者也提供了多種方法幫助我們盡量節(jié)省內(nèi)存使用,我們隨后會具體討論。
redis支持豐富的數(shù)據(jù)類型
不同的場景使用合適的數(shù)據(jù)類型可以有效的優(yōu)化內(nèi)存數(shù)據(jù)的存放空間:
- string:最基本的數(shù)據(jù)類型,二進(jìn)制安全的字符串,最大512M。
- list:按照添加順序保持順序的字符串列表。
- set:無序的字符串集合,不存在重復(fù)的元素。
- sorted set:已排序的字符串集合。
- hash:key-value對的一種集合。
- bitmap:更細(xì)化的一種操作,以bit為單位。
- hyperloglog:基于概率的數(shù)據(jù)結(jié)構(gòu)。 # 2.8.9新增
- Geo:地理位置信息儲存起來, 并對這些信息進(jìn)行操作 # 3.2新增
- 流(Stream)# 5.0新增
String 字符串
常用命令:
setnx,set,get,decr,incr,mget 等。
應(yīng)用場景
字符串是最常用的數(shù)據(jù)類型,他能夠存儲任何類型的字符串,當(dāng)然也包括二進(jìn)制、JSON化的對象、甚至是Base64編碼之后的圖片。
在Redis中一個字符串最大的容量為512MB,可以說是無所不能了。redis的key和string類型value限制均為512MB。
雖然Key的大小上限為512M,但是一般建議key的大小不要超過1KB,這樣既可以節(jié)約存儲空間,又有利于Redis進(jìn)行檢索
- 緩存,熱點數(shù)據(jù)
- 分布式session
- 分布式鎖
- INCR計數(shù)器
- 文章的閱讀量,微博點贊數(shù),允許一定的延遲,先寫入 Redis 再定時同步到數(shù)據(jù)庫
- 全局ID
- INT 類型,INCRBY,利用原子性
- INCR 限流
- 以訪問者的 IP 和其他信息作為 key,訪問一次增加一次計數(shù),超過次數(shù)則返回 false。
- setbit 位操作
內(nèi)部編碼
- int:8 個字節(jié)的長整型(long,2^63-1)
- embstr:小于等于44個字節(jié)的字符串,embstr格式的SDS(Simple Dynamic String)
- raw:SDS大于 44 個字節(jié)的字符串
接下來就是ebmstr和raw兩種內(nèi)部編碼的長度界限,請看下面的源碼
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject(const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); }
通過下圖可以直觀感受一下字符串類型和哈希類型的區(qū)別:
redis 為什么要自己寫一個SDS的數(shù)據(jù)類型,主要是為了解決C語言 char[] 的四個問題
- 字符數(shù)組必須先給目標(biāo)變量分配足夠的空間,否則可能會溢出
- 查詢字符數(shù)組長度 時間復(fù)雜度O(n)
- 長度變化,需要重新分配內(nèi)存
- 通過從字符串開始到結(jié)尾碰到的第一個\0來標(biāo)記字符串的結(jié)束,因此不能保存圖片、音頻、視頻、壓縮文件等二進(jìn)制(bytes)保存的內(nèi)容,二進(jìn)制不安全
redis SDS
- 不用擔(dān)心內(nèi)存溢出問題,如果需要會對 SDS 進(jìn)行擴容
- 因為定義了 len 屬性,查詢數(shù)組長度時間復(fù)雜度O(1) 固定長度
- 空間預(yù)分配,惰性空間釋放
- 根據(jù)長度 len來判斷是結(jié)束,而不是 \0
為什么要有embstr編碼呢?他比raw的優(yōu)勢在哪里?
embstr編碼將創(chuàng)建字符串對象所需的空間分配的次數(shù)從raw編碼的兩次降低為一次。
因為emstr編碼字符串的素有對象保持在一塊連續(xù)的內(nèi)存里面,所以那個編碼的字符串對象比起raw編碼的字符串對象能更好的利用緩存。
并且釋放embstr編碼的字符串對象只需要調(diào)用一次內(nèi)存釋放函數(shù),而釋放raw編碼對象的字符串對象需要調(diào)用兩次內(nèi)存釋放函數(shù),如圖所示,左邊emstr編碼,右邊是raw編碼:
Hash 哈希表
常用命令:
hget,hsetnx,hset,hvals,hgetall,hmset,hmget 等。
redis> HSET phone myphone nokia-1110 (integer) 1 redis> HEXISTS phone myphone (integer) 1 redis> HSET people jack "Jack Sparrow" (integer) 1 redis> HSET people gump "Forrest Gump" (integer) 1 redis> HGETALL people 1) "jack" # 域 2) "Jack Sparrow" # 值 3) "gump" 4) "Forrest Gump"
應(yīng)用場景
我們簡單舉個實例來描述下 Hash 的應(yīng)用場景,比如我們要存儲一個用戶信息對象數(shù)據(jù),包含以下信息:用戶 ID 為查找的 key,存儲的 value 用戶對象包含姓名,年齡,生日等信息,如果用普通的 key/value 結(jié)構(gòu)來存儲,主要有以下2種存儲方式:
第一種方式將用戶 ID 作為查找 key,把其他信息封裝成一個對象以序列化的方式存儲,這種方式的缺點是,增加了序列化/反序列化的開銷,并且在需要修改其中一項信息時,需要把整個對象取回,并且修改操作需要對并發(fā)進(jìn)行保護(hù),引入CAS等復(fù)雜問題。
第二種方法是這個用戶信息對象有多少成員就存成多少個 key-value 對兒,用用戶 ID +對應(yīng)屬性的名稱作為唯一標(biāo)識來取得對應(yīng)屬性的值,雖然省去了序列化開銷和并發(fā)問題,但是用戶 ID 為重復(fù)存儲,如果存在大量這樣的數(shù)據(jù),內(nèi)存浪費還是非??捎^的。
那么 Redis 提供的 Hash 很好的解決了這個問題,Redis 的 Hash 實際是內(nèi)部存儲的 Value 為一個 HashMap,并提供了直接存取這個 Map 成員的接口,如下圖:
也就是說,Key 仍然是用戶 ID,value 是一個 Map,這個 Map 的 key 是成員的屬性名,value 是屬性值,這樣對數(shù)據(jù)的修改和存取都可以直接通過其內(nèi)部 Map 的 Key(Redis 里稱內(nèi)部 Map 的 key 為 field),也就是通過 key(用戶 ID) + field(屬性標(biāo)簽)就可以操作對應(yīng)屬性數(shù)據(jù)了,既不需要重復(fù)存儲數(shù)據(jù),也不會帶來序列化和并發(fā)修改控制的問題。很好的解決了問題。
這里同時需要注意,Redis 提供了接口(hgetall)可以直接取到全部的屬性數(shù)據(jù),但是如果內(nèi)部 Map 的成員很多,那么涉及到遍歷整個內(nèi)部 Map 的操作,由于 Redis 單線程模型的緣故,這個遍歷操作可能會比較耗時,而另其它客戶端的請求完全不響應(yīng),這點需要格外注意。
購物車
內(nèi)部編碼
- ziplist(壓縮列表):當(dāng)哈希類型中元素個數(shù)小于 hash-max-ziplist-entries 配置(默認(rèn) 512 個),同時所有值都小于 hash-max-ziplist-value 配置(默認(rèn) 64 字節(jié))時,Redis 會使用 ziplist 作為哈希的內(nèi)部實現(xiàn)。
- hashtable(哈希表):當(dāng)上述條件不滿足時,Redis 則會采用 hashtable 作為哈希的內(nèi)部實現(xiàn)。
下面我們通過以下命令來演示一下 ziplist 和 hashtable 這兩種內(nèi)部編碼。
當(dāng) field 個數(shù)比較少并且 value 也不是很大時候 Redis 哈希類型的內(nèi)部編碼為 ziplist:
當(dāng) value 中的字節(jié)數(shù)大于 64 字節(jié)時(可以通過 hash-max-ziplist-value 設(shè)置),內(nèi)部編碼會由 ziplist 變成 hashtable。
當(dāng) field 個數(shù)超過 512(可以通過 hash-max-ziplist-entries 參數(shù)設(shè)置),內(nèi)部編碼也會由 ziplist 變成 hashtable
List 列表
常用命令:
lpush,rpush,lpop,rpop,lrange等。
127.0.0.1:6379> lpush list one # 將一個值或者多個值,插入到列表的頭部(左)(integer) 1 127.0.0.1:6379> lpush list two (integer) 2 127.0.0.1:6379> lpush list three (integer) 3 127.0.0.1:6379> lrange list 0 -1 # 查看全部元素 1) "three" 2) "two" 3) "one" 127.0.0.1:6379> lrange list 0 1 # 通過區(qū)間獲取值 1) "three" 2) "two" 127.0.0.1:6379> rpush list right # 將一個值或者多個值,插入到列表的尾部(右)(integer) 4 127.0.0.1:6379> lrange list 0 -1 1) "three" 2) "two" 3) "one" 4) "right" 127.0.0.1:6379>
列表(list)用來存儲多個有序的字符串,每個字符串稱為元素;一個列表可以存儲2^32-1個元素。Redis中的列表支持兩端插入和彈出,并可以獲得指定位置(或范圍)的元素,可以充當(dāng)數(shù)組、隊列、棧等
應(yīng)用場景
比如 twitter 的關(guān)注列表,粉絲列表等都可以用 Redis 的 list 結(jié)構(gòu)來實現(xiàn),可以利用lrange命令,做基于Redis的分頁功能,性能極佳,用戶體驗好。
消息隊列
列表類型可以使用 rpush 實現(xiàn)先進(jìn)先出的功能,同時又可以使用 lpop 輕松的彈出(查詢并刪除)第一個元素,所以列表類型可以用來實現(xiàn)消息隊列
發(fā)紅包的場景
在發(fā)紅包的場景中,假設(shè)發(fā)一個10元,10個紅包,需要保證搶紅包的人不會多搶到,也不會少搶到
下面我們通過下圖來看一下 Redis 中列表類型的插入和彈出操作:
下面我們看一下 Redis 中列表類型的獲取與刪除操作:
Redis 列表類型的特點如下:
- 列表中所有的元素都是有序的,所以它們是可以通過索引獲取的lindex 命令。并且在 Redis 中列表類型的索引是從 0 開始的。
- 列表中的元素是可以重復(fù)的,也就是說在 Redis 列表類型中,可以保存同名元素
內(nèi)部編碼
- ziplist(壓縮列表):當(dāng)列表中元素個數(shù)小于 512(默認(rèn))個,并且列表中每個元素的值都小于 64(默認(rèn))個字節(jié)時,Redis 會選擇用 ziplist 來作為列表的內(nèi)部實現(xiàn)以減少內(nèi)存的使用。當(dāng)然上述默認(rèn)值也可以通過相關(guān)參數(shù)修改:list-max-ziplist-entried(元素個數(shù))、list-max-ziplist-value(元素值)。
- linkedlist(鏈表):當(dāng)列表類型無法滿足 ziplist 條件時,Redis 會選擇用 linkedlist 作為列表的內(nèi)部實現(xiàn)。
Set 集合
常用命令:
sadd,spop,smembers,sunion,scard,sscan,sismember等。
# 初始化 sadd poker T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 TJ TQ TK X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 XJ XQ XK M1 M2 M3 M4 M5 M6 M7 M8 M9 M10 MJ MQ MK F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 FJ FQ FK XW DW # 數(shù)量 scard poker # 復(fù)用撲克牌 sunionstore pokernew poker # 成員 smembers poker # 是否包含 sismember poker T1 # 隨機讀取 spop poker # 設(shè)置 sadd user1tag tagID1 tagID2 tagID3 sadd user2tag tagID2 tagID3 sadd user3tag tagID2 tagID4 tagID5 # 獲取共同擁有的tag(交集) sinter user1tag user2tag user3tag # 獲取擁有的所有tag(并集) sunion user1tag user2tag user3tag # 獲取兩個之間的區(qū)別(差集) sdiff user2tag user3tag
應(yīng)用場景
Redis set 對外提供的功能與 list 類似是一個列表的功能,特殊之處在于 set 是可以自動排重的,當(dāng)你需要存儲一個列表數(shù)據(jù),又不希望出現(xiàn)重復(fù)數(shù)據(jù)時,set 是一個很好的選擇,并且 set 提供了判斷某個成員是否在一個 set 集合內(nèi)的重要接口,這個也是 list 所不能提供的。
1知乎點贊數(shù)
2京東的商品篩選
127.0.0.1:6379> sadd brand:apple iPhone11 (integer) 1 127.0.0.1:6379> sadd brand:ios iPhone11 (integer) 1 127.0.0.1:6379> sadd screensize:6.0-6.24 iPhone11 (integer) 1 127.0.0.1:6379> sadd memorysize:256GB iPhone11 (integer) 1 127.0.0.1:6379> sinter brand:apple brand:ios screensize:6.0-6.24 memorysize:256GB 1) "iPhone11"
篩選商品,蘋果,IOS,屏幕6.0-6.24,內(nèi)存大小256G
sinter brand:apple brand:ios screensize:6.0-6.24 memorysize:256GB
3.存儲社交關(guān)系
用戶(編號user001)關(guān)注
sadd focus:user001 user003
sadd focus:user002 user003 user004
相互關(guān)注
sadd focus:user001 user002
sadd focus:user002 user001
#判斷用戶2是否關(guān)注了用戶1 127.0.0.1:6379> SISMEMBER focus:user002 user001 (integer) 1
我關(guān)注得到人也關(guān)注了他(共同關(guān)注)
#獲取關(guān)注的交集 127.0.0.1:6379> sinter focus:user001 focus:user002 1) "user003"
可能認(rèn)識的人
#將所有的人存放到allusers集合 127.0.0.1:6379> SUNIONSTORE alluser:user001 focus:user001 focus:user002 (integer) 4 127.0.0.1:6379> SDIFF alluser:user001 focus:user001 1) "user004" 2) "user001" #剔除掉自己 127.0.0.1:6379> SREM alluser:user001 user001 (integer) 1 127.0.0.1:6379> SDIFF alluser:user001 focus:user001 1) "user004"
實現(xiàn)方式:
set 的內(nèi)部實現(xiàn)是一個 value 永遠(yuǎn)為 null 的 HashMap,實際就是通過計算 hash 的方式來快速排重的,這也是 set 能提供判斷一個成員是否在集合內(nèi)的原因。
Redis 中的集合類型,也就是 set。在 Redis 中 set 也是可以保存多個字符串的,經(jīng)常有人會分不清 list 與 set,下面我們重點介紹一下它們之間的不同:
- set 中的元素是不可以重復(fù)的,而 list 是可以保存重復(fù)元素的。
- set 中的元素是無序的,而 list 中的元素是有序的。
- set 中的元素不能通過索引下標(biāo)獲取元素,而 list 中的元素則可以通過索引下標(biāo)獲取元素。
- 除此之外 set 還支持更高級的功能,例如多個 set 取交集、并集、差集等
為什么 Redis 要提供 sinterstore、sunionstore、sdiffstore 命令來將集合的交集、并集、差集的結(jié)果保存起來呢?這是因為 Redis 在進(jìn)行上述比較時,會比較耗費時間,所以為了提高性能可以將交集、并集、差集的結(jié)果提前保存起來,這樣在需要使用時,可以直接通過 smembers 命令獲取。
內(nèi)部編碼
- intset(整數(shù)集合):當(dāng)集合中的元素都是整數(shù),并且集合中的元素個數(shù)小于set-max-intset-entries 參數(shù)時,默認(rèn)512,Redis 會選用 intset 作為底層內(nèi)部實現(xiàn)。
- hashtable(哈希表):當(dāng)上述條件不滿足時,Redis 會采用 hashtable 作為底層實現(xiàn)。
Sorted set 有序集合
常用命令:
zadd,zrange,zrem,zcard,zscore,zcount,zlexcount等
redis> ZRANGE salary 0 -1 WITHSCORES # 測試數(shù)據(jù) 1) "tom" 2) "2000" 3) "peter" 4) "3500" 5) "jack" 6) "5000" redis> ZSCORE salary peter # 注意返回值是字符串 "3500" redis> ZCOUNT salary 2000 5000 # 計算薪水在 2000-5000 之間的人數(shù) (integer) 3 # rank:key 100: 分?jǐn)?shù) u1: ID # 初始化 時間復(fù)雜度: O(log(N)) zadd rank 100 u1 zadd rank 200 u2 zadd rank 300 u3 zadd rank 400 u4 zadd rank 500 u5 # 數(shù)量 zcard rank # 內(nèi)容(正序、倒序)時間復(fù)雜度: O(log(N)+M) zrange rank 0 -1 zrange rank 0 -1 withscores zrevrange rank 0 -1 withscores zrevrange rank 0 -2 withscores zrevrange rank 0 -1 # 獲取用戶分?jǐn)?shù)(不存在返回空) 時間復(fù)雜度: O(1) zscore rank u1 # 修改分?jǐn)?shù) zadd rank 800 u3 # 修改分?jǐn)?shù)(累加:新增或減少,返回修改后的分?jǐn)?shù)) zincrby rank 100 u3 zincrby rank -100 u3 # 查詢分?jǐn)?shù)范圍內(nèi)容(正序、倒序) zrangebyscore rank (200 800 withscores zrevrangebyscore rank (200 800 withscores # 移除元素 zrem rank u3
下面先看一下列表、集合、有序集合三種數(shù)據(jù)類型之間的區(qū)別:
內(nèi)部編碼
- ziplist(壓縮列表):當(dāng)有序集合的元素個數(shù)小于 128 個(默認(rèn)設(shè)置),同時每個元素的值都小于 64 字節(jié)(默認(rèn)設(shè)置),Redis 會采用 ziplist 作為有序集合的內(nèi)部實現(xiàn)。
- skiplist(跳躍表):當(dāng)上述條件不滿足時,Redis 會采用 skiplist 作為內(nèi)部編碼。
備注:上述中的默認(rèn)值,也可以通過以下參數(shù)設(shè)置:zset-max-ziplist-entries 和 zset-max-ziplist-value。
應(yīng)用場景
Redis sorted set 的使用場景與 set 類似,區(qū)別是 set 不是自動有序的,而 sorted set 可以通過用戶額外提供一個優(yōu)先級(score)的參數(shù)來為成員排序,并且是插入有序的,即自動排序。
當(dāng)你需要一個有序的并且不重復(fù)的集合列表,那么可以選擇 sorted set 數(shù)據(jù)結(jié)構(gòu),比如 twitter 的 public timeline 可以以發(fā)表時間作為 score 來存儲,這樣獲取時就是自動按時間排好序的。點擊數(shù)做出排行榜。
1.商品的評價標(biāo)簽,可以記錄商品的標(biāo)簽,統(tǒng)計標(biāo)簽次數(shù),增加標(biāo)簽次數(shù),按標(biāo)簽的分值進(jìn)行排序
#添加商品(編號i5001)的標(biāo)簽tag和對應(yīng)標(biāo)簽的評價次數(shù) 127.0.0.1:6379> zadd goods_tag:i5001 442 tag1 265 tag2 264 tag3 (integer) 3 #不帶分?jǐn)?shù) 127.0.0.1:6379> zrange goods_tag:i5001 0 -1 1) "tag3" 2) "tag2" 3) "tag1" #帶分?jǐn)?shù) 127.0.0.1:6379> zrange goods_tag:i5001 0 -1 withscores 1) "tag3" 2) "264" 3) "tag2" 4) "265" 5) "tag1" 6) "442"
2.百度搜索熱點
#維護(hù)2020年1月21號的熱點新聞 127.0.0.1:6379> zadd hotspot:20200121 520 pot1 263 pot2 244 pot3 (integer) 3 127.0.0.1:6379> zrange hotspot:20200121 0 -1 withscores 1) "pot3" 2) "244" 3) "pot2" 4) "263" 5) "pot1" 6) "520" #增加點擊次數(shù) 127.0.0.1:6379> ZINCRBY hotspot 1 pot1 "521"
3.反spam系統(tǒng)
作為一個電商網(wǎng)站被各種spam攻擊是少不免(垃圾評論、發(fā)布垃圾商品、廣告、刷自家商品排名等)針對這些spam制定一系列anti-spam規(guī)則,其中有些規(guī)則可以利用redis做實時分析
譬如:1分鐘評論不得超過2次、5分鐘評論少于5次等
#獲取5秒內(nèi)操作記錄 $res = $redis->zRangeByScore('user:1000:comment', time() - 5, time()); #判斷5秒內(nèi)不能評論 if (!$res) { $redis->zAdd('user:1000:comment', time(), '評論內(nèi)容'); } else { echo '5秒之內(nèi)不能評論'; } #5秒內(nèi)評論不得超過2次 if($redis->zRangeByScore('user:1000:comment',time()-5 ,time())==1) echo '5秒之內(nèi)不能評論2次'; #5秒內(nèi)評論不得少于2次 if(count($redis->zRangeByScore('user:1000:comment',time()-5 ,time()))<2) echo '5秒之內(nèi)不能評論2次';
BitMap 位圖
在應(yīng)用場景中,有一些數(shù)據(jù)只有兩個屬性,比如是否是學(xué)生,是否是黨員等等,對于這些數(shù)據(jù),最節(jié)約內(nèi)存的方式就是用bit去記錄,以是否是學(xué)生為例,1代表是學(xué)生,0代表不是學(xué)生。那么1000110就代表7個人中3個是學(xué)生,這就是BitMaps的存儲需求。
Bitmaps是一個可以對位進(jìn)行操作的字符串,我們可以把Bitmaps想象成是一串二進(jìn)制數(shù)字,每個位置只存儲0和1。下標(biāo)是Bitmaps的偏移量。
BitMap 就是通過一個 bit 位來表示某個元素對應(yīng)的值或者狀態(tài), 其中的 key 就是對應(yīng)元素本身,實際上底層也是通過對字符串的操作來實現(xiàn)。Redis從2.2.0版本開始新增了setbit
,getbit
,bitcount
等幾個bitmap相關(guān)命令。
雖然是新命令,但是并沒有新增新的數(shù)據(jù)類型,因為setbit
等命令只不過是在set
上的擴展。
使用場景一:用戶簽到
很多網(wǎng)站都提供了簽到功能(這里不考慮數(shù)據(jù)落地事宜),并且需要展示最近一個月的簽到情況
<?php $redis = new Redis(); $redis->connect('127.0.0.1'); //用戶uid $uid = 1; //記錄有uid的key $cacheKey = sprintf("sign_%d", $uid); //開始有簽到功能的日期 $startDate = '2017-01-01'; //今天的日期 $todayDate = '2017-01-21'; //計算offset $startTime = strtotime($startDate); $todayTime = strtotime($todayDate); $offset = floor(($todayTime - $startTime) / 86400); echo "今天是第{$offset}天" . PHP_EOL; //簽到 //一年一個用戶會占用多少空間呢?大約365/8=45.625個字節(jié),好小,有木有被驚呆? $redis->setBit($cacheKey, $offset, 1); //查詢簽到情況 $bitStatus = $redis->getBit($cacheKey, $offset); echo 1 == $bitStatus ? '今天已經(jīng)簽到啦' : '還沒有簽到呢'; echo PHP_EOL; //計算總簽到次數(shù) echo $redis->bitCount($cacheKey) . PHP_EOL; /** * 計算某段時間內(nèi)的簽到次數(shù) * 很不幸啊,bitCount雖然提供了start和end參數(shù),但是這個說的是字符串的位置,而不是對應(yīng)"位"的位置 * 幸運的是我們可以通過get命令將value取出來,自己解析。并且這個value不會太大,上面計算過一年一個用戶只需要45個字節(jié) * 給我們的網(wǎng)站定一個小目標(biāo),運行30年,那么一共需要1.31KB(就問你屌不屌?) */ //這是個錯誤的計算方式 echo $redis->bitCount($cacheKey, 0, 20) . PHP_EOL;
使用場景二:統(tǒng)計活躍用戶
使用時間作為cacheKey,然后用戶ID為offset,如果當(dāng)日活躍過就設(shè)置為1
那么我該如果計算某幾天/月/年的活躍用戶呢(暫且約定,統(tǒng)計時間內(nèi)只有有一天在線就稱為活躍),有請下一個redis的命令
命令 BITOP operation destkey key [key ...]
說明:對一個或多個保存二進(jìn)制位的字符串 key 進(jìn)行位元操作,并將結(jié)果保存到 destkey 上。
說明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種參數(shù)
bitop and destKey key1 key2.... //交 bitop or destKey key1 key2.... //并 bitop not destKey key1 key2.... //非 bitop xor destKey key1 key2.... //異或
<?php //日期對應(yīng)的活躍用戶 $data = array( '2017-01-10' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),? '2017-01-11' => array(1, 2, 3, 4, 5, 6, 7, 8),? '2017-01-12' => array(1, 2, 3, 4, 5, 6),? '2017-01-13' => array(1, 2, 3, 4), '2017-01-14' => array(1, 2)? ); //批量設(shè)置活躍狀態(tài)? foreach ($data as $date => $uids) { $cacheKey = sprintf("stat_%s", $date); foreach ($uids as $uid) { $redis->setBit($cacheKey, $uid, 1); } } $redis->bitOp('AND', 'stat', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-12') . PHP_EOL; //總活躍用戶:6? echo "總活躍用戶:" . $redis->bitCount('stat') . PHP_EOL; $redis->bitOp('AND', 'stat1', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-14') . PHP_EOL; ? //總活躍用戶:2? echo "總活躍用戶:" . $redis->bitCount('stat1') . PHP_EOL; $redis->bitOp('AND', 'stat2', 'stat_2017-01-10', 'stat_2017-01-11') . PHP_EOL; //總活躍用戶:8? echo "總活躍用戶:" . $redis->bitCount('stat2') . PHP_EOL;
使用場景三:用戶在線狀態(tài)
前段時間開發(fā)一個項目,對方給我提供了一個查詢當(dāng)前用戶是否在線的接口。不了解對方是怎么做的,自己考慮了一下,使用bitmap是一個節(jié)約空間效率又高的一種方法,只需要一個key,然后用戶ID為offset,如果在線就設(shè)置為1,不在線就設(shè)置為0,和上面的場景一樣,5000W用戶只需要6MB的空間。
<?php //批量設(shè)置在線狀態(tài) $uids = range(1, 500000); foreach ($uids as $uid) { $redis->setBit('online', $uid, $uid % 2); } //一個一個獲取狀態(tài) $uids = range(1, 500000); $startTime = microtime(true); foreach ($uids as $uid) { echo $redis->getBit('online', $uid) . PHP_EOL; } $endTime = microtime(true); //在我的電腦上,獲取50W個用戶的狀態(tài)需要25秒? echo "total:" . ($endTime - $startTime) . "s";
內(nèi)部編碼
這個就是Redis實現(xiàn)的BloomFilter,BloomFilter非常簡單,如下圖所示,假設(shè)已經(jīng)有3個元素a、b和c,分別通過3個hash算法h1()、h2()和h2()計算然后對一個bit進(jìn)行賦值,接下來假設(shè)需要判斷d是否已經(jīng)存在,那么也需要使用3個hash算法h1()、h2()和h2()對d進(jìn)行計算,然后得到3個bit的值,恰好這3個bit的值為1,這就能夠說明:d可能存在集合中。再判斷e,由于h1(e)算出來的bit之前的值是0,那么說明:e一定不存在集合中:
需要說明的是,bitmap并不是一種真實的數(shù)據(jù)結(jié)構(gòu),它本質(zhì)上是String數(shù)據(jù)結(jié)構(gòu),只不過操作的粒度變成了位,即bit。因為String類型最大長度為512MB,所以bitmap最多可以存儲2^32個bit。
HyperLogLog 基數(shù)統(tǒng)計
HyperLogLog算法時一種非常巧妙的近似統(tǒng)計大量去重元素數(shù)量的算法,它內(nèi)部維護(hù)了16384個桶來記錄各自桶的元素數(shù)量,當(dāng)一個元素過來,它會散列到其中一個桶。
當(dāng)元素到來時,通過 hash 算法將這個元素分派到其中的一個小集合存儲,同樣的元素總是會散列到同樣的小集合。這樣總的計數(shù)就是所有小集合大小的總和。
使用這種方式精確計數(shù)除了可以增加元素外,還可以減少元素
一個HyperLogLog實際占用的空間大約是 13684 * 6bit / 8 = 12k 字節(jié)。但是在計數(shù)比較小的時候,大多數(shù)桶的計數(shù)值都是零。如果 12k 字節(jié)里面太多的字節(jié)都是零,那么這個空間是可以適當(dāng)節(jié)約一下的。Redis 在計數(shù)值比較小的情況下采用了稀疏存儲,稀疏存儲的空間占用遠(yuǎn)遠(yuǎn)小于 12k 字節(jié)。相對于稀疏存儲的就是密集存儲,密集存儲會恒定占用 12k 字節(jié)。
內(nèi)部編碼
HyperLogLog 整體的內(nèi)部結(jié)構(gòu)就是 HLL 對象頭 加上 16384 個桶的計數(shù)值位圖。它在 Redis 的內(nèi)部結(jié)構(gòu)表現(xiàn)就是一個字符串位圖。你可以把 HyperLogLog 對象當(dāng)成普通的字符串來進(jìn)行處理。
應(yīng)用場景
Redis 的基數(shù)統(tǒng)計,這個結(jié)構(gòu)可以非常省內(nèi)存的去統(tǒng)計各種計數(shù),比如注冊 IP 數(shù)、每日訪問 IP 數(shù)、頁面實時UV)、在線用戶數(shù)等。但是它也有局限性,就是只能統(tǒng)計數(shù)量,而沒辦法去知道具體的內(nèi)容是什么。當(dāng)然用集合也可以解決這個問題。但是一個大型的網(wǎng)站,每天 IP 比如有 100 萬,粗算一個 IP 消耗 15 字節(jié),那么 100 萬個 IP 就是 15M。而 HyperLogLog 在 Redis 中每個鍵占用的內(nèi)容都是 12K,理論存儲近似接近 2^64 個值,不管存儲的內(nèi)容是什么,它一個基于基數(shù)估算的算法,只能比較準(zhǔn)確的估算出基數(shù),可以使用少量固定的內(nèi)存去存儲并識別集合中的唯一元素。而且這個估算的基數(shù)并不一定準(zhǔn)確,是一個帶有 0.81% 標(biāo)準(zhǔn)錯誤的近似值。
HyperLogLog 主要的應(yīng)用場景就是進(jìn)行基數(shù)統(tǒng)計。這個問題的應(yīng)用場景其實是十分廣泛的。例如:對于 Google 主頁面而言,同一個賬戶可能會訪問 Google 主頁面多次。于是,在諸多的訪問流水中,如何計算出 Google 主頁面每天被多少個不同的賬戶訪問過就是一個重要的問題。那么對于 Google 這種訪問量巨大的網(wǎng)頁而言,其實統(tǒng)計出有十億 的訪問量或者十億零十萬的訪問量其實是沒有太多的區(qū)別的,因此,在這種業(yè)務(wù)場景下,為了節(jié)省成本,其實可以只計算出一個大概的值,而沒有必要計算出精準(zhǔn)的值
這個數(shù)據(jù)結(jié)構(gòu)的命令有三個:PFADD、PFCOUNT、PFMERGE
redis> PFADD databases "Redis" "MongoDB" "MySQL" (integer) 1 redis> PFADD databases "Redis" # Redis 已經(jīng)存在,不必對估計數(shù)量進(jìn)行更新 (integer) 0 redis> PFCOUNT databases (integer) 3
Geo 地理位置
Redis 的 GEO 特性在 Redis 3.2 版本中推出, 這個功能可以將用戶給定的地理位置信息儲存起來, 并對這些信息進(jìn)行操作。
GEO的數(shù)據(jù)結(jié)構(gòu)總共有六個命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash,GEO使用的是國際通用坐標(biāo)系WGS-84。
- 1.GEOADD:添加地理位置
- 2.GEOPOS:查詢地理位置(經(jīng)緯度),返回數(shù)組
- 3.GEODIST:計算兩位位置間的距離
- 4.GEORADIUS:以給定的經(jīng)緯度為中心, 返回鍵包含的位置元素當(dāng)中, 與中心的距離不超過給定最大距離的所有位置元素。
- 5.GEORADIUSBYMEMBER:以給定的地理位置為中心, 返回鍵包含的位置元素當(dāng)中, 與中心的距離不超過給定最大距離的所有位置元素。
127.0.0.1:6379> geoadd kcityGeo 116.405285 39.904989 "beijing" (integer) 1 127.0.0.1:6379> geoadd kcityGeo 121.472644 31.231706 "shanghai" (integer) 1 127.0.0.1:6379> geodist kcityGeo beijing shanghai km "1067.5980" 127.0.0.1:6379> geopos kcityGeo beijing 1) 1) "116.40528291463851929" 2) "39.9049884229125027" 127.0.0.1:6379> geohash kcityGeo beijing 1) "wx4g0b7xrt0" 127.0.0.1:6379> georadiusbymember kcityGeo beijing 1200 km withdist withcoord asc count 5 1) 1) "beijing" 2) "0.0000" 3) 1) "116.40528291463851929" 2) "39.9049884229125027" 2) 1) "shanghai" 2) "1067.5980" 3) 1) "121.47264629602432251" 2) "31.23170490709807012"
內(nèi)部編碼
但是,需要說明的是,Geo本身不是一種數(shù)據(jù)結(jié)構(gòu),它本質(zhì)上還是借助于Sorted Set(ZSET),并且使用GeoHash技術(shù)進(jìn)行填充。Redis中將經(jīng)緯度使用52位的整數(shù)進(jìn)行編碼,放進(jìn)zset中,score就是GeoHash的52位整數(shù)值。在使用Redis進(jìn)行Geo查詢時,其內(nèi)部對應(yīng)的操作其實就是zset(skiplist)的操作。通過zset的score進(jìn)行排序就可以得到坐標(biāo)附近的其它元素,通過將score還原成坐標(biāo)值就可以得到元素的原始坐標(biāo)。
總之,Redis中處理這些地理位置坐標(biāo)點的思想是:二維平面坐標(biāo)點 --> 一維整數(shù)編碼值 --> zset(score為編碼值) --> zrangebyrank(獲取score相近的元素)、zrangebyscore --> 通過score(整數(shù)編碼值)反解坐標(biāo)點 --> 附近點的地理位置坐標(biāo)。
應(yīng)用場景
比如現(xiàn)在比較火的直播業(yè)務(wù),我們需要檢索附近的主播,那么GEO就可以很好的實現(xiàn)這個功能。
- 一是主播開播的時候?qū)懭胫鞑d的經(jīng)緯度
- 二是主播關(guān)播的時候刪除主播Id元素,這樣就維護(hù)了一個具有位置信息的在線主播集合提供給線上檢索
Streams 流
這是Redis5.0引入的全新數(shù)據(jù)結(jié)構(gòu),用一句話概括Streams就是Redis實現(xiàn)的內(nèi)存版kafka。支持多播的可持久化的消息隊列,用于實現(xiàn)發(fā)布訂閱功能,借鑒了 kafka 的設(shè)計。
Redis Stream的結(jié)構(gòu)有一個消息鏈表,將所有加入的消息都串起來,每個消息都有一個唯一的ID和對應(yīng)的內(nèi)容。消息是持久化的,Redis重啟后,內(nèi)容還在。
每個Stream都有唯一的名稱,它就是Redis的key,在我們首次使用xadd指令追加消息時自動創(chuàng)建。
每個Stream都可以掛多個消費組,每個消費組會有個游標(biāo)last_delivered_id在Stream數(shù)組之上往前移動,表示當(dāng)前消費組已經(jīng)消費到哪條消息了。每個消費組都有一個Stream內(nèi)唯一的名稱,消費組不會自動創(chuàng)建,它需要單獨的指令xgroup create進(jìn)行創(chuàng)建,需要指定從Stream的某個消息ID開始消費,這個ID用來初始化last_delivered_id變量。
每個消費組(Consumer Group)的狀態(tài)都是獨立的,相互不受影響。也就是說同一份Stream內(nèi)部的消息會被每個消費組都消費到。
同一個消費組(Consumer Group)可以掛接多個消費者(Consumer),這些消費者之間是競爭關(guān)系,任意一個消費者讀取了消息都會使游標(biāo)last_delivered_id往前移動。每個消費者者有一個組內(nèi)唯一名稱。
消費者(Consumer)內(nèi)部會有個狀態(tài)變量pending_ids,它記錄了當(dāng)前已經(jīng)被客戶端讀取的消息,但是還沒有ack。如果客戶端沒有ack,這個變量里面的消息ID會越來越多,一旦某個消息被ack,它就開始減少。這個pending_ids變量在Redis官方被稱之為PEL,也就是Pending Entries List,這是一個很核心的數(shù)據(jù)結(jié)構(gòu),它用來確??蛻舳酥辽傧M了消息一次,而不會在網(wǎng)絡(luò)傳輸?shù)闹型緛G失了沒處理。
- 消息ID:消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示當(dāng)前的消息在毫米時間戳1527846880572時產(chǎn)生,并且是該毫秒內(nèi)產(chǎn)生的第5條消息。消息ID可以由服務(wù)器自動生成,也可以由客戶端自己指定,但是形式必須是整數(shù)-整數(shù),而且必須是后面加入的消息的ID要大于前面的消息ID。
- 消息內(nèi)容:消息內(nèi)容就是鍵值對,形如hash結(jié)構(gòu)的鍵值對,這沒什么特別之處。
127.0.0.1:6379> XADD mystream * field1 value1 field2 value2 field3 value3 "1588491680862-0" 127.0.0.1:6379> XADD mystream * username lisi age 18 "1588491854070-0" 127.0.0.1:6379> xlen mystream (integer) 2 127.0.0.1:6379> XADD mystream * username lisi age 18 "1588491861215-0" 127.0.0.1:6379> xrange mystream - + 1) 1) "1588491680862-0" 2) 1) "field1" 2) "value1" 3) "field2" 4) "value2" 5) "field3" 6) "value3" 2) 1) "1588491854070-0" 2) 1) "username" 2) "lisi" 3) "age" 4) "18" 3) 1) "1588491861215-0" 2) 1) "username" 2) "lisi" 3) "age" 4) "18" 127.0.0.1:6379> xdel mystream 1588491854070-0 (integer) 1 127.0.0.1:6379> xrange mystream - + 1) 1) "1588491680862-0" 2) 1) "field1" 2) "value1" 3) "field2" 4) "value2" 5) "field3" 6) "value3" 2) 1) "1588491861215-0" 2) 1) "username" 2) "lisi" 3) "age" 4) "18" 127.0.0.1:6379> xlen mystream (integer) 2
內(nèi)部編碼
streams底層的數(shù)據(jù)結(jié)構(gòu)是radix tree:Radix Tree(基數(shù)樹) 事實上就幾乎相同是傳統(tǒng)的二叉樹。僅僅是在尋找方式上,以一個unsigned int類型數(shù)為例,利用這個數(shù)的每個比特位作為樹節(jié)點的推斷。
能夠這樣說,比方一個數(shù)10001010101010110101010,那么依照Radix 樹的插入就是在根節(jié)點,假設(shè)遇到0,就指向左節(jié)點,假設(shè)遇到1就指向右節(jié)點,在插入過程中構(gòu)造樹節(jié)點,在刪除過程中刪除樹節(jié)點。如下是一個保存了7個單詞的Radix Tree:
應(yīng)用場景總結(jié)
實際上,所謂的應(yīng)用場景,其實就是合理的利用Redis本身的數(shù)據(jù)結(jié)構(gòu)的特性來完成相關(guān)業(yè)務(wù)功能
常用內(nèi)存優(yōu)化手段與參數(shù)
通過我們上面的一些實現(xiàn)上的分析可以看出 redis 實際上的內(nèi)存管理成本非常高,即占用了過多的內(nèi)存,作者對這點也非常清楚,所以提供了一系列的參數(shù)和手段來控制和節(jié)省內(nèi)存,我們分別來討論下。
首先最重要的一點是不要開啟 Redis 的 VM 選項,即虛擬內(nèi)存功能,這個本來是作為 Redis 存儲超出物理內(nèi)存數(shù)據(jù)的一種數(shù)據(jù)在內(nèi)存與磁盤換入換出的一個持久化策略,但是其內(nèi)存管理成本也非常的高,并且我們后續(xù)會分析此種持久化策略并不成熟,所以要關(guān)閉 VM 功能,請檢查你的 redis.conf 文件中 vm-enabled 為 no。
其次最好設(shè)置下 redis.conf 中的 maxmemory 選項,該選項是告訴 Redis 當(dāng)使用了多少物理內(nèi)存后就開始拒絕后續(xù)的寫入請求,該參數(shù)能很好的保護(hù)好你的 Redis 不會因為使用了過多的物理內(nèi)存而導(dǎo)致 swap,最終嚴(yán)重影響性能甚至崩潰。
另外 Redis 為不同數(shù)據(jù)類型分別提供了一組參數(shù)來控制內(nèi)存使用,我們在前面詳細(xì)分析過 Redis Hash 是 value 內(nèi)部為一個 HashMap,如果該 Map 的成員數(shù)比較少,則會采用類似一維線性的緊湊格式來存儲該 Map,即省去了大量指針的內(nèi)存開銷,這個參數(shù)控制對應(yīng)在 redis.conf 配置文件中下面2項:
hash-max-ziplist-entries 64 hash-max-zipmap-value 512
含義是當(dāng) value 這個 Map 內(nèi)部不超過多少個成員時會采用線性緊湊格式存儲,默認(rèn)是64,即 value 內(nèi)部有64個以下的成員就是使用線性緊湊存儲,超過該值自動轉(zhuǎn)成真正的 HashMap。
hash-max-zipmap-value 含義是當(dāng) value 這個 Map 內(nèi)部的每個成員值長度不超過多少字節(jié)就會采用線性緊湊存儲來節(jié)省空間。
以上2個條件任意一個條件超過設(shè)置值都會轉(zhuǎn)換成真正的 HashMap,也就不會再節(jié)省內(nèi)存了,那么這個值是不是設(shè)置的越大越好呢,答案當(dāng)然是否定的,HashMap 的優(yōu)勢就是查找和操作的時間復(fù)雜度都是 O(1) 的,而放棄 Hash 采用一維存儲則是 O(n) 的時間復(fù)雜度,如果成員數(shù)量很少,則影響不大,否則會嚴(yán)重影響性能,所以要權(quán)衡好這個值的設(shè)置,總體上還是最根本的時間成本和空間成本上的權(quán)衡。
同樣類似的參數(shù)還有
list-max-ziplist-entries 512
說明:list 數(shù)據(jù)類型多少節(jié)點以下會采用去指針的緊湊存儲格式。
list-max-ziplist-value 64
說明:list 數(shù)據(jù)類型節(jié)點值大小小于多少字節(jié)會采用緊湊存儲格式。
set-max-intset-entries 512
說明:set 數(shù)據(jù)類型內(nèi)部數(shù)據(jù)如果全部是數(shù)值型,且包含多少節(jié)點以下會采用緊湊格式存儲。
最后想說的是 Redis 內(nèi)部實現(xiàn)沒有對內(nèi)存分配方面做過多的優(yōu)化,在一定程度上會存在內(nèi)存碎片,不過大多數(shù)情況下這個不會成為 Redis 的性能瓶 頸,不過如果在 Redis 內(nèi)部存儲的大部分?jǐn)?shù)據(jù)是數(shù)值型的話,Redis 內(nèi)部采用了一個 shared integer 的方式來省去分配內(nèi)存的開銷,即在系統(tǒng)啟動時先分配一個從 1~n 那么多個數(shù)值對象放在一個池子中,如果存儲的數(shù)據(jù)恰好是這個數(shù)值范圍內(nèi)的數(shù)據(jù),則直接從池子里取出該對象,并且通過引用計數(shù)的方式來共享,這樣在系統(tǒng)存儲了大量數(shù)值下,也能一定程度上節(jié)省內(nèi)存并且提高性能,這個參數(shù)值 n 的設(shè)置需要修改源代碼中的一行宏定義 REDIS_SHARED_INTEGERS,該值 默認(rèn)是 10000,可以根據(jù)自己的需要進(jìn)行修改,修改后重新編譯就可以了。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis實現(xiàn)分布式Session管理的機制詳解
這篇文章主要介紹了Redis實現(xiàn)分布式Session管理的機制詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01Redis中LRU算法和LFU算法的區(qū)別小結(jié)
在Redis中,LRU算法和LFU算法是兩種常用的緩存淘汰算法,它們可以幫助我們優(yōu)化緩存性能,本文主要介紹了Redis中LRU算法和LFU算法的區(qū)別,感興趣的可以了解一下2023-12-12