【Redis 开发与运维】总结篇
文章目錄
- Redis 是什么?簡述它的優缺點?
- Redis 為什么這么快?
- Redis 相比 memcached 有哪些優勢?
- 說說 Redis 的數據類型和使用場景
- 如何通過 Redis 實現分布式鎖
- 如何使用 Redis 做異步隊列
- 使用 Pipeline 的好處
- Redis 過期鍵的刪除策略?
- Redis 內存淘汰機制?
- Redis 持久化機制
- 說說緩存設計中會出現的問題和解決方案
- 高并發場景下,如何解決數據庫與緩存雙寫的時候數據不一致的情況?
- Redis 中跳躍表 skiplist 實現原理
- Redis 為什么用 skiplist 而不用紅黑樹?
- 假如 Redis 里面有 1 億個key,其中有 10w 個 key 是以某個固定的已知前綴開頭的,如何將它們全部找出來?
- Redis6.0 為什么要引入多線程?
- Redis 集群方案應該怎么做?都有哪些方案?
- 講一講位圖 Bitmap
- 講一講 HyperLogLog
- Redis 單線程如何處理那么多的并發客戶端連接?
- 布隆過濾器的原理
- 慢查詢場景有哪些?
- 如何保證 redis/mysql 的一致性?
Redis 是什么?簡述它的優缺點?
- Redis 本質上是一個 Key-Value 類型的內存數據庫,很像 memcached,整個數據庫統統加載在內存當中進行操作,定期通過異步操作把數據庫數據 flush 到硬盤上進行保存。
- 因為是純內存操作,Redis 的性能非常出色,每秒可以處理超過 10 萬次讀寫操作,是已知性能最快的 Key-Value DB。
- Redis 的出色之處不僅僅是性能,Redis 最大的魅力是支持保存多種數據結構,此外單個 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的數據,因此 Redis 可以用來實現很多有用的功能。
- Redis 的主要缺點是數據庫容量受到物理內存的限制,不能用作海量數據的高性能讀寫,因此 Redis 適合的場景主要局限在較小數據量的高性能操作和運算上。
Redis 為什么這么快?
- 純內存訪問,Redis 將所有數據都放在內存中;
- 采用的是基于非阻塞的 IO 多路復用機制,避免了網絡 IO 上浪費過多時間;
- 單線程避免了線程切換和競態產生的消耗。
Redis 相比 memcached 有哪些優勢?
- 數據類型:Redis 支持更豐富的數據類型(支持更復雜的應用場景),Redis 不僅僅支持簡單的 key/value 類型的數據,同時還提供 list,set,zset,hash 等數據結構的存儲。memcache 只支持簡單的數據類型 String。
- 持久化:Redis 支持數據的持久化,可以將內存中的數據保持在磁盤中,重啟的時候可以再次加載進行使用,而 Memecache 把數據全部存在內存之中。
- 集群模式:memcached 沒有原生的集群模式,需要依靠客戶端來實現往集群中分片寫入數據;而 Redis 原生支持集群模式。
- 線程模型:Memcached 是多線程,非阻塞 IO 復用的網絡模型;Redis 使用單線程的多路 IO 復用模型。
說說 Redis 的數據類型和使用場景
Redis 支持五種數據類型:string( 字符串),hash( 哈希),list( 列表),set( 集合)及 zset(sorted set:有序集合)。
- string:redis 中字符串 value 最大可為 512M。可以用來做一些計數功能的緩存(也是實際工作中最常見的)。常規計數:微博數,粉絲數等。
- hash:鍵值對集合,是一個字符串類型的 Key 和 Value 的映射表,也就是說其存儲的 Value 是一個鍵值對(Key- Value),hash 特別適合用于存儲對象,后續操作的時候,可以直接僅僅修改這個對象中的某個字段的值。 比如我們可以 hash 數據結構來存儲用戶信息,商品信息等等。
- list:簡單的字符串列表,按照插入順序排序,可以添加一個元素到列表的頭部(左邊)或者尾部(右邊),Redis list 的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷。可以實現一個簡單消息隊列功能,做基于 redis 的分頁功能等。還可以通過 lrange 命令做(微博、朋友圈)文章列表分頁,就是從某個元素開始讀取多少個元素,可以基于 list 實現高性能分頁。
- set:是一個字符串類型的無序集合。可以用來進行全局去重等,比如:在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合,Redis 可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能,這個過程也就是求交集的過程。
- sorted set:是一個字符串類型的有序集合,給每一個元素一個固定的分數 score 來保持順序。可以用來做排行榜應用或者進行范圍查找等。
我們實際項目中比較常用的是 string、hash。 如果你是 Redis 的高級用戶,還需要加上下面幾種數據結構 HyperLogLog、Geo、Pub/Sub。
如果你說還玩過 Redis Module,像 BloomFilter,RedisSearch,Redis-ML,面試官得眼睛就開始發亮了。
詳情請參考:《【Redis 開發與運維】API 的理解和使用》
如何通過 Redis 實現分布式鎖
分布式鎖需要解決的問題
- 互斥性
- 安全性
- 死鎖
- 容錯
利用 SETNX key value:如果 key 不存在,則創建并賦值;設置成功,返回 1。設置失敗,返回 0。
問題一:如何解決 SETNX 長期有效的問題
- EXPIRE key seconds:設置 key 的生存時間,當 key 過期時(生存時間為0),會自動被刪除
- 缺點:原子性得不到滿足,因為 SETNX 和 EXPIRE 命令是兩個操作不滿足原子性。
答:可以利用:SET key value [EX seconds] [PX milliseconds] [NX|XX]
- Ex second:設置鍵的過期時間為 second 秒
- PX milliseconds:設置鍵的過期時間為 millisecond 毫秒
- NX:只在鍵不存在時,才對鍵進行設置操作
- XX:只在鍵已經存在時,才對鍵進行設置操作
- SET 操作成功完成時,返回 OK,否則返回 nil
- 例如:set lock 12345 ex 10 nx,表示設置了一個 key 為 lock 的鎖,鎖的過期時間為 10 秒
問題二:怎么處理別人把我的鎖釋放(刪除)的問題
- 因為鎖的釋放無非就是刪除掉,那個 key 嘛,刪了鎖就釋放了。
答:
- 可以把鎖的值設為用戶 ID,刪除之前比對用戶 ID 和鎖的值是否相等,只有相等才能刪除。
- 因為判斷鎖相等和刪除鎖是兩個操作,不滿足原子性,于是可以利用 Lua 做到原子性。例如:
大量的 key 同時過期的注意事項
- 集中過期,由于清除大量的 key 很耗時,會出現短暫的卡頓現象
- 解決方案:在設置 key 的過期時間的時候,給每個 key 加上隨機值
問題三:鎖過期時間到了業務沒執行完怎么辦?
- 默認情況下,加鎖的時間是 30 秒,如果加鎖的業務沒有執行完,那么到 30-10 = 20 秒的時候,就會進行一次續期,把鎖重置成 30 秒,那這個時候可能又有同學問了,那業務的機器萬一宕機了呢?宕機了定時任務跑不了,就續不了期,那自然30秒之后鎖就解開了唄。
如何使用 Redis 做異步隊列
使用 Redis 的 List 作為隊列,RPUSH 生產消息,LPOP 消費消息
- 缺點:沒有等待隊列里有值就直接消費
- 彌補:可以通過在應用層引入 Sleep 機制去調用 LPOP 重試
BLPOP key [key ...] timeout:阻塞直到隊列有消息或者超時
- 缺點:只能供一個消費者消費
pub/sub:主題訂閱者模式
- 發送者(pub)發送消息,如:publish myTopic hello,表示向 myTopic 發布消息 hello
- 訂閱者(sub)接收消息,如:subscribe myTopic,表示訂閱 myTopic 的消息
- 訂閱者可以訂閱任意數量的頻道
缺點:消息的發布是無狀態的,無法保證可達,消息還是即發即失的
使用 Pipeline 的好處
- Redis 提供了批量操作命令(例如 mget、mset 等),有效地節約 RTT(Round Trip Time,往返時間)。但大部分命令是不支持批量操作的,例如要執行 n 次 hset 命令,并沒有 mhset 命令存在,需要消耗 n 次 RTT。
- Pipeline(流水線)機制能改善上面這類問題,它能將一組 Redis 命令進行組裝,通過一次 RTT 傳輸給 Redis,再將這組 Redis 命令的執行結果按順序返回給客戶端。
詳情請參考:Redis 一些超好用的功能特性
Redis 過期鍵的刪除策略?
- 定時刪除:超時時間到達時,刪除
- 惰性刪除:再次訪問過期數據時,刪除
- 定期刪除:每隔一定周期,刪除
- 對于定時刪除:由于數據庫可能同時接受成千上萬個用戶的訪問,那么可能有大量的 key 需要刪除,如果我們為每一個 key 的超時時間都設置一個定時器,每次超時就進行刪除操作,那么會導致系統性能非常低。
- 對于惰性刪除:如果一個過期 key 長期沒有被訪問,那么該 key-value 對將會一直存儲在數據庫中,會一直占有內存。而 redis 又是一個基于內存的數據庫,這樣很容易導致內存被耗盡。
- 對于定期刪除 :redis 難以確定執行刪除操作的時長和頻率
- 因此 redis 采用惰性刪除和定期刪除相結合的方式,來刪除系統中的過期鍵
Redis 內存淘汰機制?
- Redis v4.0 前提供 6 種數據淘汰策略:
- volatile-lru:利用 LRU 算法移除設置過期時間的 key(LRU:最近最少使用,Least Recently Used )
- volatile-ttl:從已設置過期時間的數據集中挑選將要過期的數據淘汰
- volatile-random:從已設置過期時間的數據集中任意選擇數據淘汰
- allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
- allkeys-random:當內存不足以容納新寫入數據時,從數據集中任意選擇數據淘汰
- no-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。
- Redis v4.0 后增加以下 2 種:
- volatile-lfu:從已設置過期時間的數據集中挑選最不經常使用的數據淘汰(LFU,Least Frequently Used)算法,也就是最頻繁被訪問的數據將來最有可能被訪問到。
- allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的 key。
Redis 持久化機制
詳情請參考:《【Redis 開發與運維】RDB 和 AOF 數據持久化》
說說緩存設計中會出現的問題和解決方案
詳情請參考:《【Redis 開發與運維】緩存設計》
高并發場景下,如何解決數據庫與緩存雙寫的時候數據不一致的情況?
Redis 中跳躍表 skiplist 實現原理
跳表(skiplist)是一個特殊的鏈表,相比一般的鏈表,有更高的查找效率,其效率可比擬于二叉查找樹。
跳表的性質:
- 由很多層結構組成
- 每一層都是一個有序的鏈表
- 最底層(Level 1)的鏈表包含所有元素
- 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現
- 每個節點包含兩個指針,一個指向同一鏈表中的下一個元素,一個指向下面一層的元素。
隨機層數的設計:
- Redis 使用隨機層數,解決插入、刪除時,時間復雜度重新蛻化成 O(n) 的問題
- 它不要求上下相鄰兩層鏈表之間的節點個數有嚴格的對應關系,而是為每個節點隨機出一個層數(level)。比如,一個節點隨機出的層數是 3,那么就把它鏈入到第 1 層到第 3 層這三層鏈表中。
隨機層數的計算方式:
- 執行插入操作時計算隨機數的過程,是一個很關鍵的過程,它對 skiplist 的統計特性有著很重要的影響。這并不是一個普通的服從均勻分布的隨機數,它的計算過程如下:
- 首先,每個節點肯定都有第 1 層指針(每個節點都在第 1 層鏈表里)。
- 如果一個節點有第 i 層(i >= 1)指針(即節點已經在第 1 層到第 i 層鏈表中),那么它有第(i + 1)層指針的概率為 p。
- 節點最大的層數不允許超過一個最大值,記為 MaxLevel。
- 這個計算隨機層數的偽碼如下所示:
- randomLevel() 的偽碼中包含兩個參數,一個是 p,一個是 MaxLevel。在 Redis 的 skiplist 實現中,這兩個參數的取值為:
- 所以,根據前面 randomLevel() 的偽碼,我們很容易看出,產生越高的節點層數,概率越低。
- 當 skiplist 中有 n 個節點的時候,它的總層數的概率均值是多少。這個問題直觀上比較好理解。根據節點的層數隨機算法,容易得出:
- 第 1 層鏈表固定有 n 個節點;
- 第 2 層鏈表平均有 n*p 個節點;
- 第 3 層鏈表平均有 n*p*p 個節點;
Redis 為什么用 skiplist 而不用紅黑樹?
- 插入、刪除、查找以及迭代輸出有序序列這幾個操作,紅黑樹也可以完成,時間復雜度跟跳表是一樣的。但是,按照區間來查找數據這個操作,紅黑樹的效率沒有跳表高,跳表可以做到 O(logn) 的時間復雜度;
- 比起紅黑樹來說,跳表更容易代碼實現;
- 跳表更加靈活,它可以通過改變索引構建策略有效平衡執行效率和內存消耗。
假如 Redis 里面有 1 億個key,其中有 10w 個 key 是以某個固定的已知前綴開頭的,如何將它們全部找出來?
使用 keys 指令可以掃出指定模式的 key 列表。
追問:如果這個 redis 正在給線上的業務提供服務, 那使用 keys 指令會有什么問題?
keys 指令會導致線程阻塞一段時間, 線上服務會停頓, 直到指令執行完畢, 服務才能恢復。
可以使用 SCAN cusor [MATCH patten] [COUNT count],每次只返回少量元素
- 基于游標的迭代器,需要基于上一次的游標延續之前的迭代過程
- 以 0 作為游標開始一次新的迭代,直到命令返回游標 0 完成一次遍歷,記得將上次返回的游標作為下次 SCAN 的游標
- 不保證每次執行都返回某個給定數量的元素,只能是大概率符合 count 參數,支持模糊查詢
- 可能會獲取到重復 key 的問題,需要在客戶端進行去重,比如用 HashSet 接收
- 如:scan 0 match k1* count 10,表示將鍵為 k1 開頭的從 0 開始的游標數 10 個(10 個以內,不一定是 10 個)返回
Redis6.0 為什么要引入多線程?
- Redis6.0 的多線程是指,將網絡數據讀寫和協議解析通過多線程的方式來處理,對于命令執行來說,仍然使用單線程操作。也就是說,Redis6.0 的多線程是為了解決其網絡 IO 的瓶頸。
Redis 集群方案應該怎么做?都有哪些方案?
詳情請參考:《【Redis 開發與運維】Redis Cluster 集群》
講一講位圖 Bitmap
詳情請參考:【Redis 開發與運維】小功能大用處
講一講 HyperLogLog
HyperLogLog,它是 LogLog 算法的升級版,作用是能夠提供不精確的去重計數。存在以下的特點:
- 能夠使用極少的內存來統計巨量的數據,在 Redis 中實現的 HyperLogLog,只需要 12K 內存就能統計 2^64 個數據
- 計數存在一定的誤差,誤差率整體較低,標準誤差為 0.81%
- 誤差可以被設置輔助計算因子進行降低
為什么用 HyperLogLog,例如以下場景:
統計 APP 或網頁的一個頁面,每天有多少用戶點擊進入的次數,同一個用戶的反復點擊進入記為 1 次。
這種場景用 HashMap 這種數據結構也可以,但是假設 APP 中日活用戶達到百萬或千萬以上級別的話,采用 HashMap 的做法就會導致程序中占用大量的內存。
Redis 單線程如何處理那么多的并發客戶端連接?
因為 Redis 的線程模型:基于非阻塞的 IO 多路復用機制。多路指的是多個 Socket 連接,復用指的是復用一個線程。
Redis 采用 IO 多路復用機制同時監聽多個 Socket,多個 Socket 會產生不同的事件,不同的事件對應著不同的操作,當這些 Socket 產生了事件,IO 多路復用程序會將這些事件放到一個隊列中,通過這個隊列,以有序、同步、每次一個事件的方式向文件時間分派器中傳送。當事件處理器處理完一個事件后,IO 多路復用程序才會繼續向文件分派器傳送下一個事件。
布隆過濾器的原理
首先需要 k 個 hash 函數,每個函數可以把 key 散列成為 1 個整數;
初始化時,需要一個長度為 n 比特的數組,每個比特位初始化為 0;
某個 key 加入集合時,用 k 個 hash 函數計算出 k 個散列值,并把數組中對應的比特位置為 1;
判斷某個 key 是否在集合時,用 k 個 hash 函數計算出 k 個散列值,并查詢數組中對應的比特位,如果所有的比特位都是 1,則命中,但不一定在集合中,如果存在比特位為 0,則未命中,一定不在集合中。
優點:不需要存儲 key,節省空間
缺點:
- 算法判斷 key 在集合中時,有一定的概率 key 其實不在集合中
- 無法刪除
慢查詢場景有哪些?
- 使用 O(n) 復雜度的命令,比如 keys *
- 大對象的查詢也有可能造成慢查詢
- 關于阻塞的原因,請參考:Redis 的噩夢:阻塞
如何保證 redis/mysql 的一致性?
巨人的肩膀:https://github.com/cosen1024/Java-Interview/blob/main/Redis/Redis.md
總結
以上是生活随笔為你收集整理的【Redis 开发与运维】总结篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解2008年日全食全过程
- 下一篇: Redis开发与运维读书笔记