01- 你們項目中哪裡用到了Redis ? 在我們的項目中很多地方都用到了Redis , Redis在我們的項目中主要有三個作用 : 使用Redis做熱點數據緩存/介面數據緩存 使用Redis存儲一些業務數據 , 例如 : 驗證碼 , 用戶信息 , 用戶行為數據 , 數據計算結果 , 排行榜數據等 ...
01- 你們項目中哪裡用到了Redis ?
在我們的項目中很多地方都用到了Redis , Redis在我們的項目中主要有三個作用 :
- 使用Redis做熱點數據緩存/介面數據緩存
- 使用Redis存儲一些業務數據 , 例如 : 驗證碼 , 用戶信息 , 用戶行為數據 , 數據計算結果 , 排行榜數據等
- 使用Redis實現分散式鎖 , 解決併發環境下的資源競爭問題
02- Redis的常用數據類型有哪些 ?
Redis 有 5 種基礎數據結構,它們分別是:string(字元串)、list(列表)、hash(字典)、set(集 合) 和 zset(有序集合)
03- Redis的數據持久化策略有哪些 ?
Redis 提供了兩種方式,實現數據的持久化到硬碟。
- RDB 持久化(全量),是指在指定的時間間隔內將記憶體中的數據集快照寫入磁碟。
- AOF持久化(增量),以日誌的形式記錄伺服器所處理的每一個寫、刪除操作
RDB和AOF一起使用, 在Redis4.0版本支持混合持久化方式 ( 設置 aof-use-rdb-preamble yes )
04- Redis的數據過期策略有哪些 ?
-
惰性刪除 :只會在取出 key 的時候才對數據進行過期檢查。這樣對 CPU 最友好,但是可能會造成太多過期 key 沒有被刪除。
數據到達過期時間,不做處理。等下次訪問該數據時,我們需要判斷
- 如果未過期,返回數據
- 發現已過期,刪除,返回nil
-
定期刪除 : 每隔一段時間抽取一批 key 執行刪除過期 key 操作。並且,Redis 底層會通過限制刪除操作執行的時長和頻率來減少刪除操作對 CPU 時間的影響。
預設情況下 Redis 定期檢查的頻率是每秒掃描 10 次,用於定期清除過期鍵。當然此值還可以通過配置文件進行設置,在 redis.conf 中修改配置“hz”即可,預設的值為hz 10
定期刪除的掃描並不是遍歷所有的鍵值對,這樣的話比較費時且太消耗系統資源。Redis 伺服器採用的是隨機抽取形式,每次從過期字典中,取出 20 個鍵進行過期檢測,過期字典中存儲的是所有設置了過期時間的鍵值對。如果這批隨機檢查的數據中有 25% 的比例過期,那麼會再抽取 20 個隨機鍵值進行檢測和刪除,並且會迴圈執行這個流程,直到抽取的這批數據中過期鍵值小於 25%,此次檢測才算完成
Redis 伺服器為了保證過期刪除策略不會導致線程卡死,會給過期掃描增加了最大執行時間為 25ms
定期刪除對記憶體更加友好,惰性刪除對 CPU 更加友好。兩者各有千秋,所以 Redis 採用的是 定期刪除+惰性刪除
05- Redis的數據淘汰策略有哪些 ?
Redis 提供 8 種數據淘汰策略:
淘汰易失數據(具有過期時間的數據)
- volatile-lru(least recently used):從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
- volatile-lfu(least frequently used):從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
- volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
- volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
淘汰全庫數據
- allkeys-lru(least recently used):當記憶體不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
- allkeys-lfu(least frequently used):當記憶體不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的 key
- allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
不淘汰
- no-eviction:禁止驅逐數據,也就是說當記憶體不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!
06- 你們使用Redis是單點還是集群 ? 哪種集群 ?
我們Redis使用的是哨兵集群 , 一主二從 , 三個哨兵 , 三台Linux機器
07- Redis集群有哪些方案, 知道嘛 ?
我所瞭解的Redis集群方案
- 主從複製集群 : 讀寫分離, 一主多從 , 解決高併發讀的問題
- 哨兵集群 : 主從集群的結構之上 , 加入了哨兵用於監控集群狀態 , 主節點出現故障, 執行主從切換 , 解決高可用問題
- Cluster分片集群 : 多主多從 , 解決高併發寫的問題, 以及海量數據存儲問題 , 每個主節點存儲一部分集群數據
08- 什麼是 Redis 主從同步?
Redis 的主從同步(replication)機制,允許 Slave 從 Master 那裡,通過網路傳輸拷貝到完整的數據備份,從而達到主從機制。
主資料庫可以進行讀寫操作,當發生寫操作的時候自動將數據同步到從資料庫,而從資料庫一般是只讀的,並接收主資料庫同步過來的數據。一個主資料庫可以有多個從資料庫,而一個從資料庫只能有一個主資料庫。
主從數據同步主要分二個階段 :
第一階段 : 全量複製階段
- slave節點請求增量同步
- master節點判斷replid,發現不一致,拒絕增量同步
- master將完整記憶體數據生成RDB,發送RDB到slave
- slave清空本地數據,載入master的RDB
第二階段 : 增量複製階段
- master將RDB期間的命令記錄在repl_baklog,並持續將log中的命令發送給slave
- slave執行接收到的命令,保持與master之間的同步
09- Redis分片集群中數據是怎麼存儲和讀取的 ?
Redis集群採用的演算法是哈希槽分區演算法。Redis集群中有16384個哈希槽(槽的範圍是 0 -16383,哈希槽),將不同的哈希槽分佈在不同的Redis節點上面進行管理,也就是說每個Redis節點只負責一部分的哈希槽。在對數據進行操作的時候,集群會對使用CRC16演算法對key進行計算並對16384取模(slot = CRC16(key)%16383),得到的結果就是 Key-Value 所放入的槽,通過這個值,去找到對應的槽所對應的Redis節點,然後直接到這個對應的節點上進行存取操作
10- 你們用過Redis的事務嗎 ? 事務的命令有哪些 ?
Redis 作為 NoSQL 資料庫也同樣提供了事務機制。在 Redis 中,MULTI / EXEC / DISCARD / WATCH 這四個命令事務的相關操作命令
我們在開發過程中基本上沒有用到過Redis的事務
11- Redis 和 Memcached 的區別有哪些?
- Redis 提供複雜的數據結構,豐富的數據操作 , Memcached 僅提供簡單的字元串。
- Redis原生支持集群模式 , Memcached不支持原生集群
- Memcached 不支持持久化存儲,重啟時,數據被清空, Redis 支持持久化存儲,重啟時,可以恢復已持久化的數據
12- Redis的記憶體用完了會發生什麼?
如果達到設置的上限,Redis 的寫命令會返回錯誤信息( 但是讀命令還可以正常返回。)
也可以配置記憶體淘汰機制, 當 Redis 達到記憶體上限時會沖刷掉舊的內容。
13- Redis和Mysql如何保證數據⼀致?
-
先更新Mysql,再更新Redis,如果更新Redis失敗,可能仍然不⼀致
-
先刪除Redis緩存數據,再更新Mysql,再次查詢的時候在將數據添加到緩存中
這種⽅案能解決1 ⽅案的問題,但是在⾼併發下性能較低,⽽且仍然會出現數據不⼀致的問題,
⽐如線程1刪除了 Redis緩存數據,正在更新Mysql,
此時另外⼀個查詢再查詢,那麼就會把Mysql中⽼數據⼜查到 Redis中
- 使用MQ非同步同步, 保證數據的最終一致性
我們項目中會根據業務情況 , 使用不同的方案來解決Redis和Mysql的一致性問題 :
-
對於一些一致性要求不高的場景 , 不做處理
例如 : 用戶行為數據 , 我們沒有做一致性保證 , 因為就算不一致產生的影響也很小
-
對於時效性數據 , 設置過期時間
例如 : 介面緩存數據 , 我們會設置緩存的過期時間為 60S , 那麼可能會出現60S之內的數據不一致, 60S後緩存過期, 重新從資料庫載入就一致了
-
對於一致性要求比較高但是時效性要求不那麼高的場景 , 使用MQ不斷發送消息完成數據同步直到成功為止
例如 : 首頁廣告數據 , 首頁推薦數據
資料庫數據發生修改----> 發送消息到MQ -----> 接收消息更新緩存
消息不丟失/重覆消費 : 消息狀態表/消息消費表
-
對於一致性和時效性要求都比較高的場景 , 使用分散式事務 , Seata的TCC模式
很少用
14- 什麼是緩存穿透 ? 怎麼解決 ?
緩存穿透是指查詢一條資料庫和緩存都沒有的一條數據,就會一直查詢資料庫,對資料庫的訪問壓力就會增大,緩存穿透的解決方案
有以下2種解決方案 :
- 緩存空對象:代碼維護較簡單,但是效果不好。
- 布隆過濾器:代碼維護複雜,效果很好
15- 什麼是緩存擊穿 ? 怎麼解決 ?
緩存擊穿是指緩存中沒有但資料庫中有的數據(一般是緩存時間到期),這時由於併發用戶特別多,同時讀緩存沒讀到數據,又同時去資料庫去取數據,引起資料庫壓力瞬間增大
解決方案 :
- 熱點數據提前預熱
- 設置熱點數據永遠不過期。
- 加鎖 , 限流
16- 什麼是緩存雪崩 ? 怎麼解決 ?
緩存雪崩/緩存失效 指的是大量的緩存在同一時間失效,大量請求落到資料庫 導致資料庫瞬間壓力飆升。
造成這種現象的 原因是,key的過期時間都設置成一樣了。
解決方案是,key的過期時間引入隨機因素
17- 資料庫有1000萬數據 ,Redis只能緩存20w數據, 如何保證Redis中的數據都是熱點數據 ?
配置Redis的內容淘汰策略為LFU演算法 , 這樣會把使用頻率較低的數據淘汰掉 , 留下的數據都是熱點數據
18- Redis分散式鎖如何實現 ?
Redis分散式鎖主要依靠一個SETNX
指令實現的 , 這條命令的含義就是“SET if Not Exists”,即不存在的時候才會設置值。
只有在key不存在的情況下,將鍵key的值設置為value。如果key已經存在,則SETNX命令不做任何操作。
這個命令的返回值如下。
- 命令在設置成功時返回1。
- 命令在設置失敗時返回0。
假設此時有線程A和線程B同時訪問臨界區代碼,假設線程A首先執行了SETNX命令,並返回結果1,繼續向下執行。而此時線程B再次執行SETNX命令時,返回的結果為0,則線程B不能繼續向下執行。只有當線程A執行DELETE命令將設置的鎖狀態刪除時,線程B才會成功執行SETNX命令設置加鎖狀態後繼續向下執行
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
當然我們在使用分散式鎖的時候也不能這麼簡單, 會考慮到一些實際場景下的問題 , 例如 :
-
死鎖問題
在使用分散式鎖的時候, 如果因為一些原因導致系統宕機, 鎖資源沒有被釋放, 就會產生死鎖
解決的方案 : 上鎖的時候設置鎖的超時時間
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS);
-
鎖超時問題
如果業務執行需要的時間, 超過的鎖的超時時間 , 這個時候業務還沒有執行完成, 鎖就已經自動被刪除了
其他請求就能獲取鎖, 操作這個資源 , 這個時候就會出現併發問題 , 解決的方案 :
- 引入Redis的watch dog機制, 自動為鎖續期
- 開啟子線程 , 每隔20S運行一次, 重新設置鎖的超時時間
-
歸一問題
如果一個線程獲取了分散式鎖, 但是這個線程業務沒有執行完成之前 , 鎖被其他的線程刪掉了 , 又會出現線程併發問題 , 這個時候就需要考慮歸一化問題
就是一個線程執行了加鎖操作後,後續必須由這個線程執行解鎖操作,加鎖和解鎖操作由同一個線程來完成。
為瞭解決只有加鎖的線程才能進行相應的解鎖操作的問題,那麼,我們就需要將加鎖和解鎖操作綁定到同一個線程中,可以使用ThreadLocal來解決這個問題 , 加鎖的時候生成唯一標識保存到ThreadLocal , 並且設置到鎖的值中 , 釋放鎖的時候, 判斷線程中的唯一標識和鎖的唯一標識是否相同, 只有相同才會釋放
"""public class RedisLockImpl implements RedisLock{ @Autowired private StringRedisTemplate stringRedisTemplate; private ThreadLocal<String> threadLocal = new ThreadLocal<String>(); @Override public boolean tryLock(String key, long timeout, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); threadLocal.set(uuid); return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); } @Override public void releaseLock(String key){ //當前線程中綁定的uuid與Redis中的uuid相同時,再執行刪除鎖的操作 if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){ stringRedisTemplate.delete(key); } } }
"""
4、可重入問題
當一個線程成功設置了鎖標誌位後,其他的線程再設置鎖標誌位時,就會返回失敗。
還有一種場景就是在一個業務中, 有個操作都需要獲取到鎖, 這個時候第二個操作就無法獲取鎖了 , 操作會失敗
例如 : 下單業務中, 扣減商品庫存會給商品加鎖, 增加商品銷量也需要給商品加鎖 , 這個時候需要獲取二次鎖
第二次獲取商品鎖就會失敗 , 這就需要我們的分散式鎖能夠實現可重入
實現可重入鎖最簡單的方式就是使用計數器 , 加鎖成功之後計數器 + 1 , 取消鎖之後計數器 -1 , 計數器減為0 , 真正從Redis刪除鎖
"""
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}else{
isLocked = true;
}
//加鎖成功後將計數器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當前線程中綁定的uuid與Redis中的uuid相同時,再執行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計數器減為0時釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
"""
5、阻塞與非阻塞問題
在使用分散式鎖的時候 , 如果當前需要操作的資源已經加了鎖, 這個時候會獲取鎖失敗, 直接向用戶返回失敗信息 , 用戶的體驗非常不好 , 所以我們在實現分散式鎖的時候, 我們可以將後續的請求進行阻塞,直到當前請求釋放鎖後,再喚醒阻塞的請求獲得分散式鎖來執行方法。
具體的實現就是參考自旋鎖的思想, 獲取鎖失敗自選獲取鎖, 直到成功為止 , 當然為了防止多條線程自旋帶來的系統資料消耗, 可以設置一個自旋的超時時間 , 超過時間之後, 自動終止線程 , 返回失敗信息
19- 你的項目中哪裡用到了分散式鎖
在我最近做的一個項目中 , 我們在任務調度的時候使用了分散式鎖
早期我們在進行定時任務的時候我們採用的是SpringTask實現的 , 在集群部署的情況下, 多個節點的定時任務會同時執行 , 造成重覆調度 , 影響運算結果, 浪費系統資源
這裡為了防止這種情況的發送, 我們使用Redis實現分散式鎖對任務進行調度管理 , 防止重覆任務執行
後期因為我們系統中的任務越來越多 , 執行規則也比較多 , 而且單節點執行效率有一定的限制 , 所以定時任務就切換成了XXL-JOB , 系統中就沒有再使用分散式鎖了