前言:
每次你在游戲中看到玩家排行榜,或者在音樂(lè)應(yīng)用中瀏覽熱門歌單,有沒(méi)有想過(guò)這個(gè)排行榜是如何做到實(shí)時(shí)更新的?當(dāng)然,依靠 Redis 即可做到。
在技術(shù)領(lǐng)域,我們經(jīng)常聽(tīng)到「鍵值存儲(chǔ)」 這個(gè)詞。但在 Redis 的世界里,這只是冰山一角。Redis 的對(duì)象,不僅僅是簡(jiǎn)單的數(shù)據(jù),它們是為各種任務(wù)量身定制的超能工具。
接下來(lái),讓我們走進(jìn) Redis 的對(duì)象世界,Redis 5.0版本就已經(jīng)支持了下面的 9 種類型,分別是 :字符串對(duì)象、列表對(duì)象、哈希對(duì)象、集合對(duì)象、有序集合對(duì)象、Bitmaps 對(duì)象、HyperLogLog 對(duì)象、Geospatial 對(duì)象、Stream對(duì)象。
Redis 對(duì)象:
首先,我們要知道,Redis 中保存的數(shù)據(jù)是以鍵值對(duì)的形式存在的。
對(duì)象的類型與編碼
類型
在 Redis 的大家庭中,每個(gè)鍵值對(duì)都有兩個(gè)重要的“身份證”。那就是鍵的類型和值的類型。就好像我們的名字和職業(yè),其中名字(鍵)總是一個(gè)字符串,而職業(yè)(值)則可以是各種各樣:可以是字符串、列表、哈希、集合,甚至是有序集合。這就是我們所說(shuō)的對(duì)象類型,五彩斑斕,各有特色。
編碼
我們都知道超級(jí)英雄有著不同的超能力,蜘蛛俠(Spider-Man) 有蜘蛛感應(yīng),鋼鐵俠(Iron Man)有高科技裝備。同樣,Redis 中的每個(gè)對(duì)象都有一種稱為“編碼”的隱藏能力。這是什么呢?
簡(jiǎn)單說(shuō),編碼是對(duì)象的“內(nèi)部魔法”。它決定了對(duì)象在 Redis 內(nèi)部的存儲(chǔ)方式。就好像手機(jī)里的照片可以是 JPEG 或 PNG 格式,Redis 對(duì)象也可以有不同的編碼格式。
但為什么這很重要呢?因?yàn)椴煌木幋a方式意味著不同的存儲(chǔ)效率和性能。Redis 非常聰明,它會(huì)選擇最佳的編碼方式,為我們節(jié)省空間和提高性能。
我們先來(lái)看下 Redis 對(duì)象結(jié)構(gòu)體聲明
typedef struct redisObject {
unsigned type:4; # 數(shù)據(jù)類型,使用了4位來(lái)表示
unsigned encoding:4; # 編碼方式
void *ptr; # 指向底層數(shù)據(jù)結(jié)構(gòu)的指針
} robj;
Redis 中的每個(gè)對(duì)象都是由 redisObject 結(jié)構(gòu)表示,其中的 encoding 成員記錄了對(duì)象所使用的編碼,encoding 的取值不同,對(duì)象內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu)也會(huì)有所不同。關(guān)于 redis 對(duì)象的各個(gè)數(shù)據(jù)結(jié)構(gòu)的講解,本篇不涉及,后續(xù)會(huì)補(bǔ)上。
分類
字符串對(duì)象
基本概念:
字符串對(duì)象是最簡(jiǎn)單的類型,也是二進(jìn)制安全的,意味著可以存儲(chǔ)任何形式的數(shù)據(jù),例如 JPEG 圖片、序列化的對(duì)象或者純文本。
簡(jiǎn)單圖解:
value 可以存儲(chǔ)任何類型的數(shù)據(jù):包括普通字符串,數(shù)值類型(int,float) 等
內(nèi)部實(shí)現(xiàn)
Redis 的 String 對(duì)象使用一種稱為 簡(jiǎn)單動(dòng)態(tài)字符串SDS(Simple Dynamic String) 的結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù)。
對(duì)象結(jié)構(gòu)體聲明:
typedef struct redisObject {
unsigned type:4; # 數(shù)據(jù)類型,使用了4位來(lái)表示
unsigned encoding:4; # 編碼方式
void *ptr; # 指向底層數(shù)據(jù)結(jié)構(gòu)的指針
} robj;
SDS 結(jié)構(gòu)體聲明:
struct sdshdr {
size_t len; # 記錄buf數(shù)組中已使用字節(jié)的數(shù)量。
size_t alloc; # 記錄buf數(shù)組的總?cè)萘俊? char buf[]; # 字節(jié)數(shù)組,用于保存字符串。
};
String 對(duì)象的編碼
String 對(duì)象的編碼有三種: int、embstr、raw 。
- int 編碼 :對(duì)象 robj 的 ptr 成員指向的是 long 類型的整數(shù)
- embstr 和 raw 編碼 : 對(duì)象 robj 的 ptr 成員指向的是 sdshdr 結(jié)構(gòu)體,值存儲(chǔ)在 buf 中。
使用限制: 單個(gè) String 對(duì)象的值可以存儲(chǔ)的數(shù)據(jù)大小上限為 512MB
常見(jiàn)命令:
基本操作
SET key value : 設(shè)置鍵的值
GET key : 獲取鍵的值
DEL key : 刪除鍵
> SET username "xiaokang"
OK
> GET username
"xiaokang"
> DEL username
(integer) 1
字符串操作
APPEND key value : 向字符串尾部追加
STRLEN key : 獲取字符串的長(zhǎng)度
SETRANGE key offset value : 覆蓋部分內(nèi)容
GETRANGE key start end : 獲取子字符串
# 上面命令的參數(shù) offset、start、end 都指的是下標(biāo)(從0開(kāi)始)
# 初始鍵值對(duì) : SET username "xiaokang"
> APPEND username 1998
(integer) 12
> GET username
"xiaokang1998"
> STRLEN username
(integer) 12
> SETRANGE username 0 kang
(integer) 12
> GET username
"kangkang1998"
> GETRANGE username 8 -1 # -1代表的是值的結(jié)尾
"1998"
數(shù)值操作
INCR/DECY key : 自增1或自減1
INCRBY/DECRBY key increment : 自增或自減整數(shù),步長(zhǎng)為 increment 必須為整數(shù),可正可負(fù)
INCRBYFLOAT key increment : 自增、自減浮點(diǎn)數(shù),increment 推薦使用浮點(diǎn)數(shù),代表你是在操作浮點(diǎn)數(shù),可正可負(fù)
# redis 中沒(méi)有提供 DECRBYFLOAT,所以要想遞減浮點(diǎn)數(shù),increment 為負(fù)即可
> set count 2
OK
> INCR count
(integer) 3
> DECR count
(integer) 2
127.0.0.1:6379> INCRBY count 3
(integer) 5
127.0.0.1:6379> DECRBY count 3
(integer) 2
> SET float_count 5.5
OK
> INCRBYFLOAT float_count 2.5
"8"
> INCRBYFLOAT float_count -3.5
"4.5"
批量操作
MSET key1 value1 key2 value2 : 批量設(shè)置鍵值
MGET key1 key2 : 批量獲取鍵值
> MSET username xiaokang age 25
OK
> GET username
"xiaokang"
> MGET username age
1) "xiaokang"
2) "25"
條件設(shè)置
SETNX key value : 僅當(dāng)鍵不存在時(shí)設(shè)置值,鍵存在則不會(huì)執(zhí)行任何操作
MSETNX key value [key value ...] : 批量設(shè)置
> SETNX career programmer
(integer) 1
> MSETNX sex man hobby swim
(integer) 1
帶有過(guò)期時(shí)間的設(shè)置
SETEX key seconds value : 為鍵值設(shè)置過(guò)期時(shí)間
# 10s 后 redis 會(huì)自動(dòng)刪除這個(gè) key
> SETEX username 10 xiaokang
OK
應(yīng)用案例
計(jì)數(shù)器
描述: 利用 Redis 追蹤某些事物的數(shù)量。
具體應(yīng)用 :
1. 文章訪問(wèn)計(jì)數(shù):文章的閱讀次數(shù)
#當(dāng)某篇文章被訪問(wèn)時(shí),遞增該文章的閱讀計(jì)數(shù)器。
INCR article:12345:views
#獲取某篇文章的閱讀次數(shù)。
GET article:12345:views
2. 社交媒體互動(dòng)計(jì)數(shù) :點(diǎn)贊數(shù)
#當(dāng)某個(gè)帖子被點(diǎn)贊時(shí),遞增該帖子的點(diǎn)贊計(jì)數(shù)器。
INCR post:67890:likes
#檢查某個(gè)帖子的點(diǎn)贊數(shù)。
GET post:67890:likes
3. 實(shí)時(shí)統(tǒng)計(jì) :例如,一個(gè)電商網(wǎng)站可以使用 Redis 來(lái)跟蹤網(wǎng)站上當(dāng)前的在線用戶數(shù)量
#當(dāng)用戶在線時(shí),遞增在線用戶計(jì)數(shù)器。
INCR website:online_users
#檢查當(dāng)前在線的用戶數(shù)量。
GET website:online_users
4. 限流: 例如,你可能想限制一個(gè) API 在一定時(shí)間內(nèi)的調(diào)用次數(shù)。
# 設(shè)置一個(gè) API 的調(diào)用次數(shù)限制。這里以 60 秒內(nèi)最多調(diào)用 10 次為例。
SETEX api:call_limit:client_ip 60 10
# 當(dāng) API 被調(diào)用時(shí),遞減調(diào)用計(jì)數(shù)器。如果值小于或等于 0,則表示已達(dá)到限流。
DECRBY api:call_limit:client_ip 1
# 檢查某個(gè) API 的剩余調(diào)用次數(shù)。
GET api:call_limit:client_ip
分布式鎖 : 超越傳統(tǒng)的鎖機(jī)制
想象一下,一個(gè)電商網(wǎng)站正在進(jìn)行一次秒殺活動(dòng),該活動(dòng)只有100個(gè)商品庫(kù)存。當(dāng)活動(dòng)開(kāi)始時(shí),數(shù)萬(wàn)用戶同時(shí)嘗試購(gòu)買這些商品。
如果秒殺系統(tǒng)只部署在一個(gè)服務(wù)器上,那么我們可以使用普通鎖來(lái)保證庫(kù)存不會(huì)被超賣。但是,現(xiàn)在的大型電商平臺(tái)的搶購(gòu)系統(tǒng)都是部署在多個(gè)服務(wù)器上的,所以單個(gè)服務(wù)器上的普通鎖并不能保證整個(gè)系統(tǒng)的數(shù)據(jù)一致性。
這時(shí)候,我們需要一個(gè)更強(qiáng)大的鎖:分布式鎖, 那什么是分布式鎖呢? 分布式鎖,顧名思義,是能在多個(gè)系統(tǒng)或多臺(tái)機(jī)器之間都起到限制訪問(wèn)的“鎖”。
在秒殺活動(dòng)這個(gè)場(chǎng)景中,分布式鎖確保了即便是數(shù)萬(wàn)用戶在多個(gè)服務(wù)器上同時(shí)嘗試購(gòu)買,系統(tǒng)也能正確、有序地處理每一個(gè)購(gòu)買請(qǐng)求,確保不會(huì)出現(xiàn)超賣的情況。
基本概念:
分布式鎖是一種能夠在多個(gè)計(jì)算機(jī)、服務(wù)器或節(jié)點(diǎn)之間確保任何時(shí)候只有一個(gè)進(jìn)程在執(zhí)行的機(jī)制。它是在復(fù)雜的分布式環(huán)境中維持順序和一致性的關(guān)鍵工具。
實(shí)現(xiàn)方式:
使用 Redis 實(shí)現(xiàn)分布式鎖一般步驟:
- 加鎖:SET lock_key unique_id EX expire_time NX
- lock_key : 分布式鎖名
- unique_id : 唯一標(biāo)識(shí)符
- EX : 設(shè)置過(guò)期時(shí)間
- NX : 當(dāng) lock_key 不存在時(shí)命令才會(huì)成功
- 操作共享資源
- 釋放鎖:通過(guò) Lua 腳本來(lái)釋放鎖,先 GET 判斷鎖是否歸屬自己,再 DEL 釋放鎖
Lua 腳本 : 用來(lái)保證釋放鎖操作的原子性
判斷鎖是自己的,才釋放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
上面實(shí)現(xiàn)方式存在一個(gè)問(wèn)題:分布式鎖的過(guò)期時(shí)間如何確定?
如果客戶端預(yù)期的操作時(shí)間超過(guò)鎖的過(guò)期時(shí)間,這該怎么辦? 鎖的超時(shí)時(shí)間設(shè)置過(guò)長(zhǎng)過(guò)短都不好。一個(gè)合理的方案就是采用自動(dòng)續(xù)期。
續(xù)期具體做法:
加鎖時(shí),我們?cè)O(shè)定一個(gè)到期時(shí)間,啟動(dòng)一個(gè)「守護(hù)線程」 定時(shí)查看鎖的狀態(tài)。如果鎖即將到期且任務(wù)未完成,我們自動(dòng)「續(xù)期」 這個(gè)鎖,重新設(shè)置其到期時(shí)間。如果續(xù)期失敗,為避免并發(fā)問(wèn)題,客戶端應(yīng)立刻停止操作。
然而幸運(yùn)的是,一些編程語(yǔ)言已經(jīng)實(shí)現(xiàn)了專門的客戶端庫(kù),如 Java 的 Redisson和 Go 的 redsync,它們提供了簡(jiǎn)化的分布式鎖實(shí)現(xiàn)。這些庫(kù)已內(nèi)置了自動(dòng)續(xù)期等關(guān)鍵功能,避免開(kāi)發(fā)者手動(dòng)構(gòu)建這些邏輯。
注意:
以上只是實(shí)現(xiàn)了一個(gè)單機(jī)版的分布式鎖,而 Redis 在實(shí)際生產(chǎn)環(huán)境中都會(huì)采用主從集群 + 哨兵的模式部署,這樣當(dāng)主庫(kù)異常宕機(jī)時(shí),哨兵可以實(shí)現(xiàn)「故障自動(dòng)切換」,把從庫(kù)提升為主庫(kù),繼續(xù)提供服務(wù),以此保證可用性。
我們來(lái)考慮一個(gè)問(wèn)題:當(dāng)「主從發(fā)生切換」時(shí),這個(gè)分布鎖會(huì)依舊安全嗎?
關(guān)于這個(gè)問(wèn)題我這里不做深入探討,感興趣的可以參考這篇文章:「鏈接地址在文章末尾」
緩存
描述:利用 Redis 緩存 MYSQL 等關(guān)系型數(shù)據(jù)庫(kù)查詢結(jié)果,從而減少關(guān)系型數(shù)據(jù)庫(kù)的壓力。
具體實(shí)現(xiàn):
當(dāng)用戶請(qǐng)求某個(gè)數(shù)據(jù)時(shí),首先檢查 Redis 是否有這個(gè)數(shù)據(jù)。如果有,直接從 Redis 返回,這樣可以避免查詢數(shù)據(jù)庫(kù)。如果 Redis 中沒(méi)有,那么查詢關(guān)系型數(shù)據(jù)庫(kù),獲取數(shù)據(jù)后,存入 Redis,并設(shè)置一個(gè)適當(dāng)?shù)倪^(guò)期時(shí)間。
簡(jiǎn)單示例
func getUserInfo(rdb *redis.Client, userID string) string {
// 嘗試從 Redis 中獲取用戶信息
val, err := rdb.Get(ctx, userID).Result()
if err == redis.nil{
userData := queryDatabase(userID) #在這里模擬一個(gè)數(shù)據(jù)庫(kù)查詢
rdb.Set(ctx, userID, userData, time.Hour) # 緩存結(jié)果1小時(shí)
return userData
}
return val
}
func queryDatabase(userID string) string {
//查詢數(shù)據(jù)庫(kù)
// ...
return result
}
Session 存儲(chǔ)
Session 是什么:
每次你在網(wǎng)上購(gòu)物時(shí),在瀏覽、選擇商品、加入購(gòu)物車,網(wǎng)站都能“記得”你的選擇,這是因?yàn)樗褂昧?Session"。簡(jiǎn)單來(lái)說(shuō),Session 是服務(wù)器給你的一個(gè)小“記憶空間”。
在日常的網(wǎng)頁(yè)瀏覽中,每當(dāng)一個(gè)用戶的請(qǐng)求到達(dá)服務(wù)器,例如頁(yè)面訪問(wèn)、API調(diào)用,為了維持用戶狀態(tài)或提供個(gè)性化的服務(wù),系統(tǒng)通常需要讀取該用戶的 session 數(shù)據(jù)。
具體實(shí)現(xiàn):
當(dāng)用戶登錄到一個(gè)系統(tǒng)時(shí),后端通常會(huì)為該用戶生成一個(gè)唯一的 session ID 。這個(gè) ID 會(huì)被傳回給客戶端,通常存儲(chǔ)在 cookie 中。隨后,每次客戶端發(fā)出請(qǐng)求時(shí),都會(huì)攜帶這個(gè) session ID,允許服務(wù)器識(shí)別出該用戶。
為了應(yīng)對(duì)這種頻繁的數(shù)據(jù)讀取需求,我們可以將這個(gè) session ID 和 session 數(shù)據(jù)分別作為鍵值存儲(chǔ)到 redis 中。 session 數(shù)據(jù)包括 : 用戶 ID、用戶名等。使用 Redis 來(lái)存儲(chǔ) session 數(shù)據(jù)不僅提供了高速的讀取效率,還讓用戶體驗(yàn)更為流暢。
使用示例:
# 在設(shè)置 session 存儲(chǔ)時(shí),一般會(huì)設(shè)置過(guò)期時(shí)間的。
SET session:userId expired_time "user_data_in_json_or_serialized_format"
GET session:userId
列表對(duì)象
基本概念
Redis 的列表對(duì)象是一個(gè)有序的字符串集合,這里的有序指的是添加元素有先后順序的,可以被看作是一個(gè)雙向鏈表。在 Redis 中,每個(gè)列表可以包含超過(guò) 4 億個(gè)元素。
簡(jiǎn)單圖解
內(nèi)部實(shí)現(xiàn)
- 壓縮列表 (Ziplist): 當(dāng)列表對(duì)象保存的所有字符串元素的長(zhǎng)度都小于 64 字節(jié)并且列表對(duì)象保存的元素?cái)?shù)量小于 512 個(gè),列表對(duì)象會(huì)使用壓縮列表(ziplist)作為其底層實(shí)現(xiàn)。
- 雙向鏈表(Linkedlist):不滿足上述兩個(gè)條件之一的, 列表對(duì)象會(huì)使用雙向鏈表(Linkedlist) 作為其底層實(shí)現(xiàn)。
常見(jiàn)命令:
查找元素
LRANGE key start stop : 獲取列表指定范圍內(nèi)的元素
LINDEX key index : 通過(guò)索引獲取列表中的元素
LLEN key : 返回列表的長(zhǎng)度
# 查看列表對(duì)象的所有元素,-1 代表列表對(duì)象的最后一個(gè)元素
> LRANGE myList 0 -1
1) "grape"
2) "banana"
3) "apple"
4) "orange"
> LINDEX myList 1
"banana"
> LLEN myList
(integer) 4
插入元素 :
- 普通插入
LPUSH key element [element ...] : 將一個(gè)或多個(gè)值插入到列表頭部
RPUSH key element [element ...] : 將一個(gè)或多個(gè)值插入到列表尾部
> LPUSH myList "apple" "banana"
(integer) 2
> LRANGE myList 0 -1
1) "banana"
2) "apple"
> RPUSH myList "orange" "pear"
(integer) 4
> LRANGE myList 0 -1
1) "banana"
2) "apple"
3) "orange"
4) "pear"
- 條件插入
LPUSHX key element [element ...] : 只有當(dāng)列表存在時(shí),將值插入到列表的頭部,如果列表不存在,則什么也不做
RPUSHX key element [element ...] : 只有當(dāng)列表存在時(shí),將值插入到列表的尾部,如果列表不存在,則什么也不做
> LPUSHX myList "grape"
(integer) 5
> LRANGE myList 0 -1
1) "grape"
2) "banana"
3) "apple"
4) "orange"
5) "pear"
> LPUSHX noExistList "peach"
(integer) 0
刪除元素:
LPOP key [count] : 移除并返回列表的前 count 個(gè)元素
RPOP key [count] : 移除并返回列表的最后 count 個(gè)元素
LREM key count value : 移除列表中與參數(shù) value 相等的 count 個(gè)元素
# 先查看列表中的元素
> LRANGE myList 0 -1
1) "grape"
2) "banana"
3) "apple"
4) "orange"
5) "pear"
> LPOP myList
"grape"
> LRANGE myList 0 -1
1) "banana"
2) "apple"
3) "orange"
4) "pear"
> RPOP myList
"pear"
> LRANGE myList 0 -1
1) "banana"
2) "apple"
3) "orange"
修改元素:
LSET key index value : 通過(guò)索引來(lái)設(shè)置元素的值
LTRIM key start stop :裁剪列表
# 先查看列表中的元素
> LRANGE myList 0 -1
1) "grape"
2) "banana"
3) "apple"
4) "orange"
> LSET myList 0 "peach"
OK
> LRANGE myList 0 -1
1) "peach"
2) "banana"
3) "apple"
4) "orange"
> LTRIM myList 0 2
OK
> LRANGE myList 0 -1
1) "peach"
2) "banana"
3) "apple"
元素轉(zhuǎn)移:
RPOPLPUSH source destination: 將 source 列表的最后一個(gè)元素彈出,并將該元素添加到 destination 列表的頭部,同時(shí)返回該元素,如果 destination 列表不存在,redis 會(huì)自動(dòng)創(chuàng)建 destination 列表
> LRANGE myList 0 -1
1) "banana"
2) "apple"
> RPOPLPUSH myList destList
"apple"
> LRANGE destList 0 -1
1) "apple"
要注意的是列表對(duì)象并不存在 LPOPRPUSH 命令,可以通過(guò)組合 LPOP 和 RPUSH 命令來(lái)實(shí)現(xiàn)類似效果。
阻塞操作:
BLPOP key [key ...] timeout : 移除并獲取列表的第一個(gè)元素,或阻塞直到有一個(gè)可用
BRPOP key [key ...] timeout : 移除并獲取列表的最后一個(gè)元素,或阻塞直到有一個(gè)可用
BRPOPLPUSH source destination timeout : 將 source 列表的最后一個(gè)元素彈出,并將該元素添加到 destination 列表的頭部,同時(shí)返回該元素
# 先查看列表中的元素
> LRANGE myList 0 -1
1) "peach"
2) "banana"
3) "apple"
4) "orange"
> LRANGE noExistList 0 -1
(empty array)
> BLPOP myList noExistList 10
1) "myList"
2) "peach"
> LRANGE myList 0 -1
1) "banana"
2) "apple"
3) "orange"
> BRPOP myList noExistList 10
1) "myList"
2) "orange"
# 先查看列表的初識(shí)元素
> LRANGE myList 0 -1
1) "banana"
> LRANGE destList 0 -1
1) "apple"
> BRPOPLPUSH myList destList 10
"banana"
> LRANGE myList 0 -1
(empty array)
> LRANGE destList 0 -1
1) "banana"
2) "apple"
應(yīng)用案例:
1.消息隊(duì)列 :
當(dāng)用戶在網(wǎng)上購(gòu)物下訂單后,為了不讓他們等待各種后續(xù)處理(如檢查庫(kù)存、處理付款、發(fā)貨),我們直接把訂單放入一個(gè)消息隊(duì)列,然后由后臺(tái)進(jìn)程從隊(duì)列中獲取并處理訂單。
實(shí)現(xiàn)步驟:
1. 生產(chǎn)者(下訂單的用戶)
LPUSH orders_queue order_id
2. 消費(fèi)者服務(wù)獲取訂單( 消費(fèi)者:后臺(tái)處理訂單的服務(wù))
BRPOP orders_queue 5
3. 處理訂單 :
一旦消費(fèi)者從隊(duì)列中獲取了一個(gè)新訂單,它可以開(kāi)始進(jìn)行必要的處理,
例如檢查庫(kù)存、處理付款等。
2.棧和隊(duì)列 :
棧: 想象一個(gè)瀏覽器的返回功能。用戶訪問(wèn)了幾個(gè)頁(yè)面,你希望能夠提供一個(gè)[返回] 按鈕讓用戶回到他們之前瀏覽的頁(yè)面。
實(shí)現(xiàn)步驟:
1. 添加元素
LPUSH browser_history "page1"
LPUSH browser_history "page2"
LPUSH browser_history "page3"
2.當(dāng)用戶點(diǎn)擊返回按鈕時(shí):
LPOP browser_history # Fetches "page3", the most recently visited
隊(duì)列:
當(dāng)我們?cè)陔娚叹W(wǎng)站上瀏覽商品、點(diǎn)擊廣告或執(zhí)行其他操作時(shí),這些行為都可以被捕獲為一個(gè)"事件"。每個(gè)事件通常都包含一些基本信息,如用戶ID、商品ID、點(diǎn)擊時(shí)間、頁(yè)面URL等。
實(shí)現(xiàn)步驟:
- 用戶點(diǎn)擊事件捕獲
假設(shè)一個(gè)用戶點(diǎn)擊了一個(gè)商品。此時(shí),我們可以創(chuàng)建一個(gè) JSON 對(duì)象來(lái)存儲(chǔ)這次點(diǎn)擊的詳細(xì)信息。
$json = {
"user_id": "12345",
"product_id": "98765",
"timestamp": "2023-09-01T12:00:00Z",
"page_url": "/products/98765"
}
- 發(fā)送事件到隊(duì)列
LPUSH click_events $json
- 消費(fèi)者進(jìn)程獲取事件
RPOP click_events
當(dāng)消費(fèi)者進(jìn)程從隊(duì)列中獲取事件后,可以進(jìn)一步解析這個(gè)JSON對(duì)象,并進(jìn)行所需的處理,例如更新商品的點(diǎn)擊率等,以便將該商品推薦給更多的用戶。
3.歷史追蹤:
可以使用 Redis 列表來(lái)跟蹤最近的歷史記錄,例如最近訪問(wèn)的網(wǎng)頁(yè)或其他活動(dòng)。
以最近訪問(wèn)的網(wǎng)頁(yè)舉例說(shuō)明: 在一個(gè)網(wǎng)站上,你可能希望追蹤用戶最近訪問(wèn)了哪些頁(yè)面。每當(dāng)用戶訪問(wèn)一個(gè)新頁(yè)面時(shí),你可以使用 LPUSH 將這個(gè)頁(yè)面 URL 添加到一個(gè)列表中,并使用 LTRIM 確保列表只保存最近 N 次訪問(wèn)。
LPUSH user_recent_pages "/home"
LPUSH user_recent_pages "/product/1"
LTRIM user_recent_pages 0 9 # 保留最近10個(gè)訪問(wèn)的頁(yè)面
使用 Redis 列表來(lái)跟蹤最近的歷史記錄能夠高效地保存和查詢用戶的近期活動(dòng),從而為用戶提供個(gè)性化的推薦。
哈希對(duì)象
基本概念
Redis 哈希對(duì)象也是一種用于存儲(chǔ)鍵值對(duì)集合的數(shù)據(jù)結(jié)構(gòu),它允許你將多個(gè)鍵值對(duì)存儲(chǔ)在一個(gè) Redis 鍵中。
我們一般會(huì)使用 Redis 的 Hash 對(duì)象來(lái)存儲(chǔ)對(duì)象信息,比如用戶信息,用戶的名字、年齡、愛(ài)好、電子郵箱、密碼等。與使用普通的 key-value 存儲(chǔ)方式相比 , 哈希對(duì)象的存儲(chǔ)方式更為高效和節(jié)省空間,特別是當(dāng)我們要存儲(chǔ)大量小對(duì)象時(shí)。
簡(jiǎn)單圖解:
簡(jiǎn)單說(shuō)明:
假設(shè)你有一把大鑰匙,這把鑰匙可以打開(kāi)一個(gè)特定的箱子。這箱子里面有很多物品,每個(gè)物品都有標(biāo)簽來(lái)描述它。
- 大鑰匙 就是我們的 key
- 箱子 就代表一個(gè) 哈希對(duì)象
- 箱子里的物品與其標(biāo)簽 就是 field-value 鍵值對(duì)
內(nèi)部實(shí)現(xiàn)
- 壓縮列表(ziplist) : 當(dāng)哈希對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長(zhǎng)度都小于64字節(jié)并且哈希對(duì)象保存的鍵值對(duì)的數(shù)量小于 512 個(gè),哈希對(duì)象會(huì)采用壓縮列表作為其底層實(shí)現(xiàn)。
- 字典(基于哈希表實(shí)現(xiàn)) : 當(dāng)不能滿足上述條件之一時(shí),哈希對(duì)象則會(huì)采用字典作為其底層實(shí)現(xiàn)。
常見(jiàn)命令
設(shè)置值
HSET key field value [field value ...] : 設(shè)置哈希的 Field-Value 對(duì),可以設(shè)置多個(gè)
HSETNX key field value : 只有在字段 field 不存在時(shí),才設(shè)置對(duì)應(yīng)的 value 值,否則什么也不做。
> HSET userInfo username xiaokang age 25 hobby swim
(integer) 3
獲取值
HGET key field : 獲取哈希指定字段的值
HMGET key field [field ...] : 獲取哈希多個(gè)字段的值 # 批量獲取
HGETALL key : 獲取哈希表中的所有字段和值
HKEYS key : 獲取哈希中所有的字段名(field)
HVALS key : 獲取哈希中所有的字段值(value)
> HGET userInfo username
"xiaokang"
> HMGET userInfo "username" "age"
1) "xiaokang"
2) "25"
> HGETALL userInfo
1) "username"
2) "xiaokang"
3) "age"
4) "25"
5) "hobby"
6) "swim"
> HKEYS userInfo
1) "username"
2) "age"
3) "hobby"
> HVALS userInfo
1) "xiaokang"
2) "25"
3) "swim"
自增操作
HINCRBY key field increment : 為哈希字段的整數(shù)值加上增量 # increment 可正可負(fù)
HINCRBYFLOAT key field increment : 為哈希字段的浮點(diǎn)數(shù)值加上增量 # increment 可正可負(fù)
> HGET userInfo age
"25"
> HINCRBY userInfo age 1
(integer) 26
> HGET userInfo age
"26"
# HINCRBYFLOAT 命令類似,increment 既可以是整數(shù)也可以是浮點(diǎn)數(shù),可正可負(fù)
刪除操作
HDEL key field [field ...] : 刪除一個(gè)或多個(gè)哈希表字段
# 先查看 hash 字段的所有字段和值
> HGETALL userInfo
1) "username"
2) "xiaokang"
3) "age"
4) "25"
5) "hobby"
6) "swim"
> HDEL userInfo "hobby"
(integer) 1
> HGETALL userInfo
1) "username"
2) "xiaokang"
3) "age"
4) "25"
其他操作
HEXISTS key field : 檢查哈希對(duì)象中是否存在給定的字段
HLEN key : 獲取哈希中字段的數(shù)量
HSTRLEN key field : 獲取哈希字段的字符串長(zhǎng)度
# 先查看哈希的所有字段和值
> HGETALL userInfo
1) "username"
2) "xiaokang"
3) "age"
4) "25"
> HLEN userInfo
(integer) 2
> HSTRLEN userInfo "username"
(integer) 8
注意事項(xiàng)
- 小哈希優(yōu)化: Redis 對(duì)于哈希對(duì)象的內(nèi)存布局進(jìn)行了優(yōu)化。小哈希(即哈希對(duì)象字段數(shù)量很少且字段值大小較?。┑膬?nèi)存使用會(huì)更加高效。
- 避免大量刪除: 使用 HDEL 一次刪除大量字段可能會(huì)影響性能,建議分批進(jìn)行。
- 使用哈希而非多個(gè)鍵: 當(dāng)需要存儲(chǔ)有關(guān)特定對(duì)象的多個(gè)相關(guān)字段時(shí),使用單個(gè)哈希鍵比使用多個(gè)獨(dú)立的 Redis 鍵更為高效。
應(yīng)用案例
- 對(duì)象存儲(chǔ) :
主要是指將某種實(shí)體或數(shù)據(jù)(如用戶信息)持久性地保存在 Redis 中,如 :用戶的用戶名、郵箱、密碼等信息。
保存用戶信息:
HSET user:1234 name "John Doe" email "john.doe@example.com" age 30
獲取用戶的郵箱:
HGET user:1234 email
- 對(duì)象緩存:
這是指當(dāng)數(shù)據(jù)原本存儲(chǔ)在其他存儲(chǔ)系統(tǒng)(如關(guān)系型數(shù)據(jù)庫(kù))中,但由于頻繁訪問(wèn)或讀取,我們決定在 Redis 中緩存該數(shù)據(jù)的一份副本,以減少對(duì)原始數(shù)據(jù)源的訪問(wèn)壓力并提高讀取速度。
場(chǎng)景描述:
假設(shè)你有一個(gè)博客網(wǎng)站,用戶可以閱讀和評(píng)論各種博客文章。每當(dāng)用戶點(diǎn)擊一個(gè)博客標(biāo)題,系統(tǒng)都會(huì)從關(guān)系型數(shù)據(jù)庫(kù)中獲取該文章的詳細(xì)內(nèi)容進(jìn)行顯示。但是,由于某些熱門文章被大量用戶頻繁訪問(wèn),直接從數(shù)據(jù)庫(kù)中獲取文章可能會(huì)對(duì)數(shù)據(jù)庫(kù)造成很大的壓力,從而影響網(wǎng)站的性能。
具體實(shí)現(xiàn):
偽代碼展示 :
import (
"github.com/go-redis/redis/v8"
"context"
)
var ctx = context.Background()
options := &redis.Options{
Addr: "localhost:6379", // Redis服務(wù)器地址
}
// 初始化Redis客戶端:
rdb := redis.NewClient(options)
// 檢查緩存:當(dāng)用戶請(qǐng)求一篇文章時(shí),首先檢查Redis緩存中是否存在該文章的數(shù)據(jù)。
exists, err := rdb.HExists(ctx, "article:ID", "title").Result()
if err != nil {
// Handle error
}
//從緩存中獲取數(shù)據(jù):如果文章存在于Redis中,則直接從Redis的哈希對(duì)象中獲取所有相關(guān)字段,并顯示給用戶。
if exists {
articleData, err := rdb.HGetAll(ctx, "article:ID").Result()
if err != nil {
// Handle error
}
display(articleData)
}
//從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù):如果文章不在Redis緩存中,則從關(guān)系型數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)保存到 Redis 緩存中,并且設(shè)置超時(shí)。
if !exists {
articleData := Database.fetchArticleByID("article:ID")
_, err := rdb.HMSet(ctx, "article:ID", map[string]interface{}{
"title": articleData.Title,
"author": articleData.Author,
"content": articleData.Content,
}).Result()
if err != nil {
// Handle error
}
rdb.Expire(ctx, "article:ID", time.Hour) // 設(shè)置1小時(shí)的過(guò)期時(shí)間
// 顯示給用戶
display(articleData)
}
//顯示給用戶的函數(shù)
func display(articleData map[string]string) {
// ... 你的顯示邏輯
}
- 實(shí)時(shí)統(tǒng)計(jì) :
為了跟蹤網(wǎng)站的實(shí)時(shí)活動(dòng),我們可以使用哈希來(lái)保存當(dāng)前在線用戶、頁(yè)面瀏覽量和API 請(qǐng)求次數(shù)等。
增加在線用戶數(shù):
HINCRBY website:stats online_users 1
減少在線用戶數(shù):
HINCRBY website:stats online_users -1
記錄每個(gè)頁(yè)面的訪問(wèn)次數(shù):
HINCRBY pageviews:20230901 "/home" 1
集合對(duì)象
基本概念:
集合對(duì)象(Set)是一種存儲(chǔ)多個(gè)唯一元素的無(wú)序集合數(shù)據(jù)結(jié)構(gòu),它提供了豐富的操作使得集合對(duì)象成為非常強(qiáng)大和靈活的工具。集合對(duì)象特別適用于存儲(chǔ)不允許重復(fù)的數(shù)據(jù)項(xiàng),例如標(biāo)簽、社交網(wǎng)絡(luò)中的好友列表或者任何需要快速判斷某個(gè)元素是否存在的場(chǎng)景。
簡(jiǎn)單圖解
內(nèi)部實(shí)現(xiàn)
- 整數(shù)集合(intset) : 當(dāng)集合對(duì)象保存的所有元素都是整數(shù)或者集合對(duì)象保存的整數(shù)個(gè)數(shù)不超過(guò)512個(gè)時(shí),集合對(duì)象采用 intset 作為其底層實(shí)現(xiàn)。
- 字典(基于哈希表實(shí)現(xiàn)): 不滿足上面的兩個(gè)條件之一,則采用字典作為其底層實(shí)現(xiàn)。
常見(jiàn)命令
基本操作:
SADD key member [member ...] :向集合添加一個(gè)或多個(gè)成員
SMEMBERS key :獲取集合中的所有成員
SCARD key :獲取集合的成員數(shù)量
SISMEMBER key member :判斷 member 元素是否是集合 key 的成員
SREM key member [member ...] :移除集合中的一個(gè)或多個(gè)成員
> SADD mySet "apple" "banana" "peach"
(integer) 3
> SMEMBERS mySet
1) "peach"
2) "banana"
3) "apple"
> SCARD mySet
(integer) 3
> SISMEMBER mySet "peach" # 成員存在返回 1
(integer) 1
> SISMEMBER mySet "pear" # 成員不存在返回 0
(integer) 0
> SREM mySet "peach"
(integer) 1
> SMEMBERS mySet
1) "banana"
2) "apple"
集合運(yùn)算:
SINTER key [key ...] :返回所有給定集合的交集
SINTERSTORE destination key [key ...] :交集存儲(chǔ)在 destination 集合中
SUNION key [key ...] :返回所有給定集合的并集
SUNIONSTORE destination key [key ...] :并集存儲(chǔ)在 destination 集合中
SDIFF key [key ...] :返回第一個(gè)集合與其他集合之間的差集
SDIFFSTORE destination key [key ...] :差集存儲(chǔ)在 destination 集合中
> SMEMBERS mySet
1) "pear"
2) "banana"
3) "apple"
> SMEMBERS yourSet
1) "peach"
2) "banana"
3) "apple"
> SINTER mySet yourSet
1) "banana"
2) "apple"
> SINTERSTORE resultSet mySet yourSet
(integer) 2
> SMEMBERS resultSet
1) "banana"
2) "apple"
# 并集我這里不提了,和交集操作類似
> SDIFF mySet yourSet
1) "pear"
> SDIFFSTORE result mySet yourSet
(integer) 1
> SMEMBERS result
1) "pear"
隨機(jī)操作:
SRANDMEMBER key [count] :隨機(jī)返回集合中的一個(gè)或多個(gè)成員
SPOP key [count] :隨機(jī)移除并返回集合中的一個(gè)或多個(gè)成員
> SMEMBERS mySet
1) "pear"
2) "banana"
3) "apple"
# 可以看到每次返回的成員都不一樣
> SRANDMEMBER mySet
"pear"
> SRANDMEMBER mySet
"apple"
> SPOP mySet
"pear"
> SMEMBERS mySet
1) "banana"
2) "apple"
其他操作:
SMOVE source destination member : 將 member 成員從 source 集合移動(dòng)到 destination 集合
> SMEMBERS mySet
1) "pear"
2) "banana"
3) "apple"
> SMEMBERS yourSet
1) "peach"
2) "banana"
3) "apple"
> SMOVE mySet yourSet "pear"
(integer) 1
> SMEMBERS mySet
1) "banana"
2) "apple"
> SMEMBERS yourSet
1) "peach"
2) "pear"
3) "banana"
4) "apple"
應(yīng)用案例
- 社交網(wǎng)站的好友與關(guān)注系統(tǒng)
利用集合來(lái)存儲(chǔ)每個(gè)用戶的朋友或關(guān)注者列表。例如,我們可以為每個(gè)用戶維護(hù)一個(gè)集合,其中包含他們的所有朋友或關(guān)注者。
SADD "alice:friends" "bob" # Alice 關(guān)注了 Bob
SADD "bob:friends" "charlie" # Bob 關(guān)注了 Charlie
SISMEMBER "alice:friends", "bob" # 判斷 Bob 是否是 Alice 的朋友
- 標(biāo)簽系統(tǒng)
可以為內(nèi)容(如文章、圖片等)添加標(biāo)簽,并利用集合存儲(chǔ)這些標(biāo)簽。
場(chǎng)景描述:假設(shè)你正在運(yùn)行一個(gè)博客平臺(tái),你希望為每篇文章添加一組標(biāo)簽,使用戶可以更容易地根據(jù)主題或興趣查找相關(guān)文章。
1.為文章添加標(biāo)簽:
# 當(dāng)你發(fā)布文章"Redis入門指南",其ID為1001,你為它添加了標(biāo)簽"technology", "tutorial",和"redis"。
SADD tag:technology 1001
SADD tag:tutorial 1001
SADD tag:redis 1001
2.查找具有特定標(biāo)簽的文章
# 如果用戶想要查看所有與"redis"相關(guān)的文章:
SMEMBERS tag:redis # 輸出 1001 :這表示文章ID為1001的文章帶有"redis"標(biāo)簽。
3.獲取文章的所有標(biāo)簽
# 每當(dāng)你需要查看某篇文章的標(biāo)簽,你可以為其創(chuàng)建一個(gè)集合。以文章ID為1001為例:
SADD article:1001:tags "technology"
SADD article:1001:tags "tutorial"
SADD article:1001:tags "redis"
# 要查看文章1001的所有標(biāo)簽:
SMEMBERS article:1001:tags
# 輸出:
1) "technology"
2) "tutorial"
3) "redis"
- 唯一計(jì)數(shù)
例如,記錄網(wǎng)站的獨(dú)立訪客數(shù)。集合可以幫助我們做到這一點(diǎn),因?yàn)樗鼈冎淮鎯?chǔ)唯一的元素。
1.存儲(chǔ)當(dāng)天登錄的所有用戶ID:
SADD visitors:2023-09-20 user12345
SADD visitors:2023-09-20 user67890
2.獲得當(dāng)天登錄的獨(dú)立用戶數(shù)
SCARD visitors:2023-09-20
有序集合對(duì)象
基本概念:
Redis 有序集合(Sorted Set)對(duì)象是一種存儲(chǔ)非重復(fù)元素集合的數(shù)據(jù)結(jié)構(gòu),每個(gè)元素都關(guān)聯(lián)一個(gè)雙精度浮點(diǎn)數(shù)分?jǐn)?shù)(score),用于維護(hù)元素之間的排序順序。有序集合支持高效的元素插入和刪除操作,同時(shí)保持元素按分?jǐn)?shù)排序,使其非常適合于需要按照排序順序訪問(wèn)元素的場(chǎng)景,如排行榜、帶權(quán)重的任務(wù)隊(duì)列等。
簡(jiǎn)單圖解:
內(nèi)部實(shí)現(xiàn)
- 壓縮列表(ziplist): 當(dāng)有序集合保存的元素?cái)?shù)量小于 128 個(gè) 并且有序集合保存的所有元素成員的長(zhǎng)度小于 64 字節(jié),有序集合對(duì)象會(huì)采用 ziplist 作為其底層實(shí)現(xiàn)。
- 跳表(skiplist ):不滿足以上2個(gè)條件之一的采用 skiplist 作為其底層實(shí)現(xiàn)。
常見(jiàn)命令
添加與更新成員
ZADD key score member [score member ...] : 添加一個(gè)或多個(gè)成員到有序集合,或者更新已存在成員的分?jǐn)?shù)
ZINCRBY key increment member : 增加有序集合中指定成員的分?jǐn)?shù)
> ZADD myZSet 10 member1 20 member2
(integer) 2
# 獲取有序集合中的所有成員及其分?jǐn)?shù)
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "10"
3) "member2"
4) "20"
> ZINCRBY myZSet 5 "member1"
"15"
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "15"
3) "member2"
4) "20"
查詢操作
ZCARD key : 獲取有序集合的成員數(shù)量
ZSCORE key member : 獲取指定成員的分?jǐn)?shù)值
ZRANK key member : 返回有序集合中指定成員的排名,排名依次是 0,1,2 ...
ZREVRANK key member : 返回有序集合中指定成員的排名,成員按分?jǐn)?shù)值遞減排列
> ZCARD myZSet
(integer) 2
> ZSCORE myZSet "member2"
"20"
> ZRANK myZSet "member1"
(integer) 0
> ZRANK myZSet "member2"
(integer) 1
> ZREVRANK myZSet "member2"
(integer) 0
> ZREVRANK myZSet "member1"
(integer) 1
ZRANGE key min max [WITHSCORES] : 返回有序集中指定區(qū)間內(nèi)的成員 # min 和 max 是要獲取的排名范圍(排名可以理解為索引,從0開(kāi)始,其中0是分?jǐn)?shù)最低的成員),可選的 [WITHSCORES] 參數(shù)表示除了返回成員名外,還要返回它們的分?jǐn)?shù)
ZREVRANGE key start stop [WITHSCORES] : 返回有序集中指定區(qū)間內(nèi)的成員,成員按分?jǐn)?shù)遞減排列
> ZRANGE myZSet 0 1
1) "member1"
2) "member2"
> ZRANGE myZSet 0 1 WITHSCORES
1) "member1"
2) "15"
3) "member2"
4) "20"
> ZREVRANGE myZSet 0 1
1) "member2"
2) "member1"
> ZREVRANGE myZSet 0 1 WITHSCORES
1) "member2"
2) "20"
3) "member1"
4) "15"
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] : 獲取分?jǐn)?shù)在指定區(qū)間的所有成員,建議:max >= min
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]: 獲取分?jǐn)?shù)在指定區(qū)間的所有成員,成員按分?jǐn)?shù)遞減排列; 建議:max >= min
#[LIMIT offset count] 這是一個(gè)限制返回的成員數(shù)量的可選項(xiàng)。其中,offset 是開(kāi)始返回的起始位置(基于0的索引),count 是返回的成員總數(shù)。例如,LIMIT 1 3 會(huì)返回從下標(biāo)為1開(kāi)始的3個(gè)成員
> ZRANGEBYSCORE myZSet 16 22
1) "member2"
> ZRANGEBYSCORE myZSet 16 22 WITHSCORES
1) "member2"
2) "20"
> ZRANGEBYSCORE myZSet 15 22 WITHSCORES LIMIT 0 2
1) "member1"
2) "15"
3) "member2"
4) "20"
> ZREVRANGEBYSCORE myZSet 22 15 WITHSCORES
1) "member2"
2) "20"
3) "member1"
4) "15"
ZCOUNT key min max: 計(jì)算在有序集合中指定區(qū)間分?jǐn)?shù)的成員數(shù)
ZLEXCOUNT key min max: 用于計(jì)算基于成員名稱的字典序位于指定區(qū)間內(nèi)的成員數(shù)量 # 對(duì)于 min和 max,要使用方括號(hào) [ 或 ( 來(lái)指示范圍的邊界是否包含在計(jì)數(shù)中。方括號(hào) [ 表示該值是包含的,而小括號(hào) ( 表示該值是不包含的。
> ZCOUNT myZSet 15 20
(integer) 2
> ZLEXCOUNT myZSet [a [z
(integer) 2
> ZLEXCOUNT myZSet [a [b
(integer) 0
ZSCAN key cursor [MATCH pattern] [COUNT count]: 迭代Redis的有序集合(ZSET)的元素,包括它的成員和分?jǐn)?shù)。是一種漸進(jìn)地遍歷有序集的方法,而不是一次返回所有結(jié)果,這對(duì)于大型數(shù)據(jù)集尤為有用,因?yàn)樗粫?huì)因?yàn)橐祷卮罅康慕Y(jié)果而阻塞服務(wù)器。
# cursor: 用于迭代的游標(biāo)。初次調(diào)用時(shí),通常設(shè)置為"0",之后的調(diào)用將使用上次返回的游標(biāo)值。
MATCH pattern (可選): 一個(gè)可選的匹配模式,用于篩選具有特定模式的元素。
COUNT count (可選): 提示服務(wù)器每次迭代應(yīng)返回多少元素。實(shí)際數(shù)目可能會(huì)稍多或稍少。
> ZSCAN myZSet 0
1) "0"
2) 1) "member1"
2) "15"
3) "member2"
4) "20"
> ZSCAN myZSet 0 MATCH mem*
1) "0"
2) 1) "member1"
2) "15"
3) "member2"
4) "20"
> ZSCAN myZSet 0 COUNT 1
1) "0"
2) 1) "member1"
2) "15"
3) "member2"
4) "20"
刪除操作
ZREM key member [member ...] : 移除有序集合中的一個(gè)或多個(gè)成員
# 先查看有序集合中的成員及分?jǐn)?shù)
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "15"
3) "member2"
4) "20"
> ZREM myZSet "member1"
(integer) 1
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member2"
2) "20"
ZPOPMAX key [count] : 移除并返回有序集合中的最大的一些成員
> ZADD myZSet 15 "member1" 30 "member3" 55 "member5" 40 "member4"
(integer) 4
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "15"
3) "member2"
4) "20"
5) "member3"
6) "30"
7) "member4"
8) "40"
9) "member5"
10) "55"
> ZPOPMAX myZSet 2
1) "member5"
2) "55"
3) "member4"
4) "40"
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "15"
3) "member2"
4) "20"
5) "member3"
6) "30"
ZPOPMIN key [count] : 移除并返回有序集合中的最小的一些成員
# ZPOPMIN 我這里就不舉例了,和 ZPOPMAX 類似
ZREMRANGEBYRANK key start stop : 移除有序集合中給定的排名區(qū)間的所有成員
ZREMRANGEBYSCORE key min max : 移除有序集合中給定的分?jǐn)?shù)區(qū)間的所有成員
ZREMRANGEBYLEX key min max : 移除有序集合中給定的成員名字典區(qū)間的所有成員
> ZREMRANGEBYSCORE myZSet 20 30
(integer) 1
> ZRANGE myZSet 0 -1 WITHSCORES
(empty array)
> ZADD myZSet 15 "member1" 30 "member3" 55 "member5" 40 "member4"
(integer) 4
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member1"
2) "15"
3) "member3"
4) "30"
5) "member4"
6) "40"
7) "member5"
8) "55"
> ZREMRANGEBYLEX myZSet ["member1" ["member4"
(integer) 3
> ZRANGE myZSet 0 -1 WITHSCORES
1) "member5"
2) "55"
集合操作
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] : 計(jì)算給定的一個(gè)或多個(gè)有序集的并集,并存儲(chǔ)在新的 destination 中
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] : 計(jì)算給定的一個(gè)或多個(gè)有序集的交集,并存儲(chǔ)在新的 destination 中
說(shuō)明:
destination - 新的有序集合的名字,用于存放結(jié)果。
numkeys - 指定將要合并的有序集合的數(shù)量。
key [key ...] - 需要合并的有序集合的名字。
WEIGHTS - 可選參數(shù),用于為每一個(gè)輸入的有序集分配一個(gè)乘法因子。例如,如果某個(gè)集合的 WEIGHT 是2,那么在計(jì)算并集時(shí),該集合中的每個(gè)元素的分?jǐn)?shù)都會(huì)乘以2。
AGGREGATE - 可選參數(shù),它決定了當(dāng)多個(gè)有序集合中存在相同元素時(shí),如何處理這些元素的分?jǐn)?shù)。有三個(gè)選項(xiàng):SUM(默認(rèn))、MIN和MAX。SUM將相同元素的分?jǐn)?shù)加起來(lái),MIN 使用最小分?jǐn)?shù),而 MAX 使用最大分?jǐn)?shù)。
> ZADD set1 1 "one" 2 "two"
(integer) 2
> ZADD set2 1 "one" 3 "three"
(integer) 2
> ZRANGE set1 0 -1 WITHSCORES
1) "one"
2) "1"
3) "two"
4) "2"
> ZRANGE set2 0 -1 WITHSCORES
1) "one"
2) "1"
3) "three"
4) "3"
> ZUNIONSTORE result 2 set1 set2
(integer) 3
> ZRANGE result 0 -1 WITHSCORES
1) "one"
2) "2"
3) "two"
4) "2"
5) "three"
6) "3"
# 當(dāng)然,你可以使用 WEIGHTS 和 AGGREGATE 參數(shù)來(lái)調(diào)整結(jié)果的計(jì)算方式。例如,如果你想要為 set1 的每個(gè)成員加倍其分?jǐn)?shù),然后和 set2 求并集,并取每個(gè)成員的最大值作為結(jié)果:
# set1 set2 兩個(gè)集合的初始值沒(méi)變,還是上面的數(shù)據(jù)
> ZUNIONSTORE result 2 set1 set2 WEIGHTS 2 1 AGGREGATE MAX
(integer) 3
> ZRANGE result 0 -1 WITHSCORES
1) "one"
2) "2"
3) "three"
4) "3"
5) "two"
6) "4"
應(yīng)用案例
實(shí)時(shí)排行榜
- 游戲排行榜
在線游戲的實(shí)時(shí)排行榜,玩家或用戶的得分可以即時(shí)更新,并按分?jǐn)?shù)進(jìn)行排序。
# 添加或更新分?jǐn)?shù)
ZADD leaderboard 1500 "player1"
ZADD leaderboard 2200 "player2"
ZADD leaderboard 1800 "player3"
#查詢前3名玩家
ZREVRANGE leaderboard 0 2 WITHSCORES
# 獲取某個(gè)玩家的排名
ZREVRANK leaderboard "player1"
- 網(wǎng)站文章或視頻的熱門排行
網(wǎng)站可能希望展示其上點(diǎn)擊量最高的文章或視頻。每次有用戶點(diǎn)擊時(shí),相關(guān)內(nèi)容的計(jì)數(shù)就會(huì)增加,然后可以使用有序集合實(shí)時(shí)顯示熱門內(nèi)容。
# 文章被點(diǎn)擊:
ZINCRBY article_views 1 "article123"
#獲取最熱門的 3 篇文章
ZREVRANGE article_views 0 2 WITHSCORES
- 電商平臺(tái)的熱門產(chǎn)品排行
電商平臺(tái)可能希望展示最受歡迎的產(chǎn)品。每當(dāng)產(chǎn)品被購(gòu)買,其熱度都會(huì)增加。
#產(chǎn)品被購(gòu)買:
ZINCRBY product_sales 1 "product123"
#獲取最受歡迎的10個(gè)產(chǎn)品
ZREVRANGE product_sales 0 9 WITHSCORES
時(shí)間線事件記錄
場(chǎng)景描述:假設(shè)我們正在為一個(gè)社交網(wǎng)絡(luò)網(wǎng)站設(shè)計(jì)功能,用戶每次登錄、發(fā)帖或評(píng)論都會(huì)在其時(shí)間線上生成一個(gè)事件。我們希望可以跟蹤這些事件并能夠檢索特定時(shí)間范圍內(nèi)的事件。
#用戶在某個(gè)時(shí)間點(diǎn)登錄了網(wǎng)站
ZADD user123:timeline 1631498200 "Logged in"
#該用戶稍后發(fā)表了一篇文章
ZADD user123:timeline 1631498300 "Posted an article about Redis"
#查詢?cè)撚脩粼谥付ǖ臅r(shí)間范圍內(nèi)的所有事件
ZRANGEBYSCORE user123:timeline 1631498200 1631498400
延遲任務(wù)隊(duì)列
場(chǎng)景描述:用戶訂閱了一個(gè)在線服務(wù),比如說(shuō)一個(gè)音樂(lè)服務(wù),它提供了一個(gè)月的免費(fèi)試用。為了提醒用戶及時(shí)續(xù)費(fèi)或者保存他們的歌單數(shù)據(jù),服務(wù)提供商可以在試用期結(jié)束前的幾天,發(fā)送一個(gè)“您的試用即將結(jié)束,請(qǐng)及時(shí)續(xù)費(fèi)”的提醒。這個(gè)提醒任務(wù)就可以放入延遲隊(duì)列。
具體實(shí)現(xiàn)步驟:
# 用戶注冊(cè): 當(dāng)用戶開(kāi)始他們的免費(fèi)試用時(shí),你會(huì)將這個(gè)用戶和他們?cè)囉媒Y(jié)束時(shí)間的時(shí)間戳(減去3天,這樣在結(jié)束前3天提醒他們)加入到一個(gè)有序集合中。
ZADD trial_end_reminders (start_timestamp + trial_period - few_days) user_id # 一般時(shí)間戳是以秒為單位的,對(duì)于這個(gè)例子,trial_period = 24*86400,few_days = 3 * 86400。
#后臺(tái)任務(wù):后臺(tái)任務(wù)每天定期檢查這個(gè)有序集合,查看哪些用戶需要在今天被提醒。
ZRANGEBYSCORE trial_end_reminders (current_timestamp) (current_timestamp + 86400)
#發(fā)送提醒:對(duì)于上面命令返回的每一個(gè)用戶,你的系統(tǒng)會(huì)發(fā)送一個(gè)提醒郵件或者應(yīng)用內(nèi)通知,告訴他們?cè)囉眉磳⒔Y(jié)束,并提供一個(gè)續(xù)訂鏈接。
#清理:在發(fā)送提醒后,你需要從有序集合中移除這些用戶,確保他們不會(huì)被再次提醒。
ZREM trial_end_reminders user_id_1 user_id_2 ...
Bitmaps
基本概念:
Redis 的 Bitmaps(位圖)是一種特殊的數(shù)據(jù)結(jié)構(gòu),用于高效地處理大量的布爾值(true/false或者1/0)。在 Redis 中,Bitmaps 實(shí)際上并不是一種獨(dú)立的數(shù)據(jù)類型,而是字符串(String)類型的一種特殊操作方式。通過(guò) 位操作 命令,Redis 允許用戶在一個(gè)很大的字節(jié)數(shù)組中設(shè)置和獲取位(bit)的值。
簡(jiǎn)單圖解:
內(nèi)部實(shí)現(xiàn)
Bitmaps 實(shí)際上就是一個(gè) String,底層采用字節(jié)數(shù)組來(lái)存儲(chǔ)數(shù)據(jù)。
Bitmaps 提供了一系列的命令來(lái)操作和查詢二進(jìn)制位。
基本命令:
設(shè)置和獲取位值:
SETBIT key offset value : 設(shè)置或清除操作只需將對(duì)應(yīng)位 置 0 即可指定的位 # 清除操作只需將對(duì)應(yīng)位 置 0 即可, offset只能是非負(fù)整數(shù)。
GETBIT key offset : 獲取指定位的值
# 將位于索引7的位設(shè)置為1
> SETBIT mymap 7 1
(integer) 0
# 獲取位于索引7的位的值
> GETBIT mymap 7
(integer) 1
統(tǒng)計(jì)和查找:
BITCOUNT key :計(jì)算整個(gè)字符串中設(shè)置為1的位數(shù)
BITCOUNT key [start end] :計(jì)算在指定范圍內(nèi)設(shè)置為1的位數(shù)
BITPOS key bit [start] [end] : 找到第一個(gè)設(shè)置為1或0的位
其中,start和end是字符串的字節(jié)索引(不是bit索引)
> BITCOUNT mymap
(integer) 1
> BITCOUNT mymap 0 1 # 計(jì)算從字節(jié)0到字節(jié)1之間設(shè)置為1的位數(shù)
(integer) 1
> BITPOS mymap 1
(integer) 7
> BITPOS mymap 0
(integer) 0
其他運(yùn)算
BITOP operation destkey key [key ...] : 對(duì)一個(gè)或多個(gè)bitmaps進(jìn)行位運(yùn)算
operation 可能是 AND, OR, XOR, NOT 其中一種
AND:邏輯與 、OR:邏輯或、XOR:邏輯異或、NOT:邏輯非
> SETBIT yourmap 5 1
(integer) 0
> BITOP AND destMap mymap yourmap
(integer) 1 # 返回 1,表示 destMap 的長(zhǎng)度是 1 字節(jié)
應(yīng)用案列:
員工打卡簽到:
大家都知道,員工打卡簽到系統(tǒng)幾乎是每家公司的標(biāo)配功能。每天上班,員工都需要打卡來(lái)記錄他們的出勤情況。今天我們來(lái)探討一下,如何利用 Redis 中的 Bitmaps 來(lái)高效地實(shí)現(xiàn)這個(gè)功能。
為什么選擇 Bitmaps?
員工打卡簽到無(wú)非就是兩種結(jié)果:簽到和未簽到。這種二元狀態(tài)正好與Bitmaps的存儲(chǔ)方式相契合,每位員工的每天簽到狀態(tài)只需用一個(gè)位(0或1)來(lái)表示。
每天使用一個(gè)特定日期格式的 key(如 user:12345:attendance:2023-09-01)存儲(chǔ)Bitmaps。員工的 ID 直接作為位索引,如 ID 為 42 的員工簽到,則將sign_2023-09-01的第 42 位設(shè)為 1。
示例:
#員工打卡簽到
SETBIT user:42:attendance:2023-09-01 42 1 # 員工 ID 42 在2023-09-01簽到
SETBIT user:42:attendance:2023-09-01 1024 1 # 員工 ID 1024 在2023-09-01簽到
#檢查特定用戶是否在某天簽到
GETBIT user:42:attendance:2023-09-01 42 # 查看員工 ID 42 在2023-09-01是否簽到
GETBIT user:42:attendance:2023-09-01 500 # 查看員工 ID 500 在2023-09-01是否簽到
#計(jì)算某一天的簽到用戶數(shù)
BITCOUNT user:42:attendance:2023-09-01 # 查看2023-09-01的簽到用戶數(shù)
日活跟蹤:
記錄特定日期所有活躍用戶的信息。例如,我們可以使用一個(gè)特定日期的 bitmaps 來(lái)跟蹤該日期的所有活躍用戶。
# 用戶 ID 為 12345 和 67890 在 2023-09-01 都活躍了
SETBIT active_users:2023-09-01 12345 1
SETBIT active_users:2023-09-01 67890 1
# 檢查用戶 ID 為 12345 在 2023-09-01 是否活躍
GETBIT active_users:2023-09-01 12345
#檢查某一天所有的活躍用戶數(shù)
BITCOUNT user:42:attendance:2023-09-01
用戶登錄:
可以使用 bitmaps 來(lái)跟蹤用戶的登錄狀態(tài),例如,確定用戶是否已登錄。
# 用戶 ID 為 12345 登錄了
SETBIT user:12345:login_status 1
# 檢查用戶 ID 為 12345 是否登錄
GETBIT user:12345:login_status
這三個(gè)應(yīng)用的關(guān)注點(diǎn)區(qū)別:
打卡簽到: 主要關(guān)注個(gè)體用戶的連續(xù)行為,例如連續(xù)簽到多少天。
日活跟蹤: 關(guān)注整體的用戶行為,例如在某一天有多少用戶活躍了。
用戶登錄: 主要跟蹤用戶的即時(shí)狀態(tài),例如某用戶當(dāng)前是否在線或已登錄。
HyperLogLog
在大數(shù)據(jù)應(yīng)用中,我們經(jīng)常需要計(jì)算或估算某個(gè)集合中不重復(fù)元素的數(shù)量(基數(shù)),例如統(tǒng)計(jì)網(wǎng)站的獨(dú)立訪客(UV)。但當(dāng)數(shù)據(jù)規(guī)模非常大時(shí),傳統(tǒng)的統(tǒng)計(jì)方法會(huì)消耗大量的存儲(chǔ)和計(jì)算資源。這時(shí),Redis 的 HyperLogLog(簡(jiǎn)稱HLL)提供了一個(gè)很好的解決方案。
基本概念:
從廣義上說(shuō),HyperLogLog 是一個(gè)概率性的數(shù)據(jù)結(jié)構(gòu),用于估算集合的基數(shù)(即不重復(fù)元素的數(shù)量)。它不會(huì)提供完美準(zhǔn)確的計(jì)數(shù),但它使用的存儲(chǔ)空間非常?。ㄗ疃嗍褂?2KB的內(nèi)存)。
在 Redis 的語(yǔ)境中,我們可以將 HyperLogLog 視為一種特定的數(shù)據(jù)類型。
簡(jiǎn)單圖解:
步驟說(shuō)明:
- 輸入值通過(guò)哈希函數(shù)生成固定長(zhǎng)度的二進(jìn)制哈希值。
- 哈希值的前幾位決定了它應(yīng)該進(jìn)入數(shù)組的位置。
- 存儲(chǔ)值,將給定值的前導(dǎo)零的個(gè)數(shù)存儲(chǔ)在數(shù)組的對(duì)應(yīng)位置
- 數(shù)組中的值可能會(huì)根據(jù)新的哈希值進(jìn)行更新(新的哈希值的前導(dǎo)零個(gè)數(shù)大于數(shù)組值才會(huì)更新)
- 根據(jù)數(shù)組元素進(jìn)行基數(shù)估計(jì)
內(nèi)部實(shí)現(xiàn)
Redis 的 HyperLogLog 是一種特殊的數(shù)據(jù)類型,用來(lái)估計(jì)一個(gè)集合中有多少不同的元素。它不會(huì)給出完全準(zhǔn)確的答案,但它的估計(jì)接近真實(shí)值,并且使用的存儲(chǔ)空間非常小。它是基于一種名為 'HyperLogLog' 的聰明算法來(lái)工作的,這種算法使用概率學(xué)的魔法來(lái)做估計(jì),而不是真正地?cái)?shù)每一個(gè)元素。
基本命令:
PFADD key element [element ...] : 添加元素
PFCOUNT key [key ...] : 查詢基數(shù)估計(jì)值
PFMERGE destkey sourcekey [sourcekey ...] : 合并多個(gè) HyperLogLog 數(shù)據(jù)集
> PFADD myhll a b c d e f g
(integer) 1
> PFCOUNT myhll
(integer) 7
> PFADD yourhll a b c d h i j k
(integer) 1
> PFMERGE desthll myhll yourhll
OK
> PFCOUNT desthll
(integer) 11
應(yīng)用案例:
網(wǎng)站的 UV(獨(dú)立訪客)統(tǒng)計(jì)
當(dāng)你要統(tǒng)計(jì)一個(gè)網(wǎng)站的UV(獨(dú)立訪客)時(shí),你的目標(biāo)是確定有多少獨(dú)立的用戶訪問(wèn)了你的網(wǎng)站,而不是訪問(wèn)的總次數(shù)。傳統(tǒng)的方法(比如,基于關(guān)系型數(shù)據(jù)庫(kù)的計(jì)數(shù))可能會(huì)很耗資源,特別是當(dāng)訪客量非常大時(shí)。
而 Redis 的 HyperLogLog 提供了一個(gè)空間效率非常高的方式來(lái)進(jìn)行這樣的估算。每當(dāng)有用戶訪問(wèn)網(wǎng)站時(shí),你可以記錄其 IP 地址或者某個(gè)與用戶相關(guān)的唯一標(biāo)識(shí)符(sessionID),然后使用 PFADD 命令將其加入到 HyperLogLog 中。
具體實(shí)現(xiàn):
為每個(gè)獨(dú)立的訪客生成唯一標(biāo)識(shí):
現(xiàn)在大部分網(wǎng)站都會(huì)采用 cookie 技術(shù)。每當(dāng)用戶訪問(wèn)網(wǎng)站時(shí),服務(wù)器都會(huì)為這位新訪客生成一個(gè)唯一的 ID 并將其存儲(chǔ)在 cookie 中。這個(gè)唯一的 ID 用于在后續(xù)的訪問(wèn)中識(shí)別該用戶,從而跟蹤其在網(wǎng)站上的行為和偏好。
我們使用 user_cookie_12345 ,user_cookie_12346 等來(lái)標(biāo)識(shí)不同的cookie
添加 Cookie 到 HyperLogLog:
為了跟蹤一天的 UV,可以為每天創(chuàng)建一個(gè) HyperLogLog。當(dāng)用戶訪問(wèn)你的網(wǎng)站時(shí),使用PFADD命令將他們的唯一標(biāo)識(shí)添加到 HyperLogLog 中。
> PFADD uv_2023_09_01 user_cookie_12345
> PFADD uv_2023_09_01 user_cookie_12346
> PFADD uv_2023_09_01 user_cookie_12347
...
估算該日的UV:
使用 PFCOUNT 命令來(lái)獲取估算的UV值。
該命令會(huì)返回一個(gè)估算的獨(dú)立訪客數(shù)量。請(qǐng)注意,這是一個(gè)估算值,但其精度在大多數(shù)情況下是足夠的。
存儲(chǔ)歷史數(shù)據(jù):
如果你想跟蹤UV的歷史數(shù)據(jù),可以為每天保留一個(gè) HyperLogLog。例如:uv_2023_09_01、uv_2023_09_02等。
合并多日數(shù)據(jù):
如果你想要一個(gè)時(shí)間范圍內(nèi)的估算 UV(例如一個(gè)月),你可以使用 PFMERGE 命令合并多個(gè) HyperLogLog。
以上就是使用 Redis 的 HyperLogLog 進(jìn)行網(wǎng)站UV統(tǒng)計(jì)的基本方法。使用這種方法,你可以用非常少的空間(每個(gè) HyperLogLog 只需要12KB)來(lái)跟蹤大量的獨(dú)立訪客。
Geospatial
簡(jiǎn)介:
Redis支持一個(gè)稱為 Geospatial 的地理空間索引功能。這一功能在許多需要地理定位數(shù)據(jù)的應(yīng)用中都有著廣泛應(yīng)用,如找到某個(gè)位置附近的餐廳或商店等。
基本概念:
“Geo” 來(lái)源于希臘語(yǔ),意為“地球”,而 “spatial” 則表示“與空間有關(guān)”。結(jié)合起來(lái),Geospatial 主要關(guān)注地球上的空間位置或地域。在 Redis 中,這意味著我們可以使用坐標(biāo)系統(tǒng)(如經(jīng)緯度)來(lái)存儲(chǔ)和查詢地理位置的數(shù)據(jù)。
簡(jiǎn)單圖解:
說(shuō)明:圖示的(x,y)代表的是 經(jīng)緯度坐標(biāo)
內(nèi)部實(shí)現(xiàn):
地圖到數(shù)字:
想象一下,我們的地球是一個(gè)巨大的地圖。如果我們要在這張地圖上標(biāo)記一個(gè)位置,通常會(huì)使用經(jīng)緯度來(lái)描述它。但計(jì)算機(jī)更擅長(zhǎng)處理數(shù)字而不是這樣的坐標(biāo)。因此,Redis 使用了一種叫做 Geohash 的技巧,它可以把這些坐標(biāo)(比如經(jīng)緯度)轉(zhuǎn)換成一個(gè)數(shù)字。
把位置存進(jìn)列表:
Redis 有一種特殊的列表叫做 zset(有序集合)。在 Geospatial 中,位置的名字(比如"北京")作為元素,而轉(zhuǎn)換得到的數(shù)字(Geohash)作為這個(gè)元素的“分?jǐn)?shù)”。
查找附近的地方:
當(dāng)我們想知道某個(gè)位置附近的其他地方時(shí),Redis 會(huì)先找出這個(gè)位置的數(shù)字(Geohash),然后在 zset 中查找分?jǐn)?shù)接近的其他元素。這樣就可以快速地找出附近的位置。
注意:
使用 Geohash 的方法可能不是百分之百精確的,因?yàn)樗且环N近似的方法。但在實(shí)際應(yīng)用中,這種小小的誤差通常不會(huì)導(dǎo)致太大的問(wèn)題,而它讓存儲(chǔ)和查找變得非常迅速。
基本命令
添加操作:
向指定的鍵中添加地理空間位置(經(jīng)度、緯度、名稱)
GEOADD key longitude latitude member [longitude latitude member ...] : 向指定的鍵中添加地理空間位置(經(jīng)度、緯度、名稱)
> GEOADD china:city 114.085947 22.547 shenzhen 113.280637 23.125178 guangzhou
(integer) 2
> GEOADD china:city 121.472644 31.231706 shanghai 116.405285 39.904989 beijing
(integer) 2
> GEOADD china:city 108.948024 34.263161 xian 106.504962 29.533155 chongqing
(integer) 2
獲取操作:
GEOPOS key member [member ...] : 獲取一個(gè)或多個(gè)位置元素的經(jīng)緯度
GEODIST key member1 member2 [m|km|ft|mi] : 獲取兩個(gè)地點(diǎn)之間的距離 # [m|km|ft|mi]: 分別是:米,千米,英尺,英里
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] : 根據(jù)給定的經(jīng)緯度坐標(biāo),返回位于給定距離內(nèi)的位置。
#參數(shù)詳解
WITHCOORD : 返回查詢結(jié)果中地理位置的經(jīng)緯度坐標(biāo)
WITHDIST : 返回每個(gè)查詢結(jié)果地點(diǎn)到給定坐標(biāo)的距離
WITHHASH : 返回位置的 52 位整數(shù)表示的 geohash
ASC|DESC : 決定了查詢結(jié)果的排序方式,按距離升序排列、按距離降序排列
COUNT count : 限制查詢結(jié)果的數(shù)量。
> GEOADD china:city 108.948024 34.263161 xian 106.504962 29.533155 chongqing
(integer) 2
> GEOPOS china:city xian chongqing
1) 1) "108.94802302122116089"
2) "34.2631604414749944"
2) 1) "106.50495976209640503"
2) "29.53315530684997015"
> GEODIST china:city shenzhen xian km
"1396.1268"
> GEORADIUS china:city 118.767413 32.041544 500 km
1) "shanghai"
> GEORADIUS china:city 118.767413 32.041544 500 km WITHCOORD
1) 1) "shanghai"
2) 1) "121.47264629602432251"
2) "31.23170490709807012"
> GEORADIUS china:city 118.767413 32.041544 500 km WITHDIST
1) 1) "shanghai"
2) "271.5419"
我們來(lái)看一個(gè)混合使用參數(shù)的例子:
查詢距離某個(gè)位置 1000 公里內(nèi)的所有地方,并返回其名稱、距離、坐標(biāo),然后按距離升序排序,只顯示前 3 個(gè)結(jié)果。
> GEORADIUS china:city 118.767413 32.041544 1000 km WITHDIST WITHCOORD ASC COUNT 3
1) 1) "shanghai"
2) "271.5419"
3) 1) "121.47264629602432251"
2) "31.23170490709807012"
2) 1) "beijing"
2) "899.9931"
3) 1) "116.40528291463851929"
2) "39.9049884229125027"
3) 1) "xian"
2) "946.7395"
3) 1) "108.94802302122116089"
2) "34.2631604414749944"
應(yīng)用案例
位置數(shù)據(jù)存儲(chǔ)與查詢:
- 社交應(yīng)用:用戶可以查找附近的朋友或興趣點(diǎn)。例如,一個(gè)社交網(wǎng)絡(luò)應(yīng)用可以允許用戶查看附近的其他用戶或活動(dòng)。
- 出行與導(dǎo)航:用于存儲(chǔ)和查詢地理位置數(shù)據(jù),如共享單車或共享汽車的當(dāng)前位置,以及用戶附近的可用車輛。
Stream
基本概念
Redis Stream 是 Redis 5.0 中引入的新數(shù)據(jù)類型,設(shè)計(jì)用來(lái)存儲(chǔ)和查詢?nèi)罩緮?shù)據(jù)結(jié)構(gòu)。Stream 是 Redis 對(duì)“日志”數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn),這種結(jié)構(gòu)在各種場(chǎng)景中都很有用,如消息隊(duì)列和事件日志。
與簡(jiǎn)單的 List 不同,Stream 能夠更好地支持多用戶并發(fā)操作,同時(shí)還提供了復(fù)雜的消息確認(rèn)和消費(fèi)機(jī)制。
Stream 基本組件介紹
Stream:一個(gè)Stream是一個(gè)按時(shí)間順序排列的消息列表。每個(gè)消息都有一個(gè)唯一的ID和鍵值對(duì)組成的數(shù)據(jù)。
Consumer:這是一個(gè)從 Stream 讀取消息的客戶端。每個(gè) Consumer 都有一個(gè)唯一的名字。
Consumer Group:一個(gè) Consumer Group 包含一組 Consumer,它們共同讀取一個(gè) Stream。這樣做是為了并行處理消息。
簡(jiǎn)單圖解
內(nèi)部實(shí)現(xiàn):
ListPack : Redis 中的 Stream 的底層用的是一種名為 ListPack 的數(shù)據(jù)結(jié)構(gòu),它非常緊湊并且效率高。
唯一ID : 每個(gè)消息都有一個(gè)由時(shí)間戳和序列號(hào)組成的唯一ID,確保消息的全局唯一性。
基本命令:
基礎(chǔ)操作
添加操作:
XADD key *|ID field value [field value ...] :
# *|ID: 消息的 ID。使用 * 會(huì)自動(dòng)生成一個(gè)ID,或者你可以指定一個(gè)
> XADD mystream * name xiaokang age 25
"1695020949856-0"
> XADD mystream * hobby swim job programmer
"1695029970290-0"
查詢操作:
XRANGE key start end [COUNT count] : 查詢指定ID范圍內(nèi)的消息
XREVRANGE key end start [COUNT count] : 反向查詢指定ID范圍內(nèi)的消息
# 參數(shù)說(shuō)明:
key : 表示你要檢索的 Stream 的名字
[COUNT count] : 可選參數(shù),用于限制返回的消息數(shù)量
對(duì)于 XRANGE 命令,參數(shù) start 和 end 的含義:
start : 檢索的起始消息 ID
end : 檢索的結(jié)束消息 ID
對(duì)于 XRANGE 命令,參數(shù) start 和 end 的含義:
start : 反向檢索的起始消息 ID
end : 反向檢索的結(jié)束消息 ID
特殊符號(hào)解釋:
參數(shù) start 取 '-' , 表示最早的消息,取 + 表示最新的消息
# 檢索所有消息
> XRANGE mystream - +
1) 1) "1695020949856-0"
2) 1) "name"
2) "xiaokang"
3) "age"
4) "25"
2) 1) "1695029970290-0"
2) 1) "hobby"
2) "swim"
3) "job"
4) "programmer"
# 反向檢索所有消息
> XREVRANGE mystream + -
1) 1) "1695029970290-0"
2) 1) "hobby"
2) "swim"
3) "job"
4) "programmer"
2) 1) "1695020949856-0"
2) 1) "name"
2) "xiaokang"
3) "age"
4) "25"
獲取長(zhǎng)度:
XLEN key :獲取Stream中的消息數(shù)量
> XLEN mystream
(integer) 2
裁剪操作:
XTRIM key MAXLEN|MINID [=|~] threshold [LIMIT count] :裁減Stream 的長(zhǎng)度,控制其大小
# 參數(shù)說(shuō)明
MAXLEN|MINID : 裁減策略選擇。
MAXLEN :使 Stream 最多保持指定數(shù)量的消息。
MINID :刪除所有小于指定ID的消息。
[=|~]: 這是一個(gè)可選的修飾符,與上面的 MAXLEN 或 MINID 一起使用。
=:這意味著長(zhǎng)度或ID應(yīng)該精確匹配。對(duì)于 MAXLEN,它確保Stream的長(zhǎng)度恰好等于指定的長(zhǎng)度(刪除任何額外的消息);對(duì)于 MINID,它確保刪除的所有消息的ID值都小于等于給定值。
~:這意味著長(zhǎng)度或ID是一個(gè)近似值。這可能會(huì)導(dǎo)致更快的裁減操作,但Stream的實(shí)際長(zhǎng)度可會(huì)略微超過(guò)或低于指定的值。
[LIMIT count] : 這是一個(gè)可選的參數(shù),它限制了在一個(gè)操作中可以刪除的消息數(shù)
> XTRIM mystream MAXLEN = 1
(integer) 1
> XRANGE mystream - +
1) 1) "1695029970290-0"
2) 1) "hobby"
2) "swim"
3) "job"
4) "programmer"
刪除操作:
XDEL key ID [ID ...] : 從Stream中刪除指定的消息
> XDEL mystream "1695029970290-0"
(integer) 1
> XRANGE mystream - +
讀取消息操作:
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] : 用于從一個(gè)或多個(gè)流中讀取消息
參數(shù)說(shuō)明:
COUNT count :可選參數(shù),指定要從每個(gè)流中讀取的最大消息數(shù)。
BLOCK milliseconds :可選參數(shù),阻塞操作的時(shí)間(以毫秒為單位)。該命令會(huì)等待指定的時(shí)間,直到有新的消息可用。 在給定的時(shí)間內(nèi)沒(méi)有新消息,則命令會(huì)返回一個(gè)空響應(yīng)
STREAMS :指示后面要列出要從中讀取的流的名稱。這是一個(gè)固定的關(guān)鍵字。
key [key ...] :要從中讀取的流的名稱列表
ID [ID ...] :為每個(gè)指定的流提供一個(gè)消息 ID,從該 ID 之后(不包括該 ID)開(kāi)始讀取消息。
特殊 ID $ :表示只讀取新的消息,也就是那些在發(fā)出此 XREAD 命令之后添加到流中的消息。
消費(fèi)者和消費(fèi)者組操作
XGROUP:用于創(chuàng)建、修改或刪除消費(fèi)者組。
# 參數(shù)說(shuō)明:
CREATE : 創(chuàng)建一個(gè)新的消費(fèi)者組。
XGROUP CREATE key groupname ID|$ [MKSTREAM]
key : Stream 的名稱。
groupname : 消費(fèi)者組的名稱。
ID : 從哪個(gè)消息 ID 開(kāi)始消費(fèi)。如果選擇 $,則只會(huì)消費(fèi)新添加到 Stream 的消息。
MKSTREAM : 可選參數(shù)。如果 Stream 不存在,它會(huì)創(chuàng)建一個(gè)新的空 Stream。
SETID : 設(shè)置消費(fèi)者組的開(kāi)始消費(fèi)消息的 ID。
XGROUP SETID key groupname ID|$
key : Stream 的名稱。
groupname : 消費(fèi)者組的名稱。
ID : 從哪個(gè)消息 ID 開(kāi)始消費(fèi)。如果選擇 $,則只會(huì)消費(fèi)新添加到 Stream 的消息。
DESTROY : 刪除一個(gè)消費(fèi)者組。
XGROUP DESTROY key groupname
key : Stream 的名稱。
groupname : 消費(fèi)者組的名稱。
CREATECONSUMER : 在給定的消費(fèi)者組中顯式地創(chuàng)建一個(gè)消費(fèi)者。
XGROUP CREATECONSUMER key groupname consumername
key : Stream 的名稱。
groupname : 消費(fèi)者組的名稱。
consumername : 新創(chuàng)建的消費(fèi)者的名稱。
XREADGROUP:使用消費(fèi)者組從Stream中讀取消息。
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
# 參數(shù)說(shuō)明:
group : 指的是你想讀取消息的消費(fèi)者組的名稱。
consumer : 是該消費(fèi)者組內(nèi)的消費(fèi)者名稱。每次使用 XREADGROUP,都需要指定消費(fèi)者名稱。
COUNT count : 可選參數(shù),指定從每個(gè)流中讀取的最大消息數(shù)量。
NOACK : 消息在被讀取時(shí)不會(huì)被標(biāo)記為“未確認(rèn)”。因此,不需要(也不能)對(duì)它們進(jìn)行確認(rèn)。默認(rèn)情況下,當(dāng)消費(fèi)者讀取消息后,是需要對(duì)消息進(jìn)行確認(rèn)的。
ID [ID ...] : 為每個(gè)指定的流提供一個(gè)消息 ID,從該 ID 之后(不包括該 ID)開(kāi)始讀取消息。
特殊 ID > : 表示從上次讀取的位置繼續(xù)讀?。ㄖ辉谑褂?XREADGROUP 時(shí)有效)。
XACK : 用來(lái)確認(rèn)消費(fèi)者已成功處理的特定消息
XACK key group ID [ID ...]
# 參數(shù)解釋
key : 這是你要確認(rèn)消息的 Stream 的名字。
group : 這是消息所屬的消費(fèi)者組的名稱。
ID [ID ...] : 一個(gè)或多個(gè)你想確認(rèn)的消息的 ID。
XPENDING :用于查詢消費(fèi)者組中待處理(已發(fā)送但未確認(rèn))的消息
XPENDING key group [[IDLE min-idle-time] start end count [consumer]]
key : 你要查詢的 Stream 的名稱。
group: 你要查詢的消費(fèi)者組的名稱。
IDLE min-idle-time : 這是一個(gè)可選的參數(shù),它允許你只查詢那些已經(jīng)空閑或未確認(rèn)超過(guò)指定毫秒數(shù)的消息。
start : 起始消息 ID。
end : 結(jié)束消息 ID。
count : 你要返回的消息的最大數(shù)量。
consumer : 這是一個(gè)可選的參數(shù),它允許你只查詢特定消費(fèi)者的待處理消息。
XCLAIM : 允許一個(gè)消費(fèi)者重新認(rèn)領(lǐng)消費(fèi)者組中的掛起消息,通常用于處理由失效消費(fèi)者未完成的消息。
XCLAIM key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [force] [justid]
key : 指定的 Stream 名稱。
group : 你想要操作的消費(fèi)者組名稱。
consumer : 這是嘗試認(rèn)領(lǐng)消息的消費(fèi)者的名字。
min-idle-time : 以毫秒為單位的時(shí)間,僅當(dāng)消息的閑置時(shí)間超過(guò)此值時(shí),消費(fèi)者才能認(rèn)領(lǐng)該消息。
ID [ID ...] : 你想要認(rèn)領(lǐng)的消息的 ID 列表。
IDLE ms : 設(shè)置消息的新的閑置時(shí)間(自從最后一次被消費(fèi)以來(lái)的時(shí)間)。
TIME ms-unix-time : 修改消息的最后一次被讀取的時(shí)間為給定的 Unix 時(shí)間
RETRYCOUNT count : 設(shè)置消息的投遞計(jì)數(shù)(即這條消息已經(jīng)被送達(dá)的次數(shù))。
force : 這個(gè)選項(xiàng)允許你不考慮 min-idle-time 條件,直接強(qiáng)制認(rèn)領(lǐng)消息。
justid : 如果設(shè)置這個(gè)選項(xiàng),命令只返回消息 ID,不返回消息的內(nèi)容。
接下來(lái),我通過(guò)一個(gè)示例來(lái)演示如何使用Redis Stream中的命令進(jìn)行消息的添加、消費(fèi)和確認(rèn)。
場(chǎng)景:
以一個(gè)在線訂單系統(tǒng)為例,當(dāng)一個(gè)用戶下單時(shí),訂單詳細(xì)信息被添加到一個(gè)名為“orders”的stream中。有一個(gè)消費(fèi)者組叫做“order-processors”,里面有兩個(gè)消費(fèi)者:“processor1”和“processor2”。它們的任務(wù)是處理這些訂單,例如更新庫(kù)存、發(fā)送確認(rèn)郵件等。
1. 添加訂單
XADD orders * order_id 123 item_id A1 count 2 # 用戶A下了一個(gè)訂單,訂單編號(hào)為123,買了 2件
"1695103666038-0"
2.創(chuàng)建消費(fèi)者組
XGROUP CREATE orders order-processors $ MKSTREAM # 創(chuàng)建一個(gè)名為“order-processors”的消費(fèi)者組,從 stream 的開(kāi)始處監(jiān)聽(tīng)新的訂單。
OK
3.讀取和處理訂單
# processor1讀取了訂單123并開(kāi)始處理它
XREADGROUP GROUP order-processors processor1 COUNT 1 STREAMS orders >
# 同時(shí),另一個(gè)用戶B下了一個(gè)訂單,編號(hào)為124
XADD orders * order_id 124 item_id A2 quantity 1
# 然后,processor2 開(kāi)始讀取訂單:
XREADGROUP GROUP order-processors processor2 COUNT 1 STREAMS orders >
4.確認(rèn)訂單已處理
# 當(dāng)processor1成功處理訂單123時(shí),它會(huì)確認(rèn)處理完成:
XACK orders order-processors messageID1
# 注意:messageID1是processor1從Stream讀取到的訂單123的ID。
5.查看未處理的訂單
XPENDING orders order-processors
# 假設(shè)processor2由于某種原因暫時(shí)不能處理訂單124,這時(shí)我們會(huì)看到訂單124尚未被處理。
6.重新處理失敗的訂單
# 如果processor2出現(xiàn)故障,processor1可以認(rèn)領(lǐng)并處理訂單124:
XCLAIM orders order-processors processor1 3600000 messageID2
這個(gè)簡(jiǎn)單的在線訂購(gòu)系統(tǒng)例子展示了如何使用Redis Streams進(jìn)行實(shí)時(shí)訂單處理。這種模式可以確保即使某個(gè)消費(fèi)者失敗,訂單也能被其他消費(fèi)者接手并順利處理。
應(yīng)用案例:
消息隊(duì)列
消息隊(duì)列是一種應(yīng)用程序之間傳遞數(shù)據(jù)的方式。它允許應(yīng)用程序異步地發(fā)送和接收消息,這意味著發(fā)送消息的應(yīng)用程序和接收消息的應(yīng)用程序無(wú)需同時(shí)運(yùn)行。
以在線購(gòu)物系統(tǒng)的訂單處理為例進(jìn)行說(shuō)明:
考慮一個(gè)在線購(gòu)物系統(tǒng)。當(dāng)用戶下單時(shí),系統(tǒng)不應(yīng)該讓用戶等待直到所有的后端處理(例如庫(kù)存檢查、付款處理、通知倉(cāng)庫(kù)等)都完成。相反,一旦訂單提交,系統(tǒng)應(yīng)該立即給用戶一個(gè)響應(yīng),而后端的處理可以稍后進(jìn)行。
#1. 添加訂單到隊(duì)列:
#用戶下單后,我們將訂單數(shù)據(jù)添加到名為orders的Redis Stream中。
XADD orders * order_id 101 product_id P01 quantity 3
#2.處理訂單:
# 后臺(tái)有多個(gè)workers(進(jìn)程),它們不斷地監(jiān)聽(tīng)新的訂單,并進(jìn)行處理。這些worke 可以是分布在多臺(tái)機(jī)器上的多個(gè)進(jìn)程。
首先,我們創(chuàng)建一個(gè)消費(fèi)者組:
XGROUP CREATE orders order-processors $ MKSTREAM
接著,一個(gè)worker可以開(kāi)始讀取并處理訂單:
XREADGROUP GROUP order-processors worker1 COUNT 1 STREAMS orders >
# 這里,worker1是處理訂單的消費(fèi)者名稱。>意味著從最新的消息開(kāi)始讀取。
確認(rèn)訂單處理完成:
一旦worker1處理完訂單,它需要確認(rèn)該訂單已被處理:
XACK orders order-processors <messageID> # 其中,<messageID>是在步驟1中Redis生成的訂單ID。
通過(guò)此例子,我們展示了如何使用 Redis Stream 作為消息隊(duì)列,來(lái)異步處理在線購(gòu)物系統(tǒng)中的訂單。這種結(jié)構(gòu)確保了用戶下單后能夠迅速得到響應(yīng),同時(shí)訂單處理也能在后臺(tái)高效地進(jìn)行。
事件日志
事件日志就是記錄系統(tǒng)或應(yīng)用中發(fā)生的各種事件,如用戶操作、系統(tǒng)異常等。
事件日志的常見(jiàn)應(yīng)用是在電商平臺(tái)中記錄用戶的購(gòu)物行為。
假設(shè)你運(yùn)營(yíng)一個(gè)電商平臺(tái),每當(dāng)用戶瀏覽、搜索、點(diǎn)擊、購(gòu)買或者評(píng)論商品時(shí),都會(huì)產(chǎn)生一個(gè)事件。你可以使用Redis Streams來(lái)記錄這些事件。基于這些事件,可以做一些實(shí)時(shí)分析,比如:分析用戶的購(gòu)物模式和偏好,為他們提供更相關(guān)的商品推薦。
1.瀏覽商品:
事件:商品瀏覽
數(shù)據(jù):用戶ID,商品ID,瀏覽時(shí)間,來(lái)源頁(yè)面等。
XADD user-activity * event "商品瀏覽" userID "12345" productID "abcd" timestamp "1632067200" source "主頁(yè)"
2.搜索商品:
事件:商品搜索
數(shù)據(jù):用戶ID,搜索關(guān)鍵詞,搜索時(shí)間,搜索結(jié)果數(shù)等。
XADD user-activity * event "商品搜索" userID "12345" keyword "運(yùn)動(dòng)鞋" timestamp "1632067250" results "50"
3.點(diǎn)擊商品:
事件:商品點(diǎn)擊
數(shù)據(jù):用戶ID,商品ID,點(diǎn)擊時(shí)間。
XADD user-activity * event "商品點(diǎn)擊" userID "12345" productID "abcd" timestamp "1632067300"
4.購(gòu)買商品:
事件:商品購(gòu)買
數(shù)據(jù):用戶ID,商品ID,購(gòu)買數(shù)量,總價(jià),購(gòu)買時(shí)間。
XADD user-activity * event "商品購(gòu)買" userID "12345" productID "abcd" quantity "2" total "200" timestamp "1632067400"
5.商品評(píng)價(jià):
事件:商品評(píng)價(jià)
數(shù)據(jù):用戶ID,商品ID,評(píng)分,評(píng)論內(nèi)容,評(píng)價(jià)時(shí)間。
XADD user-activity * event "商品評(píng)價(jià)" userID "12345" productID "abcd" rating "5" review "非常滿意" timestamp "1632067500"
總結(jié):
在上文,我們深入探討了 Redis 的九種對(duì)象類型,包括字符串(String)、列表(List)、哈希(Hash)、集合(Set)、有序集合(Sorted Set)、Bitmaps、HyperLogLog、Geospatial 和 Stream。
這里簡(jiǎn)單總結(jié)下各種數(shù)據(jù)結(jié)構(gòu)的使用場(chǎng)景:
- 字符串對(duì)象是最簡(jiǎn)單的數(shù)據(jù)類型,適用于緩存、臨時(shí)存儲(chǔ)等場(chǎng)景。
- 列表對(duì)象提供了隊(duì)列的實(shí)現(xiàn),非常適合消息隊(duì)列和棧的應(yīng)用。
- 哈希對(duì)象是存儲(chǔ)對(duì)象屬性的理想選擇,適用于存儲(chǔ)和訪問(wèn)對(duì)象。
- 集合對(duì)象和有序集合對(duì)象適用于存儲(chǔ)不重復(fù)元素,其中有序集合還可以進(jìn)行排名和范圍查詢。
- Bitmaps和 HyperLogLog 提供了高效的計(jì)數(shù)和統(tǒng)計(jì)功能。
- Geospatial 允許進(jìn)行地理位置的存儲(chǔ)和查詢。
- Stream 為構(gòu)建復(fù)雜的消息傳遞提供了基礎(chǔ)。
Redis 作為一個(gè)高性能的鍵值數(shù)據(jù)庫(kù),已經(jīng)成為現(xiàn)代應(yīng)用開(kāi)發(fā)不可或缺的組成部分。通過(guò)深入了解 Redis 的各種對(duì)象及其編碼方式,我們不僅可以更加高效地利用其提供的功能,還能針對(duì)不同的應(yīng)用場(chǎng)景選擇最適合的對(duì)象類型,從而優(yōu)化我們的應(yīng)用性能和資源使用。
本篇文章旨在為大家提供一個(gè)關(guān)于 Redis 各個(gè)對(duì)象的全面指南,從基本概念到內(nèi)部實(shí)現(xiàn),再到實(shí)際應(yīng)用案例。不管您是剛開(kāi)始接觸 Redis 還是已經(jīng)有很多經(jīng)驗(yàn),希望本文都能為您帶來(lái)新的啟示。
最后
如果你對(duì) Linux 編程,Redis 等后端技術(shù)感興趣或者想學(xué)習(xí)計(jì)算機(jī)基礎(chǔ)相關(guān)的知識(shí),不妨關(guān)注我的公眾號(hào)「跟著小康學(xué)編程」。這里不僅有豐富的學(xué)習(xí)資源,還有持續(xù)更新的簡(jiǎn)單易懂的技術(shù)文章。
大家可以關(guān)注我 ,具體可訪問(wèn) :關(guān)注小康微信公眾號(hào)
另外大家在閱讀這篇文章的時(shí)候,如果覺(jué)得有問(wèn)題的或者有不理解的知識(shí)點(diǎn),歡迎大家評(píng)論區(qū)詢問(wèn)。我看到就會(huì)回復(fù)大家的。大家也可以加我的微信:jk-fwdkf , 備注「加群」。有任何不理解的都可以咨詢我。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-838114.html
大家如果覺(jué)得我寫的還不錯(cuò),也希望大家能夠幫忙點(diǎn)個(gè)贊,感謝大家的關(guān)注!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-838114.html
到了這里,關(guān)于全面解析 Redis 持久化:RDB、AOF與混合持久化的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!