Redis基础——数据类型详解
命令參考:http://doc.redisfans.com/
簡介
Redis 是完全開源的,遵守 BSD 協議,是一個高性能的 key-value 數據庫。
Redis 與其他 key - value 緩存產品有以下三個特點:
- Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,重啟的時候可以再次加載進行使用。
- Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
- Redis支持數據的備份,即master-slave模式的數據備份。
優勢
- 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數據類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數據類型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功執行要么失敗完全不執行。單個操作是原子性的。多個操作也支持事務,即原子性,通過MULTI和EXEC指令包起來。
- 豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性。
Redis與其他key-value存儲有什么不同?
- Redis有著更為復雜的數據結構并且提供對他們的原子性操作,這是一個不同于其他數據庫的進化路徑。Redis的數據類型都是基于基本數據結構的同時對程序員透明,無需進行額外的抽象。
- Redis運行在內存中但是可以持久化到磁盤,所以在對不同數據集進行高速讀寫時需要權衡內存,因為數據量不能大于硬件內存。在內存數據庫方面的另一個優點是,相比在磁盤上相同的復雜的數據結構,在內存中操作起來非常簡單,這樣Redis可以做很多內部復雜性很強的事情。同時,在磁盤格式方面他們是緊湊的以追加的方式產生的,因為他們并不需要進行隨機訪問。
數據類型
如圖所示,Redis中提供了9種不同的數據操作類型,他們分別代表了不同的數據存儲結構。
String
String類型是Redis用的較多的一個基本類型,也是最簡單的一種類型,一個key對應一個value;它和我們在Java中使用的字符類型什么太大區別。
string 類型是二進制安全的。意思是 redis 的 string 可以包含任何數據。比如jpg圖片或者序列化的對象。
string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。
他的結構如圖所示:
實例
我們可以通過 set 方法創建一個key為name,value為chen的鍵值對;然后通過get方法獲取name的值。
常用指令
存儲結構
學過 C++ 的應該知道,C++ 中是沒有 String 類型,但是 Redis 又是基于 C++ 來實現的,那么它是如何存儲 String 類型的呢?
Redis 并沒有采用 C 語言的傳統字符串表示方式(char* 或者 char[]),在 Redis 內部,String 類型以 int/SDS(simple dynamic string) 作為結構存儲,int 用來存放整型數據,sds 存放字節 / 字符串和浮點型數據。
在 C 的標準字符串結構下進行了封裝,用來提升基本操作的性能,同時充分利用以后的 C 的標準庫,簡化實現。我們可以在 redis 的源碼中【sds.h】中看到 sds 的結構如下;
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len;//表示當前sds的長度(單位是字節)uint8_t alloc; //表示已為sds分配的內存大小(單位是字節)unsigned char flags; //用一個字節表示當前sdshdr的類型,因為有sdshdr有五種類型,所以至少需要3位來表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到所以都為0。char buf[];//sds實際存放的位置 };也就是說實際上 sds 類型就是 char* 類型,那 sds 和 char* 有什么區別呢?
主要區別就是:sds 一定有一個所屬的結構 (sdshdr),這個 header 結構在每次創建 sds 時被創建,用來存儲 sds 以及 sds 的相關信息
對 sds 結構有一個簡單認識以后,我們如果通過 set 創建一個字符串,那么也就是會創建一個 sds 來存儲這個字符串信息,那么這個過程是怎么樣的呢?
- 首先第一個要判斷選擇一個什么類型的 sdshdr 來存放信息?這就得根據要存儲的 sds 的長度決定了,redis 在創建一個 sds 之前會調用【sds.c 文件】sdsReqType (size_t string_size) 來判斷用哪個 sdshdr。該函數傳遞一個 sds 的長度作為參數,返回應該選用的 sdshdr 類型。
- 然后把數據保存到對應的 sdshdr 中。
Redis 采用類似 C 的做法存儲字符串,也就是以’\0’結尾,’\0’只作為字符串的定界符,不計入 alloc 或者 len
命名規范
- redis 并沒有規定我們對 key 應該怎么命名,但是最好的實踐是 “對象類型:對象 id: 對象屬性: 子屬性”
- key 不要設置得太長,太長的 key 不僅僅消耗內存,而且在數據中查找這類鍵值計算成本很高
- key 不要設置得太短,比如 u:1000:pwd 來代替 user:1000:password, 雖然沒什么問題,但是后者的可讀性更好
- 為了更好的管理你的 key,對 key 進行業務上的分類;同時建議有一個 wiki 統一管理所有的 key,通過查詢這個文檔知道 redis 中的 key 的作用
應用場景
String 類型使用比較多,一般來說,不太了解 Redis 的人,幾乎所有場景都是用 String 類型來存儲數據。
分布式緩存
首先最基本的就是用來做業務數據的緩存,Redis 中會緩存一些常用的熱點數據,可以提升數據查詢的性能。
分布式全局ID
使用 String 類型的 incr 命令,實現原子遞增。
分布式 session
基于登錄場景中,保存 token 信息。
限流
計數器限流
List
列表類型 (list) 可以存儲一個有序且可重復的字符串列表,常用的操作是向列表兩端添加元素或者獲得列表的某一個片段,List 的存儲結構如下圖所示:
列表最多可存儲 232 - 1 元素 (4294967295, 每個列表可存儲40多億)。
實例
該操作就是簡單的通過LPUSH方法從隊列的左邊入隊元素,然后通過LRANGE方法遍歷指定區間內的元素。
常用命令
存儲結構
在 redis6.0 中,List 采用了 QuickList 這樣一種結構來存儲數據,QuickList 是一個雙向鏈表,鏈表的每個節點保存一個 ziplist,所有的數據實際上是存儲在 ziplist 中,ziplist 是一個壓縮列表,它可以節省內存空間。
ziplist 詳細說明:https://www.cnblogs.com/hunternet/p/11306690.html
聽到 “壓縮” 兩個字,直觀的反應就是節省內存。之所以說這種存儲結構節省內存,是相較于數組的存儲思路而言的。我們知道,數組要求每個元素的大小相同,如果我們要存儲不同長度的字符串,那我們就需要用最大長度的字符串大小作為元素的大小 (假設是 5 個字節)。存儲小于 5 個字節長度的字符串的時候,便會浪費部分存儲空間,比如下面這個圖所示。
所以,ziplist 就是根據每個節點的長度來決定占用內存大小,然后每個元素保存時同步記錄當前數據的長度,這樣每次添加元素是就可以計算下一個節點在內存中的存儲位置,從而形成一個壓縮列表。
另外,這種方式存儲數據有一個很好的優勢,就是它存儲的是在一個連續的內存空間,它可以很好的利用 CPU 的緩存來訪問數據,從而提升訪問性能。
其中,QuickList 中的每個節點稱為 QuickListNode,具體的定義在 quicklist.h 文件中。
typedef struct quicklistNode {struct quicklistNode *prev; //鏈表的上一個node節點struct quicklistNode *next; //鏈表的下一個node節點unsigned char *zl; //數據指針,如果當前節點數據沒有壓縮,它指向一個ziplist,否則,指向一個quicklistLZFunsigned int sz; /* 指向的ziplist的總大小 */unsigned int count : 16; /* ziplist中的元素個數 */unsigned int encoding : 2; /* 表示ziplist是否壓縮了,1表示沒壓縮,2表示壓縮 */unsigned int container : 2; /* 預留字段 */unsigned int recompress : 1; /* 當使用類似lindex命令查看某一個本壓縮的數據時,需要先解壓,這個用來存儲標記,等有機會再把數據重新壓縮 */unsigned int attempted_compress : 1; /* node can't compress; too small */unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;quickList 是 list 類型的存儲結構,其定義如下。
typedef struct quicklist {quicklistNode *head; //指向quicklistNode頭節點quicklistNode *tail; //指向quicklistNode的尾節點unsigned long count; /* 所有ziplist數據項的個數綜合 */unsigned long len; /* quicklist節點個數*/int fill : QL_FILL_BITS; /* ziplist大小設置 */unsigned int compress : QL_COMP_BITS; /* 節點壓縮深度設置 */unsigned int bookmark_count: QL_BM_BITS;quicklistBookmark bookmarks[]; } quicklist;當向 list 中添加元素時,會直接保存到某個 QuickListNode 中的 ziplist 中,不過不管是從頭部插入數據,還是從尾部插入數據,都包含兩種情況:
- 如果頭節點(尾部節點)上的 ziplist 大小沒有超過限制,新數據會直接插入到 ziplist 中
- 如果頭節點上的 ziplist 達到閾值,則創建一個新的 quicklistNode 節點,該節點中會創建一個 ziplist,然后把這個新創建的節點插入到 quicklist 雙向鏈表中。
應用場景
消息隊列
list類型可以使用 rpush 實現先進先出的功能,同時又可以使用 lpop 輕松的彈出(查詢并刪除)第一個元素,所以list類型可以用來實現消息隊列。
發紅包場景
在發紅包的場景中,假設發一個 10 元,10 個紅包,需要保證搶紅包的人不會多搶到,也不會少搶到,這種情況下,我們可以按照如下步驟進行操作。
Hash
Redis hash 是一個鍵值(key=>value)對集合。
Redis hash 是一個 string 類型的 field 和 value 的映射表,但是 value 是一個鍵值對(key-value),類比于 Java 里面的 Map<String,Map<String,Object>> 集合。
所以這種特性使得hash 特別適合用于存儲對象。
實例
常用命令
存儲結構
哈希類型的內部編碼有兩種:ziplist 壓縮列表 , hashtable 哈希表。只有當存儲的數據量比較小的情況下,Redis 才使用壓縮列表來實現字典類型。具體需要滿足兩個條件:
- 當哈希類型元素個數小于 hash-max-ziplist-entries 配置(默認 512 個)
- 所有值都小于 hash-max-ziplist-value 配置(默認 64 字節)
ziplist 使用更加緊湊的結構實現多個元素的連續存儲,所以在節省內存方面比 hashtable 更加優秀。當哈希類型無法滿足 ziplist 的條件時,Redis 會使用 hashtable 作為哈希的內部實現,因為此時 ziplist 的讀寫效率會下降,而 hashtable 的讀寫時間復雜度為 O(1)。
應用場景
Hash 表使用用來存儲對象數據,比如用戶信息,相對于通過將對象轉化為 json 存儲到 String 類型中,Hash 結構的靈活性更大,它可以任何添加和刪除對象中的某些字段。
購物車功能
- 以用戶 ID 作為 key
- 以商品 id 作為 field
- 以商品的數量作為 value
對象類型數據
比如優化之后的用戶信息存儲,減少數據庫的關聯查詢導致的性能慢的問題。
- 用戶信息
- 商品信息
- 計數器
Set
集合類型 (Set) 是一個無序并唯一的鍵值集合。它的存儲順序不會按照插入的先后順序進行存儲。
集合是通過哈希表實現的,所以添加,刪除,查找的復雜度都是 O(1)。
集合類型和列表類型的區別如下:
- 列表可以存儲重復元素,集合只能存儲非重復元素;
- 列表是按照元素的先后順序存儲元素的,而集合則是無序方式存儲元素的。
實例
常用命令
| SADD key member [member …] | 添加一個或者多個元素到集合 (set) 里 | O(N) |
| SCARD key | 獲取集合里面的元素數量 | O(1) |
| SDIFF key [key …] | 獲得隊列不存在的元素 | O(N) |
| SDIFFSTORE destination key [key …]] | 獲得隊列不存在的元素,并存儲在一個關鍵的結果集 | O(N) |
| SINTER key [key …] | 獲得兩個集合的交集 | O(N*M) |
| SINTERSTORE destination key [key …] | 獲得兩個集合的交集,并存儲在一個關鍵的結果集 | O(N*M) |
| SISMEMBER key member | 確定一個給定的值是一個集合的成員 | O(1) |
| SMEMBERS key | 獲取集合里面的所有元素 | O(N) |
| SMOVE source destination member | 移動集合里面的一個元素到另一個集合 | O(1) |
| SPOP key [count] | 刪除并獲取一個集合里面的元素 | O(1) |
| SRANDMEMBER key [count] | 從集合里面隨機獲取一個元素 | |
| SREM key member [member …]] | 從集合里刪除一個或多個元素 | O(N) |
| SUNION key [key …]] | 添加多個 set 元素 | O(N) |
| SUNIONSTORE destination key [key …] | 合并 set 元素,并將結果存入新的 set 里面 | O(N) |
存儲結構
Set 在的底層數據結構以 intset 或者 hashtable 來存儲。當 set 中只包含整數型的元素時,采用 intset 來存儲,否則,采用 hashtable 存儲,但是對于 set 來說,該 hashtable 的 value 值用于為 NULL,通過 key 來存儲元素。
typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[]; } intset;intset 將整數元素按順序存儲在數組里,并通過二分法降低查找元素的時間復雜度。數據量大時,依賴于 “查找” 的命令(如 SISMEMBER)就會由于 O (logn) 的時間復雜度而遇到一定的瓶頸,所以數據量大時會用 dict 來代替 intset。
但是 intset 的優勢就在于比 dict 更省內存,而且數據量小的時候 O (logn) 未必會慢于 O (1) 的 hash function,這也是 intset 存在的原因。
應用場景
標簽
-
首先給用戶添加相關標簽
-
使用 sinter 命令,可以來計算用戶共同感興趣的標簽
這種標簽系統在電商系統、社交系統、視頻網站,圖書網站,旅游網站等都有著廣泛的應用。例如一個用戶可能對娛樂、體育比較感興趣,另一個用戶可能對歷史、新聞比較感興趣,
這些興趣點就是標簽。有了這些數據就可以得到喜歡同一個標簽的人,以及用戶的共同喜好的標簽,這些數據對于用戶體驗以及增強用戶黏度比較重要。
例如一個社交系統可以根據用戶的標簽進行好友的推薦,已經用戶感興趣的新聞的推薦等,一個電子商務的網站會對不同標簽的用戶做不同類型的推薦,比如對數碼產品比較感興趣的人,在各個頁面或者通過郵件的形式給他們推薦最新的數碼產品,通常會為網站帶來更多的利益。
商品推薦
當用戶查看某個商品時,可以推薦和這個商品標簽有關的商品信息。
ZSet
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重復的成員。
不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
zset的成員是唯一的,但分數(score)卻可以重復。
實例
常用命令
數據結構
ZSet 的底層數據結構采用了 zipList(壓縮表)和 skiplist(跳躍表)組成,當同時滿足以下兩個條件時,有序集合采用的是 ziplist 存儲。
- 有序集合保存的元素個數要小于 128 個
- 有序集合保存的所有元素成員的長度必須小于 64 個字節
如果不能滿足以上任意一個條件,有序集合會采用 skiplist(跳躍表)結構進行存儲,如下圖所示,zSet 不只是用 skiplist,實際上,它使用了 dict(字典表)和 zskiplist(跳躍表)同時進行數據存儲。
- dict,字典類型, 其中 key 表示 zset 的成員數據,value 表示 zset 的分值,用來支持 O (1) 復雜度的按照成員取分值的操作
- zskiplist,跳躍表,按分值排序成員,用來支持平均復雜度為 O~~(logn)~~ 的按照分值定位成員的操作,以及范圍查找操作。
其中 zskiplistNode 中 *obj 和 Dic 中 *key 指向同一個具體元素,所以不會存在多余的內存消耗問題。另外,backward 表示后退指針,方便進行回溯。
關于跳躍表
跳表 (skip list) 對標的是平衡樹 (AVL Tree),是一種 插入 / 刪除 / 搜索 都是 O(log n) 的數據結構。它最大的優勢是原理簡單、容易實現、方便擴展、效率更高。因此在一些熱門的項目里用來替代平衡樹,如 redis, leveldb 等。
基本思想
首先,跳表處理的是有序的鏈表(一般是雙向鏈表,下圖未表示雙向),如下:
這個鏈表中,如果要搜索一個數,需要從頭到尾比較每個元素是否匹配,直到找到匹配的數為止,即時間復雜度是 O (n) O (n)。同理,插入一個數并保持鏈表有序,需要先找到合適的插入位置,再執行插入,總計也是 O (n) O (n) 的時間。
那么如何提高搜索的速度呢?很簡單,做個索引:
如上圖,我們新創建一個鏈表,它包含的元素為前一個鏈表的偶數個元素。這樣在搜索一個元素時,我們先在上層鏈表進行搜索,當元素未找到時再到下層鏈表中搜索。例如搜索數字 19 時的路徑如下圖:
先在上層中搜索,到達節點 17 時發現下一個節點為 21,已經大于 19,于是轉到下一層搜索,找到的目標數字 19。
我們知道上層的節點數目為 n/2n/2,因此,有了這層索引,我們搜索的時間復雜度降為了:O (n/2) O (n/2)。同理,我們可以不斷地增加層數,來減少搜索的時間:
在上面的 4 層鏈表中搜索 25,在最上層搜索時就可以直接跳過 21 之前的所有節點,因此十分高效。
更一般地,如果有 kk 層,我們需要的搜索次數會小于 ?n2k?+k?n2k?+k ,這樣當層數 kk 增加到 ?log2n??log2?n? 時,搜索的時間復雜度就變成了 lognlog?n。其實這背后的原理和二叉搜索樹或二分查找很類似,通過索引來跳過大量的節點,從而提高搜索效率。
動態跳表
上節的結構是 “靜態” 的,即我們先擁有了一個鏈表,再在之上建了多層的索引。但是在實際使用中,我們的鏈表是通過多次插入 / 刪除形成的,換句話說是 “動態” 的。上節的結構要求上層相鄰節點與對應下層節點間的個數比是 1:2,隨意插入 / 刪除一個節點,這個要求就被被破壞了。
因此跳表(skip list)表示,我們就不強制要求 1:2 了,一個節點要不要被索引,建幾層的索引,都在節點插入時由拋硬幣決定。當然,雖然索引的節點、索引的層數是隨機的,為了保證搜索的效率,要大致保證每層的節點數目與上節的結構相當。下面是一個隨機生成的跳表:
可以看到它每層的節點數還和上節的結構差不多,但是上下層的節點的對應關系已經完全被打破了。
現在假設節點 17 是最后插入的,在插入之前,我們需要搜索得到插入的位置:
接著,拋硬幣決定要建立幾層的索引,偽代碼如下:
randomLevel()lvl := 1-- random() that returns a random value in [0...1)while random() < p and lvl < MaxLevel dolvl := lvl + 1return lvl上面的偽代碼相當于拋硬幣,如果是正面(random() < p)則層數加一,直到拋出反面為止。其中的 MaxLevel 是防止如果運氣太好,層數就會太高,而太高的層數往往并不會提供額外的性能,
一般 MaxLevel=log1/pnMaxLevel=log1/p?n。現在假設 randomLevel 返回的結果是 2,那么就得到下面的結果。
如果要刪除節點,則把節點和對應的所有索引節點全部刪除即可。當然,要刪除節點時需要先搜索得到該節點,搜索過程中可以把路徑記錄下來,這樣刪除索引層節點的時候就不需要多次搜索了。
使用場景
排行榜系統
有序集合比較典型的使用場景就是排行榜系統。例如學生成績的排名。某視頻 (博客等) 網站的用戶點贊、播放排名、電商系統中商品的銷量排名等。我們以博客點贊為例。
-
添加用戶贊數
例如小編 Tom 發表了一篇博文,并且獲得了 10 個贊。
zadd user:ranking 10 article1 -
取消用戶贊數
這個時候有一個讀者又覺得 Tom 寫的不好,又取消了贊,此時需要將文章的贊數從榜單中減去 1,可以使用 zincrby。
zincrby user:ranking -1 article1 -
查看某篇文章的贊數
ZSCORE user:ranking arcticle1 -
展示獲取贊數最多的十篇文章
此功能使用 zrevrange 命令實現:
zrevrange user:ranking 0 10 #0 到 10表示元素個數索引 zrevrangebyscore user:ranking 99 0 # 按照分數從高到低排名,99,0表示score
熱點話題排行
比如微博的熱搜,就可以使用 ZSet 來實現。
其他數據類型介紹
在 Redis 中,還有一些使用得非常少的數據類型。
Geospatial
Geo 是 Redis3.2 推出的一個類型,它提供了地理位置的計算功能,也就是可以計算出兩個地理位置的距離。
文檔:https://www.redis.net.cn/order/3687.html
下面演示一下 Geo 的基本使用,其中需要用到經緯度信息,可以從 http://www.jsons.cn/lngcode/ 查詢。
-
添加模擬數據
geoadd china:city 116.40 39.90 beijing geoadd china:city 121.47 31.23 shanghai geoadd china:city 114.05 22.52 shengzhen geoadd china:city 113.28 23.12 guangzhou -
獲取當前位置的坐標值
geopos china:city beijing geopos china:city shanghai -
獲取兩個位置之間的距離:m-表示米/km-表示千米/mi-表示英里/ft表示英尺
# 查看北京到上海的直線距離 geodist china:city beijing shanghai km # 查看北京到深圳的直線距離 geodist china:city beijing shenzhen km -
給定一個經緯度,找出該經緯度某一半徑內的元素
# 以110 30這個點為中心,尋找方圓1000km的城市 georadius china:city 110 30 1000 km -
找出指定位置周圍的其他元素
georadiusbymember china:city shanghai 1000 km
比如現在比較火的直播業務,我們需要檢索附近的主播,那么 GEO 就可以很好的實現這個功能。
- 首先主播開播的時候寫入主播 Id 的經緯度,
- 然后主播關播的時候刪除主播 Id 元素,這樣就維護了一個具有位置信息的在線主播集合提供給線上檢索。
HyperLogLog
HyperLogLog 是 Redis2.8.9 提供的一種數據結構,他提供了一種基數統計方法。什么是基數統計呢?簡單來說就是一個集合中不重復元素的個數,比如有一個集合 {1,2,3,1,2},那么它的基數就是 3。
HyperLogLog 提供了三種指令。
- pfadd ,Redis Pfadd 命令將所有元素參數添加到 HyperLogLog 數據結構中。
- pfcount,Redis Pfcount 命令返回給定 HyperLogLog 的基數估算值。
- pgmerge,Redis Pgmerge 命令將多個 HyperLogLog 合并為一個 HyperLogLog ,合并后的 HyperLogLog 的基數估算值是通過對所有 給定 HyperLogLog 進行并集計算得出的。
使用方法如下。
pfadd uv a b c a c d e f # 創建一組元素 pfcount uv # 統計基數這個功能,我用 String 類型、或者 Set 類型都可以實現,為什么要用 HyperLogLog 呢?
最大的特性就是: HyperLogLog 在數據量非常大的情況下,占用的存儲空間非常小,每個 HyperLogLog 鍵只需要花費 12 KB 內存,就可以計算接近 2^64(2 的 64 次方) 個不同元素的基數,這個是一個非常龐大的數字,為什么能夠用這么小的空間來存儲這么大的數據呢?
不知道大家是否注意到,HyperLogLog 并沒有提供數據查詢的命令,只提供了數據添加和數據統計。這是因為 HyperLogLog 并沒有存儲每個元素的值,它使用的是概率算法,通過存儲元素的 hash 值的第一個 1 的位置,來計算元素數量,這塊在這里就不做過多展開。
應用場景
-
HyperLogLog 更適合做一些統計類的工作,比如統計一個網站的 UV。
-
計算日活、7 日活、月活數據.
如果我們通過解析日志,把 ip 信息(或用戶 id)放到集合中,例如:HashSet。如果數量不多則還好,但是假如每天訪問的用戶有幾百萬。無疑會占用大量的存儲空間。且計算月活時,還需要將一個整月的數據放到一個 Set 中,這隨時可能導致我們的程序 OOM。
有了 HyperLogLog,這件事就變得很簡單了。因為存儲日活數據所需要的內存只有 12K,例如。
# 使用日來存儲每天的ip地址 pfadd ip_20190301 192.168.8.1 pfadd ip_20190302 xxx pfadd ip_20190303 xxx ... pfadd ip_20190331 xxx計算某一天的日活,只需要執行 PFCOUNT ip_201903XX 就可以了。每個月的第一天,執行 PFMERGE 將上一個月的所有數據合并成一個 HyperLogLog,例如:ip_201903。再去執行 PFCOUNT ip_201903,就得到了 3 月的月活。
Bit
Bit,其實是 String 類型中提供的一個功能,他可以設置 key 對應存儲的值指定偏移量上的 bit 位的值,可能大家理解起來比較抽象,舉個例子:
-
使用 string 類型保存一個 key
set key m -
通過 getbit 命令獲取 key 的 bit 位的值
getbit key 0 getbit key 1 getbit key 2 getbit key 3 getbit key 4 getbit key 5 getbit key 6 getbit key 7 getbit key 8打印上面的所有輸出,會發現得到一個 0 1 1 0 1 1 0 1 的二進制數據,這個二進制拼接得到的結果。 m 的 ascII 碼對應的是 109, 109 的二進制正好是 0 1 1 0 1 1 0 1。
所以從這里可以看出來,bit 其實就是針對一個 String 類型的 value 值的 bit 位進行操作。
-
對 key 進行修改,修改第 6 位的值變成 1, 第 7 位的值編程 0.
setbit key 6 1 setbit key 7 0在此使用get key 命令,會發現得到的結果是 n。
因為 n 的二進制是 1101110,(十進制是 110)。把上面的指定位修改之后,自然就得到了這樣的結果。
bit 操作在實際應用中,可以怎么使用呢?
比如學習打卡功能就可以使用 setbit 操作,比如記錄一周的打卡記錄。
# 設置用戶id 1001的打卡記錄 set sign:1001 0 1 # 已打卡 set sign:1001 1 0 # 未打卡 set sign:1001 2 1 set sign:1001 3 1 set sign:1001 4 1查看某天是否已打卡
getbit sign 3統計當前用戶總的打卡天數
bitcount sign:1001除了這個場景之外,還有很多類似的場景都可以使用,
- 統計活躍用戶
- 記錄用戶在線狀態
bit 最大的好處在于,它通過 bit 位來存儲 0/1 表示特定含義,我們知道一個 int 類型是 8 個字節,占 32 個 bit 位,意味著一個 int 類型的數字就可以存儲 32 個有意義的場景,大大壓縮了存儲空間。
總結
數據結構總結
應用場景總結
實際上,所謂的應用場景,其實就是合理的利用 Redis 本身的數據結構的特性來完成相關業務功能,就像 mysql,它可以用來做服務注冊,也可以用來做分布式鎖,但是 mysql 它本質是一個關系型數據庫,只是用到了其他特性而已。
- 緩存 —— 提升熱點數據的訪問速度
- 共享數據 —— 數據的存儲和共享的問題
- 全局 ID —— 分布式全局 ID 的生成方案(分庫分表)
- 分布式鎖 —— 進程間共享數據的原子操作保證
- 在線用戶統計和計數
- 隊列、棧 —— 跨進程的隊列 / 棧
- 消息隊列 —— 異步解耦的消息機制
- 服務注冊與發現 —— RPC 通信機制的服務協調中心(Dubbo 支持 Redis)
- 購物車
- 新浪 / Twitter 用戶消息時間線
- 抽獎邏輯(禮物、轉發)
- 點贊、簽到、打卡
- 商品標簽
- 用戶(商品)關注(推薦)模型
- 電商產品篩選
- 排行榜
總結
以上是生活随笔為你收集整理的Redis基础——数据类型详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [附源码]JAVA+ssm计算机毕业设计
- 下一篇: ISO 8601中周数的处理及 Joda