java面試題彙總

試題列表:html

 1:如何避免對外開放接口被攻擊,有哪些經常使用的防禦手段能夠用上?前端

  ------------------------------------------------------------------------------------------------------------------------java

阿里面試題:mysql

(1):ThreadLocal 以及使用場景nginx

(2):BIO、 NIOgit

(3):使用說明RPC , duboo使用說明方式進行通訊,講講Netty線程模型程序員

(4):Tcp粘包黏包 github

(5):使用說明消息隊列,你用到的rabbitMQ 中消息是按順序的嗎?web

(6):數據庫分庫分表 面試

(7):線程池知識

(8):volatile關鍵字,volatile 是原子性嗎?

(9):redis爲何能支持高併發,redis數據持久化

(10):工做中遇到哪些技術挑戰

(11):對本身將來有什麼指望

 ------------------------------------------------------------------------------------------------------------------------

其餘公司面試題:

(1):HashMap(底層數據結構、初始化大小、擴容)爲何不是線程安全的,舉個例子或者那個操做會致使線程不安全 

(2):線上機器頻繁FullGC 

(3):用戶訪問網站愈來愈慢,怎麼排查緣由 

(4):springMVC 流程

(5):spring IOC AOP

(6):Spring bean 是線程安全的嗎 

(7):Spring事務隔離級別  事務隔離機制

(8):單例模式

(9):數據庫優化、數據庫索引優化 

(10):數據庫索引會失效嗎?什麼狀況下會失效

(11):死鎖是怎麼發生的

(12):緩存穿透 、如何解決?

(13):分佈式鎖  怎麼釋放鎖? 鎖的失效時間怎麼設定? 若是業務執行時間很快 超過鎖的失效時間 提早釋放鎖 會怎麼樣,或者業務執行時間大於緩存失效時間怎麼樣? 

(14):消息隊列:如何進行消息可靠性,以及消息的幕等性(即消息不被重複消費) 

(15):高併發下的接口冪等性  

(16):springboot使用、springCloud和dubbo有什麼區別?

(17):hibernate ,mybatis區別

(18):mybatis中的 #和$有什麼區別?

(19):緩存與數據庫一致性如何保證?緩存和數據庫誰先更新。

(20):StringBuilder爲何線程不安全

(21):分佈式事務是怎麼處理的?

(22):數據庫查詢,where條件是大的數據放在前面仍是放在後面?

(23):數據庫,是小表驅動大表,仍是大表驅動小表?

(24):Spring框架是如何解決bean的循環依賴問題?

 ------------------------------------------------------------------------------------------------------------------------

網絡上的面試題

(1)java線程中,調用start()方法就會執行run()方法,爲何咱們不能直接調用run()方法?

答: new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。

  總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。

 

 

 

 

 ------------------------------------------------------------------------------------------------------------------------

解答:

1:如何避免對外開放接口被攻擊,有哪些經常使用的防禦手段能夠用上?

  服務端對外開放API接口,尤爲對移動應用開放接口的時候,更須要關注接口安全性的問題,要確保應用APP與API之間的安全通訊,防止數據被惡意篡改等攻擊。 

Token機制

  開放接口時最基本須要考慮到接口不該該被別人隨意訪問,而我也不能隨意訪問到其餘用戶的數據,從而保證用戶與用戶之間的數據隔離。這個時候咱們就有必要引入Token機制了。
具體的作法: 在用戶成功登陸時,系統能夠返回客戶端一個Token,後續客戶端調用服務端的接口,都須要帶上Token,而服務端須要校驗客戶端Token的合法性。Token不一致的狀況下,服務端須要攔截該請求。

數據校驗

  服務端從某種層面來講須要驗證接受到數據是否和客戶端發來的數據是否一致,要驗證數據在傳輸過程當中有沒有被注入攻擊。這時候客戶端和服務端就有必要作簽名和驗籤。具體作法:
  客戶端對全部請求服務端接口參數作加密生成簽名,並將簽名做爲請求參數一併傳到服務端,服務端接受到請求同時要作驗籤的操做,對稱加密對請求參數生成簽名,並與客戶端傳過來的簽名進行比對,如簽名不一致,服務端須要攔截該請求

過載保護 

  服務端仍然須要識別一些惡意請求,防止接口被一些喪心病狂的人玩壞。對接口訪問頻率設置必定閾值,對超過閾值的請求進行屏蔽及預警。

  異常封裝:服務端須要構建異常統一處理框架,將服務可能出現的異常作統一封裝,返回固定的code與msg,防止程序堆棧信息暴露。

  其它小手段例如:

  (1)圖形驗證碼

  (2)短信發送間隔設置:設置同一號碼重複發送的時間間隔,通常設置爲60-120秒;

  (3)IP限定:置每一個IP天天的最大發送量;

  (4)發送量限定:設置每一個手機號碼天天的最大發送量;

HTTPS  

  HTTPS可以有效防止中間人攻擊,有效保證接口不被劫持,對數據竊取篡改作了安全防範。但HTTP升級HTTPS會帶來更多的握手,而握手中的運算會帶來更多的性能消耗。這也是不得不考慮的問題。

  

  總得來講,咱們很是有必要在設計接口的同時考慮安全性的問題,根據業務特色,採用的安全策略也不全相同。固然大多數安全策略更多的都是提升安全門檻,並不能保證100%的安全,但該作的仍是不能少。

 ------------------------------------------------------------------------------------------------------------------------

阿里面試:

(1):ThreadLocal 以及使用場景

   連接: ThreadLocal類詳解

 

(2):BIO、 NIO

 

 

(3):使用說明RPC , duboo使用說明方式進行通訊,講講Netty線程模型

 

 

(4):Tcp粘包黏包 

 

 

(5):使用說明消息隊列,你用到的rabbitMQ 中消息是按順序的嗎?

 

 

(6):數據庫分庫分表 

   連接: 分庫分表?如何作到永不遷移數據和避免熱點?

 

(7):線程池知識

   連接: java 線程池 - ThreadPoolExecutor

   連接: java線程池實現原理

 

(8):volatile關鍵字,volatile 是原子性嗎?

咱們知道對於可見性,Java提供了volatile關鍵字來保證可見性有序性但不保證原子性
普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。


  背景:爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。

  • 若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。
  • 在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。

總結下來

  • 第一:使用volatile關鍵字會強制將修改的值當即寫入主存;
  • 第二:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
  • 第三:因爲線程1的工做內存中緩存變量的緩存行無效,因此線程1再次讀取變量的值時會去主存讀取。

最重要的是

    • 可見性:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。
    • 原子性:對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。

舉2個例子:

/線程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//線程2
stop = true;

原文:這段代碼是很典型的一段代碼,不少人在中斷線程時可能都會採用這種標記辦法。可是事實上,這段代碼會徹底運行正確麼?即必定會將線程中斷麼?不必定,也許在大多數時候,這個代碼可以把線程中斷,可是也有可能會致使沒法中斷線程(雖然這個可能性很小,可是隻要一旦發生這種狀況就會形成死循環了)。
  下面解釋一下這段代碼爲什麼有可能致使沒法中斷線程。在前面已經解釋過,每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。
  那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。
  可是用volatile修飾以後就變得不同了:
  第一:使用volatile關鍵字會強制將修改的值當即寫入主存;
  第二:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
  第三:因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。
到這裏可能看起來沒什麼問題,咱們來看例子2

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }    
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

原文:你們想一下這段程序的輸出結果是多少?也許有些朋友認爲是10000。可是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
  可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有10個線程分別進行了1000次操做,那麼最終inc的值應該是1000*10=10000。
  這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。
  在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:
  假如某個時刻變量inc的值爲10,
  線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;
  而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。
  而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。
  那麼兩個線程分別進行了一次自增操做後,inc只增長了1。
  解釋到這裏,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?而後其餘線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,可是要注意,線程1對變量進行讀取操做以後,被阻塞了的話,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值。

你們是否是有這樣的疑問:「線程1在讀取inc爲10後被阻塞了,沒有進行修改因此不會去通知其餘線程,此時線程2拿到的仍是10,這點能夠理解。可是後來線程2修改了inc變成11後寫回主內存,這下是修改了,線程1再次運行時,難道不會去主存中獲取最新的值嗎?按照volatile的定義,若是volatile修飾的變量發生了變化,其餘線程應該去主存中拿變化後的值纔對啊?」
  是否是還有:例子1中線程1先將stop=flase讀取到了工做內存中,而後去執行循環操做,線程2將stop=true寫入到主存後,爲何線程1的工做內存中stop=false會變成無效的?

其實嚴格的說,對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。在《Java併發編程的藝術》中有這一段描述:「在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。」咱們須要注意的是,這裏的修改操做,是指的一個操做

    • 例子1中,由於是while語句,線程會不斷讀取stop的值來判斷是否爲false,每一次判斷都是一個操做。這裏是從緩存中讀取。單個讀取操做是具備原子性的,因此當例子1中的線程2修改了stop時,因爲volatile變量的可見性,線程1再讀取stop時是最新的值,爲true。
    • 而例子2中,爲何自增操做會出現那樣的結果呢?能夠知道自增操做是三個原子操做組合而成的複合操做。在一個操做中,讀取了inc變量後,是不會再讀取的inc的,因此它的值仍是以前讀的10,它的下一步是自增操做。

 

(9):redis爲何能支持高併發,redis數據持久化

   連接: redis 單線程的理解

   連接:一塊兒看懂Redis兩種持久化方式的原理  

 

(10):工做中遇到哪些技術挑戰

 

 

(11):對本身將來有什麼指望

 

 ------------------------------------------------------------------------------------------------------------------------

其餘公司面試題:

(1):HashMap(底層數據結構、初始化大小、擴容)爲何不是線程安全的,舉個例子或者那個操做會致使線程不安全

 

 

(2):線上機器頻繁FullGC

 堆內存劃分爲 Eden、Survivor 和 Tenured/Old 空間,以下圖所示:

  從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC,對老年代GC稱爲Major GC,而Full GC是對整個堆來講的,在最近幾個版本的JDK裏默認包括了對永生帶即方法區的回收(JDK8中無永生帶了),出現Full GC的時候常常伴隨至少一次的Minor GC,但非絕對的。Major GC的速度通常會比Minor GC慢10倍以上。下邊看看有那種狀況觸發JVM進行Full GC及應對策略。

一、System.gc()方法的調用

  此方法的調用是建議JVM進行Full GC,雖然只是建議而非必定,但不少狀況下它會觸發 Full GC,從而增長Full GC的頻率,也即增長了間歇性停頓的次數。強烈影響系建議能不使用此方法就別使用,讓虛擬機本身去管理它的內存,可經過經過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

二、老年代代空間不足

  老年代空間只有在新生代對象轉入及建立爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出以下錯誤:
    java.lang.OutOfMemoryError: Java heap space 
  爲避免以上兩種情況引發的Full GC,調優時應儘可能作到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。

三、永生區空間不足

  JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱爲永生代或者永生區,Permanet Generation中存放的爲一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的狀況下也會執行Full GC。若是通過Full GC仍然回收不了,那麼JVM會拋出以下錯誤信息:
    java.lang.OutOfMemoryError: PermGen space 
  爲避免Perm Gen佔滿形成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。


四、CMS GC時出現promotion failed和concurrent mode failure

  對於採用CMS進行老年代GC的程序而言,尤爲要注意GC日誌中是否有promotion failed和concurrent mode failure兩種情況,當這兩種情況出現時可能會觸發Full GC。

  promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下形成的;concurrent mode failure是在執行CMS GC的過程當中同時有對象要放入老年代,而此時老年代空間不足形成的(有時候「空間不足」是CMS GC時當前的浮動垃圾過多致使暫時性的空間不足觸發Full GC)。  

  對應措施爲:增大survivor space、老年代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會因爲JDK的bug29致使CMS在remark完畢

  後好久才觸發sweeping動做。對於這種情況,可經過設置-XX: CMSMaxAbortablePrecleanTime=5(單位爲ms)來避免。


五、統計獲得的Minor GC晉升到舊生代的平均大小大於老年代的剩餘空間

  這是一個較爲複雜的觸發狀況,Hotspot爲了不因爲新生代對象晉升到舊生代致使舊生代空間不足的現象,在進行Minor GC時,作了一個判斷,若是以前統計所獲得的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。

  例如程序第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,若是小於6MB,則執行Full GC。

  當新生代採用PS GC時,方式稍有不一樣,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。

  除了以上4種情況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認狀況下會一小時執行一次Full GC。可經過在啓動時經過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或經過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

六、堆中分配很大的對象

  所謂大對象,是指須要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,可是沒法找到足夠大的連續空間來分配給當前對象,此種狀況就會觸發JVM進行Full GC。

  爲了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在「享受」完Full GC服務以後額外免費贈送一個碎片整理的過程,內存整理的過程沒法併發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。

 

 連接:  JVM — 性能調優

 

(3):用戶訪問網站愈來愈慢,怎麼排查緣由

可能緣由是:

  一、服務器出口帶寬不夠用。

  二、服務器負載過大忙不過來,沒法承擔巨大的流量。

  三、數據庫的瓶頸,數據庫文件過大,形成讀取緩慢,沒有創建索引,形成每次查詢都對數據庫進行全局查詢。

  四、沒有設置CDN。

  五、可能遭受到了分佈式拒絕攻擊即DDOS攻擊。

  六、jvm分配內存太少了。

  七、併發高了,網站太多人訪問

  八、代碼問題,對象建立太多

排查

解決辦法:

  • 1.查看線上服務器的負載狀況,CPU負載,內存負載,網絡帶寬,查看是否已通過載。

  • 2.查看網絡鏈接狀況,是否受到DDOS攻擊,消耗盡帶寬資源,形成沒法提供服務。

  • 3.查看MySQL數據庫的日誌文件,查看mysql慢查詢日誌,查看形成MySQL訪問過慢的緣由。

  • 4.能夠查看應用程序的日誌,如Apache,nginx,PHP,Tomcat日誌文件,找出報錯緣由,查看是不是代碼問題。

 

(4):springMVC 流程

(一)總體流程

具體步驟:

  一、  首先用戶發送請求到前端控制器,前端控制器根據請求信息(如 URL)來決定選擇哪個頁面控制器進行處理並把請求委託給它,即之前的控制器的控制邏輯部分;圖中的 一、2 步驟;

  二、  頁面控制器接收到請求後,進行功能處理,首先須要收集和綁定請求參數到一個對象,這個對象在 Spring Web MVC 中叫命令對象,並進行驗證,而後將命令對象委託給業務對象進行處理;處理完畢後返回一個 ModelAndView(模型數據和邏輯視圖名);圖中的 三、四、5 步驟;

  三、  前端控制器收回控制權,而後根據返回的邏輯視圖名,選擇相應的視圖進行渲染,並把模型數據傳入以便視圖渲染;圖中的步驟 六、7;

  四、  前端控制器再次收回控制權,將響應返回給用戶,圖中的步驟 8;至此整個結束。

(二)核心流程

具體步驟:

第一步:發起請求到前端控制器(DispatcherServlet)

第二步:前端控制器請求HandlerMapping查找 Handler (能夠根據xml配置、註解進行查找)

第三步:處理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping會把請求映射爲HandlerExecutionChain對象(包含一個Handler處理器(頁面控制器)對象,多個HandlerInterceptor攔截器對象),經過這種策略模式,很容易添加新的映射策略

第四步:前端控制器調用處理器適配器去執行Handler

第五步:處理器適配器HandlerAdapter將會根據適配的結果去執行Handler

第六步:Handler執行完成給適配器返回ModelAndView

第七步:處理器適配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一個底層對象,包括 Model和view)

第八步:前端控制器請求視圖解析器去進行視圖解析 (根據邏輯視圖名解析成真正的視圖(jsp)),經過這種策略很容易更換其餘視圖技術,只須要更改視圖解析器便可

第九步:視圖解析器向前端控制器返回View

第十步:前端控制器進行視圖渲染 (視圖渲染將模型數據(在ModelAndView對象中)填充到request域)

第十一步:前端控制器向用戶響應結果

 

(三)總結 核心開發步驟

一、  DispatcherServlet 在 web.xml 中的部署描述,從而攔截請求到 Spring Web MVC

二、  HandlerMapping 的配置,從而將請求映射處處理器

三、  HandlerAdapter 的配置,從而支持多種類型的處理器

注:處理器映射求和適配器使用紓解的話包含在了註解驅動中,不須要在單獨配置

四、  ViewResolver 的配置,從而將邏輯視圖名解析爲具體視圖技術

五、  處理器(頁面控制器)的配置,從而進行功能處理 

View是一個接口,實現類支持不一樣的View類型(jsp、freemarker、pdf...)

 

(5):spring IOC AOP

   連接: Spring  IOC

   連接:   Spring AOP

 

(6):Spring bean 是線程安全的嗎

  連接: http://www.javashuo.com/article/p-aebnslvp-hc.html

 

(7):Spring事務隔離級別  事務隔離機制

  連接: 數據庫的4種隔離級別

Spring事務隔離級別

事務的隔離級別

  事務隔離級別指的是一個事務對數據的修改與另外一個並行的事務的隔離程度,當多個事務同時訪問相同數據時,若是沒有采起必要的隔離機制,就可能發生如下問題:

Spring支持的隔離級別

 

  再必須強調一遍,不是事務隔離級別設置得越高越好,事務隔離級別設置得越高,意味着勢必要花手段去加鎖用以保證事務的正確性,那麼效率就要下降,所以實際開發中每每要在效率和併發正確性之間作一個取捨,通常狀況下會設置爲READ_COMMITED,此時避免了髒讀,併發性也還不錯,以後再經過一些別的手段去解決不可重複讀和幻讀的問題就行了。

Spring設置事務隔離級別


  配置文件的方式

<tx:advice id="advice" transaction-manager="transactionManager">
     <tx:attributes>
         <tx:method name="fun*" propagation="REQUIRED" isolation="DEFAULT"/>
     </tx:attributes>
 </tx:advice>

註解的方式

@Transactional(isolation=Isolation.DEFAULT)
public void fun(){
    dao.add();
    dao.udpate();
}

總結
  Spring建議的是使用DEFAULT,就是數據庫自己的隔離級別,配置好數據庫自己的隔離級別,不管在哪一個框架中讀寫數據都不用操心了。並且萬一Spring沒有把這幾種隔離級別實現的很完善,出了問題就麻煩了。

 

(8):單例模式

   連接:http://www.javashuo.com/article/p-aqenvkdb-hr.html

 

(9):數據庫優化、數據庫索引優化 

索引的優勢

  • 大大減小了服務器須要掃描的數據量
  • 能夠幫助服務器避免排序或減小使用臨時表排序
  • 索引能夠隨機I/O變爲順序I/O

索引的缺點

  • 須要佔用磁盤空間,所以冗餘低效的索引將佔用大量的磁盤空間
  • 下降DML性能,對於數據的任意增刪改都須要調整對應的索引,甚至出現索引分裂
  • 索引會產生相應的碎片,產生維護開銷

 一:數據庫優化

  1. 如何發現有問題的SQL?  使用mysql慢查詢日誌對有效率問題的Sql進行監視

(1) show  variables like 'slow_query_log';     --查看慢查詢日誌是否開啓

(2) set global slow_qeury_log_file = '/home/mysql/sql_log/mysql_slow.log'     --設置慢查詢日誌文件的位置

(3) set global log_queries_not_using_indexes = on     --把沒有使用索引的SQL存入慢查詢日誌

(4) set global long_query_time = 1        --設置時間限制,即超過這個時間的SQL就記錄到日誌中

 這裏能夠使用查看變量的方式,查看上面參數的默認值 好比:show variables like 'slow%' 能夠看到慢查詢日誌的默認存放位置

  2. 慢查詢日誌包含的內容

 3. 經常使用的慢查詢日誌分析工具

  (1) mysqldumpslow 工具(通常在安裝mysql時就已經有了) 用法: mysqldumpslow  + 參數 + 慢查詢日誌文件路徑

經常使用參數:

 -t 數字: 顯示前n條日誌       能夠使用mysqldumpslow -h  查看全部可攜帶的參數

  (2) pt-query-digest 工具

使用這個工具分析慢查詢日誌時的輸出 共有三部分:

第一部分:顯示日誌的時間範圍,總的SQL數量和不一樣的SQL數量

 第二部分:

第三部分:顯示具體的SQL語句 

 

 4.根據日誌中的指標發現有問題的SQL

  (1) 查詢次數多且每次查詢佔用時間長的SQL        一般爲pt-query-digest分析的前幾個查詢

  (2) IO大的SQL    注意pt-query-digest 分析中的Rows examine (即掃描的行數)項

  (3) 未命中索引的SQL     注意pt-query-digest 分析中Rows examine  和 Rows  Send 的對比

 

 5. 有問題的SQL被發現後,使用explain從句查詢SQL的執行計劃,explain返回的是一個表格,下面是各列的含義:

 

 5. 優化子查詢

  儘可能使用連表查詢代替子查詢

   當有重複數據時,能夠使用distinct進行去重。

 

 6. 優化limit查詢

(1) 優化方案:使用有索引的列或主鍵進行order by 操做

(2) 優化方案:記錄上次返回的主鍵,在下次查詢時使用主鍵過濾(方向就是避免掃描過多的記錄)

select film_id, description from film where  film_id > 55 and film_id <= 60 order by film_id limit 1,5

 

(10):數據庫索引會失效嗎?什麼狀況下會失效

例如:一張USER表   有字段屬性 name,age   其中name爲索引

下面列舉幾個索引失效的狀況

 1. select * from USER where name=‘xzz’ or age=16;

   例如這種狀況:當語句中帶有or的時候 即便有索引也會失效。

 

 2.select *  from  USER where name like‘%xzz’ ;

  例如這種狀況:當語句索引 like 帶%的時候索引失效(注意:若是上句爲 like‘xzz’此時索引是生效的) 

 

 3.select * from USER where name=123;(此處只是簡單作個例子,實際場景中通常name不會爲數字的)

  例如這種狀況:若是列類型是字符串,那必定要在條件中將數據使用引號引用起來,不然不使用索引

 

 4.若是mysql估計使用全表掃描要比使用索引快,則不使用索引(這個不知道咋舉例子了 ) 

 

 5.假如上述將name和age設置爲聯合索引,必定要注意順序,mysql聯合因此有最左原則,下面以name,age的順序講下

  (1)select * from USER where name=‘xzz’ and age =11;

  (2)select * from USER where age=11 and name=‘xzz’;

  例如上訴兩種狀況:以name,age順序爲聯合索引,(1)索引是生效的,(2)索引是失效的

 

 6.好比age爲索引:select * from USER where age-1>11;

  例如這種狀況:索引失效,不要在索引字段上進行表達式操做,不然索引會失效(是有相似時間轉換的問題和上訴問題同樣)

 7.where語句中使用 Not In

        

 

 看了別人寫的文章,有說「應儘可能避免在where 子句中對字段進行null 值判斷,不然將致使引擎放棄使用索引而進行全表掃描」,實測沒有全表掃描。

        

 

(11):死鎖是怎麼發生的

 

 

(12):緩存穿透 、如何解決?

 瞭解什麼是 redis 的雪崩、穿透和擊穿?redis 崩潰以後會怎麼樣?系統該如何應對這種狀況?如何處理 redis 的穿透?

緩存雪崩

  對於系統 A,假設天天高峯期每秒 5000 個請求,原本緩存在高峯期能夠扛住每秒 4000 個請求,可是緩存機器意外發生了全盤宕機。緩存掛了,此時 1 秒 5000 個請求所有落數據庫,數據庫必然扛不住,它會報一下警,而後就掛了。此時,若是沒有采用什麼特別的方案來處理這個故障,DBA 很着急,重啓數據庫,可是數據庫立馬又被新的流量給打死了。

  這就是緩存雪崩。

redis-caching-avalanche

 大約在 3 年前,國內比較知名的一個互聯網公司,曾由於緩存事故,致使雪崩,後臺系統所有崩潰,事故從當天下午持續到晚上凌晨 3~4 點,公司損失了幾千萬。

 緩存雪崩的事前事中過後的解決方案以下。

  • 事前:redis 高可用,主從+哨兵,redis cluster,避免全盤崩潰。
  • 事中:本地 ehcache 緩存 + hystrix 限流&降級,避免 MySQL 被打死。
  • 過後:redis 持久化,一旦重啓,自動從磁盤上加載數據,快速恢復緩存數據。

redis-caching-avalanche-solution

  用戶發送一個請求,系統 A 收到請求後,先查本地 ehcache 緩存,若是沒查到再查 redis。若是 ehcache 和 redis 都沒有,再查數據庫,將數據庫中的結果,寫入 ehcache 和 redis 中。

  限流組件,能夠設置每秒的請求,有多少能經過組件,剩餘的未經過的請求,怎麼辦?走降級!能夠返回一些默認的值,或者友情提示,或者空白的值。

好處:

  • 數據庫絕對不會死,限流組件確保了每秒只有多少個請求能經過。
  • 只要數據庫不死,就是說,對用戶來講,2/5 的請求都是能夠被處理的。
  • 只要有 2/5 的請求能夠被處理,就意味着你的系統沒死,對用戶來講,可能就是點擊幾回刷不出來頁面,可是多點幾回,就能夠刷出來一次。

緩存穿透

  對於系統A,假設一秒 5000 個請求,結果其中 4000 個請求是黑客發出的惡意攻擊。

  黑客發出的那 4000 個攻擊,緩存中查不到,每次你去數據庫裏查,也查不到。

  舉個栗子。數據庫 id 是從 1 開始的,結果黑客發過來的請求 id 所有都是負數。這樣的話,緩存中不會有,請求每次都「視緩存於無物」,直接查詢數據庫。這種惡意攻擊場景的緩存穿透就會直接把數據庫給打死。

redis-caching-penetration

  解決方式很簡單,每次系統 A 從數據庫中只要沒查到,就寫一個空值到緩存裏去,好比 set -999 UNKNOWN。而後設置一個過時時間,這樣的話,下次有相同的 key 來訪問的時候,在緩存失效以前,均可以直接從緩存中取數據。

緩存擊穿

  緩存擊穿,就是說某個 key 很是熱點,訪問很是頻繁,處於集中式高併發訪問的狀況,當這個 key 在失效的瞬間,大量的請求就擊穿了緩存,直接請求數據庫,就像是在一道屏障上鑿開了一個洞。

  解決方式也很簡單,能夠將熱點數據設置爲永遠不過時;或者基於 redis or zookeeper 實現互斥鎖,等待第一個請求構建完緩存以後,再釋放鎖,進而其它請求才能經過該 key 訪問數據。

 

(13):分佈式鎖  怎麼釋放鎖? 鎖的失效時間怎麼設定? 若是業務執行時間很快 超過鎖的失效時間 提早釋放鎖 會怎麼樣,或者業務執行時間大於緩存失效時間怎麼樣?

 

(14):消息隊列:如何進行消息可靠性,以及消息的幕等性(即消息不被重複消費)

 一、如何保證消息的可靠性傳輸?(如何處理消息丟失的問題)

  這個是確定的,用 MQ 有個基本原則,就是數據不能多一條,也不能少一條,不能多,就是重複消費和冪等性問題。不能少,就是說這數據別搞丟了。那這個問題你必須得考慮一下。

  若是說你這個是用 MQ 來傳遞很是核心的消息,好比說計費、扣費的一些消息,那必須確保這個 MQ 傳遞過程當中絕對不會把計費消息給弄丟

  數據的丟失問題,可能出如今生產者、MQ、消費者中,我們從 RabbitMQ 和 Kafka 分別來分析一下吧。

RabbitMQ

生產者弄丟了數據

  生產者將數據發送到 RabbitMQ 的時候,可能數據就在半路給搞丟了,由於網絡問題啥的,都有可能。

  此時能夠選擇用 RabbitMQ 提供的事務功能,就是生產者發送數據以前開啓 RabbitMQ 事務 channel.txSelect,而後發送消息,若是消息沒有成功被 RabbitMQ 接收到,那麼生產者會收到異常報錯,此時就能夠回滾事務 channel.txRollback,而後重試發送消息;若是收到了消息,那麼能夠提交事務 channel.txCommit

// 開啓事務
channel.txSelect
try {
    // 這裏發送消息
} catch (Exception e) {
    channel.txRollback

    // 這裏再次重發這條消息
}

// 提交事務
channel.txCommit

  可是問題是,RabbitMQ 事務機制(同步)一搞,基本上吞吐量會下來,由於太耗性能

  因此通常來講,若是你要確保說寫 RabbitMQ 的消息別丟,能夠開啓 confirm 模式,在生產者那裏設置開啓 confirm 模式以後,你每次寫的消息都會分配一個惟一的 id,而後若是寫入了 RabbitMQ 中,RabbitMQ 會給你回傳一個 ack 消息,告訴你說這個消息 ok 了。若是 RabbitMQ 沒能處理這個消息,會回調你的一個 nack 接口,告訴你這個消息接收失敗,你能夠重試。並且你能夠結合這個機制本身在內存裏維護每一個消息 id 的狀態,若是超過必定時間還沒接收到這個消息的回調,那麼你能夠重發。

  事務機制和 confirm 機制最大的不一樣在於,事務機制是同步的,你提交一個事務以後會阻塞在那兒,可是 confirm 機制是異步的,你發送個消息以後就能夠發送下一個消息,而後那個消息 RabbitMQ 接收了以後會異步回調你的一個接口通知你這個消息接收到了。

  因此通常在生產者這塊避免數據丟失,都是用 confirm 機制的。

RabbitMQ 弄丟了數據

  就是 RabbitMQ 本身弄丟了數據,這個你必須開啓 RabbitMQ 的持久化,就是消息寫入以後會持久化到磁盤,哪怕是 RabbitMQ 本身掛了,恢復以後會自動讀取以前存儲的數據,通常數據不會丟。除非極其罕見的是,RabbitMQ 還沒持久化,本身就掛了,可能致使少許數據丟失,可是這個機率較小。

設置持久化有兩個步驟

  • 建立 queue 的時候將其設置爲持久化
    這樣就能夠保證 RabbitMQ 持久化 queue 的元數據,可是它是不會持久化 queue 裏的數據的。
  • 第二個是發送消息的時候將消息的 deliveryMode 設置爲 2
    就是將消息設置爲持久化的,此時 RabbitMQ 就會將消息持久化到磁盤上去。

  必需要同時設置這兩個持久化才行,RabbitMQ 哪怕是掛了,再次重啓,也會從磁盤上重啓恢復 queue,恢復這個 queue 裏的數據。

  注意,哪怕是你給 RabbitMQ 開啓了持久化機制,也有一種可能,就是這個消息寫到了 RabbitMQ 中,可是還沒來得及持久化到磁盤上,結果不巧,此時 RabbitMQ 掛了,就會致使內存裏的一點點數據丟失。

  因此,持久化能夠跟生產者那邊的 confirm 機制配合起來,只有消息被持久化到磁盤以後,纔會通知生產者 ack 了,因此哪怕是在持久化到磁盤以前,RabbitMQ 掛了,數據丟了,生產者收不到 ack,你也是能夠本身重發的。

消費端弄丟了數據

  RabbitMQ 若是丟失了數據,主要是由於你消費的時候,剛消費到,還沒處理,結果進程掛了,好比重啓了,那麼就尷尬了,RabbitMQ 認爲你都消費了,這數據就丟了。

  這個時候得用 RabbitMQ 提供的 ack 機制,簡單來講,就是你必須關閉 RabbitMQ 的自動 ack,能夠經過一個 api 來調用就行,而後每次你本身代碼裏確保處理完的時候,再在程序裏 ack 一把。這樣的話,若是你還沒處理完,不就沒有 ack了?那 RabbitMQ 就認爲你還沒處理完,這個時候 RabbitMQ 會把這個消費分配給別的 consumer 去處理,消息是不會丟的。

rabbitmq-message-lose-solution

Kafka

消費端弄丟了數據

  惟一可能致使消費者弄丟數據的狀況,就是說,你消費到了這個消息,而後消費者那邊自動提交了 offset,讓 Kafka 覺得你已經消費好了這個消息,但其實你纔剛準備處理這個消息,你還沒處理,你本身就掛了,此時這條消息就丟咯。

  這不是跟 RabbitMQ 差很少嗎,你們都知道 Kafka 會自動提交 offset,那麼只要關閉自動提交 offset,在處理完以後本身手動提交 offset,就能夠保證數據不會丟。可是此時確實仍是可能會有重複消費,好比你剛處理完,還沒提交 offset,結果本身掛了,此時確定會重複消費一次,本身保證冪等性就行了。

  生產環境碰到的一個問題,就是說咱們的 Kafka 消費者消費到了數據以後是寫到一個內存的 queue 裏先緩衝一下,結果有的時候,你剛把消息寫入內存 queue,而後消費者會自動提交 offset。而後此時咱們重啓了系統,就會致使內存 queue 裏還沒來得及處理的數據就丟失了。

Kafka 弄丟了數據

  這塊比較常見的一個場景,就是 Kafka 某個 broker 宕機,而後從新選舉 partition 的 leader。你們想一想,要是此時其餘的 follower 恰好還有些數據沒有同步,結果此時 leader 掛了,而後選舉某個 follower 成 leader 以後,不就少了一些數據?這就丟了一些數據啊。

  生產環境也遇到過,咱們也是,以前 Kafka 的 leader 機器宕機了,將 follower 切換爲 leader 以後,就會發現說這個數據就丟了。

 因此此時通常是要求起碼設置以下 4 個參數:

  • 給 topic 設置 replication.factor 參數:這個值必須大於 1,要求每一個 partition 必須有至少 2 個副本。
  • 在 Kafka 服務端設置 min.insync.replicas 參數:這個值必須大於 1,這個是要求一個 leader 至少感知到有至少一個 follower 還跟本身保持聯繫,沒掉隊,這樣才能確保 leader 掛了還有一個 follower 吧。
  • 在 producer 端設置 acks=all:這個是要求每條數據,必須是寫入全部 replica 以後,才能認爲是寫成功了
  • 在 producer 端設置 retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這裏了。

  咱們生產環境就是按照上述要求配置的,這樣配置以後,至少在 Kafka broker 端就能夠保證在 leader 所在 broker 發生故障,進行 leader 切換時,數據不會丟失。

生產者會不會弄丟數據?

  若是按照上述的思路設置了 acks=all,必定不會丟,要求是,你的 leader 接收到消息,全部的 follower 都同步到了消息以後,才認爲本次寫成功了。若是沒知足這個條件,生產者會自動不斷的重試,重試無限次。

 

二、如何保證消息不被重複消費?(如何保證消息消費的冪等性)

  其實這是很常見的一個問題,這倆問題基本能夠連起來問。既然是消費消息,那確定要考慮會不會重複消費?能不能避免重複消費?或者重複消費了也別形成系統異常能夠嗎?這個是 MQ 領域的基本問題,其實本質上仍是問你使用消息隊列如何保證冪等性,這個是你架構裏要考慮的一個問題。

  回答這個問題,首先你別聽到重複消息這個事兒,就一無所知吧,你先大概說一說可能會有哪些重複消費的問題

  首先,好比 RabbitMQ、RocketMQ、Kafka,都有可能會出現消息重複消費的問題,正常。由於這問題一般不是 MQ 本身保證的,是由咱們開發來保證的。挑一個 Kafka 來舉個例子,說說怎麼重複消費吧。

  Kafka 實際上有個 offset 的概念,就是每一個消息寫進去,都有一個 offset,表明消息的序號,而後 consumer 消費了數據以後,每隔一段時間(定時按期),會把本身消費過的消息的 offset 提交一下,表示「我已經消費過了,下次我要是重啓啥的,你就讓我繼續從上次消費到的 offset 來繼續消費吧」。

  可是凡事總有意外,好比咱們以前生產常常遇到的,就是你有時候重啓系統,看你怎麼重啓了,若是碰到點着急的,直接 kill 進程了,再重啓。這會致使 consumer 有些消息處理了,可是沒來得及提交 offset,尷尬了。重啓以後,少數消息會再次消費一次。

舉個栗子。

  有這麼個場景。數據 1/2/3 依次進入 kafka,kafka 會給這三條數據每條分配一個 offset,表明這條數據的序號,咱們就假設分配的 offset 依次是 152/153/154。消費者從 kafka 去消費的時候,也是按照這個順序去消費。假如當消費者消費了 offset=153 的這條數據,剛準備去提交 offset 到 zookeeper,此時消費者進程被重啓了。那麼此時消費過的數據 1/2 的 offset 並無提交,kafka 也就不知道你已經消費了 offset=153 這條數據。那麼重啓以後,消費者會找 kafka 說,嘿,哥兒們,你給我接着把上次我消費到的那個地方後面的數據繼續給我傳遞過來。因爲以前的 offset 沒有提交成功,那麼數據 1/2 會再次傳過來,若是此時消費者沒有去重的話,那麼就會致使重複消費。

mq-10

  若是消費者乾的事兒是拿一條數據就往數據庫裏寫一條,會致使說,你可能就把數據 1/2 在數據庫裏插入了 2 次,那麼數據就錯啦。

  其實重複消費不可怕,可怕的是你沒考慮到重複消費以後,怎麼保證冪等性

  舉個例子吧。假設你有個系統,消費一條消息就往數據庫裏插入一條數據,要是你一個消息重複兩次,你不就插入了兩條,這數據不就錯了?可是你要是消費到第二次的時候,本身判斷一下是否已經消費過了,如果就直接扔了,這樣不就保留了一條數據,從而保證了數據的正確性。

  一條數據重複出現兩次,數據庫裏就只有一條數據,這就保證了系統的冪等性。

  冪等性,通俗點說,就一個數據,或者一個請求,給你重複來屢次,你得確保對應的數據是不會改變的,不能出錯

  因此第二個問題來了,怎麼保證消息隊列消費的冪等性?

  其實仍是得結合業務來思考,我這裏給幾個思路:

  • 好比你拿個數據要寫庫,你先根據主鍵查一下,若是這數據都有了,你就別插入了,update 一下好吧。
  • 好比你是寫 Redis,那沒問題了,反正每次都是 set,自然冪等性。
  • 好比你不是上面兩個場景,那作的稍微複雜一點,你須要讓生產者發送每條數據的時候,裏面加一個全局惟一的 id,相似訂單 id 之類的東西,而後你這裏消費到了以後,先根據這個 id 去好比 Redis 裏查一下,以前消費過嗎?若是沒有消費過,你就處理,而後這個 id 寫 Redis。若是消費過了,那你就別處理了,保證別重複處理相同的消息便可。
  • 好比基於數據庫的惟一鍵來保證重複數據不會重複插入多條。由於有惟一鍵約束了,重複數據插入只會報錯,不會致使數據庫中出現髒數據。

mq-11

  固然,如何保證 MQ 的消費是冪等性的,須要結合具體的業務來看。

 

(15):高併發下的接口冪等性

  文章出處:高併發下接口冪等性解決方案

1、背景 
      咱們實際系統中有不少操做,是無論作多少次,都應該產生同樣的效果或返回同樣的結果。 例如

  1. 前端重複提交選中的數據,應該後臺只產生對應這個數據的一個反應結果;

  2. 咱們發起一筆付款請求,應該只扣用戶帳戶一次錢,當遇到網絡重發或系統bug重發,也應該只扣一次錢;

  3. 發送消息,也應該只發一次,一樣的短信發給用戶,用戶會哭的;

  4. 建立業務訂單,一次業務請求只能建立一個,建立多個就會出大問題等等不少重要的狀況都須要冪等的特性來支持。 
2、冪等性概念 
      冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。 在編程中.一個冪等操做的特色是其任意屢次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指能夠使用相同參數重複執行,並能得到相同結果的函數。這些函數不會影響系統狀態,也不用擔憂重複執行會對系統形成改變。例如,「getUsername()和setTrue()」函數就是一個冪等函數. 更復雜的操做冪等保證是利用惟一交易號(流水號)實現. 個人理解:冪等就是一個操做,不論執行多少次,產生的效果和返回的結果都是同樣的 
3、技術方案 
  一、查詢操做:查詢一次和查詢屢次,在數據不變的狀況下,查詢結果是同樣的。select是自然的冪等操做;
  二、刪除操做:刪除操做也是冪等的,刪除一次和屢次刪除都是把數據刪除。(注意可能返回結果不同,刪除的數據不存在,返回0,刪除的數據多條,返回結果多個) ;
  三、惟一索引,防止新增髒數據。好比:支付寶的資金帳戶,支付寶也有用戶帳戶,每一個用戶只能有一個資金帳戶,怎麼防止給用戶建立資金帳戶多個,那麼給資金帳戶表中的用戶ID加惟一索引,因此一個用戶新增成功一個資金帳戶記錄。要點:惟一索引或惟一組合索引來防止新增數據存在髒數據(當表存在惟一索引,併發時新增報錯時,再查詢一次就能夠了,數據應該已經存在了,返回結果便可);
  四、token機制,防止頁面重複提交。業務要求: 頁面的數據只能被點擊提交一次;發生緣由: 因爲重複點擊或者網絡重發,或者nginx重發等狀況會致使數據被重複提交;解決辦法: 集羣環境採用token加redis(redis單線程的,處理須要排隊);單JVM環境:採用token加redis或token加jvm內存。處理流程:1. 數據提交前要向服務的申請token,token放到redis或jvm內存,token有效時間;2. 提交後後臺校驗token,同時刪除token,生成新的token返回。token特色:要申請,一次有效性,能夠限流。注意:redis要用刪除操做來判斷token,刪除成功表明token校驗經過,若是用select+delete來校驗token,存在併發問題,不建議使用;
  五、悲觀鎖——獲取數據的時候加鎖獲取。

select * from table_xxx where id='xxx' for update; 

  注意:id字段必定是主鍵或者惟一索引,否則是鎖表,會死人的悲觀鎖使用時通常伴隨事務一塊兒使用,數據鎖定時間可能會很長,根據實際狀況選用; 
  六、樂觀鎖——樂觀鎖只是在更新數據那一刻鎖表,其餘時間不鎖表,因此相對於悲觀鎖,效率更高。樂觀鎖的實現方式多種多樣能夠經過version或者其餘狀態條件:1. 經過版本號實現

update table_xxx set name=#name#,version=version+1 where version=#version#

  以下圖(來自網上);2. 經過條件限制

update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0

  要求:quality-#subQuality# >= ,這個情景適合不用版本號,只更新是作數據安全校驗,適合庫存模型,扣份額和回滾份額,性能更高;

     

      注意:樂觀鎖的更新操做,最好用主鍵或者惟一索引來更新,這樣是行鎖,不然更新時會鎖表,上面兩個sql改爲下面的兩個更好 

update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#;
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0

  7.分佈式鎖——仍是拿插入數據的例子,若是是分佈是系統,構建全局惟一索引比較困難,例如惟一性的字段無法肯定,這時候能夠引入分佈式鎖,經過第三方的系統(redis或zookeeper),在業務系統插入數據或者更新數據,獲取分佈式鎖,而後作操做,以後釋放鎖,這樣實際上是把多線程併發的鎖的思路,引入多多個系統,也就是分佈式系統中得解決思路。要點:某個長流程處理過程要求不能併發執行,能夠在流程執行以前根據某個標誌(用戶ID+後綴等)獲取分佈式鎖,其餘流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成後,釋放分佈式鎖(分佈式鎖要第三方系統提供);

  8. select + insert——併發不高的後臺系統,或者一些任務JOB,爲了支持冪等,支持重複執行,簡單的處理方法是,先查詢下一些關鍵數據,判斷是否已經執行過,在進行業務處理,就能夠了。注意:核心高併發流程不要用這種方法;

  9. 狀態機冪等——在設計單據相關的業務,或者是任務相關的業務,確定會涉及到狀態機(狀態變動圖),就是業務單據上面有個狀態,狀態在不一樣的狀況下會發生變動,通常狀況下存在有限狀態機,這時候,若是狀態機已經處於下一個狀態,這時候來了一個上一個狀態的變動,理論上是不可以變動的,這樣的話,保證了有限狀態機的冪等。注意:訂單等單據類業務,存在很長的狀態流轉,必定要深入理解狀態機,對業務系統設計能力提升有很大幫助 
  10. 對外提供接口的api如何保證冪等。如銀聯提供的付款接口:須要接入商戶提交付款請求時附帶:source來源,seq序列號 
source+seq在數據庫裏面作惟一索引,防止屢次付款(併發時,只能處理一個請求) 。重點:對外提供接口爲了支持冪等調用,接口有兩個字段必須傳,一個是來源source,一個是來源方序列號seq,這個兩個字段在提供方系統裏面作聯合惟一索引,這樣當第三方調用時,先在本方系統裏面查詢一下,是否已經處理過,返回相應處理結果;沒有處理過,進行相應處理,返回結果。注意,爲了冪等友好,必定要先查詢一下,是否處理過該筆業務,不查詢直接插入業務系統,會報錯,但實際已經處理了。 

4、總結
      冪等與你是否是分佈式高併發還有JavaEE都沒有關係。關鍵是你的操做是否是冪等的。一個冪等的操做典型如:把編號爲5的記錄的A字段設置爲0這種操做無論執行多少次都是冪等的。一個非冪等的操做典型如:把編號爲5的記錄的A字段增長1這種操做顯然就不是冪等的。要作到冪等性,從接口設計上來講不設計任何非冪等的操做便可。譬如說需求是:當用戶點擊贊同時,將答案的贊同數量+1。改成:當用戶點擊贊同時,確保答案贊同表中存在一條記錄,用戶、答案。贊同數量由答案贊同表統計出來。總之冪等性應該是合格程序員的一個基因,在設計系統時,是首要考慮的問題,尤爲是在像支付寶,銀行,互聯網金融公司等涉及的都是錢的系統,既要高效,數據也要準確,因此不能出現多扣款,多打款等問題,這樣會很難處理,用戶體驗也很差。

(16):springboot使用、springCloud和dubbo有什麼區別?

 

 

(17):hibernate ,mybatis區別

 

 

(18):mybatis中的 #和$有什麼區別?

 

 

(19):緩存與數據庫一致性如何保證?緩存和數據庫誰先更新。

  你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就必定會有數據一致性的問題,那麼你如何解決一致性問題? 

  通常來講,若是容許緩存能夠稍微的跟數據庫偶爾有不一致的狀況,也就是說若是你的系統不是嚴格要求 「緩存+數據庫」 必須保持一致性的話,最好不要作這個方案,即:讀請求和寫請求串行化,串到一個內存隊列裏去。

  串行化能夠保證必定不會出現不一致的狀況,可是它也會致使系統的吞吐量大幅度下降,用比正常狀況下多幾倍的機器去支撐線上的一個請求。

Cache Aside Pattern

最經典的緩存+數據庫讀寫的模式,就是 Cache Aside Pattern。

  • 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,而後取出數據後放入緩存,同時返回響應。
  • 更新的時候,先更新數據庫,而後再刪除緩存

爲何是刪除緩存,而不是更新緩存?

  緣由很簡單,不少時候,在複雜點的緩存場景,緩存不僅僅是數據庫中直接取出來的值。

  好比可能更新了某個表的一個字段,而後其對應的緩存,是須要查詢另外兩個表的數據並進行運算,才能計算出緩存最新的值的。

  另外更新緩存的代價有時候是很高的。是否是說,每次修改數據庫的時候,都必定要將其對應的緩存更新一份?也許有的場景是這樣,可是對於比較複雜的緩存數據計算的場景,就不是這樣了。若是你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。可是問題在於,這個緩存到底會不會被頻繁訪問到?

  舉個栗子,一個緩存涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼緩存更新 20 次、100 次;可是這個緩存在 1 分鐘內只被讀取了 1 次,有大量的冷數據。實際上,若是你只是刪除緩存的話,那麼在 1 分鐘內,這個緩存不過就從新計算一次而已,開銷大幅度下降。用到緩存纔去算緩存

  其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都從新作複雜的計算,無論它會不會用到,而是讓它到須要被使用的時候再從新計算。像 mybatis,hibernate,都有懶加載思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都把裏面的 1000 個員工的數據也同時查出來啊。80% 的狀況,查這個部門,就只是要訪問這個部門的信息就能夠了。先查部門,同時要訪問裏面的員工,那麼這個時候只有在你要訪問裏面的員工的時候,纔會去數據庫裏面查詢 1000 個員工。

最初級的緩存不一致問題及解決方案

  問題:先更新數據庫,再刪除緩存。若是刪除緩存失敗了,那麼會致使數據庫中是新數據,緩存中是舊數據,數據就出現了不一致。

redis-junior-inconsistent

  解決思路:先刪除緩存,再更新數據庫。若是數據庫更新失敗了,那麼數據庫中是舊數據,緩存中是空的,那麼數據不會不一致。由於讀的時候緩存沒有,因此去讀了數據庫中的舊數據,而後更新到緩存中。

比較複雜的數據不一致問題分析

  數據發生了變動,先刪除了緩存,而後要去修改數據庫,此時還沒修改。一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫,查到了修改前的舊數據,放到了緩存中。隨後數據變動的程序完成了數據庫的修改。完了,數據庫和緩存中的數據不同了...

  爲何上億流量高併發場景下,緩存會出現這個問題?

  只有在對一個數據在併發的進行讀寫的時候,纔可能會出現這種問題。其實若是說你的併發量很低的話,特別是讀併發很低,天天訪問量就 1 萬次,那麼不多的狀況下,會出現剛纔描述的那種不一致的場景。可是問題是,若是天天的是上億的流量,每秒併發讀是幾萬,每秒只要有數據更新的請求,就可能會出現上述的數據庫+緩存不一致的狀況

解決方案以下:

  更新數據的時候,根據數據的惟一標識,將操做路由以後,發送到一個 jvm 內部隊列中。讀取數據的時候,若是發現數據不在緩存中,那麼將從新執行「讀取數據+更新緩存」的操做,根據惟一標識路由以後,也發送到同一個 jvm 內部隊列中。

  一個隊列對應一個工做線程,每一個工做線程串行拿到對應的操做,而後一條一條的執行。這樣的話,一個數據變動的操做,先刪除緩存,而後再去更新數據庫,可是還沒完成更新。此時若是一個讀請求過來,沒有讀到緩存,那麼能夠先將緩存更新的請求發送到隊列中,此時會在隊列中積壓,而後同步等待緩存更新完成。

  這裏有一個優化點,一個隊列中,其實多個更新緩存請求串在一塊兒是沒意義的,所以能夠作過濾,若是發現隊列中已經有一個更新緩存的請求了,那麼就不用再放個更新請求操做進去了,直接等待前面的更新操做請求完成便可。

  待那個隊列對應的工做線程完成了上一個操做的數據庫的修改以後,纔會去執行下一個操做,也就是緩存更新的操做,此時會從數據庫中讀取最新的值,而後寫入緩存中。

  若是請求還在等待時間範圍內,不斷輪詢發現能夠取到值了,那麼就直接返回;若是請求等待的時間超過必定時長,那麼這一次直接從數據庫中讀取當前的舊值。

高併發的場景下,該解決方案要注意的問題:

  • 讀請求長時阻塞

  因爲讀請求進行了很是輕度的異步化,因此必定要注意讀超時的問題,每一個讀請求必須在超時時間範圍內返回。

  該解決方案,最大的風險點在於說,可能數據更新很頻繁,致使隊列中積壓了大量更新操做在裏面,而後讀請求會發生大量的超時,最後致使大量的請求直接走數據庫。務必經過一些模擬真實的測試,看看更新數據的頻率是怎樣的。

  另一點,由於一個隊列中,可能會積壓針對多個數據項的更新操做,所以須要根據本身的業務狀況進行測試,可能須要部署多個服務,每一個服務分攤一些數據的更新操做。若是一個內存隊列裏竟然會擠壓 100 個商品的庫存修改操做,每一個庫存修改操做要耗費 10ms 去完成,那麼最後一個商品的讀請求,可能等待 10 * 100 = 1000ms = 1s 後,才能獲得數據,這個時候就致使讀請求的長時阻塞。

  必定要作根據實際業務系統的運行狀況,去進行一些壓力測試,和模擬線上環境,去看看最繁忙的時候,內存隊列可能會擠壓多少更新操做,可能會致使最後一個更新操做對應的讀請求,會 hang 多少時間,若是讀請求在 200ms 返回,若是你計算事後,哪怕是最繁忙的時候,積壓 10 個更新操做,最多等待 200ms,那還能夠的。

  若是一個內存隊列中可能積壓的更新操做特別多,那麼你就要加機器,讓每一個機器上部署的服務實例處理更少的數據,那麼每一個內存隊列中積壓的更新操做就會越少。

  其實根據以前的項目經驗,通常來講,數據的寫頻率是很低的,所以實際上正常來講,在隊列中積壓的更新操做應該是不多的。像這種針對讀高併發、讀緩存架構的項目,通常來講寫請求是很是少的,每秒的 QPS 能到幾百就不錯了。

咱們來實際粗略測算一下

  若是一秒有 500 的寫操做,若是分紅 5 個時間片,每 200ms 就 100 個寫操做,放到 20 個內存隊列中,每一個內存隊列,可能就積壓 5 個寫操做。每一個寫操做性能測試後,通常是在 20ms 左右就完成,那麼針對每一個內存隊列的數據的讀請求,也就最多 hang 一下子,200ms 之內確定能返回了。

  通過剛纔簡單的測算,咱們知道,單機支撐的寫 QPS 在幾百是沒問題的,若是寫 QPS 擴大了 10 倍,那麼就擴容機器,擴容 10 倍的機器,每一個機器 20 個隊列。

  • 讀請求併發量太高

  這裏還必須作好壓力測試,確保恰巧碰上上述狀況的時候,還有一個風險,就是忽然間大量讀請求會在幾十毫秒的延時 hang 在服務上,看服務能不能扛的住,須要多少機器才能扛住最大的極限狀況的峯值。

  可是由於並非全部的數據都在同一時間更新,緩存也不會同一時間失效,因此每次可能也就是少數數據的緩存失效了,而後那些數據對應的讀請求過來,併發量應該也不會特別大。

  • 多服務實例部署的請求路由

  可能這個服務部署了多個實例,那麼必須保證說,執行數據更新操做,以及執行緩存更新操做的請求,都經過 Nginx 服務器路由到相同的服務實例上

  好比說,對同一個商品的讀寫請求,所有路由到同一臺機器上。能夠本身去作服務間的按照某個請求參數的 hash 路由,也能夠用 Nginx 的 hash 路由功能等等。

  • 熱點商品的路由問題,致使請求的傾斜

  萬一某個商品的讀寫請求特別高,所有打到相同的機器的相同的隊列裏面去了,可能會形成某臺機器的壓力過大。就是說,由於只有在商品數據更新的時候纔會清空緩存,而後纔會致使讀寫併發,因此其實要根據業務系統去看,若是更新頻率不是過高的話,這個問題的影響並非特別大,可是的確可能某些機器的負載會高一些。

 

 

 

 

 

Redis爲持久化提供了兩種方式:

  • RDB:在指定的時間間隔能對你的數據進行快照存儲。
  • AOF:記錄每次對服務器寫的操做,當服務器重啓的時候會從新執行這些命令來恢復原始的數據。

本文將經過下面內容的介紹,但願可以讓你們更全面、清晰的認識這兩種持久化方式,同時理解這種保存數據的思路,應用於本身的系統設計中。

  • 持久化的配置
  • RDB與AOF持久化的工做原理
  • 如何從持久化中恢復數據
  • 關於性能與實踐建議

持久化的配置

爲了使用持久化的功能,咱們須要先知道該如何開啓持久化的功能。

RDB的持久化配置

# 時間策略
save 900 1
save 300 10
save 60 10000
 # 文件名稱
dbfilename dump.rdb
 # 文件保存路徑
dir /home/work/app/redis/data/
 # 若是持久化出錯,主進程是否中止寫入
stop-writes-on-bgsave-error yes
 # 是否壓縮
rdbcompression yes
 # 導入時是否檢查
rdbchecksum yes

配置其實很是簡單,這裏說一下持久化的時間策略具體是什麼意思。

  • save 900 1 表示900s內若是有1條是寫入命令,就觸發產生一次快照,能夠理解爲就進行一次備份
  • save 300 10 表示300s內有10條寫入,就產生快照

下面的相似,那麼爲何須要配置這麼多條規則呢?由於Redis每一個時段的讀寫請求確定不是均衡的,爲了平衡性能與數據安全,咱們能夠自由定製什麼狀況下觸發備份。因此這裏就是根據自身Redis寫入狀況來進行合理配置。

stop-writes-on-bgsave-error yes 這個配置也是很是重要的一項配置,這是當備份進程出錯時,主進程就中止接受新的寫入操做,是爲了保護持久化的數據一致性問題。若是本身的業務有完善的監控系統,能夠禁止此項配置, 不然請開啓。

關於壓縮的配置 rdbcompression yes ,建議沒有必要開啓,畢竟Redis自己就屬於CPU密集型服務器,再開啓壓縮會帶來更多的CPU消耗,相比硬盤成本,CPU更值錢。

固然若是你想要禁用RDB配置,也是很是容易的,只須要在save的最後一行寫上:save ""

AOF的配置

# 是否開啓aof
appendonly yes
 # 文件名稱
appendfilename "appendonly.aof"
 # 同步方式
appendfsync everysec
 # aof重寫期間是否同步
no-appendfsync-on-rewrite no
 # 重寫觸發配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
 # 加載aof時若是有錯如何處理
aof-load-truncated yes
 # 文件重寫策略
aof-rewrite-incremental-fsync yes

仍是重點解釋一些關鍵的配置:

appendfsync everysec 它其實有三種模式:

  • always:把每一個寫命令都當即同步到aof,很慢,可是很安全
  • everysec:每秒同步一次,是折中方案
  • no:redis不處理交給OS來處理,很是快,可是也最不安全

通常狀況下都採用 everysec 配置,這樣能夠兼顧速度與安全,最多損失1s的數據。

aof-load-truncated yes 若是該配置啓用,在加載時發現aof尾部不正確是,會向客戶端寫入一個log,可是會繼續執行,若是設置爲 no ,發現錯誤就會中止,必須修復後才能從新加載。

工做原理

關於原理部分,咱們主要來看RDB與AOF是如何完成持久化的,他們的過程是如何。

在介紹原理以前先說下Redis內部的定時任務機制,定時任務執行的頻率能夠在配置文件中經過 hz 10 來設置(這個配置表示1s內執行10次,也就是每100ms觸發一次定時任務)。該值最大可以設置爲:500,可是不建議超過:100,由於值越大說明執行頻率越頻繁越高,這會帶來CPU的更多消耗,從而影響主進程讀寫性能。

定時任務使用的是Redis本身實現的 TimeEvent,它會定時去調用一些命令完成定時任務,這些任務可能會阻塞主進程致使Redis性能降低。所以咱們在配置Redis時,必定要總體考慮一些會觸發定時任務的配置,根據實際狀況進行調整。

RDB的原理

在Redis中RDB持久化的觸發分爲兩種:本身手動觸發與Redis定時觸發。

針對RDB方式的持久化,手動觸發能夠使用:

  • save:會阻塞當前Redis服務器,直到持久化完成,線上應該禁止使用。
  • bgsave:該觸發方式會fork一個子進程,由子進程負責持久化過程,所以阻塞只會發生在fork子進程的時候。

而自動觸發的場景主要是有如下幾點:

  • 根據咱們的 save m n 配置規則自動觸發;
  • 從節點全量複製時,主節點發送rdb文件給從節點完成複製操做,主節點會觸發 bgsave
  • 執行 debug reload 時;
  • 執行 shutdown時,若是沒有開啓aof,也會觸發。

因爲 save 基本不會被使用到,咱們重點看看 bgsave 這個命令是如何完成RDB的持久化的。
image1

這裏注意的是 fork 操做會阻塞,致使Redis讀寫性能降低。咱們能夠控制單個Redis實例的最大內存,來儘量下降Redis在fork時的事件消耗。以及上面提到的自動觸發的頻率減小fork次數,或者使用手動觸發,根據本身的機制來完成持久化。

AOF的原理

AOF的整個流程大致來看能夠分爲兩步,一步是命令的實時寫入(若是是 appendfsync everysec 配置,會有1s損耗),第二步是對aof文件的重寫。

對於增量追加到文件這一步主要的流程是:命令寫入=》追加到aof_buf =》同步到aof磁盤。那麼這裏爲何要先寫入buf在同步到磁盤呢?若是實時寫入磁盤會帶來很是高的磁盤IO,影響總體性能。

aof重寫是爲了減小aof文件的大小,能夠手動或者自動觸發,關於自動觸發的規則請看上面配置部分。fork的操做也是發生在重寫這一步,也是這裏會對主進程產生阻塞。

手動觸發: bgrewriteaof自動觸發 就是根據配置規則來觸發,固然自動觸發的總體時間還跟Redis的定時任務頻率有關係。

下面來看看重寫的一個流程圖:
image2

對於上圖有四個關鍵點補充一下:

  1. 在重寫期間,因爲主進程依然在響應命令,爲了保證最終備份的完整性;所以它依然會寫入舊的AOF file中,若是重寫失敗,可以保證數據不丟失。
  2. 爲了把重寫期間響應的寫入信息也寫入到新的文件中,所以也會爲子進程保留一個buf,防止新寫的file丟失數據。
  3. 重寫是直接把當前內存的數據生成對應命令,並不須要讀取老的AOF文件進行分析、命令合併。
  4. AOF文件直接採用的文本協議,主要是兼容性好、追加方便、可讀性高可認爲修改修復。
不能是RDB仍是AOF都是先寫入一個臨時文件,而後經過  rename 完成文件的替換工做。

從持久化中恢復數據

數據的備份、持久化作完了,咱們如何從這些持久化文件中恢復數據呢?若是一臺服務器上有既有RDB文件,又有AOF文件,該加載誰呢?

其實想要從這些文件中恢復數據,只須要從新啓動Redis便可。咱們仍是經過圖來了解這個流程:
image2

啓動時會先檢查AOF文件是否存在,若是不存在就嘗試加載RDB。那麼爲何會優先加載AOF呢?由於AOF保存的數據更完整,經過上面的分析咱們知道AOF基本上最多損失1s的數據。

性能與實踐

經過上面的分析,咱們都知道RDB的快照、AOF的重寫都須要fork,這是一個重量級操做,會對Redis形成阻塞。所以爲了避免影響Redis主進程響應,咱們須要儘量下降阻塞。

  1. 下降fork的頻率,好比能夠手動來觸發RDB生成快照、與AOF重寫;
  2. 控制Redis最大使用內存,防止fork耗時過長;
  3. 使用更牛逼的硬件;
  4. 合理配置Linux的內存分配策略,避免由於物理內存不足致使fork失敗。

在線上咱們到底該怎麼作?我提供一些本身的實踐經驗。

  1. 若是Redis中的數據並非特別敏感或者能夠經過其它方式重寫生成數據,能夠關閉持久化,若是丟失數據能夠經過其它途徑補回;
  2. 本身制定策略按期檢查Redis的狀況,而後能夠手動觸發備份、重寫數據;
  3. 單機若是部署多個實例,要防止多個機器同時運行持久化、重寫操做,防止出現內存、CPU、IO資源競爭,讓持久化變爲串行;
  4. 能夠加入主從機器,利用一臺從機器進行備份處理,其它機器正常響應客戶端的命令;
  5. RDB持久化與AOF持久化能夠同時存在,配合使用。

本文的內容主要是運維上的一些注意點,但咱們開發者瞭解到這些知識,在某些時候有助於咱們發現詭異的bug。接下來會介紹Redis的主從複製與集羣的知識。

 

(20):StringBuilder爲何線程不安全

StringBuilder和StringBuffer的區別在哪?

答:StringBuilder不是線程安全的,StringBuffer是線程安全的 

那StringBuilder不安全的點在哪兒?

分析

在分析設個問題以前咱們要知道StringBuilder和StringBuffer的內部實現跟String類同樣,都是經過一個char數組存儲字符串的,不一樣的是String類裏面的char數組是final修飾的,是不可變的,而StringBuilder和StringBuffer的char數組是可變的。

首先經過一段代碼去看一下多線程操做StringBuilder對象會出現什麼問題

public class StringBuilderDemo {

    public static void main(String[] args) throws InterruptedException {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++){
                        stringBuilder.append("a");
                    }
                }
            }).start();
        }

        Thread.sleep(100);
        System.out.println(stringBuilder.length());
    }

}

咱們能看到這段代碼建立了10個線程,每一個線程循環1000次往StringBuilder對象裏面append字符。正常狀況下代碼應該輸出10000,可是實際運行會輸出什麼呢?

咱們看到輸出了「9326」,小於預期的10000,而且還拋出了一個ArrayIndexOutOfBoundsException異常(異常不是必現)。

一、爲何輸出值跟預期值不同

咱們先看一下StringBuilder的兩個成員變量(這兩個成員變量其實是定義在AbstractStringBuilder裏面的,StringBuilder和StringBuffer都繼承了AbstractStringBuilder)

//存儲字符串的具體內容
char[] value;
//已經使用的字符數組的數量
int count;

再看StringBuilder的append()方法:

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

StringBuilder的append()方法調用的父類AbstractStringBuilder的append()方法

1.public AbstractStringBuilder append(String str) {
2.    if (str == null)
3.        return appendNull();
4.    int len = str.length();
5.    ensureCapacityInternal(count + len);
6.    str.getChars(0, len, value, count);
7.    count += len;
8.    return this;
9.}

  咱們先無論代碼的第五行和第六行幹了什麼,直接看第七行,count += len不是一個原子操做。假設這個時候count值爲10,len值爲1,兩個線程同時執行到了第七行,拿到的count值都是10,執行完加法運算後將結果賦值給count,因此兩個線程執行完後count值爲11,而不是12。這就是爲何測試代碼輸出的值要比10000小的緣由。

二、爲何會拋出ArrayIndexOutOfBoundsException異常。

  咱們看回AbstractStringBuilder的append()方法源碼的第五行,ensureCapacityInternal()方法是檢查StringBuilder對象的原char數組的容量能不能盛下新的字符串,若是盛不下就調用expandCapacity()方法對char數組進行擴容。

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

  擴容的邏輯就是new一個新的char數組,新的char數組的容量是原來char數組的兩倍再加2,再經過System.arryCopy()函數將原數組的內容複製到新數組,最後將指針指向新的char數組。

void expandCapacity(int minimumCapacity) {
    //計算新的容量
    int newCapacity = value.length * 2 + 2;
    //中間省略了一些檢查邏輯
    ...
    value = Arrays.copyOf(value, newCapacity);
}

Arrys.copyOf()方法

public static char[] copyOf(char[] original, int newLength) {
    char[] copy = new char[newLength];
    //拷貝數組
    System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
    return copy;
}

AbstractStringBuilder的append()方法源碼的第六行,是將String對象裏面char數組裏面的內容拷貝到StringBuilder對象的char數組裏面,代碼以下:

str.getChars(0, len, value, count);

getChars()方法

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    //中間省略了一些檢查
    ...   
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

拷貝流程見下圖

假設如今有兩個線程同時執行了StringBuilder的append()方法,兩個線程都執行完了第五行的ensureCapacityInternal()方法,此刻count=5。
這個時候線程1的cpu時間片用完了,線程2繼續執行。線程2執行完整個append()方法後count變成6了 

  

線程1繼續執行第六行的str.getChars()方法的時候拿到的count值就是6了,執行char數組拷貝的時候就會拋出ArrayIndexOutOfBoundsException異常。

至此,StringBuilder爲何不安全已經分析完了。若是咱們將測試代碼的StringBuilder對象換成StringBuffer對象會輸出什麼呢?

固然是輸出10000啦!

 

那麼StringBuffer用什麼手段保證線程安全的?這個問題你點進StringBuffer的append()方法裏面就知道了。

 

 

(21):分佈式事務是怎麼處理的?

 參考:  分佈式事務的一種解決思路

     分佈式事務解決方案彙總:2PC、消息中間件、TCC、狀態機+重試+冪等

     分佈式事務解決方案

 

(22):數據庫查詢,where條件是大的數據放在前面仍是放在後面?

 

 

(23):數據庫,是小表驅動大表,仍是大表驅動小表?

 

 

(24):Spring框架是如何解決bean的循環依賴問題?

 參考: Spring框架是怎麼解決Bean之間的循環依賴的 (轉)

 

 


面試題模塊系列彙總

一、分佈式系統

1.1 爲何要進行系統拆分?

(1)爲何要進行系統拆分?如何進行系統拆分?拆分後不用dubbo能夠嗎?dubbo和thrift有什麼區別呢?

 

 

1.2 分佈式服務框架

(1)說一下的dubbo的工做原理?註冊中心掛了能夠繼續通訊嗎?

 

(2)dubbo支持哪些序列化協議?說一下hessian的數據結構?PB知道嗎?爲何PB的效率是最高的?

 

(3)dubbo負載均衡策略和高可用策略都有哪些?動態代理策略呢?

 

(4)dubbo的spi思想是什麼?

 

(5)如何基於dubbo進行服務治理、服務降級、失敗重試以及超時重試?

 

(6)分佈式服務接口的冪等性如何設計(好比不能重複扣款)?

 

(7)分佈式服務接口請求的順序性如何保證?

 

(8)如何本身設計一個相似dubbo的rpc框架?

 

1.3 分佈式鎖

(1)使用redis如何設計分佈式鎖?使用zk來設計分佈式鎖能夠嗎?這兩種分佈式鎖的實現方式哪一種效率比較高?

 

1.4 分佈式事務

(1)分佈式事務瞭解嗎?大家如何解決分佈式事務問題的?TCC若是出現網絡連不通怎麼辦?XA的一致性如何保證?

 

1.5 分佈式會話

(1)集羣部署時的分佈式session如何實現?

 

二、高併發架構

2.1 如何設計一個高併發系統?

 

2.2 消息隊列

(1)爲何使用消息隊列啊?消息隊列有什麼優勢和缺點啊?kafka、activemq、rabbitmq、rocketmq都有什麼優勢和缺點啊?

 

(2)如何保證消息隊列的高可用啊?

 

(3)如何保證消息不被重複消費啊(如何進行消息隊列的冪等性問題)?

 

(4)如何保證消息的可靠性傳輸(如何處理消息丟失的問題)?

 

(5)如何保證消息的順序性?

 

(6)如何解決消息隊列的延時以及過時失效問題?消息隊列滿了之後該怎麼處理?有幾百萬消息持續積壓幾小時,說說怎麼解決?

 

(7)若是讓你寫一個消息隊列,該如何進行架構設計啊?說一下你的思路

2.3 搜索引擎

(1)es的分佈式架構原理能說一下麼(es是如何實現分佈式的啊)?

 

(2)es寫入數據的工做原理是什麼啊?es查詢數據的工做原理是什麼啊?底層的lucene介紹一下唄?倒排索引瞭解嗎?

 

(3)es在數據量很大的狀況下(數十億級別)如何提升查詢效率啊?

 

(4)es生產集羣的部署架構是什麼?每一個索引的數據量大概有多少?每一個索引大概有多少個分片?

2.4 緩存

(1)在項目中緩存是如何使用的?緩存若是使用不當會形成什麼後果?

 

(2)redis和memcached有什麼區別?redis的線程模型是什麼?爲何單線程的redis比多線程的memcached效率要高得多?

 

(3)redis都有哪些數據類型?分別在哪些場景下使用比較合適?

 

(5)redis的過時策略都有哪些?手寫一下LRU代碼實現?

 

(6)如何保證Redis高併發、高可用、持久化?redis的主從複製原理能介紹一下麼?redis的哨兵原理能介紹一下麼?

 

(7)redis的持久化有哪幾種方式?不一樣的持久化機制都有什麼優缺點?持久化機制具體底層是如何實現的?

 

(8)redis集羣模式的工做原理能說一下麼?在集羣模式下,redis的key是如何尋址的?分佈式尋址都有哪些算法?瞭解一致性hash算法嗎?如何動態增長和刪除一個節點?

 

(9)瞭解什麼是redis的雪崩和穿透?redis崩潰以後會怎麼樣?系統該如何應對這種狀況?如何處理redis的穿透?

 

(10)如何保證緩存與數據庫的雙寫一致性?

 

(11)redis的併發競爭問題是什麼?如何解決這個問題?瞭解Redis事務的CAS方案嗎?

 

(12)生產環境中的redis是怎麼部署的?

 

2.5 分庫分表

(1)爲何要分庫分表(設計高併發系統的時候,數據庫層面該如何設計)?用過哪些分庫分表中間件?不一樣的分庫分表中間件都有什麼優勢和缺點?大傢俱體是如何對數據庫如何進行垂直拆分或水平拆分的?

 

(2)如今有一個未分庫分表的系統,將來要分庫分表,如何設計纔可讓系統從未分庫分表動態切換到分庫分表上?

 

(3)如何設計能夠動態擴容縮容的分庫分表方案?

 

(4)分庫分表以後,id主鍵如何處理?

 

2.6 讀寫分離

(1)如何實現mysql的讀寫分離?MySQL主從複製原理的是啥?如何解決mysql主從同步的延時問題?

 

三、高可用架構

3.1 如何設計一個高可用系統?

 

3.2 限流

(1)如何限流?在工做中是怎麼作的?說一下具體的實現?

 

3.3 熔斷

(1)如何進行熔斷?熔斷框架都有哪些?具體實現原理知道嗎?

 

3.4 降級

(1)如何進行降級?

相關文章
相關標籤/搜索