面試必會->Redis篇
01- 你們項目中哪里用到了Redis ?
在我們的項目中很多地方都用到了Redis , Redis在我們的項目中主要有三個作用 :
- 使用Redis做熱點(diǎn)數(shù)據(jù)緩存/接口數(shù)據(jù)緩存
- 使用Redis存儲一些業(yè)務(wù)數(shù)據(jù) , 例如 : 驗證碼 , 用戶信息 , 用戶行為數(shù)據(jù) , 數(shù)據(jù)計算結(jié)果 , 排行榜數(shù)據(jù)等
- 使用Redis實(shí)現(xiàn)分布式鎖 , 解決并發(fā)環(huán)境下的資源競爭問題
02- Redis的常用數(shù)據(jù)類型有哪些 ?
Redis 有 5 種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),它們分別是:string(字符串)、list(列表)、hash(字典)、set(集 合) 和 zset(有序集合)
03- Redis的數(shù)據(jù)持久化策略有哪些 ?
Redis 提供了兩種方式,實(shí)現(xiàn)數(shù)據(jù)的持久化到硬盤。
- RDB 持久化(全量),是指在指定的時間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤。
- AOF持久化(增量),以日志的形式記錄服務(wù)器所處理的每一個寫、刪除操作
RDB和AOF一起使用, 在Redis4.0版本支持混合持久化方式 ( 設(shè)置 aof-use-rdb-preamble yes )
04- Redis的數(shù)據(jù)過期策略有哪些 ?
-
惰性刪除 :只會在取出 key 的時候才對數(shù)據(jù)進(jìn)行過期檢查。這樣對 CPU 最友好,但是可能會造成太多過期 key 沒有被刪除。
數(shù)據(jù)到達(dá)過期時間,不做處理。等下次訪問該數(shù)據(jù)時,我們需要判斷
- 如果未過期,返回數(shù)據(jù)
- 發(fā)現(xiàn)已過期,刪除,返回nil
-
定期刪除 : 每隔一段時間抽取一批 key 執(zhí)行刪除過期 key 操作。并且,Redis 底層會通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對 CPU 時間的影響。
默認(rèn)情況下 Redis 定期檢查的頻率是每秒掃描 10 次,用于定期清除過期鍵。當(dāng)然此值還可以通過配置文件進(jìn)行設(shè)置,在 redis.conf 中修改配置“hz”即可,默認(rèn)的值為hz 10
定期刪除的掃描并不是遍歷所有的鍵值對,這樣的話比較費(fèi)時且太消耗系統(tǒng)資源。Redis 服務(wù)器采用的是隨機(jī)抽取形式,每次從過期字典中,取出 20 個鍵進(jìn)行過期檢測,過期字典中存儲的是所有設(shè)置了過期時間的鍵值對。如果這批隨機(jī)檢查的數(shù)據(jù)中有 25% 的比例過期,那么會再抽取 20 個隨機(jī)鍵值進(jìn)行檢測和刪除,并且會循環(huán)執(zhí)行這個流程,直到抽取的這批數(shù)據(jù)中過期鍵值小于 25%,此次檢測才算完成
Redis 服務(wù)器為了保證過期刪除策略不會導(dǎo)致線程卡死,會給過期掃描增加了最大執(zhí)行時間為 25ms
定期刪除對內(nèi)存更加友好,惰性刪除對 CPU 更加友好。兩者各有千秋,所以 Redis 采用的是 定期刪除+惰性刪除
05- Redis的數(shù)據(jù)淘汰策略有哪些 ?
Redis 提供 8 種數(shù)據(jù)淘汰策略:
淘汰易失數(shù)據(jù)(具有過期時間的數(shù)據(jù))
- volatile-lru(least recently used):從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最近最少使用的數(shù)據(jù)淘汰
- volatile-lfu(least frequently used):從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最不經(jīng)常使用的數(shù)據(jù)淘汰
- volatile-ttl:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選將要過期的數(shù)據(jù)淘汰
- volatile-random:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中任意選擇數(shù)據(jù)淘汰
淘汰全庫數(shù)據(jù)
- allkeys-lru(least recently used):當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
- allkeys-lfu(least frequently used):當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最不經(jīng)常使用的 key
- allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰
不淘汰
- no-eviction:禁止驅(qū)逐數(shù)據(jù),也就是說當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯。這個應(yīng)該沒人使用吧!
06- 你們使用Redis是單點(diǎn)還是集群 ? 哪種集群 ?
我們Redis使用的是哨兵集群 , 一主二從 , 三個哨兵 , 三臺Linux機(jī)器
07- Redis集群有哪些方案, 知道嘛 ?
我所了解的Redis集群方案
- 主從復(fù)制集群 : 讀寫分離, 一主多從 , 解決高并發(fā)讀的問題
- 哨兵集群 : 主從集群的結(jié)構(gòu)之上 , 加入了哨兵用于監(jiān)控集群狀態(tài) , 主節(jié)點(diǎn)出現(xiàn)故障, 執(zhí)行主從切換 , 解決高可用問題
- Cluster分片集群 : 多主多從 , 解決高并發(fā)寫的問題, 以及海量數(shù)據(jù)存儲問題 , 每個主節(jié)點(diǎn)存儲一部分集群數(shù)據(jù)
08- 什么是 Redis 主從同步?
Redis 的主從同步(replication)機(jī)制,允許 Slave 從 Master 那里,通過網(wǎng)絡(luò)傳輸拷貝到完整的數(shù)據(jù)備份,從而達(dá)到主從機(jī)制。
主數(shù)據(jù)庫可以進(jìn)行讀寫操作,當(dāng)發(fā)生寫操作的時候自動將數(shù)據(jù)同步到從數(shù)據(jù)庫,而從數(shù)據(jù)庫一般是只讀的,并接收主數(shù)據(jù)庫同步過來的數(shù)據(jù)。一個主數(shù)據(jù)庫可以有多個從數(shù)據(jù)庫,而一個從數(shù)據(jù)庫只能有一個主數(shù)據(jù)庫。

主從數(shù)據(jù)同步主要分二個階段 :
第一階段 : 全量復(fù)制階段
- slave節(jié)點(diǎn)請求增量同步
- master節(jié)點(diǎn)判斷replid,發(fā)現(xiàn)不一致,拒絕增量同步
- master將完整內(nèi)存數(shù)據(jù)生成RDB,發(fā)送RDB到slave
- slave清空本地數(shù)據(jù),加載master的RDB
第二階段 : 增量復(fù)制階段
- master將RDB期間的命令記錄在repl_baklog,并持續(xù)將log中的命令發(fā)送給slave
- slave執(zhí)行接收到的命令,保持與master之間的同步
09- Redis分片集群中數(shù)據(jù)是怎么存儲和讀取的 ?
Redis集群采用的算法是哈希槽分區(qū)算法。Redis集群中有16384個哈希槽(槽的范圍是 0 -16383,哈希槽),將不同的哈希槽分布在不同的Redis節(jié)點(diǎn)上面進(jìn)行管理,也就是說每個Redis節(jié)點(diǎn)只負(fù)責(zé)一部分的哈希槽。在對數(shù)據(jù)進(jìn)行操作的時候,集群會對使用CRC16算法對key進(jìn)行計算并對16384取模(slot = CRC16(key)%16383),得到的結(jié)果就是 Key-Value 所放入的槽,通過這個值,去找到對應(yīng)的槽所對應(yīng)的Redis節(jié)點(diǎn),然后直接到這個對應(yīng)的節(jié)點(diǎn)上進(jìn)行存取操作
10- 你們用過Redis的事務(wù)嗎 ? 事務(wù)的命令有哪些 ?
Redis 作為 NoSQL 數(shù)據(jù)庫也同樣提供了事務(wù)機(jī)制。在 Redis 中,MULTI / EXEC / DISCARD / WATCH 這四個命令事務(wù)的相關(guān)操作命令
我們在開發(fā)過程中基本上沒有用到過Redis的事務(wù)
11- Redis 和 Memcached 的區(qū)別有哪些?
- Redis 提供復(fù)雜的數(shù)據(jù)結(jié)構(gòu),豐富的數(shù)據(jù)操作 , Memcached 僅提供簡單的字符串。
- Redis原生支持集群模式 , Memcached不支持原生集群
- Memcached 不支持持久化存儲,重啟時,數(shù)據(jù)被清空, Redis 支持持久化存儲,重啟時,可以恢復(fù)已持久化的數(shù)據(jù)
12- Redis的內(nèi)存用完了會發(fā)生什么?
如果達(dá)到設(shè)置的上限,Redis 的寫命令會返回錯誤信息( 但是讀命令還可以正常返回。)
也可以配置內(nèi)存淘汰機(jī)制, 當(dāng) Redis 達(dá)到內(nèi)存上限時會沖刷掉舊的內(nèi)容。
13- Redis和Mysql如何保證數(shù)據(jù)?致?
-
先更新Mysql,再更新Redis,如果更新Redis失敗,可能仍然不?致
-
先刪除Redis緩存數(shù)據(jù),再更新Mysql,再次查詢的時候在將數(shù)據(jù)添加到緩存中
這種?案能解決1 ?案的問題,但是在?并發(fā)下性能較低,?且仍然會出現(xiàn)數(shù)據(jù)不?致的問題,
?如線程1刪除了 Redis緩存數(shù)據(jù),正在更新Mysql,
此時另外?個查詢再查詢,那么就會把Mysql中?數(shù)據(jù)?查到 Redis中
- 使用MQ異步同步, 保證數(shù)據(jù)的最終一致性
我們項目中會根據(jù)業(yè)務(wù)情況 , 使用不同的方案來解決Redis和Mysql的一致性問題 :
-
對于一些一致性要求不高的場景 , 不做處理
例如 : 用戶行為數(shù)據(jù) , 我們沒有做一致性保證 , 因為就算不一致產(chǎn)生的影響也很小
-
對于時效性數(shù)據(jù) , 設(shè)置過期時間
例如 : 接口緩存數(shù)據(jù) , 我們會設(shè)置緩存的過期時間為 60S , 那么可能會出現(xiàn)60S之內(nèi)的數(shù)據(jù)不一致, 60S后緩存過期, 重新從數(shù)據(jù)庫加載就一致了
-
對于一致性要求比較高但是時效性要求不那么高的場景 , 使用MQ不斷發(fā)送消息完成數(shù)據(jù)同步直到成功為止
例如 : 首頁廣告數(shù)據(jù) , 首頁推薦數(shù)據(jù)
數(shù)據(jù)庫數(shù)據(jù)發(fā)生修改----> 發(fā)送消息到MQ -----> 接收消息更新緩存
消息不丟失/重復(fù)消費(fèi) : 消息狀態(tài)表/消息消費(fèi)表
-
對于一致性和時效性要求都比較高的場景 , 使用分布式事務(wù) , Seata的TCC模式
很少用
14- 什么是緩存穿透 ? 怎么解決 ?
緩存穿透是指查詢一條數(shù)據(jù)庫和緩存都沒有的一條數(shù)據(jù),就會一直查詢數(shù)據(jù)庫,對數(shù)據(jù)庫的訪問壓力就會增大,緩存穿透的解決方案
有以下2種解決方案 :
- 緩存空對象:代碼維護(hù)較簡單,但是效果不好。
- 布隆過濾器:代碼維護(hù)復(fù)雜,效果很好
15- 什么是緩存擊穿 ? 怎么解決 ?
緩存擊穿是指緩存中沒有但數(shù)據(jù)庫中有的數(shù)據(jù)(一般是緩存時間到期),這時由于并發(fā)用戶特別多,同時讀緩存沒讀到數(shù)據(jù),又同時去數(shù)據(jù)庫去取數(shù)據(jù),引起數(shù)據(jù)庫壓力瞬間增大
解決方案 :
- 熱點(diǎn)數(shù)據(jù)提前預(yù)熱
- 設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期。
- 加鎖 , 限流
16- 什么是緩存雪崩 ? 怎么解決 ?
緩存雪崩/緩存失效 指的是大量的緩存在同一時間失效,大量請求落到數(shù)據(jù)庫 導(dǎo)致數(shù)據(jù)庫瞬間壓力飆升。
造成這種現(xiàn)象的 原因是,key的過期時間都設(shè)置成一樣了。
解決方案是,key的過期時間引入隨機(jī)因素
17- 數(shù)據(jù)庫有1000萬數(shù)據(jù) ,Redis只能緩存20w數(shù)據(jù), 如何保證Redis中的數(shù)據(jù)都是熱點(diǎn)數(shù)據(jù) ?
配置Redis的內(nèi)容淘汰策略為LFU算法 , 這樣會把使用頻率較低的數(shù)據(jù)淘汰掉 , 留下的數(shù)據(jù)都是熱點(diǎn)數(shù)據(jù)
18- Redis分布式鎖如何實(shí)現(xiàn) ?
Redis分布式鎖主要依靠一個SETNX指令實(shí)現(xiàn)的 , 這條命令的含義就是“SET if Not Exists”,即不存在的時候才會設(shè)置值。
只有在key不存在的情況下,將鍵key的值設(shè)置為value。如果key已經(jīng)存在,則SETNX命令不做任何操作。
這個命令的返回值如下。
- 命令在設(shè)置成功時返回1。
- 命令在設(shè)置失敗時返回0。
假設(shè)此時有線程A和線程B同時訪問臨界區(qū)代碼,假設(shè)線程A首先執(zhí)行了SETNX命令,并返回結(jié)果1,繼續(xù)向下執(zhí)行。而此時線程B再次執(zhí)行SETNX命令時,返回的結(jié)果為0,則線程B不能繼續(xù)向下執(zhí)行。只有當(dāng)線程A執(zhí)行DELETE命令將設(shè)置的鎖狀態(tài)刪除時,線程B才會成功執(zhí)行SETNX命令設(shè)置加鎖狀態(tài)后繼續(xù)向下執(zhí)行
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
當(dāng)然我們在使用分布式鎖的時候也不能這么簡單, 會考慮到一些實(shí)際場景下的問題 , 例如 :
-
死鎖問題
在使用分布式鎖的時候, 如果因為一些原因?qū)е孪到y(tǒng)宕機(jī), 鎖資源沒有被釋放, 就會產(chǎn)生死鎖
解決的方案 : 上鎖的時候設(shè)置鎖的超時時間
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS); -
鎖超時問題
如果業(yè)務(wù)執(zhí)行需要的時間, 超過的鎖的超時時間 , 這個時候業(yè)務(wù)還沒有執(zhí)行完成, 鎖就已經(jīng)自動被刪除了
其他請求就能獲取鎖, 操作這個資源 , 這個時候就會出現(xiàn)并發(fā)問題 , 解決的方案 :
- 引入Redis的watch dog機(jī)制, 自動為鎖續(xù)期
- 開啟子線程 , 每隔20S運(yùn)行一次, 重新設(shè)置鎖的超時時間
-
歸一問題
如果一個線程獲取了分布式鎖, 但是這個線程業(yè)務(wù)沒有執(zhí)行完成之前 , 鎖被其他的線程刪掉了 , 又會出現(xiàn)線程并發(fā)問題 , 這個時候就需要考慮歸一化問題
就是一個線程執(zhí)行了加鎖操作后,后續(xù)必須由這個線程執(zhí)行解鎖操作,加鎖和解鎖操作由同一個線程來完成。
為了解決只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作的問題,那么,我們就需要將加鎖和解鎖操作綁定到同一個線程中,可以使用ThreadLocal來解決這個問題 , 加鎖的時候生成唯一標(biāo)識保存到ThreadLocal , 并且設(shè)置到鎖的值中 , 釋放鎖的時候, 判斷線程中的唯一標(biāo)識和鎖的唯一標(biāo)識是否相同, 只有相同才會釋放
"""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){ //當(dāng)前線程中綁定的uuid與Redis中的uuid相同時,再執(zhí)行刪除鎖的操作 if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){ stringRedisTemplate.delete(key); } } }"""
4、可重入問題
當(dāng)一個線程成功設(shè)置了鎖標(biāo)志位后,其他的線程再設(shè)置鎖標(biāo)志位時,就會返回失敗。
還有一種場景就是在一個業(yè)務(wù)中, 有個操作都需要獲取到鎖, 這個時候第二個操作就無法獲取鎖了 , 操作會失敗
例如 : 下單業(yè)務(wù)中, 扣減商品庫存會給商品加鎖, 增加商品銷量也需要給商品加鎖 , 這個時候需要獲取二次鎖
第二次獲取商品鎖就會失敗 , 這就需要我們的分布式鎖能夠?qū)崿F(xiàn)可重入
實(shí)現(xiàn)可重入鎖最簡單的方式就是使用計數(shù)器 , 加鎖成功之后計數(shù)器 + 1 , 取消鎖之后計數(shù)器 -1 , 計數(shù)器減為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;
}
//加鎖成功后將計數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時,再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計數(shù)器減為0時釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
"""
5、阻塞與非阻塞問題
在使用分布式鎖的時候 , 如果當(dāng)前需要操作的資源已經(jīng)加了鎖, 這個時候會獲取鎖失敗, 直接向用戶返回失敗信息 , 用戶的體驗非常不好 , 所以我們在實(shí)現(xiàn)分布式鎖的時候, 我們可以將后續(xù)的請求進(jìn)行阻塞,直到當(dāng)前請求釋放鎖后,再喚醒阻塞的請求獲得分布式鎖來執(zhí)行方法。
具體的實(shí)現(xiàn)就是參考自旋鎖的思想, 獲取鎖失敗自選獲取鎖, 直到成功為止 , 當(dāng)然為了防止多條線程自旋帶來的系統(tǒng)資料消耗, 可以設(shè)置一個自旋的超時時間 , 超過時間之后, 自動終止線程 , 返回失敗信息

19- 你的項目中哪里用到了分布式鎖
在我最近做的一個項目中 , 我們在任務(wù)調(diào)度的時候使用了分布式鎖
早期我們在進(jìn)行定時任務(wù)的時候我們采用的是SpringTask實(shí)現(xiàn)的 , 在集群部署的情況下, 多個節(jié)點(diǎn)的定時任務(wù)會同時執(zhí)行 , 造成重復(fù)調(diào)度 , 影響運(yùn)算結(jié)果, 浪費(fèi)系統(tǒng)資源
這里為了防止這種情況的發(fā)送, 我們使用Redis實(shí)現(xiàn)分布式鎖對任務(wù)進(jìn)行調(diào)度管理 , 防止重復(fù)任務(wù)執(zhí)行
后期因為我們系統(tǒng)中的任務(wù)越來越多 , 執(zhí)行規(guī)則也比較多 , 而且單節(jié)點(diǎn)執(zhí)行效率有一定的限制 , 所以定時任務(wù)就切換成了XXL-JOB , 系統(tǒng)中就沒有再使用分布式鎖了

浙公網(wǎng)安備 33010602011771號