Redis追命連環問,你能回答到第幾問?(上)Redis簡介,數據類型及緩存雪崩緩存擊穿緩存穿透

 

Redis常見面試題連環問,你能回答到第幾問?(上)

Redis常見面試題連環問,你能回答到第幾問?(中)

Redis常見面試題連環問,你能回答到第幾問?(下)

Redis是後端工程師必備的一項技能,下面分享一位求職者在面試過程中遇到的問題。

面試官說:“我們開始吧。看了你的簡歷,覺得你對redis應該掌握的不錯,我們今天就來討論下redis…”。我想:“來就來,兵來將擋水來土掩”。

一、Redis是什麼

面試官:你先來說下redis是什麼吧

我:(這不就是總結下redis的定義和特點嘛)Redis是C語言開發的一個開源的(遵從BSD協議)高性能鍵值對(key-value)的內存數據庫,可以用作數據庫、緩存、消息中間件等。它是一種NoSQL(not-only sql,泛指非關係型數據庫)的數據庫。

我頓了一下,接着說:Redis作爲一個內存數據庫。

  1. 性能優秀,數據在內存中,讀寫速度非常快,支持併發10W QPS;

  2. 單進程單線程,是線程安全的,採用IO多路複用機制;

  3. 豐富的數據類型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等;

  4. 支持數據持久化。可以將內存中數據保存在磁盤中,重啓時加載;

  5. 主從複製,哨兵,高可用;

  6. 可以用作分佈式鎖;

  7. 可以作爲消息中間件使用,支持發佈訂閱

注:答主回答的還是不錯的,我們看一下官方簡介

Redis是一個基於BSD開源的項目,是一個把結構化的數據放在內存中的一個存儲系統,你可以把它作爲數據庫,緩存和消息中間件來使用。同時支持strings,lists,hashes,sets,sorted sets,bitmaps,hyperloglogs和geospatial indexes等數據類型。

它還內建了複製,lua腳本,LRU(Least Recently Used,最近最少使用),事務等功能,通過redis sentinel(即哨兵)實現高可用,通過redis cluster實現了自動分片。以及事務,發佈/訂閱,自動故障轉移等等。

面試官:總結的不錯,看來是早有準備啊。剛來聽你提到redis支持五種數據類型,那你能簡單說下這五種數據類型嗎?

二、五種數據類型

我:當然可以,但是在說之前,我覺得有必要先來了解下Redis內部內存管理是如何描述這5種數據類型的。說着,我拿着筆給面試官畫了一張圖:

我:首先redis內部使用一個redisObject對象來表示所有的key和value,redisObject最主要的信息如上圖所示:
type表示一個value對象具體是何種數據類型,
encoding是不同數據類型在redis內部的存儲方式。比如:type=string表示value存儲的是一個普通字符串,那麼encoding可以是raw或者int。

我頓了一下,接着說:下面我簡單說下5種數據類型:

  1. string是redis最基本的類型,可以理解成與memcached一模一樣的類型,一個key對應一個value。value不僅是string,也可以是數字。string類型是二進制安全的,意思是redis的string類型可以包含任何數據,比如jpg圖片或者序列化的對象。string類型的值最大能存儲512M。

  2. Hash是一個鍵值(key-value)的集合。redis的hash是一個string的key和value的映射表,Hash特別適合存儲對象。常用命令:hget,hset,hgetall等。

  3. list列表是簡單的字符串列表,按照插入順序排序。可以添加一個元素到列表的頭部(左邊)或者尾部(右邊) 常用命令:lpush、rpush、lpop、rpop、lrange(獲取列表片段)等。
    應用場景:list應用場景非常多,也是Redis最重要的數據結構之一,比如twitter的關注列表,粉絲列表都可以用list結構來實現。
    數據結構:list就是鏈表,可以用來當消息隊列。redis提供了List的push和pop操作,還提供了操作某一段的api,可以直接查詢或者刪除某一段的元素。
    實現方式:redis list的是實現是一個雙向鏈表,支持反向查找和遍歷,更方便操作,不過帶來了額外的內存開銷。

  4. set是string類型的無序集合。集合是通過hashtable實現的。set中的元素是沒有順序的,而且是沒有重複的。
    常用命令:sdd、spop、smembers、sunion等。
    應用場景:redis set對外提供的功能和list一樣是一個列表,特殊之處在於set是自動去重的,而且set提供了判斷某個成員是否在一個set集合中。

  5. zset和set一樣是string類型元素的集合,且不允許重複的元素。常用命令:zadd、zrange、zrem、zcard等。
    使用場景:sorted set可以通過用戶額外提供一個優先級(score)的參數來爲成員排序,並且是插入有序的,即自動排序。當你需要一個有序的並且不重複的集合列表,那麼可以選擇sorted set結構。和set相比,sorted set關聯了一個double類型權重的參數score,使得集合中的元素能夠按照score進行有序排列,redis正是通過分數來爲集合中的成員進行從小到大的排序。
    實現方式:Redis sorted set的內部使用HashMap和跳躍表(skipList)來保證數據的存儲和有序,HashMap裏放的是成員到score的映射,而跳躍表裏存放的是所有的成員,排序依據是HashMap裏存的score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。

數據類型應用場景總結

類型 簡介 特性 場景
string(字符串) 二進制安全 可以包含任何數據,比如jpg圖片或者序列化對象  
Hash(字典) 鍵值對集合,即編程語言中的map類型 適合存儲對象,並且可以像數據庫中的update一個屬性一樣只修改某一項屬性值 存儲、讀取、修改用戶屬性
List(列表) 鏈表(雙向鏈表) 增刪快,提供了操作某一元素的api 最新消息排行;消息隊列
set(集合) hash表實現,元素不重複 添加、刪除、查找的複雜度都是O(1),提供了求交集、並集、差集的操作 共同好友;利用唯一性,統計訪問網站的所有Ip
sorted set(有序集合) 將set中的元素增加一個權重參數score,元素按score有序排列 數據插入集合時,已經進行了天然排序 排行榜;帶權重的消息隊列

面試官:那Redis緩存你一定用過的吧,用的過程中遇到過什麼問題嗎?雪崩瞭解嗎?

我:緩存和數據庫數據一致性問題:分佈式環境下非常容易出現緩存和數據庫間數據一致性問題,針對這一點,如果項目對緩存的要求是強一致性的,那麼就不要使用緩存。我們只能採取合適的策略來降低緩存和數據庫間數據不一致的概率,而無法保證兩者間的強一致性。合適的策略包括合適的緩存更新策略,更新數據庫後及時更新緩存、緩存失敗時增加重試機制。

緩存雪崩

緩存雪崩是指在我們設置緩存時採用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩。

舉個栗子:
如果某電商網站首頁所有Key的失效時間都是12小時,中午12點刷新的,我零點有個大促活動大量用戶湧入,假設每秒6000個請求,本來緩存可以抗住每秒5000個請求,但是緩存中所有Key都失效了。此時6000個/秒的請求全部落在了數據庫上,數據庫必然扛不住,真實情況可能DBA都沒反應過來直接掛了,此時,如果沒什麼特別的方案來處理,DBA很着急,重啓數據庫,但是數據庫立馬又被新流量給打死了。這就是我理解的緩存雪崩。

我心想:同一時間大面積失效,瞬間Redis跟沒有一樣,那這個數量級別的請求直接打到數據庫幾乎是災難性的,你想想如果掛的是一個用戶服務的庫,那其他依賴他的庫所有接口幾乎都會報錯,如果沒做熔斷等策略基本上就是瞬間掛一片的節奏,你怎麼重啓用戶都會把你打掛,等你重啓好的時候,用戶早睡覺去了。

解決方案

緩存失效時的雪崩效應對底層系統的衝擊非常可怕。有一個簡單方案就是將緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。

如果Redis是集羣部署,將熱點數據均勻分佈在不同的Redis庫中也能避免全部失效。
或者設置熱點數據永不過期,有更新操作就更新緩存就好了(比如更新了首頁商品,那你刷下緩存就好了,不要設置過期時間),電商首頁的數據也可以用這個操作,保險。

面試官:那你瞭解緩存穿透和擊穿麼,可以說說他們跟雪崩的區別嗎?

緩存穿透 與 緩存擊穿

我:緩存穿透是指查詢一個一定不存在的數據,由於緩存是不命中時被動寫的,並且出於容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。而用戶(黑客)不斷髮起請求,這就是漏洞。

舉個栗子:我們數據庫的id都是從1自增的,如果發起id=-1的數據或者id特別大不存在的數據,這樣的不斷攻擊導致數據庫壓力很大,嚴重會擊垮數據庫。

我又接着說:至於緩存擊穿嘛,這個跟緩存雪崩有點像,但是又有一點不一樣,緩存雪崩是因爲大面積的緩存失效,打崩了DB,而緩存擊穿不同的是緩存擊穿是指一個Key非常熱點,在不停地扛着大量的請求,大併發集中對這一個點進行訪問,當這個Key在失效的瞬間,持續的大併發直接落到了數據庫上,就在這個Key的點上擊穿了緩存。併發的請求可能會瞬間把後端DB壓垮。

面試官露出欣慰的眼光:那他們分別怎麼解決?

緩存穿透我會在接口層增加校驗,比如用戶鑑權,參數做校驗,不合法的校驗直接return,比如id做基礎校驗,id<=0直接攔截。
從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫爲key-null,緩存有效時間可以設置短點,如30秒(設置太長會導致正常情況也沒法使用)。這樣可以防止攻擊用戶反覆用同一個id暴力攻擊。

Redis裏還有一個高級用法**布隆過濾器(Bloom Filter)**這個也能很好的預防緩存穿透的發生,他的原理也很簡單,就是利用高效的數據結構和算法快速判斷出你這個Key是否在數據庫中存在,不存在你return就好了,存在你就去查DB刷新KV再return。但布隆過濾器有一定的誤判性。

緩存擊穿的話,主要有三種解決方法:

  1. 使用互斥鎖(mutex key):這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其他線程等待構建緩存的線程執行完,重新從緩存獲取數據就可以了。

  2. "提前"使用互斥鎖(mutex key):在value內部設置1個超時值(timeout1), timeout1比實際的緩存失效時間timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上獲取新的數據到cache並延長timeout1並重新設置到cache。

  3. “永遠不過期”:然後通過定時job去刷新緩存。 

加鎖僞代碼如下:

public function getData($key)
{
	$data = redis->get($key);
    if (!is_null($data)) {
        //緩存未過期
        if ($data['expire'] > time()){
            return $data['data'];
        }
        //加鎖失敗說明已經有請求執行加鎖,返回之前的緩存數據
        if (!Redis::setnx($lockKey,1)) {
            return $data['data'];
        }
    }
    usleep(100);
    $data_new = $this->searchDB($key);
    $data = [
        'data' => $data_new,
        'expire' => time() + $expire
    ];
    $r = redis->set($key, $data, $expire);
    //解鎖
    redis->del($lockKey);
    return $data['data'];
}

 

今天就分享到這裏,預知後事如何且聽下回分解。

更多精彩歡迎關注公衆號。


                                                                      

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章