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