語雀完整版:
https://www.yuque.com/g/mingrun/embiys/calwqx/collaborator/join?token=sLcLnqz5Rv8hOKEB&source=doc_collaborator# 《Redis筆記》
Redis
一般問題
- Redis內存模型(I/O多路模型)多路復用IO如何解釋
-
- 為什么Redis要使用單線程?單線程為什么還如此之快?
首先官方解釋說 redis是基于內存的操作,CPU不是redis的瓶頸,限制redis性能的應該是內存的大小和網絡寬帶。 另外 使用單線程能避免線程切換和競態消耗。
- 為什么Redis要使用單線程?單線程為什么還如此之快?
- 高級數據類型(注意bitmaps 在敏感詞過濾中的應用)
- 刪除策略
-
- 定時刪除:創建一個定時器,時間一到立刻刪除,就是對cpu不太友好這樣
-
- 惰性刪除:會在查詢的時候檢查是否過期如果過期則刪除。
-
- 定期刪除:每隔一段時間檢查設置了過期時間Key,如果過期了則刪除(是上面兩種方案的折中)
上面的刪除機制有可能導致內存占滿,然后就要走redis的內存刪除機制,
另外還有以下情況會對過期鍵進行處理
-
- 新生成RDB文件的時候 會檢查過期key,不會生成
-
- 生成AOF的時候,會檢查過期key,如果過期了就會增加一個DEL指令,記錄一下、
-
- 主庫過期會向從庫發送 DEL指令,記錄改鍵已經刪除
- Redis淘汰策略(也叫逐出算法,內存不夠時觸發,它和刪除策略 保證了redis中都是熱點數據)
注:其中的lru是指淘汰最久未使用的,lfu是指淘汰使用次數最少的
- 緩存淘汰的三種策略
-
- FIFO
-
-
- 先進先出,可以定義一個雙向鏈表,如果添加數據時滿了,就從隊頭開始清理
-
-
- LRU
-
-
- 淘汰的是使用次數最少的,可以定義一個隊列,新來的放到隊尾,訪問一次就把它的引用計數+1 ,容量不夠的時候就從隊尾開始清理
-
-
- LFU
-
-
- 剛被訪問的數據放到隊頭,插入時容量不夠的話從隊尾開始淘汰
-
-
- 上述算法自己如何實現?
- Key值命名規范
- Redis事務
-
- 概述:redis事務就是 將多個命令包裝成一個整體,作為一個隊列,中間不會被打斷
-
- 使用:開啟
multi
,結束exec
- 使用:開啟
-
- 注意點:中間執行失敗了,比如命令沒有敲對,是不會回滾的
- 生產問題
-
- 緩存雪崩
-
-
- 誘因:在一個較短的時間內,大量key過期失效,大量請求無法命中緩存
-
-
-
- 解決:把過期時間設置的隨機一些
-
-
- 緩存擊穿
-
-
- 誘因:單個key過期了,然后大量請求到這個key上
-
-
-
- 解決:
-
-
-
-
- 過期時間設置長一些、二級緩存、把這個key對應的value先設置上一個值
-
-
-
-
-
- 在getKey的時候,使用setnx加個分布式鎖,然后從數據庫里面把這個key的值讀出來,放到redis里面,然后再釋放鎖
-
-
-
- 緩存穿透
-
-
- 誘因:大量的請求沒有命中緩存,直接走數據庫了(遇到了攻擊)
-
-
-
- 解決
-
-
-
-
- 把這個key對應的value先設置上一個值,并設置上較短的過期時間
-
-
-
-
-
- 用布隆過濾器把所有可能存在的key放進去,如果請求過來key不存在就直接過濾掉(布隆過濾器由于哈希沖突,就是別的字段的值會把你的覆蓋掉,所以有不一定有,沒有的就一定沒有)
-
-
- Redis高可用
-
- 主從模式:若master宕機需要手動配置slave轉為master,可以設置一主多從,主寫從讀,提高吞吐量
-
- 哨兵模式,該模式下有一個哨兵監視master和slave,若master宕機可自動將slave轉為master,但是不能動態擴充;
-
- cluster集群模式(3.x):對redis做水平擴展
- 數據分發到多個節點上需要用一定的算法來完成
-
- 哈希取模:缺點是如果一個節點掛掉所有數據則需要重新計算,原有數據取不到而全部失效。
-
- 哈希一致性哈希算法:就是一個數據環,然后服務器作為上面的節點,如果一個服務器掛了,就會順時針方向,走到下一個節點,這里的缺點也就是會造成熱點數據問題
-
- 虛擬槽分區算法:
、
- 虛擬槽分區算法:

- 連接redis的java客戶端
-
- jedis/jedisCluster
-
-
- 全媒體用了jedisCluster,他使用條件注解開啟,以此來達到可配置化
-
@ConditionalOnProperty(name = {"spring.redis.config.cluster"}, havingValue = "true")
-
- Spring提供的
-
- redission(推薦)
- Redis底層數據結構
-
- 列表(list)
-
-
- 分兩種實現方式,壓縮列表和雙向循環列表
-
-
-
- 壓縮列表
-
-
-
-
- 壓縮指的是對內存的壓縮,要滿足以下兩個條件才會使用壓縮列表,否則就會使用雙向鏈表
-
-
-
-
-
-
- 列表中保存的單個數據(有可能是字符串類型的)小于64字節:
-
-
-
-
-
-
-
- 列表中數據個數少于512個。
-
-
-
-
-
-
- 壓縮列表做的優化就是 因為數組的長度是固定的,字符串的長度不固定,放到這些數組里面就會造成浪費,所以就保存每個字符串的長度,去除空閑的空間
- 壓縮列表做的優化就是 因為數組的長度是固定的,字符串的長度不固定,放到這些數組里面就會造成浪費,所以就保存每個字符串的長度,去除空閑的空間
-
-
-
-
- 雙向鏈表
-
-
- 字典(hash)
-
-
- 他也有兩種實現方式,壓縮列表和散列表
-
-
-
- 壓縮列表:要使用壓縮列表需要滿足下面兩個條件
-
-
-
-
- 字典中保存的鍵和值的大小都要小于64字節;
-
-
-
-
-
- 字典中鍵值對的個數要小于512個。
-
-
-
-
- 散列表:使用的MurmurHash2作為哈希函數,對于哈希沖突使用的是鏈表法來解決,對于動態因子,大于一時會觸發擴容兩倍,小于0.1時就會縮容
-
-
- 集合(set)
-
-
- 也有兩種實現,有序數組和散列表
-
-
-
- 有序數組:要使用有序數組要滿足一下兩個條件
-
-
-
-
- 存儲的數據都是整數;
-
-
-
-
-
- 存儲的數據元素個數不超過512個。
-
-
-
-
- 散列表
-
-
- 有序集合(sortedset)
-
-
- 也有兩種實現,壓縮列表和跳表
-
-
-
- 壓縮列表:使用壓縮列表的條件如下:
-
-
-
-
- 所有數據的大小都要小于64字節;
-
-
-
-
-
- 元素個數要小于128個。
-
-
-
-
- 跳表
-
- 數據持久化的兩種方式
-
- 清楚原有的數據結構,直接將數據存儲到磁盤(redis采用的這個)
-
-
- 優點未知(redis不經常重新加載數據從磁盤,就是隨機選一個方案)
缺點是 比如對于散列表這種數據,恢復的時候就要重新計算散列值,如果數據量比較大就比較耗費時間
- 優點未知(redis不經常重新加載數據從磁盤,就是隨機選一個方案)
-
-
- 保留原來的存儲格式
-
-
- 可以解決上面的缺點
-
redis分布式鎖
單機實現
- 加鎖
-
- 其中key設置本業務名稱,value設置成thradId,thradId的生成方式如下
//可以看出,這個就是在Thread中自增的,這樣干在集群的時候肯定不行
private static long threadSeqNumber;
private static synchronized long nextThreadID() {return ++threadSeqNumber;
}
-
- 另外因為redis setnx和expire指令不是原子性的,所以采用如下方式進行設置,返回的結果是是否加鎖成功(非阻塞鎖),如果成功就繼續執行操作,如果失敗就設置循環獲取鎖,每次循環休眠一段時間
jedis.set(lockKey, Thread.currentThread().getId(), "nx", "ex", 100);
- 解鎖:要解決get 和del指令的原子性
// 解鎖的時候 get 指令和 del 指令不是原子性的,所以采用 LUA 腳本執行刪除 lockKey 的邏輯
// 解鎖的 LUA 腳本 lock.lua:if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
public boolean unLock(String lockKey, String threadId) {Jedis jedis = jedisPool.getResource();// 加載 LUA 腳本DefaultRedisScript<Number> script = new DefaultRedisScript<>();script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));script.setResultType(Number.class);// 執行 LUA 腳本Object result = jedis.eval(script.getScriptAsString(), Collections.singletonList(lockKey), Collections.singletonList(threadId));if(UNLOCK_SUCCESS.equals(result)) {return true;}return false;
}
- 執行時間過長導致過期問題
-
- 假如過期時間是30秒,A線程執行時間超過了30秒,導致鎖失效,B線程就進來了,解決這種情況就可以用守護線程為當前鎖續命
-
- B線程進來后開始執行,這個時候如果A線程執行完畢了要刪除鎖,按照之前的寫法是直接刪除key,不校檢value,這就相當于在B還運行著的時候,A就直接把鎖給刪除了
- 集群鎖丟失問題
-
- 假設加鎖加在了 master節點上,加完鎖master正好掛了,故障轉移之后slave變成了master,這個鎖就丟失了,這個問題可以用RedLock解決,而Redission就對RedLock做了一套實現,就是MultiLock,創建多個分組,每個分組可能是一個cluster、一個主從復制,然后加鎖和解鎖要在這上面大多數成功才行
redis集群版加鎖:Redisson
案例
- 代碼實現
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.8.1</version>
</dependency>
原理
- 找哪臺機器加鎖
-
- 其中第一步就對應代碼
RLock lock = redisson.getLock("anyLock");
,主要就是通過RedissionLock類獲取鎖對象
- 其中第一步就對應代碼
-
- 第二步是
lock.lock();
,底層先是對key獲取crc16值(不要跑,CRC沒這么難!(簡單易懂的CRC原理闡述) - SegmentFault 思否),讓后讓這個值對16384取模,然后就以此來鎖定一臺master
- 第二步是
- 客戶端線程首次加鎖
-
- 調用處 就是在上面的第二步流程中,具體實現為LUA腳本
其中 KEYS[1]為上面設置的key值 "anyLock",
ARGV[1]的值為internalLockLeaseTime=30 * 1000ms ,這個時間也是LockWatchdogTimeout
ARGV[2]的值為ThreadId
- 調用處 就是在上面的第二步流程中,具體實現為LUA腳本
-
- 上面前三行LUA腳本解釋
-
-
- 先判斷這個key存在不存在
-
-
-
- 存在的話就設置值,相當于命令
hset anyLock UUID:ThreadId 1
- 存在的話就設置值,相當于命令
-
-
-
- 設置過期時間,
pexpire anyLock 30000
- 設置過期時間,
-
-
- 流程進度
- 流程進度
- 加鎖成功后如何維持加鎖
-
- 獲取鎖成功后會走下面的步驟
- 獲取鎖成功后會走下面的步驟
-
- 接下來就會添加一個定時器,定時時間為 internalLockLeaseTime/3,也就是10秒鐘就去執行一次,執行的是下面這一段腳本
主要就是判斷線程是否還持有該鎖,如果還持有就為該key續期30s,這樣就保證了只要這個key還在 就一直維持下去,這就是看門狗機制,其中的key值得是'hexists', KEYS[1],ARGV[2]) == 1;
- 接下來就會添加一個定時器,定時時間為 internalLockLeaseTime/3,也就是10秒鐘就去執行一次,執行的是下面這一段腳本
- 可重入鎖的加鎖機制
-
- 依然是上面的LUA腳本,第二次加鎖會進入到第二個If分支,他判當前傳進來的key和線程id是否存在,存在的話就 加一(相當于 增加相同線程的持有次數,可重入鎖),加鎖成功后也會開啟一個watchDog線程
-
- 當前的流程如下
- 當前的流程如下
- 其它線程重復加鎖阻塞
-
- 其它線程過來時 會執行LUA腳本的最后一行,
pttl anyLock
,返回當前key的過期剩余時間 ttl
- 其它線程過來時 會執行LUA腳本的最后一行,
-
- 然后在循環中,他就會等待ttl這么久的時間,然后再去獲取鎖
- 可重入鎖釋放場景
-
- 客戶端宕機釋放
-
- 客戶端調用 unlock方法主動釋放,底層執行的腳本如下,其中KEYS[1]=anyLock,
ARGV[2]=30000,ARGV[3]=UUID:ThreadId
- 客戶端調用 unlock方法主動釋放,底層執行的腳本如下,其中KEYS[1]=anyLock,
-
-
- 其中直接可以看第8行,對重入鎖的次數減一,然后在判斷是否大于一,如果大于一就說明還不能解鎖,而且還會順便把過期時間 蓄滿,小于一就直接把key刪掉釋放鎖
- 其中直接可以看第8行,對重入鎖的次數減一,然后在判斷是否大于一,如果大于一就說明還不能解鎖,而且還會順便把過期時間 蓄滿,小于一就直接把key刪掉釋放鎖
-
- 嘗試獲取鎖超時
-
- 上面都是對 lock.lock()方法的分析,他如果沒獲得鎖就會一直阻塞在那,不斷嘗試,而對于
lock.tryLock(30, 10, TimeUnit.MILLISECONDS);
則可以設置阻塞時間
- 上面都是對 lock.lock()方法的分析,他如果沒獲得鎖就會一直阻塞在那,不斷嘗試,而對于
-
- 每個階段都會扣減時間,直到傳進來的 time扣到0以內,然后就可以返回加鎖失敗了
- 超時自動釋放鎖
-
- 使用 lock.tryLock ?底層不會維持看門狗機制,直接到點就自動釋放鎖了
面試題
- 客戶端線程在底層是如何實現加鎖的?
-
- 先定位master節點
通過key計算出CRC16值,再CRC16值對16384取模得hash slot,通過這個hash slot定位rediscluster集群中的master節點
- 先定位master節點
-
- 加鎖
加鎖邏輯底層是通過lua腳本來實現的,如果客戶端線程第?次去加鎖的話,會在key對應的hash
數據結構中添加線程標識UUID:ThreadId 1,指定該線程當前對這個key加鎖?次了。
- 加鎖
- 客戶端線程是如何維持加鎖的?
當加鎖成功后,此時會對加鎖的結果設置?個監聽器,如果監聽到加鎖成功了,也就是返回的結果為空,此時就會在后臺通過watchdog看?狗機制、啟動?個后臺定時任務,每隔10s執??次,檢查如果key當前依然存在,就重置key的存活時間為30s。維持加鎖底層就是通過后臺這樣的?個線程定時刷新存活時間維持的。
- 相同客戶端線程是如何實現可重?加鎖的?
第?次加鎖時,會往key對應的hash數據結構中設置 UUID:ThreadId 1,表示當前線程對key加鎖?次;
如果相同線程來再次對這個key加鎖,只需要將UUID:ThreadId持有鎖的次數加1即可,就為:
UUID:ThreadId 2 了,Redisson底層就是通過這樣的數據結構來表示重?加鎖的語義的。
- 其他線程加鎖失敗時,底層是如何實現阻塞的?
線程加鎖失敗了,如果沒有設置獲取鎖超時時間,此時就會進??個while的死循環中,?直嘗試加
鎖,直到加鎖成功才會返回。
- 客戶端宕機了,鎖是如何釋放的?
客戶端宕機了,相應的watchdog后臺定時任務當然也就沒了,此時就?法對key進?定時續期,那
么當指定存活時間過后,key就會?動失效,鎖當然也就?動釋放了。
- 客戶端如何主動釋放持有的鎖?
客戶端主動釋放鎖,底層同樣也是通過執?lua腳本的?式實現的,如果判斷當前釋放鎖的key存在,并且在key的hash數據結構中、存在當前線程的加鎖信息,那么此時就會扣減當前線程對這個key的重?鎖次數。
扣減線程的重?鎖次數之后,如果當前線程在這個key中的重?鎖次數為0,此時就會直接釋放鎖,如果當前線程在這個key中的重?鎖次數依然還?于0,此時就直接重置?下key的存活時間為30s。
- 客戶端嘗試獲取鎖超時的機制在底層是如何實現的?
如果在加鎖時就指定了嘗試獲取鎖超時的時間,如果獲取鎖失敗,此時就不會??境的在while死循環中?直獲取鎖,?是根據你指定的獲取鎖超時時間,在這段時間范圍內,要是獲取不到鎖,就會標記為獲取鎖失敗,然后直接返回false。
- 客戶端鎖超時?動釋放機制在底層?是如何實現的?
如果在加鎖時,指定了鎖超時時間,那么就算你獲取鎖成功了,也不會開啟watchdog的定時任務了,此時直接就將當前持有這把鎖的過期時間、設置為你指定的超時時間,那么當你指定的時間到了之后,key失效被刪除了,key對應的鎖相應也就?動釋放了。
面試題
- Redis為什么是單線程,單線程是如何處理并發請求的
-
- Redis的性能瓶頸就是內存 和網絡帶寬,所以順利成章就單線程了(這里的單線程指處理網絡請求的時候,其它模塊還是多線程的)
-
- 單線程是如何處理的
-
-
- 如果使用阻塞 I/O的話,從讀數據到處理數據 都是阻塞的,這期間不能處理其它讀寫請求
-
-
-
- 所以redis使用的是I/O多路復用,可以解決一個線程處理多個連接的問題(使用 select、poll、epoll函數庫)
-
-
-
- 使用 Reactor多路復用線程模型,就是redis有一個I/O多路復用模塊,用來監聽事件的發生,有如下兩種,如果多路復用器監聽到了一個 文件事件的話就會交給文件事件分發器,文件事件分發器再交給具體的 處理器
-
文件事件:Redis 客戶端通過 socket 與 Redis 服務器連接,例如 get 命令請求就是一個文件事件;
時間事件:Redis 服務器周期或者定期執行的事件,例如定期的 RDB 持久化命令就是一個時間事件。
- 如何保證redis和數據庫的一致性
-
- 方案一:先更新數據庫在更新刪除緩存
-
-
- 為什么要刪除緩存而不是更新?
如果更新的頻率大于讀的頻率,那就直接刪除掉,讀的時候再更新進緩存,效率更高
- 為什么要刪除緩存而不是更新?
-
-
-
- 刪除放到 更新緩存前
在刪除的過程中,會有其它請求讀數據庫,發現命中不了又返回緩存
- 刪除放到 更新緩存前
-
-
-
- 有什么缺點
服務器需要同時連接 Redis 和 database,需要大量的連接資源導致連接數過多。
- 有什么缺點
-
-
- 方案二:更新數據庫同時消息隊列異步更新 Redis
-
-
- 就是使用隊列,讓更新數據庫和更新緩存解耦
-
-
-
- 缺點
消息亂序可能會導致更新錯誤,而且又加了mq
- 缺點
-
-
- 方案三:訂閱BinLog更新redis
-
-
- 從bingLog解析出更新操作,然后在更新緩存,適合db壓力小的場景
-
- Sort set 的數據結構
-
- 它是用跳表實現的,對于插、刪、查這幾個操作 他和紅黑樹的時間復雜度是一樣的, 就是根據范圍查,比如[20,30],它可以從底層鏈表 只需要找到第一個值 就可以順著差了,效率較高