本篇文章給大家整理了20道經典Redis面試題,希望對大家有幫助。
Redis,英文全稱是Remote Dictionary Server(遠端字典服務),是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的紀錄檔型、Key-Value資料庫,並提供多種語言的API。
與MySQL資料庫不同的是,Redis的資料是存在記憶體中的。它的讀寫速度非常快,每秒可以處理超過10萬次讀寫操作。因此redis被廣泛應用於快取,另外,Redis也經常用來做分散式鎖。除此之外,Redis支援事務、持久化、LUA 指令碼、LRU 驅動事件、多種叢集方案。
大多數小夥伴都知道,Redis有以下這五種基本型別:
它還有三種特殊的資料結構型別
String(字串)
set key value
、get key
等int(8位元組長整型)/embstr(小於等於39位元組字串)/raw(大於39個位元組字串)
C語言的字串是char[]
實現的,而Redis使用SDS(simple dynamic string) 封裝,sds原始碼如下:
struct sdshdr{
unsigned int len; // 標記buf的長度
unsigned int free; //標記buf中未使用的元素個數
char buf[]; // 存放元素的坑
}
登入後複製
SDS 結構圖如下:
Redis為什麼選擇SDS結構,而C語言原生的char[]
不香嗎?
舉例其中一點,SDS中,O(1)時間複雜度,就可以獲取字串長度;而C 字串,需要遍歷整個字串,時間複雜度為O(n)
Hash(雜湊)
hset key field value
、hget key field
ziplist(壓縮列表)
、hashtable(雜湊表)
字串和雜湊型別對比如下圖:
List(列表)
lpush key value [value ...]
、lrange key start end
一圖看懂list型別的插入與彈出:
list應用場景參考以下:
- lpush+lpop=Stack(棧)
- lpush+rpop=Queue(佇列)
- lpsh+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(訊息佇列)
Set(集合)
sadd key element [element ...]
、smembers key
intset(整數集合)
、hashtable(雜湊表)
有序集合(zset)
zadd key score member [score member ...]
,zrank key member
ziplist(壓縮列表)
、skiplist(跳躍表)
Redis為什麼這麼快
我們都知道記憶體讀寫是比在磁碟快很多的,Redis基於記憶體儲存實現的資料庫,相對於資料存在磁碟的MySQL資料庫,省去磁碟I/O的消耗。
我們知道,Mysql索引為了提高效率,選擇了B+樹的資料結構。其實合理的資料結構,就是可以讓你的應用/程式更快。先看下Redis的資料結構&內部編碼圖:
SDS簡單動態字串
- 字串長度處理:Redis獲取字串長度,時間複雜度為O(1),而C語言中,需要從頭開始遍歷,複雜度為O(n);
- 空間預分配:字串修改越頻繁的話,記憶體分配越頻繁,就會消耗效能,而SDS修改和空間擴充,會額外分配未使用的空間,減少效能損耗。
- 惰性空間釋放:SDS 縮短時,不是回收多餘的記憶體空間,而是free記錄下多餘的空間,後續有變更,直接使用free中記錄的空間,減少分配。
- 二進位制安全:Redis可以儲存一些二進位制資料,在C語言中字串遇到'\0'會結束,而 SDS中標誌字串結束的是len屬性。
字典
Redis 作為 K-V 型記憶體資料庫,所有的鍵值就是用字典來儲存。字典就是雜湊表,比如HashMap,通過key就可以直接獲取到對應的value。而雜湊表的特性,在O(1)時間複雜度就可以獲得對應的值。
跳躍表
- 跳躍表是Redis特有的資料結構,就是在連結串列的基礎上,增加多級索引提升查詢效率。
- 跳躍表支援平均 O(logN),最壞 O(N)複雜度的節點查詢,還可以通過順序性操作批次處理節點。
Redis 支援多種資料資料型別,每種基本型別,可能對多種資料結構。什麼時候,使用什麼樣資料結構,使用什麼樣編碼,是redis設計者總結優化的結果。
- String:如果儲存數位的話,是用int型別的編碼;如果儲存非數位,小於等於39位元組的字串,是embstr;大於39個位元組,則是raw編碼。
- List:如果列表的元素個數小於512個,列表每個元素的值都小於64位元組(預設),使用ziplist編碼,否則使用linkedlist編碼
- Hash:雜湊型別元素個數小於512個,所有值小於64位元組的話,使用ziplist編碼,否則使用hashtable編碼。
- Set:如果集合中的元素都是整數且元素個數小於512個,使用intset編碼,否則使用hashtable編碼。
- Zset:當有序集合的元素個數小於128個,每個元素的值小於64位元組時,使用ziplist編碼,否則使用skiplist(跳躍表)編碼
I/O 多路複用
I/O 多路複用
多路I/O複用技術可以讓單個執行緒高效的處理多個連線請求,而Redis使用用epoll作為I/O多路複用技術的實現。並且,Redis自身的事件處理模型將epoll中的連線、讀寫、關閉都轉換為事件,不在網路I/O上浪費過多的時間。
什麼是I/O多路複用?
- I/O :網路 I/O
- 多路 :多個網路連線
- 複用:複用同一個執行緒。
- IO多路複用其實就是一種同步IO模型,它實現了一個執行緒可以監視多個檔案控制程式碼;一旦某個檔案控制程式碼就緒,就能夠通知應用程式進行相應的讀寫操作;而沒有檔案控制程式碼就緒時,就會阻塞應用程式,交出cpu。
單執行緒模型
Redis直接自己構建了VM機制 ,不會像一般的系統會呼叫系統函數處理,會浪費一定的時間去移動和請求。
Redis的虛擬記憶體機制是啥呢?
虛擬記憶體機制就是暫時把不經常存取的資料(冷資料)從記憶體交換到磁碟中,從而騰出寶貴的記憶體空間用於其它需要存取的資料(熱資料)。通過VM功能可以實現冷熱資料分離,使熱資料仍在記憶體中、冷資料儲存到磁碟。這樣就可以避免因為記憶體不足而造成存取速度下降的問題。
先來看一個常見的快取使用方式:讀請求來了,先查下快取,快取有值命中,就直接返回;快取沒命中,就去查資料庫,然後把資料庫的值更新到快取,再返回。
讀取快取
快取穿透:指查詢一個一定不存在的資料,由於快取是不命中時需要從資料庫查詢,查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到資料庫去查詢,進而給資料庫帶來壓力。
通俗點說,讀請求存取時,快取和資料庫都沒有某個值,這樣就會導致每次對這個值的查詢請求都會穿透到資料庫,這就是快取穿透。
快取穿透一般都是這幾種情況產生的:
如何避免快取穿透呢? 一般有三種方法。
布隆過濾器原理:它由初始值為0的點陣圖陣列和N個雜湊函陣列成。一個對一個key進行N個hash演演算法獲取N個值,在位元陣列中將這N個值雜湊後設定為1,然後查的時候如果特定的這幾個位置都為1,那麼布隆過濾器判斷該key存在。
快取雪奔: 指快取中資料大批次到過期時間,而查詢資料量巨大,請求都直接存取資料庫,引起資料庫壓力過大甚至down機。
快取擊穿: 指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的並行請求過來,從而大量的請求打到db。
快取擊穿看著有點像,其實它兩區別是,快取雪奔是指資料庫壓力過大甚至down機,快取擊穿只是大量並行請求到了DB資料庫層面。可以認為擊穿是快取雪奔的一個子集吧。有些文章認為它倆區別,是區別在於擊穿針對某一熱點key快取,雪奔則是很多key。
解決方案就有兩種:
什麼是熱Key呢?在Redis中,我們把存取頻率高的key,稱為熱點key。
如果某一熱點key的請求到伺服器主機時,由於請求量特別大,可能會導致主機資源不足,甚至宕機,從而影響正常的服務。
而熱點Key是怎麼產生的呢?主要原因有兩個:
- 使用者消費的資料遠大於生產的資料,如秒殺、熱點新聞等讀多寫少的場景。
- 請求分片集中,超過單Redi伺服器的效能,比如固定名稱key,Hash落入同一臺伺服器,瞬間存取量極大,超過機器瓶頸,產生熱點Key問題。
那麼在日常開發中,如何識別到熱點key呢?
- 憑經驗判斷哪些是熱Key;
- 使用者端統計上報;
- 服務代理層上報
如何解決熱key問題?
- Redis叢集擴容:增加分片副本,均衡讀流量;
- 將熱key分散到不同的伺服器中;
- 使用二級快取,即JVM本地快取,減少Redis的讀請求。
我們在set key
的時候,可以給它設定一個過期時間,比如expire key 60
。指定這key60s後過期,60s後,redis是如何處理的嘛?我們先來介紹幾種過期策略:
定時過期
每個設定過期時間的key都需要建立一個定時器,到過期時間就會立即對key進行清除。該策略可以立即清除過期的資料,對記憶體很友好;但是會佔用大量的CPU資源去處理過期的資料,從而影響快取的響應時間和吞吐量。
惰性過期
只有當存取一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對記憶體非常不友好。極端情況可能出現大量的過期key沒有再次被存取,從而不會被清除,佔用大量記憶體。
定期過期
每隔一定的時間,會掃描一定數量的資料庫的expires字典中一定數量的key,並清除其中已過期的key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和記憶體資源達到最優的平衡效果。
expires字典會儲存所有設定了過期時間的key的過期時間資料,其中,key是指向鍵空間中的某個鍵的指標,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis叢集中儲存的所有鍵。
Redis中同時使用了惰性過期和定期過期兩種過期策略。
但是呀,如果定期刪除漏掉了很多過期的key,然後也沒走惰性刪除。就會有很多過期key積在記憶體記憶體,直接會導致記憶體爆的。或者有些時候,業務量大起來了,redis的key被大量使用,記憶體直接不夠了,運維小哥哥也忘記加大記憶體了。難道redis直接這樣掛掉?不會的!Redis用8種記憶體淘汰策略保護自己~
- volatile-lru:當記憶體不足以容納新寫入資料時,從設定了過期時間的key中使用LRU(最近最少使用)演演算法進行淘汰;
- allkeys-lru:當記憶體不足以容納新寫入資料時,從所有key中使用LRU(最近最少使用)演演算法進行淘汰。
- volatile-lfu:4.0版本新增,當記憶體不足以容納新寫入資料時,在過期的key中,使用LFU演演算法進行刪除key。
- allkeys-lfu:4.0版本新增,當記憶體不足以容納新寫入資料時,從所有key中使用LFU演演算法進行淘汰;
- volatile-random:當記憶體不足以容納新寫入資料時,從設定了過期時間的key中,隨機淘汰資料;。
- allkeys-random:當記憶體不足以容納新寫入資料時,從所有key中隨機淘汰資料。
- volatile-ttl:當記憶體不足以容納新寫入資料時,在設定了過期時間的key中,根據過期時間進行淘汰,越早過期的優先被淘汰;
- noeviction:預設策略,當記憶體不足以容納新寫入資料時,新寫入操作會報錯。
我們一提到redis,自然而然就想到快取,國內外中大型的網站都離不開快取。合理的利用快取,比如快取熱點資料,不僅可以提升網站的存取速度,還可以降低資料庫DB的壓力。並且,Redis相比於memcached,還提供了豐富的資料結構,並且提供RDB和AOF等持久化機制,強的一批。
當今網際網路應用,有各種各樣的排行榜,如電商網站的月度銷量排行榜、社交APP的禮物排行榜、小程式的投票排行榜等等。Redis提供的zset
資料型別能夠實現這些複雜的排行榜。
比如,使用者每天上傳視訊,獲得點讚的排行榜可以這樣設計:
zadd user:ranking:2021-03-03 Jay 3
登入後複製
zincrby user:ranking:2021-03-03 Jay 1
登入後複製
zrem user:ranking:2021-03-03 John
登入後複製
zrevrangebyrank user:ranking:2021-03-03 0 2
登入後複製
各大網站、APP應用經常需要計數器的功能,如短視訊的播放數、電商網站的瀏覽數。這些播放數、瀏覽數一般要求實時的,每一次播放和瀏覽都要做加1的操作,如果並行量很大對於傳統關係型資料的效能是一種挑戰。Redis天然支援計數功能而且計數的效能也非常好,可以說是計數器系統的重要選擇。
如果一個分散式Web服務將使用者的Session資訊儲存在各自伺服器,使用者重新整理一次可能就需要重新登入了,這樣顯然有問題。實際上,可以使用Redis將使用者的Session進行集中管理,每次使用者更新或者查詢登入資訊都直接從Redis中集中獲取。
幾乎每個網際網路公司中都使用了分散式部署,分散式服務下,就會遇到對同一個資源的並行存取的技術難題,如秒殺、下單減庫存等場景。
贊/踩、粉絲、共同好友/喜好、推播、下拉重新整理等是社群網站的必備功能,由於社群網站存取量通常比較大,而且傳統的關係型資料不太適儲存 這種型別的資料,Redis提供的資料結構可以相對比較容易地實現這些功能。
訊息佇列是大型網站必用中介軟體,如ActiveMQ、RabbitMQ、Kafka等流行的訊息佇列中介軟體,主要用於業務解耦、流量削峰及非同步處理實時性低的業務。Redis提供了釋出/訂閱及阻塞佇列功能,能實現一個簡單的訊息佇列系統。另外,這個不能和專業的訊息中介軟體相比。
用於資料量上億的場景下,例如幾億使用者系統的簽到,去重登入次數統計,某使用者是否線上狀態等等。騰訊10億使用者,要幾個毫秒內查詢到某個使用者是否線上,能怎麼做?千萬別說給每個使用者建立一個key,然後挨個記(你可以算一下需要的記憶體會很恐怖,而且這種類似的需求很多。這裡要用到位元運算——使用setbit、getbit、bitcount命令。原理是:redis內構建一個足夠長的陣列,每個陣列元素只能是0和1兩個值,然後這個陣列的下標index用來表示使用者id(必須是數位哈),那麼很顯然,這個幾億長的大陣列就能通過下標和元素值(0和1)來構建一個記憶系統。
Redis是基於記憶體的非關係型K-V資料庫,既然它是基於記憶體的,如果Redis伺服器掛了,資料就會丟失。為了避免資料丟失了,Redis提供了持久化,即把資料儲存到磁碟。
Redis提供了RDB和AOF兩種持久化機制,它持久化檔案載入流程如下:
RDB,就是把記憶體資料以快照的形式儲存到磁碟上。
什麼是快照?可以這樣理解,給當前時刻的資料,拍一張照片,然後儲存下來。
RDB持久化,是指在指定的時間間隔內,執行指定次數的寫操作,將記憶體中的資料集快照寫入磁碟中,它是Redis預設的持久化方式。執行完操作後,在指定目錄下會生成一個dump.rdb
檔案,Redis 重新啟動的時候,通過載入dump.rdb
檔案來恢復資料。RDB觸發機制主要有以下幾種:
RDB 的優點
RDB缺點
AOF(append only file) 持久化,採用紀錄檔的形式來記錄每個寫操作,追加到檔案中,重新啟動時再重新執行AOF檔案中的命令來恢復資料。它主要解決資料持久化的實時性問題。預設是不開啟的。
AOF的工作流程如下:
AOF的優點
AOF的缺點
我們在專案中使用Redis,肯定不會是單點部署Redis服務的。因為,單點部署一旦宕機,就不可用了。為了實現高可用,通常的做法是,將資料庫複製多個副本以部署在不同的伺服器上,其中一臺掛了也可以繼續提供服務。Redis 實現高可用有三種部署模式:主從模式,哨兵模式,叢集模式。
主從模式中,Redis部署了多臺機器,有主節點,負責讀寫操作,有從節點,只負責讀操作。從節點的資料來自主節點,實現原理就是主從複製機制
主從複製包括全量複製,增量複製兩種。一般當slave第一次啟動連線master,或者認為是第一次連線,就採用全量複製,全量複製流程如下:
redis2.8版本之後,已經使用psync來替代sync,因為sync命令非常消耗系統資源,psync的效率更高。
slave與master全量同步之後,master上的資料,如果再次發生更新,就會觸發增量複製。
當master節點發生資料增減時,就會觸發replicationFeedSalves()
函數,接下來在 Master節點上呼叫的每一個命令會使用replicationFeedSlaves()
來同步到Slave節點。執行此函數之前呢,master節點會判斷使用者執行的命令是否有資料更新,如果有資料更新的話,並且slave節點不為空,就會執行此函數。這個函數作用就是:把使用者執行的命令傳送到所有的slave節點,讓slave節點執行。流程如下:
主從模式中,一旦主節點由於故障不能提供服務,需要人工將從節點晉升為主節點,同時還要通知應用方更新主節點地址。顯然,多數業務場景都不能接受這種故障處理方式。Redis從2.8開始正式提供了Redis Sentinel(哨兵)架構來解決這個問題。
哨兵模式,由一個或多個Sentinel範例組成的Sentinel系統,它可以監視所有的Redis主節點和從節點,並在被監視的主節點進入下線狀態時,自動將下線主伺服器屬下的某個從節點升級為新的主節點。但是呢,一個哨兵程序對Redis節點進行監控,就可能會出現問題(單點問題),因此,可以使用多個哨兵來進行監控Redis節點,並且各個哨兵之間還會進行監控。
Sentinel哨兵模式
簡單來說,哨兵模式就三個作用:
故障切換的過程是怎樣的呢
假設主伺服器宕機,哨兵1先檢測到這個結果,系統並不會馬上進行 failover 過程,僅僅是哨兵1主觀的認為主伺服器不可用,這個現象成為主觀下線。當後面的哨兵也檢測到主伺服器不可用,並且數量達到一定值時,那麼哨兵之間就會進行一次投票,投票的結果由一個哨兵發起,進行 failover 操作。切換成功後,就會通過釋出訂閱模式,讓各個哨兵把自己監控的從伺服器實現切換主機,這個過程稱為客觀下線。這樣對於使用者端而言,一切都是透明的。
哨兵的工作模式如下:
每個Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他Sentinel範例傳送一個 PING命令。
如果一個範例(instance)距離最後一次有效回覆 PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 則這個範例會被 Sentinel標記為主觀下線。
如果一個Master被標記為主觀下線,則正在監視這個Master的所有 Sentinel 要以每秒一次的頻率確認Master的確進入了主觀下線狀態。
當有足夠數量的 Sentinel(大於等於組態檔指定的值)在指定的時間範圍內確認Master的確進入了主觀下線狀態, 則Master會被標記為客觀下線。
在一般情況下, 每個 Sentinel 會以每10秒一次的頻率向它已知的所有Master,Slave傳送 INFO 命令。
當Master被 Sentinel 標記為客觀下線時,Sentinel 向下線的 Master 的所有 Slave 傳送 INFO 命令的頻率會從 10 秒一次改為每秒一次
若沒有足夠數量的 Sentinel同意Master已經下線, Master的客觀下線狀態就會被移除;若Master 重新向 Sentinel 的 PING 命令返回有效回覆, Master 的主觀下線狀態就會被移除。
哨兵模式基於主從模式,實現讀寫分離,它還可以自動切換,系統可用性更高。但是它每個節點儲存的資料是一樣的,浪費記憶體,並且不好線上擴容。因此,Cluster叢集應運而生,它在Redis3.0加入的,實現了Redis的分散式儲存。對資料進行分片,也就是說每臺Redis節點上儲存不同的內容,來解決線上擴容的問題。並且,它也提供複製和故障轉移的功能。
Cluster叢集節點的通訊
一個Redis叢集由多個節點組成,各個節點之間是怎麼通訊的呢?通過Gossip協定!
Redis Cluster叢集通過Gossip協定進行通訊,節點之前不斷交換資訊,交換的資訊內容包括節點出現故障、新節點加入、主從節點變更資訊、slot資訊等等。常用的Gossip訊息分為4種,分別是:ping、pong、meet、fail。
- meet訊息:通知新節點加入。訊息傳送者通知接收者加入到當前叢集,meet訊息通訊正常完成後,接收節點會加入到叢集中並進行週期性的ping、pong訊息交換。
- ping訊息:叢集內交換最頻繁的訊息,叢集內每個節點每秒向多個其他節點傳送ping訊息,用於檢測節點是否線上和交換彼此狀態資訊。
- pong訊息:當接收到ping、meet訊息時,作為響應訊息回覆給傳送方確認訊息正常通訊。pong訊息內部封裝了自身狀態資料。節點也可以向叢集內廣播自身的pong訊息來通知整個叢集對自身狀態進行更新。
- fail訊息:當節點判定叢集內另一個節點下線時,會向叢集內廣播一個fail訊息,其他節點接收到fail訊息之後把對應節點更新為下線狀態。
特別的,每個節點是通過叢集匯流排(cluster bus) 與其他的節點進行通訊的。通訊時,使用特殊的埠號,即對外伺服器埠號加10000。例如如果某個node的埠號是6379,那麼它與其它nodes通訊的埠號是 16379。nodes 之間的通訊採用特殊的二進位制協定。
Hash Slot插槽演演算法
既然是分散式儲存,Cluster叢集使用的分散式演演算法是一致性Hash嘛?並不是,而是Hash Slot插槽演演算法。
插槽演演算法把整個資料庫被分為16384個slot(槽),每個進入Redis的鍵值對,根據key進行雜湊,分配到這16384插槽中的一個。使用的雜湊對映也比較簡單,用CRC16演演算法計算出一個16 位的值,再對16384取模。資料庫中的每個鍵都屬於這16384個槽的其中一個,叢集中的每個節點都可以處理這16384個槽。
叢集中的每個節點負責一部分的hash槽,比如當前叢集有A、B、C個節點,每個節點上的雜湊槽數 =16384/3,那麼就有:
Redis Cluster叢集
Redis Cluster叢集中,需要確保16384個槽對應的node都正常工作,如果某個node出現故障,它負責的slot也會失效,整個叢集將不能工作。
因此為了保證高可用,Cluster叢集引入了主從複製,一個主節點對應一個或者多個從節點。當其它主節點 ping 一個主節點 A 時,如果半數以上的主節點與 A 通訊超時,那麼認為主節點 A 宕機了。如果主節點宕機時,就會啟用從節點。
在Redis的每一個節點上,都有兩個玩意,一個是插槽(slot),它的取值範圍是0~16383。另外一個是cluster,可以理解為一個叢集管理的外掛。當我們存取的key到達時,Redis 會根據CRC16演演算法得出一個16 bit的值,然後把結果對16384取模。醬紫每個key都會對應一個編號在 0~16383 之間的雜湊槽,通過這個值,去找到對應的插槽所對應的節點,然後直接自動跳轉到這個對應的節點上進行存取操作。
雖然資料是分開儲存在不同節點上的,但是對使用者端來說,整個叢集Cluster,被看做一個整體。使用者端端連線任意一個node,看起來跟操作單範例的Redis一樣。當用戶端操作的key沒有被分配到正確的node節點時,Redis會返回轉向指令,最後指向正確的node,這就有點像瀏覽器頁面的302 重定向跳轉。
故障轉移
Redis叢集實現了高可用,當叢集內節點出現故障時,通過故障轉移,以保證叢集正常對外提供服務。
redis叢集通過ping/pong訊息,實現故障發現。這個環境包括主觀下線和客觀下線。
主觀下線: 某個節點認為另一個節點不可用,即下線狀態,這個狀態並不是最終的故障判定,只能代表一個節點的意見,可能存在誤判情況。
主觀下線
客觀下線: 指標記一個節點真正的下線,叢集內多個節點都認為該節點不可用,從而達成共識的結果。如果是持有槽的主節點故障,需要為該節點進行故障轉移。
流程如下:
客觀下線
故障恢復:故障發現後,如果下線節點的是主節點,則需要在它的從節點中選一個替換它,以保證叢集的高可用。流程如下:
分散式鎖,是控制分散式系統不同程序共同存取共用資源的一種鎖的實現。秒殺下單、搶紅包等等業務場景,都需要用到分散式鎖,我們專案中經常使用Redis作為分散式鎖。
選了Redis分散式鎖的幾種實現方法,大家來討論下,看有沒有啥問題哈。
if(jedis.setnx(key,lock_value) == 1){ //加鎖
expire(key,100); //設定過期時間
try {
do something //業務請求
}catch(){
}
finally {
jedis.del(key); //釋放鎖
}
}
登入後複製
如果執行完setnx
加鎖,正要執行expire設定過期時間時,程序crash掉或者要重新啟動維護了,那這個鎖就「長生不老」了,別的執行緒永遠獲取不到鎖啦,所以分散式鎖不能這麼實現。
long expires = System.currentTimeMillis() + expireTime; //系統時間+設定的過期時間
String expiresStr = String.valueOf(expires);
// 如果當前鎖不存在,返回加鎖成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果鎖已經存在,獲取鎖的過期時間
String currentValueStr = jedis.get(key);
// 如果獲取到的過期時間,小於系統當前時間,表示已經過期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 鎖已過期,獲取上一個鎖的過期時間,並設定現在鎖的過期時間(不瞭解redis的getSet命令的小夥伴,可以去官網看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考慮多執行緒並行的情況,只有一個執行緒的設定值和當前值相同,它才可以加鎖
return true;
}
}
//其他情況,均返回加鎖失敗
return false;
}
登入後複製
筆者看過有開發小夥伴是這麼實現分散式鎖的,但是這種方案也有這些缺點:
jedis.getSet()
,最終只能有一個使用者端加鎖成功,但是該使用者端鎖的過期時間,可能被別的使用者端覆蓋。10.3:set的擴充套件命令(set ex px nx)(注意可能存在的問題)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加鎖
try {
do something //業務處理
}catch(){
}
finally {
jedis.del(key); //釋放鎖
}
}
登入後複製
這個方案可能存在這樣的問題:
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加鎖
try {
do something //業務處理
}catch(){
}
finally {
//判斷是不是當前執行緒加的鎖,是才釋放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //釋放鎖
}
}
}
登入後複製
在這裡,判斷當前執行緒加的鎖和釋放鎖是不是一個原子操作。如果呼叫jedis.del()釋放鎖的時候,可能這把鎖已經不屬於當前使用者端,會解除他人加的鎖。
一般也是用lua指令碼代替。lua指令碼如下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
登入後複製
這種方式比較不錯了,一般情況下,已經可以使用這種實現方式。但是存在鎖過期釋放了,業務還沒執行完的問題(實際上,估算個業務處理的時間,一般沒啥問題了)。
分散式鎖可能存在鎖過期釋放,業務沒執行完的問題。有些小夥伴認為,稍微把鎖過期時間設定長一些就可以啦。其實我們設想一下,是否可以給獲得鎖的執行緒,開啟一個定時守護執行緒,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。
當前開源框架Redisson就解決了這個分散式鎖問題。我們一起來看下Redisson底層原理是怎樣的吧:
只要執行緒一加鎖成功,就會啟動一個watch dog
看門狗,它是一個後臺執行緒,會每隔10秒檢查一下,如果執行緒1還持有鎖,那麼就會不斷的延長鎖key的生存時間。因此,Redisson就是使用Redisson解決了鎖過期釋放,業務沒執行完問題。
Redis一般都是叢集部署的,假設資料在主從同步過程,主節點掛了,Redis分散式鎖可能會有哪些問題呢?一起來看些這個流程圖:
如果執行緒一在Redis的master節點上拿到了鎖,但是加鎖的key還沒同步到slave節點。恰好這時,master節點發生故障,一個slave節點就會升級為master節點。執行緒二就可以獲取同個key的鎖啦,但執行緒一也已經拿到鎖了,鎖的安全性就沒了。
為了解決這個問題,Redis作者 antirez提出一種高階的分散式鎖演演算法:Redlock。Redlock核心思想是這樣的:
搞多個Redis master部署,以保證它們不會同時宕掉。並且這些master節點是完全相互獨立的,相互之間不存在資料同步。同時,需要確保在這多個master範例上,是與在Redis單範例,使用相同方法來獲取和釋放鎖。
我們假設當前有5個Redis master節點,在5臺伺服器上面執行這些Redis範例。
RedLock的實現步驟:如下
- 1.獲取當前時間,以毫秒為單位。
- 2.按順序向5個master節點請求加鎖。使用者端設定網路連線和響應超時時間,並且超時時間要小於鎖的失效時間。(假設鎖自動失效時間為10秒,則超時時間一般在5-50毫秒之間,我們就假設超時時間是50ms吧)。如果超時,跳過該master節點,儘快去嘗試下一個master節點。
- 3.使用者端使用當前時間減去開始獲取鎖時間(即步驟1記錄的時間),得到獲取鎖使用的時間。當且僅當超過一半(N/2+1,這裡是5/2+1=3個節點)的Redis master節點都獲得鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。(如上圖,10s> 30ms+40ms+50ms+4m0s+50ms)
- 如果取到了鎖,key的真正有效時間就變啦,需要減去獲取鎖所使用的時間。
- 如果獲取鎖失敗(沒有在至少N/2+1個master範例取到鎖,有或者獲取鎖時間已經超過了有效時間),使用者端要在所有的master節點上解鎖(即便有些master節點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網之魚)。
簡化下步驟就是:
跳躍表
什麼是延時雙刪呢?流程圖如下:
延時雙刪流程
這個休眠一會,一般多久呢?都是1秒?
這個休眠時間 = 讀業務邏輯資料的耗時 + 幾百毫秒。為了確保讀請求結束,寫請求可以刪除讀請求可能帶來的快取髒資料。
這種方案還算可以,只有休眠那一會(比如就那1秒),可能有髒資料,一般業務也會接受的。但是如果第二次刪除快取失敗呢?快取和資料庫的資料還是可能不一致,對吧?給Key設定一個自然的expire過期時間,讓它自動過期怎樣?那業務要接受過期時間內,資料的不一致咯?還是有其他更佳方案呢?
因為延時雙刪可能會存在第二步的刪除快取失敗,導致的資料不一致問題。可以使用這個方案優化:刪除失敗就多刪除幾次呀,保證刪除快取成功就可以了呀~ 所以可以引入刪除快取重試機制
刪除快取重試流程
重試刪除快取機制還可以吧,就是會造成好多業務程式碼入侵。其實,還可以這樣優化:通過資料庫的binlog來非同步淘汰key。
以mysql為例吧
redis使用多執行緒並非是完全摒棄單執行緒,redis還是使用單執行緒模型來處理使用者端的請求,只是使用多執行緒來處理資料的讀寫和協定解析,執行命令還是使用單執行緒。
這樣做的目的是因為redis的效能瓶頸在於網路IO而非CPU,使用多執行緒能提升IO讀寫的效率,從而整體提高redis的效能。
Redis通過MULTI、EXEC、WATCH等一組命令集合,來實現事務機制。事務支援一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會按照順序序列化執行佇列中的命令,其他使用者端提交的命令請求不會插入到事務執行命令序列中。
簡言之,Redis事務就是順序性、一次性、排他性的執行一個佇列中的一系列命令。
Redis執行事務的流程如下:
Redis 作為一個K-V的記憶體資料庫,它使用用一張全域性的雜湊來儲存所有的鍵值對。這張雜湊表,有多個雜湊桶組成,雜湊桶中的entry元素儲存了key和value指標,其中*key指向了實際的鍵,*value指向了實際的值。
雜湊表查詢速率很快的,有點類似於Java中的HashMap,它讓我們在O(1) 的時間複雜度快速找到鍵值對。首先通過key計算雜湊值,找到對應的雜湊桶位置,然後定位到entry,在entry找到對應的資料。
什麼是雜湊衝突?
雜湊衝突:通過不同的key,計算出一樣的雜湊值,導致落在同一個雜湊桶中。
Redis為了解決雜湊衝突,採用了鏈式雜湊。鏈式雜湊是指同一個雜湊桶中,多個元素用一個連結串列來儲存,它們之間依次用指標連線。
有些讀者可能還會有疑問:雜湊衝突鏈上的元素只能通過指標逐一查詢再操作。當往雜湊表插入資料很多,衝突也會越多,衝突連結串列就會越長,那查詢效率就會降低了。
為了保持高效,Redis 會對雜湊表做rehash操作,也就是增加雜湊桶,減少衝突。為了rehash更高效,Redis還預設使用了兩個全域性雜湊表,一個用於當前使用,稱為主雜湊表,一個用於擴容,稱為備用雜湊表。
可以的,Redis提供兩個指令生成RDB,分別是save和bgsave。
RESP,英文全稱是Redis Serialization Protocol,它是專門為redis設計的一套序列化協定. 這個協定其實在redis的1.2版本時就已經出現了,但是到了redis2.0才最終成為redis通訊協定的標準。
RESP主要有實現簡單、解析速度快、可讀性好等優點。
應對快取穿透問題,我們可以使用布隆過濾器。布隆過濾器是什麼呢?
布隆過濾器是一種佔用空間很小的資料結構,它由一個很長的二進位制向量和一組Hash對映函陣列成,它用於檢索一個元素是否在一個集合中,空間效率和查詢時間都比一般的演演算法要好的多,缺點是有一定的誤識別率和刪除困難。
布隆過濾器原理是?假設我們有個集合A,A中有n個元素。利用k個雜湊雜湊函數,將A中的每個元素對映到一個長度為a位的陣列B中的不同位置上,這些位置上的二進位制數均設定為1。如果待檢查的元素,經過這k個雜湊雜湊函數的對映後,發現其k個位置上的二進位制數全部為1,這個元素很可能屬於集合A,反之,一定不屬於集合A。
來看個簡單例子吧,假設集合A有3個元素,分別為{d1,d2,d3}。有1個雜湊函數,為Hash1。現在將A的每個元素對映到長度為16位元陣列B。
我們現在把d1對映過來,假設Hash1(d1)= 2,我們就把陣列B中,下標為2的格子改成1,如下:
我們現在把d2也對映過來,假設Hash1(d2)= 5,我們把陣列B中,下標為5的格子也改成1,如下:
接著我們把d3也對映過來,假設Hash1(d3)也等於 2,它也是把下標為2的格子標1:
因此,我們要確認一個元素dn是否在集合A裡,我們只要算出Hash1(dn)得到的索引下標,只要是0,那就表示這個元素不在集合A,如果索引下標是1呢?那該元素可能是A中的某一個元素。因為你看,d1和d3得到的下標值,都可能是1,還可能是其他別的數對映的,布隆過濾器是存在這個缺點的:會存在hash碰撞導致的假陽性,判斷存在誤差。
如何減少這種誤差呢?
我們又增加一個Hash2雜湊對映函數,假設Hash2(d1)=6,Hash2(d3)=8,它倆不就不衝突了嘛,如下:
即使存在誤差,我們可以發現,布隆過濾器並沒有存放完整的資料,它只是運用一系列雜湊對映函數計算出位置,然後填充二進位制向量。如果數量很大的話,布隆過濾器通過極少的錯誤率,換取了儲存空間的極大節省,還是挺划算的。
目前布隆過濾器已經有相應實現的開源類庫啦,如Google的Guava類庫,Twitter的 Algebird 類庫,信手拈來即可,或者基於Redis自帶的Bitmaps自行實現設計也是可以的。
更多程式設計相關知識,請存取:!!
以上就是20個Redis經典面試題彙總及答案(分享)的詳細內容,更多請關注TW511.COM其它相關文章!