Redis 深度歷險: 核心原理和應用實踐

目錄

 

1.Redis 可以做什麼? 

2.基礎:萬丈高樓平地起 ——Redis 基礎數據結構 

string (字符串)  

list (列表) 

hash (字典) 

set (集合) 

zset (有序列表)  

容器型數據結構的通用規則 

過期時間 

應用 1:千帆競發 —— 分佈式鎖 

分佈式鎖 


1.Redis 可以做什麼? 

Redis 的業務應用範圍非常廣泛,讓我們以掘金技術社區(juejin.im)的帖子模塊爲實
例,梳理一下,Redis 可以用在哪些地方? 
1、記錄帖子的點贊數、評論數和點擊數 (hash)。  
2、記錄用戶的帖子 ID 列表 (排序),便於快速顯示用戶的帖子列表 (zset)。  
3、記錄帖子的標題、摘要、作者和封面信息,用於列表頁展示 (hash)。  
4、記錄帖子的點贊用戶 ID 列表,評論 ID 列表,用於顯示和去重計數 (zset)。  
5、緩存近期熱帖內容 (帖子內容空間佔用比較大),減少數據庫壓力 (hash)。  
6、記錄帖子的相關文章 ID,根據內容推薦相關帖子 (list)。  
7、如果帖子 ID 是整數自增的,可以使用 Redis 來分配帖子 ID(計數器)。  
8、收藏集和帖子之間的關係 (zset)。  
9、記錄熱榜帖子 ID 列表,總熱榜和分類熱榜 (zset)。  
10、緩存用戶行爲歷史,進行惡意行爲過濾 (zset,hash)

以上提到的只是 Redis 的基礎應用,也是日常開發中最常見的應用

2.基礎:萬丈高樓平地起 ——Redis 基礎數據結構 

Redis 基礎數據結構 :  Redis 有 5 種基礎數據結構,分別爲:string (字符串)、list (列表)、set (集合)、hash (哈
希) 和 zset (有序集合)。

string (字符串)  


字符串 string 是 Redis 最簡單的數據結構。Redis 所有的數據結構都是以唯一的 key 
字符串作爲名稱,然後通過這個唯一 key 值來獲取相應的 value 數據。不同類型的數據結
構的差異就在於 value 的結構不一樣。  

字符串結構使用非常廣泛,一個常見的用途就是緩存用戶信息。我們將用戶信息結構體
使用 JSON 序列化成字符串,然後將序列化後的字符串塞進 Redis 來緩存。同樣,取用戶
信息會經過一次反序列化的過程。 

Redis 的字符串是動態字符串,是可以修改的字符串,內部結構實現上類似於 Java 的 
ArrayList,採用預分配冗餘空間的方式來減少內存的頻繁分配,如圖中所示,內部爲當前字
符串實際分配的空間 capacity 一般要高於實際字符串長度 len。當字符串長度小於 1M 時,
擴容都是加倍現有的空間,如果超過 1M,擴容時一次只會多擴 1M 的空間
。需要注意的是
字符串最大長度爲 512M

過期和 set 命令擴展

 可以對 key 設置過期時間,到點自動刪除,這個功能常用來控制緩存的失效時間

> set name codehole  > get name "codehole"  > expire name 5 # 5s 後過期  ... # wait for 5s  > get name  (nil)  > setex name 5 codehole # 5s 後過期,等價於 set+expire  > get name  "codehole"  ... # wait for 5s  > get name  (nil)

計數

如果 value 值是一個整數,還可以對它進行自增操作。自增是有範圍的,它的範圍是 
signed long 的最大最小值,超過了這個值,Redis 會報錯。 
> set age 30  OK  > incr age  (integer) 31  > incrby age 5  (integer) 36  > incrby age -5  (integer) 31 

list (列表) 

Redis 的列表相當於 Java 語言裏面的 LinkedList,注意它是鏈表而不是數組。這意味着 
list 的插入和刪除操作非常快,時間複雜度爲 O(1),但是索引定位很慢,時間複雜度爲 
O(n),這點讓人非常意外。

Redis 的列表結構常用來做異步隊列使用。將需要延後處理的任務結構體序列化成字符
串塞進 Redis 的列表,另一個線程從這個列表中輪詢數據進行處理

慢操作 

lindex 相當於 Java 鏈表的 get(int index)方法,它需要對鏈表進行遍歷,性能隨着參數
index 增大而變差。 ltrim 和字面上的含義不太一樣,個人覺得它叫 lretain(保留) 更合適一
些,因爲 ltrim 跟的兩個參數 start_index 和 end_index 定義了一個區間,在這個區間內的值,
ltrim 要保留,區間之外統統砍掉。我們可以通過 ltrim 來實現一個定長的鏈表,這一點非常
有用。index 可以爲負數,index=-1 表示倒數第一個元素,同樣 index=-2 表示倒數第二個元
。  

快速列表

如果再深入一點,你會發現 Redis 底層存儲的還不是一個簡單的 linkedlist,而是稱之爲
快速鏈表 quicklist 的一個結構

hash (字典) 

Redis 的字典相當於 Java 語言裏面的 HashMap,它是無序字典。內部實現結構上同 
Java 的 HashMap 也是一致的,同樣的數組 + 鏈表二維結構。第一維 hash 的數組位置碰撞
時,就會將碰撞的元素使用鏈表串接起來。 
 不同的是,Redis 的字典的值只能是字符串,另外它們 rehash 的方式不一樣,因爲 
Java 的 HashMap 在字典很大時,rehash 是個耗時的操作,需要一次性全部 rehash。Redis 
爲了高性能,不能堵塞服務,所以採用了漸進式 rehash 策略

漸進式 rehash 會在 rehash 的同時,保留新舊兩個 hash 結構,查詢時會同時查詢兩個 
hash 結構,然後在後續的定時任務中以及 hash 的子指令中,循序漸進地將舊 hash 的內容
一點點遷移到新的 hash 結構中
hash 結構也可以用來存儲用戶信息,不同於字符串一次性需要全部序列化整個對象,
hash 可以對用戶結構中的每個字段單獨存儲。這樣當我們需要獲取用戶信息時可以進行部分
獲取。
而以整個字符串的形式去保存用戶信息的話就只能一次性全部讀取,這樣就會比較浪
費網絡流量。 

hash 也有缺點,hash 結構的存儲消耗要高於單個字符串,到底該使用 hash 還是字符
串,需要根據實際情況再三權衡

 

同字符串一樣,hash 結構中的單個子 key 也可以進行計數,它對應的指令是 hincrby,
和 incr 使用基本一樣

set (集合) 

Redis 的集合相當於 Java 語言裏面的 HashSet,它內部的鍵值對是無序的唯一的。它的
內部實現相當於一個特殊的字典,字典中所有的 value 都是一個值 NULL

當集合中最後一個元素移除之後,數據結構自動刪除,內存被回收。 set 結構可以用來
存儲活動中獎的用戶 ID,因爲有去重功能,可以保證同一個用戶不會中獎兩次

zset (有序列表)  

zset 可能是 Redis 提供的最爲特色的數據結構,它也是在面試中面試官最愛問的數據結
構。它類似於 Java 的 SortedSet 和 HashMap 的結合體,一方面它是一個 set,保證了內部 
value 的唯一性,另一方面它可以給每個 value 賦予一個 score,代表這個 value 的排序權
重。它的內部實現用的是一種叫着「跳躍列表」的數據結構。 

zset 可以用來存
粉絲列表,value 值是粉絲的用戶 ID,score 是關注時間。我們可以對粉絲列表按關注時間
進行排序。 

zset 還可以用來存儲學生的成績,value 值是學生的 ID,score 是他的考試成績。我們
可以對成績按分數進行排序就可以得到他的名次

跳躍列表(類似java的跳錶Skiplist)

zset 內部的排序功能是通過「跳躍列表」數據結構來實現的,它的結構非常特殊,也比
較複雜。 
因爲 zset 要支持隨機的插入和刪除,所以它不好使用數組來表示。我們先看一個普通的
鏈表結構。 

跳躍列表就是類似於這種層級制,最下面一層所有的元素都會串起來。然後每隔幾個元
素挑選出一個代表來,再將這幾個代表使用另外一級指針串起來。然後在這些代表裏再挑出
二級代表,再串起來。最終就形成了金字塔結構。 

容器型數據結構的通用規則 

list/set/hash/zset 這四種數據結構是容器型數據結構,它們共享下面兩條通用規則:

1、create if not exists  
如果容器不存在,那就創建一個,再進行操作。比如 rpush 操作剛開始是沒有列表的,
Redis 就會自動創建一個,然後再 rpush 進去新元素。  
2、drop if no elements  
如果容器裏元素沒有了,那麼立即刪除元素,釋放內存。這意味着 lpop 操作到最後一
個元素,列表就消失了。

過期時間 

Redis 所有的數據結構都可以設置過期時間,時間到了,Redis 會自動刪除相應的對象。
需要注意的是過期是以對象爲單位,比如一個 hash 結構的過期是整個 hash 對象的過期,
而不是其中的某個子 key。  
還有一個需要特別注意的地方是如果一個字符串已經設置了過期時間,然後你調用了 
set 方法修改了它,它的過期時間會消失

應用 1:千帆競發 —— 分佈式鎖 

分佈式應用進行邏輯處理時經常會遇到併發問題。 
比如一個操作要修改用戶的狀態,修改狀態需要先讀出用戶的狀態,在內存裏進行修
改,改完了再存回去。如果這樣的操作同時進行了,就會出現併發問題,因爲讀取和保存狀
態這兩個操作不是原子的。(Wiki 解釋:所謂原子操作是指不會被線程調度機制打斷的操
作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch 線程切換。) 
 這個時候就要使用到分佈式鎖來限制程序的併發執行。Redis 分佈式鎖使用非常廣泛,
它是面試的重要考點之一

分佈式鎖 
 

分佈式鎖本質上要實現的目標就是在 Redis 裏面佔一個「茅坑」,當別的進程也要來佔
時,發現已經有人蹲在那裏了,就只好放棄或者稍後再試。 
佔坑一般是使用 setnx(set if not exists) 指令,只允許被一個客戶端佔坑。先來先佔, 用
完了,再調用 del 指令釋放茅坑

但是有個問題,如果邏輯執行到中間出現異常了,可能會導致 del 指令沒有被調用,這樣
就會陷入死鎖,鎖永遠得不到釋放。 

於是我們在拿到鎖之後,再給鎖加上一個過期時間,比如 5s,這樣即使中間出現異常也
可以保證 5 秒之後鎖會自動釋放

但是以上邏輯還有問題。如果在 setnx 和 expire 之間服務器進程突然掛掉了,可能是因
爲機器掉電或者是被人爲殺掉的,就會導致 expire 得不到執行,也會造成死鎖。

爲了解決這個疑難,Redis 開源社區涌現了一堆分佈式鎖的 library,專門用來解決這個問
題。實現方法極爲複雜,小白用戶一般要費很大的精力纔可以搞懂。如果你需要使用分佈式鎖,
意味着你不能僅僅使用 Jedis 或者 redis-py 就行了,還得引入分佈式鎖的 library。 

 爲了治理這個亂象,Redis 2.8 版本中作者加入了 set 指令的擴展參數,使得 setnx 和 
expire 指令可以一起執行,徹底解決了分佈式鎖的亂象。從此以後所有的第三方分佈式鎖 
library 可以休息了。 > set lock:codehole true ex 5 nx OK ... do something critical ... > del 
lock:codehole 上面這個指令就是 setnx 和 expire 組合在一起的原子指令,它就是分佈式鎖的
奧義所在

P26