1. 程式人生 > 其它 >騰訊雲後端十五連問

騰訊雲後端十五連問

前言

大家好,我是撿田螺的小男孩,最近一位朋友(6年工作經驗)面了騰訊雲,以下是面試題和答案。加油,一起卷。

  1. 聊聊專案,好的設計,好的程式碼
  2. 談談什麼是零拷貝?
  3. 一共有幾種 IO 模型?NIO 和多路複用的區別?
  4. Future 實現阻塞等待獲取結果的原理?
  5. ReentrantLock和 Synchronized 的區別?Synchronized 的原理?
  6. 聊聊AOS?ReentrantLock的實現原理?
  7. 樂觀鎖和悲觀鎖, 讓你來寫你怎麼實現?
  8. Paxos 協議瞭解?工作流程是怎麼樣的?
  9. B+樹聊一下?B+樹是不是有序?B+樹和B-樹的主要區別?B+樹索引,一次查詢過程?
  10. TCP的擁塞機制
  11. 聊聊JVM調優
  12. 資料庫分庫分表的缺點是啥?
  13. 分散式事務如何解決?TCC 瞭解?
  14. RocketMQ 如何保證訊息的準確性和安全性?
  15. 演算法題:三個數求和
  • 公眾號:撿田螺的小男孩

1.聊聊專案,好的設計,好的程式碼

專案的話,你可以聊聊你平時做的專案,尤其有亮點的專案。如果沒有什麼特別亮點的專案,也可以說說一些好的設計,或者你優化了什麼介面,效能提升了多少,優化了什麼慢SQL都可以。甚至是一些好的程式碼寫法都可以。

如果是講優化介面的話,你可以看下我這篇文章哈:

記一次介面效能優化實踐總結:優化介面效能的八個建議

如果是程式碼優化細節,可以看我這篇:

工作四年,分享50個讓你程式碼更好的小建議

如果是慢SQL優化,可以看下我之前MySQL專欄系列文章哈:

2. 談談什麼是零拷貝?

零拷貝是指計算機執行IO操作時,CPU不需要將資料從一個儲存區域複製到另一個儲存區域,從而可以減少上下文切換以及CPU的拷貝時間。它是一種I/O操作優化技術。

傳統 IO 的執行流程

傳統的IO流程,包括read和write的過程。

  • read:把資料從磁碟讀取到核心緩衝區,再拷貝到使用者緩衝區
  • write:先把資料寫入到socket緩衝區,最後寫入網絡卡裝置。
  1. 使用者應用程序呼叫read函式,向作業系統發起IO呼叫,上下文從使用者態轉為核心態(切換1)
  2. DMA控制器把資料從磁碟中,讀取到核心緩衝區。
  3. CPU把核心緩衝區資料,拷貝到使用者應用緩衝區,上下文從核心態轉為使用者態(切換2),read函式返回
  4. 使用者應用程序通過write函式,發起IO呼叫,上下文從使用者態轉為核心態(切換3)
  5. CPU將使用者緩衝區中的資料,拷貝到socket緩衝區
  6. DMA控制器把資料從socket緩衝區,拷貝到網絡卡裝置,上下文從核心態切換回使用者態(切換4),write函式返回

傳統IO的讀寫流程,包括了4次上下文切換(4次使用者態和核心態的切換),4次資料拷貝(兩次CPU拷貝以及兩次的DMA拷貝)。

零拷貝實現方式

零拷貝並不是沒有拷貝資料,而是減少使用者態/核心態的切換次數以及CPU拷貝的次數。零拷貝一般有這三種實現方式:

  • mmap+write
  • sendfile
  • 帶有DMA收集拷貝功能的sendfile

mmap+write

mmap就是用了虛擬記憶體這個特點,它將核心中的讀緩衝區與使用者空間的緩衝區進行對映,以減少資料拷貝次數!

  1. 使用者程序通過mmap方法向作業系統核心發起IO呼叫,上下文從使用者態切換為核心態。
  2. CPU利用DMA控制器,把資料從硬碟中拷貝到核心緩衝區。
  3. 上下文從核心態切換回使用者態,mmap方法返回。
  4. 使用者程序通過write方法向作業系統核心發起IO呼叫,上下文從使用者態切換為核心態。
  5. CPU將核心緩衝區的資料拷貝到的socket緩衝區。
  6. CPU利用DMA控制器,把資料從socket緩衝區拷貝到網絡卡,上下文從核心態切換回使用者態,write呼叫返回。

mmap+write實現的零拷貝,I/O發生了4次使用者空間與核心空間的上下文切換,以及3次資料拷貝(包括了2次DMA拷貝和1次CPU拷貝)。

sendfile

sendfile表示在兩個檔案描述符之間傳輸資料,它是在作業系統核心中操作的,避免了資料從核心緩衝區和使用者緩衝區之間的拷貝操作

  1. 使用者程序發起sendfile系統呼叫,上下文(切換1)從使用者態轉向核心態
  2. DMA控制器,把資料從硬碟中拷貝到核心緩衝區。
  3. CPU將讀緩衝區中資料拷貝到socket緩衝區
  4. DMA控制器,非同步把資料從socket緩衝區拷貝到網絡卡,
  5. 上下文(切換2)從核心態切換回使用者態,sendfile呼叫返回。

sendfile實現的零拷貝,I/O發生了2次使用者空間與核心空間的上下文切換,以及3次資料拷貝。其中3次資料拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

帶有DMA收集拷貝功能的sendfile

linux 2.4版本之後,對sendfile做了優化升級,引入SG-DMA技術,其實就是對DMA拷貝加入了scatter/gather操作,它可以直接從核心空間緩衝區中將資料讀取到網絡卡。使用這個特點搞零拷貝,即還可以多省去一次CPU拷貝。

  1. 使用者程序發起sendfile系統呼叫,上下文(切換1)從使用者態轉向核心態
  2. DMA控制器,把資料從硬碟中拷貝到核心緩衝區。
  3. CPU把核心緩衝區中的檔案描述符資訊(包括核心緩衝區的記憶體地址和偏移量)傳送到socket緩衝區
  4. DMA控制器根據檔案描述符資訊,直接把資料從核心緩衝區拷貝到網絡卡
  5. 上下文(切換2)從核心態切換回使用者態,sendfile呼叫返回。

可以發現,sendfile+DMA scatter/gather實現的零拷貝,I/O發生了2次使用者空間與核心空間的上下文切換,以及2次資料拷貝。其中2次資料拷貝都是包DMA拷貝。這就是真正的 零拷貝(Zero-copy) 技術,全程都沒有通過CPU來搬運資料,所有的資料都是通過DMA來進行傳輸的。

看一遍就理解:零拷貝詳解

3. 一共有幾種 IO 模型?NIO 和多路複用的區別?

一共有五種IO模型

  • 阻塞IO模型
  • 非阻塞IO模型
  • IO多路複用模型
  • IO模型之訊號驅動模型
  • IO 模型之非同步IO(AIO)

NIO(非阻塞IO模型)

NIO,即Non-Blocking IO,是非阻塞IO模型。非阻塞IO的流程如下:

  1. 應用程序向作業系統核心,發起recvfrom讀取資料。
  2. 作業系統核心資料沒有準備好,立即返回EWOULDBLOCK錯誤碼。
  3. 應用程式程序輪詢呼叫,繼續向作業系統核心發起recvfrom讀取資料。
  4. 作業系統核心資料準備好了,從核心緩衝區拷貝到使用者空間。
  5. 完成呼叫,返回成功提示。

NIO(非阻塞IO模型)存在效能問題,即頻繁的輪詢,導致頻繁的系統呼叫,同樣會消耗大量的CPU資源。可以考慮IO複用模型去解決這個問題。

IO多路複用模型

IO多路複用就是,等到核心資料準備好了,主動通知應用程序再去進行系統呼叫。

IO複用模型核心思路:系統給我們提供一類函式(如我們耳濡目染的select、poll、epoll函式),它們可以同時監控多個fd的操作,任何一個返回核心資料就緒,應用程序再發起recvfrom系統呼叫。

IO多路複用之select

應用程序通過呼叫select函式,可以同時監控多個fd,在select函式監控的fd中,只要有任何一個數據狀態準備就緒了,select函式就會返回可讀狀態,這時應用程序再發起recvfrom請求去讀取資料。

非阻塞IO模型(NIO)中,需要N(N>=1)次輪詢系統呼叫,然而藉助select的IO多路複用模型,只需要發起一次詢問就夠了,大大優化了效能。

但是呢,select有幾個缺點:

  • 監聽的IO最大連線數有限,在Linux系統上一般為1024。
  • select函式返回後,是通過遍歷fdset,找到就緒的描述符fd。(僅知道有I/O事件發生,卻不知是哪幾個流,所以遍歷所有流)

因為存在連線數限制,所以後來又提出了poll。與select相比,poll解決了連線數限制問題。但是呢,select和poll一樣,還是需要通過遍歷檔案描述符來獲取已經就緒的socket。如果同時連線的大量客戶端,在一時刻可能只有極少處於就緒狀態,伴隨著監視的描述符數量的增長,效率也會線性下降。

IO多路複用之epoll

為了解決select/poll存在的問題,多路複用模型epoll誕生,它採用事件驅動來實現,流程圖如下:

epoll先通過epoll_ctl()來註冊一個fd(檔案描述符),一旦基於某個fd就緒時,核心會採用回撥機制,迅速啟用這個fd,當程序呼叫epoll_wait()時便得到通知。這裡去掉了遍歷檔案描述符的坑爹操作,而是採用監聽事件回撥的機制。這就是epoll的亮點。

4. Future 實現阻塞等待獲取結果的原理?

Future.get()用於非同步結果的獲取。它是阻塞的,背後原理是什麼呢?

我們可以看下FutureTask的類結構圖:

FutureTask實現了RunnableFuture介面,RunnableFuture繼承了Runnable和Future這兩個介面, 對於Runnable,我們太熟悉了, 那麼Future呢?

Future 表示一個任務的生命週期,並提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);
    //Future 是否被取消
    boolean isCancelled();
    //當前 Future 是否已結束
    boolean isDone();
    //或取Future的結果值。如果當前 Future 還沒有結束,當前執行緒阻塞等待,
    V get() throws InterruptedException, ExecutionException;
    //獲取 Future 的結果值。與 get()一樣,不過多了超時時間設定
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
複製程式碼

FutureTask 就是Runnable和Future的結合體,我們可以把Runnable看作生產者, Future 看作消費者。而FutureTask 是被這兩者共享的,生產者執行run方法計算結果,消費者通過get方法獲取結果。

生產者消費者模式,如果生產者資料還沒準備的時候,消費者會被阻塞。當生產者資料準備好了以後會喚醒消費者繼續執行。我們來看下FutureTask內部是如何實現的。

FutureTask內部維護了任務狀態state

//NEW 新建狀態,表示FutureTask新建還沒開始執行
private static final int NEW          = 0;
//完成狀態,表示FutureTask
private static final int COMPLETING   = 1;
//任務正常完成,沒有發生異常
private static final int NORMAL       = 2;
//發生異常
private static final int EXCEPTIONAL  = 3;
//取消任務
private static final int CANCELLED    = 4;
//發起中斷請求
private static final int INTERRUPTING = 5;
//中斷請求完成
private static final int INTERRUPTED  = 6;
複製程式碼

生產者run方法:

 public void run() {
        // 如果狀態state不是 NEW,或者設定 runner 值失敗,直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //呼叫callable的call方法,獲取結果
                    result = c.call();
                    //執行成功
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    //執行不成功
                    ran = false;
                    //設定異常
                    setException(ex);
                }
                //執行成功設定返回結果
                if (ran)
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
複製程式碼

消費者的get方法

 public V get() throws InterruptedException, ExecutionException {
     int s = state;
     //如果狀態小於等於 COMPLETING,表示 FutureTask 任務還沒有完成, 則呼叫awaitDone讓當前執行緒等待。
     if (s <= COMPLETING)
         s = awaitDone(false, 0L);
     return report(s);
 }
複製程式碼

awaitDone做了什麼事情呢?

 private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            // 如果當前執行緒是中斷標記,則  
            if (Thread.interrupted()) {
                //那麼從列表中移除節點 q,並丟擲 InterruptedException 異常
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //如果狀態已經完成,表示FutureTask任務已結束
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                //返回
                return s;
            }
            // 表示還有一些後序操作沒有完成,那麼當前執行緒讓出執行權
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //將當前執行緒阻塞等待
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //timed 為 true 表示需要設定超時                                        
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //讓當前執行緒等待 nanos 時間
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }
複製程式碼

當然,面試的時候,不一定要講到原始碼這麼細,只需要將個大概思路就好啦。

5. ReentrantLock和 Synchronized 的區別?Synchronized 的原理?

ReentrantLock和 Synchronized 的區別?

  • Synchronized是依賴於JVM實現的,而ReenTrantLock是API實現的。
  • 在Synchronized優化以前,synchronized的效能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者效能就差不多了。
  • Synchronized的使用比較方便簡潔,它由編譯器去保證鎖的加鎖和釋放。而ReenTrantLock需要手工宣告來加鎖和釋放鎖,最好在finally中宣告釋放鎖。
  • ReentrantLock可以指定是公平鎖還是⾮公平鎖。⽽synchronized只能是⾮公平鎖。
  • ReentrantLock可響應中斷、可輪迴,而Synchronized是不可以響應中斷的,

至於Synchronized的原理,大家可以看我這篇文章哈

Synchronized解析——如果你願意一層一層剝開我的心

6. 聊聊AOS?ReentrantLock的實現原理?

AQS(抽象同步佇列)的核心回答要點就是:

  • state 狀態的維護。
  • CLH佇列
  • ConditionObject通知
  • 模板方法設計模式
  • 獨佔與共享模式。
  • 自定義同步器。

大家可以看下我之前這篇文章哈:AQS解析與實戰

大家綜合ReentrantLock的功能,比如可重入,公平鎖,非公平鎖等,與AQS結合一起講就好啦。

7. 樂觀鎖和悲觀鎖, 讓你來寫你怎麼實現?

悲觀鎖:

悲觀鎖她專一且缺乏安全感了,她的心只屬於當前執行緒,每時每刻都擔心著它心愛的資料可能被別的執行緒修改。因此一個執行緒擁有(獲得)悲觀鎖後,其他任何執行緒都不能對資料進行修改啦,只能等待鎖被釋放才可以執行。

  • SQL語句select ...for update就是悲觀鎖的一種實現
  • 還有Java的synchronized關鍵字也是悲觀鎖的一種體現

樂觀鎖:

樂觀鎖的很樂觀,它認為資料的變動不會太頻繁,操作時一般都不會產生併發問題。因此,它不會上鎖,只是在更新資料時,再去判斷其他執行緒在這之前有沒有對資料進行過修改。實現方式:樂觀鎖一般會使用版本號機制或CAS演算法實現。

之前業務上使用過CAS解決併發問題,大家有興趣可以看一下哈:

8. Paxos 協議瞭解?工作流程是怎麼樣的?

8.1 為什麼需要Paxos演算法?

當前我們應用都是叢集部署的,要求所有機器狀態一致。假設當前有兩臺機器A和B,A要把狀態修改為a,B要把狀態修改為b,那麼應該聽誰的呢?這時候可以像2PC一樣,引入一個協調者,誰最先到就聽誰的。

這裡有個問題,就是協調者是單節點,如果它掛了呢。因為可以引入多個協調者

但是這麼多協調者,應該聽誰的呢?

引入Paxos演算法解決這個問題,Paxos演算法是一種基於訊息傳遞的分散式一致性演算法。

8.2 Paxos的角色

Paxos涉及三種角色,分別是Proposer、Accecptor 、Learners。

  • Proposer:它可以提出提案 (Proposal),提案資訊包括提案編號和提案值
  • Acceptor:接受接受(accept)提案。一旦接受提案,提案裡面的提案值(可以用V表示)就被選定了。
  • Learner: 哪個提案被選定了, Learner就學習這個被選擇的提案

一個程序可能是Proposer,也可能是Acceptor,也可能是Learner。

8.2 Paxos演算法推導過程

一致性演算法需要前置條件

  • 在這些被提出的提案中,只有一個會被選定
  • 如果沒有提案被提出,就不應該有被選定的提案

-當提案被選定後,learner可以學習被選中的提案

假設只有一個Acceptor,只要Acceptor接受它收到的第一個提案,就可以保證只有一個value會被選定。但是這個 Acceptor 宕機,會導致整個系統不可用。

如果是是多個Proposer和多個Acceptor,如何選定一個提案呢?

我們可以加個約定條件,假設就叫約束P1一個 Acceptor 必須接受它收到的第一個提案。但是這樣還是可能會有問題,如果每個Proposer分別提出不同的value(如下圖V1,V2,V3),發給了不同的Acceptor,最後會導致不同的value被選擇。

我們可以給多一個額外的約定P1a:一個提案被選定,需要被半數以上 Acceptor 接受。這跟P1有點矛盾啦,我們可以使用一個全域性的編號來標識每一個Acceptor批准的提案,當一個具有某value值的提案被半數以上的Acceptor批准後,我們就認為該value被選定了。即提案P= 提案引數 + 提案值,可以記為【M,V】。

現在可以允許多個提案被選定,但必須保證所有被選定的提案都具有相同的value值。要不然又會出現不一致啦。因此可以再加個約束P2:

如果提案 P[M1,V1] 被選定了,那麼所有比M1編號更高的被選定提案P,其 value 的值也必須是 V1。
複製程式碼

一個提案要被選定,至少要被一個 Acceptor 接受,因此我們可以把P2約束改成對Acceptor接受的約束P2a:

 如果提案 P[M1,V1] 被接受了,那麼所有比M1編號更高的,且被Acceptor接受的P,其值也是 V1。
複製程式碼

多提案被選擇的問題解決了,但是如果是網路不穩定或者宕機的原因,還是會有問題。

假設有 5 個 Acceptor。Proposer2 提出 [M1,V1]的提案,Acceptor25(半數以上)均接受了該提案,於是對於 Acceptor25 和 Proposer2 來講,它們都認為 V1 被選定。Acceptor1 剛剛從 宕機狀態 恢復過來(之前 Acceptor1 沒有收到過任何提案),此時 Proposer1 向 Acceptor1 傳送了 [M2,V2] 的提案 (V2≠V1且M2>M1)。對於 Acceptor1 來講,這是它收到的 第一個提案。根據 P1(一個 Acceptor 必須接受它收到的 第一個提案),Acceptor1 必須接受該提案。同時 Acceptor1 認為 V2 被選定。

這就出現了兩個問題:

  • Acceptor1 認為V2被選定,Acceptor2~5Proposer2認為V1被選定。出現了不一致。
  • V1被選定了,但是編號更高的被Acceptor1接受的提案[M2,V2]的 value 為 V2,且 V2≠V1。這就跟 P2a(如果提案 P[M1,V1] 被接受了,那麼所有比M1編號更高的,且被Acceptor接受的P,其值也是 V1。)矛盾了。

我們要對P2a約束強化一下得到約束P2b,

如果 P[M1,V1] 被選定後,任何Proposer 產生的 P,其值也是 V1。
複製程式碼

對於 P2b 中的描述,如何保證任何Proposer產生的P,其值也是V1 ?只要滿足 P2c 即可:

對於任意的M和V,如果提案[M,V]被提出,那麼肯定存在一個由半數以上的Acceptor組成的集合S,滿足以下兩個條件 中的任意一個:

  • 要麼S中每個Acceptor都沒有接受過編號小於M的提案。
  • 要麼S中所有Acceptor批准的所有編號小於Mn的提案中,編號最大的那個提案的value值為Vn

8.3 演算法流程

8.3.1. Proposer生成提案

  • Prepare請求
  • Accept請求

P2c約束基礎上,如何生成提案呢?

Proposer選擇一個新的提案編號N,向 Acceptor 集合 S(數目在半數以上)傳送請求,要求 S 中的每一個 Acceptor 做出如下響應:

  • 如果 Acceptor 沒有接受過提案,則向 Proposer 保證 不再接受編號小於N的提案。
  • 如果 Acceptor 接受過請求,則向 Proposer 返回 已經接受過的編號小於N的編號最大的提案。

我們將這個請求稱為編號為N的Prepare請求

  • 如果Proposer收到半數以上的Acceptor 響應,則生成編號為N,value 為 V 的提案 [N,V],V 為所有響應中編號最大的提案的value。
  • 如果 Proposer收到的響應中沒有提案,那麼 value 由 Proposer 自己生成,生成後將此提案發給 S,並期望Acceptor 能接受此提案。

我們稱這個請求為Accept請求

8.3.2 Acceptor接受提案

一個Acceptor可能會受到來自Proposer的兩種請求:Prepare請求和Accept請求。Acceptor 什麼時候可以響應一個請求呢,它也有個約束:P1b

一個Acceptor只要尚未響應過任何編號大於N的Prepare請求,那麼他就可以接受這個編號為N的提案。
複製程式碼

Acceptor收到編號為 N的Prepare 請求,如果在此之前它已經響應過編號大於N的Prepare請求。由約束P1b,該Acceptor不會接受這個編號為N的提案。因此,Acceptor會忽略這個請求。

一個 Acceptor 只需記住兩點:已接受的編號最大的提案和已響應的請求的最大編號。

8.3.3 Paxos演算法描述

階段一:

  • Proposer選擇一個提案編號N,然後向半數以上的Acceptor傳送編號為N的Prepare請求。
  • 如果一個Acceptor收到一個編號為N的Prepare請求,且N大於該Acceptor已經響應過的所有Prepare請求的編 號,那麼它就會將它已經接受過的編號最大的提案(如果有的話)作為響應反饋給Proposer,同時該Acceptor 承諾不再接受任何編號小於N的提案。

階段二:

  • 如果Proposer收到半數以上Acceptor對其發出的編號為N的Prepare請求的響應,那麼它就會發送一個針對 [N,V]提案的Accept請求給半數以上的Acceptor。注意:V就是收到的響應中編號最大的提案的value,如果響應 中不包含任何提案,那麼V就由Proposer自己決定。
  • 如果Acceptor收到一個針對編號為N的提案的Accept請求,只要該Acceptor沒有對編號大於N的Prepare請求 做出過響應,它就接受該提案。

8.3.4 Learner學習被選定的value

9. B+樹聊一下?B+樹是不是有序?B+樹和B-樹的主要區別?B+樹索引,一次查詢過程?

B+樹是有序的。

B+樹和B-樹的主要區別?

  • B-樹內部節點是儲存資料的;而B+樹內部節點是不儲存資料的,只作索引作用,它的葉子節點才儲存資料。
  • B+樹相鄰的葉子節點之間是通過連結串列指標連起來的,B-樹卻不是。
  • 查詢過程中,B-樹在找到具體的數值以後就結束,而B+樹則需要通過索引找到葉子結點中的資料才結束
  • B-樹中任何一個關鍵字出現且只出現在一個結點中,而B+樹可以出現多次。

假設有這麼一個SQL:

select * from Temployee where age=32;
複製程式碼

age加個一個索引,這條SQL是如何在索引上執行的?大家可以舉例子畫個示意圖哈,比如二級索引樹,

再畫出id主鍵索引,我們先畫出聚族索引結構圖,如下:

因此,這條 SQL 查詢語句執行大概流程就是醬紫:

  • 搜尋idx_age索引樹,將磁碟塊1載入到記憶體,由於32<37,搜尋左路分支,到磁碟定址磁碟塊2。
  • 將磁碟塊2載入到記憶體中,在記憶體繼續遍歷,找到age=32的記錄,取得id = 400.
  • 拿到id=400後,回到id主鍵索引樹。
  • 搜尋id主鍵索引樹,將磁碟塊1載入記憶體,在記憶體遍歷,找到了400,但是B+樹索引非葉子節點是不儲存資料的。索引會繼續搜尋400的右分支,到磁碟定址磁碟塊3.
  • 將磁碟塊3載入記憶體,在記憶體遍歷,找到id=400的記錄,拿到R4這一行的資料,好的,大功告成。

10. TCP 怎麼實現擁塞控制?

擁塞控制是作用於網路的,防止過多的資料包注入到網路中,避免出現網路負載過大的情況。它的目標主要是最大化利用網路上瓶頸鍊路的頻寬。

實際上,擁塞控制主要有這幾種常用演算法

  • 慢啟動
  • 擁塞避免
  • 擁塞發生
  • 快速恢復

慢啟動演算法

慢啟動演算法,表面意思就是,別急慢慢來。它表示TCP建立連線完成後,一開始不要傳送大量的資料,而是先探測一下網路的擁塞程度。由小到大逐漸增加擁塞視窗的大小,如果沒有出現丟包,每收到一個ACK,就將擁塞視窗cwnd大小就加1(單位是MSS)每輪次傳送視窗增加一倍,呈指數增長,如果出現丟包,擁塞視窗就減半,進入擁塞避免階段。

  • TCP連線完成,初始化cwnd = 1,表明可以傳一個MSS單位大小的資料。
  • 每當收到一個ACK,cwnd就加一;
  • 每當過了一個RTT,cwnd就增加一倍; 呈指數讓升

為了防止cwnd增長過大引起網路擁塞,還需設定一個慢啟動閥值ssthresh(slow start threshold)狀態變數。當cwnd到達該閥值後,就好像水管被關小了水龍頭一樣,減少擁塞狀態。即當cwnd >ssthresh時,進入了擁塞避免演算法。

擁塞避免演算法

一般來說,慢啟動閥值ssthresh是65535位元組,cwnd到達慢啟動閥值

  • 每收到一個ACK時,cwnd = cwnd + 1/cwnd
  • 當每過一個RTT時,cwnd = cwnd + 1

顯然這是一個線性上升的演算法,避免過快導致網路擁塞問題。

擁塞發生

當網路擁塞發生丟包時,會有兩種情況:

  • RTO超時重傳
  • 快速重傳

如果是發生了RTO超時重傳,就會使用擁塞發生演算法

  • 慢啟動閥值sshthresh = cwnd /2
  • cwnd 重置為 1
  • 進入新的慢啟動過程

這真的是辛辛苦苦幾十年,一朝回到解放前。其實還有更好的處理方式,就是快速重傳。傳送方收到3個連續重複的ACK時,就會快速地重傳,不必等待RTO超時再重傳。

慢啟動閥值ssthresh 和 cwnd 變化如下:

  • 擁塞視窗大小 cwnd = cwnd/2
  • 慢啟動閥值 ssthresh = cwnd
  • 進入快速恢復演算法

快速恢復

快速重傳和快速恢復演算法一般同時使用。快速恢復演算法認為,還有3個重複ACK收到,說明網路也沒那麼糟糕,所以沒有必要像RTO超時那麼強烈。

正如前面所說,進入快速恢復之前,cwnd 和 sshthresh已被更新:

- cwnd = cwnd /2
- sshthresh = cwnd
複製程式碼

然後,真正的快速演算法如下:

  • cwnd = sshthresh + 3
  • 重傳重複的那幾個ACK(即丟失的那幾個資料包)
  • 如果再收到重複的 ACK,那麼 cwnd = cwnd +1
  • 如果收到新資料的 ACK 後, cwnd = sshthresh。因為收到新資料的 ACK,表明恢復過程已經結束,可以再次進入了擁塞避免的演算法了。

11. JVM調優

11.1 一般什麼時候考慮JVM調優呢?

  • Heap記憶體(老年代)持續上漲達到設定的最大記憶體值;
  • Full GC 次數頻繁;
  • GC 停頓時間過長(超過1秒);
  • 應用出現OutOfMemory 等記憶體異常;
  • 應用中有使用本地快取且佔用大量記憶體空間;
  • 系統吞吐量與響應效能不高或下降。

11.2 JVM調優的目標

  • 延遲:GC低停頓和GC低頻率;
  • 低記憶體佔用;
  • 高吞吐量;

11.3 JVM調優量化目標

  • Heap 記憶體使用率 <= 70%;
  • Old generation記憶體使用率<= 70%;
  • avgpause <= 1秒;
  • Full gc 次數0 或 avg pause interval >= 24小時 ;

11.4 JVM調優的步驟

  • 分析GC日誌及dump檔案,判斷是否需要優化,確定瓶頸問題點;
  • 確定JVM調優量化目標;
  • 確定JVM調優引數(根據歷史JVM引數來調整);
  • 依次調優記憶體、延遲、吞吐量等指標;
  • 對比觀察調優前後的差異;
  • 不斷的分析和調整,直到找到合適的JVM引數配置;
  • 找到最合適的引數,將這些引數應用到所有伺服器,並進行後續跟蹤。

11.5 常見的JVM引數

堆疊配置相關

-Xmx3550m -Xms3550m -Xmn2g -Xss128k 
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
複製程式碼
  • -Xmx3550m: 最大堆大小為3550m。
  • -Xms3550m: 設定初始堆大小為3550m。
  • -Xmn2g: 設定年輕代大小為2g。
  • -Xss128k: 每個執行緒的堆疊大小為128k。
  • -XX:MaxPermSize: 設定持久代大小為16m
  • -XX:NewRatio=4: 設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。
  • -XX:SurvivorRatio=4: 設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區佔整個年輕代的1/6
  • -XX:MaxTenuringThreshold=0: 設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代。

垃圾收集器相關

-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC 
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:
-XX:+UseConcMarkSweepGC
複製程式碼
  • -XX:+UseParallelGC: 選擇垃圾收集器為並行收集器。
  • -XX:ParallelGCThreads=20: 配置並行收集器的執行緒數
  • -XX:+UseConcMarkSweepGC: 設定年老代為併發收集。
  • -XX:CMSFullGCsBeforeCompaction:由於併發收集器不對記憶體空間進行壓縮、整理,所以執行一段時間以後會產生“碎片”,使得執行效率降低。此值設定執行多少次GC以後對記憶體空間進行壓縮、整理。
  • -XX:+UseCMSCompactAtFullCollection: 開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片
  • -XX:+UseConcMarkSweepGC 使用CMS垃圾收集器

輔助資訊

-XX:+PrintGC
-XX:+PrintGCDetails
複製程式碼

11.6 常用調優策略

  • 選擇合適的垃圾回收器
  • 調整記憶體大小(垃圾收集頻率非常頻繁,如果是記憶體太小,可適當調整記憶體大小)
  • 調整記憶體區域大小比率(某一個區域的GC頻繁,其他都正常。)
  • 調整物件升老年代的年齡(老年代頻繁GC,每次回收的物件很多。)
  • 調整大物件的標準(老年代頻繁GC,每次回收的物件很多,而且單個物件的體積都比較大。)
  • 調整GC的觸發時機(CMS,G1 經常 Full GC,程式卡頓嚴重。)
  • 調整 JVM本地記憶體大小(GC的次數、時間和回收的物件都正常,堆記憶體空間充足,但是報OOM)

12. 資料庫分庫分表的缺點是啥?

  1. 事務問題,已經不可以用本地事務了,需要用分散式事務。
  2. 跨節點Join的問題:解決這一問題可以分兩次查詢實現
  3. 跨節點的count,order by,group by以及聚合函式問題:分別在各個節點上得到結果後在應用程式端進行合併。
  4. ID問題:資料庫被切分後,不能再依賴資料庫自身的主鍵生成機制啦,最簡單可以考慮UUID
  5. 跨分片的排序分頁問題(後臺加大pagesize處理?)

13. 分散式事務如何解決?TCC 瞭解?

分散式事務:

就是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。簡單來說,分散式事務指的就是分散式系統中的事務,它的存在就是為了保證不同資料庫節點的資料一致性。

聊到分散式事務,需要知道這兩個基本理論哈。

  • CAP 理論
  • BASE 理論

CAP 理論

  • 一致性(C:Consistency):一致性是指資料在多個副本之間能否保持一致的特性。例如一個數據在某個分割槽節點更新之後,在其他分割槽節點讀出來的資料也是更新之後的資料。
  • 可用性(A:Availability):可用性是指系統提供的服務必須一直處於可用的狀態,對於使用者的每一個操作請求總是能夠在有限的時間內返回結果。這裡的重點是"有限時間內"和"返回結果"。
  • 分割槽容錯性(P:Partition tolerance):分散式系統在遇到任何網路分割槽故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務。

BASE 理論

它是對CAP中AP的一個擴充套件,對於我們的業務系統,我們考慮犧牲一致性來換取系統的可用性和分割槽容錯性。BASE是Basically Available,Soft state,和 Eventually consistent三個短語的縮寫。

  • Basically Available(基本可用):通過支援區域性故障而不是系統全域性故障來實現的。如將使用者分割槽在 5 個數據庫伺服器上,一個使用者資料庫的故障隻影響這臺特定主機那 20% 的使用者,其他使用者不受影響。
  • Soft State(軟狀態):狀態可以有一段時間不同步
  • Eventually Consistent(最終一致):最終資料是一致的就可以了,而不是時時保持強一致。

分散式事務的幾種解決方案:

  • 2PC(二階段提交)方案、3PC
  • TCC(Try、Confirm、Cancel)
  • 本地訊息表
  • 最大努力通知
  • seata事務

TCC(補償機制)

TCC 採用了補償機制,其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。TCC(Try-Confirm-Cancel)包括三段流程:

  • try階段:嘗試去執行,完成所有業務的一致性檢查,預留必須的業務資源。
  • Confirm階段:該階段對業務進行確認提交,不做任何檢查,因為try階段已經檢查過了,預設Confirm階段是不會出錯的。
  • Cancel 階段:若業務執行失敗,則進入該階段,它會釋放try階段佔用的所有業務資源,並回滾Confirm階段執行的所有操作。

下面再拿使用者下單購買禮物作為例子來模擬TCC實現分散式事務的過程:

假設使用者A餘額為100金幣,擁有的禮物為5朵。A花了10個金幣,下訂單,購買10朵玫瑰。餘額、訂單、禮物都在不同資料庫。

TCC的Try階段:

  • 生成一條訂單記錄,訂單狀態為待確認。
  • 將使用者A的賬戶金幣中餘額更新為90,凍結金幣為10(預留業務資源)
  • 將使用者的禮物數量為5,預增加數量為10。
  • Try成功之後,便進入Confirm階段
  • Try過程發生任何異常,均進入Cancel階段

TCC的Confirm階段:

  • 訂單狀態更新為已支付
  • 更新使用者餘額為90,可凍結為0
  • 使用者禮物數量更新為15,預增加為0
  • Confirm過程發生任何異常,均進入Cancel階段
  • Confirm過程執行成功,則該事務結束

TCC的Cancel階段:

  • 修改訂單狀態為已取消
  • 更新使用者餘額回100
  • 更新使用者禮物數量為5
  • TCC的優點是可以自定義資料庫操作的粒度,降低了鎖衝突,可以提升效能
  • TCC的缺點是應用侵入性強,需要根據網路、系統故障等不同失敗原因實現不同的回滾策略,實現難度大,一般藉助TCC開源框架,ByteTCC,TCC-transaction,Himly。

大家有興趣可以看下我之前這篇文章哈:

後端程式設計師必備:分散式事務基礎篇

14, RocketMQ 如何保證訊息的準確性和安全性?

我個人理解的話,這道題換湯不換藥,就是為如何保證RocketMQ 不丟訊息,保證不重複消費,訊息有序性,訊息堆積的處理。

訊息不丟失的話,即從生產者、儲存端、消費端去考慮

大家可以看下我之前這篇文章哈:

訊息佇列經典十連問

15. 三個數求和

給你一個包含 n 個整數的陣列 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有和為 0 且不重複的三元組。

注意:答案中不可以包含重複的三元組

例項1:

輸入:nums = [-1,0,1,2,-1,-4]
輸出:[[-1,-1,2],[-1,0,1]]
複製程式碼

例項2:

輸入:nums = [0]
輸出:[]
複製程式碼

思路:

這道題可以先給陣列排序,接著用左右雙指標。

完整程式碼如下:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {

        List<List<Integer>> result = new LinkedList<>();
        if(nums==null||nums.length<3){ //為空或者元素個數小於3,直接返回
            return result;

        }

        Arrays.sort(nums); //排序

        for(int i=0;i<nums.length-2;i++){ //遍歷到倒數第三個,因為是三個數總和
            if(nums[i]>0){ //大於0可以直接跳出迴圈了
                break;
            }

            if(i>0&&nums[i]==nums[i-1]){ //過濾重複
                continue;
            }

            int left = i+1;  //左指標
            int right = nums.length-1; //右指標
            int target = - nums[i];  //目標總和,是第i個的取反,也就是a+b+c=0,則b+c=-a即可

            while(left<right){
                if(nums[left]+ nums[right]==target){ //b+c=-a,滿足a+b+c=0
                   result.add(Arrays.asList(nums[i],nums[left],nums[right]));
                   left++;  //左指標右移
                   right--;  //右指標左移
                   while(left<right&&nums[left]==nums[left-1]) left++; //繼續左邊過濾重複
                   while(left<right&&nums[right]==nums[right+1]) right--; //繼續右邊過濾重複
                }else if(nums[left]+ nums[right]<target){
                   left++; //小於目標值,需要右移,因為排好序是從小到大的
                }else{
                  right--;  
                }

            }
        }
            return result;
        }
}

作者:撿田螺的小男孩
連結:https://juejin.cn/post/7075132492168036388
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。