文章目錄
- 1. Redis為什么這么快?
- 2. Redis的持久化機制是怎樣的?
- 3. Redis 的過期策略是怎么樣的?
- 4. Redis的內存淘汰策略是怎么樣的?
- 5. 什么是熱Key問題,如何解決熱key問題?
- 6. 什么是大Key問題,如何解決?
- 7. 什么是緩存擊穿、緩存穿透、緩存雪崩?
- 8. 什么情況下會出現數據庫和緩存不一致的問題?
- 9. 如何解決Redis和數據庫的一致性問題?
- 10. 為什么需要延遲雙刪,兩次刪除的原因是什么?
- 11. 如何用SETNX實現分布式鎖?
- 12. 如何用Redisson實現分布式鎖?
- 13. 公平鎖和非公平鎖的區別?
- 14. Redisson看門狗(watch dog)機制了解嗎?
- 15. 什么是RedLock,他解決了什么問題?
- 16. Redis的哨兵機制?
- 17. 介紹一下Redis的集群模式?
- 18. 介紹下Redis集群的腦裂問題?
- 19. Redis為什么被設計成是單線程的?
- 20. 為什么Redis 6.0引入了多線程?
- 21. 為什么Lua腳本可以保證原子性?
- 22. Redis 的事務機制是怎樣的?
- 23. Redis中key過期了一定會立即刪除嗎?
- 24. Redisson的lock和tryLock有什么區別?
- 25. 什么是Redis的Pipeline,和事務有什么區別?
- 26. 為什么Redis不支持回滾?
- 27. 如何用Redis實現樂觀鎖?
- 28. Redisson解鎖失敗,watchdog會不會一直續期下去?
- 29. Redis中的setnx和setex有啥區別?
- 30. Redis 與 Memcached 有什么區別?
- 31. Redis實現分布鎖的時候,哪些問題需要考慮?
- 32. 如何用setnx實現一個可重入鎖?
- 33. Redis 支持哪幾種數據類型?
- 34. Redis中跳表的實現原理?(實現ZSet的主要數據結構)
- 35. Redis為什么要自己定義SDS?
- 36. Redis除了做緩存,Redis還能用來干什么?
- 37. Redis如何實現延遲消息?
參考:
https://www.yuque.com/hollis666
、https://www.mianshiya.com
、https://javaguide.cn
1. Redis為什么這么快?
基于內存
:Redis 是一種基于內存的數據庫,數據存儲在內存中,數據的讀寫速度非常快,因為內存訪問速度比硬盤訪問速度快得多。單線程模型
:Redis 使用單線程模型,這意味著它的所有操作都是在一個線程內完成的,不需要進行線程切換和上下文切換。這大大提高了 Redis 的運行效率和響應速度。多路復用 I/O 模型
:Redis 在單線程的基礎上,采用了I/O 多路復用技術,實現了單個線程同時處理多個客戶端連接的能力,從而提高了 Redis 的并發性能。高效的數據結構
:Redis 提供了多種高效的數據結構,如哈希表、有序集合、列表等,這些數據結構都被實現得非常高效,能夠在 O(1) 的時間復雜度內完成數據讀寫操作,這也是 Redis 能夠快速處理數據請求的重要因素之一。多線程的引入
:在Redis 6.0中,為了進一步提升IO的性能,引入了多線程的機制。采用多線程,使得網絡處理的請求并發進行,就可以大大的提升性能。多線程除了可以減少由于網絡 I/O 等待造成的影響,還可以充分利用 CPU 的多核優勢。
2. Redis的持久化機制是怎樣的?
Redis 提供了兩種主要的持久化機制:RDB(Redis Database Backup) 和 AOF(Append Only File),以及兩者結合的混合持久化模式。它們的核心目標是確保 Redis 在崩潰或重啟后能恢復數據。
-
RDB(快照持久化)
RDB 會在指定的時間間隔將 Redis 內存中的數據生成一個快照(snapshot),并保存為一個二進制文件。
RDB的優點是:快照文件小、恢復速度快,適合做備份和災難恢復。
RDB的缺點是:RDB 不是實時持久化,如果 Redis 崩潰,最后一次 RDB 之后的變更會丟失。 -
AOF(追加文件持久化)
AOF 會以日志的形式,將 Redis 執行的每一個寫命令追加到一個文件中。當 Redis 重啟時,通過重放 AOF 文件中的命令來恢復數據。
AOF的優點是:可以實現更高的數據可靠性、支持更細粒度的數據恢復,適合做數據存檔和數據備份。
AOF的缺點是:文件大占用空間更多,每次寫操作都需要寫磁盤導致負載較高。
3. Redis 的過期策略是怎么樣的?
Redis 的過期策略采用的是定期刪除
和惰性刪除
相結合的方式。
定期刪除
:Redis 默認每隔 100ms 就隨機抽取一些設置了過期時間的 key,并檢查其是否過期,如果過期才刪除。定期刪除是 Redis 的主動刪除策略,它可以確保過期的 key 能夠及時被刪除,但是會占用 CPU 資源去掃描 key,可能會影響 Redis 的性能。惰性刪除
:當一個 key 過期時,不會立即從內存中刪除,而是在訪問這個 key 的時候才會觸發刪除操作。惰性刪除是 Redis 的被動刪除策略,它可以節省 CPU 資源,但是會導致過期的 key 始終保存在內存中,占用內存空間。
Redis默認同時開啟定期刪除和惰性刪除兩種過期策略。
定期刪除其實并不會立即釋放內存,而是把這些鍵標記為“已過期”,并放入一個專門的鏈表中。然后,在Redis的內存使用率達到一定閾值時,Redis會對這些“已過期”的鍵進行一次內存回收操作,釋放被這些鍵占用的內存空間。
而惰性刪除則是在鍵被訪問時進行過期檢查,如果過期了則刪除鍵并釋放內存。
需要注意的是,即使Redis進行了內存回收操作,也不能完全保證被刪除的內存空間會立即被系統回收。
因為把內存返回給操作系統的開銷很大,會導致頻繁的系統調用和內存碎片化。當 Redis 釋放對象時,jemalloc(Redis 默認使用 jemalloc 作為內存分配器) 只是把這些內存塊標記為空閑,以供 Redis 進程內部再次使用,但并不把內存歸還給操作系統。即使 jemalloc 把大塊內存釋放了,Linux 或其他操作系統也未必馬上回收。操作系統會緩存內存以優化性能,并在其他進程需要時再回收。
4. Redis的內存淘汰策略是怎么樣的?
不淘汰數據(默認)
:
- noeviction:當運行內存超過最大設置內存的時候,不會淘汰數據,而是直接返回報錯禁止寫入
設置了過期時間的數據淘汰
:
- volatile-random:隨機淘汰掉設置了過期時間的key
- volatile-ttl:優先淘汰掉較早過期的key
- volatile-lru(redis3.0之前默認策略):淘汰掉所有設置了過期時間的,然后最久未使用的key
- volatile-Ifu(redis4.0后新增):與上面類似,不過是淘汰掉最少使用的key
所有數據的數據淘汰
:
- allkeys-random:隨機淘汰掉任意的key
- allkeys-lru:淘汰掉緩存中最久沒有使用的key
- allkeys-Ifu(redis4.0后新增):淘汰掉緩存中最少使用的key
5. 什么是熱Key問題,如何解決熱key問題?
熱Key問題指在同一個時間點上,Redis中的同一個key被大量訪問,就會導致流量過于集中,使得很多物理資源無法支撐,如網絡帶寬、物理存儲空間、數據庫連接等。
解決方案:
熱點key拆分
:將熱點數據分散到多個Key中,例如通過引l入隨機前綴,使不同用戶請求分散到多個Key,多個key分布在多實例中,避免集中訪問單一Key。多級緩存
:在Redis前增加其他緩存層(如CDN、本地緩存),以分擔Redis的訪問壓力。限流和降級
:在熱點Key訪問過高時,應用限流策略,減少對Redis的請求,或者在必要時返回降級的數據或空值。
6. 什么是大Key問題,如何解決?
Big Key是Redis中存儲了大量數據的Key,包括value過大或者元素數量過多的情況,Big Key可能造成一些問題,包括:
- 內存分布不均。在集群模式下,不同slot分配到不同實例中,如果大key都映射到一個實例,則分布不均,查詢效率也會受到影響。
- 由于Redis單線程執行命令,操作大Key時耗時較長,從而導致Redis出現其它命令阻塞的問題。
- 大Key對資源的占用巨大,在你進行網絡I/O傳輸的時候,導致你獲取過程中產生的網絡流量較大,從而產生網絡傳輸時間延長甚至網絡傳輸發現阻塞的現象,例如一個key2MB,請求個1000次2000MB。
- 客戶端超時。因為操作大Key時耗時較長,可能導致客戶端等待超時。
7. 什么是緩存擊穿、緩存穿透、緩存雪崩?
緩存擊穿
:是指當某一key的緩存過期時大并發量的請求同時訪問此key,瞬間擊穿緩存服務器直接訪問數據庫,讓數據庫處于負載的情況。緩存穿透
:是指緩存服務器中沒有緩存數據,數據庫中也沒有符合條件的數據,導致業務系統每次都繞過緩存服務器查詢下游的數據庫,緩存服務器完全失去了其應有的作用。緩存雪崩
:是指當大量緩存同時過期或緩存服務宕機,所有請求的都直接訪問數據庫,造成數據庫高負載,影響性能,甚至數據庫宕機。
8. 什么情況下會出現數據庫和緩存不一致的問題?
在非并發的場景中
:緩存的操作和數據庫的操作沒辦法保證原子性,有可能一個操作成功,一個操作失敗的。所以存在不一致的情況。在并發場景中
:如果兩個線程,同時進行先寫數據庫,后更新緩存的操作,就可能會出現不一致:
W | W |
---|---|
寫數據庫,更新成20 | |
- | 寫數據庫,更新成10 |
- | 寫緩存,更新成10 |
寫緩存,更新成20(數據不一致) |
如果在并發場景中,如果兩個線程,同時進行先更新緩存,后寫數據庫的操作,同理,也可能會出現不一致:
W | W |
---|---|
寫緩存,更新成20 | - |
- | 寫緩存,更新成10 |
- | 寫數據庫,更新成10 |
寫數據庫,更新成20(數據不一致) | - |
還有一種低概率的場景,讀寫并發:
W | R |
---|---|
- | 讀緩存,緩存中沒有值 |
- | 讀數據庫,數據庫中得到結果為10 |
寫數據庫和緩存,更新成20 | |
- | 寫緩存,更新成10(數據不一致) |
9. 如何解決Redis和數據庫的一致性問題?
1、先更新數據庫, 再刪除緩存。(并發量不高可以選擇)
2、延遲雙刪:先刪除緩存,再更新數據庫,再刪除一次緩存(并發量高可以選擇)
先操作數據庫,后操作緩存,是一種比較典型的設計模式——
Cache Aside Pattern
探討一下第一個方案,先寫數據庫還是先刪緩存?
都會存在問題,解決辦法:延遲雙刪
- 先刪緩存
如果我們是先刪除緩存,再更新數據庫,有一個好處,那就是:如果是先刪除緩存成功了,但是第二步更新數據庫失敗了,這種情況是可以接受的,因為這樣只是把緩存給清空了而已,但是不會有臟數據,也沒什么影響,只需要重試就好了。
但是會存在讀寫并發導致數據不一致
的情況:
W | R |
---|---|
刪除緩存 | - |
- | 讀緩存,緩存中沒有值 |
- | 讀數據庫,數據庫中得到結果為10 |
更新數據庫,更新成20 | |
- | 寫緩存,更新成10(數據不一致) |
假如一個讀線程,在讀緩存的時候沒查到值,他就會去數據庫中查詢,但是如果自查詢到結果之后,更新緩存之前,數據庫被更新了,但是這個讀線程是完全不知道的,那么就導致最終緩存會被重新用一個"舊值"覆蓋掉。這也就導致了緩存和數據庫的不一致的現象。
- 先寫數據庫
如果我們先更新數據庫,再刪除緩存,有一個好處,那就是緩存刪除失敗的概率還是比較低的
,除非是網絡問題或者緩存服務器宕機的問題,否則大部分情況都是可以成功的。
并且這個方案還有一個好處,那就是數據庫是作為持久層存儲的,先更新數據庫就能確保數據先寫入持久層可以保證數據的可靠性和一致性,即使在刪除緩存失敗的情況下,數據庫中已有最新數據。
但是這個方案也存在一個問題,那就是先寫數據庫,后刪除緩存,如果第二步失敗了,會導致數據庫中的數據已經更新,但是緩存還是舊數據,導致數據不一致。
10. 為什么需要延遲雙刪,兩次刪除的原因是什么?
第一次刪除緩存的原因:
第一次之所以要選擇先刪除緩存,而不是直接更新數據庫,主要是因為先寫數據庫會存在一個比較關鍵的問題,那就是緩存的更新和數據庫的更新不是一個原子操作,那么就存在失敗的可能性。
如果寫數據庫成功了,但是刪緩存失敗了!那么就會導致數據不一致。
而如果先刪緩存成功了,后更新數據庫失敗了,沒關系,因為緩存刪除了就刪除了,又不是更新,不會有錯誤數據,也沒有不一致問題。
所以,為了避免這個因為兩個操作無法作為一個原子操作而導致的不一致問題,我們選擇先刪除緩存,再更新數據庫。這是第一次刪除緩存的原因。
第二次刪除緩存的原因:
一般來說,一些并發量不大的業務,這么做就已經可以了,先刪緩存,后更新數據(如果業務量不大,其實先更新數據庫,再刪除緩存其實也可以),基本上就能滿足業務上的需求了。
但是如果是并發量比較高的話,那么就可能存在讀寫并發導致的不一致
的情況
"讀寫并發"的問題會導致并發發生后,緩存中的數被讀線程寫進去臟數據,那么就只需要在寫線程在刪緩存、寫數據庫之后,延遲一段時間,再執行一把刪除動作就行了。
所以,為了避免因為先刪除緩存而導致的讀寫并發問題,所以引入了第二次緩存刪除。
有了第二次刪除,第一次還有意義嗎?
如果不要第一次刪除,只保留第二次刪除那么就這個流程就變成了:先更新數據庫, 再刪除緩存。
那么這個方案的缺點前面講過了,一旦刪除緩存失敗,就會導致數據不一致的問題。
那么延遲雙刪的第二次刪除不也一樣可能失敗嗎?
確實第二次刪除也還是有概率失敗,但是因為我們在延遲雙刪的方案中先做了一次刪除,而延遲雙刪的第二次刪除只為了嘗試解決
因為讀寫并發導致的不一致問題,或者說盡可能降低這種情況發生的概率。
11. 如何用SETNX實現分布式鎖?
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在時,為 key 設置指定的值。設置成功,返回 1 。 設置失敗,返回 0 。
(1) 獲取鎖
SETNX lock_key unique_value
lock_key
:鎖的名稱(全局唯一標識這把鎖)。
unique_value
:通常用一個隨機值或UUID,標識這個鎖是由哪個客戶端持有的,用于解鎖時確認鎖的歸屬。
(2) 設置鎖的過期時間
因為 SETNX 本身不會設置過期時間,如果客戶端崩潰而未主動釋放鎖,鎖會被永久占用。
所以通常需要在 SETNX
成功后,立即設置一個過期時間:
EXPIRE lock_key 10 # 設置10秒過期
問題:SETNX 和 EXPIRE 是兩條命令,不是原子操作。可能在 SETNX 成功后、EXPIRE 執行前客戶端崩潰,導致鎖無過期時間。
解決方案: Redis 2.6.12+ 支持
SET lock_key unique_value NX EX 10
NX:等價于 SETNX,只在鍵不存在時設置。
EX 10:設置過期時間為 10 秒。
這是一個原子操作,可以避免上面的問題。
(3) 釋放鎖
釋放鎖時,必須 確保自己加的鎖自己才能解,否則可能會誤刪別人的鎖。
實現方法:
-
先 GET lock_key,判斷 value 是否等于自己持有的 unique_value。
-
如果相等,則執行 DEL lock_key。
但這不是原子操作,可能在 GET 后,其他客戶端已經獲得鎖。
為保證原子性,需要使用 Lua 腳本:
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
優點
(1)實現簡單:SETNX命令實現簡單,易于理解和使用。
(2)性能較高:由于SETNX命令的執行原子性,保證了分布式鎖的正確性,SETNX命令是單線程執行的,所以性能較高。
缺點
(1)鎖無法續期:如果加鎖方在加鎖后的執行時間較長,而鎖的超時時間設置的較短,可能導致鎖被誤釋放。
(2)無法避免死鎖:如果加鎖方在加鎖后未能及時解鎖(也未設置超時時間),且該客戶端崩潰,可能導致死鎖。
(3)存在競爭:由于SETNX命令是對Key的操作,所以在高并發情況下,多個客戶端之間仍可能存在競爭,從而影響性能。
(4)setnx不支持可重入,可以借助redission封裝的能力實現可重入鎖。
12. 如何用Redisson實現分布式鎖?
在使用SETNX實現的分布式鎖中,因為存在鎖無法續期導致并發沖突的問題,所以在真實的生產環境中用的并不是很多,其實,真正在使用Redis時,用的比較多的是基于Redisson實現分布式鎖。
為了避免鎖超時,Redisson中引入了看門狗的機制,他可以幫助我們在Redisson實例被關閉前,不斷的延長鎖的有效期。
可重入鎖
基于Redisson可以非常簡單的就獲取一個可重入的分布式鎖。基本步驟如下:
引入依賴
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>最新版</version>
</dependency>
定義一個Redisson客戶端:
@Configuration
public class RedissonConfig {@Bean(destroyMethod="shutdown")public RedissonClient redisson() throws IOException {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);return redisson;}
}
接下來,在想要使用分布式鎖的地方做如下調用即可:
@Service
public class LockTestService{@AutowiredRedissonClient redisson;public void testLock(){RLock lock = redisson.getLock("myLock");try {lock.lock();// 執行需要加鎖的代碼} finally {lock.unlock();}}
}
也可以設置超時時間:
// 設置鎖的超時時間為30秒
lock.lock(30, TimeUnit.SECONDS);
try {// 執行需要保護的代碼
} finally {lock.unlock();
}
且這個鎖也只能被這個線程解鎖。Redisson 的 unlock 方法在解鎖時,會去判斷當前線程 ID 是否存在于redis 的加鎖的 hash 結構中,如果有則認為可以解鎖,如果沒有,則無法解鎖。
除了可重入鎖以外,Redisson還支持公平鎖(FairLock)以及聯鎖(MultiLock)的使用。
公平鎖(FairLock)
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lock();
聯鎖(MultiLock)
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3
// 所有的鎖都上鎖成功才算成功。
lock.lock();
...
lock.unlock();
13. 公平鎖和非公平鎖的區別?
非公平鎖
:多個線程不按照申請鎖的順序去獲得鎖,而是直接去嘗試獲取鎖,獲取不到,再進入隊列等待,如果能獲取到,就直接獲取到鎖。
公平鎖
:多個線程按照申請鎖的順序去獲得鎖,所有線程都在隊列里排隊,這樣就保證了隊列中的第一個先得到鎖。
兩種鎖分別適合不同的場景中,存在著各自的優缺點,對于公平鎖來說,他的優點是所有的線程都能得到資源,不會餓死在隊列中。但是他存在著吞吐量會下降很多,隊列里面除了第一個線程,其他的線程都會阻塞,cpu喚醒阻塞線程的開銷會很大的缺點。
而對于非公平鎖來說,他可以減少CPU喚醒線程的開銷,整體的吞吐效率會高點,CPU也不必去喚醒所有線程,會減少喚起線程的數量。但是他可能會導致隊列中排隊的線程一直獲取不到鎖或者長時間獲取不到鎖,活活餓死的情況。
默認一般使用非公平鎖,它的效率和吞吐量都比公平鎖高的多
14. Redisson看門狗(watch dog)機制了解嗎?
Redisson的看門狗(watch dog)主要用來避免Redis中的鎖在超時后業務邏輯還未執行完畢,鎖卻被自動釋放的情況。它通過定期刷新鎖的過期時間來實現自動續期。
主要原理:
定時刷新
:如果當前分布式鎖未設置過期時間,Redisson基于Netty時間輪啟動一個定時任務,定期向Redis發送命令更新鎖的過期時間,默認每10s發送一次請求,每次續期30s。釋放鎖
:當客戶端主動釋放鎖時,Redisson會取消看門狗刷新操作。一旦客戶端宕機,看門狗線程自然消失,鎖也會在 30 秒后自動過期。
15. 什么是RedLock,他解決了什么問題?
RedLock是Redis的作者提出的一個多節點分布式鎖算法,旨在解決使用單節點Redis分布式鎖可能存在的單點故障問題。
Redis的單點故障問題:
1、在使用單節點Redis實現分布式鎖時,如果這個Redis實例掛掉,那么所有使用這個實例的客戶端都會出現無法獲取鎖的情況。
2、當使用集群模式部署的時候,如果master一個客戶端在master節點加鎖成功了,然后沒來得及同步數據到其他節點上,他就掛了, 那么這時候如果選出一個新的節點,再有客戶端來加鎖的時候,就也能加鎖成功,因為數據沒來得及同步,新的master會認為這個key是不存在的。
RedLock通過使用多個Redis節點,來提供一個更加健壯的分布式鎖解決方案,能夠在某些Redis節點故障的情況下,仍然能夠保證分布式鎖的可用性。
RedLock是通過引入多個Redis節點來解決單點故障的問題。
在進行加鎖操作時,RedLock會向每個Redis節點發送相同的命令請求,每個節點都會去競爭鎖,如果至少在大多數節點上成功獲取了鎖,那么就認為加鎖成功。反之,如果大多數節點上沒有成功獲取鎖,則加鎖失敗。這樣就可以避免因為某個Redis節點故障導致加鎖失敗的情況發生。
這樣,當超過半數以上的節點都寫入成功之后,即使master掛了,新選出來的master也能保證剛剛的那個key一定存在(否則這個節點就不會被選為master)。
需要注意的是,RedLock并不能完全解決分布式鎖的問題。例如,在腦裂的情況下,RedLock可能會產生兩個客戶端同時持有鎖的情況。
16. Redis的哨兵機制?
主從架構中,如果采用讀寫分離的模式,即主節點負責寫請求,從節點負責讀請求。假設這個時候主節點宕機了,沒有新的
主節點頂替上來的話,就會出現很長一段時間寫請求沒響應的情況。
針對這個情況,便出現了哨兵這個機制。它主要進行監控作用,如果主節點掛了,將從節點切換成主節點,從而最大限度地
減少停機時間和數據丟失。
哨兵節點(Sentinel)
:主要作用是對Redis的主從服務節點進行監控,當主節點發生故障的時候,哨兵節點會選擇一個
合適的從節點升級為主節點,并通知其他從節點和客戶端進行更新操作。Redis節點
:主要包括master以及slave節點,就是Redis提供服務的實例。
主觀下線和客觀下線
主觀下線
Sentinel每隔1s會發送ping命令給所有的節點。如果Sentinel超過一段時間還未收到對應節點的pong回復,就會認為
這個節點主觀下線。
客觀下線
假設目前有個主節點被一個sentinel的判斷主觀下線了,但可能主節點并沒問題,只是因為網絡抖動導致了一臺哨兵的誤判。因此,它會向其他哨兵發起投票,其他哨兵會判斷主節點的狀態進行投票,可以投贊成或反對,以此來確定這個主節點是不是真的出了問題!
如果認為下線的總投票數大于quorum(一般為集群總數/2+1,假設哨兵集群有3臺實例,那么3/2+1=2),則判定
該主節點客觀下線,此時就需要進行主從切換,而只有哨兵的leader才能操作主從切換。
17. 介紹一下Redis的集群模式?
Redis有三種主要的集群模式,用于在分布式環境中實現高可用性和數據復制。這些集群模式分別是:主從復制(Master-Slave Replication)
、哨兵模式(Sentinel
)和Redis Cluster模式
主從復制
主從模式中,包括一個主節點(Master)和一個或多個從節點(Slave)。主節點負責處理所有寫操作和讀操作,而從節點則復制主節點的數據,并且只能處理讀操作。當主節點發生故障時,可以將一個從節點升級為主節點,實現故障轉移(需要手動實現)。
主從復制的優勢在于簡單易用,適用于讀多寫少的場景。它提供了數據備份功能,并且可以有很好的擴展性,只要增加更多的從節點,就能讓整個集群的讀的能力不斷提升。
但是主從模式最大的缺點,就是不具備故障自動轉移的能力,沒有辦法做容錯和恢復。
哨兵模式
為了解決主從模式的無法自動容錯及恢復的問題,Redis引入了一種哨兵模式的集群架構。
哨兵模式是在主從復制的基礎上加入了哨兵節點。哨兵節點是一種特殊的Redis節點,用于監控主節點和從節點的狀態。當主節點發生故障時,哨兵節點可以自動進行故障轉移,選擇一個合適的從節點升級為主節點,并通知其他從節點和應用程序進行更新。
在原來的主從架構中,引入哨兵節點,其作用是監控Redis主節點和從節點的狀態。通常需要部署多個哨兵節點,以確保故障轉移的可靠性。
哨兵節點定期向所有主節點和從節點發送PING命令,如果在指定的時間內未收到PONG響應,哨兵節點會將該節點標記為主觀下線。如果一個主節點被多數哨兵節點標記為主觀下線,那么它將被標記為客觀下線。
當主節點被標記為客觀下線時,哨兵節點會觸發故障轉移過程。它會從所有健康的從節點中選舉一個新的主節點,并將所有從節點切換到新的主節點,實現自動故障轉移。同時,哨兵節點會更新所有客戶端的配置,指向新的主節點。
哨兵節點通過發布訂閱功能來通知客戶端有關主節點狀態變化的消息。客戶端收到消息后,會更新配置,將新的主節點信息應用于連接池,從而使客戶端可以繼續與新的主節點進行交互。
這個哨兵模式的優點就是為整個集群提供了一種故障轉移和恢復的能力。
Cluster模式
Redis Cluster是Redis中推薦的分布式集群解決方案。它將數據自動分片到多個節點上,每個節點負責一部分數據。
在Redis的Cluster 集群模式中,使用哈希槽(hash slot)的方式來進行數據分片,將整個數據集劃分為多個槽,每個槽分配給一個節點。客戶端訪問數據時,先計算出數據對應的槽,然后直接連接到該槽所在的節點進行操作。
Redis Cluster將整個數據集劃分為16384個槽,每個槽都有一個編號(0~16383),集群的每個節點可以負責多個hash槽,客戶端訪問數據時,先根據key計算出對應的槽編號,然后根據槽編號找到負責該槽的節點,向該節點發送請求。
18. 介紹下Redis集群的腦裂問題?
腦裂是指在分布式系統中,由于網絡分區或其他問題導致系統中的多個節點(特別是主節點)
誤以為自己是唯一的主節點
。
這種情況會導致多個主節點同時提供寫入服務,從而引起數據不一致。
為什么會產生腦裂?
Redis的腦裂問題可能發生在網絡分區或者主節點出現問題的時候:
-
網絡分區
:網絡故障或分區導致了不同子集之間的通信中斷。
Master節點,哨兵和Slave節點被分割為了兩個網絡,Master處在一個網絡中,Slave庫和哨兵在另外一個網絡中,此時哨兵發現和Master連不上了,就會發起主從切換,選一個新的Master,這時候就會出現兩個主節點的情況。 -
主節點問題
:集群中的主節點之間出現問題,導致不同的子集認為它們是正常的主節點。
Master節點有問題,哨兵就會開始選舉新的主節點,但是在這個過程中,原來的那個Master節點又恢復了,這時候就可能會導致一部分Slave節點認為他是Master節點,而另一部分Slave新選出了一個Master
如何避免腦裂?
配置參數:
min-replicas-to-write 1
min-replicas-max-lag 10
含義:
-
至少有 1 個從節點且同步延遲不超過 10 秒時,主節點才接受寫操作。
-
如果主節點與從節點斷開(或者延遲太大),主節點將拒絕寫請求,防止腦裂數據不一致。
19. Redis為什么被設計成是單線程的?
Redis并沒有在網絡請求模塊和數據操作模塊中使用多線程模型,主要是基于以下四個原因:
- Redis 操作基于內存,絕大多數操作的性能瓶頸不在 CPU
- 單線程模型,避免了線程間切換帶來的性能開銷
- 在單線程中使用多路復用 I/O技術也能提升Redis的I/O利用率
20. 為什么Redis 6.0引入了多線程?
雖然之前采用了多路復用技術,但是多路復用的IO模型本質上仍然是同步阻塞型IO模型。
從上圖我們可以看到,在多路復用的IO模型中,在處理網絡請求時,調用 select (其他函數同理)的過程是阻塞的,也就是說這個過程會阻塞線程,如果并發量很高,此處可能會成為瓶頸。
如果能采用多線程,使得網絡處理的請求并發進行,就可以大大的提升性能。多線程除了可以減少由于網絡 I/O 等待造成的影響,還可以充分利用 CPU 的多核優勢。
所以,Redis 6.0采用多個IO線程來處理網絡請求,網絡請求的解析可以由其他線程完成,然后把解析后的請求交由主線程進行實際的內存讀寫。提升網絡請求處理的并行度,進而提升整體性能。
但是,Redis 的多 IO 線程只是用來處理網絡請求的,對于讀寫命令,Redis 仍然使用單線程來處理。
21. 為什么Lua腳本可以保證原子性?
Lua腳本可以保證原子性,因為Redis會將Lua腳本封裝成一個單獨的事務,而這個單獨的事務會在Redis客戶端運行時,由Redis服務器自行處理并完成整個事務,如果在這個進程中有其他客戶端請求的時候,Redis將會把它暫存起來,等到 Lua 腳本處理完畢后,才會再把被暫存的請求恢復。
這樣就可以保證整個腳本是作為一個整體執行的,中間不會被其他命令插入。但是,如果命令執行過程中命令產生錯誤,事務是不會回滾的,將會影響后續命令的執行。
也就是說,Redis保證以原子方式執行Lua腳本,但是不保證腳本中所有操作要么都執行或者都回滾。
22. Redis 的事務機制是怎樣的?
Redis中是支持事務的,他的事務主要目的是保證多個命令執行的原子性,即要在一個原子操作中執行,不會被打斷。
需要注意的是,Redis的事務是不支持回滾的
從 Redis 2.6.5 開始,服務器會在累積命令的過程中檢測到錯誤。然后,在執行 EXEC 期間會拒絕執行事務,并返回一個錯誤,同時丟棄該事務。
如果事務執行過程中發生錯誤,Redis會繼續執行剩余的命令而不是回滾整個事務。
Redis錯誤有兩種情況,一種是在命令排隊的過程就就檢測到的錯誤,比如語法錯誤,比如內存不夠了,等等。在這種錯誤,會在調用 EXEC 后,命令可能會直接失敗。
還有一種錯誤,是在調用 EXEC 后,命令執行過程中出現的錯誤,最常見的就是操作類型不一致,比如對字符串進行列表相關的操作。這種就是在執行過程中才會出現的。
23. Redis中key過期了一定會立即刪除嗎?
Redis的鍵有兩種過期方式:一種是被動過期,另一種是主動過期。
被動過期指的是當某個客戶端嘗試訪問一個鍵,發現該鍵已經超時,那么它會被從Redis中刪除。
當然,僅僅依靠被動過期還不夠,因為有些過期的鍵可能永遠不會再被訪問。這些鍵應該被及時刪除,因此Redis會定期隨機檢查一些帶有過期時間的鍵。所有已經過期的鍵都會從鍵空間中刪除。
具體來說,Redis每秒會執行以下操作10次:
- 從帶有過期時間的鍵集合中隨機選擇20個鍵。
- 刪除所有已經過期的鍵。
- 如果已經過期的鍵占比超過25%,則重新從步驟1開始。
直到過期Key的比例下降到 25% 或者這次任務的執行耗時超過了25毫秒,才會退出循環
所以,Redis其實是并不保證Key在過期的時候就能被立即刪除的。因為一方面惰性刪除中需要下次訪問才會刪除,即使是主動刪除,也是通過輪詢的方式來實現的。如果要過期的key很多的話,就會帶來延遲的情況。
24. Redisson的lock和tryLock有什么區別?
tryLock是嘗試獲取鎖,如果能獲取到直接返回true,如果無法獲取到鎖,他會按照我們指定的waitTime進行阻塞,在這個時間段內他還會再嘗試獲取鎖。如果超過這個時間還沒獲取到則返回false。如果我們沒有指定waitTime,那么他就在未獲取到鎖的時候,就直接返回false了。
lock的原理是以阻塞的方式去獲取鎖,如果獲取鎖失敗會一直等待,直到獲取成功。
25. 什么是Redis的Pipeline,和事務有什么區別?
Redis 的 Pipeline 機制是一種用于優化網絡延遲的技術,主要用于在單個請求/響應周期內執行多個命令。在沒有 Pipeline 的情況下,每執行一個 Redis 命令,客戶端都需要等待服務器響應之后才能發送下一個命令。這種往返通信尤其在網絡延遲較高的環境中會顯著影響性能。
在 Pipeline 模式下,客戶端可以一次性發送多個命令到 Redis 服務器,而無需等待每個命令的響應。Redis 服務器接收到這批命令后,會依次執行它們并返回響應。
所以,Pipeline通過減少客戶端與服務器之間的往返通信次數,可以顯著提高性能,特別是在執行大量命令的場景中。
但是,需要注意的是,Pipeline是不保證原子性的,他的多個命令都是獨立執行的,Redis并不保證這些命令可以以不可分割的原子操作進行執行。這是Pipeline和Redis的事務的最大的區別。
雖然都是執行一些相關命令,但是Redis的事務提供了原子性保障,保證命令執行以不可分割、不可中斷的原子性操作進行,而Pipeline則沒有原子性保證。
但是他們在命令執行上有一個相同點,那就是如果執行多個命令過程中,有一個命令失敗了,其他命令還是會被執行,而不會回滾的。
26. 為什么Redis不支持回滾?
Redis是不支持回滾的,即使是Redis的事務和Lua腳本,在執行的過程中,如果出現了錯誤,也是無法回滾的
因為
-
Redis的設計就是簡單、高效等,所以引入事務的回滾機制會讓系統更加的復雜,并且影響性能。
-
從使用場景上來說,Redis一般都是被用作緩存的,不太需要很復雜的事務支持,當人們需要復雜的事務時會考慮持久化的關系型數據庫。
-
相比于關系型數據庫,Redis是通過單線程執行的,在執行過程中,出現錯誤的概率比較低,并且這些問題一般在編譯階段都應該被發現,所以就不太需要引入回滾機制。
27. 如何用Redis實現樂觀鎖?
在Redis中,想要實現這個功能,我們可以依賴 WATCH 命令。這個命令一旦運行,他會確保只有在 WATCH 監視的鍵在調用 EXEC 之前沒有改變時,后續的事務才會執行。
WATCH counter
GET counter
MULTI
SET counter <從 GET 獲得的值 + 任何增量>
EXEC
EXEC:使用 EXEC 命令執行事務。如果自從事務開始以來監視的鍵被修改過,EXEC 將返回 nil,這表示事務中的命令沒有被執行。
通過這種方式,Redis 保證了只有在監視的數據自事務開始以來沒有改變的情況下,事務才會執行,從而實現了樂觀鎖定。
28. Redisson解鎖失敗,watchdog會不會一直續期下去?
不會的,因為在解鎖過程中,不管是解鎖失敗了,還是解鎖時拋了異常,都還是會把本地的續期任務停止,避免下次續期。
29. Redis中的setnx和setex有啥區別?
SETNX ,SET if Not eXists , 只有鍵不存在時才設置值,不能設置過期時間
SETEX , SET with EXpiration, 設置值并指定過期時間,無條件進行設置,并帶有過期時間
30. Redis 與 Memcached 有什么區別?
Redis 和 Memcached 都是常見的緩存服務器,它們的主要區別包括以下幾個方面:
數據結構不同
:Redis 提供了多種數據結構,如字符串、哈希表、列表、集合、有序集合等,而 Memcached 只支持簡單的鍵值對存儲。持久化方式不同
:Redis 支持多種持久化方式,如 RDB 和 AOF,可以將數據持久化到磁盤上;而 Memcached 不支持持久化。處理數據的方式不同
:Redis 使用單線程處理數據請求,支持事務、Lua 腳本等高級功能;而 Memcached 使用多線程處理數據請求,只支持基本的 GET、SET 操作。內存管理方式不同
:Redis 的內存管理比 Memcached 更加復雜,支持更多的內存優化策略。
Redis 和 Memcached 有著不同的設計理念和應用場景。Redis 適用于數據結構復雜、需要高級功能和數據持久化的場景;而 Memcached 則適用于簡單的鍵值存儲場景。
31. Redis實現分布鎖的時候,哪些問題需要考慮?
鎖的互斥性
對于鎖的互斥性,可以借助setnx來保證,因為這個操作本身就是一個原子性操作,并且結合Redis的單線程的機制,就可以保證互斥性。
鎖的可重入性
至于可重入性,其實就是說一個線程,在鎖沒有釋放的情況下,他是可以反復的拿到同一把鎖的。并且需要在鎖中記錄加鎖次數,用來保證重入幾次就需要解鎖幾次。用setnx也是可以實現的。
如果我們直接使用Redisson的話,他是支持可重入鎖的實現的。可以直接用。
鎖的性能
因為Redis是基于內存的,所以他的性能也是很高的。
誤解鎖問題
要確保只有鎖的持有者能釋放鎖,避免其他客戶端誤解鎖。
鎖的有效時間
為了避免死鎖,我們一般會給一個分布式鎖設置一個超時時間,如上面我們用的setnx的方案,其實就是設置了一個超時時間的。
但是有的是,代碼如果執行的比較慢的話,比如設置的超時時間是3秒,但是代碼執行了5秒,那么就會導致在第三秒的時候,key超時了就自動解鎖了,那么其他的線程就可以拿到鎖了,這時候就會發生并發的問題了。
可以像redisson一樣,實現一個watch dog的機制,給鎖自動做續期,讓鎖不會提前釋放。
32. 如何用setnx實現一個可重入鎖?
可重入鎖是一種多線程同步機制,允許同一線程多次獲取同一個鎖而不會導致死鎖。
加鎖的邏輯:
- 當線程嘗試獲取鎖時,它首先檢查鎖是否已經存在。
- 如果鎖不存在(即 SETNX 返回成功),線程設置鎖,存儲自己的標識符和計數器(初始化為1)。
- 如果鎖已存在,線程檢查鎖中的標識符是否與自己的相同。
- 如果是,線程已經持有鎖,只需增加計數器的值。
- 如果不是,獲取鎖失敗,因為鎖已被其他線程持有。
解鎖的邏輯:
- 當線程釋放鎖時,它會減少計數器的值。
- 如果計數器降至0,這意味著線程已完成對鎖的所有獲取請求,可以完全釋放鎖。
- 如果計數器大于0,鎖仍被視為被該線程持有。
33. Redis 支持哪幾種數據類型?
Redis 中支持了多種數據類型,其中比較常用的有五種:
- 字符串(String)
- 哈希(Hash)
- 列表(List)
- 集合(Set)
- 有序集合(Sorted Set)也稱為ZSet
另外,Redis中還支持一些高級的數據類型,如:Streams、Bitmap、Geospatial以及HyperLogLog
34. Redis中跳表的實現原理?(實現ZSet的主要數據結構)
跳表主要是通過多層鏈表來實現,底層鏈表保存所有元素,而每一層鏈表都是下一層的子集。
- 插入時,首先從最高層開始查找插入位置,然后隨機決定新節點的層數,最后在相應的層中插入節點并更新指針。
- 刪除時,同樣從最高層開始查找要刪除的節點,并在各層中更新指針,以保持跳表的結構。
- 查找時,從最高層開始,逐層向下,直到找到目標元素或確定元素不存在。查找效率高,時間復雜度為O(logn)
35. Redis為什么要自己定義SDS?
Redis自己本身是通過C語言實現的,但是他并沒有直接使用C語言中的字符數組的方式來實現字符串,而是自己實現了一個SDS(Simple Dynamic Strings),即簡單動態字符串,這是為什么呢?
C語言字符串的問題:
- 在C語言中,當識別到字符數組中的\0字符的時候,就認為字符串結束了,這樣實現的字符串中就不能保存任意內容了。
- C中的字符串以\0作為識別字符串結束的方式,所以他的字符串長度判斷、字符串追加等操作,都需要從頭開始遍歷,一直遍歷到\0的時候再返回長度或者做追加。這就使得字符串相關的操作效率都很低。
解決辦法:
-
在這個字符串中增加一個表示分配給該字符數組的總長度的alloc字段,和一個表示字符串現有長度的len字段。這樣在獲取長度的時候就不依賴\0了,直接返回len的值就行了。
-
在做追加操作的時候,只需要判斷新追加的部分的len加上已有的len是否大于alloc,如果超過就重新再申請新空間,如果沒超過,就直接進行追加就行了。
36. Redis除了做緩存,Redis還能用來干什么?
消息隊列(不建議)
:Redis 支持發布/訂閱模式和Stream,可以作為輕量級消息隊列使用,用于異步處理任務或處理高并發請求。延遲消息(不建議)
:Redis的ZSET可以用來實現延遲消息,也可以基于Key的過期消息實現延遲消息,還可以借助Redisson的RDelayQueue來實現延遲消息,都是可以的。排行榜(建議)
:利用Redis 的有序集合和列表結構,可以成為設計實時排行榜的絕佳選擇,例如各類熱門排行榜、熱門商品列表等。計數器(建議)
:基于Redis可以實現一些計數器的功能,比如網站的訪問量、朋友圈點贊等。通過 incr 命令就能實現原子性的自增操作,從而實現一個全局計數器。·分布式ID(可以)
:因為他有全局自增計數的功能,所以在分布式場景,我們也可以利用Redis來實現一個分布式ID來保障全局的唯一且自增。分布式鎖(建議)
:Redis 的單線程特性可以保證多個客戶端之間對同一把鎖的操作是原子性的,可以輕松實現分布式鎖,用于控制多個進程對共享資源的訪問。地理位置應用(建議)
:Redis 支持GEO,支持地理位置定位和查詢,可以存儲地理位置信息并通過 Redis 的查詢功能獲取附近的位置信息。比如"附近的人"用它來實現就非常方便。分布式限流(可以)
:Redis提供了令牌桶和漏桶算法的實現,可以用于實現分布式限流。分布式Session(建議)
:可以使用Redis實現分布式Session管理,保證多臺服務器之間用戶的會話狀態同步。布隆過濾器(建議)
:Redis提供了布隆過濾器(Bloom Filter)數據結構的實現,可以高效地檢測一個元素是否存在于一個集合中狀態統計(數據量大建議用)
:Redis中支持BitMap這種數據結構,它不僅查詢和存儲高效,更能節省很多空間,所以我們可以借助他做狀態統計,比如記錄億級用戶的登錄狀態,或者是拿他來做簽到統計也比較常見。共同關注(建議)
:Redis中支持Set集合類型,這個類型非常適合我們做一些取并集、交集、差集等,基于這個特性,我們就能取交集的方式非常方便的實現共同好友、或者共同關注的功能。推薦關注(可以)
:和上面的共同關注類似,交集實現共同好友,那么并集或者差集就能實現推薦關注的功能。
37. Redis如何實現延遲消息?
Redis的zset實現延遲消息
我們可以借助Redis中的有序集合——zset來實現這個功能。
zset是一個有序集合,每一個元素(member)都關聯了一個 score,可以通過 score 排序來取集合中的值。
我們將訂單超時時間的時間戳(下單時間+超時時長)與訂單號分別設置為 score 和 member。這樣redis會對zset按照score延時時間進行排序。然后我們再開啟redis掃描任務,獲取”當前時間 > score”的延時任務,掃描到之后取出訂單號,然后查詢到訂單進行關單操作即可。
使用redis zset來實現訂單關閉的功能的優點是可以借助redis的持久化、高可用機制。避免數據丟失。但是這個方案也有缺點,那就是在高并發場景中,有可能有多個消費者同時獲取到同一個訂單號,一般采用加分布式鎖解決,但是這樣做也會降低吞吐型。
Redission實現延遲消息
Redission中定義了分布式延遲隊列RDelayedQueue,這是一種基于zset結構實現的延時隊列,它允許以指定的延遲時長將元素放到目標隊列中。
調用 RDelayedQueue.offer(message, delay, TimeUnit.SECONDS)
,該消息被序列化并存入一個 ZSet,score = 當前時間 + delay,Redisson 的后臺線程會不斷掃描 ZSet,找到 score <= 當前時間 的消息,然后將它們投遞到消費者隊列,消費者通過 RQueue.take()
獲取到期消息。
例子:
// 1. 定義一個真正的消費者隊列 RQueue
RQueue<String> queue = redisson.getQueue("myQueue");// 2. 獲取一個延遲隊列 RDelayedQueue,并綁定到 RQueue
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(queue);// 3. 向延遲隊列投遞消息
delayedQueue.offer("task1", 5, TimeUnit.SECONDS);
執行過程:
-
task1 會先存入 ZSet(score = 當前時間 + 5 秒)。
-
當 5 秒后,Redisson 的后臺線程會把 task1 移動到 myQueue(RQueue)。
消費者可以:
String msg = queue.take(); // 阻塞等待獲取消息