老弟第一次學(xué) Redis,被坑慘了!小白可懂的保姆級 Redis 教程
你是小阿巴,剛?cè)肼毜某绦騿T。
這天,產(chǎn)品經(jīng)理找到你:阿巴阿巴,用戶吐槽咱們網(wǎng)站首頁加載太慢,快優(yōu)化!

你打開監(jiān)控一看,好家伙!每秒有上萬個用戶在訪問首頁,每次都要查詢 MySQL 數(shù)據(jù)庫來獲取熱門文章。

雖然你運用畢生所學(xué)優(yōu)化了數(shù)據(jù)庫查詢,但它還是扛不住這么高的并發(fā)。
你急得滿頭大汗:數(shù)據(jù)庫快撐不住了,怎么辦啊?

這時,你的導(dǎo)師 —— 號稱 “后端之狗” 的魚皮路過,淡定地說了 3 個字:Redis(瑞迪斯)。
你一臉懵:Redis?那是啥?

?? 推薦觀看本文對應(yīng)視頻:
第一階段:認(rèn)識 Redis
魚皮:Redis 的全稱是 Remote Dictionary Server(遠(yuǎn)程字典服務(wù)器),是一個 基于內(nèi)存的 K/V 存儲系統(tǒng)。
你:遠(yuǎn)程?字典?K/V?這些都是什么啊?

魚皮:你可以把 Redis 當(dāng)成一個數(shù)據(jù)庫,服務(wù)器可以通過網(wǎng)絡(luò)遠(yuǎn)程操作它寫入和讀取數(shù)據(jù)。K/V 就是 Key-Value 鍵值對,數(shù)據(jù)就像字典一樣保存。

你可以通過 Key 快速查詢到對應(yīng)的 Value。而且因為數(shù)據(jù)存在內(nèi)存中,Redis 的讀寫速度有時比 MySQL 快 100 倍!

你眼前一亮:那我趕緊裝一個試試!
機智如你,直接打開

但是怎么操作 Redis 呢?
魚皮:可以使用官方提供的命令行工具 Redis CLI 連接并操作 Redis。
打開終端輸入下列命令:
redis-cli -h 127.0.0.1 -p 6379

連上之后,咱們試試最基礎(chǔ)的操作,利用 SET 命令保存一個鍵值對:
SET name "xiaoaba"
然后通過鍵讀取到值:
GET name
你剛敲完命令,屏幕立刻返回 "小阿巴"。

你大為震驚:秒回!這也太快了!

魚皮:你剛剛保存的值是 Redis 的 String 字符串 類型,只是最簡單的一種。
你:還有其他類型?
魚皮:當(dāng)然,Redis 能存的東西可多著呢~ 它有 5 種基本數(shù)據(jù)結(jié)構(gòu),適用于不同場景:
1)String 字符串:存簡單的值,比如用戶名、計數(shù)器
SET username:1 "魚皮"
GET username:1
2)Hash 哈希表:存對象,比如用戶信息 {name: "小阿巴", age: 18}
HSET user:1001 name "小阿巴" age 18
HGET user:1001 name
3)List 列表:存有序數(shù)據(jù),比如最新的 10 條評論
LPUSH comments "太棒了!" "學(xué)到了"
LRANGE comments 0 -1
4)Set 集合:存不重復(fù)的數(shù)據(jù),比如點贊用戶列表
SADD post:1:likes user1 user2 user3
SMEMBERS post:1:likes
5)SortedSet 有序集合:存需要排序且不重復(fù)的數(shù)據(jù),比如游戲排行榜
ZADD leaderboard 100 "玩家A" 95 "玩家B" 90 "玩家C"
ZRANGE leaderboard 0 -1 WITHSCORES

你感嘆道:這用法也太多了,我要背這些命令嗎?
魚皮:千萬別背!用的時候去查

而且想偷懶的話,推薦你裝個 Redis 官方的可視化工具

你:哇,確實方便多了!但是我怎么用 Java 代碼操作 Redis 呢?
魚皮:主流編程語言都有操作 Redis 的客戶端 SDK。

對于 Java 開發(fā)者,可以選擇 Jedis、Lettuce、Spring Data Redis 和 Redisson,它們封裝了很多操作 Redis 的方法,幾行代碼就能搞定。

時間充足的話,建議你看看某馬的 Redis 教程來系統(tǒng)學(xué)習(xí),我當(dāng)年也看過,講的很好,先把基礎(chǔ)篇和實戰(zhàn)篇看完就夠了。

你:得咧,我這就看!

第二階段:實戰(zhàn)應(yīng)用
半個月后,你已經(jīng)看了不少教程,準(zhǔn)備解決熱門文章查詢太慢的問題。

你實現(xiàn)了經(jīng)典的緩存邏輯:第一次查詢時,先從 Redis 中查找,如果沒有找到,再從 MySQL 數(shù)據(jù)庫中查詢,然后將結(jié)果存入 Redis 中,并設(shè)置過期時間。后續(xù)相同的查詢就能直接從 Redis 返回結(jié)果。

示例代碼:
public List<Article> getHotArticles() {
// 1. 先從 Redis 查詢
String cacheKey = "hot_articles";
String cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
// 緩存命中,直接返回
return JSON.parseArray(cachedData, Article.class);
}
// 2. 緩存未命中,查詢數(shù)據(jù)庫
List<Article> articles = articleMapper.selectHotArticles();
// 3. 將結(jié)果存入 Redis,設(shè)置 10 分鐘過期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(articles), 10, TimeUnit.MINUTES);
return articles;
}
使用 Redis 后,查詢速度從 3 秒縮短到了 0.3 秒,產(chǎn)品經(jīng)理看你的眼神都變了,你很是得意。

魚皮:不錯,不過 Redis 的作用可不僅僅是緩存,你還能用它繼續(xù)優(yōu)化項目么?比如之前用戶反饋的登錄態(tài)失效問題。
你想了想:Redis 的本質(zhì)是 存儲系統(tǒng),也就是說,可以利用 Redis 來存儲多個服務(wù)器間需要共享的數(shù)據(jù)。公司有 2 臺服務(wù)器,用戶第一次登錄時,Session 存在服務(wù)器 A 本地;刷新頁面后,請求被分配到服務(wù)器 B,就找不到 Session 了,導(dǎo)致丟失登錄態(tài)。那我就把需要共享的 Session 數(shù)據(jù)存到 Redis 中,這樣無論請求分配到哪臺服務(wù)器,都能從 Redis 里拿到 Session。

魚皮欣慰地點了點頭:不錯,這就是典型的 分布式 Session 問題。
類似的,分布式鎖也可以利用 Redis 實現(xiàn),讓多個服務(wù)器都從 Redis 去獲取鎖資源,誰先拿到鎖,誰就能操作,其他人排隊等待。

你:確實,那 Redis 還有其他應(yīng)用場景么?
魚皮:那可太多了,Redis 除了 5 種基本數(shù)據(jù)結(jié)構(gòu)外,還提供了很多 “高級” 數(shù)據(jù)結(jié)構(gòu),專門解決特定場景的問題。
-
GEO:存地理位置坐標(biāo),實現(xiàn)附近的人、附近的餐廳功能
-
Bitmap:用 1 個 bit 表示一個狀態(tài),節(jié)約內(nèi)存,實現(xiàn)用戶簽到、在線狀態(tài)統(tǒng)計
-
HyperLogLog:適用于大規(guī)模統(tǒng)計 UV(獨立訪客數(shù)),雖然存在誤差,但內(nèi)存占用只有 12 KB
-
布隆過濾器:快速判斷數(shù)據(jù)是否存在,可用來防止無效查詢打到數(shù)據(jù)庫
-
Stream:消息隊列功能,可以實現(xiàn)異步任務(wù),比如用戶下單后異步發(fā)送短信通知(但更推薦用專業(yè)的消息隊列)

魚皮:對了,還有更高級的玩法,Redis 支持編寫 Lua 腳本 保證多個操作的原子性,確保要么全部成功,要么全部失敗,避免數(shù)據(jù)不一致的問題。

你:這些聽起來好高級,感覺學(xué)不完了……
魚皮:想什么呢,肯定學(xué)不完啊!
Redis 是在持續(xù)更新的,去看看

Redis 是實戰(zhàn)型技術(shù),光看不練等于白學(xué),一定要多動手實踐。
你:好,那我就先給

第三階段:常見問題和解決方案
一個月后的某天,凌晨 3 點,你被電話吵醒了。
運維小哥急切地說:阿巴阿巴,網(wǎng)站掛了!數(shù)據(jù)庫查詢一直超時,你快看看!
你很是疑惑:都用 Redis 緩存了,還能超時?

你趕緊去公司看了下日志,發(fā)現(xiàn)原來有惡意用戶在瘋狂查詢一個不存在的文章 ID,每次 Redis 緩存中都查不到,請求就直接打到了 MySQL 數(shù)據(jù)庫上!

魚皮這時也趕到了公司:這就是經(jīng)典的 緩存穿透 問題,惡意請求故意查詢不存在的數(shù)據(jù),繞過緩存,直接攻擊數(shù)據(jù)庫。

你汗流浹背了:那怎么辦?

魚皮:最簡單的方法是,即使數(shù)據(jù)庫查詢結(jié)果為空,仍將這個空結(jié)果緩存到 Redis 中,并設(shè)置一個較短的過期時間。后續(xù)相同請求會直接命中緩存的空值,避免訪問數(shù)據(jù)庫。

你恍然大悟,趕緊寫代碼補充了緩存空值的邏輯。
public Article getArticleById(Long id) {
String cacheKey = "article:" + id;
String cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
// 如果是空值標(biāo)識,直接返回 null
if ("__NULL__".equals(cachedData)) {
return null;
}
return JSON.parseObject(cachedData, Article.class);
}
// 查詢數(shù)據(jù)庫
Article article = articleMapper.selectById(id);
if (article != null) {
// 存儲正常數(shù)據(jù),10 分鐘過期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(article), 10, TimeUnit.MINUTES);
} else {
// 存儲空值標(biāo)識,2 分鐘過期
redisTemplate.opsForValue().set(cacheKey, "NULL", 2, TimeUnit.MINUTES);
}
return article;
}
魚皮提醒道:緩存穿透只是其中一個問題,實際項目中還會遇到 緩存擊穿 和 緩存雪崩。
1)緩存擊穿:熱門數(shù)據(jù)突然過期,大量請求同時涌向數(shù)據(jù)庫。可以用互斥鎖,讓請求排隊一個一個來,第一個請求負(fù)責(zé)查數(shù)據(jù)庫并重建緩存,其他請求等緩存重建完成后再查詢。

2)緩存雪崩:大量緩存同時過期,數(shù)據(jù)庫瞬間壓力巨大。解決方法是給過期時間加上隨機值,避免同時失效。

你感慨道:Redis 的坑還真不少!
魚皮:這就是為什么要學(xué)習(xí) Redis 的最佳實踐,比如合理設(shè)計緩存鍵名、設(shè)置合適的過期時間、選擇正確的數(shù)據(jù)結(jié)構(gòu)等等。

魚皮:不過這才剛開始呢,你以后還會遇到緩存和數(shù)據(jù)庫的數(shù)據(jù)不一致、查詢 Redis 阻塞、內(nèi)存爆滿等問題,都是需要學(xué)習(xí)的,以后我再給你講吧。

你:那如果 Redis 本身也扛不住高并發(fā)怎么辦?
魚皮:那就搭建 多級緩存,比如服務(wù)器本地內(nèi)存緩存 => Redis 分布式緩存 => 數(shù)據(jù)庫,層層過濾。用戶請求先查本地緩存,沒有再查 Redis,還沒有才查數(shù)據(jù)庫。每一層都能攔截一部分請求,大大減少 Redis 的壓力。

還有 緩存預(yù)熱,比如大促活動前,提前把熱門商品數(shù)據(jù)加載到 Redis 中,避免活動開始時大量請求同時打到數(shù)據(jù)庫。

第四階段:高級特性和生產(chǎn)部署
用了一段時間 Redis 后,你開始有點飄了。

你對著新來的實習(xí)生阿坤炫耀:Redis 我閉著眼睛都能寫!什么緩存、分布式鎖、多級緩存,我都搞定過!
結(jié)果第二天,老板黑著臉找到你:昨晚機房斷電重啟后,很多用戶反饋自己的學(xué)習(xí)進(jìn)度丟失了,怎么回事?!
你這才意識到問題的嚴(yán)重性:Redis 數(shù)據(jù)存儲在內(nèi)存,斷電不就沒了嗎?

魚皮及時出現(xiàn):看來你對 Redis 的理解還停留在玩具階段啊,生產(chǎn)環(huán)境的 Redis 可不是這么簡單!我來問你幾個問題。
第一問:怎么防止數(shù)據(jù)丟失?
你支支吾吾:做…… 做備份?
沒想到,旁邊的阿坤突然雞叫起來:可以利用 Redis 提供的 2 種持久化方案!

1)RDB 快照:定期把內(nèi)存數(shù)據(jù)完整保存到硬盤,像拍照一樣,恢復(fù)快但可能丟失最新數(shù)據(jù)。

2)AOF 日志:把每個寫操作都記錄下來,像記日記,數(shù)據(jù)更安全但恢復(fù)較慢(需要重新執(zhí)行所有寫操作)。

第二問:Redis 服務(wù)器掛了怎么辦?
你撓頭:重啟?

魚皮搖頭:用戶等得起嗎?
阿坤又雞叫起來:要部署 主從集群,主節(jié)點負(fù)責(zé)寫數(shù)據(jù),從節(jié)點實時同步主節(jié)點的數(shù)據(jù)。這樣即使主節(jié)點掛了,從節(jié)點立刻頂上,用戶毫無感知。

第三問:主節(jié)點掛了,誰來決定哪個從節(jié)點升級?
你無言以對,但是那阿坤竟從容不迫:用 哨兵機制,哨兵就像監(jiān)工,可以實時監(jiān)控 Redis 集群健康狀態(tài)。發(fā)現(xiàn)主節(jié)點掛了,自動選擇一個從節(jié)點升級為新主節(jié)點,實現(xiàn)自動故障轉(zhuǎn)移。

第四問:數(shù)據(jù)量太大,單臺 Redis 存不下怎么辦?
你眼前一亮,終于等到自己會的問題了,搶答道:刪除數(shù)據(jù)!

阿坤用看流浪狗的眼神看了你一眼,回答道:首先 Redis 自身有多種淘汰策略,會自動刪除不常用的數(shù)據(jù)。

還可以搭建 分片集群,把數(shù)據(jù)按照某種規(guī)則分散到多臺 Redis 上,每臺只存一部分。Redis 使用 哈希槽 機制來分配數(shù)據(jù),既能存儲更多數(shù)據(jù),又能承受更高并發(fā)。

魚皮拍了拍阿坤的肩膀:小伙子年輕有為啊!
你羞愧地抬不起頭:我以為自己已經(jīng)掌握了 Redis,原來只是學(xué)了個皮毛……

魚皮:這些都是 Redis 在生產(chǎn)環(huán)境必須考慮的問題,大廠的 Redis 集群動輒幾十上百個節(jié)點,就是為了保證 高可用、高性能、高可擴展性。小阿巴,你還要好好跟阿坤學(xué)習(xí)啊。
第五階段:深入底層原理
被魚皮連環(huán)拷問后,你主動找到阿坤:我想深入學(xué)習(xí) Redis,不能只停留在會用的層面,請問你是怎么學(xué)習(xí)底層原理的呀?
阿坤有些驚訝:咦?你不背八股文的么?去

你震驚了:現(xiàn)在的校招生,竟然恐怖如斯!

魚皮:阿坤你別逗他了,其實我們可以帶著問題學(xué)習(xí)。比如你知道 Redis 為什么這么快 嗎?
你想了想:因為數(shù)據(jù)存在內(nèi)存里?

魚皮:這只是表面原因。深層次的原因有很多:
-
高效的數(shù)據(jù)結(jié)構(gòu):Redis 底層使用了動態(tài)字符串、跳表、壓縮列表等經(jīng)過優(yōu)化的數(shù)據(jù)結(jié)構(gòu)
-
單線程模型:避免了多線程的上下文切換開銷
-
IO 多路復(fù)用:一個線程就能同時處理成千上萬個連接
-
內(nèi)存管理:有完善的內(nèi)存淘汰策略,比如 LRU 算法

你驚訝:單線程還能這么快?

魚皮:對!這就是 Redis 設(shè)計的巧妙之處。從這些問題出發(fā),去閱讀相關(guān)的文章,或者直接像阿坤說的刷一刷 Redis 面試題,就能快速學(xué)會很多核心知識點。

如果想系統(tǒng)學(xué)習(xí),可以看看《Redis 設(shè)計與實現(xiàn)》這本書,講得很透徹;甚至可以看看 Redis 的開源代碼。

要記住,學(xué)習(xí)底層原理不是為了炫技,而是為了更好地使用 Redis,遇到問題時能夠快速定位和解決。
你:好的,我這就去學(xué)!

結(jié)尾
若干年后,你已經(jīng)成為了公司的 Redis 專家。

不僅能熟練使用 Redis 解決各種業(yè)務(wù)問題,搭個 Redis 集群架構(gòu)也是手拿把掐的。
你也像魚皮當(dāng)時一樣,耐心地給新人分享學(xué)習(xí) Redis 的經(jīng)驗,讓他們謹(jǐn)記魚皮的教誨 “Redis 是實戰(zhàn)型技術(shù),一定要多動手實踐”。

再次遇到魚皮是在一條昏暗的小巷,此時的他年過 35,灰頭土臉。你什么都沒說,只是給他點了個贊,投了 2 個幣,不打擾,是你的溫柔。


這天,產(chǎn)品經(jīng)理找到你:阿巴阿巴,用戶吐槽咱們網(wǎng)站首頁加載太慢,快優(yōu)化!
浙公網(wǎng)安備 33010602011771號