這篇文章幫大家整理了20道經典Redis面試題,希望對大家有幫助。
Redis,英文全名是Remote Dictionary Server(遠端字典服務),是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、 Key-Value資料庫,並提供多種語言的API。
與MySQL資料庫不同的是,Redis的資料是存在記憶體中的。它的讀寫速度非常快,每秒鐘可以處理超過10萬次讀寫操作。因此redis被廣泛應用於快取,另外,Redis也常用來做分散式鎖定。除此之外,Redis支援交易、持久化、LUA 腳本、LRU 驅動事件、多種叢集方案。
大多數小夥伴都知道,Redis有以下這五種基本類型:
它也有三種特殊的資料結構類型
String(字串)
、
get key等
char[]實作的,而Redis使用
SDS(simple dynamic string) 封裝,sds原始碼如下:
struct sdshdr{ unsigned int len; // 标记buf的长度 unsigned int free; //标记buf中未使用的元素个数 char buf[]; // 存放元素的坑 }
SDS結構,而C語言原生的char[]不香嗎?
舉例其中一點,SDS中,O(1)時間複雜度,就可以取得字串長度;而C 字串,需要遍歷整個字串,時間複雜度為O(n)Hash(雜湊)
、
hget key field
、
hashtable(雜湊表)
、
lrange key start end
Set(集合)lpush lpop=Stack(堆疊)lpush rpop=Queue(佇列)lpsh ltrim=Capped Collection(有限集合)lpush brpop=Message Queue(訊息佇列)
、
smembers key
、
hashtable(雜湊表)
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的缺點
主從模式,哨兵模式,叢集模式。
主從複製機制#
主從複製包括全量複製,增量複製兩種。一般當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為例吧MULTI、EXEC、WATCH等一組指令集合,來實現交易機制。事務支援一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會依照順序串列化執行佇列中的命令,其他客戶端提交的命令請求不會插入到交易執行命令序列中。
簡言之,Redis事務就是順序性、一次性、排他性的執行一個佇列中的一系列指令。
Redis執行交易的流程如下: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經典面試題彙整及答案(分享)的詳細內容。更多資訊請關注PHP中文網其他相關文章!