Redis面試題
更多技術(shù)干貨、面試筆記、自學(xué)Java經(jīng)歷,可以關(guān)注微信公眾號:【程序員大彬】
本文目錄:
- Redis是什么?
- Redis優(yōu)缺點?
- Redis為什么這么快?
- Redis為何選擇單線程
- Redis6.0為何引入多線程?
- Redis應(yīng)用場景有哪些?
- Memcached和Redis的區(qū)別?
- Redis 數(shù)據(jù)類型有哪些?
- keys命令存在的問題?
- SortedSet和List異同點?
- Redis事務(wù)
- 持久化機制
- RDB方式
- AOF方式
- RDB和AOF如何選擇?
- Redis常見的部署方式有哪些?
- 主從復(fù)制
- 哨兵Sentinel
- Redis cluster
- 哈希分區(qū)算法有哪些?
- 過期鍵的刪除策略?
- 內(nèi)存淘汰策略有哪些?
- 如何保證緩存與數(shù)據(jù)庫雙寫時的數(shù)據(jù)一致性?
- 緩存穿透
- 緩存雪崩
- 緩存擊穿
- Redis 怎么實現(xiàn)消息隊列?
- pipeline的作用?
- LUA腳本
- 什么是RedLock?
Redis是什么?
Redis(Remote Dictionary Server
)是一個使用 C 語言編寫的,高性能非關(guān)系型的鍵值對數(shù)據(jù)庫。與傳統(tǒng)數(shù)據(jù)庫不同的是,Redis 的數(shù)據(jù)是存在內(nèi)存中的,所以讀寫速度非???,被廣泛應(yīng)用于緩存方向。Redis可以將數(shù)據(jù)寫入磁盤中,保證了數(shù)據(jù)的安全不丟失,而且Redis的操作是原子性的。
Redis優(yōu)缺點?
優(yōu)點:
- 基于內(nèi)存操作,內(nèi)存讀寫速度快。
- Redis是單線程的,避免線程切換開銷及多線程的競爭問題。單線程是指網(wǎng)絡(luò)請求使用一個線程來處理,即一個線程處理所有網(wǎng)絡(luò)請求,Redis 運行時不止有一個線程,比如數(shù)據(jù)持久化的過程會另起線程。
- 支持多種數(shù)據(jù)類型,包括String、Hash、List、Set、ZSet等。
- 支持持久化。Redis支持RDB和AOF兩種持久化機制,持久化功能可以有效地避免數(shù)據(jù)丟失問題。
- 支持事務(wù)。Redis的所有操作都是原子性的,同時Redis還支持對幾個操作合并后的原子性執(zhí)行。
- 支持主從復(fù)制。主節(jié)點會自動將數(shù)據(jù)同步到從節(jié)點,可以進(jìn)行讀寫分離。
缺點:
- 對結(jié)構(gòu)化查詢的支持比較差。
- 數(shù)據(jù)庫容量受到物理內(nèi)存的限制,不適合用作海量數(shù)據(jù)的高性能讀寫,因此Redis適合的場景主要局限在較小數(shù)據(jù)量的操作。
- Redis 較難支持在線擴容,在集群容量達(dá)到上限時在線擴容會變得很復(fù)雜。
Redis為什么這么快?
基于內(nèi)存:Redis是使用內(nèi)存存儲,沒有磁盤IO上的開銷。數(shù)據(jù)存在內(nèi)存中,讀寫速度快。
單線程實現(xiàn)( Redis 6.0以前):Redis使用單個線程處理請求,避免了多個線程之間線程切換和鎖資源爭用的開銷。
IO多路復(fù)用模型:Redis 采用 IO 多路復(fù)用技術(shù)。Redis 使用單線程來輪詢描述符,將數(shù)據(jù)庫的操作都轉(zhuǎn)換成了事件,不在網(wǎng)絡(luò)I/O上浪費過多的時間。
高效的數(shù)據(jù)結(jié)構(gòu):Redis 每種數(shù)據(jù)類型底層都做了優(yōu)化,目的就是為了追求更快的速度。
Redis為何選擇單線程
- 避免過多的上下文切換開銷。程序始終運行在進(jìn)程中單個線程內(nèi),沒有多線程切換的場景。
- 避免同步機制的開銷:如果 Redis選擇多線程模型,需要考慮數(shù)據(jù)同步的問題,則必然會引入某些同步機制,會導(dǎo)致在操作數(shù)據(jù)過程中帶來更多的開銷,增加程序復(fù)雜度的同時還會降低性能。
- 實現(xiàn)簡單,方便維護:如果 Redis使用多線程模式,那么所有的底層數(shù)據(jù)結(jié)構(gòu)的設(shè)計都必須考慮線程安全問題,那么 Redis 的實現(xiàn)將會變得更加復(fù)雜。
Redis6.0為何引入多線程?
Redis支持多線程主要有兩個原因:
可以充分利用服務(wù)器 CPU 資源,單線程模型的主線程只能利用一個cpu;
多線程任務(wù)可以分?jǐn)?Redis 同步 IO 讀寫的負(fù)荷。
Redis應(yīng)用場景有哪些?
- 緩存熱點數(shù)據(jù),緩解數(shù)據(jù)庫的壓力。
- 利用 Redis 原子性的自增操作,可以實現(xiàn)計數(shù)器的功能,比如統(tǒng)計用戶點贊數(shù)、用戶訪問數(shù)等。
- 簡單的消息隊列,可以使用Redis自身的發(fā)布/訂閱模式或者List來實現(xiàn)簡單的消息隊列,實現(xiàn)異步操作。
- 限速器,可用于限制某個用戶訪問某個接口的頻率,比如秒殺場景用于防止用戶快速點擊帶來不必要的壓力。
- 好友關(guān)系,利用集合的一些命令,比如交集、并集、差集等,實現(xiàn)共同好友、共同愛好之類的功能。
Memcached和Redis的區(qū)別?
- Redis 只使用單核,而 Memcached 可以使用多核。
- MemCached 數(shù)據(jù)結(jié)構(gòu)單一,僅用來緩存數(shù)據(jù),而 Redis 支持多種數(shù)據(jù)類型。
- MemCached 不支持?jǐn)?shù)據(jù)持久化,重啟后數(shù)據(jù)會消失。Redis 支持?jǐn)?shù)據(jù)持久化。
- Redis 提供主從同步機制和 cluster 集群部署能力,能夠提供高可用服務(wù)。Memcached 沒有提供原生的集群模式,需要依靠客戶端實現(xiàn)往集群中分片寫入數(shù)據(jù)。
- Redis 的速度比 Memcached 快很多。
- Redis 使用單線程的多路 IO 復(fù)用模型,Memcached使用多線程的非阻塞 IO 模型。
Redis 數(shù)據(jù)類型有哪些?
基本數(shù)據(jù)類型:
1、String:最常用的一種數(shù)據(jù)類型,String類型的值可以是字符串、數(shù)字或者二進(jìn)制,但值最大不能超過512MB。
2、Hash:Hash 是一個鍵值對集合。
3、Set:無序去重的集合。Set 提供了交集、并集等方法,對于實現(xiàn)共同好友、共同關(guān)注等功能特別方便。
4、List:有序可重復(fù)的集合,底層是依賴雙向鏈表實現(xiàn)的。
5、SortedSet:有序Set。內(nèi)部維護了一個score
的參數(shù)來實現(xiàn)。適用于排行榜和帶權(quán)重的消息隊列等場景。
特殊的數(shù)據(jù)類型:
1、Bitmap:位圖,可以認(rèn)為是一個以位為單位數(shù)組,數(shù)組中的每個單元只能存0或者1,數(shù)組的下標(biāo)在 Bitmap 中叫做偏移量。Bitmap的長度與集合中元素個數(shù)無關(guān),而是與基數(shù)的上限有關(guān)。
2、Hyperloglog。HyperLogLog 是用來做基數(shù)統(tǒng)計的算法,其優(yōu)點是,在輸入元素的數(shù)量或者體積非常非常大時,計算基數(shù)所需的空間總是固定的、并且是很小的。典型的使用場景是統(tǒng)計獨立訪客。
3、Geospatial :主要用于存儲地理位置信息,并對存儲的信息進(jìn)行操作,適用場景如定位、附近的人等。
keys命令存在的問題?
redis的單線程的。keys指令會導(dǎo)致線程阻塞一段時間,直到執(zhí)行完畢,服務(wù)才能恢復(fù)。scan采用漸進(jìn)式遍歷的方式來解決keys命令可能帶來的阻塞問題,每次scan命令的時間復(fù)雜度是O(1)
,但是要真正實現(xiàn)keys的功能,需要執(zhí)行多次scan。
scan的缺點:在scan的過程中如果有鍵的變化(增加、刪除、修改),遍歷過程可能會有以下問題:新增的鍵可能沒有遍歷到,遍歷出了重復(fù)的鍵等情況,也就是說scan并不能保證完整的遍歷出來所有的鍵。
SortedSet和List異同點?
相同點:
- 都是有序的;
- 都可以獲得某個范圍內(nèi)的元素。
不同點:
- 列表基于鏈表實現(xiàn),獲取兩端元素速度快,訪問中間元素速度慢;
- 有序集合基于散列表和跳躍表實現(xiàn),訪問中間元素時間復(fù)雜度是OlogN;
- 列表不能簡單的調(diào)整某個元素的位置,有序列表可以(更改元素的分?jǐn)?shù));
- 有序集合更耗內(nèi)存。
Redis事務(wù)
事務(wù)的原理是將一個事務(wù)范圍內(nèi)的若干命令發(fā)送給Redis,然后再讓Redis依次執(zhí)行這些命令。
事務(wù)的生命周期:
使用MULTI開啟一個事務(wù)
在開啟事務(wù)的時候,每次操作的命令將會被插入到一個隊列中,同時這個命令并不會被真的執(zhí)行
EXEC命令進(jìn)行提交事務(wù)
一個事務(wù)范圍內(nèi)某個命令出錯不會影響其他命令的執(zhí)行,不保證原子性:
127.0.0.1:6379> multi OK 127.0.0.1:6379> set a 1 QUEUED 127.0.0.1:6379> set b 1 2 QUEUED 127.0.0.1:6379> set c 3 QUEUED 127.0.0.1:6379> exec 1) OK 2) (error) ERR syntax error 3) OK
WATCH命令
WATCH
命令可以監(jiān)控一個或多個鍵,一旦其中有一個鍵被修改,之后的事務(wù)就不會執(zhí)行(類似于樂觀鎖)。執(zhí)行EXEC
命令之后,就會自動取消監(jiān)控。
127.0.0.1:6379> watch name OK 127.0.0.1:6379> set name 1 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name 2 QUEUED 127.0.0.1:6379> set gender 1 QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get gender (nil)
比如上面的代碼中:
watch name
開啟了對name
這個key
的監(jiān)控- 修改
name
的值 - 開啟事務(wù)a
- 在事務(wù)a中設(shè)置了
name
和gender
的值 - 使用
EXEC
命令進(jìn)提交事務(wù) - 使用命令
get gender
發(fā)現(xiàn)不存在,即事務(wù)a沒有執(zhí)行
使用UNWATCH
可以取消WATCH
命令對key
的監(jiān)控,所有監(jiān)控鎖將會被取消。
持久化機制
持久化就是把內(nèi)存的數(shù)據(jù)寫到磁盤中,防止服務(wù)宕機導(dǎo)致內(nèi)存數(shù)據(jù)丟失。
Redis支持兩種方式的持久化,一種是RDB
的方式,一種是AOF
的方式。前者會根據(jù)指定的規(guī)則定時將內(nèi)存中的數(shù)據(jù)存儲在硬盤上,而后者在每次執(zhí)行完命令后將命令記錄下來。一般將兩者結(jié)合使用。
RDB方式
RDB
是 Redis 默認(rèn)的持久化方案。RDB持久化時會將內(nèi)存中的數(shù)據(jù)寫入到磁盤中,在指定目錄下生成一個dump.rdb
文件。Redis 重啟會加載dump.rdb
文件恢復(fù)數(shù)據(jù)。
bgsave
是主流的觸發(fā) RDB 持久化的方式,執(zhí)行過程如下:
- 執(zhí)行
BGSAVE
命令 - Redis 父進(jìn)程判斷當(dāng)前是否存在正在執(zhí)行的子進(jìn)程,如果存在,
BGSAVE
命令直接返回。 - 父進(jìn)程執(zhí)行
fork
操作創(chuàng)建子進(jìn)程,fork操作過程中父進(jìn)程會阻塞。 - 父進(jìn)程
fork
完成后,父進(jìn)程繼續(xù)接收并處理客戶端的請求,而子進(jìn)程開始將內(nèi)存中的數(shù)據(jù)寫進(jìn)硬盤的臨時文件; - 當(dāng)子進(jìn)程寫完所有數(shù)據(jù)后會用該臨時文件替換舊的 RDB 文件。
Redis啟動時會讀取RDB快照文件,將數(shù)據(jù)從硬盤載入內(nèi)存。通過 RDB 方式的持久化,一旦Redis異常退出,就會丟失最近一次持久化以后更改的數(shù)據(jù)。
觸發(fā) RDB 持久化的方式:
手動觸發(fā):用戶執(zhí)行
SAVE
或BGSAVE
命令。SAVE
命令執(zhí)行快照的過程會阻塞所有客戶端的請求,應(yīng)避免在生產(chǎn)環(huán)境使用此命令。BGSAVE
命令可以在后臺異步進(jìn)行快照操作,快照的同時服務(wù)器還可以繼續(xù)響應(yīng)客戶端的請求,因此需要手動執(zhí)行快照時推薦使用BGSAVE
命令。被動觸發(fā):
- 根據(jù)配置規(guī)則進(jìn)行自動快照,如
SAVE 100 10
,100秒內(nèi)至少有10個鍵被修改則進(jìn)行快照。 - 如果從節(jié)點執(zhí)行全量復(fù)制操作,主節(jié)點會自動執(zhí)行
BGSAVE
生成 RDB 文件并發(fā)送給從節(jié)點。 - 默認(rèn)情況下執(zhí)行
shutdown
命令時,如果沒有開啟 AOF 持久化功能則自動執(zhí)行·BGSAVE·。
- 根據(jù)配置規(guī)則進(jìn)行自動快照,如
優(yōu)點:
- Redis 加載 RDB 恢復(fù)數(shù)據(jù)遠(yuǎn)遠(yuǎn)快于 AOF 的方式。
- 使用單獨子進(jìn)程來進(jìn)行持久化,主進(jìn)程不會進(jìn)行任何 IO 操作,保證了 Redis 的高性能。
缺點:
- RDB方式數(shù)據(jù)無法做到實時持久化。因為
BGSAVE
每次運行都要執(zhí)行fork
操作創(chuàng)建子進(jìn)程,屬于重量級操作,頻繁執(zhí)行成本比較高。 - RDB 文件使用特定二進(jìn)制格式保存,Redis 版本升級過程中有多個格式的 RDB 版本,存在老版本 Redis 無法兼容新版 RDB 格式的問題。
AOF方式
AOF(append only file)持久化:以獨立日志的方式記錄每次寫命令,Redis重啟時會重新執(zhí)行AOF文件中的命令達(dá)到恢復(fù)數(shù)據(jù)的目的。AOF的主要作用是解決了數(shù)據(jù)持久化的實時性,AOF 是Redis持久化的主流方式。
默認(rèn)情況下Redis沒有開啟AOF方式的持久化,可以通過appendonly
參數(shù)啟用:appendonly yes
。開啟AOF方式持久化后每執(zhí)行一條寫命令,Redis就會將該命令寫進(jìn)aof_buf
緩沖區(qū),AOF緩沖區(qū)根據(jù)對應(yīng)的策略向硬盤做同步操作。
默認(rèn)情況下系統(tǒng)每30秒會執(zhí)行一次同步操作。為了防止緩沖區(qū)數(shù)據(jù)丟失,可以在Redis寫入AOF文件后主動要求系統(tǒng)將緩沖區(qū)數(shù)據(jù)同步到硬盤上??梢酝ㄟ^appendfsync
參數(shù)設(shè)置同步的時機。
appendfsync always //每次寫入aof文件都會執(zhí)行同步,最安全最慢,不建議配置 appendfsync everysec //既保證性能也保證安全,建議配置 appendfsync no //由操作系統(tǒng)決定何時進(jìn)行同步操作
接下來看一下 AOF 持久化執(zhí)行流程:
- 所有的寫入命令會追加到 AOP 緩沖區(qū)中。
- AOF 緩沖區(qū)根據(jù)對應(yīng)的策略向硬盤同步。
- 隨著 AOF 文件越來越大,需要定期對 AOF 文件進(jìn)行重寫,達(dá)到壓縮文件體積的目的。AOF文件重寫是把Redis進(jìn)程內(nèi)的數(shù)據(jù)轉(zhuǎn)化為寫命令同步到新AOF文件的過程。
- 當(dāng) Redis 服務(wù)器重啟時,可以加載 AOF 文件進(jìn)行數(shù)據(jù)恢復(fù)。
優(yōu)點:
- AOF可以更好的保護數(shù)據(jù)不丟失,可以配置 AOF 每秒執(zhí)行一次
fsync
操作,如果Redis進(jìn)程掛掉,最多丟失1秒的數(shù)據(jù)。 - AOF以
append-only
的模式寫入,所以沒有磁盤尋址的開銷,寫入性能非常高。
缺點:
- 對于同一份文件AOF文件比RDB數(shù)據(jù)快照要大。
- 數(shù)據(jù)恢復(fù)比較慢。
RDB和AOF如何選擇?
通常來說,應(yīng)該同時使用兩種持久化方案,以保證數(shù)據(jù)安全。
- 如果數(shù)據(jù)不敏感,且可以從其他地方重新生成,可以關(guān)閉持久化。
- 如果數(shù)據(jù)比較重要,且能夠承受幾分鐘的數(shù)據(jù)丟失,比如緩存等,只需要使用RDB即可。
- 如果是用做內(nèi)存數(shù)據(jù),要使用Redis的持久化,建議是RDB和AOF都開啟。
- 如果只用AOF,優(yōu)先使用everysec的配置選擇,因為它在可靠性和性能之間取了一個平衡。
當(dāng)RDB與AOF兩種方式都開啟時,Redis會優(yōu)先使用AOF恢復(fù)數(shù)據(jù),因為AOF保存的文件比RDB文件更完整。
Redis常見的部署方式有哪些?
Redis的幾種常見使用方式包括:
- 單機版
- Redis主從
- Redis Sentinel(哨兵)
- Redis Cluster
使用場景:
單機版:很少使用。存在的問題:1、內(nèi)存容量有限 2、處理能力有限 3、無法高可用。
主從模式:master 節(jié)點掛掉后,需要手動指定新的 master,可用性不高,基本不用。
哨兵模式:master 節(jié)點掛掉后,哨兵進(jìn)程會主動選舉新的 master,可用性高,但是每個節(jié)點存儲的數(shù)據(jù)是一樣的,浪費內(nèi)存空間。數(shù)據(jù)量不是很多,集群規(guī)模不是很大,需要自動容錯容災(zāi)的時候使用。
Redis cluster:主要是針對海量數(shù)據(jù)+高并發(fā)+高可用的場景,如果是海量數(shù)據(jù),如果你的數(shù)據(jù)量很大,那么建議就用Redis cluster,所有主節(jié)點的容量總和就是Redis cluster可緩存的數(shù)據(jù)容量。
主從復(fù)制
Redis的復(fù)制功能是支持多個數(shù)據(jù)庫之間的數(shù)據(jù)同步。主數(shù)據(jù)庫可以進(jìn)行讀寫操作,當(dāng)主數(shù)據(jù)庫的數(shù)據(jù)發(fā)生變化時會自動將數(shù)據(jù)同步到從數(shù)據(jù)庫。從數(shù)據(jù)庫一般是只讀的,它會接收主數(shù)據(jù)庫同步過來的數(shù)據(jù)。一個主數(shù)據(jù)庫可以有多個從數(shù)據(jù)庫,而一個從數(shù)據(jù)庫只能有一個主數(shù)據(jù)庫。
redis-server //啟動Redis實例作為主數(shù)據(jù)庫 redis-server --port 6380 --slaveof 127.0.0.1 6379 //啟動另一個實例作為從數(shù)據(jù)庫 slaveof 127.0.0.1 6379 SLAVEOF NO ONE //停止接收其他數(shù)據(jù)庫的同步并轉(zhuǎn)化為主數(shù)據(jù)庫。
主從復(fù)制的原理?
- 當(dāng)啟動一個從節(jié)點時,它會發(fā)送一個
PSYNC
命令給主節(jié)點; - 如果是從節(jié)點初次連接到主節(jié)點,那么會觸發(fā)一次全量復(fù)制。此時主節(jié)點會啟動一個后臺線程,開始生成一份
RDB
快照文件; - 同時還會將從客戶端 client 新收到的所有寫命令緩存在內(nèi)存中。
RDB
文件生成完畢后, 主節(jié)點會將RDB
文件發(fā)送給從節(jié)點,從節(jié)點會先將RDB
文件寫入本地磁盤,然后再從本地磁盤加載到內(nèi)存中; - 接著主節(jié)點會將內(nèi)存中緩存的寫命令發(fā)送到從節(jié)點,從節(jié)點同步這些數(shù)據(jù);
- 如果從節(jié)點跟主節(jié)點之間網(wǎng)絡(luò)出現(xiàn)故障,連接斷開了,會自動重連,連接之后主節(jié)點僅會將部分缺失的數(shù)據(jù)同步給從節(jié)點。
哨兵Sentinel
主從復(fù)制存在不能自動故障轉(zhuǎn)移、達(dá)不到高可用的問題。哨兵模式解決了這些問題。通過哨兵機制可以自動切換主從節(jié)點。
客戶端連接Redis的時候,先連接哨兵,哨兵會告訴客戶端Redis主節(jié)點的地址,然后客戶端連接上Redis并進(jìn)行后續(xù)的操作。當(dāng)主節(jié)點宕機的時候,哨兵監(jiān)測到主節(jié)點宕機,會重新推選出某個表現(xiàn)良好的從節(jié)點成為新的主節(jié)點,然后通過發(fā)布訂閱模式通知其他的從服務(wù)器,讓它們切換主機。
工作原理
- 每個
Sentinel
以每秒鐘一次的頻率向它所知道的Master
,Slave
以及其他Sentinel
實例發(fā)送一個PING
命令。 - 如果一個實例距離最后一次有效回復(fù)
PING
命令的時間超過指定值, 則這個實例會被Sentine
標(biāo)記為主觀下線。 - 如果一個
Master
被標(biāo)記為主觀下線,則正在監(jiān)視這個Master
的所有Sentinel
要以每秒一次的頻率確認(rèn)Master
是否真正進(jìn)入主觀下線狀態(tài)。 - 當(dāng)有足夠數(shù)量的
Sentinel
(大于等于配置文件指定值)在指定的時間范圍內(nèi)確認(rèn)Master
的確進(jìn)入了主觀下線狀態(tài), 則Master
會被標(biāo)記為客觀下線 。若沒有足夠數(shù)量的Sentinel
同意Master
已經(jīng)下線,Master
的客觀下線狀態(tài)就會被解除。 若Master
重新向Sentinel
的PING
命令返回有效回復(fù),Master
的主觀下線狀態(tài)就會被移除。 - 哨兵節(jié)點會選舉出哨兵 leader,負(fù)責(zé)故障轉(zhuǎn)移的工作。
- 哨兵 leader 會推選出某個表現(xiàn)良好的從節(jié)點成為新的主節(jié)點,然后通知其他從節(jié)點更新主節(jié)點信息。
Redis cluster
哨兵模式解決了主從復(fù)制不能自動故障轉(zhuǎn)移、達(dá)不到高可用的問題,但還是存在主節(jié)點的寫能力、容量受限于單機配置的問題。而cluster模式實現(xiàn)了Redis的分布式存儲,每個節(jié)點存儲不同的內(nèi)容,解決主節(jié)點的寫能力、容量受限于單機配置的問題。
Redis cluster集群節(jié)點最小配置6個節(jié)點以上(3主3從),其中主節(jié)點提供讀寫操作,從節(jié)點作為備用節(jié)點,不提供請求,只作為故障轉(zhuǎn)移使用。
Redis cluster采用虛擬槽分區(qū),所有的鍵根據(jù)哈希函數(shù)映射到0~16383個整數(shù)槽內(nèi),每個節(jié)點負(fù)責(zé)維護一部分槽以及槽所映射的鍵值數(shù)據(jù)。
哈希槽是如何映射到 Redis 實例上的?
- 對鍵值對的
key
使用crc16
算法計算一個結(jié)果 - 將結(jié)果對 16384 取余,得到的值表示
key
對應(yīng)的哈希槽 - 根據(jù)該槽信息定位到對應(yīng)的實例
優(yōu)點:
- 無中心架構(gòu),支持動態(tài)擴容;
- 數(shù)據(jù)按照
slot
存儲分布在多個節(jié)點,節(jié)點間數(shù)據(jù)共享,可動態(tài)調(diào)整數(shù)據(jù)分布; - 高可用性。部分節(jié)點不可用時,集群仍可用。集群模式能夠?qū)崿F(xiàn)自動故障轉(zhuǎn)移(failover),節(jié)點之間通過
gossip
協(xié)議交換狀態(tài)信息,用投票機制完成Slave
到Master
的角色轉(zhuǎn)換。
缺點:
- 不支持批量操作(pipeline)。
- 數(shù)據(jù)通過異步復(fù)制,不保證數(shù)據(jù)的強一致性。
- 事務(wù)操作支持有限,只支持多
key
在同一節(jié)點上的事務(wù)操作,當(dāng)多個key
分布于不同的節(jié)點上時無法使用事務(wù)功能。 key
作為數(shù)據(jù)分區(qū)的最小粒度,不能將一個很大的鍵值對象如hash
、list
等映射到不同的節(jié)點。- 不支持多數(shù)據(jù)庫空間,單機下的Redis可以支持到16個數(shù)據(jù)庫,集群模式下只能使用1個數(shù)據(jù)庫空間。
哈希分區(qū)算法有哪些?
節(jié)點取余分區(qū)。使用特定的數(shù)據(jù),如Redis的鍵或用戶ID,對節(jié)點數(shù)量N取余:hash(key)%N計算出哈希值,用來決定數(shù)據(jù)映射到哪一個節(jié)點上。
優(yōu)點是簡單性。擴容時通常采用翻倍擴容,避免數(shù)據(jù)映射全部被打亂導(dǎo)致全量遷移的情況。
一致性哈希分區(qū)。為系統(tǒng)中每個節(jié)點分配一個token,范圍一般在0~232,這些token構(gòu)成一個哈希環(huán)。數(shù)據(jù)讀寫執(zhí)行節(jié)點查找操作時,先根據(jù)key計算hash值,然后順時針找到第一個大于等于該哈希值的token節(jié)點。
這種方式相比節(jié)點取余最大的好處在于加入和刪除節(jié)點只影響哈希環(huán)中相鄰的節(jié)點,對其他節(jié)點無影響。
虛擬槽分區(qū),所有的鍵根據(jù)哈希函數(shù)映射到0~16383整數(shù)槽內(nèi),計算公式:slot=CRC16(key)&16383。每一個節(jié)點負(fù)責(zé)維護一部分槽以及槽所映射的鍵值數(shù)據(jù)。Redis Cluser采用虛擬槽分區(qū)算法。
過期鍵的刪除策略?
1、被動刪除。在訪問key時,如果發(fā)現(xiàn)key已經(jīng)過期,那么會將key刪除。
2、主動刪除。定時清理key,每次清理會依次遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那么就繼續(xù)對這個db進(jìn)行清理,否則開始清理下一個db。
3、內(nèi)存不夠時清理。Redis有最大內(nèi)存的限制,通過maxmemory參數(shù)可以設(shè)置最大內(nèi)存,當(dāng)使用的內(nèi)存超過了設(shè)置的最大內(nèi)存,就要進(jìn)行內(nèi)存釋放, 在進(jìn)行內(nèi)存釋放的時候,會按照配置的淘汰策略清理內(nèi)存。
內(nèi)存淘汰策略有哪些?
當(dāng)Redis的內(nèi)存超過最大允許的內(nèi)存之后,Redis 會觸發(fā)內(nèi)存淘汰策略,刪除一些不常用的數(shù)據(jù),以保證Redis服務(wù)器正常運行。
Redisv4.0前提供 6 種數(shù)據(jù)淘汰策略:
- volatile-lru:LRU(
Least Recently Used
),最近使用。利用LRU算法移除設(shè)置了過期時間的key - allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,從數(shù)據(jù)集中移除最近最少使用的key
- volatile-ttl:從已設(shè)置過期時間的數(shù)據(jù)集中挑選將要過期的數(shù)據(jù)淘汰
- volatile-random:從已設(shè)置過期時間的數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰
- allkeys-random:從數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰
- no-eviction:禁止刪除數(shù)據(jù),當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯
Redisv4.0后增加以下兩種:
- volatile-lfu:LFU,Least Frequently Used,最少使用,從已設(shè)置過期時間的數(shù)據(jù)集中挑選最不經(jīng)常使用的數(shù)據(jù)淘汰。
- allkeys-lfu:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,從數(shù)據(jù)集中移除最不經(jīng)常使用的key。
內(nèi)存淘汰策略可以通過配置文件來修改,相應(yīng)的配置項是maxmemory-policy
,默認(rèn)配置是noeviction
。
如何保證緩存與數(shù)據(jù)庫雙寫時的數(shù)據(jù)一致性?
1、先刪除緩存再更新數(shù)據(jù)庫
進(jìn)行更新操作時,先刪除緩存,然后更新數(shù)據(jù)庫,后續(xù)的請求再次讀取時,會從數(shù)據(jù)庫讀取后再將新數(shù)據(jù)更新到緩存。
存在的問題:刪除緩存數(shù)據(jù)之后,更新數(shù)據(jù)庫完成之前,這個時間段內(nèi)如果有新的讀請求過來,就會從數(shù)據(jù)庫讀取舊數(shù)據(jù)重新寫到緩存中,再次造成不一致,并且后續(xù)讀的都是舊數(shù)據(jù)。
2、先更新數(shù)據(jù)庫再刪除緩存
進(jìn)行更新操作時,先更新MySQL,成功之后,刪除緩存,后續(xù)讀取請求時再將新數(shù)據(jù)回寫緩存。
存在的問題:更新MySQL和刪除緩存這段時間內(nèi),請求讀取的還是緩存的舊數(shù)據(jù),不過等數(shù)據(jù)庫更新完成,就會恢復(fù)一致,影響相對比較小。
3、異步更新緩存
數(shù)據(jù)庫的更新操作完成后不直接操作緩存,而是把這個操作命令封裝成消息扔到消息隊列中,然后由Redis自己去消費更新數(shù)據(jù),消息隊列可以保證數(shù)據(jù)操作順序一致性,確保緩存系統(tǒng)的數(shù)據(jù)正常。
緩存穿透
緩存穿透是指查詢一個不存在的數(shù)據(jù),由于緩存是不命中時被動寫的,如果從DB查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個不存在的數(shù)據(jù)每次請求都要到DB去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了。
- 緩存空值,不會查數(shù)據(jù)庫。
- 采用布隆過濾器,將所有可能存在的數(shù)據(jù)哈希到一個足夠大的
bitmap
中,查詢不存在的數(shù)據(jù)會被這個bitmap
攔截掉,從而避免了對DB
的查詢壓力。
布隆過濾器的原理:當(dāng)一個元素被加入集合時,通過K個散列函數(shù)將這個元素映射成一個位數(shù)組中的K個點,把它們置為1。查詢時,將元素通過散列函數(shù)映射之后會得到k個點,如果這些點有任何一個0,則被檢元素一定不在,直接返回;如果都是1,則查詢元素很可能存在,就會去查詢Redis和數(shù)據(jù)庫。
緩存雪崩
緩存雪崩是指在我們設(shè)置緩存時采用了相同的過期時間,導(dǎo)致緩存在某一時刻同時失效,請求全部轉(zhuǎn)發(fā)到DB,DB瞬時壓力過重掛掉。
解決方法:在原有的失效時間基礎(chǔ)上增加一個隨機值,使得過期時間分散一些。
緩存擊穿
緩存擊穿:大量的請求同時查詢一個 key 時,此時這個 key 正好失效了,就會導(dǎo)致大量的請求都落到數(shù)據(jù)庫。緩存擊穿是查詢緩存中失效的 key,而緩存穿透是查詢不存在的 key。
解決方法:加分布式鎖,第一個請求的線程可以拿到鎖,拿到鎖的線程查詢到了數(shù)據(jù)之后設(shè)置緩存,其他的線程獲取鎖失敗會等待50ms然后重新到緩存取數(shù)據(jù),這樣便可以避免大量的請求落到數(shù)據(jù)庫。
public String get(String key) { String value = redis.get(key); if (value == null) { //緩存值過期 String unique_key = systemId + ":" + key; //設(shè)置30s的超時 if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) { //設(shè)置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(unique_key); } else { //其他線程已經(jīng)到數(shù)據(jù)庫取值并回寫到緩存了,可以重試獲取緩存值 sleep(50); get(key); //重試 } } else { return value; } }
Redis 怎么實現(xiàn)消息隊列?
使用一個列表,讓生產(chǎn)者將任務(wù)使用LPUSH命令放進(jìn)列表,消費者不斷用RPOP從列表取出任務(wù)。
BRPOP和RPOP命令相似,唯一的區(qū)別就是當(dāng)列表沒有元素時BRPOP命令會一直阻塞連接,直到有新元素加入。
BRPOP queue 0 //0表示不限制等待時間
優(yōu)先級隊列
如果多個鍵都有元素,則按照從左到右的順序取元素。
BLPOP queue:1 queue:2 queue:3 0
發(fā)布/訂閱模式
PSUBSCRIBE channel?*
按照規(guī)則訂閱。PUNSUBSCRIBE channel?*
退訂通過PSUBSCRIBE命令按照某種規(guī)則訂閱的頻道。其中訂閱規(guī)則要進(jìn)行嚴(yán)格的字符串匹配,PUNSUBSCRIBE *
無法退訂channel?*
規(guī)則。
PUBLISH channel1 hi SUBSCRIBE channel1 UNSUBSCRIBE channel1 //退訂通過SUBSCRIBE命令訂閱的頻道。
缺點:在消費者下線的情況下,生產(chǎn)的消息會丟失。
延時隊列
使用sortedset,拿時間戳作為score,消息內(nèi)容作為key,調(diào)用zadd來生產(chǎn)消息,消費者用zrangebyscore
指令獲取N秒之前的數(shù)據(jù)輪詢進(jìn)行處理。
pipeline的作用?
redis客戶端執(zhí)行一條命令分4個過程: 發(fā)送命令、命令排隊、命令執(zhí)行、返回結(jié)果。使用pipeline
可以批量請求,批量返回結(jié)果,執(zhí)行速度比逐條執(zhí)行要快。
使用pipeline
組裝的命令個數(shù)不能太多,不然數(shù)據(jù)量過大,增加客戶端的等待時間,還可能造成網(wǎng)絡(luò)阻塞,可以將大量命令的拆分多個小的pipeline
命令完成。
原生批命令(mset和mget)與pipeline
對比:
原生批命令是原子性,
pipeline
是非原子性。pipeline命令中途異常退出,之前執(zhí)行成功的命令不會回滾。原生批命令只有一個命令,但
pipeline
支持多命令。
LUA腳本
Redis 通過 LUA 腳本創(chuàng)建具有原子性的命令: 當(dāng)lua腳本命令正在運行的時候,不會有其他腳本或 Redis 命令被執(zhí)行,實現(xiàn)組合命令的原子操作。
在Redis中執(zhí)行Lua腳本有兩種方法:eval
和evalsha
。eval
命令使用內(nèi)置的 Lua 解釋器,對 Lua 腳本進(jìn)行求值。
//第一個參數(shù)是lua腳本,第二個參數(shù)是鍵名參數(shù)個數(shù),剩下的是鍵名參數(shù)和附加參數(shù) > eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
lua腳本作用
1、Lua腳本在Redis中是原子執(zhí)行的,執(zhí)行過程中間不會插入其他命令。
2、Lua腳本可以將多條命令一次性打包,有效地減少網(wǎng)絡(luò)開銷。
應(yīng)用場景
舉例:限制接口訪問頻率。
在Redis維護一個接口訪問次數(shù)的鍵值對,key
是接口名稱,value
是訪問次數(shù)。每次訪問接口時,會執(zhí)行以下操作:
- 通過
aop
攔截接口的請求,對接口請求進(jìn)行計數(shù),每次進(jìn)來一個請求,相應(yīng)的接口訪問次數(shù)count
加1,存入redis。 - 如果是第一次請求,則會設(shè)置
count=1
,并設(shè)置過期時間。因為這里set()
和expire()
組合操作不是原子操作,所以引入lua
腳本,實現(xiàn)原子操作,避免并發(fā)訪問問題。 - 如果給定時間范圍內(nèi)超過最大訪問次數(shù),則會拋出異常。
private String buildLuaScript() { return "local c" + "\nc = redis.call('get',KEYS[1])" + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + "\nreturn c;" + "\nend" + "\nc = redis.call('incr',KEYS[1])" + "\nif tonumber(c) == 1 then" + "\nredis.call('expire',KEYS[1],ARGV[2])" + "\nend" + "\nreturn c;"; } String luaScript = buildLuaScript(); RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class); Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
PS:這種接口限流的實現(xiàn)方式比較簡單,問題也比較多,一般不會使用,接口限流用的比較多的是令牌桶算法和漏桶算法。
什么是RedLock?
Redis 官方站提出了一種權(quán)威的基于 Redis 實現(xiàn)分布式鎖的方式名叫 Redlock,此種方式比原先的單節(jié)點的方法更安全。它可以保證以下特性:
- 安全特性:互斥訪問,即永遠(yuǎn)只有一個 client 能拿到鎖
- 避免死鎖:最終 client 都可能拿到鎖,不會出現(xiàn)死鎖的情況,即使原本鎖住某資源的 client 掛掉了
- 容錯性:只要大部分 Redis 節(jié)點存活就可以正常提供服務(wù)
#Java求職##Redis##Java##面經(jīng)#更多技術(shù)干貨、面試筆記、自學(xué)Java經(jīng)歷,可以關(guān)注微信公眾號:【程序員大彬】