快手Java一面11問(附參考答案)

現在已經到了面試招聘比較火熱的時候,後續會分享一些面試真題供大家複習參考。準備面試的過程中,一定要多看面經,多自測!

今天分享的是一位貴州大學的同學分享的快手一面面經。

快手一面主要會問一些基礎問題,也就是比較簡單且容易準備的常規八股,通常不會問項目。到了二面,會開始問項目,各種問題也挖掘的更深一些。

很多同學覺得這種基礎問題的考查意義不大,實際上還是很有意義的,這種基礎性的知識在日常開發中也會需要經常用到。例如,線程池這塊的拒絕策略、核心參數配置什麼的,如果你不瞭解,實際項目中使用線程池可能就用的不是很明白,容易出現問題。而且,其實這種基礎性的問題是最容易準備的,像各種底層原理、系統設計、場景題以及深挖你的項目這類纔是最難的!

1、Long 的長度和範圍,爲什麼要減 1 ?

先來複習一下Java 中的 8 種基本數據類型:

  • 6 種數字類型:
    • 4 種整數型:byteshortintlong
    • 2 種浮點型:floatdouble
  • 1 種字符類型:char
  • 1 種布爾型:boolean

這 8 種基本數據類型的默認值以及所佔空間的大小如下:

基本類型 位數 字節 默認值 取值範圍
byte 8 1 0 -128 ~ 127
short 16 2 0 -32768(-2^15) ~ 32767(2^15 - 1)
int 32 4 0 -2147483648 ~ 2147483647
long 64 8 0L -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)
char 16 2 'u0000' 0 ~ 65535(2^16 - 1)
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false true、false

可以看到,像 byteshortintlong能表示的最大正數都減 1 了。這是爲什麼呢?這是因爲在二進制補碼錶示法中,最高位是用來表示符號的(0 表示正數,1 表示負數),其餘位表示數值部分。所以,如果我們要表示最大的正數,我們需要把除了最高位之外的所有位都設爲 1。如果我們再加 1,就會導致溢出,變成一個負數。

對於 boolean,官方文檔未明確定義,它依賴於 JVM 廠商的具體實現。邏輯上理解是佔用 1 位,但是實際中會考慮計算機高效存儲因素。

另外,Java 的每種基本類型所佔存儲空間的大小不會像其他大多數語言那樣隨機器硬件架構的變化而變化。這種所佔存儲空間大小的不變性是 Java 程序比用其他大多數語言編寫的程序更具可移植性的原因之一(《Java 編程思想》2.2 節有提到)。

2、JAVA 異常的層次結構

Java 異常類層次結構圖概覽:

Java 異常類層次結構圖

在 Java 中,所有的異常都有一個共同的祖先 java.lang 包中的 Throwable 類。Throwable 類有兩個重要的子類:

  • Exception :程序本身可以處理的異常,可以通過 catch 來進行捕獲。Exception 又可以分爲 Checked Exception (受檢查異常,必須處理) 和 Unchecked Exception (不受檢查異常,可以不處理)。
  • ErrorError 屬於程序無法處理的錯誤 ,我們沒辦法通過 catch 來進行捕獲不建議通過catch捕獲 。例如 Java 虛擬機運行錯誤(Virtual MachineError)、虛擬機內存不夠錯誤(OutOfMemoryError)、類定義錯誤(NoClassDefFoundError)等 。這些異常發生時,Java 虛擬機(JVM)一般會選擇線程終止。

3、JAVA的集合類有了解麼?

Java 集合, 也叫作容器,主要是由兩大接口派生而來:一個是 Collection接口,主要用於存放單一元素;另一個是 Map 接口,主要用於存放鍵值對。對於Collection 接口,下面又有三個主要的子接口:ListSetQueue

Java 集合框架如下圖所示:

Java 集合框架概覽

注:圖中只列舉了主要的繼承派生關係,並沒有列舉所有關係。比方省略了AbstractList, NavigableSet等抽象類以及其他的一些輔助類,如想深入瞭解,可自行查看源碼。

  • List(對付順序的好幫手): 存儲的元素是有序的、可重複的。
  • Set(注重獨一無二的性質): 存儲的元素不可重複的。
  • Queue(實現排隊功能的叫號機): 按特定的排隊規則來確定先後順序,存儲的元素是有序的、可重複的。
  • Map(用 key 來搜索的專家): 使用鍵值對(key-value)存儲,類似於數學上的函數 y=f(x),"x" 代表 key,"y" 代表 value,key 是無序的、不可重複的,value 是無序的、可重複的,每個鍵最多映射到一個值。

4、ArrayList和LinkedList 區別

  • 是否保證線程安全: ArrayListLinkedList 都是不同步的,也就是不保證線程安全;
  • 底層數據結構: ArrayList 底層使用的是 Object 數組LinkedList 底層使用的是 雙向鏈表 數據結構(JDK1.6 之前爲循環鏈表,JDK1.7 取消了循環。注意雙向鏈表和雙向循環鏈表的區別,下面有介紹到!)
  • 插入和刪除是否受元素位置的影響:
    • ArrayList 採用數組存儲,所以插入和刪除元素的時間複雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間複雜度就是 O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element)),時間複雜度就爲 O(n)。因爲在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向前移一位的操作。
    • LinkedList 採用鏈表存儲,所以在頭尾插入或者刪除元素不受元素位置的影響(add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()),時間複雜度爲 O(1),如果是要在指定位置 i 插入和刪除元素的話(add(int index, E element)remove(Object o),remove(int index)), 時間複雜度爲 O(n) ,因爲需要先移動到指定位置再插入和刪除。
  • 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList(實現了 RandomAccess 接口) 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)方法)。
  • 內存空間佔用: ArrayList 的空間浪費主要體現在在 list 列表的結尾會預留一定的容量空間,而 LinkedList 的空間花費則體現在它的每一個元素都需要消耗比 ArrayList 更多的空間(因爲要存放直接後繼和直接前驅以及數據)。

我們在項目中一般是不會使用到 LinkedList 的,需要用到 LinkedList 的場景幾乎都可以使用 ArrayList 來代替,並且,性能通常會更好!就連 LinkedList 的作者約書亞 · 布洛克(Josh Bloch)自己都說從來不會使用 LinkedList

另外,不要下意識地認爲 LinkedList 作爲鏈表就最適合元素增刪的場景。我在上面也說了,LinkedList 僅僅在頭尾插入或者刪除元素的時候時間複雜度近似 O(1),其他情況增刪元素的平均時間複雜度都是 O(n) 。

補充內容: 雙向鏈表和雙向循環鏈表

雙向鏈表: 包含兩個指針,一個 prev 指向前一個節點,一個 next 指向後一個節點。

雙向鏈表

雙向循環鏈表: 最後一個節點的 next 指向 head,而 head 的 prev 指向最後一個節點,構成一個環。

雙向循環鏈表

補充內容:RandomAccess 接口

public interface RandomAccess {
}

查看源碼我們發現實際上 RandomAccess 接口中什麼都沒有定義。所以,在我看來 RandomAccess 接口不過是一個標識罷了。標識什麼? 標識實現這個接口的類具有隨機訪問功能。

binarySearch() 方法中,它要判斷傳入的 list 是否 RandomAccess 的實例,如果是,調用indexedBinarySearch()方法,如果不是,那麼調用iteratorBinarySearch()方法

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArrayList 實現了 RandomAccess 接口, 而 LinkedList 沒有實現。爲什麼呢?我覺得還是和底層數據結構有關!ArrayList 底層是數組,而 LinkedList 底層是鏈表。數組天然支持隨機訪問,時間複雜度爲 O(1),所以稱爲快速隨機訪問。鏈表需要遍歷到特定位置才能訪問特定位置的元素,時間複雜度爲 O(n),所以不支持快速隨機訪問。ArrayList 實現了 RandomAccess 接口,就表明了他具有快速隨機訪問功能。 RandomAccess 接口只是標識,並不是說 ArrayList 實現 RandomAccess 接口才具有快速隨機訪問功能的!

5、HashMap 有了解麼,它的底層實現,爲什麼線程不安全?

HashMap 主要用來存放鍵值對,它基於哈希表的 Map 接口實現,是常用的 Java 集合之一,是非線程安全的。

HashMap 可以存儲 null 的 key 和 value,但 null 作爲鍵只能有一個,null 作爲值可以有多個

JDK1.8 之前 HashMap 由 數組+鏈表 組成的,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的(“拉鍊法”解決衝突)。 JDK1.8 以後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於等於閾值(默認爲 8)(將鏈表轉換成紅黑樹前會判斷,如果當前數組的長度小於 64,那麼會選擇先進行數組擴容,而不是轉換爲紅黑樹)時,將鏈表轉化爲紅黑樹,以減少搜索時間。

HashMap 默認的初始化大小爲 16。之後每次擴充,容量變爲原來的 2 倍。並且, HashMap 總是使用 2 的冪作爲哈希表的大小。

JDK1.7 及之前版本,在多線程環境下,HashMap 擴容時會造成死循環和數據丟失的問題。

數據丟失這個在 JDK1.7 和 JDK 1.8 中都存在,這裏以 JDK 1.8 爲例進行介紹。

JDK 1.8 後,在 HashMap 中,多個鍵值對可能會被分配到同一個桶(bucket),並以鏈表或紅黑樹的形式存儲。多個線程對 HashMapput 操作會導致線程不安全,具體來說會有數據覆蓋的風險。

舉個例子:

  • 兩個線程 1,2 同時進行 put 操作,並且發生了哈希衝突(hash 函數計算出的插入下標是相同的)。
  • 不同的線程可能在不同的時間片獲得 CPU 執行的機會,當前線程 1 執行完哈希衝突判斷後,由於時間片耗盡掛起。線程 2 先完成了插入操作。
  • 隨後,線程 1 獲得時間片,由於之前已經進行過 hash 碰撞的判斷,所有此時會直接進行插入,這就導致線程 2 插入的數據被線程 1 覆蓋了。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // ...
    // 判斷是否出現 hash 碰撞
    // (n - 1) & hash 確定元素存放在哪個桶中,桶爲空,新生成結點放入桶中(此時,這個結點是放在數組中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已經存在元素(處理hash衝突)
    else {
    // ...
}

還有一種情況是這兩個線程同時 put 操作導致 size 的值不正確,進而導致數據覆蓋的問題:

  1. 線程 1 執行 if(++size > threshold) 判斷時,假設獲得 size 的值爲 10,由於時間片耗盡掛起。
  2. 線程 2 也執行 if(++size > threshold) 判斷,獲得 size 的值也爲 10,並將元素插入到該桶位中,並將 size 的值更新爲 11。
  3. 隨後,線程 1 獲得時間片,它也將元素放入桶位中,並將 size 的值更新爲 11。
  4. 線程 1、2 都執行了一次 put 操作,但是 size 的值只增加了 1,也就導致實際上只有一個元素被添加到了 HashMap 中。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // ...
    // 實際大小大於閾值則擴容
    if (++size > threshold)
        resize();
    // 插入後回調
    afterNodeInsertion(evict);
    return null;
}

6、CoucurHashMap和HashTable

ConcurrentHashMapHashtable 的區別主要體現在實現線程安全的方式上不同。

  • 底層數據結構: JDK1.7 的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟 HashMap1.8 的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
  • 實現線程安全的方式(重要):
    • 在 JDK1.7 的時候,ConcurrentHashMap 對整個桶數組進行了分割分段(Segment,分段鎖),每一把鎖只鎖容器其中一部分數據(下面有示意圖),多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。
    • 到了 JDK1.8 的時候,ConcurrentHashMap 已經摒棄了 Segment 的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6 以後 synchronized 鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本;
    • Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

下面,我們再來看看兩者底層數據結構的對比圖。

Hashtable :

Hashtable 的內部結構

https://www.cnblogs.com/chengxiao/p/6842045.html>

JDK1.7 的 ConcurrentHashMap

Java7 ConcurrentHashMap 存儲結構

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。

Segment 數組中的每個元素包含一個 HashEntry 數組,每個 HashEntry 數組屬於鏈表結構。

JDK1.8 的 ConcurrentHashMap

Java8 ConcurrentHashMap 存儲結構

JDK1.8 的 ConcurrentHashMap 不再是 Segment 數組 + HashEntry 數組 + 鏈表,而是 Node 數組 + 鏈表 / 紅黑樹。不過,Node 只能用於鏈表的情況,紅黑樹的情況需要使用 TreeNode。當衝突鏈表達到一定長度時,鏈表會轉換成紅黑樹。

TreeNode是存儲紅黑樹節點,被TreeBin包裝。TreeBin通過root屬性維護紅黑樹的根結點,因爲紅黑樹在旋轉的時候,根結點可能會被它原來的子節點替換掉,在這個時間點,如果有其他線程要寫這棵紅黑樹就會發生線程不安全問題,所以在 ConcurrentHashMapTreeBin通過waiter屬性維護當前使用這棵紅黑樹的線程,來防止其他線程的進入。

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
...
}

7、線程池有了解麼,講一下。

顧名思義,線程池就是管理一系列線程的資源池,其提供了一種限制和管理線程資源的方式。每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java 併發編程的藝術》書中的部分內容來總結一下使用線程池的好處:

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

線程池一般用於執行多個不相關聯的耗時任務,沒有多線程的情況下,任務順序執行,使用了線程池的話可讓多個不相關聯的任務同時執行。

關於線程池的詳細介紹,可以參考 Java 線程池詳解 這篇文章。

8、線程池配置無界隊列了之後,拒絕策略怎麼搞,什麼時候用到無界對列?

線程池配置無界隊列了之後,拒絕策略其實就失去了意義,因爲無論有多少任務提交到線程池,都會被放入隊列中等待執行,不會觸發拒絕策略。不過,這樣可能堆積大量的請求,從而導致 OOM。因此,一般不推薦使用誤解隊列。

假設不是無界隊列的話,如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任務時,ThreadPoolTaskExecutor 定義一些拒絕策略:

  • ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務的處理。
  • ThreadPoolExecutor.CallerRunsPolicy:調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程序的整體性能。如果您的應用程序可以承受此延遲並且你要求任何一個任務請求都要被執行的話,你可以選擇這個策略。
  • ThreadPoolExecutor.DiscardPolicy:不處理新任務,直接丟棄掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略將丟棄最早的未處理的任務請求。

舉個例子:

Spring 通過 ThreadPoolTaskExecutor 或者我們直接通過 ThreadPoolExecutor 的構造函數創建線程池的時候,當我們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy。在默認情況下,ThreadPoolExecutor 將拋出 RejectedExecutionException 來拒絕新來的任務 ,這代表你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略爲我們提供可伸縮隊列(這個直接查看 ThreadPoolExecutor 的構造函數源碼就可以看出,比較簡單的原因,這裏就不貼代碼了)。

9、MVCC 講一下

MVCC 是多版本併發控制方法,即對一份數據會存儲多個版本,通過事務的可見性來保證事務能看到自己應該看到的版本。通常會有一個全局的版本分配器來爲每一行數據設置版本號,版本號是唯一的。

MVCC 在 MySQL 中實現所依賴的手段主要是: 隱藏字段、read view、undo log

  • undo log : undo log 用於記錄某行數據的多個版本的數據。
  • read view 和 隱藏字段 : 用來判斷當前版本數據的可見性。

關於 InnoDB 對 MVCC 的具體實現可以看這篇文章:InnoDB存儲引擎對MVCC的實現

10、事務特性、隔離級別

關係型數據庫(例如:MySQLSQL ServerOracle 等)事務都有 ACID 特性:

ACID

  1. 原子性Atomicity):事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;
  2. 一致性Consistency):執行事務前後,數據保持一致,例如轉賬業務中,無論事務是否成功,轉賬者和收款人的總額應該是不變的;
  3. 隔離性Isolation):併發訪問數據庫時,一個用戶的事務不被其他事務所幹擾,各併發事務之間數據庫是獨立的;
  4. 持久性Durability):一個事務被提交之後。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。

🌈 這裏要額外補充一點:只有保證了事務的持久性、原子性、隔離性之後,一致性才能得到保障。也就是說 A、I、D 是手段,C 是目的! 想必大家也和我一樣,被 ACID 這個概念被誤導了很久! 我也是看周志明老師的公開課《周志明的軟件架構課》才搞清楚的(多看好書!!!)。

AID->C

另外,DDIA 也就是 《Designing Data-Intensive Application(數據密集型應用系統設計)》 的作者在他的這本書中如是說:

Atomicity, isolation, and durability are properties of the database, whereas consis‐
tency (in the ACID sense) is a property of the application. The application may rely
on the database’s atomicity and isolation properties in order to achieve consistency,
but it’s not up to the database alone.

翻譯過來的意思是:原子性,隔離性和持久性是數據庫的屬性,而一致性(在 ACID 意義上)是應用程序的屬性。應用可能依賴數據庫的原子性和隔離屬性來實現一致性,但這並不僅取決於數據庫。因此,字母 C 不屬於 ACID 。

《Designing Data-Intensive Application(數據密集型應用系統設計)》這本書強推一波,值得讀很多遍!豆瓣有接近 90% 的人看了這本書之後給了五星好評。另外,中文翻譯版本已經在 GitHub 開源,地址:https://github.com/Vonng/ddia

SQL 標準定義了四個隔離級別:

  • READ-UNCOMMITTED(讀取未提交) :最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致髒讀、幻讀或不可重複讀。
  • READ-COMMITTED(讀取已提交) :允許讀取併發事務已經提交的數據,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生。
  • REPEATABLE-READ(可重複讀) :對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。
  • SERIALIZABLE(可串行化) :最高的隔離級別,完全服從 ACID 的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。

隔離級別 髒讀 不可重複讀 幻讀
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

MySQL 的隔離級別基於鎖和 MVCC 機制共同實現的。

SERIALIZABLE 隔離級別是通過鎖來實現的,READ-COMMITTED 和 REPEATABLE-READ 隔離級別是基於 MVCC 實現的。不過, SERIALIZABLE 之外的其他隔離級別可能也需要用到鎖機制,就比如 REPEATABLE-READ 在當前讀情況下需要使用加鎖讀來保證不會出現幻讀。

MySQL InnoDB 存儲引擎的默認支持的隔離級別是 REPEATABLE-READ(可重讀)。我們可以通過SELECT @@tx_isolation;命令來查看,MySQL 8.0 該命令改爲SELECT @@transaction_isolation;

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+

關於 MySQL 事務隔離級別的詳細介紹,可以看看我寫的這篇文章:MySQL事務隔離級別詳解

11、Redis 有了解過麼?zset 的底層數據結構?

Redis是一個基於 C 語言開發的開源數據庫(BSD 許可),與傳統數據庫不同的是 Redis 的數據是存在內存中的(內存數據庫),讀寫速度非常快,被廣泛應用於緩存方向。並且,Redis 存儲的是 KV 鍵值對數據。

爲了滿足不同的業務場景,Redis 內置了多種數據類型實現(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。並且,Redis 還支持事務、持久化、Lua 腳本、多種開箱即用的集羣方案(Redis Sentinel、Redis Cluster)。

Redis 沒有外部依賴,Linux 和 OS X 是 Redis 開發和測試最多的兩個操作系統,官方推薦生產環境使用 Linux 部署 Redis。

Redis 內部做了非常多的性能優化,比較重要的有下面 3 點:

  1. Redis 基於內存,內存的訪問速度是磁盤的上千倍;
  2. Redis 基於 Reactor 模式設計開發了一套高效的事件處理模型,主要是單線程事件循環和 IO 多路複用(Redis 線程模式後面會詳細介紹到);
  3. Redis 內置了多種優化過後的數據結構實現,性能非常高。

Redis 除了做緩存,還能做什麼?

  • 分佈式鎖:通過 Redis 來做分佈式鎖是一種比較常見的方式。通常情況下,我們都是基於 Redisson 來實現分佈式鎖。關於 Redis 實現分佈式鎖的詳細介紹,可以看我寫的這篇文章:分佈式鎖詳解
  • 限流:一般是通過 Redis + Lua 腳本的方式來實現限流。相關閱讀:《我司用了 6 年的 Redis 分佈式限流器,可以說是非常厲害了!》
  • 消息隊列:Redis 自帶的 list 數據結構可以作爲一個簡單的隊列使用。Redis 5.0 中增加的 stream 類型的數據結構更加適合用來做消息隊列。它比較類似於 Kafka,有主題和消費組的概念,支持消息持久化以及 ACK 機制。
  • 延時隊列:Redisson 內置了延時隊列(基於 sorted set 實現的)。
  • 分佈式 Session :利用 string 或者 hash 保存 Session 數據,所有的服務器都可以訪問。
  • 複雜業務場景:通過 Redis 以及 Redis 擴展(比如 Redisson)提供的數據結構,我們可以很方便地完成很多複雜的業務場景比如通過 bitmap 統計活躍用戶、通過 sorted set 維護排行榜。
  • ......

Redis 共有 5 種基本數據結構:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

這 5 種數據結構是直接提供給用戶使用的,是數據的保存形式,其底層實現主要依賴這 8 種數據結構:簡單動態字符串(SDS)、LinkedList(雙向鏈表)、Dict(哈希表/字典)、SkipList(跳躍表)、Intset(整數集合)、ZipList(壓縮列表)、QuickList(快速列表)。

Redis 基本數據結構的底層數據結構實現如下:

String List Hash Set Zset
SDS LinkedList/ZipList/QuickList Dict、ZipList Dict、Intset ZipList、SkipList

Redis 3.2 之前,List 底層實現是 LinkedList 或者 ZipList。 Redis 3.2 之後,引入了 LinkedList 和 ZipList 的結合 QuickList,List 的底層實現變爲 QuickList。從 Redis 7.0 開始, ZipList 被 ListPack 取代。

Redis相關面試題總結

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