最近,在看一些java高級面試題,我發現我在認真研究一個面試題的時候,我自己的收獲是很大的,我們在看看面試題的時候,不僅僅要看這個問題本身,還要看這個問題的衍生問題,一個問題有些時候可能是一個問題群(如果只關注問題本身,可以跳過補充部分)。
這個是我一個多星期的奮戰結果,把它記錄下來,如有不當,希望大家不吝賜教。
java 線程池的實現原理,threadpoolexecutor關鍵參數解釋
原理見下圖 (或者:https://blog.csdn.net/u013332124/article/details/79587436) 實際開發中,直接調用ThreadPoolExecutor的情況比較少,更多的是使用Executors實現(Executors內部很多地方都是調用的ThreadPoolExecutor) Executors 是一個工廠類,用於創建線程池,常用的有5個api: newCachedThreadPool():創建無界限線程池 newFixedThreadPool(int) :創建的是有界線線程池(線程個數可以指定最大數量,如果超過最大數量,則后加入的線程需要等待) newSingleThreadExecutor() :創建單一線程池,實現以隊列的方式來執行任務 newScheduledThreadPool(...):創建即將執行的任務線程池,每隔多久執行一次 newSingleThreadScheduledExecutor():只有一個線程,用來調度任務在指定時間內執行 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) corePoolSize:線程池核心線程數量 maximumPoolSize:線程池最大線程數量( 當workQueue滿了,不能添加任務的時候,這個參數才會生效。) keepAliverTime:當活躍線程數大於核心線程數時,空閑的多余線程最大存活時間 unit:存活時間的單位 workQueue:存放任務的隊列 handler:超出線程范圍和隊列容量的任務的處理程序
hashmap的原理,容量為什么是2的冪次
HashMap使用的是哈希表(也稱散列表,是根據關鍵碼值(Key value)而直接進行訪問的數據結構),哈希表有多種不同的實現方法,常用的實現方式是“數組+鏈表”實現,即“鏈表的數組”。 首先,HashMap中存儲的對象為數組Entry[](每個Entry就是一個<key,value>),存和取時根據 key 的 hashCode 相關的算法得到數組的下標,put 和 get 都根據算法的下標取得元素在數組中的位置。 萬一兩個key分別有相同的下標,那么怎么處理呢? 使用鏈表,把下標相同的 Entry 放到一個鏈表中,存儲時,存到第一個位置,並把next指向之前的第一個元素(如果是已存在的key,則需要遍歷鏈表),取值時,遍歷鏈表,通過equals方法獲取Entry。 // 存儲時: int hash = key.hashCode(); // 這個hashCode方法這里不詳述,只要理解每個key的hash是一個固定的int值 int index = hash & (Entry[].length-1); // 二進制的按位與運算 Entry[index] = value; // 取值時: int hash = key.hashCode(); int index = hash & (Entry[].length-1); // 二進制的按位與運算 return Entry[index]; // 此處需要遍歷 Entry[index] 的鏈表取值,此處為簡寫 關於原理細節,可參考文章 https://www.cnblogs.com/holyshengjie/p/6500463.html 前半部分 關於源碼實現,可參考文章 https://www.cnblogs.com/chengxiao/p/6059914.html 【關於為什么覆蓋 equals方法 后需要同時覆蓋 hashCode方法】 在此文末尾 通過以上理解,如果 hashCode方法 寫得不好,比如所有 key 對象 都返回 同一個 int 值,那么效率也不會高,因為就相當於一個鏈表了。 至於容量為什么是2的冪次? 因為HashMap的機制是,當發生哈希沖突並且size大於閾值的時候,需要進行數組擴容,因為獲取數組下標的方法是【hashCode和數組容量的按位與運算】結果,故只有數組大小為2^n-1(即二進制所有位都是1,如:3,7,15,31...)才能保按位與運算得到的 index 均勻分布。
補充:二進制的按位與運算舉例:23 & 15 = 7
0 1 0 1 1 1
0 0 1 1 1 1
-------------
0 0 0 1 1 1
為什么要同時重寫hashcode和equals
原因在上一個問題中已經描述過了,這里再說一下,注意看main方法中的注釋:
舉個小例子來看看,如果重寫了equals而不重寫hashcode會發生什么樣的問題
public class MyTest { private static class Person{ int idCard; String name; public Person(int idCard, String name) { this.idCard = idCard; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()){ return false; } Person person = (Person) o; //兩個對象是否等值,通過idCard來確定 return this.idCard == person.idCard; } } public static void main(String []args){ HashMap<Person,String> map = new HashMap<Person, String>(); Person person = new Person(1234,"喬峰"); //put到HashMap中去 map.put(person,"天龍八部"); //get取出,從邏輯上講應該能輸出“天龍八部” System.out.println("結果:"+map.get(new Person(1234,"蕭峰"))); // 結果:null } }
要理解為什么要同時重寫hashCode和equals,需要首先理解 HashMap 的原理。
這里的例子中,由於沒有重寫hashCode方法,雖然兩個對象的 equals 方法時一致的,但是兩個對象的hashCode 返回的 int 值不一致,導致put操作和get操作時,最終存和取的數組下標不同,取出來的就是null 了。
ConcurrentHashMap如何實現線程安全?
這個問題可以參考文章:https://blog.csdn.net/V_Axis/article/details/78616700 以下是個人總結:
先說一下HashMap為什么線程不安全(hash碰撞與擴容導致,2點):
1、Entry內部的變量分別是 key、value、next,如果多個線程,在某一時刻同時操作HashMap並執行put操作,而有兩個key的hash值相同(這兩個key分別是a1 和 a2),這個時候需要解決碰撞沖突,不論是從鏈表頭部插入還是從尾部初入,這個時候兩個線程如果恰好都取到了對應位置的頭結點e1,而最終的結果可想而知,a1、a2兩個數據中勢必會有一個會丟失;
2、在擴容時,當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的均會丟失。
解決HashMap的線程不安全,可以使用HashTable或者Collections.synchronizedMap,但是這兩位選手都有一個共同的問題:性能。因為不管是讀還是寫操作,他們都會給整個集合上鎖(每個方法都是 synchronized 的),導致同一時間的其他操作被阻塞。
ConcurrentHashMap 是 HashMap 與 HashTable 的折中,ConcurrentHashMap 在進行操作時,會給集合上鎖,但是只鎖了部分(segment)。
ConcurrentHashMap 集合 中有多個Segment,Segment其實就是一個HashMap,Segment也包含一個HashEntry數組,數組中的每一個HashEntry既是一個鍵值對,也是一個鏈表的頭節點。
Segment對象在 ConcurrentHashMap 集合中有2的n次方個,共同保存在一個名為segments的數組當中(類比HashMap來理解Segment就好)。換言之,ConcurrentHashMap是一個雙層哈希表。
每個segment的讀寫是高度自治的,segment之間互不影響。這稱之為“鎖分段技術”。
補充:
每一個segment各自持有鎖,那么在調用size()方法的時候(size()在實際開發大量使用),怎么保持一致性呢?
1.遍歷所有的Segment。
2.把Segment的修改次數累加起來。
3.把Segment的元素數量累加起來。
4.判斷所有Segment的總修改次數(重復獲取一次2的統計)是否大於上一次的總修改次數(第2步統計的結果)。如果大於,說明統計過程中有修改,重新統計,嘗試次數+1;如果不是。說明沒有修改,統計結束。
5.如果嘗試次數超過閾值,則對每一個Segment加鎖,再重新統計。
6.再次判斷所有Segment的總修改次數是否大於上一次的總修改次數。由於已經加鎖,次數一定和上次相等。
7.釋放鎖,統計結束。
這個邏輯有些類似於樂觀鎖(參考:https://blog.csdn.net/V_Axis/article/details/78532806)
介紹Java多線程的5大狀態,以及狀態圖流轉過程
1. 新建(NEW):新創建了一個線程對象。
2. 可運行(RUNNABLE):線程對象創建后,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
3. 運行(RUNNING):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。
4. 阻塞(BLOCKED):阻塞狀態是指線程因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,才有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:
(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。
(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池(lock pool)中。
(三). 其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。
5. 死亡(DEAD):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命周期。死亡的線程不可再次復生。
詳見:https://blog.csdn.net/maijia0754/article/details/79004412
介紹下synchronized、Volatile、CAS、AQS,以及各自的使用場景
synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操作系統的互斥鎖(Mutex Lock)來實現的。synchronized屬於重量級的,多線程時只能一個線程訪問相關代碼塊,安全性高,但有時會效率低下。 volatile 通常用來修飾變量,能保證變量的內存可見性(即變量的讀取和修改是直接操作原變量<每次讀取前必須先從主內存刷新最新的值,每次寫入后必須立即同步回主內存當中>,而不是copy后的變量),同時能防止指令被重排序(JVM運行時,為了效率考慮會對某些指令的執行順序進行調整),但無法保證原子性(如語句:count++; 在多線程高並發時,就會出現數值不准,此時可以使用synchronized 或者 ReentrantLock 加鎖)。 CAS(compare and swap 的簡寫,即比較並交換)是樂觀鎖的一個實現,它需要三個參數,分別是內存位置 V,舊的預期值 A 和新的值 B。操作時,先從內存位置讀取到值,然后和預期值A比較。如果相等,則將此內存位置的值改為新值 B,返回 true。如果不相等,說明和其他線程沖突了,則不做任何改變,返回 false。樂觀鎖中,一般比較操作比的是版本號,CAS中也可以改舊值比較為版本號比較(安全性更高)。 AQS(AbstractQueuedSynchronizer)中有兩個重要的成員: 1、成員變量 state。用於表示鎖現在的狀態,用 volatile 修飾,保證內存一致性。同時所用對 state 的操作都是使用 CAS 進行的。state 為0表示沒有任何線程持有這個鎖,線程持有該鎖后將 state 加1,釋放時減1。多次持有釋放則多次加減。 2、還有一個雙向鏈表,鏈表除了頭結點外,每一個節點都記錄了線程的信息,代表一個等待線程。這是一個 FIFO 的鏈表。 AQS 請求鎖時有三種可能: 1、如果沒有線程持有鎖,則請求成功,當前線程直接獲取到鎖。 2、如果當前線程已經持有鎖,則使用 CAS 將 state 值加1,表示自己再次申請了鎖,釋放鎖時減1。這就是可重入性的實現。 3、如果由其他線程持有鎖,那么將自己添加進等待隊列。
其中,ReentrantLock 就是 AQS原理實現的,而synchronized 則是jvm層面實現的。
補充:
ReentrantLock 使用代碼實現了和 synchronized 一樣的語義,包括可重入,保證內存可見性和解決競態條件問題等。相比 synchronized,它還有如下好處:
支持以非阻塞方式獲取鎖
可以響應中斷
可以限時
支持了公平鎖和非公平鎖(公平鎖是指各個線程在加鎖前先檢查有無排隊的線程,按排隊順序去獲得鎖。 非公平鎖是指線程加鎖前不考慮排隊問題,直接嘗試獲取鎖,獲取不到再去隊尾排隊。)
基本用法如下:
public class Counter { private final Lock lock = new ReentrantLock(); // 如果 new ReentrantLock(true); 則表示公平鎖 private volatile int count; public void incr() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } }
B+樹和紅黑樹時間復雜度
B+數時間復雜度是 O(lgn)
紅黑樹時間復雜度是 O(lgn)
補充:
B+樹是為磁盤或其他直接存取輔助設備而設計的一種平衡查找樹,所有記錄節點都是按鍵值的大小順序存放在同一層的葉節點中,各葉節點指針進行連接。
(B+樹不詳細說了,詳細網上資料很多)
紅黑樹(RBT)的定義:它或者是一顆空樹,或者是具有一下性質的二叉查找樹:
1.節點非紅即黑。
2.根節點是黑色。
3.所有NULL結點稱為葉子節點,且認為顏色為黑。
4.所有紅節點的子節點都為黑色。
5.從任一節點到其葉子節點的所有路徑上都包含相同數目的黑節點。
紅黑樹插入:
插入點不能為黑節點,應插入紅節點。因為你插入黑節點將破壞性質5,所以每次插入的點都是紅結點,但是若他的父節點也為紅,那豈不是破壞了性質4?對啊,所以要做一些“旋轉”和一些節點的變色。
紅黑樹參考自:http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
平衡二叉樹(AVL)適合用於插入刪除次數比較少,但查找多的情況;紅黑樹的旋轉保持平衡(插入和刪除時會旋轉保持平衡)次數較少,用於搜索時,插入刪除次數多的情況下我們就用紅黑樹來取代AVL。
如果頻繁老年代回收怎么分析解決
老年代頻繁回收,一般是Full GC,Full GC 消耗很大,因為在所有用戶線程停止的情況下完成回收,而造成頻繁 Full GC 的原因可能是,程序存在問題,或者環境存在問題。
對jvm的GC進行必要的監控,操作如下:
1、使用jps命令(或者ps -eaf|grep java)獲取到當前的java進程(取得進程id,假如pid為 1234)
2、使用jstat查看GC情況(如:jstat -gc 1234 1000,后面的1000表示每個1000毫米打印一次監控),jstat命令可以參考:https://www.cnblogs.com/yjd_hycf_space/p/7755633.html (此文使用的是jdk8,但是本人親測jstat在jdk7也是這樣的)
jstat -class pid:顯示加載class的數量,及所占空間等信息。
jstat -compiler pid:顯示VM實時編譯的數量等信息。
jstat -gc pid:可以顯示gc的信息,查看gc的次數,及時間。其中最后五項,分別是young gc的次數,young gc的時間,full gc的次數,full gc的時間,gc的總時間。
jstat -gccapacity:可以顯示,VM內存中三代(young,old,perm)對象的使用和占用大小,如:PGCMN顯示的是最小perm的內存使用量,PGCMX顯示的是perm的內存最大使用量,PGC是當前新生成的perm內存占用量,PC是但前perm內存占用量。其他的可以根據這個類推, OC是old內純的占用量。
jstat -gcnew pid:new對象的信息。
jstat -gcnewcapacity pid:new對象的信息及其占用量。
jstat -gcold pid:old對象的信息。
jstat -gcoldcapacity pid:old對象的信息及其占用量。
jstat -gcpermcapacity pid: perm對象的信息及其占用量。
jstat -util pid:統計gc信息統計。
jstat -printcompilation pid:當前VM執行的信息。
除了以上一個參數外,還可以同時加上 兩個數字,如:jstat -printcompilation 3024 250 6是每250毫秒打印一次,一共打印6次,還可以加上-h3每三行顯示一下標題
3、使用jmap(jmap 是一個可以輸出所有內存中對象的工具)導出對象文件。如對於java進程(pid=1234),可以這樣:jmap -histo 1234 > a.log 將對象導出到文件,然后通過查看對象內存占用大小,返回去代碼里面找問題。
(也可以使用命令,導出對象二進制內容(這樣導出內容比jmap -histo多得多,更耗時,對jvm的消耗也更大):jmap -dump:format=b,file=a.log 1234)
(以上操作本人有親自實驗,jdk版本是7,第2步其實可以跳過)
JVM內存模型,新生代和老年的回收機制
新生代的GC:
新生代通常存活時間較短,因此基於復制算法來進行回收,所謂復制算法就是掃描出存活的對象,並復制到一塊新的完全未使用的空間中。
在Eden和其中一個Survivor,復制到另一個之間Survivor空間中,然后清理掉原來就是在Eden和其中一個Survivor中的對象。
新生代采用空閑指針的方式來控制GC觸發,指針保持最后一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。
當連續分配對象時,對象會逐漸從eden到 survivor,最后到老年代,然后清空繼續裝載,當老年代也滿了后,就會報outofmemory的異常。
老年代的GC:
老年代與新生代不同,對象存活的時間比較長,比較穩定,因此采用標記(Mark)算法來進行回收。
掃描出存活的對象,然后再進行回收未被標記的對象,回收后對用空出的空間要么進行合並,要么標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。
補充: Java 中的堆也是 GC 收集垃圾的主要區域: GC 分為兩種:Minor GC、Full GC ( 或稱為 Major GC )。 Minor GC 是發生在新生代中的垃圾收集動作,所采用的是復制算法。 Full GC 是發生在老年代的垃圾收集動作,所采用的是標記-清除算法。 Minor GC思路: 當對象在 Eden ( 包括一個 Survivor 區域,這里假設是 from 區域 ) 出生后,在經過一次 Minor GC 后,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納,並且將這些對象的年齡設置為1,以后對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。 對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。 Full GC 發生的次數不會有 Minor GC 那么頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長。 標記-清除算法收集垃圾的時候會產生許多的內存碎片(不連續內存空間)。 標記:標記的過程其實就是,遍歷所有的GC Roots,然后將所有GC Roots可達的對象標記為存活的對象。 清除:清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。 標記-清除算法 容易產生內存碎片,故老年代中會使用標記整理算法(首先標記,但后續不是直接清除,而是將存活的對象都向一端移動,最后直接清理掉邊界以外的內存。) GC Roots有哪些: Class - 由系統類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態字段的方式保存持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應的java.lang.Class實例以其它的某種(或多種)方式成為roots,否則它們並不是roots,. Thread - 活着的線程 Stack Local - Java方法的local變量或參數(個人理解,就是ThreadLocal 相關的類 ) JNI Local - JNI方法的local變量或參數(JNI 是 Java Native Interface的縮寫,它提供了若干的API實現了Java和其他語言的通信(主要是C&C++)) JNI Global - 全局JNI引用 Monitor Used - 用於同步的監控對象 Held by JVM - 用於JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現是有關的。可能已知的一些類型是:系統類加載器、一些JVM知道的重要的異常類、一些用於處理異常的預分配對象以及一些自定義的類加載器等。然而,JVM並沒有為這些對象提供其它的信息,因此需要去確定哪些是屬於"JVM持有"的了。
mysql limit分頁如何保證可靠性
limit 查詢,當表中數據量很大時,在十萬條后面 limit 速度會變慢,如: select XXX from tableA limit 1000000,10; 上面的語句是取1000000后面的10條記錄,但是這樣會導致mysql將1000000之前的所有數據全部掃描一次,大量浪費了時間。 解決思路1: 直接使用主鍵查詢,如: -- 此查詢耗時:0.00 秒 select * from couponmodel_user where id > 3000000 and id < 3000000+10; 這種方式會造成某次查詢出來的條數不足10條,有不足,但是優點就在特別快。 解決思路2: 直接貼我實際操作的代碼吧 -- couponmodel_user 表中有一千萬條數據,此查詢耗時:3.02 秒 select * from couponmodel_user order by id limit 3000000,10; -- 只查id,此查詢耗時:0.85 秒 select id from couponmodel_user order by id limit 3000000,10; -- 通過以上的啟發,修改sql,此查詢耗時:0.84 秒 select a.* from couponmodel_user a,(select id from couponmodel_user order by id limit 3000000,10) b where a.id=b.id order by a.id; 簡單講,就是使用主鍵的復合索引,關聯查詢 思路2解決了思路1的不足,速度也顯著提升,但是問題就是速度還不如思路1快。
java nio,bio,aio,操作系統底層nio實現原理
BIO:
同步並阻塞,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
BIO方式適用於連接數目比較小且固定的架構,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
NIO:
同步非阻塞,服務器實現模式為一個請求一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,並發局限於應用中,編程比較復雜,JDK1.4開始支持。
AIO(NIO.2):
異步非阻塞,服務器實現模式為一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啟動線程進行處理。
AIO方式使用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與並發操作,編程比較復雜,JDK7開始支持。
可以參考文章:https://blog.csdn.net/ty497122758/article/details/78979302 或者 https://www.cnblogs.com/diegodu/p/6823855.html
Spring事務傳播機制
PROPAGATION_REQUIRED:有事務就用已有的,沒有就重新開啟一個
PROPAGATION_SUPPORTS:有事務就用已有的,沒有也不會重新開啟
PROPAGATION_MANDATORY:必須要有事務,沒事務拋異常
PROPAGATION_REQUIRES_NEW:開啟新事務,若當前已有事務,掛起當前事務
PROPAGATION_NOT_SUPPORTED:不需要事務,若當前已有事務,掛起當前事務
PROPAGATION_NEVER:不需要事務,若當前已有事務,拋出異常
PROPAGATION_NESTED:嵌套事務,如果外部事務回滾,則嵌套事務也會回滾!!!外部事務提交的時候,嵌套它才會被提交。嵌套事務回滾不會影響外部事務。
線程死鎖排查
使用 jps + jstack,排查步驟:
1、使用命令列出當前運行的java進程的pid: jps -l
2、使用命令查看(假如第1步查到的java進程pid為 1234):jstack -l 1234
注意第2步中是否有提示 “ Found x deadlock ” 如有,則表示有死鎖存在,其中 x 表示死鎖的個數
以上兩個命令的 -l 選項都可以省略,以上操作本人親自實驗可用。
參考:https://blog.csdn.net/mynamepg/article/details/80758540
補充:
死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。
MySQL引擎及區別,項目用的哪個,為什么
ISAM:
ISAM執行讀取操作的速度很快,且不占用大量的內存和存儲資源。
ISAM的兩個主要不足之處在於,不支持事務處理,也不能夠容錯(如果你的硬盤崩潰了,那么數據文件就無法恢復了,需實時備份數據)。
MyISAM:
MyISAM是MySQL的ISAM擴展格式和缺省的數據庫引擎。提供了索引和字段管理的大量功能,使用一種表格鎖定的機制,來優化多個並發的讀寫操作。
MyISAM的不足仍然是不支持事務管理。
HEAP:
HEAP允許只駐留在內存里的臨時表格。駐留在內存里讓HEAP要比ISAM和MYISAM都快。
不足:管理的數據是不穩定的,而且如果在關機之前沒有進行保存,那么所有的數據都會丟失。在數據行被刪除的時候,HEAP也不會浪費大量的空間。
HEAP表格在你需要使用SELECT表達式來選擇和操控數據的時候非常有用。要記住,在用完表格之后就刪除表格。
InnoDB:
InnoDB數據庫引擎盡管要比ISAM和 MyISAM引擎慢很多,但是InnoDB包括了對事務處理和外來鍵的支持,這兩點都是前兩個引擎所沒有的。如前所述,如果你的設計需要這些特性中的一者 或者兩者,那你就要被迫使用后兩個引擎中的一個了。
InnoDB和MyISAM是許多人在使用MySQL時最常用的兩個表類型,這兩個表類型各有優劣:
MyISAM類型不支持事務處理等高級處理,而InnoDB類型支持。MyISAM類型的表強調的是性能,其執行數度比InnoDB類型更快,但是不提供事務支持,而InnoDB提供事務支持已經外部鍵等高級數據庫功能。
RPC為什么用http做通信?
RPC(Remote Produce Call)是一種概念,HTTP是一種協議,RPC可以通過HTTP來實現。RPC也可以使用socket來實現,但是問題就是socket是阻塞式的,並不好,大多RPC框架可能會使用netty來實現,而netty支持多種通訊協議,HTTP、WebSocket等協議,也可以使用自定義的協議進行通訊。
之所以使用HTTP協議,個人認為有可能是HTTP協議的特點決定的,還有就是HTTP比較成熟。
補充:
HTTP協議的主要特點概括如下:
1.支持客戶/服務器模式。
2.簡單快速:客戶向服務器請求服務時,只需傳送請求方法和路徑。請求方法常用的有GET、HEAD、POST。每種方法規定了客戶與服務器聯系的類型不同。由於HTTP協議簡單,使得HTTP服務器的程序規模小,因而通信速度很快。
3.靈活:HTTP允許傳輸任意類型的數據對象。正在傳輸的類型由Content-Type加以標記。
4.無連接:無連接的含義是限制每次連接只處理一個請求。服務器處理完客戶的請求,並收到客戶的應答后,即斷開連接。采用這種方式可以節省傳輸時間。
5.無狀態:HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。缺少狀態意味着如果后續處理需要前面的信息,則它必須重傳,這樣可能導致每次連接傳送的數據量增大。另一方面,在服務器不需要先前信息時它的應答就較快。
RPC兩端如何進行負載均衡?
個人理解,常見的算法是輪詢(將請求輪流的分配到后端服務器上,均衡的對待后端的每一台服務器,而不關心服務器的實際連接數和當前的系統負載),擴展一下,是加權輪詢法(每個主機輪詢的多少)。
比如dubbo,可以使用zookeeper作為注冊中心,將服務注冊到zookeeper,進行負載均衡。
mycat分庫分表、讀寫分離的實現
MyCat是mysql中間件,其核心就是分表分庫,具體如何實現分表分庫,由配置文件的配置決定:
配置文件,conf目錄下主要以下三個需要熟悉。
server.xml Mycat的配置文件,設置賬號、參數等
schema.xml Mycat對應的物理數據庫和數據庫表的配置
rule.xml Mycat分片(分庫分表)規則
分庫分表配置:在 rule.xml 中進行配置
讀寫分離配置:schema.xml 中的 dataHost 的 balance 屬性為1、2 或者 3,因為 為 0 表示不支持讀寫分離。參考:https://www.cnblogs.com/ivictor/p/5131480.html
補充:
MyCat是一個開源的分布式數據庫系統,MyCat是mysql中間件,是一個實現了MySQL協議的服務器,前端用戶可以把它看作是一個數據庫代理,其核心功能是分表分庫,即將一個大表水平分割為N個小表,存儲在后端MySQL服務器里或者其他數據庫里。
分布式數據如何保證數據一致性
在分布式系統來說,如果不想犧牲一致性,CAP 理論告訴我們只能放棄可用性,這顯然不能接受。 強一致: 當更新操作完成之后,任何多個后續進程或者線程的訪問都會返回最新的更新過的值。這種是對用戶最友好的,就是用戶上一次寫什么,下一次就保證能讀到什么。根據 CAP 理論,這種實現需要犧牲可用性。 弱一致性: 系統並不保證續進程或者線程的訪問都會返回最新的更新過的值。系統在數據寫入成功之后,不承諾立即可以讀到最新寫入的值,也不會具體的承諾多久之后可以讀到。 最終一致性: 弱一致性的特定形式。系統保證在沒有后續更新的前提下,系統最終返回上一次更新操作的值。在沒有故障發生的前提下,不一致窗口的時間主要受通信延遲,系統負載和復制副本的個數影響。DNS 是一個典型的最終一致性系統。 互聯網系統大多將強一致性需求轉換成最終一致性的需求,既保證一致性,又保證可用性。 保證最終一致性的 3 種解決方案: 1、經典方案 - eBay 模式 核心是將需要分布式處理的任務通過消息日志的方式來異步執行。消息日志可以存儲到本地文本、數據庫或消息隊列,再通過業務規則自動或人工發起重試。 一個最常見的場景,如果產生了一筆交易,需要在交易表增加記錄,同時還要修改用戶表的金額。這兩個表屬於不同的遠程服務,所以就涉及到分布式事務一致性的問題。 以上場景解決方案:將主要修改操作以及更新用戶表的消息放在一個本地事務來完成。同時為了避免重復消費用戶表消息帶來的問題,增加一個更新記錄表 updates_applied 來記錄已經處理過的消息。 2、 隨着業務規模不斷地擴大,電商網站一般都要面臨拆分之路。 將原來一個單體應用拆分成多個不同職責的子系統。比如以前可能將面向用戶、客戶和運營的功能都放在一個系統里,現在拆分為訂單中心、代理商管理、運營系統、報價中心、庫存管理等多個子系統。 還是和方案1一樣,優先使用異步消息。 拆分多了之后,涉及到一個問題:分布式事物。 處理分布式事物,將分布式事務轉換為多個本地事務,然后依靠重試等方式達到最終一致性。如果其中的一個事物確實是執行不成功,則考慮回滾整個分布式事物。 比如 A 同步調用 B 和 C,比如 B 是扣庫存服務,在第一次調用的時候因為某種原因失敗了,但是重試的時候庫存已經變為 0,無法重試成功,這個時候只有回滾 A 和 C 了。 3、 如果分布式事物交易訂單中是10個步驟,最后一個步驟操作失敗了,其他步驟都要回滾,步驟過多,可以這樣: 首先創建一個不可見訂單,其中某一步失敗后,發送一個MQ廢單消息(如果消息發送失敗,本地繼續異步重試),其他步驟收到消息就可以進行回滾。 還可以使用支付寶的 分布式事務服務框架 DTS(由支付寶在 2PC <2 Phase Commit, 兩階段提交> 的基礎上改進而來) 參考:https://www.cnblogs.com/wangdaijun/p/7272677.html
補充:
CAP原則,指的是在一個分布式系統中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),三者不可得兼。
一致性(C):在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的數據副本)
可用性(A):在集群中一部分節點故障后,集群整體是否還能響應客戶端的讀寫請求。(對數據更新具備高可用性)
分區容忍性(P):以實際效果而言,分區相當於對通信的時限要求。系統如果不能在時限內達成數據一致性,就意味着發生了分區的情況,必須就當前操作在C和A之間做出選擇。
高並發請求處理,流量削峰措施有哪些
削峰從本質上來說就是更多地延緩用戶請求,以及層層過濾用戶的訪問需求,遵從“最后落地到數據庫的請求數要盡量少”的原則。
1、消息隊列解決削峰:使用較多的消息隊列有 ActiveMQ、RabbitMQ、 ZeroMQ、Kafka、MetaMQ、RocketMQ 等。
2、流量削峰漏斗:層層削峰
通過在不同的層次盡可能地過濾掉無效請求。
通過CDN過濾掉大量的圖片,靜態資源的請求;
再通過類似Redis這樣的分布式緩存,過濾請求等就是典型的在上游攔截讀請求;
對讀數據不做強一致性校驗,減少因為一致性校驗產生瓶頸的問題;
對寫請求做限流保護,將超出系統承載能力的請求過濾掉(如可以使用 nginx 進行限流)
補充:
CDN 的基本原理是廣泛采用各種緩存服務器,將這些緩存服務器分布到用戶訪問相對集中的地區或網絡中,在用戶訪問網站時,利用全局負載技術將用戶的訪問指向距離最近的工作正常的緩存服務器上,由緩存服務器直接響應用戶請求。
CDN的優勢很明顯:(1)CDN節點解決了跨運營商和跨地域訪問的問題,訪問延時大大降低;(2)大部分請求在CDN邊緣節點完成,CDN起到了分流作用,減輕了源站的負載。
瀏覽器本地緩存失效后,瀏覽器會向CDN邊緣節點發起請求。類似瀏覽器緩存,CDN邊緣節點也存在着一套緩存機制。
可參考:https://www.cnblogs.com/tinywan/p/6067126.html
Redis持久化RDB和AOF 的區別
RDB持久化是指在指定的時間間隔內將內存中的數據集快照寫入磁盤,實際操作過程是fork一個子進程,先將數據集寫入臨時文件,寫入成功后,再替換之前的文件,用二進制壓縮存儲。
AOF持久化以日志的形式記錄服務器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文本的方式記錄,可以打開文件看到詳細的操作記錄。
RDB是二進制存儲的,恢復和啟動更快,AOF 啟動時需要重新加載執行一次所有操作日志 速度相對要慢 但優點是安全性更高。
補充:
RDB優勢:
1、一旦采用該方式,那么你的整個Redis數據庫將只包含一個文件,這對於文件備份而言是非常完美的(比如,你可能打算每個小時歸檔一次最近24小時的數據,同時還要每天歸檔一次最近30天的數據。通過這樣的備份策略,一旦系統出現災難性故障,我們可以非常容易的進行恢復)。
2、對於災難恢復而言,RDB是非常不錯的選擇。因為我們可以非常輕松的將一個單獨的文件壓縮后再轉移到其它存儲介質上。
3、性能最大化。對於Redis的服務進程而言,在開始持久化時,它唯一需要做的只是fork出子進程,之后再由子進程完成這些持久化的工作,這樣就可以極大的避免服務進程執行IO操作了。
4、相比於AOF機制,如果數據集很大,RDB的啟動效率會更高。
RDB劣勢:
1、如果你想保證數據的高可用性,即最大限度的避免數據丟失,那么RDB將不是一個很好的選擇。因為系統一旦在定時持久化之前出現宕機現象,此前沒有來得及寫入磁盤的數據都將丟失。
2、由於RDB是通過fork子進程來協助完成數據持久化工作的,因此,如果當數據集較大時,可能會導致整個服務器停止服務幾百毫秒,甚至是1秒鍾。
AOF優勢:
1、該機制可以帶來更高的數據安全性,即數據持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。
2、由於該機制對日志文件的寫入操作采用的是append模式,因此在寫入過程中即使出現宕機現象,也不會破壞日志文件中已經存在的內容。然而如果我們本次操作只是寫入了一半數據就出現了系統崩潰問題,不用擔心,在Redis下一次啟動之前,我們可以通過redis-check-aof工具來幫助我們解決數據一致性的問題。
3、如果日志過大,Redis可以自動啟用rewrite機制。即Redis以append模式不斷的將修改數據寫入到老的磁盤文件中,同時Redis還會創建一個新的文件用於記錄此期間有哪些修改命令被執行。因此在進行rewrite切換時可以更好的保證數據安全性。
4、AOF包含一個格式清晰、易於理解的日志文件用於記錄所有的修改操作。事實上,我們也可以通過該文件完成數據的重建。
AOF劣勢:
1、對於相同數量的數據集而言,AOF文件通常要大於RDB文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。
2、根據同步策略的不同,AOF在運行效率上往往會慢於RDB。
MQ底層實現原理
就ActiveMQ來說:
消息通信機制:
點對點模式(p2p),每個消息只有1個消費者,它的目的地稱為queue隊列;
發布/訂閱模式(pub/sub),每個消息可以有多個消費者,而且訂閱一個主題的消費者,只能消費自它訂閱之后發布的消息。
底層使用的是RPC調用。
詳細介紹下分布式 一致性Hash算法
典型的應用場景是: 有N台服務器提供緩存服務,需要對服務器進行負載均衡,將請求平均分發到每台服務器上,每台機器負責1/N的服務。
Memcached client也選擇這種算法,解決將key-value均勻分配到眾多Memcached server上的問題。
1、一致性哈希將整個哈希值空間組織成一個虛擬的圓環;
2、將各個服務器使用H進行一個哈希(可以選擇服務器的ip或主機名作為關鍵字進行哈希,這樣每台機器就能確定其在哈希環上的位置);
3、將數據key使用相同的函數H計算出哈希值h,通根據h確定此數據在環上的位置,從此位置沿環順時針“行走”,第一台遇到的服務器就是其應該定位到的服務器。
此算法的容錯性較高,某個服務器掛了之后,會自動定位到下一個主機。
詳細參考:https://www.cnblogs.com/moonandstar08/p/5405991.html
nginx負載均衡的算法
1、輪詢(默認):
每個請求按時間順序逐一分配到不同的后端服務,如果后端某台服務器死機,自動剔除故障系統,使用戶訪問不受影響。
2、weight(輪詢權值):
weight的值越大分配到的訪問概率越高,主要用於后端每台服務器性能不均衡的情況下。或者僅僅為在主從的情況下設置不同的權值,達到合理有效的地利用主機資源。
如:
upstream server_myServer_pool {
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}
這里配置了的 server_myServer_pool 可在 location 中使用,如: location / { proxy_pass http://server_myServer_pool }
3、ip_hash:
每個請求按訪問IP的哈希結果分配,使來自同一個IP的訪客固定訪問一台后端服務器,並且可以有效解決動態網頁存在的session共享問題。
如:
upstream server_myServer_pool {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
4、fair:
比 weight、ip_hash更加智能的負載均衡算法,fair算法可以根據頁面大小和加載時間長短智能地進行負載均衡,也就是根據后端服務器的響應時間 來分配請求,響應時間短的優先分配。Nginx本身不支持fair,如果需要這種調度算法,則必須安裝upstream_fair模塊。
如:
upstream server_myServer_pool {
fair;
server 192.168.0.14:88;
server 192.168.0.15:80;
}
5、url_hash:
按訪問的URL的哈希結果來分配請求,使每個URL定向到一台后端服務器,可以進一步提高后端緩存服務器的效率。Nginx本身不支持url_hash,如果需要這種調度算法,則必須安裝Nginx的hash軟件包。
如:
upstream resinserver{
server 10.0.0.10:7777;
server 10.0.0.11:8888;
hash $request_uri;
hash_method crc32;
}
Nginx 的 upstream 目前支持 哪4 種方式的分配
weight(輪詢權值)、ip_hash(訪問IP的哈希結果分配)、fair(根據頁面大小和加載時間長短智能分配,第三方)、url_hash(按訪問的URL的哈希結果分配,第三方)
詳細見上一個問題
Dubbo默認使用什么注冊中心,還有別的選擇嗎?
Dubbo默認使用的注冊中心是Zookeeper,其他的選擇包括 Multicast、Zookeeper、Redis、Simple 等。
mongoDB、redis和memcached的應用場景,各自優勢
mongoDB(基於分布式文件存儲的數據庫):
文檔型的非關系型數據庫,使用bson結構。其優勢在於查詢功能比較強大,能存儲海量數據,缺點是比較消耗內存。
一般可以用來存放評論等半結構化數據,支持二級索引。適合存儲json類型數據,不經常變化。
redis(內存數據庫):
是內存型數據庫,數據保存在內存中,通過tcp直接存取,優勢是讀寫性能高。
redis是內存型KV數據庫,不支持二級索引,支持list,set等多種數據格式。適合存儲全局變量,適合讀多寫少的業務場景。很適合做緩存。
memcached(內存Cache):
Memcached基於一個存儲鍵/值對的hashmap,是一個高性能的分布式內存對象緩存系統,用於動態Web應用以減輕數據庫負載。
性能:都比較高,性能都不是瓶頸,redis 和 memcache 差不多,要大於 mongodb。
操作的便利性:memcache 數據結構單一(key-value);redis 豐富一些,還提供 list、set、hash 等數據結構的存儲;mongodb 支持豐富的數據表達,索引,最類似關系型數據庫。
談談你性能優化的實踐案例,優化思路?
個人感覺這個問題比較開放,可以舉例上面的問題來回答:
如:
mysql limit分頁如何保證可靠性
高並發請求處理,流量削峰措施有哪些
或者可以舉例分布式架構方面內容:數據庫分表分庫、讀寫分離,使用緩存(分布式緩存),等等
兩千萬用戶並發搶購,你怎么來設計?
此問題和 《高並發請求處理,流量削峰措施有哪些》這個問題類似,可以參考這個問題來回答。
這些就是這一期總結的問題,有些問題總結的可能不到位,也希望還能有下一期,大家共同進步。