金九銀十即將到來,這篇文章為大家整理分享20道Redis經典面試題,希望對大家有幫助!
Redis,英文全名為Remote Dictionary Server(遠端字典服務),是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌類型、Key-Value資料庫,並提供多種語言的API。 【相關推薦:Redis影片教學】
與MySQL資料庫不同的是,Redis的資料是存在記憶體中的。它的讀寫速度非常快,每秒鐘可以處理超過10萬次讀寫操作。因此redis被廣泛應用於快取,另外,Redis也常用來做分散式鎖定。除此之外,Redis支援交易、持久化、LUA 腳本、LRU 驅動事件、多種叢集方案。
大多數小夥伴都知道,Redis有以下這五種基本型別:
、
get key等
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(雜湊)
、
hget key field
、
hashtable(雜湊表)
、
lrange key start end
##內部編碼:ziplist(壓縮清單) 、linkedlist(鍊錶)lpush lpop=Stack(堆疊)
- ##list應用程式場景參考以下:
lpsh ltrim=Capped Collection(有限集合)
lpush brpop=Message Queue(訊息佇列)############Set(集合)########### #sadd key element [element ... ]
、smembers key
intset(整數集合)
、hashtable(雜湊表)
zadd key score member [score member ...]
,zrank key member
ziplist(壓縮列表)
、skiplist (跳躍表)
我們都知道記憶體讀寫是比在磁碟快很多的,Redis基於記憶體儲存實作的資料庫,相對於資料存在磁碟的MySQL資料庫,省去磁碟I/O的消耗。
我們知道,Mysql索引為了提高效率,選擇了B 樹的資料結構。其實合理的資料結構,就是可以讓你的應用程式/程式更快。先看一下Redis的資料結構&內部編碼圖:
- 字串長度處理: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復用技術可以讓單一執行緒高效的處理多個連線請求,而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:預設策略,當記憶體不足以容納新寫入資料時,新寫入操作會報錯。
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節點,並且各個哨兵之間還會進行監控。
簡單來說,哨兵模式就三個作用:
故障切換的過程是怎麼樣的呢
假設主伺服器宕機,哨兵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節點上儲存不同的內容,來解決線上擴充的問題。並且,它也提供複製和故障轉移的功能。
一個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 之間的通訊採用特殊的二進位協定。
既然是分散式存儲,Cluster叢集使用的分散式演算法是一致性Hash嘛?並不是,而是Hash Slot插槽演算法。
插槽演算法把整個資料庫被分成16384個slot(槽),每個進入Redis的鍵值對,根據key進行散列,分配到這16384插槽中的一個。使用的雜湊映射也比較簡單,用CRC16演算法計算出一個16 位元的值,再對16384取模。資料庫中的每個鍵都屬於這16384個槽的其中一個,叢集中的每個節點都可以處理這16384個槽。
叢集中的每個節點負責一部分的hash槽,例如目前叢集有A、B、C個節點,每個節點上的雜湊槽數=16384/3,那麼就有:
Redis Cluster叢集中,需要確保16384個插槽對應的node都正常運作,如果某個node出現故障,它負責的slot也會失效,整個集群將不能工作。
因此為了保證高可用,Cluster叢集引入了主從複製,一個主節點對應一個或多個從節點。當其它主節點 ping 一個主節點 A 時,如果半數以上的主節點與 A 通訊逾時,那麼就認為主節點 A 宕機了。如果主節點宕機時,就會啟用從節點。
在Redis的每一個節點上,都有兩個玩意,一個是插槽(slot),它的取值範圍是016383。另外一個是cluster,可以理解為一個叢集管理的插件。當我們訪問的key到達時,Redis 會根據CRC16演算法得到一個16 bit的值,然後把結果對16384取模。醬紫每個key都會對應一個編號在016383 之間的哈希槽,透過這個值,去找到對應的插槽所對應的節點,然後直接自動跳到這個對應的節點上進行訪問操作。
雖然資料是分開儲存在不同節點上的,但對客戶端來說,整個叢集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()
,最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。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秒?
這個休眠時間 = 讀業務邏輯資料的耗時 幾百毫秒。為了確保讀取請求結束,寫入請求可以刪除讀取請求可能帶來的快取髒資料。
這種方案還算可以,只有休眠那一會(比如就那1秒),可能有髒數據,一般業務也會接受的。但是如果第二次刪除快取失敗呢?快取和資料庫的資料還是可能不一致,對吧?給Key設定一個自然的expire過期時間,讓它自動過期怎麼樣?那業務要接受過期時間內,數據的不一致咯?還是有其他更佳方案呢?
因為延時雙刪可能會存在第二步驟的刪除快取失敗,導致的資料不一致問題。可以使用這個方案優化:刪除失敗就多刪除幾次呀,保證刪除快取成功就可以了呀~ 所以可以引入刪除快取重試機制
#寫入請求更新資料庫
快取因為某些原因,刪除失敗
把刪除失敗的key放到訊息佇列
消費訊息佇列的訊息,取得要刪除的key
#重試刪除快取作業
重試刪除快取機制還可以吧,就是會造成好多業務程式碼入侵。其實,還可以這樣優化:透過資料庫的binlog來非同步淘汰key。
以mysql為例吧
redis使用多線程並非是完全摒棄單線程,redis還是使用單線程模型來處理客戶端的請求,只是使用多線程來處理資料的讀寫和協議解析,執行命令還是使用單線程。
這樣做的目的是因為redis的效能瓶頸在於網路IO而非CPU,使用多執行緒能提升IO讀寫的效率,進而整體提升redis的效能。
Redis透過MULTI、EXEC、WATCH等一組指令集合,來實現交易機制。事務支援一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會依照順序串列化執行佇列中的命令,其他客戶端提交的命令請求不會插入到交易執行命令序列中。
簡言之,Redis事務就是順序性、一次性、排他性的執行一個佇列中的一系列指令。
Redis執行交易的流程如下:
指令 | #描述 |
---|---|
EXEC | |
DISCARD | |
MULTI | |
UNWATCH |
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的經典面試題(附答案分析)的詳細內容。更多資訊請關注PHP中文網其他相關文章!