一、redis 特點
redis(Remote Dictionary Server ),即全稱是:遠程字典服務。
- 所有數據存儲在內存中,高速讀寫
- 提供豐富多樣的數據類型:string、 hash、list、 set、 sorted set、bitmap、hyperloglog
- 提供了 AOF 和 RDB 兩種數據的持久化保存方式,保證了 Redis 重啟后數據不丟失
- Redis 的所有操作都是原子性的,還支持對幾個操作合並后的原子性操作,支持事務
通常我們都把數據存到關系型數據庫中,但為了提升應用的性能,我們應該把訪頻率高且不會經常變動的數據緩存到內存中。
Redis 沒有像 MySQL 這類關系型數據庫那樣強大的查詢功能,需要考慮如何把關系型數據庫中的數據,合理的對應到緩存的 key-value 數據結構中。
二、分段設計法設計 Redis Key
使用冒號把 key 中要表達的多種含義分開表示,步驟如下:
- 把表名轉化為 key 前綴
- 主鍵名(或其他常用於搜索的字段)
- 主鍵值
- 要存儲的字段
eg. 用戶表(user)
id | name | |
---|---|---|
1 | zj | 156577812@qq.com |
2 | ai | 156577813@qq.com |
這個簡單的表可能經常會有這個的需求:>根據用戶 id 查詢用戶郵箱地址,可以選擇把郵箱地址這個數據存到 redis 中:
set user:id:1:email 156577812@qq.com;
set user:id:2:email 156577812@qq.com;
三、 String數據類型的應用場景
1. 簡介
string 類型是 Redis 中最基本的數據類型,最常用的數據類型,甚至被很多玩家當成 redis 唯一的數據類型去使用。string 類型在 redis 中是二進制安全(binary safe)的,這意味着 string 值關心二進制的字符串,不關心具體格式,你可以用它存儲 json 格式或 JPEG 圖片格式的字符串。
2. 應用場景
(1)存儲 MySQL 中某個字段的值
把 key 設計為 表名:主鍵名:主鍵值:字段名
eg.
set user:id:1:email 156577812@qq.com
(2)存儲對象
string 類型支持任何格式的字符串,應用最多的就是存儲 json 或其他對象格式化的字符串。(這種場景下推薦使用 hash 數據類型)
set user:id:1 '[{"id":1,"name":"zj","email":"156577812@qq.com"},{"id":1,"name":"zj","email":"156577812@qq.com"}]'
(3)生成自增 id
當 redis 的 string 類型的值為整數形式時,redis 可以把它當做是整數一樣進行自增(incr)自減(decr)操作。由於 redis 所有的操作都是原子性的,所以不必擔心多客戶端連接時可能出現的事務問題。
四、hash 數據類型的應用場景
1. 簡介
hash 類型很像一個關系型數據庫的數據表,hash 的 Key 是一個唯一值,Value 部分是一個 hashmap 的結構。
2. 數據模型
假設有一張數據庫表如下:
id | name | type |
---|---|---|
1 | redis | hash |
如果要用 redis 的 hash 結構存儲,數據模型如下:
hash數據類型在存儲上述類型的數據時具有比 string 類型更靈活、更快的優勢,具體的說,使用 string 類型存儲,必然需要轉換和解析 json 格式的字符串,即便不需要轉換,在內存開銷方面,還是 hash 占優勢。
3. 應用場景
hash 類型十分適合存儲對象類數據,相對於在 string 中介紹的把對象轉化為 json 字符串存儲,hash 的結構可以任意添加或刪除‘字段名’,更加高效靈活。
hmset user:1 name zj email 156577812@qq.com
HGETALL user:1
輸出:
1) "name"
2) "zj"
3) "email"
4) "156577812@qq.com"
五、list 數據類型的應用場景
1. 簡介
list 是按照插入順序排序的字符串鏈表,可以在頭部和尾部插入新的元素(雙向鏈表實現,兩端添加元素的時間復雜度為 O(1))。插入元素時,如果 key 不存在,redis 會為該 key 創建一個新的鏈表,如果鏈表中所有的元素都被移除,該 key 也會從 redis 中移除。
2. 數據模型
常見操作時用 lpush 命令在 list 頭部插入元素, 用 rpop 命令在 list 尾取出數據。
3. 應用場景
(1) 消息隊列
redis 的 list 數據類型對於大部分使用者來說,是實現隊列服務的最經濟,最簡單的方式。
LPUSH bkey redis
LPUSH bkey mongodb
LPUSH bkey mysql
LRANGE bkey 0 10
輸出:
1) "mysql"
2) "mongodb"
3) "redis"
(2) “最新內容”
因為 list 結構的數據查詢兩端附近的數據性能非常好,所以適合一些需要獲取最新數據的場景,比如新聞類應用的 “最近新聞”。
4.優化建議
list 是鏈表結構,所有如果在頭部和尾部插入數據,性能會非常高,不受鏈表長度的影響;但如果在鏈表中插入數據,性能就會越來越差。
六、set 數據類型的應用場景
1. 簡介
- set 數據類型是一個集合(沒有排序,不重復),可以對 set 類型的數據進行添加、刪除、判斷是否存在等操作(時間復雜度是 O(1) )
- set 集合不允許數據重復,如果添加的數據在 set 中已經存在,將只保留一份。
- set 類型提供了多個 set 之間的聚合運算,如求交集、並集、補集,這些操作在 redis 內部完成,效率很高。
2. 數據模型
3. 應用場景
set 類型的特點是——不重復且無序的一組數據,並且具有豐富的計算功能,在一些特定的場景中可以高效的解決一般關系型數據庫不方便做的工作。
共同好友列表
社交類應用中,獲取兩個人或多個人的共同好友,兩個人或多個人共同關注的微博這樣類似的功能,用 MySQL 的話操作很復雜,可以把每個人的好友 id 存到集合中,獲取共同好友的操作就可以簡單到一個取交集的命令就搞定。
// 這里為了方便閱讀,把 id 替換成姓名
sadd user:wade james melo paul kobe
sadd user:james wade melo paul kobe
sadd user:paul wade james melo kobe
sadd user:melo wade james paul kobe
// 獲取 wade 和 james 的共同好友
sinter user:wade user:james
輸出:
1) "kobe"
2) "paul"
3) "melo"
// 獲取香蕉四兄弟的共同好友
sinter user:wade user:james user:paul user:melo
輸出:
1) "kobe"
類似的需求場景
還有很多,比如:
- 必須把每個標簽下的文章 id 存到集合中,可以很容易的求出幾個不同標簽下的共同文章;
- 把每個人的愛好存到集合中,可以很容易的求出幾個人的共同愛好。
七、sorted set 數據類型的應用場景
1.簡介
在 set 的基礎上給集合中每個元素關聯了一個分數,往有序集合中插入數據時會自動根據這個分數排序。
2.應用場景
在集合類型的場景上加入排序就是有序集合的應用場景了。比如根據好友的“親密度”排序顯示好友列表。
// 用元素的分數(score)表示與好友的親密度
zadd user:kobe 80 james 90 wade 85 melo 90 paul
// 根據“親密度”給好友排序
zrevrange user:kobe 0 -1
輸出:
1) "wade"
2) "paul"
3) "melo"
4) "james"
// 增加好友的親密度
zincrby user:kobe 15 james
// 再次根據“親密度”給好友排序
zrevrange user:kobe 0 -1
輸出:
1) "james"
2) "wade"
3) "paul"
2) "melo"
//類似的需求還出現在根據文章的閱讀量或點贊量對文章列表排序
八、Redis 的 8 大應用場景
1. 緩存
緩存現在幾乎是所有中大型網站都在用的必殺技,合理的利用緩存不僅能夠提升網站訪問速度,還能大大降低數據庫的壓力。Redis提供了鍵過期功能,也提供了靈活的鍵淘汰策略,所以,現在Redis用在緩存的場合非常多。
2. 排行榜
很多網站都有排行榜應用的,如京東的月度銷量榜單. 商品按時間的上新排行榜等。Redis提供的有序集合數據類構能實現各種復雜的排行榜應用。
3. 計數器
什么是計數器,如電商網站商品的瀏覽量. 視頻網站視頻的播放數等。為了保證數據實時效,每次瀏覽都得給+1,並發量高時如果每次都請求數據庫操作無疑是種挑戰和壓力。Redis提供的incr命令來實現計數器功能,內存操作,性能非常好,非常適用於這些計數場景。
4. 分布式會話
集群模式下,在應用不多的情況下一般使用容器自帶的session復制功能就能滿足,當應用增多相對復雜的系統中,一般都會搭建以Redis等內存數據庫為中心的session服務,session不再由容器管理,而是由session服務及內存數據庫管理。
5. 分布式鎖
在很多互聯網公司中都使用了分布式技術,分布式技術帶來的技術挑戰是對同一個資源的並發訪問,如全局ID. 減庫存. 秒殺等場景,並發量不大的場景可以使用數據庫的悲觀鎖. 樂觀鎖來實現,但在並發量高的場合中,利用數據庫鎖來控制資源的並發訪問是不太理想的,大大影響了數據庫的性能。可以利用Redis的setnx功能來編寫分布式的鎖,如果設置返回1說明獲取鎖成功,否則獲取鎖失敗,實際應用中要考慮的細節要更多。
6. 社交網絡
點贊. 踩. 關注/被關注. 共同好友等是社交網站的基本功能,社交網站的訪問量通常來說比較大,而且傳統的關系數據庫類型不適合存儲這種類型的數據,Redis提供的哈希. 集合等數據結構能很方便的的實現這些功能。
7. 最新列表
Redis列表結構,LPUSH可以在列表頭部插入一個內容ID作為關鍵字,LTRIM可用來限制列表的數量,這樣列表永遠為N個ID,無需查詢最新的列表,直接根據ID去到對應的內容頁即可。
8. 消息系統
消息隊列是大型網站必用中間件,如ActiveMQ. RabbitMQ. Kafka等流行的消息隊列中間件,主要用於業務解耦. 流量削峰及異步處理實時性低的業務。Redis提供了發布/訂閱及阻塞隊列功能,能實現一個簡單的消息隊列系統。另外,這個不能和專業的消息中間件相比。
九、SETNX
可用版本: >= 1.0.0
時間復雜度: O(1)
- 只在鍵 key 不存在的情況下, 將鍵 key 的值設置為 value 。
- 若鍵 key 已經存在, 則 SETNX 命令不做任何動作。
- SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。
返回值:命令在設置成功時返回 1 , 設置失敗時返回 0
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 設置成功
(integer) 1
redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗
(integer) 0
redis> GET job # 沒有被覆蓋
"programmer"
十、HyperLogLog 使用與應用場景
- HyperLogLog是一種算法,並非redis獨有
- Redis HyperLogLog是用來做基數統計的算法, 優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的。
- 在Redis里面,每個HyperLogLog鍵只需要花費12KB內存,就可以計算接近2^64個不同元素的基數。和計算基數時,元素越多耗費內存就越多的集合形成鮮明對比。
- 因為HyperLogLog只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以HyperLogLog不能像集合那樣,返回輸入的各個元素。
- 核心是基數估算算法,主要表現為計算時內存的使用和數據合並的處理。最終數值存在一定誤差
- pfadd命令並不會一次性分配12k內存,而是隨着基數的增加而逐漸增加內存分配;而pfmerge操作則會將sourcekey合並后存儲在12k大小的key中,由hyperloglog合並操作的原理(兩個hyperloglog合並時需要單獨比較每個桶的值)可以很容易理解。
- Redis對HyperLogLog的存儲進行優化,在計數比較小時,存儲空間采用稀疏矩陣存儲,空間占用很小,僅僅在計數慢慢變大,稀疏矩陣占用空間漸漸超過了閾值時才會一次性轉變成稠密矩陣,才會占用12k的空間
誤差說明:基數估計的結果是一個帶有0.81%標准錯誤(standarderror)的近似值。是可接受的范圍
延伸閱讀:探索HyperLogLog算法(含Java實現)
pfadd 添加
- 影響基數估值則返回1否則返回0.若key不存在則創建
- 時間復雜度O(1)
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount 獲得基數值
- 得到基數值,叫做去重值(1,1,2,2,3)的插入pfcount得到的是3
- 可一次統計多個key
- 時間復雜度為O(N),N為key的個數
- 返回值是一個帶有 0.81% 標准錯誤(standard error)的近似值.
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4
pfmerge 合並多個key
- 取多個key的並集
- 命令只會返回 OK.
- 時間復雜度為O(N)
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
127.0.0.1:6379> pfcount m1
(integer) 4
127.0.0.1:6379> pfadd m2 3 3 3 4 4 4 5 5 5 6 6 6 1
(integer) 1
127.0.0.1:6379> pfcount m2
(integer) 5
127.0.0.1:6379> pfmerge mergeDes m1 m2
OK
127.0.0.1:6379> pfcount mergeDes
(integer) 6
應用場景
基數不大,數據量不大就用不上,會有點大材小用浪費空間,有局限性,就是只能統計基數數量,而沒辦法去知道具體的內容是什么,和bitmap相比,屬於兩種特定統計情況,簡單來說,HyperLogLog 去重比 bitmap 方便很多,一般可以bitmap和hyperloglog配合使用,bitmap標識哪些用戶活躍,hyperloglog計數
一般使用:
- 統計注冊 IP 數
+統計每日訪問 IP 數
+統計頁面實時 UV 數
+統計在線用戶數
+統計用戶每天搜索不同詞條的個數
十一、Redis Big Key問題
數據量大的 key ,由於其數據大小遠大於其他key,導致經過分片之后,某個具體存儲這個 big key 的實例內存使用量遠大於其他實例,造成內存不足,拖累整個集群的使用。
big key 在不同業務上,通常體現為不同的數據,比如:
- 論壇中的大型持久蓋樓活動;
- 聊天室系統中熱門聊天室的消息列表;
1. 帶來的問題
bigkey 通常會導致內存空間不平衡,超時阻塞,如果 key 較大,redis 又是單線程,操作 bigkey 比較耗時,那么阻塞 redis 的可能性增大。每次獲取 bigKey 的網絡流量較大,假設一個 bigkey 為 1MB,每秒訪問量為 1000,那么每秒產生 1000MB 的流量,對於普通千兆網卡,按照字節算 128M/S 的服務器來說可能扛不住。而且一般服務器采用單機多實例方式來部署,所以還可能對其他實例造成影響。
如果是集群模式下,無法做到負載均衡,導致請求傾斜到某個實例上,而這個實例的QPS會比較大,內存占用也較多;對於Redis單線程模型又容易出現CPU瓶頸,當內存出現瓶頸時,只能進行縱向庫容,使用更牛逼的服務器。
涉及到大key的操作,尤其是使用hgetall、lrange、get、hmget 等操作時,網卡可能會成為瓶頸,也會到導致堵塞其它操作,qps 就有可能出現突降或者突升的情況,趨勢上看起來十分不平滑,嚴重時會導致應用程序連不上,實例或者集群在某些時間段內不可用的狀態。
假如這個key需要進行刪除操作,如果直接進行DEL 操作,被操作的實例會被Block住,導致無法響應應用的請求,而這個Block的時間會隨着key的變大而變長。
2. 什么是 big key
字符串類型:一般認為超過 10k 的就是 bigkey,但是這個值和具體的 OPS 相關。
非字符串類型:體現在哈希,列表,集合類型元素過多。
3. 尋找big key
- redis-cli自帶--bigkeys。
$ redis-cli -p 999 --bigkeys -i 0.1
#Scanning the entire keyspace to find biggest keys as well as average sizes per key type. You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands (not usually needed).
- 獲取生產Redis的rdb文件,通過rdbtools分析rdb生成csv文件,再導入MySQL或其他數據庫中進行分析統計,根據size_in_bytes統計bigkey
$ git clone https://github.com/sripathikrishnan/redis-rdb-tools
$ cd redis-rdb-tools
$ sudo python setup.py install
$ rdb -c memory dump-10030.rdb > memory.csv
-
通過python腳本,迭代scan key,每次scan 1000,對掃描出來的key進行類型判斷,例如:string長度大於10K,list長度大於10240認為是big bigkeys
-
其他第三方工具,例如:redis-rdb-cli
4. 優化big key
優化big key的原則就是string減少字符串長度,list、hash、set、zset等減少成員數。
- string類型的big key,建議不要存入redis
用文檔型數據庫MongoDB代替或者直接緩存到CDN上等方式優化。有些 key 不只是訪問量大,數據量也很大,這個時候就要考慮這個 key 使用的場景,存儲在redis集群中是否是合理的,是否使用其他組件來存儲更合適;如果堅持要用 redis 來存儲,可能考慮遷移出集群,采用一主一備(或1主多備)的架構來存儲。
- 單個簡單的key存儲的value很大
該對象需要每次都整存整取: 可以嘗試將對象分拆成幾個key-value, 使用multiGet獲取值,這樣分拆的意義在於分拆單次操作的壓力,將操作壓力平攤到多個redis實例中,降低對單個redis的IO影響;
該對象每次只需要存取部分數據: 可以像第一種做法一樣,分拆成幾個key-value,也可以將這個存儲在一個hash中,每個field代表一個具體的屬性,使用hget,hmget來獲取部分的value,使用hset,hmset來更新部分屬性。
- hash, set,zset,list 中存儲過多的元素
可以將這些元素分拆。以hash為例,原先的正常存取流程是 hget(hashKey, field) ; hset(hashKey, field, value)
現在,固定一個桶的數量,比如 10000, 每次存取的時候,先在本地計算field的hash值,模除 10000,確定了該field落在哪個key上。
newHashKey = hashKey + (hash(field) % 10000);
hset(newHashKey, field, value) ;
hget(newHashKey, field)
set, zset, list 也可以類似上述做法。但有些不適合的場景。比如,要保證 lpop 的數據的確是最早push到list中去的,這個就需要一些附加的屬性,或者是在 key的拼接上做一些工作(比如list按照時間來分拆)。
十二、Redis 發布訂閱
Redis 發布訂閱 (pub/sub) 是一種消息通信模式:發送者 (pub) 發送消息,訂閱者 (sub) 接收消息。
Redis 客戶端可以訂閱任意數量的頻道。
// 訂閱
SUBSCRIBE bbChat
// 發布
PUBLISH bbChat "Redis PUBLISH test"
// 退閱
UNSUBSCRIBE bbChat