java后端必會【基礎知識點】

(一)java集合類(done)

  1. 在java集合類中最常用的是Collection和Map的接口實現類。Collection又分為List和Set兩類接口,List的實現類有ArrayList、LinkedList、Vector、Stack,Set接口的實現類有HashSet、TreeSet,而Map的實現類主要有HashMap、ConcurrentHashMap、TreeMap。
  2. ArrayList是容量可以改變的非線程安全集合(可以使用Collections.synchronizedList方法實現線程安全),內部使用數組進行存儲。ArrayList支持對元素的快速隨機訪問,但是插入和刪除時速度通常會很慢,因為這個過程需要移動其他元素。ArrayList的默認大小為10,在不傳入指定的列表大小時,默認使用空列表。ArrayList在每次添加數據時都會檢查空間是否足夠,若不足就會按原數組空間的1.5倍擴容(向下取整),最后會將新的數組空間大小和插入數據后需要的空間大小作比較,取兩者之前的更大者。之后就會將舊數組整體拷貝進新的數組空間。
  3. HashMap的默認容量值為16,默認負載因子為0.75,默認閾值為12。HashMap的容量不會在new的時候就分配,而是在put的時候才進行分配。HashMap的實際容量為比傳入容量參數大的2的冪,而ConcurrentHashMap實際容量為比(傳入容量參數除4/3后)大的2的冪,此后每次擴容都是增加2倍。HashMap的容量為2的冪的主要是因為HashMap在計算槽位的時候使用的算法是(n-1)& hash,當n為2的冪時元素可以更加均勻的散列。HashTable是HashMap的線程安全版本,兩者主要區別在于HashTable在一些關鍵的函數中增加了synchronized關鍵字,由于性能不佳目前已經被ConcurrentHashMap淘汰了。HashMap是線程不安全的,在java8以前甚至可能出現由于多線程resize導致的循環鏈表,線程不安全的兩個小例子:兩個線程同時對同一桶位插入數據可能導致某個線程的數據被覆蓋、某個線程在執行resize時另一線程可能讀不到數據。
  4. HashMap的key和value都可以為null,key為null的元素會被散列到數組的第一個元素,但在使用stream的Collectors.toMap方法時HashMap的value不可以為null,且key不可以重復,否則會拋出異常。另外不同于HashMap,ConcurrentHashMap的key和value都不能為null。
  5. java8對HashMap的改進?
    主要有兩大改動
    (1)在數組桶的鏈表長度超過8時,HashMap通過紅黑樹替換鏈表。
    (2)在HashMap擴容時不再重新對每個節點計算桶位,而是通過(節點的hash值&舊數組容量 == 0)來形成兩條新的鏈表,分別插在原桶位和舊數組容量+原桶位。
  6. 樹的基礎知識:
    高度:從某節點出發到葉子節點的簡單路徑上邊的數量被稱為該節點的“高度”
    重要的二叉樹:平衡二叉樹、二叉查找樹、紅黑樹
    平衡二叉樹:樹及所有子樹的左右高度差不超過1
    二叉查找樹:對于樹的任意節點而言它的左子樹的所有節點值都小于它,而他的右子樹的所有節點值都大于它。它主要有前序遍歷、中序遍歷、后序遍歷三種遍歷方式。隨著數據不停的增加或刪除,二叉查找樹容易失衡
    紅黑樹:紅黑樹是一種可以自平衡的二叉查找樹。紅黑樹在每個節點上增加了顏色屬性,可以為紅色或黑色,紅黑樹通過按規則著色和特定的旋轉來保持自身的平衡,它新增、刪除、查找的最壞時間復雜度均為O(logN)。相對于其他的自平衡樹例如AVL樹,紅黑樹并不嚴格保證時左右子樹的高度差超過1,這使得紅黑樹在刪除時能夠更快的恢復平衡,成本比較低,所以面對頻繁的插入刪除時,紅黑樹更適合,而面對低頻修改,大量查詢時AVL樹更合適
  7. Comparable接口和Comparator接口的區別?
    如果要使用Comparable接口,就必須實現該接口并重寫compareTo方法。而Comparator接口可以在類外實現,并可以將其實現對象傳入到Collections.sort或Arrays.sort方法中以實現排序。Comparator接口的使用體現了基于開閉原則的設計。
  8. 集合中的hashCode和equals?
    hashCode和equals用來標識對象,兩者協同工作來判斷對象是否相等,當hashCode的值相同時,還需要調用equals進行一次值比較。任何時候在覆寫equals時一定要同時覆寫hashCode,因為Map、Set等集合都是同時使用兩者來判斷對象是否相同的。hashCode是根據對象地址進行相關計算得到的int類型數值。在做對象間的比較時,盡量使用Objects.equals方法來避免空指針。
  9. 集合的fail-fast機制?
    fail-fast是一種在集合遍歷時的錯誤檢測機制,如果在遍歷的過程中出現了意料之外的修改就會拋出ConcurrentModificationException異常,這種機制經常出現在多線程環境下。線程會維護一一個modCount用來記錄集合已經修改的次數,在遍歷集合時會時時檢查modCount的數值是否發生變化,若發生變化則拋出異常。
  10. ConcurrentHashMap知識:
    在java8以前ConcurrentHashMap通過鎖分段的思想將整個hashMap分為16個segment,每個segment負責一部分數組元素,并通過reentrantLock鎖來保證每個segment的數據安全。而在java8后ConcurrentHashMap取消了segment,大量的使用了volatile、cas等技術進一步減少了鎖競爭造成的性能影響。java8后ConcurrentHashMap有三點主要的改動:(1)取消分段鎖機制,進一步降低沖突概率、(2)引入紅黑樹、(3)使用了更加優秀的方式統計元素的數量。
    get操作邏輯:計算出key的hash值并計算出槽位,然后通過getObjectVolatile獲取數該槽位的元素,并比較hash值和key值,若相同則返回,如果槽位內節點的hash值小于0則說明正在進行擴容,則通過ForwardingNode的find函數去新的數組nextTable中進行查找。否則就遍歷單鏈表查找相應節點。
    put操作邏輯:首先檢查核心的Node<K,V>[] table是否已經初始化,如果沒有初始化,則利用CAS將sizeCtl的值置為-1進行初始化。查詢key相應的槽位是否為 null,若為null直接通過CAS將鍵值對放入槽位。如果相應的槽位已經有節點,并且其hash值為-1,則表示正在進行擴容,則當前線程幫忙進行擴容。否則通過synchronized鎖住槽位內的節點即鏈表的頭結點,然后遍歷鏈表,尋找是否有hash值及key值相同的節點,若有則將value設置進去,否者創建新的節點加入鏈表。通過addCount函數更新ConcurrentHashMap鍵值對的數量,并檢查是否需要進行擴容。
    擴容操作邏輯:首先新建一個兩倍長度的數組nextTable。初始化ForwardingNode節點,其中保存了新數組nextTable的引用,在處理完每個槽位節點后當做占位節點,表示該槽位已經處理過了。通過倒序的方式為線程分配需要處理數組元素個數(默認每個線程16個元素)。每個線程處理自己負責的數組元素,具體邏輯和HashMap基本相應,處理完的元素的位置上會放入ForwardingNode節點。
    計數方法:代碼里的變量baseCount用于在無競爭環境下記錄元素的個數,每當插入元素或刪除元素時都會利用CAS更新鍵值對個數。當有線程競爭時,會使用CounterCell數組來計數,每個ConuterCell都是一個獨立的計數單元。線程可以通過ThreadLocalRandom.getProbe() & m找到屬于它的CounterCell進行計數。這種方法能夠降低線程的競爭,相比所有線程對一個共享變量不停進行CAS操作性能上要好很多。這里的CounterCell數組初始容量為2,最大容量是機器的CPU數。

(二)java并發包(done)

  1. 同步框架AQS介紹?
    AQS是構建鎖和其他同步組件基礎框架,內部維護一個整形字段state來表示同步狀態,同時維護了一個雙向鏈表(FIFO的CLH同步隊列),鏈表的每個節點都表示一個線程,包含了線程引用、節點狀態、前驅節點引用、后驅節點引用。整數state在不同的同步組件中可以表示不同的狀態,例如:reentrantLock可以用它來表示鎖的重入次數、semaphore用它來表示剩余的許可數量,futureTask用來表示任務的狀態(尚未開始、正在運行、已完成、已取消)。AQS是一個抽象模板類,內部實現了大量的構建同步器通用方法,通過繼承實現AQS的相應接口就可以構建出很多同步組件,例如:reentrantLock、semaphore、futureTask等組件都通過了繼承AQS的內部類來實現。除此之外AQS還實現了Condition接口,通過ConditionObject實現了條件隊列(單向鏈表)。Condition條件隊列需要配合ReentrantLock一起使用,當線程獲取到ReentrantLock鎖后,可以通過await進入條件隊列并釋放鎖,或者通過singinal方法喚醒條件隊列等待時間最久的線程并加入到等待隊列中,條件隊列使用和同步隊列相同的節點類型。
  2. reetrantLock的實現方式?
    ReetrantLock提供了nonfairSync和fairSyn兩個實例,分別用于實現非公平鎖和公平鎖,兩者都繼承于AQS。ReetrantLock主要提供了lock和unlock兩個方法。
    (1)非公平鎖的lock方式實現:(1)首先直接通過CAS嘗試將state從0設置為1,若成功則說明獲取到了鎖,保存當前線程引用以實現可重入。(2)否則獲取state若值為0,則再次嘗試將state設置為1,成功則同樣保存當前線程。(3)否則檢查獲取鎖的是否就是當前線程,如果是就增加state值,記錄重入次數。(4)如果都不滿足,則將當前線程封裝成Node放入同步隊列中,并將線程掛起。公平鎖的不同之處在于:沒有(1)這一步,另外在執行(2)時需檢測同步隊列是否有等待中的線程,若沒有才能執行。
    (2)非公平鎖和公平鎖的unlock方法完全相同: 獲取state值,通過cas減去釋放鎖定的個數,如果值為0,則釋放鎖,并將同步隊列中的第一個節點內的線程喚醒(LockSupport的unpark方法)。
  3. reetrantLock和synchronize的差別?
    reetrantLock和synchronize都是可重入的獨占鎖,且在加鎖和內存上兩者語義完全相同。reetrantLock還提供了一些高級的功能:可定時/可輪詢/可中斷的鎖獲取操作、公平隊列、非塊狀結構的鎖等,同時reetrantLock更加靈活且性能稍稍優于synchronize。但是reetrantLock的操作更加復雜,加鎖和釋放都需要手動操作,存在可能忘記釋放鎖的風險。reetrantLock的死鎖檢測更加復雜,在調試reetrantLock導致的死鎖問題時會更加復雜。最后由于synchronize是jvm的內置屬性字段,未來會隨著版本持續優化,而reetrantLock的優化的可能性不大。所以在synchronize不能滿足我們的高級需求時可以使用reetrantLock,否則最好還是使用synchronize。
  4. countDownLatch的實現方式?
    CountDownLatch主要用來保障某批線程的執行需要等待另一批線程執行完成后方可執行,例如老板巡視工人完成工作情況,需要等待三個工人全部完成好工作以后才能巡視。首先會獲取要求同時開始某個動作的線程數量,將數量值設置給state。當state的值大于0時,調用CountDownLatch的await方法,相應線程會被掛起并放入AQS的同步隊列中,直到調用countdown()函數將相應的state的值減為0,被掛起的線程才會被重新喚醒。和它比較相似的是CyclicBarrier,CyclicBarrier會確保所有線程都執行到某個點后,才可以繼續往下執行。
  5. futureTask的實現方式?
    FutureTask利用了AQS的state來表示不同狀態:主要有未開始、進行中、已完成。FutureTask繼承了Runnable接口,對線程實際需要執行的邏輯進行了一層包裝,當執行了線程實際邏輯獲取到結果后,會更新state的值來標識任務已完成并將結果保存起來,同時會喚醒等待結果的線程。其他需要結果的線程通過get接口獲取結果,如果任務已經執行完就直接獲取相應結果,如果任務尚未執行完就掛起等待,知道執行任務的線程將其喚醒。
  6. volatile的實現方式?
    volatile具備兩個關鍵的特性:一個是保證變量對所有線程的可見性,另一個是禁止指令重排序(包括cpu層面的指令亂序)。volatile在抽象邏輯層面上通過“內存柵欄”實現,而實際字節碼層面通過lock指令實現:這個命令會將變量數據立即刷到主內存中,并利用cpu總線嗅探機制使其他線程高速緩存內的cacheline失效(cacheline是cpu高速緩存cache的基本讀寫單位),使用時必須重新到主內存Memory讀取。同時因為需要立刻刷數據到內存中,那么volatile變量操作前的所有操作都需要完全執行完成,這樣進而也保證了volatile變量寫操作前后不會出現重排序。通常volatile變量的讀寫效率和普通變量沒有多大差別,但在volatile變量并發訪問沖突非常頻繁的情況下可能造成性能的下降,具體的例子及解決方案可以百度“偽共享”問題。
  7. synchronize的實現方式?
    synchronized底層實現依賴于jvm用C++實現的管程(ObjectMonitor),管程是一種類似于信號量的程序結構,它封裝了同步操作并對進程隱蔽了同步細節,其整體實現邏輯和ReentrantLock很相似。我們在使用synchronized時通常有兩種方式:1.修飾方法、2.修飾代碼塊,其實兩者差別不大,本質上都是同步代碼塊。在虛擬機層面上,當用synchronized修飾方法時,class文件中會在方法表中為相應方法增加ACC_SYNCHRONIZED訪問標志,用以標識該方法為同步方法。而當用synchronized修飾代碼塊時,會在相應代碼段字節碼的前后分別插入monitorenter和monitorexit字節碼指令,用以表示該段代碼需要同步。
    在代碼即將進入同步代碼塊的時候,如果此時同步對象還沒有被鎖定,虛擬機先會在棧幀中建立一個名為鎖記錄的空間(Lock Record)用于存儲對象目前的mark word拷貝數據。然后虛擬機會把對象的mark word數據拷貝到鎖記錄空間中,并嘗試通過CAS操作將對象的mark word數據更新為指向鎖記錄空間的引用,如果成功了就說明獲取到了鎖。如果出現兩個以上的線程爭用同一個鎖時,那么輕量級鎖就會膨脹為重量級鎖,后面等待鎖的線程都會進入阻塞狀態。
    在使用synchronized時,我們可以把synchronized修飾的方法或代碼段想象成一段不可以并發訪問的臨界區資源,這種資源必須獨占使用。而如何實現獨占訪問呢?我們可以想象每個對象都有把獨占鎖,我們需要借助某個對象的獨占鎖來訪問這種臨界區資源,而同一個鎖某個時刻只能被一個線程所獲取,其他線程都得等待鎖的釋放。用synchronized修飾的實例方法(public synchronized void method())默認使用當前對象(this)的鎖,而用synchronized修飾的靜態方法(public synchronized static void method())默認使用當前對象對應的Class對象鎖,他們分別對應于synchronized修飾代碼塊中的synchronized(object)和synchronized(Object.class)。Class對象存在于方法區中,具有全局唯一性,在一個jvm實例中一個Class對象只有一把鎖,所有使用該Class對象作為鎖的靜態方法或代碼塊,執行前都必須先獲得該Class對象鎖。而同一個Class可以有很多實例對象,每個實例對象都有一個自己的鎖,使用實例對象A鎖的線程和使用實例對象B鎖的線程間不存在競爭關系。
  8. 線程池的實現原理?
    ThreadPoolExecutor是線程池的主要實現類,它有以下幾個構造參數:
    (1)corePoolSize:常駐核心線程數,如果其值為0,則任務執行完成后,沒有任何請求時會銷毀線程池的線程。如果值大于0,沒有任務時核心線程也不會被銷毀。
    (2)maximumPoolSize:線程池能夠容納的最大并行線程數,如果與corePoolSize相等,即為固定大小線程池。
    (3)keepAliveTime:線程池中線程的空閑時間,當線程數量大于corePoolSize時才會起作用,當線程的空閑時間到達keepAliveTime時,線程會被銷毀直到只剩下corePoolSize個線程為止。
    (4)timeUnit:表示空閑時間單位。
    (5)workQueue:需要使用的緩存隊列,當核心線程無法處理新來的任務時,就會將任務放入到緩存隊列workQueue中,當緩存隊列存滿后,如果還有需要處理,那么線程池就會繼續創建新的線程,直到線程數量達到maximumPoolSize。
    (6)threadFactory:用來生產線程的工廠。
    (7)rejectedExecutionHandler:用來執行拒絕策略的對象。當緩存隊列存滿后,且線程數量已達到了maximumPoolSize時,就會執行拒絕策略。
    線程池在創建后,當線程數量小于corePoolSize時,每來一個任務后就會創建一個線程來執行該任務,直到線程數達到corePoolSize。之后若新來的任務沒有核心線程能夠處理,就會將任務存入阻塞隊列workQueue中,直到阻塞隊列workQueue存滿。若阻塞隊列workQueue存滿后,仍有大量的任務無法處理,就會繼續增加線程處理任務,直到線程數量達到maximumPoolSize。若此時仍有無法處理的任務,就會執行任務的拒絕策略。ThreadPoolExecutor提供了4個公開的內部靜態類:
    (1)AbortPolicy(默認策略):丟棄任務并拋出RejectedExecutionException異常。
    (2)DiscardPolicy:丟棄任務且不拋出異常。
    (3)DiscardOldestPolicy:拋棄等待最久的任務,然后把當前任務加入隊列中。
    (4)CallerRunsPolicy:主線程直接執行任務。
    在spring中對ThreadPoolExecutor進一步進行了封裝,提供了ThreadPoolTaskExecutor,幫忙我們提供了默認的timeUnit、workQueue、threadFactory、rejectedExecutionHandler實現對象。同時也可以讓我們自主設置:阻塞隊列大小、線程名稱前綴、線程池關閉等待任務執行完成時間等等。除了ThreadPoolExecutor外,java還提供了線程池靜態工廠類Executors,該類提供了幾種默認的工廠實現,但實際開發中不建議使用:
    (1)Executors.newCachedThreadPool:使用的是無界線程池,線程最大數量可達Integer.MAX_VALUE(2的31次冪減1),具有高度的伸縮性,keepAliveTime的默認值為60s,一旦線程空閑時間超過keepAliveTime,線程就會被回收,當長時間沒有任何任務時,線程池的線程數為0,新的任務來時會建立新的線程。該線程使用的阻塞隊列是SynchronousQueue,不會緩存任何任務,每次任務過來后,都需要線程池內的線程連接處理,
    (2)Executors.newSingleThreadExecutor:使用的是無界隊列,線程池內只有一個線程,相當于單線程串行執行所有任務,保證任務能夠按照提交順序依次執行。
    (3)Executors.newFixedThreadPool:同樣使用的是無界隊列,輸入的參數即為固定線程數,不存在空閑線程。
    線程池不應該通過Executors創建,而應該通過ThreadPoolExecutor創建,這樣能夠更加明確線程池的運行規則,規避資源耗盡的風險。以上線程池都不推薦使用,因為這些隊列要么使用的是無界隊列,要么使用的就是無界線程池,都可能會將服務器資源耗盡導致OOM。
  9. 阻塞隊列LinkedBlockingQueue的實現原理?
    LinkedBlockingQueue主要使用單向鏈表實現,并且使用了兩個reetrantLock,用來分別生成notEmpty條件隊列和notFull條件隊列,這兩個條件隊列分別用來控制存操作和取操作。之所以使用兩個reetrantLock同步隊列,主要是因為使用單個reetrantLock則同一時刻只能進行讀或者進行寫。
  10. ThreadLocal的實現原理及可能存在的問題?
    ThreadLocal的實現并不算復雜,首先每個線程Thread對象都維護了一個ThreadLocalMap,這個Map是由ThreadLocal類實現的一個使用線性探測的自定義Map,Map的key是ThreadLocal對象的引用,而value就是我們需要存儲的本地線程變量。值得注意的是ThreadLocalMap并沒有使用拉鏈法,而是使用了線性探測法,這可能主要是因為ThreadLocalMap存儲的數據量一般不會很大。另外還有一點非常重要:ThreadLocalMap的key被封裝成了弱引用。當ThreadLocal對象threadLocalA沒有其他強引用時,在下次GC來臨時threadLocalA就會被回收,同時ThreadLocalMap相應槽位的key值會變為null,ThreadLocalMap在每次進行get/set操作時都會主動的去清空key為null的鍵值對。ThreadLocal的這種設計主要是為了防止出現內存泄露。假如key為強引用,那么當threadLocalA使用完后,ThreadLocalMap仍持有threadLocalA的強引用,將會導致threadLocalA無法回收。所以當我們使用ThreadLocal時一般都會定義一個static的threadLocal,并且在使用完相應的數據后,需要手動的執行ThreadLocal的remove移除相應的數據,防止出現內存泄露。
    ThreadLocal主要存在兩點可能的問題:(1)臟數據問題:因為線程池內的線程采取復用策略,如果線程執行上個任務時,沒能顯示的通過remove清理線程相關的ThreadLocal數據,那么在下個任務中便可能讀到上個任務設置的ThreadLocal數據。(2)內存泄露:一般使用ThreadLocal時都會使用static,此時在使用完ThreadLocal后,若忘記通過remove清理數據,就可能導致內存泄露。
  11. 對線程安全的理解,什么樣的類是線程安全的?
    我們通常會將能夠安全的被多個線程使用的對象稱為線程安全對象,這個對象就是線程安全的。能夠保證線程安全有以下幾種情況:
    (1)僅在單線程內可見的數據是線程安全的,比如使用ThreadLocal存儲的數據。
    (2)無狀態對象總是線程安全的,即對象不包含任何屬性以及對其他對象屬性的引用,有的僅僅是純代碼方法。
    (3)不可變的只讀對象是線程安全的,這種對象只在構建時會被初始化,之后不允許進行任何修改和變更,通常會通過final修飾類或屬性,例如String、Integer等等。
    (4)java提供的線程安全類,比如concurrentHashMap等。
    除了上面的這幾種情況外,其他的并發情況就需要我們自己通過鎖或者合適的同步工具保證數據安全。
  12. 線程的同步方式?
    計算機的線程同步主要指的是:線程之間按照某種機制協調先后執行順序,當有某個線程對內存進行操作時,其他線程都不可以對這塊內存地址進行操作,直到該線程完成操作。實現線程同步的方式比較多,包括volatile、synchronized鎖、reetrantLock鎖、阻塞隊列、及AQS實現的各種線程同步類等等。
  13. Thread的join()函數實現原理是怎樣的?
    Thread threadObject = new Thread(new Runnable() {});threadObject.join();join方法會阻塞當前執行的主線程,而不是threadObject線程,直到threadObject線程執行完畢后才會喚醒當前執行的主線程。實現原理也比較簡單,主要就是調用join()時將當前線程阻塞,當threadObject執行結束后,將所有因為自己阻塞的線程喚醒。
  14. Thread.sleep、Object.wait、LockSupport.park區別?
    三者都可以使線程阻塞掛起,Thread.sleep需要指定掛起時長,且不會釋放持有的鎖,相應的線程狀態為TIMED_WAITING。Object.wait會讓線程掛起但會釋放掉持有的鎖,且需要通過notify或notifyAll方法喚醒后才能繼續運行,另外必須保證wait和notify的執行順序,否則會出現問題,其線程狀態為WAITING。LockSupport.park不會釋放持有的鎖,可以通過LockSupport.unpark喚醒,由于采用的是類似二元信號量的實現方式,所以unpark方法可以比park方法先執行,不會丟失喚醒信號,其線程狀態同樣為WAITING。
  15. 同步/異步&阻塞/非阻塞?
    阻塞還是非阻塞,取決于線程所做的操作是否需要將線程掛起等待。同步還是非同步,取決于是否是當前線程親自執行操作,若當前線程親自執行操作則為同步,當前線程通過創建或利用其他線程執行操作則為異步。同步/異步&阻塞/非阻塞
  16. 高并發情況下pv、uv的統計?
    pv:使用redis的incr原子命令進行統計,然后為key設置相應的過期時間即可。
    uv:在uv量不大的情況下,可以使用redis的set類型數據,設置相應的過期時間,將用戶id存入set集合中,通過scard就可獲取某個時間點的uv數據。在uv量很大且數據并不要求十分精準時,可以使用hyperLogLog。
  17. 高并發情況下的限流?
    在高并發下保護系統的三大利器:緩存、降級、限流。緩存可以增加系統訪問速度和增大系統處理容量,降級是當服務器壓力劇增時,根據業務情況和流量對一些服務和頁面降級,以保證核心業務功能的正常運行。限流的目的主要是通過對并發請求進行限速和限量來保護系統,請求速率或請求量一旦達到閾值就可以排隊、等待或者拒絕服務。
    常用的限流方法:有計數器方式、令牌桶和和漏桶。計數器法比較簡單粗暴,主要用來限制并發請求數量,一旦并發請求數量達到閾值,就可以直接拒絕請求,可以通過semaphore來實現。令牌桶除了能夠限制請求的平均速率,還能夠允許一定程度的突發流量。漏桶算法能夠使請求速率均勻,不會接受突發流量。
  18. 死鎖的檢查與排查?
    死鎖主要分為兩種情況:(1)鎖順序死鎖、(2)資源死鎖。鎖順序死鎖:主要是因為線程之間分別持有對方需要的鎖,但互相不釋放自己持有的鎖資源,最終就導致了死鎖,誰也無法繼續往下推進。在mysql數據庫中存在同樣的情況,但是mysql會通過在表示等待關系的有向圖中搜索是否有環,若存在環則說明發生了死鎖,mysql會選擇代價比較低的事務進行回滾,這樣就能保證另一個事務可以正常的執行。而在java中一旦發生死鎖,這些線程就永遠不能在使用了,可能造成系統性能降低,甚至造成應用程序完全停止。當線程需要獲取多個鎖時,如果所有線程都以固定的順序獲取鎖,那么程序中就不會出現鎖順序死鎖問題。資源死鎖:當我們在使用線程池或信號量來限制對資源的使用時,這些限制也可能會導致資源死鎖。比如線程的“饑餓死鎖”,如果線程池中只有有限個線程,并且線程執行的任務的完成依賴于其生成的子任務,子任務同樣在該線程池中運行,那么當子任務無法獲取線程執行,一直存儲在阻塞隊列中,就造成了線程的“饑餓死鎖”。
    java死鎖的排查比較簡單:首先通過jps或ps -ef | grep java命令獲取java進程的pid,然后通過jstack命令查看當前虛擬機內所有線程的快照,jstack會展示所有線程的狀態信息,同時也會幫我們統計展示存在死鎖的線程信息,包括死鎖線程持有的鎖及需要的鎖,及相關鎖的地址,如下圖所示。在這里插入圖片描述

(三)jvm(done)

  1. java類加載過程?
    類的加載流程主要分為:加載、驗證、準備、解析、初始化、使用、卸載七個流程。(1)加載:通過ClassLoader去加載Class文件到jvm內存中,Class文件的來源并沒有任何限制,既可以是我們的程序通過javac編譯器生成的,也可以來源于網絡等其他途徑。(2)驗證:這個過程主要就是對Class文件中的字節流進行檢查,看看是否符合規范,是否存在安全問題等等。(3)準備:這個過程主要是為類變量(static修飾的變量)分配內存,此時的初始值指的是通常情況下的零值,例如private static int a = 3,類變量a在這個階段會被賦值為0。唯一例外的類變量是使用final修飾的類變量,使用final修飾的類變量會在這個階段就賦給程序中設定的值。(4)解析:這個階段主要將Class文件中對常量池的引用轉為直接內存引用,舉個簡單的例子:我們常會在代碼中使用import xxx.xxx.xxx.class,這里面的xxx.xxx.xxx.class就是符號引用,解析階段就是把這種符號引用轉換為對xxx.class具體內存地址的引用。(5)這個階段主要負責初始化類對象(Class對象),會按照在文件中出現的順序為所有類變量賦值,同時執行靜態代碼塊。千萬注意這里初始化的是類對象而不是實例對象!(6)使用、(7)卸載:這兩個階段就是對象使用和類型卸載的過程。
    上面就是整個類加載的流程,有一點需要強調的是,這個流程講的主要是類的加載流程,它面向的是類對象(Class對象),初始化的也是類對象,并不是我們通常意義上的實例對象。當我們需要構建實例對象時,可以通過new Object()實例對象,jvm首先會檢查相應的類型對象Class是否存在,若不存在則會通過上面的類加載機制生成對應的Class對象。之后會在堆內存中為對象分配內存空間,并為所有實例屬性賦零值。在之后會設置對象頭信息,包括Mark Word(對象hashCode、對象GC年齡等)和對應Class類對象的引用。最后會按照文件中出現順序為實例變量賦值,同時執行代碼塊,最后執行構造方法完成對象的構建。
  2. 類加載器介紹
    java類加載器是在java虛擬機外部實現的,可以讓用戶程序自己決定如何獲取所需的類。任意一個類,它的唯一性都由加載它的類加載器和這個類本身一起確定,兩個類即使名稱完全相同,但若由同一個類加載器加載的,那么它們也是兩個不同的類。java程序主要由三層類加載器加載,按優先級它們分別為:(1)啟動類加載器Bootstrap ClassLoader:該類加載器由C++實現,負責裝載最核心的java類。(2)擴展類加載器Extension ClassLoader:用以加載一些擴展系統類,通過java實現。(3)應用程序類加載器Application ClassLoader:加載用戶自定義的類,同樣是通過java實現。當一個類加載器收到類加載的請求時,首先會將請求委派給父類加載器,直到父類無法加載,自己才會加載,這種類加載的方式被稱為“雙親委派”,但加載器間并非繼承關系,而是以組合的方式來復用父加載器的功能。雙親委派的加載模式主要是為了保證系統加載類的安全性,如果沒有雙親委派,假如我們自定義一個Object對象,然后通過自定義的加載器加載,那么在系統中就存在多個Obect,導致系統的混亂。在日常的使用中也經常會破壞雙親委派的情況,比如jdbc的代碼是由啟動類加載器加載的,但是由于數據庫的驅動通常都由不同的廠商提供,啟動類加載器無法加載,這時需要通過線程中的ContextClassLoader加載器加載相關的驅動類,這就破壞了雙親委派。除此之外,osgi等熱部署技術也打破了雙親委派的模式,在osgi中不同模塊都有不同的類加載器,在進行替換時會將組間和類加載器一同替換掉。
  3. jvm的內存布局?
    jvm的內存主體可以分為堆和棧兩大塊,每個線程都會分配相應的棧空間,主要包括虛擬機棧和本地方法棧,分別用來執行java方法和本地方法,同時每個線程還會為程序計數器分配相應的空間,這些空間都是線程私有的。在虛擬機棧中主要存儲局部變量表、方法棧,方法返回地址等等。而堆主要分為新生代、老年代、永久代(方法區),新生代與老年代占用空間大小默認比例為1:2,可以通過-XX:Newratio設置。其中新生代主要用來存儲新建對象,包括一個eden空間和兩個survivor空間,默認按占用8:1:1的大小分配。而老年區根據對象經歷的GC次數,將一些比較老的對象升級進入老年代,默認經歷GC次數為15次。永久代(方法區)主要用來存儲類型信息、靜態變量、常量等,在java8后元空間取代了永久代(方法區),并且元空間在本地內存中分配。之所以放棄永久代(方法區),是因為永久代之前由jvm分配固定的空間大小,經常會出現OOM問題,同時FGC時經常需要移動永久代內的數據,同時為了和其他類型的虛擬機統一規范,就廢棄了永久代,改為使用元空間,但是元空間內存溢出問題非常難以排查。
  4. GC類型有哪些?
    一.部分收集(Partial GC):不是完整收集整個堆的垃圾回收,其包括:
    (1)新生代收集(Minor GC/Young GC):指僅對新生代進行垃圾收集
    (2)老年帶收集(Major GC/Old GC):指僅對老年代進行垃圾收集
    (3)混合收集(Mixed GC):收集整個新生代和部分老年代
    二. 整堆收集(Full GC):對整個java堆和方法區進行的垃圾收集(java8后垃圾收集器實際上并不能直接回收metaspace,而是通過回收堆中的Class對象和String常量池間接回收metaspace,另外如果不指定metaspace的大小,默認情況下元空間最大是系統內存的大小)
  5. 有哪些垃圾回收算法?
    目前垃圾回收算法主要有三種:標記清除算法、標記復制算法、標記整理算法。
    (1)標記清除算法:
    標記清除法首先會標記存活的對象,之后會統一回收所有未被標記的對象。它存在兩個問題:1.當需要回收的對象比較多時,回收效率會比較低,所以不適合回收新生代、2.存在內存碎片化的問題。
    (2)標記復制算法:
    標記復制算法是應用于新生代收集的主要回收算法,jvm會把新生代分為一塊比較大的Eden空間和兩塊比較小的survivor空間,每次只使用eden空間和其中一塊survivor空間,在進行回收時將存活的對象復制到剩余的一塊survivor空間中,然后將使用過的Eden空間和survivor空間清理掉。一般eden空間和survivor空間的內存占用比例為8:1,當剩余的survivor空間不足以存儲所有存活的對象時,就會啟動分配擔保機制,多余的對象會直接進入老年代。
    (3)標記整理算法:
    標記整理算法首先會標記存活的對象,之后將所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。標記整理算法由于需要移動對象,所以一般使用于老年代,同時對象移動操作必須暫停所有用戶應用程序,所以會增大垃圾回收造成的延遲,但是卻能夠提高系統的吞吐量,關注吞吐量的Parallel Old收集使用的是標記整理算法,而關注延遲的CMS使用的是標記清除算法。
  6. 常用的GC收集器有哪些?
    GC收集器主要分為兩大類:新生代收集器和老年代收集器,其中新生代收集器主要有:serial收集器、parnew收集器、parallel scavenge收集器,而老年帶收集器主要有:serial old收集器、parallel old收集器、cms收集器。其中parnew經常配合cms收集器收集器一起使用,適用于要求低系統停頓的應用。而parnew收集器配合parallel old收集器一起使用,適用于要求高吞吐量的應用。serial old收集器為單線程的老年代收集器,在CMS和G1收集器內存空間不足時,會降級為使用serial old收集器,暫停所有用戶線程,單線程執行垃圾回收。最后就是新生代/老年代一體回收的大名鼎鼎的G1收集器。
    在這里插入圖片描述
  7. G1收集器執行流程?
    G1收集器開創了面向局部收集的設計思路和基于region的內存布局。G1收集器將jvm堆分為了很多大小相等的獨立區域region,每個region都可能是Eden、Survivor或者老年代空間,每塊region的大小默認等于(最大堆空間 + 最小堆空間)/ 2 / 2048,最小空間大小為1M,同時還提供了特殊的humongous region用于存儲大對象。G1收集器會記錄每個region的回收價值,并且維護一個優先級列表,每次根據用戶設定的停頓時間,優先回收收益大的region區域。其具體運作流程可分為以下四步:
    (1)初始標記:
    垃圾回收主要針對的是堆和方法區,首先會標記所有GC Roots關聯的對象(GC Roots主要包括常量、類靜態屬性、棧幀中的局部變量表),在查找對象引用時會使用OopMap數據結構,該結構會記錄對象引用的位置。但由于用戶線程的執行可能實時變更對象引用關系,所以在初始標記階段需要短暫的停頓用戶線程,為了保證用戶線程能夠生成正確的OopMap數據,jvm設置了線程執行的安全點和安全區域,線程在執行到安全點(方法調用、循環跳轉、異常跳轉)或安全區域時,會主動掛起等待初始化標記的完成。在開始初始化標記時還會劃出一塊內存區域用于垃圾收集期間新對象的內存分配。
    (2)并發標記:
    從GC Roots開始對堆中數據進行可達性分析,這個階段可以與用戶線程并發執行。由于用戶線程可能并發的修改對象引用,可能造成兩種情況,一種是某些需要回收的對象被漏掉,這種對象被稱為“浮動垃圾”,我們可以在下次垃圾回收時再次進行回收,另一種情況是某些仍然存活的對象可能會被標識為垃圾,并發標記階段會記錄對象引用分變更,以便于在第三階段重新標識這部分對象,防止錯誤的回收不該回收的的對象。
    (3)最終標記:
    進行一個短暫的暫停,來處理并發階段引用變更導致的對象標記錯誤。
    (4)篩選回收:
    更新region的統計數據,對各個region的回收價值和成本進行排序,根據用戶指定的停頓時間制定執行計劃。將需要回收的region區域內數據復制到空的region中,并清理掉舊的region空間,由于這一步需要移動對象,所以必須要暫停用戶線程。
  8. G1收集器的優缺點?
    優點: 1.采用了面向region的回收思想,可以指定GC收集的最大停頓時間、2.不會產生內存碎片,能夠提供規整的可用空間。
    缺點: 1.比其他收集器占用更大的內存空間(每個region設置一個RSet),消耗了更多的cpu資源。
    所以一般內存空間小于8g的應用一般都會使用parNew + CMS,而大于8g的應用一般都會使用G1,隨著計算機技術的發展,目前大部分應用的內都很大了,所以parNew + CMS的收集器組合已經逐漸被廢棄了。
  9. 常用的JVM參數?
    -Xms:設置最小堆內存空間
    -Xmx:設置最大堆內存空間
    -Xss:設置虛擬機棧內存空間
    -XX:NewRatio:設置新生代和老年代占用空間比例
    -XX:+UseG1GC:設置使用G1收集器
    -XX:MaxTenuringThreshold:設置進入老年代需要經歷的GC次數
    -XX:+DisableExplicitGC:設置禁止使用System.gc()
    -XX:+PrintGCDetails:打印GC日志詳情
    -XX:+HeapDumpOnOutOfMemoryError:出現OOM時保存dump文件
  10. 內存泄露與內存溢出問題?
    在java中內存泄露主要指的是:本該被回收的內存空間沒能被釋放,大量的內存泄露會導致最終內存溢出,內存溢出指的內存空間不足,無法繼續存儲數據了。內存溢出主要有四種情況:
    (1)java堆溢出
    堆溢出是我們最常見的內存溢出,在內存溢出時通常會發生OOM異常,同時會生產相應的dump文件。我們可以通過MAT分析dump文件,查找占用內存空間比較大的對象,看該對象是夠存在內存泄露問題,若不存在,可以考慮是否能夠增加堆內存空間大小。
    (2)虛擬機棧溢出
    我們知道每個線程都會分配特定大小的虛擬機棧空間,如果棧幀中的局部變量表占用內存比較多,或者棧的深度非常大,那我們分配的棧空間很可能不夠用,這時就會出現StackOverFlowError。出現這種異常時,基本都會有明確的錯誤堆棧信息可供分析,比較容易定位到特定的問題。
    (3)方法區溢出
    方法區溢出主要有兩種:一種是常量池導致的溢出,比如程序使用了String.intern()方法產生了大量的字符串常量,就可能導致方法區的內存溢出,不過在java8后String的常量池被移到了java堆中,可能會導致java堆的溢出。另外一種比較常見的就是大量類型信息導致的方法區溢出,比如在程序中通過cglib等技術動態的生成了大量的類,就有可能導致方法區內存溢出,在java8后代表類型的Class對象同樣在堆中存儲,當出現元空間不足時,便會通過GC收集器進行類型卸載,間接回收元空間。
    (4)本機直接內存溢出/堆外內存溢出
    直接內存并不是虛擬機運行時數據區的一部分,不歸jvm虛擬機管。java在使用NIO時會通過本地方法直接分配堆外內存,然后通過DirectByteBuffer對象作為這塊內存的引用進行操作,這樣可以避免java堆和native堆間的數據復制。雖然直接內存不受java堆大小的限制,但是會受到本機總內存大小的限制,同樣可能導致OOM。直接內存導致的溢出,在dump文件中看不存什么異常,是最難處理的內存溢出問題。

(四)mysql(done)

  1. B樹和B+樹的區別?
    B樹全名平衡多路查找樹,B樹是能夠自平衡的多路查找,樹的非葉子節點也會存儲記錄數據。B+樹的非葉子節點只存儲相應的鍵值,而不存儲實際記錄數據,同時B+樹中所有葉子節點都會通過雙向鏈表連接。在mysql的實際使用中,mysql使用B+樹的層高一般都是2-4層,這就保證了磁盤和內存不會有過多的IO。B+樹每次能夠讀取的最小單位是頁(頁是innodb磁盤管理的最小單位),它的每個非葉子節點都是一頁,頁的默認大小為16KB。B+樹在增刪改數據時,為了保證樹的平衡是需要進行類似二叉樹的旋轉的,同時也可能需要進行拆分頁的操作,所以當B+內存儲的數據很多的時候,每次的自平衡過程都需要移動很多數據,所以操作B+樹的時間更久,這也是當mysql數據量大的時候增刪改的效率降低的原因。另外還有一點值得說,我們mysql每次插入的主鍵基本都是自增的,這樣的插入性能會好些,如果生成的主鍵不是遞增的就會導致大量的拆分頁和樹的旋轉操作,這將對mysql數據庫的性能帶來很大的影響。在mysql中主鍵索引和非主鍵索引都是由B+樹實現的,輔助索引的葉子節點存儲的是主鍵id,而主鍵索引的葉子節點存儲的是數據記錄,數據記錄按照主鍵順序邏輯有序的依次存儲,所以主鍵索引也被稱為“聚簇索引”,而輔助索引被稱為“非聚簇索引”。
  2. 事務的ACID特性?
    事務是數據庫區別于文件系統的一大特點之一,Innodb存儲引擎的事務完全符合ACID的特性。ACID分別代表著:原子性、一致性、隔離性、持久性,事務是訪問并更新數據庫的基本程序執行單元。原子性:數據庫事務是不可分割的單元,要么事務中的所有操作都執行成功,要么都執行失敗。一致性:指事務能夠使數據庫從一種一致性狀態轉化為另一種一致性狀態。隔離性:某個事務在提交前,其他事務對該事務修改的內容都不可見。持久性:事務一旦提交,結果就能永久保存,除非發生的是硬件故障,持久性能夠保障系統的高可靠性。事務的原子性和持久性通過redo日志實現,一致性通過undo日志實現,而隔離性通過數據庫鎖實現。
  3. mySQL事務的不同隔離級別及可能出現的問題?
    數據庫事務一般有4種隔離級別:read uncommitted、read committed、repeatable read、serializable。
    (1)read uncommitted是最低的隔離級別,存在臟讀的問題,即會讀到尚未提交的數據,同時也存在不可重復讀和幻讀問題。
    (2)read committed是很多數據庫的默認隔離級別,這一級別解決了臟讀問題,但是同樣存在著不可重復讀和幻讀問題。
    (3)repeatable read是mysql的默認級別,普通的數據庫在這一級別中解決了不可重復讀的問題,但是仍無法解決幻讀問題,不過mysql通過next key lock解決了幻讀問題,所以mysql在repeatable read級別就能提供滿足ACID要求的隔離性。mysql數據repeatable read隔離級別下支持一致性非鎖定讀MVCC。
    (4)serializable是最嚴格的隔離級別,在這一級別下所有的事務操作都必須依次順序進行,同時將無法使用一致性非鎖定讀MVCC,所有的select語句都會自動加上lock in share mode共享鎖。
    不可重復讀和幻讀的區別重點在于針對不同的操作類型:不可重復讀防止update和delete導致的數據錯誤,而幻讀防止insert導致的數據錯誤。例如select count(*) from table where sex = "man"語句在事務A中,開始時能夠讀取的數據為10,此時事務B執行了delete操作將sex = "man"的記錄刪除了一條,那么事務A再次執行相同語句時,數據就會發生變化,這就是不可重復讀問題。而當事務C進行insert操作時同樣可能造成事務A讀取的數據發生變化,這種情況導致的問題叫做幻讀問題。
  4. mysql事務的實現原理?
    mysql在repeatable read隔離級別下就可以完全滿足事務的ACID原理,事務主要有幾種類型:扁平事務、帶保存點的扁平事務、嵌套事務、分布式事務。事務的隔離性由鎖來實現,事務的原子性和持久性由redo日志來實現,而事務的一致性由undo日志來保證。
    (1)redo日志:redo日志主要由重做日志緩存(redo log buffer)和重做日志文件(redo log file)兩部分組成,innodb存儲引擎會保證事務在提交時,事務的所有增刪改操作日志能夠寫入到重做日志文件中持久化,這種機制被稱為Force Log at Commit。redo日志會隨著事務的執行,順序的將數據頁的物理修改寫入到做重做日志緩存中,并且在事務提交前通過fsync操作,將緩存數據寫入到文件中。因為fsync操作的效率取決于磁盤性能,所以一般我們會在mysql中進行相應的配置,事務提交時并不會立刻執行fsync操作,而是由master thread定時的去執行fsync操作以提升數據庫性能,當然這樣就可能存在當數據庫宕機部分數據未能刷入磁盤文件而導致的數據丟失。redo日志和我們熟知的binlog日志是不同的,binlo日志是由mysql服務器層生成的,所有的mysql存儲引擎都能夠生成binlog日志,而redo日志是innodb引擎獨有的,并且兩者保存的內容也并不相同,redo日志保存是的事務對數據頁的物理修改,而binlog日志保存的是事務執行的增刪改操作mysql日志。
    (2)undo日志:在mysql中專門有個特定的segment用來存儲undo日志,undo日志用于將數據庫恢復到執行事務之前的狀態,mysql的一致性非鎖定讀也是通過undo日志實現的。每條mysql記錄都會包含一個隱式的deleted標識位,用來標識記錄是否被刪除,同時記錄還有兩個系統列:DATA_TRX_ID:用于標識記錄當前的事務版本,DATA_ROLL_PTR:用來指向當前記錄項的undo日志鏈表。每次有事務操作記錄時都會更新記錄的版本,同時舊版本的記錄數據就會被存入到undo日志中,同時放入DATA_ROLL_PTR指向的undo日志鏈表中。undo日志中除了保存舊版本記錄的數據外,還會保存事務對記錄進行的邏輯操作。在開啟MVCC后通過select讀取數據時,innodb會為當前事務創建一個read view用以記錄當前事務能夠使用的記錄版本,假如我們需要讀取的記錄正在被其他事務操作,那么我們便可以通過read view去undo日志鏈表中找到我們可以使用的版本數據,這樣就不會阻塞數據的讀操作。在repeatable read隔離級別下,不論是否有新的事務提交,總是讀取開始時的版本數據,而在read committed隔離級別下,一旦有新的事務提交就能都讀取事務提交的數據。
  5. mysql中多列索引的優點?
    在mysql中單列索引和多列索引的主要區別就是,多列索引同時使用多個字段作為索引的鍵值。在mysql中當使用的字段占用空間不是很大時,其實多列索引并不會造成索引性能的明顯下降。同時多列索引保存了更多的列數據,能夠適應更多的查詢場景,可以使用覆蓋索引等所以優化技術。除此之外多列索引的字段存在一定的有序性,合理的使用可以避免排序操作。
  6. mysql中count函數的實現原理?
    在mysql中count(*)和count(1)的實現邏輯是完全相同的,當select語句的where條件能夠確定特定輔助索引時會通過輔佐索引統計記錄數。若沒有任何where條件,則innodb引擎會選擇一個占用空間比較小的輔助索引統計記錄條數。
  7. explain中常用字段信息?
    在explain信息中比較常用的字段有:
    (1)key:用于表示本次查詢使用的索引
    (2)rows:預估本次查詢需要掃描的行數
    (3)type:本次查詢的類型,主要的類型如下圖所示
    (4)extra:展示mysql查詢的額外信息
    mysql中type的所有類型mysql中extra可能出現信息
  8. mysql使用的一些優化建議?
    (1)盡量避免使用join操作。因為使用join操作會增加鎖競爭的風險,同時會給日后的數據庫拆分帶來不便,另外性能不一定比拆分成多條語句高。
    (2)需要謹慎的考慮在varchar字段上建立索引,適當的時候可以考慮使用前綴索引。
    (3)合理的使用 order by,避免出現using filesort。
    (4)利用覆蓋索引來進行查詢操作,避免回表。
    (5)利用延遲關聯優化超多分頁場景。
  9. mysql中limit的優化方案?
    在mysql中我們經常會遇到使用limit m,n進行分頁的場景,當limit m,n中m越來越大時,分頁查詢的性能就會越來越差。一般我們會有兩種方式解決這個問題,第一種是每次在查詢時都將上次查詢時的最后一條記錄的主鍵id傳入,然后利用主鍵id在去查下一批數據,但這種方式不是很好使用,因為它的限制比較多,面對需要跳轉到某個特定頁面的場景根本無法處理。其實還存在一種寫法即在這里插入圖片描述mysql實例在實際使用中常按功能被劃分為三層:(1)客戶端層:負責處理連接、安全認證等等(2)服務器層:包含了mysql很多核心服務,包括sql語句的解析優化、執行計劃的制定執行、存儲過程的實現、觸發器的實現、所有內置函數的實現、binlog的生產等等所有跨存儲引擎的服務都在本層實現(3)儲引擎層:負責mysql中數據的存儲和提取。
    limit m,n之所以性能比較差,是因為limit m,n的實現是由服務器層完成,存儲引擎收到的命令只是獲取滿足where條件的m條記錄,這一點和mysql5.6之前的“索引截斷”問題是一樣的,在idx_a_b_c的索引中若查詢語句為where a = xxx and b > xxx and c = xxx,則服務器層在經過分析后只會把條件where a = xxx and b > xxx傳遞給存儲引擎層,最后在服務器層根據c = xxx進行條件過濾,這樣就會導致數據庫讀取了很多無用的數據,這個問題在mysql5.6后引入ICP技術得到了解決,但是limit m,n還是存在著相同的問題,每次都需要多讀很多數據,隨著m值的越來越大,語句的查詢性能就越來越差。
    上述語句之所以能夠大幅度減少語句的執行時間,是因為子語句select id from table where XXXX limit m,n使用了覆蓋索引,只會使用輔助索引查詢m條主鍵id,然后從m條主鍵索引中取出n條主鍵id,最后實際只有n條記錄會回表查詢,這樣就大大減少了數據庫的IO操作,也就減少了語句的執行時間。
  10. mysql的加鎖機制?
    在mysql中表鎖主要是在服務器層實現的,而innodb在存儲引擎層實現了行級鎖,innodb會在每個數據頁用特定數據結構管理該頁上的所有鎖,并通過bit數組標識每條記錄的鎖定狀態。mysql的行級鎖主要有幾種:(1)LOCK_REC_NOT_GAP:純正意義上的行鎖,只鎖住特定的行記錄(2)LOCK_GAP:間隙鎖,鎖兩個記錄之間的 GAP(3)LOCK_ORDINARY:即next-key lock,是間隙鎖和行鎖的結合體,既鎖住記錄也鎖住行間的間隙(4)LOCK_INSERT_INTENSION:插入意向gap鎖,一種特殊的gap lock,只會鎖住要插入記錄的位置。它們之前的兼容矩陣如下圖:
    在這里插入圖片描述
    上面的兼容性指的是不同事務間的兼容性,實際在同一事務中很多鎖是可以同時持有的,并不會出現同一事務內部的兼容問題。另外值得注意的是間隙鎖之間是兼容的,插入意向鎖之間也都是兼容的。接下來介紹在read repeatable隔離級別下增刪改的加鎖流程:
    (1)insert加鎖流程
    事務首先會嘗試獲取插入意向鎖(Insert Intension Locks),若已有其他事務在同樣的位置加了GAP鎖或Next-Key鎖,則當前事務加鎖失敗進入等待。否則會進行進行唯一性約束檢查,若不存在相同鍵值記錄則進行插入操作,如果存在相同的鍵值,事務會檢測相應的記錄是否已被標記為刪除或是否有鎖,若記錄已被標記為刪除或者被其他事務加了鎖,那么當前事務會加record行鎖(S共享鎖)等待(其他事務可能在執行更新或刪除操作,等待其他事務結束),否則就會拋出duplicate key異常。最后在相應記錄加X鎖后插入記錄,直到事務提交后才會釋放持有的所有鎖。
    mysql在官方文檔到提到過insert這套加鎖流程可能導致的死鎖,舉個例子:事務A、事務B、事務C同時插入某一有相同唯一索引鍵值的記錄,當事務A在插入期間,事務B和事務C獲取到插入意向鎖后,發現事務A持有排他鎖,兩者就會自動獲取S共享鎖(實為record行鎖)進入等待,之后事務A發生了回滾釋放了排它鎖,事務B和事務C都想要升級為排他鎖進行插入,但是由于雙方本身都持有一個共享鎖,導致兩者誰也無法獲取記錄的排他鎖,這就產生了死鎖。
    在mysql5.7中,當我們使用insert on duplicate key update時,在并發插入數據并且存在特定列使用了唯一索引時,即使插入的不是相同記錄,也可能出現死鎖問題,這主要是因為insert on duplicate key update相對于正常insert操作前需要先獲取間隙鎖,據說是為了保障repeatable read隔離級別下的select的數據一致性,舉個例子:三個事務A、事務B、事務C并發的在同一間隙內插入不同的數據,事務A首先獲取了該間隙并加上了間隙鎖,之后獲取插入意圖鎖,然后進行插入操作。這時事務B、事務C同時到來并且都獲取了該間隙鎖,然后嘗試獲取插入意圖鎖,由于事務A的間隙鎖存在,所以兩者阻塞等待,在事務A執行完成后,后兩者同時嘗試獲取插入意向鎖,但是因為兩者都有該間隙鎖,所以兩者互相不兼容導致死鎖。不過這個問題在5.7以上的版本進行了修復,另外就是沒必要非要使用insert on duplicate key update,我們完全可以在業務層實現相同的功能,在數據庫層不應該有過多的復雜操作邏輯。
    (2)delete加鎖流程
    當where條件使用的是唯一索引時:
    1.未找到滿足條件的記錄時:在下一條記錄前加間隙鎖
    2.在找到滿足條件的記錄且記錄有效時:對記錄加行鎖,不用加間隙鎖
    3.在找到滿足條件的記錄但記錄無效時:對記錄加next key鎖(鎖住已經標記為刪除狀態的記錄)
    當where條件使用的是非唯一索引時:
    僅2不同,在where條件使用的是非唯一索引時,2中情況下會直接加next key鎖
    相應的死鎖問題參見:一個最不可思議的MySQL死鎖分析
    (3)update加鎖流程
    在mysql中的update操作,都是一條一條進行的,先對一條滿足條件的記錄加鎖,返回給服務器層,做相應的操作,然后繼續處理下一條滿足條件的記錄,直到所有的記錄都處理完畢,最后會統一釋放掉所有鎖。
    在實際加鎖時,會首先在輔助索引上加鎖,之后會對輔助索引指向的主鍵索引記錄加鎖。在Read Committed隔離級別下只會對滿足條件的輔助索引鍵值和主鍵索引加鎖,而在Repeatable Read隔離級別下在非唯一索引加鎖時還會加間隙鎖。當where語句走不到索引時,會在輔助索引上把所有記錄都加上排他行鎖,在Repeatable Read級別下還會加入間隙鎖,所以可想而知,這種情況會導致mysql服務的性能急劇下降,雖然mysql針對這種情況作了優化,但這仍是非常影響數據庫性能的問題,所以必須避免出現走不到索引的情況。
  11. mysql動態數據源實現方式?
    在Dao層通過注解標注每條語句需要使用的數據庫主庫或從庫,然后利用spring提供的AbstractRoutingDataSource數據源類動態的進行數據庫切換。在項目啟動初期AbstractRoutingDataSource就會將所有的數據庫配置項加載到targetDataSources屬性中。用戶程序通過determineCurrentLookupKey決定切換數據源的方式。
  12. mysql分庫分表實現方式?
    分庫分表通常有兩種方案:垂直切分、水平切分。
    (1)垂直切分
    垂直切分可以分為垂直拆庫垂直拆表。1.垂直拆庫就是根據業務場景將某個庫中的部分表遷到其他庫中,以減少數據庫的請求量,垂直拆庫的實現比較簡單,分成多個庫后能夠將請求流量分攤到多個庫上,這樣單個庫的請求量減少了,因為數據庫的連接數是固定的,所以能夠在某種程度上減輕數據庫壓力。2.垂直拆表就是根據業務場景將一張表拆成多張小表,可以將這些表放在同一個數據庫中,也可以放在其他的庫中,這種方式因為減少表中每行記錄的長度,能夠減少數據庫查詢IO次數進而提升數據庫性能。這兩種拆分方式都能夠一定程度上緩解數據庫的壓力,但是并不能根本上解決數據量不斷增加帶來的數據庫查詢壓力。
    (2)水平切分
    水平切分又分為庫內分表和分庫分表。水平切分會將一張大表按某種維度拆分成多張數據結構完全相同的小表,拆分后的小表如果仍放在原來的庫中則稱為庫內分表,放在其他庫中則稱為分庫分表。庫內分表由于所有的表仍然使用同一數據庫的固定連接數,所以仍會給數據庫帶來很大的壓力,所以通常水平切分最常使用的方式是分庫分表,將拆分的表分別存放在不同的庫中。水平切分能夠有效的解決數據庫數據量過大的問題,提升數據庫系統的穩定性和負載能力,但是卻需要比較大的業務改動。
    分庫分表帶來的問題:
    1.單機的ACID被打破,查詢同樣的數據在進行數據庫拆分后,需要到多個庫中分別查詢,在某些場景下需要使用分布式事務。
    2.join操作會更加復雜,后端開發中應該盡量避免使用join操作。
    3.單機情況下的主鍵自增邏輯無法再使用,需要生成全局唯一主鍵。
    4.查詢中的分頁排序操作實現起來將會更加復雜。
  13. mysql分庫分表后如何解決主鍵id問題?
    在分庫分表后可以使用幾種方案生成全局唯一的主鍵id:UUID方案、TDDL方案、Snowflake方案(雪花算法)。
    (1)UUID方法:經由一定的算法機器生成UUID,為了保證UUID的唯一性,規范定義了包括網卡MAC地址、時間戳、名字空間(Namespace)、隨機或偽隨機數、時序等元素,以及從這些元素生成UUID的算法。UUID的復雜特性在保證了其唯一性的同時,意味著只能由計算機生成。由于UUID生成的主鍵是字符串占用空間很大,并且具有很大的隨機性,生成的主鍵不能全局遞增,非遞增的主鍵會導致mysql大量的頁裂和記錄移動,所以性能非常差,所以該方案基本無法使用。
    (2)TDDL方案:TDDL是阿里采用的全局主鍵生成方案,通過數據庫存儲主鍵id信息,應用服務器每次申請一批主鍵id在內存中,使用完后在申請下一批。該方案的缺點是強依賴數據庫,當數據庫異常時整個系統不可用,但主鍵id生成的速度比較快且效率比較高。
    (3)Snowflake方案(雪花算法):snowflake算法的特性是有序、唯一,并且要求高性能,低延遲(每臺機器每秒至少生成10k條數據,并且響應時間在2ms以內),適合在分布式環境(多集群,跨機房)下使用,snowflake算法由多段組成,占用空間并不多,共占8個字節64位。雪花算法的開頭部分是時間戳(單位毫秒),第二部分是使用的機器id,第三部分是序列號,用于記錄1ms內生成的不同id。雪花算法不依賴數據庫及其他三方系統,并且生成主鍵id的速度和性能都非常好,但是強依賴于機器時鐘,如果機器時鐘發生回撥就會出現重復的主鍵id。一般公司在使用該方案時都會對雪花算法進行一定的優化以解決時鐘回撥導致的重復主鍵問題。
    在這里插入圖片描述
  14. 數據庫樂觀鎖實現方案?
    樂觀鎖總是假設不會產生數據沖突,在執行具體操作時才會進行沖突檢測。mysql數據庫的樂觀鎖實現方案和java并發中CAS的思想十分相似,都是在進行數據更新時先檢測待更新數據是否發生變化,由于可能存在ABA問題,所以一般還會使用版本號version,mysql樂觀鎖的使用一定要合理的設計索引,保證更新數據時走到索引。除了這種方式外還可以通過在where語句中增加一定的條件限制來實現樂觀鎖,這種方式適用于商品搶購的場景,舉個例子如下:
    在這里插入圖片描述

(五)redis(done)

  1. 與memcache有什么不同?
    redis和memcache都是基于內存的數據存儲系統。兩者有以下區別:1.memcached僅支持key-value數據結構,而redis支持多種類型的數據結構:String、Hash、List、Set、Sorted Set。2.memcached所有數據都存儲在內存中間,而redis會根據一定的策略將數據保存到磁盤中。3.memcached在客戶端通過使用一致性hash等分布式算法來實現分布式存儲,而redis更偏向于在服務器端構建分布式存儲。
  2. redis有哪些數據類型及每種類型的實現方式?
    redis共有5種基礎數據結構,分別為:string(字符串)、hash(字典)、list(列表)、set(集合)、zset(有序集合)。
    (1)string類型實現:
    redis中的字符串是可修改的字符串,在內存中以字節數組的形式存在,這點和java是不同的,java采用的是char數組。redis使用的字符串數據結構被稱為"SDS",即Simple Dynamic Strong,它是一個帶長度信息的字節數組。其原理和java的StringBuilder類似,兩者都是內部維護了一個數組,在進行append時,先判斷數組長度是否足夠,如果足夠則直接存入該數組,否則新建一個空間更大的數組,然后將數據存入。
    (2)hash類型實現:
    hash類型在redis中經常會被稱為“字典”,其主要通過hahstable實現,其結構和java中的hashmap幾乎完全一樣。因為在redis中有很多“大字典”,“大字典”的擴容是非常耗時的,對于單線程的redis來說是比較難以承受的,所以redis會采用漸進式hash。“字典”內部包含兩個hashtable,通常只有一個hashtable有值,當字典需要進行擴容或縮容時,就會分配新的hashtable,然后進行漸進式搬遷。在每次進行增刪改查時都會隨便進行數據搬遷,同時redis還會定時進行主動搬遷。
    (3)list類型實現:
    list列表數據結構使用的是quickList,quickList是zipList和linkedList的混合體,它將linkedList按段切分,每一段使用zipList讓存儲緊湊,多個zipList之間使用雙向指針串接起來。我們都知道鏈表linkedList,但zipList是什么呢,我們來介紹下:壓縮列表是一塊連續的內存空間,元素之間緊挨著存儲,沒有任何冗余空隙,它有點類似于數組,但由于每個元素占用的空間不同,所以列表會記錄最后一個元素的偏移量,每個元素會記錄前一個元素的偏移量,使用的是偏移量而并不是指針,以節省內存空間。當zipList占用內存太大時,重新分配內存和拷貝內存都會有很大的消耗,所以不適合存儲大型字符串,存儲的元素也不宜過多。而如果使用linkedList,鏈表的附加空間相對太高,另外每個節點的內存都是單獨分配的,會加劇內存的碎片化,影響內存的使用效率。所以redis將兩者相結合推出了quickList。
    (4)set類型實現:
    redis集合對象set底層使用了intset和hashtable兩種數據結構存儲,intset類似于java數組而hashtable類似于java哈希表(key為set的值,value為null)。intset的使用是有一定條件的:1.保存的所有元素都是整數值、2.保存的元素數量不超過512個,若不滿足該條件則會使用hashtable。intset內部其實是一個數組,而且存儲的數據是有序的,因為在查找數據的時候是通過二分查找來實現的。
    (5)zset類型實現:
    redis的zset類型是一個復合類型,一方面需要hash結構來存儲value和score的對應關系,另一方面還需要按照score進行排序,所以zset實際是hash字典加跳躍表實現的。跳躍表的最底層相當于一條有序鏈表,每一個節點都是key/value的數據節點,節點的key即我們需要存儲的數據,value即對應的score。鏈表的每個節點都會通過隨機算法確定層數,高層的節點在同一層通過單向鏈表連接。header節點記錄了最高的層數,并且是每層鏈表的起始節點,通過這種方式可以使查詢的效率從O(n)降為O(lg(n))。同時節點在每層都會記錄其到前一個節點需要跨越的節點數(span),將“查詢路徑”經過的所有節點記錄的span累加在一起就是當前元素的排名,這樣就實現了排名的功能。
  3. redis如何實現限流?
    redis可以通過zset來實現滑動窗口限流,即保證在指定時間內只允許發生N次請求。具體實現思路:zset的value和score值都使用時間戳,每次請求到來時都維護下時間窗口,利用Zremrangebyscore命令將指定窗口外的所有記錄都清除,然后通過Zcard獲取窗口內的記錄條數,若未超過限制值,則通過Zadd命令加入記錄,并且為相應的key設置過期時間防止浪費內存空間。除此之外,redis4.0提供了redis-cell組件,它是通過rust語言實現的,支持實現漏斗限流和令牌桶限流。
  4. redis單線程為什么還能這么快?
    主要有以下幾個原因:1:redis的數據存儲在內存中,所有操作都是基于內存的、2:redis針對內存操作設計了簡單的數據結構,使數據的存取速度更快、3:單線程避免的多線程上下文切換的成本,也不用考慮加鎖和死鎖問題、4:redis采用了“IO多路復用”,即使是單線程仍能夠支持大量的客戶端請求。
    這里的“IO多路復用”實際就相當于java中的NIO,當我們使用BIO時,每當服務收到請求就需要新建一個線程用于和客戶端保持連接,線程在進行I/O操作的時候,由于沒有辦法知道到底能不能寫、能不能讀,只能“傻等”,這就導致服務器需要使用大量的線程資源。當服務其的連接數達到十萬甚至百萬級時,BIO便無能為力了。而NIO是基于事件機制的,會提前注冊所有事件和相應的事件處理器,在相應的事件到來時在回調相應的處理器。
  5. redis持久化的實現方式?
    redis的數據都保存在內存里,如果系統宕機就會導致數據丟失,redis通過其持久化機制來保障數據不會丟失。redis的持久化機制有兩種:RDB快照、AOF增量日志,快照是某個時間點數據的二進制全量備份,而AOF日志記錄的是修改內存數據的指令記錄,兩者都存儲在磁盤文件中。在重啟redis恢復數據時,會先加載RBD快照文件,然后再重放本次快照后的AOF文件。因為redis是單線程的,為了防止阻塞對請求的響應,redis會fork子進程來進行快照持久化,而持久化期間客戶端請求進行的內存數據的修改都采取COW(copy on write)的方式。redis在內存修改指令執行后會將指令記錄存入AOF文件中,由于是文件的形式存儲的,所以需要使用fsync命令,確保將數據刷入磁盤,為了保證性能,redis通常每1s執行一次fsync命令。
  6. redis如何實現分布式鎖?
    最簡單的方式就是使用setnx key value命令,獲取特定key的鎖,在使用完臨界區資源后釋放掉該鎖。但假如獲取鎖的機器宕機了就會導致鎖一直無法釋放,所以在setnx后需要對鎖加過期時間,但由于獲取鎖和設置過期時間不是原子操作,仍然可能存在風險,所以可以使用SET key value [EX seconds] [PX milliseconds] [NX|XX] 命令,在原子的獲取鎖并設置過期時間。但是這種方式仍存在問題,就是當程序執行時間超過了鎖的過期時間,就會導致其他應用也能夠獲取鎖使用臨界區資源,同時當程序執行完成后進行解鎖時有可能錯誤的釋放其他應用獲得的鎖,數據的安全性就得不到保障了。此時我們可以使用一個守護線程去為當前應用的鎖“續命”,就是增加它的過期時間。
    除了上面提到的各種問題外,還有一點需要強調,就是一般redis實例都會進行主從同步,如果主庫掛了,在切換到從庫時,鎖可能會存在丟失的情況,因為redis的主從同步是異步的,不過這種功能情況是比較少,如果想要解決這個問題,那么可以使用redLock,redLock需要提供多個redis實例,這些實例間相互獨立且沒有主從關系,在申請鎖時需要向所有實例發出請求,只有超過半數節點實例通過后才能成功,所以使用redLock的讀寫性能會相對差些。
  7. redis的過期策略?
    redis的過期策略主要有兩種:1.定時掃描策略、2.惰性策略。
    定時掃描策略:redis會將所有設置了過期時間的key都放入到一個獨立的字典中,之后會定時遍歷這個字典來刪除到期的key。默認每秒進行10次掃描,過期掃描并不會遍歷過期字典中的所有key,而是采用一種貪心策略,每次隨機取20個key并刪除其中已過期的key,直到過期key的比例不超過1/4。因為redis是單線程的,所以我們應該避免大量key同時過期,這會導致線上的讀寫請求出現明顯的卡頓,因為一方面redis需要持續的掃描過期字典,另一方面內存管理器也需要頻繁的回收內存頁。
    惰性策略:所謂的惰性刪除就是在客戶端訪問這個key時,redis對key的過期時間進行檢測,如果過期了就刪除。redis并不是純粹的單線程,他還有幾個異步線程專門處理一些耗時任務,比如當惰性刪除過期key時,如果被刪除的key是一個非常大的對象,此時就會將刪除任務放入一個專門的異步隊列中,由特定的線程來處理刪除任務。
  8. redis的LRU實現?
    當redis使用的內存超出了服務器的物理內存限制后,內存數據會和磁盤進行swap,這會讓redis的性能急劇下降,對于redis來說這樣的存取效率幾乎等于不可用,所以在生產環境中我們經常會禁止內存磁盤swap。在實際內存過大時,redis提供了幾種策略來讓用戶決定如何騰出內存空間,其中默認的策略是禁止服務的寫請求,除此之外我們最常用的就是,對設置了過期時間的key根據LRU進行刪除。我們都知道可以通過單鏈表來實現LRU算法,每當某個節點的數據被訪問時,就將該節點移到鏈表的頭部,鏈表尾部的節點就是不常用的可以踢除的數據。但是使用鏈表結構是需要浪費很大內存空間,所以redis并沒有采用這種策略,而是為每個key增加了額外的小字段,用于標識key最近訪問時間,在redis執行寫操作時,發現內存不足就會在設置了過期時間的key中隨意選出5個,并將最近最少使用的key刪除掉,直到內存空間足夠進行寫操作,這個過程只會發生在redis執行寫操作的過程中。
  9. 緩存穿透、緩存雪崩、緩存擊穿的解決方案?
    【緩存穿透】指的是請求訪問了數據庫中不存在的數據,緩存中同樣不可能存在相應數據。這種情況比較簡單的解決方案是對數據庫查詢出的空數據也設置短時間的緩存。除此之外也可以通過布隆過濾器過濾掉訪問不存在數據的請求,也可以在邏輯層對非法數據訪問進行控制。
    【緩存雪崩】指的是大量key在同一時間過期,這種情況會給數據庫帶來很大壓力,同時redis也需要耗費系統資源處理過期緩存數據。所以我們應該盡量避免出現大量key同時失效的情況,一個比較簡單的方案就是在設置過期時間時在固定過期時長后面加上一段隨機時長。
    【緩存擊穿】指的是某些請求量非常大的熱點key在過期時,會導致大量查詢請求打入數據庫。為了解決這個問題:可以在緩存過期后,從數據庫讀取數據時加一個分布式鎖,獲得鎖的線程查詢數據并將數據設置進緩存,其他線程可以在阻塞或睡眠后重新從緩存中讀取數據。

(六)spring(done)

  1. Spring Bean的加載過程?
    個人習慣將spring的啟動過程分為三個階段:容器初始化階段、bean實例化階段、bean初始化階段。
    (1)容器初始化階段:該階段主要是初始化容器上下文環境,檢驗系統屬性和環境變量。這一階段會注冊所有的beanPostProcessor,同時會執行所有的beanFactoryPostProcessor,根據scanning-path掃描所有@Component、@Service等注解標識的類,并將其封裝成BeanDefinition存儲起來以便后續使用。
    (2)bean實例化階段:該階段主要是選定合適的構造器構造對象實例,構造出的對象實例仍然需要進行屬性的依賴注入。此階段還會進行循環依賴的檢查和處理,還會檢查是否@DependsOn依賴的必須構建的實例并進行構建,最終會根據合適的構造函數通過反射或者cglib來構造一個實例對象。
    (3)bean初始化階段:該階段主要是進行依賴的注入和執行自定義的初始化邏輯,同時也會注冊對象實例的銷毀邏輯。首先會處理所有的Aware接口,然后執行@PostConstruct定義的方法,之后執行InitializingBean接口的afterPropertiesSet方法,再之后執行自定義的init-method,再然后為對象實例生成動態代理,最后檢查是否存在循環引用問題及注冊bean銷毀的回調接口。
    在這里插入圖片描述
  2. Spring Bean的循環依賴問題?
    在spring中使用三個HashMap處理循環依賴問題(也有人稱之為"三級緩存"),這三個HashMap分別為:singletonObjects、singletonFactories、earlySingletonObjects。在每次bean實例化好后都會將獲取該bean的工廠對象存入singletonFactories中,如果其他bean在實例化時若依賴該bean就會首先從singletonObjects中獲取,獲取不到就會去earlySingletonObjects中獲取,在獲取不到就會從singletonFactories中獲取該bean的工廠對象,通過工廠對象獲取該bean。之所以使用一個工廠對象是為了通過beanPostProcessor在不同場景下能夠返回不同的”半成品“bean。目前spring中循環依賴主要有三種情況:
    (1)prototype類型的bean不支持循環依賴
    (2)singleton類型的bean在使用構造器注入時不支持循環依賴。在使用構造器注入時,會先通過getBean()方法實例化構造器中的參數對象,如果參數對象又通過構造器注入當前bean,就會產生循環依賴,甚至可能產生死循環,spring在bean構建時會將其放入正在構建中的set集合中,如果出現重復構建就會拋出異常,spring就是通過這種方式解決構造器的循環依賴問題的。
    (3)singleton類型bean使用field注入(即@Autowired或@Resource注入)時,在沒有動態代理時支持循環依賴。當進行動態代理時分為兩種情況:(1)使用普通切面生成代理支持循環依賴、(2)使用@Async等注解生成代理則不支持循壞依賴。這主要是因為使用普通代理時,在當前bean構建中若有其他bean想要注入該bean,那么普通代理會檢查當前bean是否有代理切面,若存在的或就為該bean生成代理,并將代理對象返回。而使用@Async等注解在遇到這種情況時,就會直接將當前bean實例返回,最后在初始化bean后生成代理對象,這時其他bean獲取的該bean引用是原生對象,而不是該bean的代理,這時spring就會拋出異常。
    在這里插入圖片描述
  3. Spring Bean的生命周期?
    目前spring容器中的bean主要有兩種類型:singleton類型、prototype類型。其中singleton類型的bean幾乎與spring容器擁有相同的生命周期,singleton類型的bean在容器中只有一個實例,所有對該對象的引用都共享這個實例,該實例在容器啟動被第一次初始化后,將一直存活到容器退出。而prototype類型的bean,容器在收到請求時,每次都會返回一個新的對象實例給請求方,和new有點像,但是會進行依賴注入和代理操作,對象實例在返回給請求方后,容器就不在擁有該對象的引用,它的生命周期取決于應用方,在應用方使用完成后就有可能被jvm回收掉。
  4. Spring IoC實現原理?
    目前我們常用的spring容器是ApplicationContext,由容器來幫我實現具體的依賴注入。常用的依賴注入方式有兩種:構造器注入、setter方法注入,目前spring官方推薦的是使用構造器注入,這主要是因為構造器注入能夠保證對象在實例化后就能夠正常的使用,而當我們沒有使用構造器注入時,如果我們使用new操作去構建對象,構建出的對象不會進行依賴注入,使用時就有可能導致npe異常,而構造器注入就能夠避免這個問題,但是構造器注入的寫法很麻煩,在依賴的對象很多時代碼就會很臃腫,同時使用構造器注入是不能夠支持循環依賴的,同時在spring項目中直接new對象的邏輯也不多,所以還是可以直接使用setter注入的。setter注入實際就是我們常用的@Autowired、@Resource注解的注入方式,容器在對象進行依賴注入前就會在對象信息類BeanDefination中掃描出所有@Autowired、@Resource注解標識的屬性字段,然后通過getBean()方法獲取相應的實例,再通過反射將實例引用set給對應的屬性字段。通過構造器注入的方式,在對象實例化時會選擇合適的構造器,然后通過getBean獲取所有的構造器參數對象,最后利用構造器通過反射或者cglib來生成對象實例,在利用對象實例進行后續的初始化和動態代理等操作。
  5. Spring AOP實現原理?
    spring實現了自己的AOP框架(spring AOP),同時在spring2.0版本中也集成了AspectJ,spring的動態代理主要由兩種實現方式:JDK動態代理和cglib動態代理。動態代理能夠動態的為目標對象成代理對象,其中JDK動態代理需要目標對象實現特定的接口,而cglib動態代理主要是通過動態的生成目標對象的子類來覆寫目標對象的方法。jdk動態代理需要實現InvocationHandler接口,同時通過Proxy類生成相應的代理對象,而cglib動態代理需要實現MethodInterceptor接口,并通過Enhancer類生成特定代理對象。由于是通過繼承來實現代理對象,所以cglib動態代理的代理方法不能夠使用final、private修飾,否則將導致代理失效,而一旦代理時效,接口的請求將會直接進入代理對象的方法中,因為生成的代理對象是不會進行依賴注入的,所以如果該方法內需要使用其他對象,那就會出現NPE異常,所以在使用cglib動態代理時一定要注意代理方法不能使用final修飾(private修飾的方法只在類內可見,外部對象無法使用,所以這里沒提private)。在spring實現的cglib動態代理中,實際上對代理對象的所有接口調用,無論該接口是否有代理,都會先進入MethodInterceptor接口的intercept方法中,spring在intercept方法中會判斷相應的接口是否有切面邏輯,如果有的話就執行相應的邏輯,否則就會調用目標對象的原生方法執行。實際上spring的動態代理存在一點的瑕疵,就是在我們對同一個類的接口A和接口B都進行代理時,若接口A中調用了接口B,那么當外部對象調用接口A時,只有接口A會執行切面邏輯,而接口A內部的接口B不會執行切面邏輯,這時因為接口A內保存的是原生對象的引用,所以在接口A內調用接口B,調用的是原生對象的接口B。如果想要解決這種問題需要讓原生對象持有它的代理對象的引用,可以通過AopContext.currentProxy()方法獲取原生對象當前的代理對象。
    Spring Aop主要由幾大組件構成,其中主要包括:Joinpoint、Pointcut、Advice、Advisor。其中Joinpoint指的是切面邏輯需要切入的點,在Spring Aop中目前只支持方法調用類型的切入點,而Pointcut就是切入點的具體表達式,我們通過Pointcut可以匹配到具體的Joinpoint。Advice指的是需要切入的具體邏輯,有多種類型的Advice,包括Before Advice、After Advice以及全能的Around Advice。Advisor是Spring Aop切面的概念實體,一般包含一個Pointcut和多個Advice。在同一個Advisor中,若存在多個Advice,則會按照在文件中的出現順序先后執行,若在不同Advisor中的Advice都匹配到了同一個Joinpoint,那么此時需要通過@Order來標識相應切面類的優先級來指定執行順序,否則Advice的執行順序是不確定的。所以一定要注意:如果程序對切面的執行順序有要求,一定要使用@Order注解標識切面的執行順序。
    Spring在對第一個對象實例化前就會解析所有使用@Aspect修飾的類,并通過反射獲取Pointcut和Advice,最后生成Advisor存儲起來,在對象初始化完成后,會檢測是該對象是否匹配特定的Pointcut,并對需要代理的對象通過cglib或反射生成動態代理。
    在這里插入圖片描述
  6. Spring事務實現原理?
    在一般的事務模型中通常有三個組件:
    (1)Application Program(應用程序):應用程序通常用來定義事務的邊界及事務的隔離性等各種事務屬性。
    (2)Resource Manager(資源管理器):一般指數據庫實例,提供了事務的具體實現,并且實現了XA協議的所有接口。
    (3)Transaction Manager(事務管理器):負責協調各管理事務,提供給應用程序事務管理的接口,一般只有在分布式事務中才會使用事務管理器。
    對于Spring而言,事務的實現是由具體使用的數據庫實現的,而Spring需要做的一般是界定事務的邊界(即事務的開始和結束),定義事務的傳播性,定義事務的回滾規則等,然后在合適的場景下調用具體數據庫提供的相應事務接口進行事務操作。最簡單的事務操作邏輯如下:
    在這里插入圖片描述
    由于存在很多不同的數據庫訪問技術,且這些數據庫訪問技術都有自己的數據庫訪問API,為了統一各種數據訪問技術的操作流程,spring推出了PlatformTransactionManager接口,其抽象實現類AbstractPlatformTransactionManager使用模板方法,實現了大多數事務操作的通用邏輯,而具體數據訪問操作接口需要由子類實現相應的接口,它的子類有:JpaTransactionManager、DataSourceTransactionManager、HibernateTransactionManager等,其中我們最常用的是mybatis提供的DataSourceTransactionManager事務管理器,通常配合hikariCp數據庫連接池一起使用。除了統一不同數據訪問技術的事務操作邏輯外,spring還通過ThreadLocal存儲相應的connection來實現事務的跨方法操作,在事務開始時獲取相應的connection,然后將connection綁定到當前線程,數據訪問對象在進行數據訪問就從線程中獲取該connection,最后使用這個connection進行事務的提交或回滾,最后解除它和當前線程的綁定。并且要求不同的數據訪問技術處理各自的自定義異常,拋出Spring制定的特定事務異常,這就由統一異常的處理邏輯。
    spring為我們提供了很多類型的事務傳播行為,其中REQUIRED是我們最常用的事務傳播行為,同時也是Spring默認的事務傳播行為,在沒有事務時創建事務,若已存在事務就加入當前事務。除此之外,對于一些查詢方法可以使用SUPPORTS,如果當前存在事務就加入事務,保證事務能夠讀到實時的數據,而當沒有事務時,就正常的使用一致性非鎖定讀來讀取數據。當方法內調用了某個方法,而不希望影響當前的事務,可以使用REQUIRES_NEW標識被調用方法來保證不影響當前事務,例如當前事務執行過程中,需要執行某個方法來記錄日志,那么此時就可以使用REQUIRES_NEW標識記錄日志的方法,無論日志記錄是否保存成功,都不會影響當前事務的進行,REQUIRES_NEW不管當前是否存在事務都會創建新的事務。最后一個是NESTED,就是大名鼎鼎的嵌套事務,在嵌套事務中由一個頂層事務控制各個層次的事務,頂層事務下的嵌套事務被稱為子事務,子事務的提交和回滾都需要在父事務提交后生效。在mysql中只能通過SavePoint來模擬實現嵌套事務,用戶無法選擇哪些鎖被子事務繼承,事務執行期間子事務可以獲得和使用所有父事務持有的鎖,這個真正意義上的嵌套事務優有所不同,真正意義上的嵌套事務允許父事務傳遞特定的鎖給子事務,同時SavePoint模擬的嵌套事務也無法實現子事務的并發執行。SavePoint實現的嵌套事務,本質上還是只有一個事務,只不過在事務中使用了很多SavePoint保存點,在出現回滾時,事務可以回滾到特定的保存點。嵌套事務解決的是類似換乘飛機的問題,當我想要從沈陽旅行去大理時,由于某些特殊原因,我們可能需要先坐飛機從沈陽到上海,在從上海坐飛機到大理,在普通事務中當上海到大理的飛機出現延誤等異常時,就會使事務回滾到初始點,也就我們需要回到沈陽,這顯然是我們無法接受,同時也沒有必要的,在這種情景下就出現了嵌套事務。最后需要說的一點是:在通過@Transactional標注方法時,若該方法調用了類內另外一個方法,若該方法也使用了@Transactional,此時@Transactional是沒有效果的,這是Spring事務的一個小小的問題。
    最后Spring通過AOP解耦了事務邏輯和業務邏輯,使用@Transactional標識的方法或類會生成特定的代理對象,Spring實現了方法攔截MethodInterceptor接口,并在invoke接口中通過PlatformTransactionManager實現了事務的操作邏輯,而具體的事務屬性也會利用反射從@Transactional注解中獲取,并生成特定的TransactionDefination對象,在實際方法執行前根據定義好的事務傳播行為,生成相應的TransactionStatus執行事務,在實際方法執行出現異常時,根據相應的規則進行回滾,在實際方法正常完成時,進行事務的提交并清理ThreadLocal中特定的connection及清理其他事務使用的資源。
  7. 分布式事務的兩階段提交?三階段提交?
    CAP理論:在存在網絡分區的情況下,無法同時保證一致性和可用性。分布式系統的節點往往都是分布在不同的機器上進行網絡隔離的,這意味著必然存在網絡斷開的風險,這種網絡斷開的場景被稱為網絡分區,目前大部分的公司基本都會出現網絡分區的場景,所以CAP理論一般都是在一致性和可用性間進行取舍,在基于BASE模型的基礎上,基本都是不保證數據變更后的強一致性,而是保證數據的最終一致性。redis的主從模式實際上能夠保證的是最終一致性,因為redis的主從是異步復制的,而mysql更加多元化,支持同步、異步、半異步的主從復制。
    分布式事務相對于單機事務,需要引入事務管理器進行事務的協調,事務管理器控制著全局事務,管理事務的生命周期并協調相應的資源。事務管理器要求數據庫實現XA協議的所有接口,同時會提供特定的分布式接口給應用程序。分布式事務通過事務管理器實現了兩階段提交,在兩階段提交的過程中,首先第一階段會通過事務管理器要求所有的數據庫準備相關的資源,如果所有數據庫都準備好了,第二階段就會要求所有的數據庫執行commit操作,如果第一階段執行過程中出現了異常,那么第二階段就會執行回滾操作,將鎖定的資源都釋放掉。
    在兩階段提交中,如果第二階段提交過程中,在通知所有數據庫commit后,如果某個數據庫宕機了,就有可能出現數據庫不一致的情況,為了解決這個問題,還出現了三階段提交,在兩階段提交的基礎上增加了preCommit的過程。除了兩階段、三階段提交外,還可以使用更輕量級的Paxos協議,ZooKeeper就是使用了這種協議。在實際的使用場景中,由于事務管理器的自身穩定性、可用性的影響以及網絡通信中可能產生的問題,事務管理器需要處理的情況比較多,同時事務管理器需要記錄很多日志,同時執行期間需要鎖定很多數據庫資源,兩階段提交及三階段提交的使用會給系統帶來很大的開銷,所以需要慎重的考慮是否一定需要使用。
    除了最終一致性方案、兩階段提交方案外、Paxos協議外,還可以使用TCC分布式事務處理方案。TCC實際上相當于業務層的XA協議,通過在業務代碼中控制資源的鎖定,來減少數據庫資源的鎖定,TCC比起兩階段提交性能會更好,但是會增加業務層代碼的復雜度會,增加事務的執行時長。一般都是大公司才會使用這種方案,大部分中小型公司使用的還是最終一致性方案。
  8. 事務型消息隊列的實現方式?
    在日常的開發中,我們經常會使用到MQ消息隊列,消息隊列的引入有以下幾個優點:(1)可以減少服務的響應時間,提高核心鏈路的吞吐量。(2)降低應用程序之間的耦合度。(3)高峰期緩存部分消息,提高服務的可用性。日常使用的普通消息都比較簡單,實現的成本也比較低,只要將需要發送的消息成功發出即可,消息隊列可以實現一些消息持久化或者消息消費確認等功能,但是在一些金融級分布式架構中需要使用事務消息來保障消息的一致性,不過這里的一致性僅僅是最終一致性,因為隊列消息本身就是異步的根本無法保證強一致性。事務消息最簡單的一個場景就是付款的場景:某個用戶進行付款操作,付好款后需要向用戶發送短信。付款功能和短信發送在不同的服務中,為了提高付款操作的響應時間,可以使用MQ消息進行兩個業務功能的解耦。此時就存在兩個操作不一致的問題,如果付款操做更新數據庫后,而發短信操作出現了問題,那就出現了操作不一致的問題。為了解決這個問題我們可以在付款操作更新數據庫時使用數據庫事務,在事務提交前先確認消息是否已經成功發送,成功后再進行commit,但是這種方式又依賴MQ消息的發送,如果MQ消息的發送超時了,那么我們無法確認短信是否已經成功發送了,超時可能是短信發送成功了,也可能發送失敗了,那么此時便無法決定是否提交數據庫事務了。所以一般我們都會先發出MQ消息的請求,在轉賬的數據庫操作commit后再實際MQ發送消息,這樣就能保證兩個操作的原子性。但是仍存在一點問題就是在退款操作執行完成后需要通知消息隊列發送消息,但是如果這步操作失敗了,那么消息隊列就無法決定是否能夠發送MQ消息了,為了解決這個問題,需要消息隊列可以進行特定的回調,主動詢問數據庫操作是否成功。這就是一般事務消息的實現方案,事務消息的實現會導致消息隊列性能下降,在rabbitMq中甚至可能會“吸干”rabbitMq的性能,所以在非必要的場景下一般還是使用普通的MQ消息,如果一定要使用事務消息也需要選擇合適的MQ以及進行合適的優化和測試。

(七)中間件相關(done)

  1. 負載均衡和反向代理
    當客戶端發送請求到服務端時,請求首先會通過DNS進行域名解析,將相應的域名解析為具體的IP地址,然后與相應的機器通過三次握手建立socket連接并發送http請求。在分布式系統中為了避免單點問題,通常會對服務器進行集群部署,此時為了能夠均衡的將請求發送到集群中,通常會引入負載均衡器,DNS返回的永遠是負載均衡器器的地址,請求首先會被發送到負載均衡器上,由負載均衡器決定具體轉發給哪臺服務器。負載均衡技術通常可以分為硬件負載均衡和軟件負載均衡,由于軟件負載均衡的代價更低且可控性更強,所以使用的更為普遍。為了避免負載均衡器自己成為單點,通常可以使用兩臺機器構成,其中一臺處于服務狀態,而另一臺處于standby狀態,當出現問題時standby可以實現自動接管。可以在負載均衡器內直接配置業務處理機器的IP地址列表,也可以通過consul、zk等中間件獲取特定業務處理機器的IP地址列表,之后負載均衡器會通過特定的選擇方式轉發請求給特定業務處理機器,主要方式包括:1.Hash選擇:對應用層的請求信息做hash,從而將請求分配到特定的機器上。這種方式可以應用于靜態圖片的加載,通過hash保證每次請求都能夠訪問相同的機器,進而提升服務器性能。2.Round-Robin選擇:根據地址列表按順序選擇,這是目前使用最多的方式。除了上面兩種比較常用的選擇方式外,還有按權重選擇、按負載選擇、按連接數選擇等方式。負載均衡器通過NAT技術修改報文的目標地址和端口號來實現消息的轉發,由于請求包和響應包都要經過負載均衡器,隨著請求量的上漲,負載均衡器的壓力就會迅速上升,尤其是對于響應包非常大的應用,這種方式不但會導致網絡流量的增加,也會導致響應的延遲,也會給負載均衡器帶來很大的壓力,為了解決這個問題可以使用IP Tunneling技術將響應包的數據直接返回給客戶端。一般的分布式架構中都會使用反向代理服務器,并通過反向代理服務器實現負載均衡,使用反向代理能夠保護內網服務器的安全,節約IP地址資源并降低內部服務器壓力,我們常用的反向代理技術主要是Nginx及Nginx擴展技術OpenResty等。
  2. Http服務器和Application服務器
    在經過反向代理服務器后,請求會被直接轉交給具體處理請求的業務機器,所有部署在Web上的服務器都被稱為Web服務器,而Web服務器按功能又可以分為Http服務器和Application服務器,Http服務器主要用來做靜態內容服務、代理服務器、負載均衡等,而Application服務器支持開發語言的運行環境,能夠動態的生成資源返回給客戶端。最常用的Application服務器是Tomcat,Tomcat運行在JVM之上,用來管理Servlet,主要由兩部分組成:connector和container,前者負責接收請求,后者負責處理請求,采用責任鏈的設計模式,把請求和響應封裝好后傳給servlet進行處理。Application服務器除了Tomcat外還有Apache和Weblogic,Tomcat能夠支持的并發連接數是1000左右,Apache能夠支持的并發連接數是200-300左右,而Weblogic的并發連接數平均能達到3000左右。
  3. 服務注冊與發現
    在微服務中,一個用戶的請求往往涉及多個內部服務調用,每個服務單元為了避免單點問題,通常都是采取集群部署的方式,部署在不同的機器上,在進行服務調用時,需要獲取被調用服務的所有機器IP地址列表,此時便需要引入服務的自動注冊與發現工具。首先需要部署服務發現服務,各個應用服務在啟動時自動將自己的信息注冊到服務發現服務上,應用服務啟動后能夠實時的從服務發現服務上獲取被調用服務的地址列表。服務發現服務也會定期檢查應用服務的健康狀態,去掉不健康的實例地址,這樣新增實例時只需要部署新實例,實例下線時直接關停服務即可,服務發現會自動檢查服務實例的增減。客戶端在獲取到被調用服務的所有地址后,可以自己決定負載策略,甚至可以在服務注冊時增加些元數據,之后在客戶端根據這些元數據進行流量控制、A/B測試、藍綠發布等。
    Consul服務注冊中心:Consul用于實現分布式系統的服務注冊與發現,Consul 是分布式的、高可用的、 可橫向擴展的。Consul可以通過DNS/HTTP接口來進行服務的注冊和發現,同時提供了健康監測機制來監測服務機器是否可用,同時支持開箱即用的多數據中心的支持。Consul分為Client和Server兩種節點,其中Server節點負責保存數據,Client節點負責健康檢查及轉發數據請求到Server節點。一般Server節點部署在單獨的不同的機器上,來維護Consul Server的穩定性。而一般Client節點和具體的應用(例如Tomcat)部署在同一臺機器上,這樣能夠避免通過網絡調用進行健康檢查帶來的不確定性,防止因為網絡環境不好造成的誤判,同時也能夠避免給業務服務器帶來額外的請求壓力。Consul使用Gossip協議進行數據通信,同時使用raft算法保證數據一致性。
    ZooKeeper簡介:Zookeeper是一個開源的分布式協調服務,功能十分強大,除了支持服務注冊和發現外,還可以用于負載均衡、集群管理、分布式鎖等等。Zookeeper致力于為那些高吞吐的大型分布式系統提供一個高性能、高可用、且具有嚴格順序訪問控制能力的分布式協調服務。Zookeeper通過樹形結構來存儲數據,它由一系列被稱為ZNode的數據節點組成,類似文件系統的組織結構,但是數據確是存儲在內存中的。Zookeeper內的數據節點ZNode可以分為持久節點和臨時節點,同時Zookeeper支持通過Watcher對特定的數據節點進行事件監聽,當特定事件發生時,監聽器會被觸發,并將事件信息推送到客戶端,該機制是Zookeeper實現分布式協調服務的重要特性。Zookeeper使用ZAB協議進行崩潰恢復和數據廣播,同時使用Paxos算法保證數據的一致性:所有的事務請求必須由唯一的Leader服務來處理,Leader服務將事務請求轉換為事務Proposal,并將該Proposal分發給集群中所有的Follower服務。如果有半數的Follower服務進行了正確的反饋,那么Leader就會再次向所有的Follower發出Commit消息,要求將前一個Proposal進行提交。Zookeeper通過Watcher機制可以實現服務注冊和發現:分布式系統的所有的服務節點可以對某個ZNode注冊監聽,之后只要有新的服務上線或者下線都會更改該ZNode,所有服務節點都會收到該事件。
    不同服務注冊中心對比:ZooKeeper因為采用ZNode事件監聽機制,能夠實時獲取服務節點上下線狀態,但是需要在服務中使用ZK的SDK,同時原生并不支持數據中心,dubbo內部通過ZooKeeper實現服務注冊中心。Consul簡單易用,可以使用http/dns進行交互,不需要集成SDK,支持多數據中心,同時還提供web管理界面,但是使用基于接口的健康檢查,不能實時獲取服務的上線狀態。Eureka明顯的不同點是:保證了CAP中的AP,而不是CP,也就是只能保證最終一致性,進行數據訪問時可能出現數據錯誤。
    在這里插入圖片描述
  4. 服務框架/RPC客戶端
    在沒有服務化之前,應用都是通過本地調用的方式來使用其他組件的,服務化會使得原來的本地調用變為遠程調用。在服務比較簡單時,大家通常會選擇實現簡單的RPC客戶端,但當提供服務的集群非常多的時候,通用的服務框架就非常重要了。簡單的RPC客戶端實現,可以通過HttpClient或者OkHttp,兩者都是Http客戶端,通過Http調用其他服務。相對于HttpClient,OkHttp的SDK更容易使用,且性能稍微更好些,目前在Android中得到了廣泛的使用。這種RPC客戶端通常作為應用的依賴包,與應用一起打包,隨應用一起啟動,但是如果RPC客戶端需要升級,就需要更新應用版本。在微服務架構中,通過這種RPC客戶端直接進行服務間調用可能存在一些隱患,在介紹完服務框架后會詳細說明,不過因為它的實現簡單,所以有很多公司使用該種方式。
    服務框架的實現主要包括客戶端實現和服務端實現:1.客戶端:首先需要根據調用服務名來獲取提供服務的機器地址列表,根據特定的負載均衡策略選擇要調用的機器。然后將請求數據序列化為二進制數據,并根據特定的協議封裝成數據包,選擇特定的通信方式進行數據傳輸。2.服務端:在啟動后持續地接受請求,對收到的數據進行反序列化得到請求對象。根據請求接口名和參數定位具體實例,然后通過反射來進行服務調用,最后將執行結果序列化為二進制數據返回給客戶端。
    服務框架可以通過Consul、ZooKeeper或Eureka作為服務注冊中心,客戶端通過服務注冊中心拿到可用的服務提供者列表后,可以使用特定的負載均衡策略,包括隨機、輪詢、權重等。除此之外還可以根據業務需求制定特定的路由策略,對客戶端請求進行分組,不同組內的客戶端請求會打到不同的機器上,這樣就能將不同業務的請求隔離開,保護核心業務不被非核心業務影響。在多機房的場景中,也可以通過客戶端路由策略,盡量將請求分配到同機房的服務端上,減少請求的響應時間。同時客戶端也可以進行特定的流量控制,在使用一些基礎服務時,根據請求來源的不同級別進行不同的流量控制,來保證基礎服務的穩定。在對請求進行序列化和反序列化時,可以使用java自帶的序列化工具,也可以使用jackson、fastjson等工具,在服務層可以使用xml或json等作為序列化數據格式,在通信層可以使用Http協議或者自定義的協議,比如Dubbo使用的就是自定義的Dubbo協議。具體的通信方式通常有BIO、NIO、AIO,其中BIO采用的是阻塞IO的方式,每個請求使用一個線程并需要建立一個連接,這種方式比較簡單,但是會消耗很多線程且需要建立很多連接,消耗比較大。服務框架通常采用NIO方式,NIO使用專門的IO線程,由IO線程處理實際的IO操作,同時也不再需要建立大量的連接,在一個連接上便可以處理大量的IO操作。服務框架可以利用NIO提供同步方式進行遠程調用,同時也可以支持幾種異步方式進行遠程調用。服務端通信部分同樣不能使用BIO,也要使用NIO來實現,接受到請求后需要進行協議解析及反序列化,之后根據服務名稱、接口名稱、版本號等找到提供服務的具體對象,然后根據傳過來的參數進行方法調用。在服務器端同樣也可以設置特定的路由策略,服務端內有很多工作線程池,將請求路由到不同的工作線程池,便能夠在服務端進行資源隔離,保證服務端的穩定。同樣服務端也可以進行流量控制,對不同的服務調用者進行分級,保證能夠對優先級高的服務者優先提供服務。
    在微服務架構中直接使用簡單的RPC客戶端可能存在一些不足:(1)簡單RPC客戶端不能對請求進行路由分組,當核心服務和非核心服務共用某些基礎服務時,可能出現非核心服務將基礎服務拖垮,導致基礎服務不可用,進而影響核心服務功能的問題。(2)大部分RPC客戶端僅支持同步方式進行遠程接口調用,甚至很多RPC客戶端使用的是BIO通信方式,這將會浪費大量的數據連接,同步請求方式下線程需要阻塞等待RPC客戶端返回結果,在一些業務場景中這會浪費寶貴的線程資源,而服務框架可以支持CallBack、Future等異步通信方式,能夠提高線程資源的利用率,其中Future能夠主動控制超時、獲取結果的方式,并且它的執行仍然在原請求線程中,在很多業務場景下能夠提升系統單位時間的處理能力。(3)簡單RPC客戶端無法進行流量控制,沒有相應的限流機制,在某些業務高峰期,巨大的請求流量可能會導致系統崩潰。(4)簡單RPC客戶端沒有數據監控,需要自己在應用中通過切面記錄請求參數和響應結果,同時也無法支持全鏈路的日志監控,會導致查詢線上問題的難度更大。
  5. 消息中間件
    在微服務架構內,不同服務間除了可以通過服務框架進行RPC調用外,還可以使用消息隊列在應用間傳輸數據。消息中間件提供了有保證的消息發送功能,開發人員無需了解遠程調用過程(RPC)和網絡通信協議的細節,消息中間件適用于需要可靠數據傳輸的分布式環境,消息中間件有非常多的優點:(1)解耦:消息隊列屏蔽了生產者和消費者間的平臺差異,同時允許獨立的擴展和修改兩邊的處理過程,只需確保它們遵守同樣的MQ接口規范。(2)異步:消息中間件提供了消息的持久化功能,可以暫時存儲MQ消息,生產者和消費者都只需要將消息存入消息隊列或者從消息隊列中獲取消息,即可繼續執行其他業務邏輯。(3)削峰:消息中間件在系統訪問量劇增時,可以暫時存儲大量MQ消息,消費者可以慢慢消費MQ消息。對一臺普通的MQ服務器來說,一個隊列中堆積1萬至10萬條消息絲毫不會有什么影響,但是當隊列內的消息超過1千萬乃至一億消息時,可能會導致內存或磁盤告警,進而造成Connection阻塞。為了解決問題,可以根據業務特點,選擇:清空隊列、增加消費者機器或將消息轉到其他集群等方案。目前市面上比較主流的消息隊列主要有:Kafka、ActiveMQ、RabbitMQ、RocketMQ等,接下來詳細介紹RabbitMQ和Kafka,最后介紹各種MQ隊列的特點與區別。
    RabbitMQ簡介:RabbitMQ是采用Erlang語言實現AMQP協議的消息中間件,用在分布式系統中存儲轉發消息。在RabbitMQ內部包含幾大組件:(1)Producer:負責創建消息的生產者,創建的消息一般包括標簽和消息體,消息體被稱為payload,而標簽主要包括交換器Exchange的名稱、路由鍵RoutingKey等等。(2)Broker:消息中間件的服務節點,一般一個RabbitMQ Broker可以簡單的看作一個RabbitMQ Broker服務節點,或者RabbitMQ Broker服務實例,且大多數情況下可以將一個RabbitMQ Broker看作一臺RabbitMQ服務器。(3)Vhost:Vhost是建立在Broker上的虛擬主機,提供在實例上的邏輯分離,Vhost是RabbitMQ分配權限的最小細粒度,每個Vhost都可以對自己的隊列、綁定、交換器進行權限控制。(4)Queue:隊列,是RabbitMQ的內部對象,用于存儲消息,RabbitMQ中的消息都只能存儲在隊列中,這一點和Kafka完全不同,Kafka將消息存儲在Topic中,而實際的隊列邏輯只是Topic實際存儲文件中的位移標識。(5)Exchange、RoutingKey、BindingKey:Queue和Exchange綁定時會指定特定的BindingKey,而生產者發送消息到Exchange時,一般會指定一個RoutingKey,Exchange會根據RoutingKey及Queue綁定的BindingKey,將消息投遞到特定的Queue中。
    在這里插入圖片描述
    RabbitMQ在生產者/消費者與RabbitMQ Broker建立TCP連接時,使用了類似NIO的作法,對TCP連接進行復用,不僅可以減少性能開銷,同時也更加便于管理連接。交換器Exchange主要有fanout、direct、topic、headers四種,(1)fanout:將交換器消息路由到所有與該交換器綁定的隊列中。(2)direct:將交換器消息路由到RoutingKey和BindingKey完全相同的隊列中。(3)topic:提供了類似正則表達式的規則匹配,將消息路由到RoutingKey和BindingKey相匹配的隊列中。(4)headers:headers類型的交換器性能很差,也不實用,所以基本很少有人使用。
    在分布式系統傳輸中,消息可靠傳輸主要分為三個級別:At most once、At least once、Exactly once。在進行數據傳輸時通常會采用超時策略:在消息超時時,可能是消息已經到達服務器,因故障或其他原因未能返回確認消息,也可能是消息未能到達服務器。在這種情況下At most once不會再進行消息重傳,所以服務器最多只能接收到一次消息,存在丟失消息的可能性。而At least once會選擇消息重傳,這樣能保證消息不丟失,但是可能出現重復消息。如果服務器使用了冪等策略,且進行消息重傳,這樣既能保證消息不會丟失,同時服務器的冪等去重機制能夠保證消息的唯一性,這種場景被稱為Exactly once。 目前RabbitMQ僅支持At most once和At least once,Exactly once需要在業務客戶端中實現,在實際生產環境中,可以根據自身的業務特點進行去重,比如業務消息本身具備冪等性,或者借助redis等其他產品進行去重處理。RabbitMQ為了實現At least once需要確保消息傳輸過程的可靠性,保證在無網絡異常或故障時,消息能夠安全正確地傳輸給消費者。首先需要確保生產者的消息能夠成功到達RabbitMQ服務器,可以通過事務機制或生產者確認機制,事務機制的性能比較差,一般比較少使用,通常采用的都是生產者確認機制。在生產者客戶端會注冊消息確認ACK回調接口和消息異常NACK回調接口,生產者發送消息后無需等待結果,RabbitMQ服務器會在收到消息后會回調生產者客戶端ACK接口或NACK接口。在RabbitMQ服務器收到消息后,通過Exchange進行路由時,可能由于沒有綁定隊列或者沒有匹配的綁定導致消息無法路由,此時可以將無法路由的消息返回給生產者或存儲到備份交互器(Alternate Exchange)。在將消息路由到相應的隊列后,需要對消息和隊列進行持久化,來保證RabbitMQ服務器遇到異常時不會造成消息的丟失,不過這樣也不是完全可靠的,和mysql持久化redo日志一樣,MQ消息會先緩存在操作系統緩存中,定時通過fsync將數據刷到磁盤中,若這個過程中出現故障就可能導致消息丟失,為了進一步確保消息安全可以使用鏡像隊列。最后消費者消費消息時,需要將autoAck設置為false,通過手動確認的方式保證消費者已經成功地消費消息。
    RabbitMQ這款消息隊列中間件產品本身是基于Erlang編寫,Erlang語言天生具備分布式特性(通過同步Erlang集群各節點的magic cookie來實現)。因此,RabbitMQ天然支持Clustering。這使得RabbitMQ本身不需要像ActiveMQ、Kafka那樣通過ZooKeeper分別來實現HA方案和保存集群的元數據。集群是保證可靠性的一種方式,同時可以通過水平擴展以達到增加消息吞吐量能力的目的。 下面先來看下RabbitMQ集群的整體方案(RabbitMQ集群方案):
    在這里插入圖片描述
    RabbitMQ集群只會同步元數據,包括:(1)隊列元數據:隊列名稱和它的屬性;(2)交換器元數據:交換器名稱、類型和屬性;(3)綁定元數據:交換器與隊列或者交換器與交換器之間的綁定關系;(4)vhost元數據:為vhost內的隊列、交換器和綁定提供命名空間和安全屬性。RabbitMQ集群中的所有節點都會備份所有元數據信息,但卻不會備份MQ消息,即每個節點都存在相同的Exchange、Queue、綁定關系等元數據,但是每個Broker節點Queue內存儲的MQ消息都是完全不同的。RabbitMQ要想備份MQ消息數據,需要使用鏡像隊列,可以將隊列鏡像到集群中其他Broker節點上,如果集群中的一個節點失效了,隊列能夠自動切換到另一個節點的鏡像隊列上。鏡像隊列的所有請求都是發送或轉發到master隊列,由master隊列進行處理,最后廣播請求執行結果給所有slave隊列。RabbitMQ并沒有采用類似mysql的主主模式,沒有在所有的節點中備份MQ消息,主要是考慮到集群的吞吐性能及集群間大量數據同步的性能壓力。
    RabbitMQ集群一般還會使用HAProxy進行負載均衡,HAProxy支持從4層至7層的網絡交換,即覆蓋所有的TCP協議,甚至還支持Mysql的均衡負載。為了確保負載均衡服務HAProxy的可靠性,通常還會引入Keepalived工具,它能夠通過自身健康檢查、資源接管功能做雙機熱備,在出現問題時,實現關故障轉移。Keepalived會將兩臺Linux機器組成一個熱備組,同一時間內熱備組只有一臺機器Master提供服務,同時Master會虛擬出一個公用的虛擬IP地址,簡稱VIP,當Keepalived檢測到Master宕機或故障時,備份服務器Backup會自動接管VIP并成為Master。
    Kafka簡介:Kafka雖然也可以作為消息隊列中間件,但是定位更偏向于分布式流平臺,Kafka主要包含以下組件:
    (1)Producer:消息的產生者。
    (2)Broker:Kafka的實例,每臺服務器上可以有一個或多個Kafka實例,但通常一臺服務器只會部署一個Kafka實例。Kafka集群內的Broker都有一個不重復的編號,如圖中的Broker-0、Broker-1等。
    (3)Topic:消息的主題,可以理解為消息的分類,Kafka的數據就保存在Topic中,在每個Broker上都可以創建多個Topic。
    (4)Partition:Topic的分區,每個Topic可以有多個分區,分區的作用是做負載,提高Kafka的吞吐量。 同一個Topic在不同分區的數據是不重復的,Partition的表現形式就是一個一個的文件夾。
    (5)Replication:每一個分區都有多個副本,副本的作用是做備胎。當主分區(Leader)故障的時候會選擇一個備胎(Follower)上位,成為 Leader。 在Kafka中默認副本的最大數量是10個,且副本的數量不能大于Broker的數量,Follower和Leader絕對是在不同的機器,同一機器對同一個分區也只可能存放一個副本(包括自己)。Kafka采用分區的方式,便于通過增加機器應對日益增長的數據量,同時也可以大幅度的提升系統的吞吐量。
    (6)Message:消息主體。
    (7)Consumer:即消息的消費方。
    (8)Consumer Group:我們可以將多個消費者組成一個消費者組,在Kafka的設計中同一個分區的數據只能被消費者組中的某一個消費者消費。 同一個消費者組的消費者可以消費同一個Topic不同分區的數據,這也是為了提高 Kafka 的吞吐量。
    在這里插入圖片描述
    Kafka的Producer采用Push模式將消息發布到Broker,Producer生產的消息寫入時都需要經過leader分區處理,每條消息都是被追加到分區中,順序地寫入磁盤,所以可以保證同一分區內的數據是有序的,這點和RabbitMQ是不同的,RabbitMQ無法嚴格保證消息的有序性。Kafka同樣通過ACK應答機制來確保Producer生產的消息不丟失,消息的ACK機制主要有三種模式,可以通過參數設置:
    0:代表Producer發送數據,不需要等集群的返回,不確保消息發送成功,可能丟失消息,安全性最低但是效率最高。
    1:代表只要Leader分區成功落盤就可以返回ACK消息,若Follower分區同步前Leader分區發生故障,則可能丟失消息。
    -1:代表所有Leader分區和Follower分區全部落盤成功后才返回ACK消息,若在發送ACK消息時發生故障,則可能造成數據重復。
    當參數設置為-1時就可以達到At least once級別,而當參數設置為0時即可保證At most once級別。0.11版本的Kafka引入了冪等性,當開啟冪等功能且參數設置為-1時,即可以保證Exactly once級別。
    不同于RabbitMQ,Kafka的Consumer通常采用Pull模式從Broker中讀取數據,Pull模式可以根據Consumer的消費能力拉取適量的消息,但當沒有消息時可能一直循環獲取空數據。Kafka內的消息在被消費后并不會像RabbitMQ一樣立刻刪除,而是會繼續存儲很長一段時間。在Consumer進行消費時會維護一個Offset,記錄自己消費到的位置,以保證故障恢復后可以繼續正常消費。在0.9版本之前,Consumer默認將 Offset保存在ZooKeeper中,從0.9版本開始Consumer默認將Offset保存在Kafka一個內置的Topic中。
    各種消息中間件的差異與特點
    目前市面上比較主流的消息隊列中間件主要有:Kafka、ActiveMQ、RabbitMQ、RocketMQ等這幾種。ActiveMQ和RabbitMQ因為吞吐量不夠高且社區活躍度不高,大型互聯公司使用的比較少,業務體量一般的公司還有在用的,但是越來越多的公司更青睞RocketMQ這樣的消息中間件了。同時在部署方式上,ActiveMQ和RabbitMQ更為復雜,不如本身就是分布式架構的Kafka和RocketMQ,在分布式架構中后兩者的擴展伸縮能力更好,同時可以保證消息不丟失。而其中RabbitMQ是通過Erlang語言開發的,很難再進行二次開發和包裝。Kafka與其他的消息隊列有些不同,Kafka無法像其他消息隊列一樣提供靈活的消息路由能力,不能設置消息的TTL過期時間,同時也沒有死信隊列、延遲隊列、優先級隊列等高級隊列功能。另外在進行技術選型時,還需要考慮到各種消息中間件對業務開發語言的支持,雖然RocketMQ更好用,但是卻僅僅支持java語言,對很多非java開發的公司而言是沒辦法使用的。在這里插入圖片描述
  6. mysql和redis集群方案
    redis是目前比較常用的分布式緩存存儲系統,而mysql也是互聯網公司比較流行的關系型數據庫管理系統。之前已經介紹過兩者的基礎知識,這里簡單介紹兩者的集群方案。
    mysql集群:
    隨著業務的不停發展,通常需要對系統進行水平伸縮,這會導致數據庫連接數大量增加,會給數據庫連接池帶來很大的壓力,而大部分數據庫對水平伸縮支持都不是很好,可以通過分庫分表來減輕數據庫的壓力。數據庫分庫分表會使簡單的sql語句變得復雜,甚至可能需要引入分布式事務,通常需要在業務系統內增加一個DAL(Data Access Layer)數據庫訪問層,來透明化分庫分表對業務服務器帶來的影響。DAL可以根據分庫分表規則改變業務請求的sql及要連接的目標數據庫,DAL通常會對請求的sql進行解析或提供一種專門的定制sql語言。使用專門的定制sql語言,好處是無需解析sql語句,可以更加簡單的獲取分表規則所需的信息,但會導致開發的難度增大,同時DBA審核sql的難度也會增大。
    數據量增長會導致讀寫性能下降,除了分庫分表外,還可以通過讀寫分離多Master方案來提升數據讀寫性能。讀寫分離適用于讀多寫少,并允許一定延時的業務。對于讀寫比例基本相等的業務而言,如采用讀寫分離反而會帶來大幅度復制,造成系統運行緩慢。實現讀寫分離后,可以降低數據庫讀壓力及提升數據庫讀速度。mysql的讀寫分離通常會配合主從復制一起使用,主庫負責寫操作而從庫負責讀操作。
    在mysql數據庫通過主從復制進行了讀寫分離后,實際上仍然不能很好地解決單點問題,當主庫發生故障時,需要進行一定的改動才能把從庫切換為主庫。為了保證高可用,可以建立多Master并保證多Master數據強一致:一種方式是進行雙寫,需要采用兩階段提交、三階段提交或Paxos算法,保證寫數據時多個Master數據一致,在某個Master發生故障時,通過負載均衡器將請求切換到其他Master服務器。第二種方式是引入Keepalived組件實現雙機熱備,只使用一臺MasterA負責數據的寫入,另一臺MasterB作為backup機器,并使用半同步復制同步MasterA數據,同時MasterB也可以負責處理一部分讀請求。在這里插入圖片描述
    另外目前大部分數據庫訪問采用的仍然是同步方式,每進行一次數據庫操作就需要占用一個數據庫連接,并且需要等到數據庫操作執行完成后才會將連接釋放,這對于高并發系統而言,很容易出現數據庫連接不夠或數據庫競爭激烈的現象。采用異步數據庫訪問是解決這種問題的一種方式,異步數據庫訪問能夠支持連接的復用,可以做到用很少的連接來支持大量的數據庫訪問,同時也能夠避免連接資源競爭,減輕數據庫的壓力。
    各種類型數據庫的優缺點:(1)SQL(關系型數據庫):隨著數據量的增長,數據庫的性能會越來越差,且水平伸縮性很差,只能通過分庫分表來提供水平伸縮能力。除此之外,mysql單點問題的通用解決方案是主從模式,但是這種方式在出現故障時,需要手動切換到從庫,并且很難保障強一致性。也可以使用多Master,但是需要通過兩階段、三階段或者Paxos協議來保證數據的一致性,性能就會很差。(2)NoSQL:它建立在分布式系統上,更擅長實現水平伸縮和數據分片,并且具有非常好的性能,但卻不能夠保證完整的ACID屬性,它采用的是最終一致性原則。(3)NewSQL:NewSQL的目標是將SQL的ACID保證與NoSQL的可擴展性和高性能相結合。
    redis集群:
    很多企業沒有使用redis集群,但一般都會做主從,有了主從,當主節點掛掉時,運維可以讓從節點來接管。redis同時支持:主節點與從節點數據同步模式、從節點與從節點數據同步模式,redis數據同步是異步的,也就意味著redis只能保證最終一致性,它實現了CAP中的AP。
    redis主要兩種數據同步方式:(1)增量同步:redis主節點會將數據修改指令記錄在本地內存buffer中,然后異步將buffer中的指令同步到從節點。因為內存的buffer是有限的,所以redis只會存儲部分指令,redis用來存儲指令記錄的內存buffer是一個定長的環形數組,如果數組內容滿了,就會從頭開始覆蓋前面的內容。當網絡很差的時候,從節點長時間沒和主節點進行數據同步,就可能導致從節點落后太多而無法在進行增量同步,這時需要使用快照同步。(2)快照同步:主節點會將當前內存的所有數據全部快照到磁盤文件,然后將快照文件發送給從節點,從節點接受完畢后執行全量加載。
    Sentinel哨兵:雖然redis使用了主從數據同步,但是如果主節點宕機了,運維仍然需要手工進行主從切換,同時程序猿也需要修改相應地址并上線,這無疑是很大的問題。為了解決這個問題redis推出Sentinel哨兵,redis哨兵支持在發生故障時自動進行主從切換,程序可以不用重啟,這一點是優于mysql的,很多公司的mysql目前沒有類似的工具,每次mysql節點發生問題時,都需要手動修改地址并重啟服務。redis哨兵可以看成一個zookeeper集群,通常由3-5個節點組成。redis哨兵會持續監控主從節點的健康,當主節點掛掉時自動選擇最優從節點進行切換。redis客戶端在連接集群時,會首先連接Redis哨兵,通過Redis哨兵查詢主節點地址,然后在連接主節點進行數據交互。因為redis采用異步復制,當主節點掛掉時,從節點可能沒有收到所有同步消息,這部分未同步消息就丟失了。redis哨兵無法保證消息不丟失,只能盡量減少消息的丟失,在這一點上mysql做的更好,mysql提供了同步復制、半同步復制、異復制方式,當使用同步復制和半同步復制時能夠保證消息不丟失。
    在這里插入圖片描述
    Codis集群方案:
    Codis是國內團隊開發的Redis集群方案之,他的創始人同時也開發了分布式數據庫TiDB。Codis是一個通過Go語言開發的代理中間件,和Redis一樣也使用Redis協議對外提供服務,當客戶端向Codis節點發送指令時,Codis負責將指令轉發到后面的Redis實例來執行,并將返回結果在轉回客戶端。客戶端操作Codis和直接操作Redis幾乎沒有區別,Codis是無狀態的,它只是一個轉發代理中間件,我們可以啟動多個Codis節點來提升集群吞吐量。Codis節點主要負責將特定key轉發到特定的Redis實例,默認情況下Codis將所有key劃分為1024個槽位,它首先對客戶端傳過來的key通過crc32算法進行hash,再將hash后的整數值對1024取模,得到的余數就是key的槽位。每個槽位都會被映射到一個redis實例中,而這份映射關系就是由Codis維護的,為了保證映射關系數據在多個Codis節點中的一致性,Codis使用zookeeper來管理映射關系數據。
    Codis在擴容時,會遍歷所有key,然后對需要遷移的key數據逐個遷移到新節點。在擴容過程中,如果剛好有請求打到正在遷移的槽位上,此時Codis無法確定相應的key到底在哪個實例中,此時Codis會立刻強制對當前的key進行遷移,然后再將請求發到新的redis實例上。當增加新的redis實例時,Codis提供了自動均衡功能。
    Codis在設計上比Redis Cluster簡單很多,他將分布式問題交給了Zookeeper處理,省去了分布式一致性代碼的編寫維護工作。不過因為增加了Proxy作為中轉層,所以會稍微增加一定的網絡開銷,同時因為不是Redis官方集群方案,所以對Redis新功能的支持都比較慢。
    在這里插入圖片描述
    Redis Cluster集群方案:
    Redis Cluster采取去中心的集群方案,每個節點負責集群中的一部分數據,節點間通過流言協議交換信息。Redis Cluster將所有數據劃分為16384個槽位,每個Redis節點負責一部分槽位,每個redis節點都會保存所有槽位和節點的映射關系,并通過流言協議實時更新映射關系。Redis Cluster會對key值通過crc16算法進行hash,然后將得到的整數值和16384進行取模,得到的余數就是具體的槽位。Redis客戶端內同樣會保存槽位和節點的映射關系,但可能存在槽位信息不一致的情況,此時需要使用糾正機制來進行槽位信息的調整,當客戶端向一個錯誤的節點發出了指令后,節點發現指令key所在槽位不歸自己管理,就會向客戶端發送特殊的跳轉指令,告訴客戶端去連接其他節點,同時客戶端也會更新自己的映射關系數據。
    Redis Cluster對遷移中的槽位進行請求時,相應的指令會先被發送到舊槽位節點,如果舊節點存在數據就直接返回,如果不存在數據,節點會通知客戶端去新槽位節點嘗試獲取數據。Redis Cluster可以為每個主節點設置從節點來保證高可用,如果某個主節點沒有從節點,那么當它發生故障時,集群將會處于完全不可用的狀態。
    Redis Cluster無需像Codis一樣設置中間代理層,能夠減少網絡開銷,但是Redis Cluster內部實現非常復雜,為了實現去中心化,它混合使用了Raft協議和流言Cossip協議,同時還需要進行大量配置參數的調優,如果對Redis Cluster內部實現沒有充分了解,發生故障時將會非常難以處理。
    在這里插入圖片描述
  7. mybatis的二級緩存?
    在mybatis中提供了二級緩存,這個二級緩存的實現原理和舊版本mysql內部緩存的實現原理很相近,不過在當前的開發中基本都不被使用,實際開發中緩存功能由業務邏輯通過redis來實現更合適更高效。
    (1)一級緩存:在mybatis中一級緩存主要是sqlSession會話級別,它僅僅對一次會話中的數據進行緩存。在某個會話內執行SQL語句時,首次執行它會將從數據庫獲取的數據存儲在一段高速緩存中,今后執行這條語句時就會從高速緩存中讀取結果,而不是再次查詢數據庫,不過隨著會話的結束,相應的緩存也會失效。當存在多個sqlSession時,sqlSessionA進行的update操作只會更新sqlSessionA的緩存,而sqlSessionB內的緩存無法被更新,會導致臟數據的產生。mybatis的一級緩存默認是開啟的,不過在Spring中使用的mybatis一般每次會話都執行一條語句,所以即使開啟了一級緩存實際也用不到相應的緩存數據,同樣不存在臟數據問題。
    (2)二級緩存:mybatis二級緩存的開啟需要在SQL映射文件中添加一行<cache/>,二級緩存是Mapper級別的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的。同時二級緩存是事務性的,這意味著增刪改查的數據修改在SqlSession完成并提交時才會生效。另外myBatis的二級緩存不適應用于映射文件中存在聯表操作的情況,當在其他的Mapper中通過聯表操作更新了當前Mapper對應表中的數據時并不會更新緩存,這樣就會造成臟數據。另外myBatis的緩存的都是基于本地的,在分布式架構中可能會有多臺機器操作同一數據庫,這樣同樣會出現臟數據。
  8. 分布式session的實現方案?
    由于日常使用過程中,瀏覽器經常需要與web服務器進行多次交互,而Http協議本身是無狀態的,為了提高訪問的效率和用戶的體驗,一些與用戶狀態相關的信息會被保存在服務器端的Session中,而相應的會話標識SessionId則會保存在Cookie中,每次瀏覽器請求時都會帶上這個SessionId來告訴web服務器當前請求屬于哪個會話,在web服務器上每個會話都會有獨立的存儲來保存不同的會話信息。如果遇到禁用Cookie的情況,相應的SessionId會被放到url參數中。而隨著服務的發展,當web服務器從單臺演變為多臺的集群方案,那么Session數據的存放就成了問題:Http請求無法確認相應的Session數據具體存放在哪臺機器上?為了解決這個問題出現了以下幾種方案。
    (1)Session Sticky:通過負載均衡器將特定的請求轉發到固定的機器上來保證訪問到正確的Session。采用這種方案在某臺機器重啟或者宕機時就會導致部分用戶的Session數據丟失,如果Session數據中保存了用戶的登錄信息,那么用戶就需要重新登錄了。另外負載均衡器需要解析應用層的請求信息,需要耗費一定的性能,同時也需要保存特定請求到特定機器的映射關系,會損耗一定的內存,同時在容災設計等反面會帶來一定的麻煩。
    (2)Session Replication:web服務器間進行Session數據的主動同步,保證集群中的每臺機器上都有相應的Session數據。這種方案明顯會增加集群中網絡帶寬的壓力,另外當需要存儲的Session特別多,同時集群中的機器數也特別多時,就會造成大量內存空間和網絡帶寬的浪費。
    (3)Session數據集中存儲:通過特定的分布式存儲系統統一保存Session數據,在需要使用時由web服務請在分布式存儲系統中獲取。例如可以用Memcache或者redis進行Session數據數據的保存,不過這種方式會增加一些網絡請求,所以存在一定的時延和不穩定性,但目前是最通用的Session數據問題解決方案。
    (4)Cookie Based:在Cookie中保存特定的Session數據,然后在請求時將Session數據傳遞給web服務器,這種方案會導致請求數據包增大很多,進而增大帶寬消耗,同時也會存在數據安全的風險問題。
  9. Hystrix熔斷組件的使用?
    在分布式系統中,服務間通常會有很多依賴,在高并發訪問下,這些依賴的穩定性對系統影響非常大,但是依賴有很多不可控問題:如網絡連接緩慢、資源繁忙、暫時不可用、服務脫機等。例如:當某個服務的某個接口依賴的數據庫出了問題,數據庫查詢時間從原來的100ms變為5s,在接口Qps維持不變的情況下,很快大部分的Tomcat線程就會被故障接口阻塞占用,這會導致其他接口的請求無法處理,進而導致其他接口響應時間變長,大量的請求堆積無法處理,同時也會造成大量系統資源的浪費,最后故障蔓延致使整個服務不可用。
    在這里插入圖片描述
    Hystrix使用命令模式將依賴調用邏輯(請求)封裝為HystrixCommand,每個命令在單獨線程中/信號量授權下執行。可以配置超時時間,超時時間一般設為比TP99平均時間略高即可,當調用超時時,可以直接返回或執行fallback邏輯。每個依賴提供一個小的線程池(或信號量),如果線程池已滿,調用將被立即拒絕,默認使用同步隊列(即不存儲請求),加速失敗判定時間,使用同步隊列能夠避免線程上線文切換導致的資源消耗。請求執行失敗(異常,拒絕,超時,短路)時可以執行特定的fallback(降級)邏輯。Hystrix提供了斷路器組件,可以自動運行或手動調用,Hystrix通過滑動窗口監控HystrixCommand執行情況,當HystrixCommand數量達到閾值(默認20),且失敗比率超過閾值(50%)時,就會開啟斷路器,停止服務一段時間(10秒),服務暫停期間,所有請求直接執行特定的fallback(降級)邏輯,之后會允許部分請求進入,如果能夠正常處理,就會關閉斷路器。
    在這里插入圖片描述Hystrix使用兩種隔離方式:線程池隔離和信號量隔離,來限制依賴的并發量和防止阻塞擴散。(1)線程池隔離:把執行依賴代碼的線程與請求線程(如:jetty線程)分離,請求線程可以自由控制離開的時間(異步過程)。 通過線程池大小可以控制并發量,當線程池飽和時可以提前拒絕服務,防止依賴問題擴散。(2)信號量隔離:信號量隔離也可以用于限制并發訪問,防止阻塞擴散,與線程隔離最大不同在于執行依賴代碼的線程依然是請求線程(該線程需要通過信號量申請)。
    Hystrix使用問題:(1)熔斷使用位置比較難確定:有人會將熔斷邏輯加在RPC客戶端層,但需要根據依賴接口設置熔斷超時時間,由于無法感知依賴接口變更,可能會出現依賴接口修改導致響應時間大幅增加,進而導致大量接口超時觸發斷路器開啟。大部分情況下,大家喜歡將熔斷邏輯加在Controller層,但是很多人會將熔斷邏輯加在整個Controller上,一個Controller下一般會有很多接口,當某個接口發生問題時,會導致Controller下所有的接口都不可用,隔離的效果很不好。個人覺得最好的方式是將熔斷邏輯加在接口上,為接口設置小線程池實現接口間的隔離,將故障隔離在單個接口維度。(2)熔斷超時時間不好設置:需要根據接口的TP99設置熔斷超時間,如果監控的TP99數據不準確,可能會導致斷路器頻繁觸發。另外每次修改接口內部邏輯,都可能會導致接口響應時間變化,需要及時更改熔斷超時時間。除此之外很多服務的集群只有兩臺機器,在發布期間,其中一臺機器在重啟時,請求量全部都打在另一臺機器上,可能會導致服務器響應時間變長,同樣可能導致斷路器開啟。(3)隔離線程池的線程數量很難設置:為了防止出現大量上下文切換,Hystrix在隔離線程池中使用的是同步隊列,這樣在設置線程池內線程數量時,必須要根據接口Qps設置足夠的線程數,才能夠保證請求被正常處理,需要研發根據監控數據手動計算滿足要求的線程池線程數量。

(八)其他知識點(done)

  1. http和https的區別?https如何保證數據安全?
    HTTPS全稱HTTP over SSL,簡單的說就是在之前的HTTP傳輸上增加了SSL協議的加密能力,SSL全稱安全套接字層,工作于傳輸層和應用層之間,為應用提供數據的加密傳輸。在普通的數據傳輸中,我們通常會在客戶端和服務器端使用對稱協議進行加密,即兩端使用一個相同的秘鑰來對傳輸的數據進行加密和解密。但是由于秘鑰同樣需要在網絡上進行傳輸,我們仍然需要對秘鑰進行保護,為了解決這個問題,我們可以使用非對稱加密,就是我們常見的RSA算法,該算法能夠生成公鑰和私鑰,公鑰任何人都可見,用來對信息加密,而私鑰用來對加密信息進行解密,這種非對稱加密算法,加密和解密的耗時都比較長,所以只適合對少量的數據進行加密傳輸,就剛好可以用來進行對稱加密算法的秘鑰傳輸。有了這種加密方式后,整個傳輸流程看起來安全多了,但是如果在端對端的傳輸過程中,某一端的傳輸數據被黑客劫持,那么黑客同樣可以自己生成公鑰,并和另一端進行數據傳輸,為了解決這個問題,在使用HTTPS進行數據傳輸時,需要提供CA辦法的數字證書來證明傳輸者的身份。
  2. TCP與UDP的區別?
    UDP只是在IP數據包上增加了端口等部分信息,是面向無連接的,是不可靠傳輸,多用于視頻通信,電話會議等。與之相反,TCP是面向連接的,是一種端到端間通過失敗重試機制建立的可靠數據傳輸方式,就像是一條一條固定的道路承載著數據的可靠傳輸。
  3. Java泛型原理?
    泛型的本質是類型參數化,可以解決不確定具體對象類型的問題,如果不使用泛型而使用Object,則在使用不當的情況下可能出現類型轉換異常,泛型實際上主要是為了在編譯器進行類型檢查,保證程序員使用泛型時能夠安全的存儲數據和使用數據,在完成編譯泛型類型就會被擦除,使用泛型的好處:保證類型安全、提升代碼的可讀性。泛型使用時,約定俗成的符號包括:E代表Element,用于集合中的元素;T代表the Type of object,表示某個類;K代表Key,V代表Value,用于鍵值對元素。當List和泛型相結合時,可以把泛型的功能發揮到極致。List完全沒有類型限制和賦值限制,如果隨意使用,可能會造成類型轉換異常。List<Object>并不完全等同于List,List<Object>不能接受其他泛型賦值,比如List<Object>不能接受List<Integer>類型變量的賦值,而List卻可以。類型List<?>表示接受任何類型的集合賦值,但是賦值之后就不能在隨意添加元素了,但仍可以執行remove和clear操作。<? extends T>表示T及T的子類集合,<? super T>表示T及T的父類集合。<? extends T>除了null外不能add任何元素,這主要是因為T及其子類類型很多,無法匹配添加元素的類型,<? extends T>可以進行get操作,但是get的元素類型都是T及其父類。<? super T>可以添加元素,但是只能添加T及T子類的對象,這主要是因為滿足了上轉型,<? super T>在進行get操作時,雖然能夠返回對象,但是只能返回Object類型對象。
  4. java異常分類?
    在java中Throwable是所有異常類的父類,它有兩種類型分別為: Error(錯誤)和Exception(異常)。其中Error為程序無法處理的異常,一般都是嚴重故障,例如OOM異常、死鎖異常等等。而Exception又可分為unchecked exception(RuntimeException)和checked exception。其中checked exception,系統在編譯期間就會檢查此類異常,并且要求必須進行顯示的處理,而unchecked exception不會進行編譯器檢查。我認為checked exception和unchecked exception的區別在于異常的可預測性,程序中無法預測非預期的異常即為unchecked exception,例如程序中的代碼錯誤導致的數組越界異常以及服務調用超時等異常,并不是我們預期的程序執行結果,這些我們無法預測,也不是我們預期的,出現這種異常需要我們根據錯誤信息去處理修正,否則程序的運行結果也不是我們預期的。而checked exception是可預測的,我們預期會出現的情況,例如我們查詢騎士信息,而騎士信息可能不存在,這種異常是預期內的異常,是可預測的,這種異常即為checked exception,我們可以定義特定的錯誤碼和錯誤信息傳遞給調用方,以保證程序的后續流程的順利執行。
    我們通常會使用try catch finally來進行異常的處理,finally中的代碼即使發生OOM這種Error錯誤也會執行,通常用于處理善后清理工作,如果finally代碼沒有執行,那么可能是程序沒執行到代碼塊,或者try中代碼進入了死循環,或者在try中執行了System.exit()操作。在無異常的操作流程中,一般生成的finally字節碼會直接在try內代碼生成的字節碼后面執行。但是當finally中進行了變量操作時,try操作內的字節碼執行后,系統會將return結果暫時緩存起來,在finally執行完成后再取出返回,所以即使finally中對返回結果變量進行了操作也不會對最終結果造成影響。但是假如在finally中執行了return操作,那么方法就會直接結束,但是try內的操作已經執行了,在finally中執行return操作和執行邊變量操作都是不規范的代碼行為。
  5. java中的反射和內省?
    java反射指的是在運行狀態中,對于任何一個類都能夠知道這個類的屬性和方法,對于任何一個對象都能夠調用它的任意方法和獲取它的任意屬性。java反射主要提供以下功能:(1)在運行時判斷任意一個對象所屬的類。(2)在運行時構造任意一個類的對象。(3)運行時判斷任意一個類所具有的成員變量和方法。(4)在運行時調用任意一個對象的方法生成動態代理。
    java內省主要針對的是Bean類屬性的,主要是利用Bean的getXXX方法和setXXX方法,設置屬性值或者獲取屬性值。一般的做法是通過類Introspector的getBeanInfo方法來獲取某個對象的BeanInfo信息,然后通過BeanInfo來獲取屬性的描述器(PropertyDescriptor),通過這個屬性描述器就可以獲取某個屬性對應的getter/setter方法,然后我們就可以通過反射機制來調用這些方法,使用內省會比反射的性能更好。
  6. Linux常用命令及CPU消耗分析方法?
    (1)cat命令:可以顯示整個文件、可以創建文件、可以將幾個文件合并。
    (2)tail命令:用于顯示指定文件末尾內容。
    (3)grep命令:是強大的文本搜索命令,支持全局正則表達式搜索。grep 的工作方式是這樣的:它在一個或多個文件中搜索字符串模板。如果模板包括空格,則必須被引用,模板后的所有字符串被看作文件名。搜索的結果被送到標準輸出,不影響原文件內容。
    (4)ps命令:一次性查看當前進程的運行狀況,ps -ef:用于顯示當前所有進程環境變量及進程間關系,ps -aux | grep apache:與grep聯用查找某進程。
    (5)free命令:顯示系統內存使用情況,包括物理內存、交互區內存(swap)和內核緩沖區內存。
    (6)top命令:顯示當前系統正在執行進程的相關信息,包括進程ID、內存利用率、cpu占用率等。top命令與cpu相關的主要有幾個指標:1.us:表示用戶進程執行占用cpu的百分比,造成us高的可能原因是線程正在執行無阻塞的循環、正則、或純粹的計算等操作,另外頻繁的GC也會造成us的飚高。可以首先通過ps命令查詢到目標進程id,然后通過top命令查看進程的cpu利用率,通過shift+h可以看出全部線程的cpu利用率,之后利用相應的id就可以轉化出十六進制的線程nid,在之后通過jstack導出進程內所有線程的堆棧信息,最后可以通過線程的nid找到特定的線程查看堆棧信息。當程序中存在一些循環掃描任務集的線程時,應該增加一定的sleep操作來避免消耗過多的cpu,還有一種經典的場景是狀態的掃描,例如某線程要等其他線程執行完成后才能繼續執行,這種情況可以使用wait/notify策略。
    2.sy:表示內核狀態占用百分比,sy的飚高主要原因是線程頻繁的上線文切換,造成這種情況主要有三個原因:(1)線程的數量過多,并不是系統中的線程數越多,系統的吞吐量就越大,應該為系統設置合理的線程。(2)線程間鎖的競爭非常激烈,導致頻繁的上線文切換。這種情況需要我們合理的處理鎖的使用,盡量減少鎖的范圍,保證鎖的快進快出。除此之外可以根據鎖內不同功能變量對鎖進行鎖分解,根據讀和寫的場景分別使用讀寫鎖。另外可以根據鎖內的獨立變量對鎖進行鎖分段,比如java8前的concurrentHashMap就會將鎖分段為16個來減少鎖的沖突。(3)還有一種情況就是系統中存在大量的同步阻塞操作會導致大量的線程資源浪費,例如同步文件IO、同步網絡IO、sleep操作等,這些操作會導致線程掛起,但是線程并不會釋放資源,進而就導致系統需要生成更多的線程去處理其他事務,可以通過協程來解決這種問題。
    3.id:表示cpu空閑所占的百分比。
    4.wa:表示執行過程中等待IO所占的百分比。
    5.hi:表示硬件中斷所占的百分比,例如網卡接受數據頻繁的狀況。
  7. 線程間的通信方式?
    (1)基于共享容器協同:比如在生產消費模型中,我們便可以使用阻塞隊列來實現線程間的協同。
    (2)基于事件協同:當線程間存在狀態依賴時,就需要相應的工具來完成線程間同步,比如LockSupport.park、Thread.join、countDownLatch、CyclicBarrier等等。
    (3)基于進程內的共享內存:可以通過共享的進程內的內存空間進行數據傳輸,但是需要特定的鎖或同步工具保證數據的線程安全。
  8. 進程間的通信方式?
    (1)管道:它是半雙工的,數據只能在一個方向上移動,具有固定的讀端和寫端,它只能用于具有親緣關系的進程間通信(父子進程或兄弟進程),它可以使用read、write函數,但是它不是普通的文件,且只存在于內存中。
    (2)命名管道:它是一種文件系統,可以在無關的進程間交換數據,但是速度比較慢,這種文件系統也具有管道的特性,數據先進先出,數據被讀取后也會被清除。
    (3)消息隊列:消息隊列是放在內核中的消息鏈表,一個消息隊列由一個標識符來標識。消息隊列內的記錄有特定格式和優先級,消息可以獨立于發送進程和接受進程,除了支持先進先出的查詢,同樣支持隨機查詢,但是能夠存儲的數據量比較少。
    (4)信號量:信號量的實現基于操作系統的PV操作,程序對信號量的操作都是原子操作,它主要實現的是進程間的互斥和同步,并不能實現進程間的數據傳輸。
    (5)共享內存:多個進程同時共享某塊內存區域,是最快的進程間交互方式,但是需要配合信號量一起使用以保證數據的安全,但因為進程是資源獨立的,多進程內存共享的代價比多線程大的多,會涉及序列化和反序列化的開銷。
  9. 單例模式Singleton介紹?
    Spring Bean的生命周期有Singleton類型,雖然和單例模式名字相同,但兩者語義是不同的:Singleton類型的bean是由容器來保證這種類型bean在容器中只存在一個,而單例模式則是保證在同一個ClassLoader中只存在一個這種類型的實例。單例模式主要有以下幾種實現方式:
    (1)懶漢模式:
    在這里插入圖片描述
    (2)餓漢模式:
    在這里插入圖片描述
    (3)雙重檢測鎖定(Double Check Lock)
    在這里插入圖片描述
    (4)靜態內部類單例模式
    在這里插入圖片描述
    (5)枚舉單例模式
    在這里插入圖片描述
  10. 工廠模式介紹?
    工廠模式可以把對象的創建和使用過程解耦開來,同時可以減少重復代碼,并且降低代碼的維護成本。工廠模式主要分為:簡單工廠模式、工廠方法模式、抽象工廠模式。
    (1)簡單工廠模式:
    在這里插入圖片描述
    在這里插入圖片描述
    (2)工廠方法模式:
    在這里插入圖片描述
    (3)抽象工廠模式:
    在這里插入圖片描述
    在這里插入圖片描述
  11. 使用python和java進行后端開發的優缺點?
    java是半編譯半解釋型的強類型靜態語言,而python是解釋型的強類型動態語言。java需要事先給變量進行數據類型定義,而python在運行時才會進行數據類型檢查,所以python是動態語言而java是靜態語言,不過動態語言無法通過編譯器檢查變量類型,會增加代碼開發復雜度,在使用python開發時就經常會出現類型錯誤的問題。強類型語言在變量類型確定后,就不能夠再隨意改變變量類型(除了強轉外),java和python都是強類型語言。
    使用python作后端開發語言,編碼難度不高,功能實現速度快。但是相對于java而言,使用python進行后端開發卻存在兩個大問題:(1)性能不佳:單機python后端應用能夠支撐的Qps比較低。一個比較重要的原因是python默認實現是單線程,對多核CPU資源利用不夠充分,通過使用異步tornado等框架能夠提升些性能,但是效果依然不佳。部署同樣功能的后端服務,python應用使用的機器數可能比java應用使用的機器數多很多。(2)跨平臺性比較差:一般使用python需要很多擴展組件,但是很多組件平臺兼容性不好。當使用windows電腦進行python開發時就會非常麻煩,需要在電腦內安裝linux虛擬機,在虛擬機中進行開發,跨平臺性完全沒法和java比。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450900.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450900.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450900.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

無法連接虛擬設備ide1:0,主機上沒有相對應的設備... 解決

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 運行虛擬機出現報錯&#xff1a; 無法連接虛擬設備ide1:0&#xff0c;主機上沒有相對應的設備&#xff0c;您 要在每次開啟此虛擬機時都…

繳滿15年能領多少錢 養老金計算公式網上瘋傳

社保人員稱我省計算方式與各設區市平均工資掛鉤&#xff0c;與網上不同 最近&#xff0c;關于“延遲退休”引起各方高度關注&#xff0c;成為廣大居民十分關心的話題。是否延遲退休尚無定論&#xff0c;但在網上有不少關于養老金的計算。那網上流傳的計算方法是否科學&#xff…

48_并發編程-線程-資源共享/鎖

一、數據共享多個線程內部有自己的數據棧&#xff0c;數據不共享&#xff1b;全局變量在多個線程之間是共享的。1 # 線程數據共享不安全加鎖2 3 import time4 from threading import Thread, Lock5 6 7 num 1008 9 def func(t_lock): 10 global num 11 t_lock.acquire…

移動硬盤提示無法訪問設備硬件出現致命錯誤,導致請求失敗的資料尋回方案

J盤打不開設備硬件出現致命錯誤,導致請求失敗&#xff0c;是因為這個I盤的文件系統內部結構損壞導致的。要恢復里面的數據就必須要注意&#xff0c;這個盤不能格式化&#xff0c;否則數據會進一步損壞。具體的恢復方法看正文 工具/軟件&#xff1a;星空數據恢復軟件 步驟1&…

VMware10上新建虛擬機步驟圖解

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 第一種 : 自定義方式&#xff1a; 安裝虛擬機的過程步驟&#xff0c;基本上過程的每一步都有截圖&#xff0c;跟著過程就可以很容易的創…

怎么理解 IaaS、SaaS 和 PaaS 的區別?

原文鏈接&#xff1a;怎么理解 IaaS、SaaS 和 PaaS 的區別&#xff1f; 一、定義層面的區別 SaaS、PaaS、IaaS簡單的說都屬于云計算服務&#xff0c;也就是云計算服務。我們對于云計算的概念&#xff0c;維基百科有以下定義&#xff1a; Cloud computing is a new form of In…

三星“打法”:先模仿對手 再吃掉對手

臺灣地區電子業者將三星視為“臺灣公敵”&#xff0c;事實上&#xff0c;它幾乎是全球電子業者的敵人。 這家韓國電子業巨頭十年之間奪取了日本企業在這一領域中縱橫30年的榮光&#xff0c;更是建立起了令人嘆為觀止的垂直整合帝國。 韓國政府的大力支持、日元升值韓元貶值等均…

SharpZipLib 壓縮ZIP導出

1      var uploadSectionDir Path.Combine("Upload", "QQ", DateTime.Now.ToString("yyyyMMdd"));2 string uploadDir Path.Combine(HttpRuntime.AppDomainAppPath, uploadSectionDir);3 if (!Directory.Exi…

java動態調用c++庫

前言 最近在做一個通過java程序調用c動態語言庫&#xff0c;在網上百度&#xff0c;谷歌找了諸多例子&#xff0c;還是屢試不爽。經過一番折騰還是披荊斬棘&#xff0c;創出一條道路。希望分享給正在迷茫的朋友們... 使用的環境 spring boot gradle JNI介紹 JNI全拼是Java Nat…

如何刪除虛擬機上的操作系統、刪除新建的虛擬機

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 打開VMware&#xff0c;我安裝了三個虛擬系統&#xff0c;要對win98進行刪除&#xff0c;從磁盤上刪除~~ 2、雙擊你要刪除的系統&#xf…

什么是QoS技術

QoS&#xff08;Quality of Service&#xff09;是服務質量的簡稱。從傳統意義上來講&#xff0c;無非就是傳輸的帶寬、傳送的時延、數據的丟包率等&#xff0c;而提高服務質量無非也就是保證傳輸的帶寬&#xff0c;降低傳送的時延&#xff0c;降低數據的丟包率以及時延抖動等。…

一套完整的用戶增長系統架構

互聯網的世界里一切都是為了增長&#xff0c;靈光一現的創新可能會讓一個產品成功&#xff0c;但絕不可能長久。 在用戶增長的領域里&#xff0c;如何復用一套框架&#xff0c;找到最佳實踐的一條路徑&#xff0c;再配備一點運氣&#xff0c;去實現商業成功是我一直所探索的話題…

編譯性語言、解釋性語言和腳本語言

什么是編譯性語言、解釋性語言和腳本語言 計算機不能直接理解高級語言&#xff0c;只能直接理解機器語言&#xff0c;所以必須要把高級語言翻譯成機器語言&#xff0c;計算機才能值型高級語言編寫的程序。  翻譯的方式有兩種&#xff0c;一個是編譯&#xff0c;一個是解釋。…

對多租戶的理解

一、 多租戶定義 多租戶定義&#xff1a; 多租戶技術或稱多重租賃技術&#xff0c;簡稱SaaS&#xff0c;是一種軟件架構技術&#xff0c;是實現如何在多用戶環境下&#xff08;此處的多用戶一般是面向企業用戶&#xff09;共用相同的系統或程序組件&#xff0c;并且可確保各用…

查看VMware上虛擬機的 ip 地址

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. 開啟虛擬機&#xff1a; 2.輸入賬號密碼登陸到虛擬機中 3. 選擇 xxx Home 右鍵---- Open in Terinal 進入命令行頁面 ----- 輸入命令…

Hibernate之表間關系

ManyToOne 多對一&#xff0c;是最常見的表間關系&#xff0c;對應關系數據庫中的外鍵關系。通常用于建立子實體和其父實體的關聯關系 Entity(name "Person") public static class Person {IdGeneratedValueprivate Long id;//Getters and setters are omitted for …

Python大神告訴你,學習Python應該讀哪些書!

關注頭條號&#xff0c;私信回復資料會有意外驚喜呦………………最后一張照片有資料呦。在傳統的Web開發之外的領域&#xff0c;Python開發人員的就業機會越來越多&#xff0c;無論你是初學者還是大神&#xff0c;現在正是投入到Python學習的好時機。一個IBM的博客文章報道了如…

腳本語言

腳本語言&#xff08;Script language&#xff0c;scripting language&#xff0c;scripting programming language&#xff09;是為了縮短傳統的編寫-編譯-鏈接-運行&#xff08;edit-compile-link-run&#xff09;過程而創建的計算機編程語言。此命名起源于一個腳本“screenp…

Java Agent

一、什么是 Java Agent &#xff1f; 籠統地來講&#xff0c;Java Agent 是一個統稱&#xff0c;該功能是 Java 虛擬機提供的一整套后門。通過這套后門可以對虛擬機方方面面進行監控與分析。甚至干預虛擬機的運行。 Java Agent 又叫做 Java 探針&#xff0c;Java Agent 是在 …

JDK 1.8 官網下載地址(linux / windows)

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 JDK 1.8 官網下載地址&#xff1a; JDK 1.8 官網下載地址&#xff08;linuxwindows&#xff09; 上面連接可以直接點擊&#xff0c;連接…