高級JAVA面試題詳解(二)——Redis(分佈式鎖、重入鎖、緩存數據一致性、單線程、緩存穿透/擊穿/雪崩)

Redis詳解 上篇

redis分佈式鎖如何實現

使用set(String key, String value, String nxxx, String expx, long time)方法;
方法參數詳解:

  • key 鎖的key值不做過多解釋
  • value 很多同學會問弄個鎖還要value幹啥?value可以去控制誰能來解鎖,或者用於重入鎖來比對是否可重入。
  • nxxx 只能取NX或者XX:
    • NX 當Key不存在時set成功
    • XX 當key存在時set
  • pxex 只能取EX或者PX:代表數據過期時間的單位,用於描述time字段
    • EX單位秒
    • PX單位毫秒
  • time 過期時間,單位是expx所代表的單位

方法返回值詳解: 返回字符串“OK” 代表成功。否則爲插入失敗

光知道這個方法是沒用的,因爲沒有處理當鎖被佔用時的場景。
還需要設置鎖自旋與超時。
自旋相當於多久嘗試再次獲取鎖,
超時相當於自旋多久後依然未成功獲取到鎖則丟棄

設置一個超時時間假設timeout = 10000毫秒 每次自旋間隔爲100毫秒
每次自旋timeout-=100; 當timeout<=0時結束自旋。代碼如下

int timeout = 10000;
long expires = 60000L;
while(timeout >= 0) {
	if ("OK".equalsIgnoreCase(jedis.set(lockKey, lockValue, "NX", "PX", expires))) {
		return true;
	}
	timeout -= 100;
	Thread.sleep(100L);
}

擴展篇(韭菜課堂開課啦!)

很多博客裏面有寫分佈式鎖使用setnx + expire方法其實是錯誤的,因爲無法保證整個操作的原子性。
如果程序在執行完setnx()之後突然崩潰,導致鎖沒有設置過期時間。那麼將會發生死鎖(一直不會被釋放和過期)。
錯誤案例代碼:

if (1L == jedis.setnx(lockKey, lockValue)) {
	// 若在這裏程序突然崩潰,則無法設置過期時間,將發生死鎖
	jedis.expire(lockKey, expireTime);
}

如何實現重入鎖呢?

首先我們來看看什麼是重入鎖

重入鎖相當於給鎖配了一把鑰匙,任何持有對應鑰匙的線程可在鎖尚未釋放的情況下開鎖然後再次加鎖。

知道重入鎖的意思之後 根據上面的set方法是不是很容易就想到了解決方案?
思路:上面所說的set方法的value值可以設置爲requestId(UUID或其他唯一標識),將requestId存入局部線程變量(ThreadLocal),當發現被鎖時從ThreadLocal取出requestId(如果存在)嘗試和鎖的value值比較,如果相等,則返回true,可以重新設置失效時間。
是不是覺得到這裏就完成了重入鎖?然而並沒有!
重入鎖必須記錄重複獲取鎖的次數。釋放鎖不能直接刪除,因爲鎖是可重入的,如果鎖進入了多次,在內層的某個業務執行結束出來就直接釋放鎖, 導致外部的業務在沒有鎖的情況下執行,會有安全問題。因此必須獲取鎖時累計重入的次數,釋放時則減去重入次數,如果減到0,則可以刪除鎖。

如何做到緩存和數據庫的數據一致性呢

數據一致性這一塊一定是在發生寫的操作的情況下才需要去保證的,如果全是讀自然不可能出現不一致的情況:
方案:先修改數據庫再刪除Redis緩存,且緩存一般都要設置失效時間,避免在極端情況下不一致的問題。

爲什麼要選擇刪除而不是直接修改? 爲什麼要把刪除放在數據庫寫入的後面而不是前面?

  • 選擇刪除而不是直接修改
    如果在高併發情況下線程A執行寫操作,成功更新數據庫;
    這時候線程B同樣執行和線程A一樣的操作,但是在線程A執行更新緩存的過程中,線程B更新了新的數據庫數據到緩存中;
    線程A在線程B全部操作完成以後纔將相對老的數據又更新到了緩存中;當出現這種情況就導致緩存和數據庫數據不一致的問題。
  • 什麼要把刪除放在數據庫寫入的後面而不是前面
    因爲如果先刪除在還沒有寫入數據庫成功前有一個線程去查詢就會把舊數據更新到緩存中出現數據庫數據不一致的問題。

Redis是爲什麼單線程的?Redis爲什麼這麼快?

爲什麼單線程
Redis是基於內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成爲瓶頸,那就順理成章地採用單線程的方案了!
爲什麼這麼快

  • 完全基於內存,絕大部分請求是純粹的內存操作,非常快速。數據存在內存中,類似於HashMap,HashMap的優勢就是查找和操作的時間複雜度都是O(1);
  • 數據結構簡單,對數據操作也簡單,Redis中的數據結構是專門進行設計的;
  • 採用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因爲可能出現死鎖而導致的性能消耗;
  • 使用多路I/O複用模型,非阻塞IO;這裏“多路”指的是多個網絡連接,“複用”指的是複用同一個線程
  • 使用底層模型不同,它們之間底層實現方式以及與客戶端之間通信的應用協議不一樣,Redis直接自己構建了VM 機制 ,因爲一般的系統調用系統函數的話,會浪費一定的時間去移動和請求;

緩存穿透、緩存擊穿、緩存雪崩解決方案

緩存穿透

  • 名詞解釋:用戶想要查詢一個key的數據,發現redis內存沒有,於是查數據庫,數據庫也沒有。當大量的請求去查這個key的時候就會對持久層造成很大的壓力,這就是緩存穿透
  • 解決方案:
    1、緩存空對象,數據庫不存在也到redis裏保存一下,並設置過期時間。
    2、布隆過濾器,對所有可能查詢的參數以hash形式存儲,當用戶想要查詢的時候,使用布隆過濾器發現不在集合中(或者自己定義校驗規則),就直接丟棄,不再對持久層查詢。

緩存擊穿

  • 名詞解釋:是指一個key非常熱點,在不停的扛着大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫,導致數據庫壓力暴增。
  • 解決方案:
    1、熱點key永不失效
    2、互斥鎖,查詢緩存如果不存在就去獲取鎖,如果獲取到了就去查數據庫,查到的結果寫入緩存,如果沒有獲取到鎖就在一定時間後再去獲取緩存。

緩存雪崩

  • 名詞解釋:緩存在短時間內大量失效或者redis突然掛了,導致數據庫查詢壓力暴增,造成存儲層也會掛掉的情況。
  • 解決方案:
    1、redis高可用 既然redis有可能掛掉,那我多增設幾臺redis,這樣一臺掛掉之後其他的還可以繼續工作,其實就是搭建的集羣。
    2、互斥鎖 (同上)
    3、設置不同的過期時間,讓緩存失效的時間點儘量均勻。

小夥子你終於聊到了redis集羣,那麼咱麼來聊聊redis集羣相關的問題吧,面試問問題嘛都是一路順着問問到你不會或者面試官不會了就換一個方向。所以基本你的回答意味着接下來的問題可能從你的回答中出發。
集羣的我下一篇再整理吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章