首頁  >  文章  >  資料庫  >  使用Redis可能遇到的15個坑,快來收藏避雷吧! !

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

青灯夜游
青灯夜游轉載
2021-04-21 10:46:233210瀏覽

這篇文章跟大家介紹一下使用Redis可能會遇到的15個坑。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

大家好,我是 Kaito。

這篇文章,我想和你聊一聊在使用 Redis 時,可能會踩到的「坑」。

如果你在使用Redis 時,也遇到過以下這些「詭異」的場景,那很大機率是踩到「坑」了:

  • 明明一個key 設定了過期時間,怎麼變成不過期了?

  • 使用 O(1) 複雜度的 SETBIT 指令,Redis 竟然被 OOM 了?

  • 執行 RANDOMKEY 隨機拿出一個 key,竟然也會阻塞 Redis?

  • 同樣的指令,為什麼主庫查不到數據,從函式庫卻可以查到?

  • 從庫記憶體為什麼比主函式庫用得還多?

  • 寫入到 Redis 的數據,為什麼莫名其妙丟了?

  • ...

【相關推薦:Redis影片教學

究竟是什麼原因,導致的這些問題呢?

這篇文章,我就來和你盤點一下,使用 Redis 時可能會踩到「坑」,以及如何去規避。

我把這些問題分成三大部分:

  • 常見指令有哪些坑了?

  • 資料持久化有哪些坑?

  • 主從函式庫同步有哪些坑?

導致這些問題的原因,很有可能會「顛覆」你的認知,如果你準備好了,那就跟著我的思路開始吧!

這篇文章乾貨很多,希望你可以耐心讀完。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

常見指令有哪些坑?

首先,我們來看一下,平常在使用 Redis 時,有哪些常見的指令會遇到「意料之外」的結果。

1) 過期時間意外遺失?

你在使用 Redis 時,一定常常使用 SET 指令,它非常簡單。

SET 除了可以設定key-value 之外,還可以設定key 的過期時間,就像下面這樣:

127.0.0.1:6379> SET testkey val1 EX 60
OK
127.0.0.1:6379> TTL testkey
(integer) 59

此時如果你想修改key 的值,但只是單純地使用SET 指令,而沒有加上「過期時間」的參數,那這個key 的過期時間將會被「擦除」。

127.0.0.1:6379> SET testkey val2
OK
127.0.0.1:6379> TTL testkey  // key永远不过期了!
(integer) -1

看到了麼? testkey 變成永遠不過期了!

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

如果你剛開始使用 Redis,相信你一定也踩過這個坑。

導致這個問題的原因在於:SET 指令如果不設定過期時間,那麼 Redis 會自動「擦除」這個 key 的過期時間。

如果你發現 Redis 的記憶體持續成長,而且很多 key 原來設定了過期時間,後來發現過期時間遺失了,很有可能是因為這個原因導致的。

這時你的 Redis 中就會存在大量不過期的 key,消耗過多的記憶體資源。

所以,你在使用 SET 指令時,如果剛開始就設定了過期時間,那麼之後修改這個 key,也務必要加上過期時間的參數,避免過期時間遺失問題。

2) DEL 竟然也會阻塞 Redis?

刪除一個 key,你一定會用 DEL 指令,不知道你沒有思考過它的時間複雜度是多少?

O(1)?其實不一定。

如果你有認真閱讀 Redis 的官方文檔,你會發現:刪除一個 key 的耗時,與這個 key 的類型有關。

Redis 官方文件在介紹DEL 指令時,是這樣描述的:

  • key 是String 類型,DEL 時間複雜度是O(1)

  • key 是List/Hash/Set/ZSet 類型,DEL 時間複雜度為O(M),M 為元素數量

也就是說,如果你要刪除的是一個非String 類型的key,這個key 的元素越多,那麼在執行DEL 時耗時就越久!

為什麼會這樣?

原因在於,刪除這種 key 時,Redis 需要依序釋放每個元素的內存,元素越多,這個過程就會越耗時。

而這麼長的操作耗時,勢必會阻塞整個 Redis 實例,影響 Redis 的效能。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

所以,當你在刪除List/Hash/Set/ZSet 類型的key 時,一定要格外注意,不能無腦執行DEL,而是應該用以下方式刪除:

  • 查詢元素數量:執行LLEN/HLEN/SCARD/ZCARD 指令

  • 判斷元素數量:如果元素數量較少,可直接執行DEL 刪除,否則分批刪除

  • 分批刪除:執行LRANGE/HSCAN/SSCAN/ZSCAN LPOP/RPOP/HDEL/SREM/ZREM 刪除

#了解了DEL 對於List/Hash/Set/ZSet 類型資料的影響,我們再來分析下,刪除一個String 類型的key 會不會有這種問題?

啊?前面不是提到,Redis 官方文件的描述,刪除 String 類型的 key,時間複雜度是 O(1) 麼?這不會導致 Redis 阻塞吧?

其實這也不一定!

你思考一下,如果這個 key 佔用的記憶體非常大呢?

例如,這個 key 儲存了 500MB 的資料(很明顯,它是一個 bigkey),那在執行 DEL 時,耗時依舊會變長!

這是因為,Redis 釋放這麼大的記憶體給作業系統,也是需要時間的,所以操作耗時也會變長。

所以,對於 String 類型來說,你最好也不要儲存過大的數據,否則在刪除它時,也會有效能問題。

此時,你可能會想:Redis 4.0 不是推出了 lazy-free 機制麼?開啟這個機制,釋放記憶體的操作會放到後台執行緒執行,那是不是就不會阻塞主執行緒了?

這個問題非常好。

真的會是這樣嗎?

這裡我先告訴你結論:即使Redis 打開了lazy-free,在刪除一個String 類型的bigkey 時,它仍舊是在主線程中處理,而不是放到後台線程中執行。所以,依舊有阻塞 Redis 的風險!

這是為什麼?

這裡先賣一個關子,有興趣的同學可以先自行查閱 lazy-free 相關資料尋找答案。 :)

其實,關於lazy-free 的知識點也很多,由於篇幅原因,所以我打算後面專門寫一篇文章來講,歡迎持續關注~

3) RANDOMKEY 竟然也會阻塞Redis?

如果你想隨機檢視 Redis 中的一個 key,通常會使用 RANDOMKEY 這個指令。

這個指令會從 Redis 中「隨機」取出一個 key。

既然是隨機,那這個執行速度一定非常快吧?

其實不然。

要解釋清楚這個問題,就要結合 Redis 的過期策略來講。

如果你對 Redis 的過期策略有所了解,應該知道 Redis 清理過期 key,是採用定時清理 懶惰清理 2 種方式結合來做的。

而 RANDOMKEY 在隨機拿出一個 key 後,首先會先檢查這個 key 是否已過期。

如果該 key 已經過期,那麼 Redis 會刪除它,這個過程就是懶惰清理

但清理完了還不能結束,Redis 還要找出一個「不過期」的 key,回傳給客戶端。

此時,Redis 則會繼續隨機拿出一個 key,然後再判斷是它否過期,直到找出一個未過期的 key 回傳給客戶端。

整個流程就是這樣的:

  • master 隨機取出一個key,判斷是否已過期

  • 如果key 已過期,刪除它,繼續隨機取key

  • 以此循環往復,直到找到一個不過期的key,返回

##但這裡就有一個問題了:

如果此時Redis 中,有大量key 已經過期,但還未來得及被清理掉,那這個循環就會持續很久才能結束,而且,這個耗時都花費在了清理過期key 尋找不過期key 上。

导致的结果就是,RANDOMKEY 执行耗时变长,影响 Redis 性能。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

以上流程,其实是在 master 上执行的。

如果在 slave 上执行 RANDOMEKY,那么问题会更严重!

为什么?

主要原因就在于,slave 自己是不会清理过期 key。

那 slave 什么时候删除过期 key 呢?

其实,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据一致性。

还是同样的场景:Redis 中存在大量已过期,但还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就会发生以下问题:

  • slave 随机取出一个 key,判断是否已过期

  • key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key

  • 由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入「死循环」!

也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死!

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

是不是没想到?在 slave 上随机拿一个 key,竟然有可能造成这么严重的后果?

这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复。

修复的解决方案是,在 slave 上执行 RANDOMKEY 时,会先判断整个实例所有 key 是否都设置了过期时间,如果是,为了避免长时间找不到符合条件的 key,slave 最多只会在哈希表中寻找 100 次,无论是否能找到,都会退出循环。

这个方案就是增加上了一个最大重试次数,这样一来,就避免了陷入死循环。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

虽然这个方案可以避免了 slave 陷入死循环、卡死整个实例的问题,但是,在 master 上执行这个命令时,依旧有概率导致耗时变长。

所以,你在使用 RANDOMKEY 时,如果发现 Redis 发生了「抖动」,很有可能是因为这个原因导致的!

4) O(1) 复杂度的 SETBIT,竟然会导致 Redis OOM?

在使用 Redis 的 String 类型时,除了直接写入一个字符串之外,还可以把它当做 bitmap 来用。

具体来讲就是,我们可以把一个 String 类型的 key,拆分成一个个 bit 来操作,就像下面这样:

127.0.0.1:6379> SETBIT testkey 10 1
(integer) 1
127.0.0.1:6379> GETBIT testkey 10
(integer) 1

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

其中,操作的每一个 bit 位叫做 offset。

但是,这里有一个坑,你需要注意起来。

如果这个 key 不存在,或者 key 的内存使用很小,此时你要操作的 offset 非常大,那么 Redis 就需要分配「更大的内存空间」,这个操作耗时就会变长,影响性能。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

所以,当你在使用 SETBIT 时,也一定要注意 offset 的大小,操作过大的 offset 也会引发 Redis 卡顿。

这种类型的 key,也是典型的 bigkey,除了分配内存影响性能之外,在删除它时,耗时同样也会变长。

5) 执行 MONITOR 也会导致 Redis OOM?

这个坑你肯定听说过很多次了。

当你在执行 MONITOR 命令时,Redis 会把每一条命令写到客户端的「输出缓冲区」中,然后客户端从这个缓冲区读取服务端返回的结果。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

但是,如果你的 Redis QPS 很高,这将会导致这个输出缓冲区内存持续增长,占用 Redis 大量的内存资源,如果恰好你的机器的内存资源不足,那 Redis 实例就会面临被 OOM 的风险。

所以,你需要謹慎使用 MONITOR,尤其在 QPS 很高的情況下。

以上這些問題場景,都是我們在使用常見指令時發生的,而且,很可能都是「無意」就會觸發的。

下面我們來看 Redis「資料持久化」都存在哪些坑?

資料持久化有哪些坑?

Redis 的資料持久化,分為 RDB 和 AOF 兩種方式。

其中,RDB 是資料快照,而 AOF 會記錄每一個寫入指令到日誌檔案。

在資料持久化方面發生問題,主要也集中在這兩大塊,我們依序來看。

1) master 宕機,slave 資料也遺失了?

如果你的Redis 採用以下模式部署,就會發生資料遺失的問題:

  • master-slave 哨兵部署實例

  • master 沒有開啟資料持久化功能

  • Redis 進程使用supervisor 管理,並配置為「進程宕機,自動重新啟動」

#如果此時master 宕機,就會導致下面的問題:

  • #master 宕機,哨兵還未發起切換,此時master 進程立即被supervisor 自動拉起

  • 但master 沒有開啟任何資料持久化,啟動後是一個「空」實例

  • 此時slave 為了與master 保持一致,它會自動「清空」實例中的所有數據,slave 也變成了一個「空」實例

看到了麼?在這個場景下,master / slave 的資料就全部遺失了。

這時,業務應用在存取Redis 時,發現快取中沒有任何數據,就會把請求全部打到後端資料庫上,這還會進一步引發「快取雪崩」,對業務影響非常大。

所以,你一定要避免這種情況發生,我給你的建議是:

  • #Redis 實例不使用進程管理工具自動拉起

  • master 宕機後,讓哨兵發起切換,把slave 提升為master

  • 切換完成後,再重啟master,讓其退化成slave

你在配置資料持久化時,要避免這個問題的發生。

2) AOF everysec 真的不會阻塞主執行緒嗎?

當 Redis 開啟 AOF 時,需要設定 AOF 的刷盤策略。

基於效能和資料安全的平衡,你一定會採用 appendfsync everysec 這種方案。

這個方案的工作模式為,Redis 的後台執行緒每間隔 1 秒,就把 AOF page cache 的數據,刷到磁碟(fsync)。

這種方案的優點在於,把 AOF 刷盤的耗時操作,放到了後台執行緒中去執行,避免了對主執行緒的影響。

但真的不會影響主執行緒嗎?

答案是否定的。

其實有這個場景:Redis 後台執行緒在執行 AOF page cache 刷盤(fysnc)時,如果此時磁碟 IO 負載過高,那麼呼叫 fsync 就會被阻塞住。

此時,主執行緒仍接收寫入請求進來,那麼此時的主執行緒會先判斷,上一次後台執行緒是否已刷盤成功。

如何判斷呢?

後台執行緒在刷盤成功後,都會記錄刷盤的時間。

主執行緒會根據這個時間來判斷,距離上次刷盤已經過了多久。整個流程是這樣的:

  • 主執行緒在寫 AOF page cache(write系統呼叫)前,先檢查後台 fsync 是否已完成?

  • fsync 已完成,主執行緒直接寫入 AOF page cache

  • fsync 未完成,則檢查距離上次 fsync 過去多久?

  • 如果距離上次fysnc 成功在2 秒內,那麼主執行緒會直接傳回,不寫AOF page cache

  • 如果距離上次fysnc 成功超過了2 秒,那主執行緒會強制寫AOF page cache(write系統呼叫)

  • 由於磁碟IO 負載過高,此時,後台執行緒fynsc 會發生阻塞,那主執行緒在寫AOF page cache 時,也會發生阻塞等待(操作同一個fd,fsync 和write 是互斥的,一方必須等另一方成功才可以繼續執行,否則阻塞等待)

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

#

透過分析我們可以發現,即使你設定的 AOF 刷盤策略是 appendfsync everysec,也依舊會有阻塞主執行緒的風險。

其實,產生這個問題的重點在於,磁碟 IO 負載過高導致 fynsc 阻塞,進而導致主執行緒寫 AOF page cache 也會發生阻塞。

所以,你一定要保證磁碟有充足的 IO 資源,避免這個問題。

3) AOF everysec 真的只會遺失 1 秒資料?

接著上面的問題繼續分析。

如上所述,這裡我們需要專注於上面的步驟 4。

也就是:主執行緒寫AOF page cache 時,會先判斷上一次fsync 成功的時間,如果距離上次fysnc 成功在2 秒內,那麼主執行緒會直接回傳,不再寫AOF page cache。

這意味著,後台執行緒在執行 fsync 刷盤時,主執行緒最多等待 2 秒不會寫 AOF page cache。

如果此時 Redis 發生了宕機,那麼,AOF 檔案中遺失是 2 秒的數據,而不是 1 秒!

我們繼續分析,Redis 主執行緒為什麼要等 2 秒不寫 AOF page cache 呢?

其實,Redis AOF 配置為 appendfsync everysec 時,正常來講,後台執行緒每隔 1 秒執行一次 fsync 刷盤,如果磁碟資源充足,是不會被阻塞住的。

也就是說,Redis 主執行緒其實根本不用關心後台執行緒是否刷盤成功,只要無腦寫 AOF page cache 即可。

但是,Redis 作者考慮到,如果此時的磁碟 IO 資源比較緊張,那麼後台執行緒 fsync 就有機率發生阻塞風險。

所以,Redis 作者在主執行緒寫AOF page cache 之前,先檢查距離上一次fsync 成功的時間,如果大於1 秒沒有成功,那麼主執行緒此時就能知道,fsync 可能阻塞了。

所以,主執行緒會等待2 秒不寫AOF page cache,目的在於:

  • 降低主執行緒阻塞的風險(如果無腦寫入AOF page cache ,主執行緒則會立即阻塞住)

  • 如果fsync 阻塞,主執行緒就會給後台執行緒留出1 秒的時間,等待fsync 成功

#但代價就是,如果此時發生宕機,AOF 遺失的就是2 秒的數據,而不是1 秒。

這個方案應該是 Redis 作者對效能和資料安全性的進一步權衡。

無論如何,這裡你只需要知道的是,即使 AOF 配置為每秒刷盤,在發生上述極端情況時,AOF 遺失的資料其實是 2 秒。

4) RDB 和 AOF rewrite 時,Redis 發生 OOM?

最後,我們來看一下,當 Redis 在執行 RDB 快照和 AOF rewrite 時,會發生的問題。

Redis 在做 RDB 快照和 AOF rewrite 時,會採用創建子程序的方式,把實例中的資料持久化到磁碟上。

建立子程序,會呼叫作業系統的 fork 函數。

fork 執行完成後,父行程和子行程會同時共用同一份記憶體資料。

但此時的主進程依舊是可以接收寫入請求的,而進來的寫入請求,會採用 Copy On Write(寫時複製)的方式操作記憶體資料。

也就是說,主程序一旦有數據需要修改,Redis 並不會直接修改現有記憶體中的數據,而是先將這塊記憶體資料拷貝出來,再修改這塊新記憶體的數據,這就是所謂的「寫時複製」。

寫時複製你也可以理解成,誰需要發生寫入操作,誰就先拷貝,再修改。

你應該發現了,如果父進程要修改一個 key,就需要拷貝原有的內存數據,到新內存中,這個過程涉及到了“新內存”的申請。

如果你的業務特點是「寫多讀少」,而且 OPS 非常高,那在 RDB 和 AOF rewrite 期間,就會產生大量的記憶體拷貝工作。

這會有什麼問題呢?

因為寫入請求很多,這會導致 Redis 父進程會申請非常多的記憶體。在這段期間,修改 key 的範圍越廣,新記憶體的申請就越多。

如果你的機器記憶體資源不足,這就會導致 Redis 面臨被 OOM 的風險!

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

这就是你会从 DBA 同学那里听到的,要给 Redis 机器预留内存的原因。

其目的就是避免在 RDB 和 AOF rewrite 期间,防止 Redis OOM。

以上这些,就是「数据持久化」会遇到的坑,你踩到过几个?

下面我们再来看「主从复制」会存在哪些问题。

主从复制有哪些坑?

Redis 为了保证高可用,提供了主从复制的方式,这样就可以保证 Redis 有多个「副本」,当主库宕机后,我们依旧有从库可以使用。

在主从同步期间,依旧存在很多坑,我们依次来看。

1) 主从复制会丢数据吗?

首先,你需要知道,Redis 的主从复制是采用「异步」的方式进行的。

这就意味着,如果 master 突然宕机,可能存在有部分数据还未同步到 slave 的情况发生。

这会导致什么问题呢?

如果你把 Redis 当做纯缓存来使用,那对业务来说没有什么影响。

master 未同步到 slave 的数据,业务应用可以从后端数据库中重新查询到。

但是,对于把 Redis 当做数据库,或是当做分布式锁来使用的业务,有可能因为异步复制的问题,导致数据丢失 / 锁丢失。

关于 Redis 分布式锁可靠性的更多细节,这里先不展开,后面会单独写一篇文章详细剖析这个知识点。这里你只需要先知道,Redis 主从复制是有概率发生数据丢失的。

2) 同样命令查询一个 key,主从库却返回不同的结果?

不知道你是否思考过这样一个问题:如果一个 key 已过期,但这个 key 还未被 master 清理,此时在 slave 上查询这个 key,会返回什么结果呢?

  • slave 正常返回 key 的值

  • slave 返回 NULL

你认为是哪一种?可以思考一下。

答案是:不一定

嗯?为什么会不一定?

这个问题非常有意思,请跟紧我的思路,我会带你一步步分析其中的原因。

其实,返回什么结果,这要取决于以下 3 个因素:

  1. Redis 的版本

  2. 具体执行的命令

  3. 机器时钟

先来看 Redis 版本。

如果你使用的是 Redis 3.2 以下版本,只要这个 key 还未被 master 清理,那么,在 slave 上查询这个 key,它会永远返回 value 给你。

也就是说,即使这个 key 已过期,在 slave 上依旧可以查询到这个 key。

// Redis 2.8 版本 在 slave 上执行
127.0.0.1:6479> TTL testkey
(integer) -2    // 已过期
127.0.0.1:6479> GET testkey
"testval"       // 还能查询到!

但如果此时在 master 上查询这个 key,发现已经过期,就会把它清理掉,然后返回 NULL。

// Redis 2.8 版本 在 master 上执行
127.0.0.1:6379> TTL testkey
(integer) -2
127.0.0.1:6379> GET testkey
(nil)

发现了吗?在 master 和 slave 上查询同一个 key,结果竟然不一样?

其实,slave 应该要与 master 保持一致,key 已过期,就应该给客户端返回 NULL,而不是还正常返回 key 的值。

为什么会发生这种情况?

其实这是 Redis 的一个 Bug:3.2 以下版本的 Redis,在 slave 上查询一个 key 时,并不会判断这个 key 是否已过期,而是直接无脑返回给客户端结果。

这个 Bug 在 3.2 版本进行了修复,但是,它修复得「不够彻底」。

什么叫修复得「不够彻底」?

这就要结合前面提到的,第 2 个影响因素「具体执行的命令」来解释了。

Redis 3.2 虽然修复了这个 Bug,但却遗漏了一个命令:EXISTS

也就是说,一个 key 已过期,在 slave 直接查询它的数据,例如执行 GET/LRANGE/HGETALL/SMEMBERS/ZRANGE 这类命令时,slave 会返回 NULL。

但如果执行的是 EXISTS,slave 依旧会返回:key 还存在

// Redis 3.2 版本 在 slave 上执行
127.0.0.1:6479> GET testkey
(nil)           // key 已逻辑过期
127.0.0.1:6479> EXISTS testkey
(integer) 1     // 还存在!

原因在于,EXISTS 与查询数据的命令,使用的不是同一个方法。

Redis 作者只在查询数据时增加了过期时间的校验,但 EXISTS 命令依旧没有这么做。

直到 Redis 4.0.11 这个版本,Redis 才真正把这个遗漏的 Bug 完全修复。

如果你使用的是这个之上的版本,那在 slave 上执行数据查询或 EXISTS,对于已过期的 key,就都会返回「不存在」了。

这里我们先小结一下,slave 查询过期 key,经历了 3 个阶段:

  1. 3.2 以下版本,key 过期未被清理,无论哪个命令,查询 slave,均正常返回 value

  2. 3.2 - 4.0.11 版本,查询数据返回 NULL,但 EXISTS 依旧返回 true

  3. 4.0.11 以上版本,所有命令均已修复,过期 key 在 slave 上查询,均返回「不存在」

这里要特别鸣谢《Redis开发与运维》的作者,付磊。

这个问题我是在他的文章中看到的,感觉非常有趣,原来 Redis 之前还存在这样的 Bug 。随后我又查阅了相关源码,并对逻辑进行了梳理,在这里才写成文章分享给大家。

虽然已在微信中亲自答谢,但在这里再次表达对他的谢意~

最后,我们来看影响查询结果的第 3 个因素:「机器时钟」。

假设我们已规避了上面提到的版本 Bug,例如,我们使用 Redis 5.0 版本,在 slave 查询一个 key,还会和 master 结果不同吗?

答案是,还是有可能会的。

这就与 master / slave 的机器时钟有关了。

无论是 master 还是 slave,在判断一个 key 是否过期时,都是基于「本机时钟」来判断的。

如果 slave 的机器时钟比 master 走得「快」,那就会导致,即使这个 key 还未过期,但以 slave 上视角来看,这个 key 其实已经过期了,那客户端在 slave 上查询时,就会返回 NULL。

是不是很有意思?一个小小的过期 key,竟然藏匿这么多猫腻。

如果你也遇到了类似的情况,就可以通过上述步骤进行排查,确认是否踩到了这个坑。

3) 主从切换会导致缓存雪崩?

这个问题是上一个问题的延伸。

我们假设,slave 的机器时钟比 master 走得「快」,而且是「快很多」。

此时,从 slave 角度来看,Redis 中的数据存在「大量过期」。

如果此时操作「主从切换」,把 slave 提升为新的 master。

它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果:

  • master 大量清理过期 key,主线程发生阻塞,无法及时处理客户端请求

  • Redis 中数据大量过期,引发缓存雪崩

你看,当 master / slave 机器时钟严重不一致时,对业务的影响非常大!

所以,如果你是 DBA 运维,一定要保证主从库的机器时钟一致性,避免发生这些问题。

4) master / slave 大量数据不一致?

还有一种场景,会导致 master / slave 的数据存在大量不一致。

这就涉及到 Redis 的 maxmemory 配置了。

Redis 的 maxmemory 可以控制整个实例的内存使用上限,超过这个上限,并且配置了淘汰策略,那么实例就开始淘汰数据。

但这里有个问题:假设 master / slave 配置的 maxmemory 不一样,那此时就会发生数据不一致。

例如,master 配置的 maxmemory 为 5G,而 slave 的 maxmemory 为 3G,当 Redis 中的数据超过 3G 时,slave 就会「提前」开始淘汰数据,此时主从库数据发生不一致。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

另外,儘管master / slave 設定的maxmemory 相同,如果你要調整它們的上限,也要格外注意,否則也會導致slave 淘汰資料:

  • ##調大maxmemory時,先調整slave,再調整master

  • 調小maxmemory 時,先調整master,再調整slave

  • ##以方式操作,就避免了slave 提前超過maxmemory 的問題。

其實,你可以思考一下,發生這些問題的關鍵在哪?

其根本原因在於,

slave 超過 maxmemory 後,會「自行」淘汰資料

如果不讓 slave 自己淘汰數據,那這些問題是不是都可以規避了?

沒錯。

針對這個問題,Redis 官方應該也收到了很多用戶的回饋。在 Redis 5.0 版本,官方終於把這個問題徹底解決了!

Redis 5.0 增加了一個配置項目:replica-ignore-maxmemory,預設 yes。

這個參數表示,儘管 slave 記憶體超過了 maxmemory,也不會自己淘汰資料了!

這樣一來,slave 永遠會向 master 看齊,只會老老實實地複製 master 發送過來的數據,不會自己再搞「小動作」。

至此,master / slave 的資料就可以保證完全一致了!

如果你使用的剛好是 5.0 版本,就不用擔心這個問題了。

5) slave 竟然會有記憶體外洩問題?

是的,你沒看錯。

這是怎麼發生的?我們具體來看一下。

當你在使用Redis 時,符合以下場景,就會觸發slave 記憶體外洩:

    Redis 使用的是4.0 以下版本
  • #slave 設定項目為read-only=no(從函式庫可寫)
  • #向slave 寫入了有過期時間的key
  • 這時的slave 就會發生記憶體洩漏:
slave 中的key,即使到了過期時間,也不會自動清理。

如果你不主動刪除它,那麼這些 key 就會一直殘留在 slave 記憶體中,消耗 slave 的記憶體。

最麻煩的是,你用指令來查詢這些 key,卻還查不到任何結果!

這就 slave 「記憶體外洩」問題。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !這其實也是 Redis 的一個 Bug,Redis 4.0 才修復了這個問題。

解決方案是,

在可寫入的 slave 上,寫入有過期時間 key 時,slave 會「記錄」下來這些 key。

然後 slave 會定時掃描這些 key,如果到達過期時間,則清理之。

如果你的業務需要在 slave 上暫時儲存數據,而這些 key 也都設定了過期時間,那麼就要注意這個問題了。

你需要確認你的 Redis 版本,如果是 4.0 以下版本,一定要避免踩踏這個坑。

其實,最好的方案是,制定一個Redis 使用規範,slave 必須強制設定為read-only,不允許寫,這樣不僅可以保證master / slave 的資料一致性,還避免了slave 內存洩漏問題。

6) 為什麼主從全量同步一直失敗?

當主從全量同步時,你可能會遇到同步失敗的問題,具體場景如下:

slave 向master 發起全量同步請求,master 生成RDB 後發給slave,slave 載入RDB。

由於 RDB 資料太大,slave 載入耗時也會變得很長。

此時你會發現,slave 載入 RDB 還沒完成,master 和 slave 的連線卻斷了,資料同步也失敗了。

之後你又會發現,slave 又啟動了全量同步,master 又產生 RDB 傳送給 slave。

同樣地,slave 在載入 RDB 時,master / slave 同步又失敗了,以此往復。

這是怎麼回事?

其實,這就是 Redis 的「複製風暴」問題。

什麼是複製風暴?

就像剛才描述的:主從全量同步失敗,又重新開始同步,之後又同步失敗,以此往復,惡性循環,持續浪費機器資源。

為什麼會導致這種問題呢?

如果你的Redis 有以下特點,就有可能發生這種問題:

  • master 的實例資料過大,slave 在載入RDB 時耗時太長

  • 複製緩衝區(slave client-output-buffer-limit)配置過小

  • master 寫入請求量很大

主從在全量同步資料時,master 接收的寫入請求,會先寫到主從「複製緩衝區」中,這個緩衝區的「上限」是設定決定的。

當 slave 載入 RDB 太慢時,就會導致 slave 無法及時讀取「複製緩衝區」的數據,這就引發了複製緩衝區「溢位」。

為了避免記憶體持續成長,此時的 master 會「強制」斷開 slave 的連接,這時全量同步就會失敗。在

之後,同步失敗的 slave 又會「重新」發起全量同步,進而陷入上面描述的問題中,以此往復,惡性循環,這就是所謂的「複製風暴」。

如何解決這個問題呢?我給你以下幾點建議:

  • Redis 實例不要太大,避免過大的RDB

  • 複製緩衝區配置的盡量大有些,給slave 載入RDB 留足時間,降低全量同步失敗的機率

如果你也踩到了這個坑,可以透過這個方案來解決。

總結

好了,總結一下,這篇文章我們主要講了Redis 在「指令使用」、「資料持久化」、「主從同步」 3 個方面可能存在的「坑」。

怎麼樣?有沒有顛覆你的認知呢?

這篇文章資訊量還是比較大的,如果你現在的思維已經有些「凌亂」了,別急,我也給你準備好了心智圖,方便你更好地理解和記憶。

使用Redis可能遇到的15個坑,快來收藏避雷吧! !

希望你在使用 Redis 時,可以提前規避這些坑,讓 Redis 更好地提供服務。

後記

最後,我想和你聊一聊在開發過程中,關於踩坑的經驗和心得。

其實,接觸任何一個新領域,都會經歷陌生、熟悉、踩坑、吸收經驗、游刃有餘這幾個階段。

那在踩坑這個階段,如何少踩坑?或者踩坑後如何高效率地排除問題呢?

這裡我總結出了4 個方面,應該可以幫助到你:

1) 多看官方文件設定檔的註解

一定要多看官方文檔,以及設定檔的註釋說明。其實很多可能有風險的地方,優秀的軟體會在文件和註解裡提示你的,認真讀一讀,可以提前規避很多基礎問題。

2) 不放過疑問細節,多思考為什麼?

永遠保持好奇心。遇到問題,掌握剝絲抽繭,逐步定位問題的能力,時時保持探索事物問題本質的心態。

3) 敢於提出質疑,源碼不會騙人

如果你覺得一個問題很蹊蹺,可能是一個 Bug,要敢於提出質疑。

透過原始碼尋找問題的真相,這種方式要好過你看一百篇網路上互相抄襲的文章(抄來抄去很有可能都是錯的)。

4) 沒有完美的軟體,優秀軟體都是一步一步迭代出來的

任何優秀的軟體,都是一步步迭代出來的。在迭代過程中,有 Bug 很正常,我們需要抱著正確的心態去看待它。

這些經驗和心得,適用於學習任何領域,希望對你有幫助。

更多程式相關知識,請造訪:程式設計教學! !

以上是使用Redis可能遇到的15個坑,快來收藏避雷吧! !的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除