2020年 近期出去面試Java的總結(持續更新)

kouxinsu8594發表於2020-06-24

近期出去面試Java的總結

一、Java基礎

1、Java的類載入順序

面試官問:“class A和class B,其中class A中有靜態方法和建構函式,class B中也有靜態方法和建構函式,class A為父類,class B為子類,請問他們的載入順序如何?”

對於有繼承關係的載入順序

關於關鍵字static,大家 都知道它是靜態的,相當於一個全域性變數,也就是這個屬性或者方法是可以通過類來訪問,當class檔案被載入進記憶體,開始初始化的時候,被static修飾的變數或者方法即被分配了記憶體,而其他變數是在物件被建立後,才被分配了記憶體的。
所以在類中,載入順序為:

  1. 首先載入父類的靜態欄位或者靜態語句塊
  2. 子類的靜態欄位或靜態語句塊
  3. 父類普通變數以及語句塊
  4. 父類構造方法被載入
  5. 子類變數或者語句塊被載入
  6. 子類構造方法被載入

詳細可見:
Java的類載入順序

2、Java的建立物件的幾種方式

1、使用new關鍵字

這是我們最常見的也是最簡單的建立物件的方式,通過這種方式我們還可以呼叫任意的建構函式(無參的和有參的)。

User user = new User();

2、使用反射機制

運用反射手段,呼叫Java.lang.Class或者java.lang.reflect.Constructor類的newInstance()例項方法。

1)使用Class類的newInstance方法

可以使用Class類的newInstance方法建立物件。這個newInstance方法呼叫無參的建構函式建立物件。

//建立方法1
User user = (User)Class.forName("根路徑.User").newInstance(); 
//建立方法2(用這個最好)
User user = User.class.newInstance();
2)使用Constructor類的newInstance方法

Class類的newInstance方法很像, java.lang.reflect.Constructor類裡也有一個newInstance方法可以建立物件。我們可以通過這個newInstance方法呼叫有引數的和私有的建構函式。

Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();

這兩種newInstance方法就是大家所說的反射。事實上Class的newInstance方法內部呼叫ConstructornewInstance方法。

3、使用clone方法

無論何時我們呼叫一個物件的clone方法,jvm就會建立一個新的物件,將前面物件的內容全部拷貝進去。用clone方法建立物件並不會呼叫任何建構函式。
要使用clone方法,我們需要先實現Cloneable介面並實現其定義的clone方法。

Employee emp4 = (Employee) emp3.clone();

4、使用反序列化

序列化與反序列化實現方式:序列化的物件所對應的類必須實現Serializable介面或Externalizable介面;

  • Serializable介面序列化舉例:Serializable介面是一個標記介面,不用實現任何方法。一旦實現了此介面,該類的物件就是可序列化的;當然還有Externalizable介面序列化方式,詳細的情況另行介紹;
  • 反序列化:從IO流中恢復物件;

詳細可見:
Java的建立物件的幾種方式

3、java的基礎資料型別

Java語言提供了八種基本型別。六種數字型別(四個整數型,兩個浮點型),一種字元型別,還有一種布林型。

byte:

byte 資料型別是8位、有符號的,以二進位制補碼錶示的整數;
最小值是 -128(-2^7);
最大值是 127(2^7-1);
預設值是 0;
byte 型別用在大型陣列中節約空間,主要代替整數,因為 byte 變數佔用的空間只有 int 型別的四分之一;
例子:byte a = 100,byte b = -50。

short:

short 資料型別是 16 位、有符號的以二進位制補碼錶示的整數
最小值是 -32768(-2^15);
最大值是 32767(2^15 - 1);
Short 資料型別也可以像 byte 那樣節省空間。一個short變數是int型變數所佔空間的二分之一;
預設值是 0;
例子:short s = 1000,short r = -20000。

int:

int 資料型別是32位、有符號的以二進位制補碼錶示的整數;
最小值是 -2,147,483,648(-2^31);
最大值是 2,147,483,647(2^31 - 1);
一般地整型變數預設為 int 型別;
預設值是 0 ;
例子:int a = 100000, int b = -200000。

long:

long 資料型別是 64 位、有符號的以二進位制補碼錶示的整數;
最小值是 -9,223,372,036,854,775,808(-2^63);
最大值是 9,223,372,036,854,775,807(2^63 -1);
這種型別主要使用在需要比較大整數的系統上;
預設值是 0L;
例子: long a = 100000L,Long b = -200000L。
"L"理論上不分大小寫,但是若寫成"l"容易與數字"1"混淆,不容易分辯。所以最好大寫。

float:

float 資料型別是單精度、32位、符合IEEE 754標準的浮點數;
float 在儲存大型浮點陣列的時候可節省記憶體空間;
預設值是 0.0f;
浮點數不能用來表示精確的值,如貨幣;
例子:float f1 = 234.5f。

double:

double 資料型別是雙精度、64 位、符合IEEE 754標準的浮點數;
浮點數的預設型別為double型別;
double型別同樣不能表示精確的值,如貨幣;
預設值是 0.0d;
例子:double d1 = 123.4。

boolean:

boolean資料型別表示一位的資訊;
只有兩個取值:true 和 false;
這種型別只作為一種標誌來記錄 true/false 情況;
預設值是 false;
例子:boolean one = true。

char:

char型別是一個單一的 16 位 Unicode 字元;
最小值是 \u0000(即為0);
最大值是 \uffff(即為65,535);
char 資料型別可以儲存任何字元;
例子:char letter = ‘A’;。

參考:
Java 基本資料型別

4、Java中的CAS

1 CAS是如何保證原子性

CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個運算元:記憶體地址V,舊的預期值A,即將要更新的目標值B。

CAS指令執行時,當且僅當記憶體地址V的值與預期值A相等時,將記憶體地址V的值修改為B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。

擴充套件

1)CAS的缺點:

CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在三大問題。

  1. 迴圈時間長開銷很大。
  2. 只能保證一個變數的原子操作。
  3. ABA問題。

1.1)迴圈時間長開銷很大

CAS 通常是配合無限迴圈一起使用的,我們可以看到 getAndAddInt 方法執行時,如果 CAS 失敗,會一直進行嘗試。如果 CAS 長時間一直不成功,可能會給 CPU 帶來很大的開銷。

1.2)只能保證一個變數的原子操作

當對一個變數執行操作時,我們可以使用迴圈 CAS 的方式來保證原子操作,但是對多個變數操作時,CAS 目前無法直接保證操作的原子性。
但是我們可以通過以下兩種辦法來解決:

  1. 使用互斥鎖來保證原子性;
  2. 將多個變數封裝成物件,通過 AtomicReference 來保證原子性。

1.3)什麼是ABA問題?ABA問題怎麼解決?

如果記憶體地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然為A,那我們就能說它的值沒有被其他執行緒改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。

Java併發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變數值的版本來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

參考:
面試必問的CAS,你懂了嗎?

2 AQS瞭解嗎

AQS核心思想是,如果被請求的共享資源空閒,那麼就將當前請求資源的執行緒設定為有效的工作執行緒,將共享資源設定為鎖定狀態;如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH佇列的變體實現的,將暫時獲取不到鎖的執行緒加入到佇列中。

CLH:Craig、Landin and Hagersten佇列,是單向連結串列,AQS中的佇列是CLH變體的虛擬雙向佇列(FIFO),AQS是通過將每條請求共享資源的執行緒封裝成一個節點來實現鎖的分配。

主要原理圖如下:
在這裡插入圖片描述
AQS使用一個Volatile的int型別的成員變數來表示同步狀態,通過內建的FIFO佇列來完成資源獲取的排隊工作,通過CAS完成對State值的修改。

參考:
從ReentrantLock的實現看AQS的原理及應用

5、HashSet是如何實現的

HashSet實際上是一個HashMap例項,都是一個存放連結串列的陣列。它不保證儲存元素的迭代順序此類允許使用null元素HashSet中不允許有重複元素,這是因為HashSet是基於HashMap實現的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是統一的一個固定物件private static final Object PRESENT = new Object();

HashSet中add方法呼叫的是底層HashMap中的put()方法,而如果是在HashMap中呼叫put,首先會判斷key是否存在,如果key存在則修改value值,如果key不存在這插入這個key-value。而在set中,因為value值沒有用,也就不存在修改value值的說法,因此往HashSet中新增元素,首先判斷元素(也就是key)是否存在,如果不存在這插入,如果存在著不插入,這樣HashSet中就不存在重複值。

所以判斷key是否存在就要重寫元素的類的equals()和hashCode()方法,當向Set中新增物件時,首先呼叫此物件所在類的hashCode()方法,計算次物件的雜湊值,此雜湊值決定了此物件在Set中存放的位置;若此位置沒有被儲存物件則直接儲存,若已有物件則通過物件所在類的equals()比較兩個物件是否相同,相同則不能被新增。

參考:
HashSet的實現原理,簡單易懂

6、你瞭解迭代器嗎?說一說是如何使用的?

迭代器(Iterator)

迭代器是一種設計模式,它是一個物件,它可以遍歷並選擇序列中的物件,而開發人員不需要了解該序列的底層結構。迭代器通常被稱為“輕量級”物件,因為建立它的代價小。
  Java中的Iterator功能比較簡單,並且只能單向移動:
  (1) 使用方法iterator()要求容器返回一個Iterator。第一次呼叫Iterator的next()方法時,它返回序列的第一個元素。注意:iterator()方法是java.lang.Iterable介面,被Collection繼承。
  (2) 使用next()獲得序列中的下一個元素。
  (3) 使用hasNext()檢查序列中是否還有元素。
  (4) 使用remove()將迭代器新返回的元素刪除。

Iterator是Java迭代器最簡單的實現,為List設計的ListIterator具有更多的功能,它可以從兩個方向遍歷List,也可以從List中插入和刪除元素。

public class IteratorTest
{
	public static void main(String[] args)
	{
		List<String> list = new ArrayList<>();
		list.add("aa");
		list.add("bb");
		list.add("cc");
		//		for (Iterator iter = list.iterator(); iter.hasNext(); )
		//		{
		//			String str = (String) iter.next();
		//			System.out.println(str);
		//		}

		//用while迴圈
		Iterator iter = list.iterator();
		while (iter.hasNext())
		{
			String str = (String) iter.next();
			System.out.println(str);
		}
	}
}
 

7、JVM記憶體是如何劃分

根據《Java虛擬機器規範(Java SE 7版)》的規定,Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域:程式計數器,Java虛擬機器棧,本地方法棧,Java堆,方法區
HotSpot主要有:虛擬機器棧,堆,程式計數器,Metaspace,直接記憶體
下圖為各個區域以及進一步細化圖:
在這裡插入圖片描述
其中,元空間(Metaspace)的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。

因此,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過以下引數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
-XX:MaxMetaspaceSize,最大空間,預設-1,即沒有限制,或者說只受本地記憶體大小。
-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集。

對於方法區,Java8之後的變化:
1.移除了永久代(PermGen),替換為元空間(Metaspace);
2.永久代中的 class metadata 轉移到了 native memory(本地記憶體,而不是虛擬機器);
3.永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
4.永久代引數 (PermSize MaxPermSize) -> 元空間引數(MetaspaceSize MaxMetaspaceSize)

1 JVM執行時資料區

1)程式計數器(Program Counter Register)

程式計數器(Program Counter Register)是一塊較小的記憶體空間,可以看做當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。如果當前方法是本地方法,PC 暫存器的值就是 undefined,該區域無OutOfMemoryError情況。

2)Java虛擬機器棧(Java Virtual Machine Stacks)

Java Virtual Machine Stacks,也是執行緒私有的,它的生命週期與執行緒相同。
虛擬機器棧描述的是Java方法執行的記憶體模型(非native方法)

每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表,運算元棧,動態連結,方法出口等資訊。

每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程,當方法被呼叫則入棧,一旦完成呼叫則出棧。所有的棧幀都出棧後,執行緒就結束了。

區域性變數表存放了編譯器可知的各種基本資料型別、物件引用、returnAddress型別。區域性變數表所需的記憶體空間在編譯器完成分配。當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變數表的大小。

基本型別boolean, byte, char, short, int, float, long, double,其中long和double佔用2個區域性變數空間slot其餘的佔用1個;

物件引用reference型別,它不等同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制程式碼或其他與此物件相關的位置;

returnAddress型別:指向了一條位元組碼指令的地址

在Java虛擬機器規範中,對這個區域規定了兩種異常:執行緒請求的棧深度大於虛擬機器所允許的深度,丟擲StackOverflowError異常;如果虛擬機器棧可以動態擴充套件(目前大部分的Java虛擬機器都可動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體,丟擲OutOfMemoryError異常。

3)本地方法棧(Native Method Stack)

Native Method Stack與虛擬機器棧的作用非常相似,區別是:虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法
本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊

備註:HotSpot直接把本地方法棧和虛擬機器棧合二為一。本地方法棧區域也會丟擲StackOverflowErrorOutOfMemoryError異常。

4)Java堆(Java Heap)

Java Heap是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此區域的唯一目的就是存放物件例項(Java虛擬機器規範中的描述時:所有的物件例項以及陣列都要在堆上分配)

Java堆是GC的主要區域,因此很多時候也被稱為GC堆。

從記憶體分配的角度來看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)

從記憶體回收的角度來看,由於現在收集器基本都採用分代收集演算法,所以Java堆中還可以細分為:新生代和老年代,在細緻一點的有Eden空間,From Survivor空間,To Survivor空間等。

備註:有OOM異常
1.對dump出來的堆快照分析出記憶體洩漏還是記憶體溢位
2.記憶體洩漏: 檢視洩漏物件的GCRoots的引用鏈,通過怎樣的路徑與GCRoots相關聯導致垃圾收集器無法自動回收
3 不存在洩漏,則物件必須存活,則檢查-Xmx與-Xms 及程式碼檢查物件生命週期

5)方法區(Method Area)

Method Area是各個執行緒共享記憶體區域,用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。

執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本,欄位,方法,介面等描述資訊,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。
執行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性

6)補充

1)直接記憶體(Direct Memory)

Direct Memory,並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但執行緒共享。

在jdk1.4加入的NIO類,引入了一種基於通道(Chanel)與緩衝區(Buffer)的IO方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料

備註:本機直接記憶體的分配不會受到Java堆大小的限制,受到本機總記憶體和處理器定址空間的限制,有OOM異常

7.1)其中執行緒共享的區域是:

  1. Java堆區
  2. 方法區
  3. 直接記憶體(不屬於執行時資料區,但執行緒共享)

7.2)其中執行緒私有的區域是:

  1. Java虛擬機器棧
  2. 本地方法棧
  3. 程式計數器

7.3)其中會出現oom的區域是:

  1. Java虛擬機器棧
  2. Java堆區
  3. 直接記憶體
  4. 本地方法棧

參考:
JVM記憶體劃分

2 OOM發生的位置以及如何處理

1)Java堆溢位

這種場景最為常見,報錯資訊:

java.lang.OutOfMemoryError: Java heap space
原因
  1. 程式碼中可能存在大物件分配
  2. 可能存在記憶體洩露,導致在多次GC之後,還是無法找到一塊足夠大的記憶體容納當前物件。
解決方法
  1. 檢查是否存在大物件的分配,最有可能的是大陣列分配
  2. 通過jmap命令,把堆記憶體dump下來,使用mat工具分析一下,檢查是否存在記憶體洩露的問題
  3. 如果沒有找到明顯的記憶體洩露,使用-Xmx加大堆記憶體
  4. 還有一點容易被忽略,檢查是否有大量的自定義的 Finalizable物件,也有可能是框架內部提供的,考慮其存在的必要性

2)永久代/元空間溢位

報錯資訊:

java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace
原因

永久代是 HotSot 虛擬機器對方法區的具體實現,存放了被虛擬機器載入的類資訊、常量、靜態變數、JIT編譯後的程式碼等。
JDK8後,元空間替換了永久代,元空間使用的是本地記憶體,還有其它細節變化:

  • 字串常量由永久代轉移到堆中
  • 和永久代相關的JVM引數已移除

可能原因有如下幾種:

  1. 在Java7之前,頻繁的錯誤使用String.intern()方法
  2. 執行期間生成了大量的代理類,導致方法區被撐爆,無法解除安裝
  3. 應用長時間執行,沒有重啟
解決方法

因為該OOM原因比較簡單,解決方法有如下幾種:

  1. 檢查是否永久代空間或者元空間設定的過小
  2. 檢查程式碼中是否存在大量的反射操作
  3. dump之後通過mat檢查是否存在大量由於反射生成的代理類
  4. 放大招,重啟JVM

3 GC overhead limit exceeded

這個異常比較的罕見,報錯資訊:

java.lang.OutOfMemoryError:GC overhead limit exceeded

原因

這個是JDK6新加的錯誤型別,一般都是堆太小導致的。Sun 官方對此的定義:超過98%的時間用來做GC並且回收了不到2%的堆記憶體時會丟擲此異常。

解決方法

  1. 檢查專案中是否有大量的死迴圈或有使用大記憶體的程式碼,優化程式碼。
  2. 新增引數-XX:-UseGCOverheadLimit禁用這個檢查,其實這個引數解決不了記憶體問題,只是把錯誤的資訊延後,最終出現 java.lang.OutOfMemoryError: Java heap space
  3. dump記憶體,檢查是否存在記憶體洩露,如果沒有,加大記憶體。

4 方法棧溢位

報錯資訊:

java.lang.OutOfMemoryError : unable to create new native Thread

原因

出現這種異常,基本上都是建立的了大量的執行緒導致的,以前碰到過一次,通過jstack出來一共8000多個執行緒。

解決方法

  1. 通過-Xss降低的每個執行緒棧大小的容量
  2. 執行緒總數也受到系統空閒記憶體和作業系統的限制,檢查是否該系統下有此限制:
    • /proc/sys/kernel/pid_max/
    • proc/sys/kernel/thread-max
    • maxuserprocess(ulimit -u)
    • /proc/sys/vm/maxmapcount

非常規溢位下面這些OOM異常,可能大部分的同學都沒有碰到過,但還是需要了解一下

5 分配超大陣列

報錯資訊 :

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因

這種情況一般是由於不合理的陣列分配請求導致的,在為陣列分配記憶體之前,JVM 會執行一項檢查。要分配的陣列在該平臺是否可以定址(addressable),如果不能定址(addressable)就會丟擲這個錯誤。

解決方法

就是檢查你的程式碼中是否有建立超大陣列的地方。

6 swap溢位

報錯資訊 :

java.lang.OutOfMemoryError: Out of swap space

原因

這種情況一般是作業系統導致的,可能的原因有:

  1. swap 分割槽大小分配不足;
  2. 其他程式消耗了所有的記憶體。

解決方案

  1. 其它服務程式可以選擇性的拆分出去
  2. 加大swap分割槽大小,或者加大機器記憶體大小

7 本地方法溢位

報錯資訊 :

java.lang.OutOfMemoryError: stack_trace_with_native_method

本地方法在執行時出現了記憶體分配失敗,和之前的方法棧溢位不同,方法棧溢位發生在 JVM 程式碼層面,而本地方法溢位發生在JNI程式碼或本地方法處。這個異常出現的概率極低,只能通過作業系統本地工具進行診斷,難度有點大,還是放棄為妙。

參考:
JVM出現OOM的八種原因及解決辦法

8、陣列和列表的轉換

1 List轉陣列

1)使用List.toArray()方法

public class ListToArraysTest
{
	public static void main(String[] args)
	{
		//要轉換的list集合
		List<String> testList = new ArrayList<String>()
		{{
			add("aa");
			add("bb");
			add("cc");
		}};

		//使用toArray(T[] a)方法
		String[] array2 = testList.toArray(new String[testList.size()]);

		//列印該陣列
		for (int i = 0; i < array2.length; i++)
		{
			System.out.println(array2[i]);
		}

	}
}

2)使用for迴圈

public class ListToArraysTest
{
	public static void main(String[] args)
	{
		//要轉換的list集合
		List<String> testList = new ArrayList<String>()
		{{
			add("aa");
			add("bb");
			add("cc");
		}};

		//初始化需要得到的陣列
		String[] array = new String[testList.size()];

		//使用for迴圈得到陣列
		for(int i = 0; i < testList.size();i++){
			array[i] = testList.get(i);
		}

		//列印陣列
		for(int i = 0; i < array.length; i++){
			System.out.println(array[i]);
		}
	}
}

2 陣列轉List

1)使用Arrays.asList()

public class ArraysToListTest
{
	public static void main(String[] args)
	{
		String[] arrays = new String[] { "aa", "bb", "cc" };
		ArrayList<String> arrayList = new ArrayList<String>(
				Arrays.asList(arrays));
		System.out.println(arrayList);
	}
}

2)使用Collections.addAll()

public class ArraysToListTest
{
	public static void main(String[] args)
	{
		String[] arrays = new String[] { "aa", "bb", "cc" };

		List<String> list = new ArrayList<String>(arrays.length);
		Collections.addAll(list, arrays);

		System.out.println(list);
	}
}

3)使用Arrays.asList()的另一種寫法

public class ArraysToListTest
{
	public static void main(String[] args)
	{
		String[] arrays = new String[] { "aa", "bb", "cc" };
		List<String> list = Arrays.asList(arrays);
		System.out.println(list);
		list.add("dd");
		System.out.println("add後:"+list);
	}
}

這裡在list繼續使用add()方法之後會報如下UnsupportedOperationException異常

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at com.sunsun.designmodedemo.test.ArraysToListTest.main(ArraysToListTest.java:28)

原因如下:

因為asList()返回的列表的大小是固定的。事實上,返回的列表不是java.util.ArrayList,而是定義在java.util.Arrays中一個私有靜態類。我們知道ArrayList的實現本質上是一個陣列,而asList()返回的列表是由原始陣列支援的固定大小的列表。這種情況下,如果新增或刪除列表中的元素,程式會丟擲異常UnsupportedOperationException。

4)使用for迴圈

public class ArraysToListTest
{
	public static void main(String[] args)
	{
		String[] arrays = new String[] { "aa", "bb", "cc" };
		//初始化list
		List<String> list3 = new ArrayList<String>();
		//使用for迴圈轉換為list
		for(String str : arrays){
			list3.add(str);
		}
		//列印得到的list
		System.out.println(list3);
	}
}

參考:
java List和陣列相互轉換方法

9、Java的容器有哪些

1 常用容器的圖錄

在這裡插入圖片描述

2、如何在列表中找出相同的物件或值

1)如何在列表中找出相同的物件

面試官問:“假如有一個List,存放的是Bean型別的資料,如List<Student>Student實體類中包含idname ,如何篩選出id相同的List。”

使用Collectors.groupingBy()方法

Map<String, List<Student>> listMap = list.stream()
				.collect(Collectors.groupingBy(Student::getId));

完整程式碼:

@Data
@NoArgsConstructor
public class Student
{
	private String id;
	private String name;
}
@Slf4j
public class StudentTest
{

	@Test
	public void getStudentList()
	{
		List<Student> list = new ArrayList<>();

		Student student1 = new Student();
		student1.setId("123456");
		student1.setName("張三");

		Student student2 = new Student();
		student2.setId("567890");
		student2.setName("李四");

		Student student3 = new Student();
		student3.setId("567890");
		student3.setName("李四");

		list.add(student1);
		list.add(student2);
		list.add(student3);

		Map<String, List<Student>> listMap = list.stream()
				.collect(Collectors.groupingBy(Student::getId));

		for (Map.Entry<String, List<Student>> entry : listMap.entrySet())
		{
			String id = entry.getKey();
			String s = JSONObject.toJSONString(entry.getValue());

			log.info("id ={},value = {}",id,s);
		}

	}
}

2)List集合取交集(包含並集、差集、去重並集)

public class ListTest
{
	@Test
	public void getList()
	{
		List<String> list1 = new ArrayList<String>();
		list1.add("1");
		list1.add("2");
		list1.add("3");
		list1.add("5");
		list1.add("6");

		List<String> list2 = new ArrayList<String>();
		list2.add("2");
		list2.add("3");
		list2.add("7");
		list2.add("8");

		// 交集
		List<String> intersection = list1.stream()
				.filter(item -> list2.contains(item)).collect(toList());
		System.out.println("---交集 intersection---");
		intersection.parallelStream().forEach(System.out ::println);

		// 差集 (list1 - list2)
		List<String> reduce1 = list1.stream().filter(item -> !list2.contains(item))
				.collect(toList());
		System.out.println("---差集 reduce1 (list1 - list2)---");
		reduce1.parallelStream().forEach(System.out ::println);

		// 差集 (list2 - list1)
		List<String> reduce2 = list2.stream().filter(item -> !list1.contains(item))
				.collect(toList());
		System.out.println("---差集 reduce2 (list2 - list1)---");
		reduce2.parallelStream().forEach(System.out ::println);

		// 並集
		List<String> listAll = list1.parallelStream().collect(toList());
		List<String> listAll2 = list2.parallelStream().collect(toList());
		listAll.addAll(listAll2);
		System.out.println("---並集 listAll---");
		listAll.parallelStream().forEachOrdered(System.out ::println);

		// 去重並集
		List<String> listAllDistinct = listAll.stream().distinct()
				.collect(toList());
		System.out.println("---得到去重並集 listAllDistinct---");
		listAllDistinct.parallelStream().forEachOrdered(System.out ::println);

		System.out.println("---原來的List1---");
		list1.parallelStream().forEachOrdered(System.out ::println);
		System.out.println("---原來的List2---");
		list2.parallelStream().forEachOrdered(System.out ::println);

	}

}

參考:
java8兩個List集合取交集、並集、差集、去重並集

10、執行緒池你瞭解嗎?執行緒池的引數是如何的?

1 執行緒池

執行緒池通過 ThreadPoolExecutor 的方式建立,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險

1)執行緒池生命週期

ThreadPoolExecutor生命週期轉換:在這裡插入圖片描述

  1. RUNNING :能接受新提交的任務,並且也能處理阻塞佇列中的任務;
  2. SHUTDOWN:關閉狀態,不再接受新提交的任務,但卻可以繼續處理阻塞佇列中已儲存的任務。線上程池處於 RUNNING 狀態時,呼叫 SHUTDOWN()方法會使執行緒池進入到該狀態。(finalize() 方法在執行過程中也會呼叫SHUTDOWN()方法進入該狀態);
  3. STOP:不能接受新任務,也不處理佇列中的任務,會中斷正在處理任務的執行緒。線上程池處於 RUNNINGSHUTDOWN 狀態時,呼叫 SHUTDOWNNow() 方法會使執行緒池進入到該狀態;
  4. TIDYING:如果所有的任務都已終止了,workerCount (有效執行緒數) 為0,執行緒池進入該狀態後會呼叫 terminated() 方法進入TERMINATED 狀態。
  5. TERMINATED:在terminated() 方法執行完後進入該狀態,預設terminated()方法中什麼也沒有做。
    進入TERMINATED的條件如下:
    • 執行緒池不是RUNNING狀態;
    • 執行緒池狀態不是TIDYING狀態或TERMINATED狀態;
    • 如果執行緒池狀態是SHUTDOWN並且workerQueue為空;
    • workerCount為0;
    • 設定TIDYING狀態成功。

2)常用的執行緒池

1. new SingleThreadExecutor()
建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

2. new FixedThreadPool()
建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

3. new CachedThreadPool()
建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,
那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。

4. new ScheduledThreadPool()
建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

2 執行緒池引數

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

1)corePoolSize:核心執行緒數

核心執行緒會一直存活,及時沒有任務需要執行
當執行緒數小於核心執行緒數時,即使有執行緒空閒,執行緒池也會優先建立新執行緒處理
設定allowCoreThreadTimeout=true(預設false)時,核心執行緒會超時關閉

2)maximumPoolSize:最大允許執行緒數量

執行緒池內部執行緒數量已經達到核心執行緒數量,即corePoolSize,並且任務佇列已滿,此時如果繼續有任務被提交,將判斷執行緒池內部執行緒總數是否達到maximumPoolSize,如果小於maximumPoolSize,將繼續使用執行緒工廠建立新執行緒。如果執行緒池內執行緒數量等於maximumPoolSize,就不會繼續建立執行緒,將觸發拒絕策略RejectedExecutionHandler。新建立的同樣是一個Work物件,並最終放入workers集合中。

3)keepAliveTime、unit:超出執行緒的存活時間

當執行緒池內部的執行緒數量大於corePoolSize,則多出來的執行緒會在keepAliveTime時間之後銷燬。
如果allowCoreThreadTimeout=true,則會直到執行緒數量=0

4)workQueue:任務佇列

被提交但未執行的任務佇列,它是一個BlockingQueue介面的物件,僅用於存放Runnable物件。ThreadPoolExecutor的建構函式中,可使用以下幾種BlockingQueue,通常有固定數量的ArrayBlockingQueue,無限制的LinkedBlockingQueue
1、直接提交佇列
SynchronousQueue ,這是一個比較特殊的BlockKingQueueSynchronousQueue沒有容量,每一個插入操作都要等待對應的刪除操作,反之 一個刪除操作都要等待對應的插入操作。 也就是如果使用SynchronousQueue,提交的任務不會被真實儲存,而是將新任務交給空閒執行緒執行,如果沒有空閒執行緒,則建立執行緒如果執行緒數都已經大於最大執行緒數,則執行拒絕策略。使用這種佇列,需要將maximumPoolSize設定的非常大,不然容易執行拒絕策略。比如說

沒有最大執行緒數限制的newCachedThreadPool()

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

但是這個在大量任務的時候,會啟用等量的執行緒去處理,有風險造成系統資源不足。

2、有界任務佇列
有界任務佇列可以使用ArrayBlockingQueue實現。需要給一個容量參數列示該佇列的最大值。當有新任務進來時,如果當前執行緒數小於corePoolSize,則會建立新執行緒執行任務。如果大於,則會將任務放到任務佇列中,如果任務佇列滿了,在當前執行緒小於將maximumPoolSize的情況下,將會建立新執行緒,如果大於maximumPoolSize,則執行拒絕策略。
也就是,一階段,當執行緒數小於coresize的時候,建立執行緒;二階段,當執行緒任務數大於coresize的時候,放入到佇列中;三階段,佇列滿,但是還沒大於maxsize的時候,建立新執行緒。 四階段,佇列滿,執行緒數也大於了maxsize, 則執行拒絕策略。
可以發現,有界任務佇列,會大概率將任務保持在coresize上,只有佇列滿了,也就是任務非常繁忙的時候,會到達maxsize

3、無界任務佇列
使LinkedBlockingQueue實現,佇列最大長度限制為Integer.MAX。無界任務佇列,不存在任務入隊失敗的情況, 當任務過來時候,如果執行緒數小於coresize ,則建立執行緒,如果大於,則放入到任務佇列裡面。也就是,執行緒數幾乎會一直維持在coresize大小。FixedThreadPoolSingleThreadPool即是如此。 風險在於,如果任務佇列裡面任務堆積過多,可能導致記憶體不足
4、優先順序任務佇列
使用PrioriBlockingQueue ,特殊的無界佇列,和普通的先進先出佇列不同,它是優先順序高的先出。

5)threadFactory:執行緒工廠

執行緒池內初初始沒有執行緒,任務來了之後,會使用執行緒工廠建立執行緒。

6)rejectedExecutionHandler:任務拒絕處理器

兩種情況會拒絕處理任務:

  • 當執行緒數已經達到maxPoolSize,切佇列已滿,會拒絕新任務
  • 當執行緒池被呼叫shutdown()後,會等待執行緒池裡的任務執行完畢,再shutdown。如果在呼叫shutdown()和執行緒池真正shutdown之間提交任務,會拒絕新任務

執行緒池會呼叫rejectedExecutionHandler來處理這個任務。如果沒有設定預設是AbortPolicy,會丟擲異常

ThreadPoolExecutor類有幾個內部實現類來處理這類情況:

  • AbortPolicy 丟棄任務,拋執行時異常
  • CallerRunsPolicy 執行任務
  • DiscardPolicy 忽視,什麼都不會發生
  • DiscardOldestPolicy從佇列中踢出最先進入佇列(最後一個執行)的任務
    實現RejectedExecutionHandler介面,可自定義處理器

參考:
JAVA執行緒池引數詳解
Java執行緒池,知道這些就夠了
執行緒池各個引數詳解以及如何自定義執行緒池
深入理解 Java 執行緒池:ThreadPoolExecutor

12、常用的時間類

在Java 8之前,所有關於時間和日期的API都存在各種使用方面的缺陷,主要有:

  1. Java的 java.util.Datejava.util.Calendar類易用性差,不支援時區,而且他們都不是執行緒安全的;
  2. 用於格式化日期的類DateFormat被放在java.text包中,它是一個抽象類,所以我們需要例項化一個SimpleDateFormat物件來處理日期格式化,並且DateFormat也是非執行緒安全,這意味著如果你在多執行緒程式中呼叫同一個DateFormat物件,會得到意想不到的結果。
  3. 對日期的計算方式繁瑣,而且容易出錯,因為月份是從0開始的,從Calendar中獲取的月份需要加一才能表示當前月份。

在Java1.8中使用新的時間和日期API
Java 8的日期和時間類包含LocalDateLocalTimeInstantDuration 以及 Period,這些類都包含在 java.time 包中,Java 8 新的時間API的使用方式,包括建立、格式化、解析、計算、修改。
1) LocalDate(只會獲取年月日)
2) LocalTime (只會獲取時分秒)
3)LocalDateTime 獲取年月日時分秒,相當於LocalDate + LocalTime
4)Instant用於表示一個時間戳,它與我們常使用的System.currentTimeMillis()有些類似,不過Instant可以精確到納秒(Nano-Second),System.currentTimeMillis()方法只精確到毫秒(Milli-Second)。ofEpochSecond()方法的第一個引數為秒,第二個引數為納秒,下面的程式碼表示從1970-01-01 00:00:00開始後兩分鐘的10萬納秒的時刻,

Instant instant = Instant.ofEpochSecond(120, 100000);

控制檯上的輸出為:

1970-01-01T00:02:00.000100Z

5)Duration 表示一個時間段

LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0);    // 2017-01-05 10:07:00
LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0);     // 2017-02-05 10:07:00
Duration duration = Duration.between(from, to);     // 表示從 2017-01-05 10:07:00 到 2017-02-05 10:07:00 這段時間

long days = duration.toDays();              // 這段時間的總天數
long hours = duration.toHours();            // 這段時間的小時數
long minutes = duration.toMinutes();        // 這段時間的分鐘數
long seconds = duration.getSeconds();       // 這段時間的秒數
long milliSeconds = duration.toMillis();    // 這段時間的毫秒數
long nanoSeconds = duration.toNanos();      // 這段時間的納秒數

6)Period 是以年月日來衡量一個時間段,比如2年3個月6天:

Period period = Period.of(2, 3, 6);

7) 時間格式化
DateTimeFormatter 類用於處理日期格式化操作,它被包含在java.time.format包中,Java 8的日期類有一個format()方法用於將日期格式化為字串

LocalDateTime dateTime = LocalDateTime.now();
String strDate1 = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE);    // 20170105

8)時區
java.time.ZoneId 時區類,ZoneId物件可以通過ZoneId.of()方法建立,也可以通過 ZoneId.systemDefault()獲取系統預設時區:

ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
ZoneId systemZoneId = ZoneId.systemDefault();

參考:
Java 8新特性(四):新的時間和日期API
為什麼不建議使用Date,而是使用Java8新的時間和日期API?

13、Java動態代理

動態代理:代理類在程式執行時建立的代理方式。也就是說,這種情況下,代理類並不是在Java程式碼中定義的,而是在執行時根據我們在Java程式碼中的“指示”動態生成的。相比於靜態代理, 動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類的函式。

在執行期動態建立一個interface例項的方法如下:

  1. 定義一個InvocationHandler例項,它負責實現介面的方法呼叫;
  2. 通過Proxy.newProxyInstance()建立interface例項,它需要3個引數:
    1) 使用的ClassLoader,通常就是介面類的ClassLoader
    2) 需要實現的介面陣列,至少需要傳入一個介面進去;
    3) 用來處理介面方法呼叫的InvocationHandler例項。
  3. 將返回的Object強制轉型為介面。

總結來說:首先通過newProxyInstance方法獲取代理類例項,而後我們便可以通過這個代理類例項呼叫代理類的方法,對代理類的方法的呼叫實際上都會呼叫中介類(呼叫處理器)的invoke方法,在invoke方法中我們呼叫委託類的相應方法,並且可以新增自己的處理邏輯。

參考:
Java動態代理
動態代理

擴充套件

1 靜態代理

靜態代理代理方式需要代理物件和目標物件實現一樣的介面。

優點
可以在不修改目標物件的前提下擴充套件目標物件的功能。

缺點

  • 冗餘。由於代理物件要實現與目標物件一致的介面,會產生過多的代理類。
  • 不易維護。一旦介面增加方法,目標物件與代理物件都要進行修改。

2 cglib代理

cglib (Code Generation Library )是一個第三方程式碼生成類庫,執行時在記憶體中動態生成一個子類物件從而實現對目標物件功能的擴充套件。

cglib特點

  • JDK的動態代理有一個限制,就是使用動態代理的物件必須實現一個或多個介面。
    如果想代理沒有實現介面的類,就可以使用CGLIB實現。
  • CGLIB是一個強大的高效能的程式碼生成包,它可以在執行期擴充套件Java類與實現Java介面。
    它廣泛的被許多AOP的框架使用,例如Spring AOPdynaop,為他們提供方法的interception(攔截)。
  • CGLIB包的底層是通過使用一個小而快的位元組碼處理框架ASM,來轉換位元組碼並生成新的類。
    不鼓勵直接使用ASM,因為它需要你對JVM內部結構包括class檔案的格式和指令集都很熟悉。

cglib與動態代理最大的區別就是

  • 使用動態代理的物件必須實現一個或多個介面
  • 使用cglib代理的物件則無需實現介面,達到代理類無侵入。

參考:
Java三種代理模式:靜態代理、動態代理和cglib代理

14、HashMap的擴容機制

1 什麼時候擴容

HashMap的容量是有限的。當經過多次元素插入的時候,使得HashMap達到一定的飽和度,Key對映位置的機率不斷變大。這個時候,HashMap就需要擴容了,也就是resize()

HashMap擴容的條件:
1、HashMap中的資料達到閾值。
2、出現hash碰撞的情況。

2 怎麼擴容

1)建立一個新的Entry空陣列,使用的是2次冪的擴充套件(長度是原來的2倍)。
2)ReHash:遍歷原Entry陣列,把所有的Entry重新Hash到新陣列。
為什麼要重新Hash呢?因為長度擴大以後,Hash的規則也隨之改變。
Hash的公式—>index = HashCode(Key) & (Length - 1)
擴容前:
在這裡插入圖片描述
擴容後:
在這裡插入圖片描述
參考:
一個HashMap跟面試官扯了半個小時
阿里面試官沒想到一個HashMap,我能跟他扯半小時
HashMap擴容

15、concurrentashmap和HashTable的區別

ConcurrentHashMapHashtable 的區別主要體現在實現執行緒安全的方式上不同。

底層資料結構:
ConcurrentHashMap:
JDK1.7的 ConcurrentHashMap 底層採用 分段的陣列+連結串列 實現,JDK1.8 採用的資料結構跟HashMap1.8的結構一樣,陣列+連結串列/紅黑二叉樹

Hashtable :
Hashtable 和 JDK1.8 之前的 HashMap 的底層資料結構類似都是採用 陣列+連結串列 的形式,陣列是 HashMap 的主體,連結串列則是主要為了解決雜湊衝突而存在的;

實現執行緒安全的方式(重要):
① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶陣列進行了分割分段(Segment),每一把鎖只鎖容器其中一部分資料,多執行緒訪問容器裡不同資料段的資料,就不會存在鎖競爭,提高併發訪問率。(預設分配16個Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 陣列+連結串列+紅黑樹的資料結構來實現,併發控制使用 synchronizedCAS來操作。(JDK1.6以後 對synchronized鎖做了很多優化) 整個看起來就像是優化過且執行緒安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的資料結構,但是已經簡化了屬性,只是為了相容舊版本;
Hashtable(同一把鎖) :使用 synchronized來保證執行緒安全,效率非常低下。當一個執行緒訪問同步方法時,其他執行緒也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 新增元素,另一個執行緒不能使用 put 新增元素,也不能使用 get,競爭會越來越激烈效率越低。

參考:
這幾道Java集合框架面試題在面試中幾乎必問

16、自動裝箱和拆箱

1 什麼是自動裝箱和拆箱

1)自動裝箱

自動裝箱就是Java自動將原始型別值轉換成對應的物件,比如將int的變數轉換成Integer物件,這個過程叫做裝箱。

原理:
自動裝箱時編譯器呼叫valueOf將原始型別值轉換成物件

2)自動拆箱

自動拆箱就是Java自動將物件轉成原始型別,將Integer物件轉換成int型別值,這個過程叫做拆箱。
原理:
編譯器通過呼叫類似intValue()doubleValue()這類的方法將物件轉換成原始型別值。

參考:
Java 自動裝箱與拆箱的實現原理

2 裝箱/拆箱有關的問題

1 Integer和int型別

public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
         
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}

返回鍵結果:

true
false

原因:
在通過valueOf方法建立Integer物件的時候,如果數值在[-128,127]之間,便返回指向IntegerCache.cache中已經存在的物件的引用;否則建立一個新的Integer物件。

上面的程式碼中i1i2的數值為100,因此會直接從cache中取已經存在的物件,所以i1i2指向的是同一個物件,而i3i4則是分別指向不同的物件。

參考:
深入剖析Java中的裝箱和拆箱

17、Java Lambda 表示式應用場景

1 列表迭代

對一個列表的每一個元素進行操作,不使用 Lambda 表示式時如下:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(x -> System.out.println(x));

2 Map 對映

使用 Stream 物件的 map 方法將原來的列表經由 Lambda 表示式對映為另一個列表,並通過 collect 方法轉換回 List 型別:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> mapped = numbers.stream().map(x -> x * 2).collect(Collectors.toList());
mapped.forEach(System.out::println);

3 代替 Runnable

以建立執行緒為例,使用 Runnable 類的程式碼如下:

Runnable r = new Runnable() {
    @Override
    public void run() {
        //to do something
    }
};
Thread t = new Thread(r);
t.start();

使用 Lambda 表示式:

Runnable r = () -> {
    //to do something
};
Thread t = new Thread(r);
t.start();

或者使用更加緊湊的形式:

Thread t = new Thread(() -> {
    //to do something
});
t.start;

4 Predicate 介面

java.util.function 包中的 Predicate介面可以很方便地用於過濾。如果你需要對多個物件進行過濾並執行相同的處理邏輯,那麼可以將這些相同的操作封裝到filter方法中,由呼叫者提供過濾條件,以便重複使用。
使用 Predicate 介面,將相同的處理邏輯封裝到 filter 方法中,重複呼叫:

public static void main(String[] args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<String> words = Arrays.asList("a", "ab", "abc");

    filter(numbers, x -> (int)x % 2 == 0);
    filter(words, x -> ((String)x).length() > 1);
}

public static void filter(List list, Predicate condition) {
    list.forEach(x -> {
        if (condition.test(x)) {
            //process logic
        }
    })
}

filter 方法也可寫成:

public static void filter(List list, Predicate condition) {
    list.stream().filter(x -> condition.test(x)).forEach(x -> {
        //process logic
    })
}

5 事件監聽

使用 Lambda 表示式,需要編寫多條語句時用花括號包圍起來:

button.addActionListener(e -> {
    //handle the event
});

參考:
Java Lambda 表示式的常見應用場景

18、JDK1.7和JDK1.8有什麼差別

JDK1.8增加了主要有:

  • Lambda 表示式 − Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞到方法中)。

  • 方法引用 − 方法引用提供了非常有用的語法,可以直接引用已有Java類或物件(例項)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘程式碼。

  • 預設方法 − 預設方法就是一個在介面裡面有了一個實現的方法。

  • 新工具 − 新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器jdeps。

  • Stream API −新新增的Stream API(java.util.stream) 把真正的函數語言程式設計風格引入到Java中。

  • Date Time API − 加強對日期與時間的處理。

  • Optional 類 − Optional 類已經成為 Java 8 類庫的一部分,用來解決空指標異常。

  • Nashorn, JavaScript 引擎 − Java 8提供了一個新的Nashorn javascript引擎,它允許我們在JVM上執行特定的javascript應用。

19、Java中Synchronized的用法

synchronized是Java中的關鍵字,是一種同步鎖。它修飾的物件有以下幾種:
  1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;
  2. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件;
  3. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;
  4. 修改一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。

參考:
Java中Synchronized的用法(簡單介紹)

20、Java類載入過程

系統載入 Class 型別的檔案主要三步:載入->連線->初始化。連線過程又可分為三步:驗證->準備->解析
在這裡插入圖片描述
1)載入
類載入過程的第一步,主要完成下面3件事情:

  • 通過全類名獲取定義此類的二進位制位元組流
  • 將位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構
  • 在記憶體中生成一個代表該類的 Class 物件,作為方法區這些資料的訪問入口

2)驗證
在這裡插入圖片描述
3)準備
準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:

  • 這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在 Java 堆中。
  • 這裡所設定的初始值"通常情況"下是資料型別預設的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,那麼 value 變數在準備階段的初始值就是 0 而不是111(初始化階段才會賦值)。特殊情況:比如給 value 變數加上了 fianl 關鍵字public static final int value=111 ,那麼準備階段 value 的值就被賦值為 111。

4)解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫限定符7類符號引用進行。

5)初始化
初始化是類載入的最後一步,也是真正執行類中定義的 Java 程式程式碼(位元組碼),初始化階段是執行類構造器 <clinit> ()方法的過程。

參考:
JavaGuide/docs/java/jvm/類載入過程.md

21、static關鍵字的作用

static的主要意義是在於建立獨立於具體物件的域變數或者方法。以致於即使沒有建立物件,也能使用屬性和呼叫方法!

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態程式碼塊以優化程式效能static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被載入的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

static的獨特之處
1、被static修飾的變數或者方法是獨立於該類的任何物件,也就是說,這些變數和方法不屬於任何一個例項物件,而是被類的例項物件所共享
2、在該類被第一次載入的時候,就會去載入被static修飾的部分,而且只在類第一次使用時載入並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。
3、static變數值在類載入的時候分配空間,以後建立類物件的時候不會重新分配。賦值的話,是可以任意賦值的!
4、被static修飾的變數或者方法是優先於物件存在的,也就是說當一個類載入完畢之後,即便沒有建立物件,也可以去訪問。

參考:
深入理解static關鍵字

二、Spring相關的問題

1、Spring Aop怎麼使用

AOP:是一種面向切面的程式設計正規化,是一種程式設計思想,旨在通過分離橫切關注點,提高模組化,可以跨越物件關注點。Aop的典型應用即spring的事務機制,日誌記錄。 利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。主要功能是:日誌記錄,效能統計,安全控制,事務處理,異常處理等等;主要的意圖是:將日誌記錄,效能統計,安全控制,事務處理,異常處理等程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的程式碼。

AOP技術利用一種稱為“橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“物件”是一個空心的圓柱體,其中封裝的是物件的屬性和行為;那麼面向方面程式設計的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的訊息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。

AspectJ和Spring AOP 是AOP的兩種實現方案,Aspectj是aop的java實現方案,是一種編譯期的用註解形式實現的AOP;Spring aop是aop實現方案的一種,它支援在執行期基於動態代理的方式將aspect織入目的碼中來實現aop,其中動態代理有兩種方式(jdk動態代理和cglib動態代理

參考:
spring專案中aop的使用

2、說一說你對Spring Ioc的理解

控制反轉(IoC), IOC是Inversion of Control的縮寫,多數書籍翻譯成“控制反轉”,簡單來說就是把複雜系統分解成相互作用合作的物件,這些物件類通過封裝以後,內部實現對外部是透明的,從而降低了解決問題的複雜度,而且可以靈活地被重用和擴充套件.IOC理論提出的觀點大體是這樣的:藉助於“第三方”實現具有依賴關係的物件之間的,如圖:
在這裡插入圖片描述
IOC的別名:依賴注入(DI)
實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在執行期間,動態地將某種依賴關係注入到物件之中。
所以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,指就是通過引入IOC容器,利用依賴關係注入的方式,實現物件之間的解耦

參考:
Spring的IOC原理(通俗易懂)

3、Spring事務

Spring 支援兩種方式的事務管理

1 程式設計式事務管理

通過 TransactionTemplate或者TransactionManager手動管理事務,實際應用中很少使用,但是對於你理解 Spring 事務管理原理有幫助。

使用TransactionTemplate進行程式設計式事務管理的示例程式碼如下:

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

                try {

                    // ....  業務程式碼
                } catch (Exception e){
                    //回滾
                    transactionStatus.setRollbackOnly();
                }

            }
        });
}

2 宣告式事務管理

推薦使用(程式碼侵入性最小),實際是通過 AOP 實現(基於@Transactional的全註解方式使用最多)。

使用@Transactional註解進行事務管理的示例程式碼如下:

@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
  //do something
  B b = new B();
  C c = new C();
  b.bMethod();
  c.cMethod();
}

事務的其他詳解:Spring事務詳解

3、spring是如何載入事務的

以註解方式為例子

  1. 配置檔案開啟註解驅動,在相關的類和方法上通過註解@Transactional標識。
  2. spring在啟動的時候會去解析生成相關的bean,這時候會檢視擁有相關注解的類和方法,並且為這些類和方法生成代理,並根據@Transaction的相關引數進行相關配置注入,這樣就在代理中為我們把相關的事務處理掉了(開啟正常提交事務,異常回滾事務)。
  3. 真正的資料庫層的事務提交和回滾是通過binlog或者redo log實現的。

參考:
一文帶你深入理解Spring事務原理

三、Spring boot和Spring Cloud相關

1、Spring boot的主要優點

  1. 開發基於 Spring 的應用程式很容易。
  2. Spring Boot 專案所需的開發或工程時間明顯減少,通常會提高整體生產力。
  3. Spring Boot不需要編寫大量樣板程式碼、XML配置和註釋。
  4. Spring引導應用程式可以很容易地與Spring生態系統整合,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
  5. Spring Boot遵循“固執己見的預設配置”,以減少開發工作(預設配置可以修改)。
  6. Spring Boot 應用程式提供嵌入式HTTP伺服器,如Tomcat和Jetty,可以輕鬆地開發和測試web應用程式。(這點很贊!普通執行Java程式的方式就能執行基於Spring Boot web 專案,省事很多)
  7. Spring Boot提供命令列介面(CLI)工具,用於開發和測試Spring Boot應用程式,如Java或Groovy。
  8. Spring Boot提供了多種外掛,可以使用內建工具(如Maven和Gradle)開發和測試Spring Boot應用程式。

2、說一說Spring boot是如何自動載入配置

這個是因為 @SpringBootApplication 註解的原因,在上一個問題中已經提到了這個註解。我們知道 @SpringBootApplication 看作是 @Configuration@EnableAutoConfiguration@ComponentScan 註解的集合。

  • @EnableAutoConfiguration:啟用 SpringBoot 的自動配置機制
  • @ComponentScan:掃描被@Component(@Service,@Controller)註解的bean,註解預設會掃描該類所在的包下所有的類。
  • @Configuration:允許在上下文中註冊額外的bean或匯入其他配置類

@EnableAutoConfiguration是啟動自動配置的關鍵

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

@EnableAutoConfiguration 註解通過Spring 提供的 @Import 註解匯入了AutoConfigurationImportSelector類(@Import 註解可以匯入配置類或者Bean到當前類中)。

AutoConfigurationImportSelector類中getCandidateConfigurations方法會將所有自動配置類的資訊以 List 的形式返回。這些配置資訊會被 Spring 容器作 bean 來管理。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

自動配置資訊有了,那麼自動配置還差什麼呢?

@Conditional 註解。@ConditionalOnClass(指定的類必須存在於類路徑下),@ConditionalOnBean(容器中是否有指定的Bean)等等都是對@Conditional註解的擴充套件。拿 Spring Security 的自動配置舉個例子:

SecurityAutoConfiguration中匯入了WebSecurityEnablerConfiguration類,WebSecurityEnablerConfiguration原始碼如下:

@Configuration
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {

}

WebSecurityEnablerConfiguration類中使用 @ConditionalOnBean 指定了容器中必須還有WebSecurityConfigurerAdapter 類或其實現類。所以,一般情況下 Spring Security配置類都會去實現 WebSecurityConfigurerAdapter,這樣自動將配置就完成了。

參考:
(重要)Spring Boot 的自動配置是如何實現的?

3、說一說Spring Cloud中的Eureka的原理

Eureka高可用叢集:
官方給出的 Eureka 架構圖
這是基於叢集配置的eureka;

  • 處於不同節點的eureka通過Replicate進行資料同步
  • Application Service為服務提供者
  • Application Client為服務消費者
  • Make Remote Call完成一次服務呼叫

服務啟動後向Eureka註冊,Eureka Server會將註冊資訊向其他Eureka Server進行同步,當服務消費者要呼叫服務提供者,則向服務註冊中心獲取服務提供者地址,然後會將服務提供者地址快取在本地,下次再呼叫時,則直接從本地快取中取,完成一次呼叫。

當服務註冊中心Eureka Server檢測到服務提供者因為當機、網路原因不可用時,則在服務註冊中心將服務置為DOWN狀態,並把當前服務提供者狀態向訂閱者釋出,訂閱過的服務消費者更新本地快取。

服務提供者在啟動後,週期性(預設30秒)向Eureka Server傳送心跳,以證明當前服務是可用狀態。Eureka Server在一定的時間(預設90秒)未收到客戶端的心跳,則認為服務當機,登出該例項。

參考:
深入淺出 Spring Cloud 之 Eureka

4、說一說Fegin的工作流程

Feign的工作流:
@EnableFeignClients 表示開啟Feign功能,然後掃描 註解@FeignClient,程式啟動後,會將這些類掃描進IOC容器;Feign會 對 RestTemplate 進行 封裝,生成代理時,Feign會為每個介面方法建立一個RequestTemplate物件,簡化HTTP遠端過程呼叫;RestTemplate使用Request 模板生成新的Requst傳送請求,其底層通常是基於URLConnection,最後Client被封裝到LoadBalanceClient類,這個類結合Ribbon負載均衡發器實現服務之間的呼叫。

從原始碼上看Feign的呼叫過程:

  1. EnableFeignClients註解對應的配置屬性注入;

  2. FeignClient註解對應的屬性注入。

  3. 生成FeignClient對應的bean,注入到Spring的IOC容器。

  4. registerFeignClient方法中構造了一個BeanDefinitionBuilder物件,BeanDefinitionBuilder的主要作用就是構建一個AbstractBeanDefinitionAbstractBeanDefinition類最終被構建成一個BeanDefinitionHolder 然後註冊到Spring中。
    注意:beanDefinition類為FeignClientFactoryBean,故在Spring獲取類的時候實際返回的是FeignClientFactoryBean類。

  5. 通過FeignClientFactoryBeangetObject()方法得到不同動態代理的類併為每個方法建立一個SynchronousMethodHandler物件;

  6. 為每一個方法建立一個動態代理物件, 動態代理的實現是 ReflectiveFeign.FeignInvocationHanlder,代理被呼叫的時候,會根據當前呼叫的方法,轉到對應的 SynchronousMethodHandler

參考:
springcloud-Feign配置
Spring Cloud Feign 呼叫過程分析

5、spring cloud Hystrix

1 熔斷器機制

Hystrix Command請求後端服務失敗數量超過一定比例(預設50%), 斷路器會切換到開路狀態(Open). 這時所有請求會直接失敗而不會傳送到後端服務. 斷路器保持在開路狀態一段時間後(預設5秒), 自動切換到半開路狀態(HALF-OPEN). 這時會判斷下一次請求的返回情況, 如果請求成功, 斷路器切回閉路狀態(CLOSED), 否則重新切換到開路狀態(OPEN)。Hystrix的斷路器就像我們家庭電路中的保險絲, 一旦後端服務不可用, 斷路器會直接切斷請求鏈, 避免傳送大量無效請求影響系統吞吐量, 並且斷路器有自我檢測並恢復的能力.

2 降級

Fallback相當於是降級操作。 對於查詢操作, 我們可以實現一個fallback方法, 當請求後端服務出現異常的時候, 可以使用fallback方法返回的值。 fallback方法的返回值一般是設定的預設值或者來自快取。

3 資源隔離

Hystrix中,主要通過執行緒池來實現資源隔離。 通常在使用的時候我們會根據呼叫的遠端服務劃分出多個執行緒池。 例如呼叫產品服務的Command放入A執行緒池,呼叫賬戶服務的Command放入B執行緒池。 這樣做的主要優點是執行環境被隔離開了。 這樣就算呼叫服務的程式碼存在bug或者由於其他原因導致自己所線上程池被耗盡時,不會對系統的其他服務造成影響。 但是帶來的代價就是維護多個執行緒池會對系統帶來額外的效能開銷。 如果是對效能有嚴格要求而且確信自己呼叫服務的客戶端程式碼不會出問題的話,可以使用Hystrix的訊號模式(Semaphores)來隔離資源。

Hystrix的整合參考白話SpringCloud | 第五章:服務容錯保護(Hystrix)

參考:
springcloud(四):熔斷器Hystrix

6、Spring Cloud Ribbon

1 什麼是負載呼叫

負載均衡(Load Balance)是分散式系統架構設計中必須考慮的因素之一,它通常是指,將請求/資料【均勻】分攤到多個操作單元上執行,負載均衡的關鍵在於【均勻】。

1 伺服器端負載均衡和客戶端負載均衡的區別

伺服器端負載均衡:例如Nginx,通過Nginx進行負載均衡,先傳送請求,然後通過負載均衡演算法,在多個伺服器之間選擇一個進行訪問;

客戶端負載均衡:例如spring cloud中的ribbon,客戶端會有一個伺服器地址列表,在傳送請求前通過負載均衡演算法選擇一個伺服器,然後進行訪問,這是客戶端負載均衡;

2 負載呼叫的實現方式

1)【協議層】http重定向協議實現負載均衡
2)【協議層】dns域名解析負載均衡
3)【協議層】反向代理負載均衡
4)【網路層】IP負載均衡
5)【鏈路層】資料鏈路層負載均衡

參考:
幾種負載均衡技術的實現

3 Ribbon的幾種負載均衡演算法

負載均衡,不管 Nginx 還是 Ribbon 都需要其演算法的支援,如果我沒記錯的話 Nginx 使用的是 輪詢和加權輪詢演算法。而在 Ribbon 中有更多的負載均衡排程演算法,其預設是使用的 RoundRobinRule輪詢策略。

  • RoundRobinRule:輪詢策略。Ribbon 預設採用的策略。若經過一輪輪詢沒有找到可用的 provider,其最多輪詢 10 輪。若最終還沒有找到,則返回 null。
  • RandomRule: 隨機策略,從所有可用的 provider中隨機選擇一個。
  • RetryRule: 重試策略。先按照RoundRobinRule 策略獲取 provider,若獲取失敗,則在指定的時限內重試。預設的時限為 500 毫秒。

更換預設的負載均衡演算法,只需要在配置檔案中做出修改就行。

providerName:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

當然,在 Ribbon 中你還可以自定義負載均衡演算法,你只需要實現IRule 介面,然後修改配置檔案或者自定義Java Config 類。

7、Spring Cloud Zuul

Zuul的功能:

  • Zuul 註冊於 Eureka 並整合了 Ribbon 所以自然也是可以從註冊中心獲取到服務列表進行客戶端負載。
  • 功能豐富的路由功能,解放運維。
  • 具有過濾器,所以鑑權、驗籤都可以整合。

通過服務發現自動對映路由
Eureka配合Zuul使用的優美之處在於,不僅可以通過單個端點來訪問應用的所有服務,而且,在新增或移除服務例項的時候不用修改Zuul的路由配置。另外,也可以新增一個新的服務到Eureka,而Zuul會對訪問新新增的服務自動路由,因為Zuul是通過與Eureka通訊然後從Eureka獲取微服務例項真正實體地址,只要服務託管在Eureka中。

在專案中使用到Zuul這塊主要是用在了負載均衡反向代理。這裡記錄一下Zuul的路由的使用。

參考:
sbc(六) Zuul GateWay 閘道器應用
服務閘道器——Spring Cloud Zuul

8、Spring boot中配置相關

1 配置檔案中Map和List型別是如何配置的?

  1. yml 或者properties檔案中寫需要的配置(下面是yml檔案中的寫法):
    Map

    person.maps: {key1: value1,key2: value2}
    

    ListSet

    person:
     list: [1,2,3]
    
  2. 在pom檔案加入spring-boot-configuration-processor依賴

    <!--匯入配置檔案處理器,配置檔案進行繫結就會有提示-->
    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-configuration-processor</artifactId>
     <optional>true</optional>
    </dependency>
    
  3. 建立一個與配置檔案相關的配置檔案,如之前的配置檔案字首是person

    @Component // 或者@Configuration
    @ConfigurationProperties(prefix = "person")
    public class Person
    {
    	private Map<String,Object> maps;
    	private List<String> list;
    	private String name;
    	private int age;
    
    	public Map<String, Object> getMaps()
    	{
    		return maps;
    	}
    
    	public void setMaps(Map<String, Object> maps)
    	{
    		this.maps = maps;
    	}
    
    	public List<String> getList()
    	{
    		return list;
    	}
    
    	public void setList(List<String> list)
    	{
    		this.list = list;
    	}
    
    	public String getName()
    	{
    		return name;
    	}
    
    	public void setName(String name)
    	{
    		this.name = name;
    	}
    
    	public int getAge()
    	{
    		return age;
    	}
    
    	public void setAge(int age)
    	{
    		this.age = age;
    	}
    }
    
    

    @ConfigurationProperties 註解向Spring Boot宣告該類中的所有屬性和配置檔案中相關的配置進行繫結。

    prefix = "person":宣告配置前戳,將該前戳下的所有屬性進行對映。
    @Component 或者@Configuration:將該元件加入Spring Boot容器,只有這個元件是容器中的元件,配置才生效。

參考:
最全面的SpringBoot配置檔案詳解
Spring Boot配置檔案詳解

9、spring cloud config和apollo有什麼區別

在這裡插入圖片描述

四、sql相關

1、索引相關

1 sql索引命中

SQL優化建議:

  1. 應儘量避免在 where 子句中使用!=<>操作符,否則將引擎放棄使用索引而進行全表掃描。

  2. 對查詢進行優化,應儘量避免全表掃描,首先應考慮在 whereorder by 涉及的列上建立索引。

  3. 應儘量避免在where 子句中對欄位進行null值判斷,否則將導致引擎放棄使用索引而進行全表掃描。如:
    select id from t where num is null
    可以在num上設定預設值為0,確保表中num列沒有null值,然後這樣查詢:
    select id from t where num =0

  4. 儘量避免在 where 子句中使用 or 來連線條件,前後都得有索引否則可能導致引擎放棄使用索引而進行全表掃描(上面已有例子).

  5. 下面的查詢也將導致全表掃描:(不能前置百分號),例:
    select id from t where name like ‘%product_no%’
    若要提高效率可以考慮全文索引最好改為:
    select id from t where name like 'product_no%'

  6. 對於innot in 要慎用。 例 : EXPLAIN SELECT * FROM tc_test WHERE pripid NOT IN (1,2,3)

  7. 應儘量避免在 where 子句中對欄位進行表示式操作,這將導致引擎放棄使用索引而進行全表掃描。如:EXPLAIN SELECT * FROM tc_test WHERE pripid/2=100
    應改為: select * from t where pripid=200

  8. 應儘量避免在where子句中對欄位進行函式操作,這將導致引擎放棄使用索引而進行全表掃描。如:

-- enttype以41開頭的pripid
EXPLAIN SELECT pripid FROM tc_test WHERE SUBSTRING(enttype,1,2)='41' 

應改為: expalin select pripid from tc_test where pripid like '41%'

  1. 不要在where子句中的=左邊進行函式、算術運算或其他表示式運算,否則系統將可能無法正確使用索引

  2. 在使用索引欄位作為條件時,如果該索引是複合索引,那麼必須使用到該索引中的第一個欄位作為條件時才能保證系統使用該索引,否則該索引將不會被使用,並且應儘可能的讓欄位順序與索引順序相一致。

  3. 很多時候用 exists 代替 in 是一個好的選擇:
    select num from a where num in(select num from b)
    用下面的語句替換:
    select num from a where exists(select 1 from b where num=a.num)

  4. 並不是所有索引對查詢都有效,SQL是根據表中資料來進行查詢優化的,當索引列有大量資料重複時,SQL查詢可能不會去利用索引,如一表中有欄位 sex,male、female幾乎各一半,那麼即使在sex上建了索引也對查詢效率起不了作用

  5. 索引並不是越多越好,索引固然可以提高相應的 select的效率,但同時也降低了 insertupdate 的效率,因為 insertupdate 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數較好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有必要.

  6. 應儘可能的避免更新 clustered索引資料列,因為 clustered 索引資料列的順序就是表記錄的物理儲存順序,一旦該列值改變將導致整個表記錄的順序的調整,會耗費相當大的資源。若應用系統需要頻繁更新 clustered 索引資料列,那麼需要考慮是否應將該索引建為 clustered索引

  7. 儘量使用數字型欄位,若只含數值資訊的欄位儘量不要設計為字元型,這會降低查詢和連線的效能,並會增加儲存開銷。這是因為引擎在處理查詢和連線時會 逐個比較字串中每一個字元,而對於數字型而言只需要比較一次就夠了!

  8. 儘可能的使用varchar/nvarchar代替 char/nchar ,因為首先變長欄位儲存空間小,可以節省儲存空間,其次對於查詢來說,在一個相對較小的欄位內搜尋效率顯然要高些。

  9. 任何地方都不要使用 select * from t ,用具體的欄位列表代替*,不要返回用不到的任何欄位。

  10. 儘量避免向客戶端返回大資料量,若資料量過大,應該考慮相應需求是否合理。

  11. 儘量避免大事務操作,提高系統併發能力.

  12. MySql的子查詢實現的非常糟糕。最糟糕的一類查詢是WHERE條件中包含IN()的子查詢語句。應該儘可能用關聯替換子查詢,可以提高查詢效率。

參考:
SQL優化一鍵命中索引

2 索引型別

從索引的實現上,我們可以將其分為聚集索引與非聚集索引,或稱輔助索引或二級索引,這兩大類;從索引的實際應用中,又可以細分為普通索引、唯一索引、主鍵索引、聯合索引、外來鍵索引、全文索引這幾種。

參考:
MySQL 索引的原理與應用:索引型別,儲存結構與鎖

3 索引的資料結構

目前大部分資料庫系統及檔案系統都採用B-Tree或其變種B+Tree作為索引結構。MySQL就普遍使用B+Tree實現其索引結構。

B+的特性:

  1. 所有關鍵字都出現在葉子結點的連結串列中(稠密索引),且連結串列中的關鍵字恰好是有序的;
  2. 不可能在非葉子結點命中;
  3. 非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是儲存(關鍵字)資料的資料層;
  4. 更適合檔案索引系統;

B+tree的優點:

  1. B±tree的磁碟讀寫代價更低
    B+ tree的內部結點並沒有指向關鍵字具體資訊的指標。因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那麼盤塊所能容納的關鍵字數量也越多。一次性讀入記憶體中的需要查詢的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
    舉個例子,假設磁碟中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體資訊指標2bytes。一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而**B+樹內部結點只需要1個盤快。當需要把內部結點讀入記憶體中的時候,B 樹就比B+ **樹多一次盤塊查詢時間(在磁碟中就是碟片旋轉的時間)。
  2. B±tree的查詢效率更加穩定
    由於非終結點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個資料的查詢效率相當。

參考:
乾貨:mysql索引的資料結構

4 索引匹配原則

1 最左匹配原則

在mysql建立聯合索引時會遵循字首匹配原則,即最左優先,在檢索資料時從聯合索引的最左邊開始匹配,示例:

KEY test_col1_col2_col3 on test(col1,col2,col3);

聯合索引test_col1_col2_col3實際建立了(col1)(col1,col2)(col1,col2,col3)三個索引。

select * from test where col1="1" and col2="2" and col4="4"

上面這個查詢語句執行時會按照最左匹配的原則,檢索時會使用索引
(col1,col2)

2 為什麼要使用聯合索引

  • 減少開銷。建一個聯合索引(col1,col2,col3),實際相當於建了(col1),(col1,col2),(col1,col2,col3)三個索引。每多一個索引,都會增加寫操作的開銷和磁碟空間的開銷。對於大量資料的表,使用聯合索引會大大的減少開銷!

  • 覆蓋索引。對聯合索引(col1,col2,col3),如果有如下的sql:

    select col1,col2,col3 from test where col1=1 and col2=2.
    

    那麼MySQL可以直接通過遍歷索引取得資料,而無需回表,這減少了很多的
    隨機io操作。減少io操作,特別的隨機io其實是dba主要的優化策略。所以,在真正的實際應用中,蓋索引是主要的提升效能的優化手段之一。

  • 效率高。索引列越多,通過索引篩選出的資料越少。有1000W條資料的表,有如下sql:

    select from table where col1=1 and col2=2 and col3=3
    

,假設假設每個條件可以篩選出10%的資料,如果只有單值索引,那麼通過該索引能篩選出1000W10%=100w條資料,然後再回表從100w條資料中找到
符合col2=2 and col3=3的資料,然後再排序,再分頁;如果是聯合索引,通過索引篩選出1000w10%10%*10%=1w,效率提升可想而知!
參考:
Mysql聯合索引最左匹配原則

2、左連線和內連線的區別

內連線:取的兩個表的(有能連線的欄位)的交集,即欄位相同的。

外連線:左右連線。

left join(左連線)返回包括左表中的所有記錄和右表中連線欄位相等的記錄,將左表沒有的對應項顯示,右表的列為NULL

right join(右連線)返回包括右表中的所有記錄和左表中連線欄位相等的記錄,將右表沒有的對應項顯示,左表的列為NULL

inner join (等值連線)只返回兩個表中聯結欄位相等的

3、如何區分是行鎖還是表鎖

mysql的行鎖是通過索引載入的,即是行鎖是加在索引響應的行上
要是對應的SQL語句沒有走索引,則會全表掃描,行鎖則無法實現,取而代之的是表鎖。

  • 表鎖:不會出現死鎖,發生鎖衝突機率高,併發低。
  • 行鎖:會出現死鎖,發生鎖衝突機率低,併發高。

鎖衝突:例如說事務A將某幾行上鎖後,事務B又對其上鎖,鎖不能共存否則會出現鎖衝突。(但是共享鎖可以共存,共享鎖和排它鎖不能共存,排它鎖和排他鎖也不可以)

死鎖:例如說兩個事務,事務A鎖住了1-5行,同時事務B鎖住了6-10行,此時事務A請求鎖住6-10行,就會阻塞直到事務B施放6-10行的鎖,而隨後事務B又請求鎖住1-5行,事務B也阻塞直到事務A釋放1~5行的鎖。死鎖發生時,會產生Deadlock錯誤。

鎖是對錶操作的,所以自然鎖住全表的表鎖就不會出現死鎖。

4、mybatis框架

1 使用mybatis框架連線資料庫與直連資料庫有什麼區別?說一說mybatis框架的優點

JDBC的工作量大:需要先註冊驅動和資料庫資訊、操作Connection、通過statement物件執行SQL,將結果返回給resultSet,然後從resultSet中讀取資料並轉換為pojo物件,最後需要關閉資料庫相關資源。
而且還需要自己對JDBC過程的異常進行捕捉和處理。

而MyBatis使用SqlSessionFactoryBuilder來連線完成JDBC需要程式碼完成的資料庫獲取和連線,減少了程式碼的重複。JDBC將SQL語句寫到程式碼裡,屬於硬編碼,非常不易維護,MyBatis可以將SQL程式碼寫入xml中,易於修改和維護。JDBC的resultSet需要使用者自己去讀取並生成對應的POJO,MyBatis的mapper會自動將執行後的結果對映到對應的Java物件中。

mybatis框架優點:

  • 與JDBC相比,減少了50%以上的程式碼量
  • 最簡單的持久化框架、小巧簡單易學
  • SQL程式碼從程式程式碼中徹底分離出來,可重用
  • 提供XML標籤,支援編寫動態SQL
  • 提供對映標籤,支援物件與資料庫的ORM欄位關係對映

mybatis框架缺點:

  • SQL語句編寫工作量大,熟練度要高
  • 資料庫移植性差,比如mysql移植到Orecle,SQL語句會有差異從而引起err

mybatis框架適用場合:
MyBatis專注於SQL本身,是一個足夠靈活的DAO層解決方案。
對效能的要求很高,或者需求變化較多的專案,如網際網路專案,MyBatis將是不錯的選擇。

參考:
MyBatis與JDBC的比較
MyBatis框架的優缺點及其適用場合
MyBatis框架的優缺點

2 mybatis框架中#和$有什麼區別

#{}:佔位符號(在對資料解析時會對資料自動新增' '
${}sql拼接符號(替換結果不會增加單引號‘ ’likeorder by後使用,存在sql注入問題,需手動程式碼中過濾)

mybatis什麼情況下必須要使用${}格式?

在涉及到動態表名和列名時,必須使用${xxxxx}進行注入。

order by:
因為#是按string型別拼接,就成為:order by ‘cloumn’ ‘desc’。
使用$則為:order by cloumn desc。

limit:
例: limit #{index}, #{rows}

like
例: and prod_name like ‘%${prodName}%’

解決這個類${xxxx}的防SQL的方案:java判斷一下輸入的引數的長度是否正常。

參考:
用了MyBatis就不會發生SQL隱碼攻擊風險嗎?

3 mybatis中的一級快取和二級快取有了解嗎?

一級快取

一級快取是SqlSession級別的快取。在運算元據庫時需要構造sqlSession物件,在物件中有一個資料結構用於儲存快取資料。不同的sqlSession之間的快取資料區域是互相不影響的。也就是他只能作用在同一個sqlSession中,不同的sqlSession中的快取是互相不能讀取的。

一級快取的工作原理:
在這裡插入圖片描述
使用者發起查詢請求,查詢某條資料,sqlSession先去快取中查詢,是否有該資料,如果有,讀取;如果沒有,從資料庫中查詢,並將查詢到的資料放入一級快取區域,供下次查詢使用。但sqlSession執行commit,即增刪改操作時會清空快取。這麼做的目的是避免髒讀。
如果commit不清空快取,會有以下場景:A查詢了某商品庫存為10件,並將10件庫存的資料存入快取中,之後被客戶買走了10件,資料被delete了,但是下次查詢這件商品時,並不從資料庫中查詢,而是從快取中查詢,就會出現錯誤。

二級快取
二級快取是mapper級別的快取,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級快取,二級快取是跨SqlSession的。二級快取的作用範圍更大。還有一個原因,實際開發中,MyBatis通常和Spring進行整合開發。Spring將事務放到Service中管理,對於每一個service中sqlsession是不同的,這是通過mybatis-spring中的org.mybatis.spring.mapper.MapperScannerConfigurer建立sqlsession自動注入到service中的。 每次查詢之後都要進行關閉sqlSession,關閉之後資料被清空。所以Spring整合之後,如果沒有事務,一級快取是沒有意義的。二級快取預設是關閉的狀態,開啟需要再setting全域性引數中配置二級快取。

二級快取原理:
在這裡插入圖片描述
UserMapper有一個二級快取區域(按namespace分),其它mapper也有自己的二級快取區域(按namespace分)。每一個namespace的mapper都有一個二級快取區域,兩個mapper的namespace如果相同,這兩個mapper執行sql查詢到資料將存在相同的二級快取區域中。

參考:
深入理解MyBatis中的一級快取與二級快取
Mybatis的快取機制(一級快取二級快取和重新整理快取)和Mybatis整合ehcache

4、MyBatis SqlSession的作用

SqlSession的作用類似於一個 JDBC 中的 Connection 物件,代表著一個連線資源的啟用。具體而言,它的作用有 3 個:

  • 獲取 Mapper 介面。
  • 傳送 SQL 給資料庫。
  • 控制資料庫事務。

SqlSession的最佳的作用域是請求或方法作用域。

5、MyBatis中resultType和resultMap的區別

  1. 類的名字和資料庫相同時,可以直接設定 resultType 引數為 Pojo 類
  2. 若不同,需要設定 resultMap 將結果名字和 Pojo 名字進行轉換

6、Mybatis的xml檔案中常用的標籤

參考:
Mybatis的xml對映檔案的常用標籤彙總

7、Mybatis中 mapper.java和mapper.xml是如何對映起來的

mybatis裡所有mapper介面的實現類都可以看做是mapperProxymapper代理類,然後呼叫MapperProxy.invoke()方法,invoke()方法會執行相應sql語句,並將結果返回。

參考:
【Mybatis】- mapper.java和mapper.xml是如何對映起來的-原始碼分析

5、如何高效的在大量資料(百萬級甚至更多)的oracle資料庫表中篩選錯誤資料

第一步:分表
如果歷史表中儲存了很多年的資料,會造成嚴重的資料冗餘。那如果將歷史表分表儲存,比如每年建立一個表,資料儲存到對應的年表中,必定會減少很多資料量。(如果分成年表資料量還是過大,可以細分到月表,天表…)。

第二步:分割槽
年表建立過後,查詢就是查詢年表中的資料,可是雖然分表了,但是年表中的資料量仍然很大,查詢速度雖然有提升,但並不能滿足使用者的要求。便考慮到分表再分割槽,即將歷史資料以不同的年表來儲存,在年表中按月分割槽。
資料庫分割槽:就是減少SQL操作的資料量,從而提升查詢效率。表分割槽後,邏輯上仍然是一張表,只不過將表中的資料在物理上存放到多個表空間上。這樣在查詢資料時,會查詢相應分割槽的資料,避免了全表掃描。
分割槽又分為水平分割槽、垂直分割槽。

水平分割槽:就是對行進行分割槽,舉個例子來說,就是一個表中有1000萬條資料,每100萬條資料劃一個分割槽,這樣就將表中資料分到10個分割槽中去。水平分割槽要通過某個特定的屬性列進行分割槽,比如我用的列就是Date時間。

垂直分割槽:通過對標垂直劃分來減少表的寬度,從而提升查詢效率。比如一個學生表中,有他相關的資訊列,還有論文列以CLOB儲存。這些以CLOB儲存的論文並不會經常被訪問到,這時候就要把這些不經常使用的CLOB劃分到另一個分割槽,需要訪問時再呼叫它。

參考
Oracle億級資料查詢處理(資料庫分表、分割槽實戰)

6、事務相關

1 簡述一下ACID

1)原子性(Atomicity)

事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;

這指的是在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。事務檢視資料更新時,資料所處的狀態要麼是另一事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視到中間狀態的資料。

2)一致性(Consistency)

一致性是指在事務開始之前和事務結束以後,資料庫的完整性約束沒有被破壞。這是說資料庫事務不能破壞關係資料的完整性以及業務邏輯上的一致性。

3)隔離性(Isolation)

多個事務併發訪問時,事務之間是隔離的,一個事務不應該影響其它事務執行效果。

隔離級別
  • 未提交讀: 在讀資料時不會檢查或使用任何鎖。因此,在這種隔離級別中可能讀取到沒有提交的資料。 不可避免 髒讀、不可重複讀、虛讀。

  • 已提交讀:只讀取提交的資料並等待其他事務釋放排他鎖。讀資料的共享鎖在讀操作完成後立即釋放。已提交讀是SQL Server的預設隔離級別。 避免了髒讀,但是可能會造成不可重複讀。oracle採用讀已提交

  • 可重複讀: 像已提交讀級別那樣讀資料,但會保持共享鎖直到事務結束。 可以避免不可重複讀。但還有可能出現幻讀 。mysql採用可重複讀

  • 可序列讀:工作方式類似於可重複讀。但它不僅會鎖定受影響的資料,還會鎖定這個範圍。這就阻止了新資料插入查詢所涉及的範圍。可避免 髒讀、不可重複讀、幻讀情況的發生

4)永續性(Durability)

永續性,意味著在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,並不會被回滾。即使出現了任何事故比如斷電等,事務一旦提交,則持久化儲存在資料庫中。

參考:
談談資料庫的ACID

五、Redis相關

1、Redis可以儲存的資料型別

1 資料型別

Redis可儲存的資料型別

2 Redis在網際網路公司一般有以下應用

String:快取、限流、計數器、分散式鎖、分散式Session;累加型別的資料
Hash:儲存使用者資訊、使用者主頁訪問量、組合查詢
List:微博關注人時間軸列表、簡單佇列
Set:贊、踩、標籤、好友關係
Zset:排行榜

參考:
【Redis】redis各型別資料儲存分析

3 Redis在使用者資料統計中value的設計

  1. 對於累加型別的資料,可以使用String,比如:每日消耗的金幣,每日註冊人數等
  2. 對於需要計算唯一性的資料,比如登入使用者,充值人數等,不能繼續使用 String,可以選用的有Set,Hash,bitmap等。
    考慮到平均下來每個伺服器分擔的使用者可能只在萬級左右,並且線上人數統計要頻繁地統計長度,最後選擇了使Set來對這些資料統計
  3. 在計算每小時平均線上的時候,使用String來儲存,後續只要對字串進行格式化,即可方便地計算出每小時平均線上,每小時最高線上,整天的資料。
  4. 新增的資料計算
    這一類要記錄的資料可能比較多,因為涉及到集合的交集,並集計算,一開始想到的就是Set,又考慮到記憶體的問題,便將目光轉移到bitmap上。

參考:
Redis在使用者資料統計中的簡單應用

2、什麼是快取雪崩和快取擊穿

1 快取雪崩

快取雪崩是指:快取在同一時間大面積的失效,後面的請求都直接落到了資料庫上,造成資料庫短時間內承受大量請求,導致資料庫的壓力過大而當機。

解決辦法

1)Redis服務不可用:

  1. 採用Redis叢集,避免單機出現問題整個快取服務都沒辦法使用。
  2. 限流,避免同時處理大量的請求。

2)熱點快取失效:

  1. 設定不同的失效時間比如隨機設定快取的失效時間。
  2. 快取永不失效。

2 快取擊穿

快取穿透說簡單點就是大量請求的 key 根本不存在於快取中,導致請求直接到了資料庫上,根本沒有經過快取這一層。

解決辦法

最基本的就是首先做好引數校驗,一些不合法的引數請求直接丟擲異常資訊返回給客戶端。

1)快取無效 key

如果快取和資料庫都查不到某個 key 的資料就寫一個到 Redis 中去並設定過期時間,具體命令如下: SET key value EX 10086 。這種方式可以解決請求的 key 變化不頻繁的情況。如果黑客惡意攻擊,每次構建不同的請求 key,會導致 Redis 中快取大量無效的 key 。很明顯,這種方案並不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,儘量將無效的 key 的過期時間設定短一點比如 1 分鐘。

一般情況下我們是這樣設計 key 的: 表名:列名:主鍵名:主鍵值

public Object getObjectInclNullById(Integer id) {
    // 從快取中獲取資料
    Object cacheValue = cache.get(id);
    // 快取為空
    if (cacheValue == null) {
        // 從資料庫中獲取
        Object storageValue = storage.get(key);
        // 快取空物件
        cache.set(key, storageValue);
        // 如果儲存資料為空,需要設定一個過期時間(300秒)
        if (storageValue == null) {
            // 必須設定過期時間,否則有被攻擊的風險
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}
2)布隆過濾器

把所有可能存在的請求的值都存放在布隆過濾器中,當使用者請求過來,先判斷使用者發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求引數錯誤資訊給客戶端,存在的話才會走下面的流程。
在這裡插入圖片描述
布隆過濾器可能會存在誤判的情況。總結來說就是: 布隆過濾器說某個元素存在,小概率會誤判。布隆過濾器說某個元素不在,那麼這個元素一定不在。
解決:可以適當增加位陣列大小或者調整我們的雜湊函式來降低概率。

i 布隆過濾器的原理

布隆過濾器的原理是,當一個元素被加入集合時,通過K個雜湊函式將這個元素對映成一個位陣列中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。

優點:
相比於其它的資料結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器儲存空間和插入/查詢時間都是常數( O(k))。另外,雜湊函式相互之間沒有關係,方便由硬體並行實現。布隆過濾器不需要儲存元素本身,在某些對保密要求非常嚴格的場合有優勢。

布隆過濾器可以表示全集,其它任何資料結構都不能;

km相同,使用同一組雜湊函式的兩個布隆過濾器的交併[來源請求]運算可以使用位操作進行。

缺點
但是布隆過濾器的缺點和優點一樣明顯。誤算率是其中之一。隨著存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用雜湊表足矣。

另外,一般情況下不能從布隆過濾器中刪除元素。我們很容易想到把位陣列變成整數陣列,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全地刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裡面。這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。

在降低誤算率方面,有不少工作,使得出現了很多布隆過濾器的變種。

參考:
快取雪崩和快取穿透問題解決方案

3、Redis的應用場景

1、會話快取(最常用)

2、訊息佇列,比如支付

3、活動排行榜或計數

4、釋出、訂閱訊息(訊息通知)

5、商品列表、評論列表等

4、Redis持久化

1 AOF(Append-only file)

Append-only file,將“操作 + 資料”以格式化指令的方式追加到操作日誌檔案的尾部,在append操作返回後(已經寫入到檔案或者即將寫入),才進行實際的資料變更,“日誌檔案”儲存了歷史所有的操作過程;當server需要資料恢復時,可以直接replay此日誌檔案,即可還原所有的操作過程。AOF相對可靠,它和mysql中bin.log、apache.log、zookeeper中txn-log簡直異曲同工。AOF檔案內容是字串,非常容易閱讀和解析。

優點:可以保持更高的資料完整性,如果設定追加file的時間是1s,如果redis發生故障,最多會丟失1s的資料;且如果日誌寫入不完整支援redis-check-aof來進行日誌修復;AOF檔案沒被rewrite之前(檔案過大時會對命令進行合併重寫),可以刪除其中的某些命令(比如誤操作的flushall)。

缺點:AOF檔案比RDB檔案大,且恢復速度慢。

AOF預設關閉

2 RDB(Redis DataBase)

RDB是在某個時間點將資料寫入一個臨時檔案,持久化結束後,用這個臨時檔案替換上次持久化的檔案,達到資料恢復。

優點:使用單獨子程式來進行持久化,主程式不會進行任何IO操作,保證了redis的高效能
缺點:RDB是間隔一段時間進行持久化,如果持久化之間redis發生故障,會發生資料丟失。所以這種方式更適合資料要求不嚴謹的時候

這裡說的這個執行資料寫入到臨時檔案的時間點是可以通過配置來自己確定的,通過配置redis在n秒內如果超過m個key被修改這執行一次RDB操作。這個操作就類似於在這個時間點來儲存一次Redis的所有資料,一次快照資料。所以這個持久化方法也通常叫做snapshots。RDB預設開啟。

3 AOF和RDB如何選擇?

  1. 架構良好的環境中,master通常使用AOF,slave使用snapshot,主要原因是master需要首先確保資料完整性,它作為資料備份的第一選擇;slave提供只讀服務(目前slave只能提供讀取服務),它的主要目的就是快速響應客戶端read請求;
  2. 但是如果你的redis執行在網路穩定性差/物理環境糟糕情況下,建議你master和slave均採取AOF,這個在master和slave角色切換時,可以減少“人工資料備份”/“人工引導資料恢復”的時間成本;
  3. 如果你的環境一切非常良好,且服務需要接收密集性的write操作,那麼建議master採取snapshot,而slave採用AOF。

參考:
Redis持久化儲存(AOF與RDB兩種模式)

六、RocketMQ相關的問題

1、RocketMQ在專案中的使用

這裡不展開,只是記錄一下

2、使用RocketMQ如何解決分散式事務

在這裡插入圖片描述
針對這裡的 可靠訊息最終一致性方案 來說,我們說的 可靠 是指保證訊息一定能傳送到訊息中介軟體裡面去,保證這裡可靠。

對於下游的系統來說,消費不成功,一般來說就是採取失敗重試,重試多次不成功,那麼就記錄日誌,後續人工介入來進行處理。所以這裡得強調一下,後面的系統,一定要處理 冪等,重試,日誌 這幾個東西。

如果是對於資金類的業務,後續系統回滾了以後,得想辦法去通知前面的系統也進行回滾,或者是傳送報警由人工來手工回滾和補償。

3、擴充套件:其他幾種分散式事務解決方案

1 TCC

TCC 的全程分為三個階段,分別是 Try、Confirm、Cancel

  1. Try階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留
  2. Confirm階段:這個階段說的是在各個服務中執行實際的操作
  3. Cancel階段:如果任何一個服務的業務方法執行出錯,那麼這裡就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作

還是以轉賬的例子為例,在跨銀行進行轉賬的時候,需要涉及到兩個銀行的分散式事務,從A 銀行向 B 銀行轉 1 塊,如果用TCC 方案來實現:
在這裡插入圖片描述大概思路就是這樣的:

  1. Try 階段:先把A 銀行賬戶先凍結 1 塊,B銀行賬戶中的資金給預加 1 塊。
  2. Confirm 階段:執行實際的轉賬操作,A銀行賬戶的資金扣減 1塊,B 銀行賬戶的資金增加 1 塊。
  3. Cancel 階段:如果任何一個銀行的操作執行失敗,那麼就需要回滾進行補償,就是比如A銀行賬戶如果已經扣減了,但是B銀行賬戶資金增加失敗了,那麼就得把A銀行賬戶資金給加回去。

這種方案就比較複雜了,一步操作要做多個介面來配合完成。
以 ByteTCC 框架的實現例子來大概描述一下上面的流程,示例地址 https://gitee.com/bytesoft/ByteTCC-sample/tree/master/dubbo-sample

該方案使用場景
一般來說和錢相關的支付、交易等相關的場景,我們會用TCC,嚴格嚴格保證分散式事務要麼全部成功,要麼全部自動回滾,嚴格保證資金的正確性!

2 2PC

在這裡插入圖片描述
在XA協議中分為兩階段:
第一階段:事務管理器要求每個涉及到事務的資料庫預提交(precommit)此操作,並反映是否可以提交.
第二階段:事務協調器要求每個資料庫提交資料,或者回滾資料。
優點: 儘量保證了資料的強一致,實現成本較低,在各大主流資料庫都有自己實現,對於MySQL是從5.5開始支援。

缺點:

  • 單點問題:事務管理器在整個流程中扮演的角色很關鍵,如果其當機,比如在第一階段已經完成,在第二階段正準備提交的時候事務管理器當機,資源管理器就會一直阻塞,導致資料庫無法使用。
  • 同步阻塞:在準備就緒之後,資源管理器中的資源一直處於阻塞,直到提交完成,釋放資源。
  • 資料不一致:兩階段提交協議雖然為分散式資料強一致性所設計,但仍然存在資料不一致性的可能,比如在第二階段中,假設協調者發出了事務commit的通知,但是因為網路問題該通知僅被一部分參與者所收到並執行了commit操作,其餘的參與者則因為沒有收到通知一直處於阻塞狀態,這時候就產生了資料的不一致性。

總的來說,XA協議比較簡單,成本較低,但是其單點問題,以及不能支援高併發(由於同步阻塞)依然是其最大的弱點。

參考:
面網際網路公司必備的分散式事務方案
突破Java面試(44)-分散式事務解決方案
再有人問你分散式事務,把這篇扔給他

4、RocketMQ的消費模式

1 廣播模式(BROADCASTING)

廣播消費:當使用廣播消費模式時,訊息佇列 RocketMQ 版會將每條訊息推送給叢集內所有註冊過的消費者,保證訊息至少被每個消費者消費一次。

適用場景
適用於消費端叢集化部署,每條訊息需要被叢集下的每個消費者處理的場景。具體消費示例如下圖所示。
在這裡插入圖片描述
注意事項

  • 廣播消費模式下不支援順序訊息。
  • 廣播消費模式下不支援重置消費位點。
  • 每條訊息都需要被相同訂閱邏輯的多臺機器處理。
  • 消費進度在客戶端維護,出現重複消費的概率稍大於叢集模式。
  • 廣播模式下,訊息佇列 RocketMQ 版保證每條訊息至少被每臺客戶端消費一次,但是並不會重投消費失敗的訊息,因此業務方需要關注消費失敗的情況。
  • 廣播模式下,客戶端每一次重啟都會從最新訊息消費。客戶端在被停止期間傳送至服務端的訊息將會被自動跳過,請謹慎選擇。
  • 廣播模式下,每條訊息都會被大量的客戶端重複處理,因此推薦儘可能使用叢集模式。
  • 廣播模式下服務端不維護消費進度,所以訊息佇列 RocketMQ 版控制檯不支援訊息堆積查詢、訊息堆積報警和訂閱關係查詢功能。

2 叢集模式(CLUSTERING)

叢集消費:當使用叢集消費模式時,訊息佇列 RocketMQ 版認為任意一條訊息只需要被叢集內的任意一個消費者處理即可。

適用場景
適用於消費端叢集化部署,每條訊息只需要被處理一次的場景。此外,由於消費進度在服務端維護,可靠性更高。具體消費示例如下圖所示。
在這裡插入圖片描述
注意事項

  • 叢集消費模式下,每一條訊息都只會被分發到一臺機器上處理。如果需要被叢集下的每一臺機器都處理,請使用廣播模式。
  • 叢集消費模式下,不保證每一次失敗重投的訊息路由到同一臺機器上。

多個 Group ID 通過叢集訂閱方式實現廣播消費模式

適用場景
適用於每條訊息都需要被多臺機器處理,每臺機器的邏輯可以相同也可以不一樣的場景。具體消費示例如下圖所示。
在這裡插入圖片描述
如果業務需要使用廣播模式,也可以建立多個 Group ID,用於訂閱同一個 Topic。

注意事項

  • 消費進度在服務端維護,可靠性高於廣播模式。
  • 對於一個 Group ID 來說,可以部署一個消費者例項,也可以部署多個消費者例項。當部署多個消費者例項時,例項之間又組成了叢集模式(共同分擔消費訊息)。假設 Group ID 1 部署了三個消費者例項 C1、C2、C3,那麼這三個例項將共同分擔伺服器傳送給 Group ID 1 的訊息。同時,例項之間訂閱關係必須保持一致。

參考:
訊息佇列 RocketMQ 版 叢集消費和廣播消費

5、RocketMQ訊息消費模型

RockerMQ 中的訊息模型就是按照 主題模型 所實現的,又可以稱為 釋出訂閱模型。不需要每個消費者維護自己的訊息佇列,生產者將訊息傳送到topic,消費者訂閱此topic讀取訊息。
所以,RocketMQ 中的 主題模型的實現:
在這裡插入圖片描述
 訊息模型:訊息模型包括producerconsumerbroker三部分。producer生產訊息,consumer消費訊息,broker儲存訊息,broker可以是叢集部署,其中topic位於broker

  • Producer: 一般是業務系統為生產者,將訊息投遞到broker,投遞訊息要經歷“請求-確認”機制,確保訊息不會在投遞過程中丟失。過程:生產者生產訊息到brokerbroker接受訊息寫入topic
    之後給生產者傳送確認相應,如果生產者沒有收到服務端的確認或者收到失敗的響應,則會重新傳送訊息;在消費端,消費者在收到訊息並完成自己的消費業務邏輯(比如,將資料儲存到資料庫中)後,也會給服務端傳送消費成功的確認,服務端只有收到消費確認後,才認為一條訊息被成功消費,否則它會給消費者重新傳送這條訊息,直到收到對應的消費成功確認。

  • Topic:表示一類訊息的集合,每個主題包含若干條訊息,每條訊息只能屬於一個主題,是RocketMQ進行訊息訂閱的基本單位。

  • 生產者組:同一類Producer的集合,這類Producer傳送同一類訊息且傳送邏輯一致。如果傳送的是事物訊息且原始生產者在傳送之後崩潰,則broker伺服器會聯絡同一生產者組的其他生產者例項以提交或回溯消費。

  • 消費者組:同一類consumer的集合,這類consumer通常消費同一類訊息且消費邏輯一致。消費者組使得在訊息消費方面,實現負載均衡和容錯的目標變得非常容易。要注意的是,消費者組的消費者例項必須訂閱完全相同的Topic

RocketMQ 支援兩種訊息模式:叢集消費(Clustering)和廣播消費(Broadcasting)

參考:
RocketMQ訊息模型

6、RocketMQ訊息消費流程

在這裡插入圖片描述

  1. 啟動 NameServerNameServer啟動後進行埠監聽,等待 BrokerProducerConsumer 連上來,相當於一個路由控制中心
  2. Broker 啟動,跟所有的 NameServer 保持長連線,定時傳送心跳包
    • 心跳包中,包含當前 Broker 資訊(IP+埠等)以及儲存所有 Topic 資訊
    • 註冊成功後,NameServer 叢集中就有 TopicBroker 的對映關係
  3. 收發訊息前,先建立 Topic 。建立 Topic 時,需要指定該 Topic 要儲存在哪些 Broker上。也可以在傳送訊息時自動建立Topic
  4. Producer 傳送訊息
    • 啟動時,先跟 NameServer 叢集中的其中一臺建立長連線,並從NameServer 中獲取當前傳送的 Topic 存在哪些 Broker
    • 然後跟對應的 Broker 建立長連線,直接向 Broker 發訊息
  5. Consumer 消費訊息
    • 跟其中一臺 NameServer 建立長連線,獲取當前訂閱 Topic 存在哪些 Broker
    • 然後直接跟 Broker 建立連線通道,開始消費訊息RocketMQ的訊息領域模型

參考:
必須先理解的RocketMQ入門手冊,才能再次深入解讀

7、如何解決訊息重複消費

需要給我們的消費者實現 冪等 ,也就是對同一個訊息的處理結果,執行多少次都不變。

在業務上實現冪等:

  1. 可以使用 寫入 Redis來保證,因為 Rediskeyvalue 就是天然支援冪等的。
  2. 當然還有使用 資料庫插入法 ,基於資料庫的唯一鍵來保證重複資料不會被插入多條。或者先根據主鍵查一下,如果這資料都有了,你就別插入了,update資料。
  3. 可以讓生產者傳送每條資料的時候,裡面加一個全域性唯一的id,類似訂單id之類的東西,然後你這裡消費到了之後,先根據這個id去比如redis裡查一下之前是否消費過,如果沒有消費過,你就處理,然後這個id寫redis。如果消費過了,那你就別處理了,保證別重複處理相同的訊息即可。

參考:
04、如何保證訊息佇列中的訊息不被重複消費

七、git命令的使用

1、版本回退命令

程式碼未提交,本地回退到上一次commit

git reset commit_id

程式碼已提交,回退到指定版本

git revert commit_id

具體可以看第三點

2、git add加入錯誤檔案後如何回退

git status 先看一下add 中的檔案
git reset HEAD 如果後面什麼都不跟的話 就是上一次add 裡面的全部撤銷了
git reset HEAD XXX/XXX/XXX.java 就是對某個檔案進行撤銷了

3、git reset和git revert有什麼區別

git reset 可以用在git commit 錯誤 的時候
先使用

git log 檢視節點
commit xxxxxxxxxxxxxxxxxxxxxxxxxx
Merge:
Author:
Date:

然後

git reset commit_id

還沒有 push 也就是 repo upload 的時候

git reset commit_id (回退到上一個 提交的節點 程式碼還是原來你修改的)
git reset –hard commit_id (回退到上一個commit節點, 程式碼也發生了改變,變成上一次的)

如果要是 提交了以後,可以使用 git revert 用來還原已經提交的修改
此次操作之前和之後的commit和history都會保留,並且把這次撤銷作為一次最新的提交

git revert HEAD 撤銷前一次 commit
git revert HEAD^ 撤銷前前一次 commit
git revert commit-id (撤銷指定的版本,撤銷也會作為一次提交進行儲存)
git revert是提交一個新的版本,將需要revert的版本的內容再反向修改回去,版本會遞增,不影響之前提交的內容。

參考:
git add , git commit 新增錯檔案 撤銷

4、git rebase的用法

git rebase:將多次commit合併,只保留一次提交歷史。
具體的操做參考:
使用git rebase合併多次commit

5、如何把一個專案遷移到另一個git倉庫

1、使用git remote set-url origin命令替換遠端的git倉庫

git remote set-url origin git@git.xxxxxxxxx/xxx.git

2、使用git clone --baregit push --mirror命令

  1. 從原地址克隆一份裸版本庫,比如原本託管於 GitHub。

    git clone --bare git://github.com/username/project.git
    
  2. 然後到新的 Git 伺服器上建立一個新專案,比如 GitCafe。

  3. 以映象推送的方式上傳程式碼到 GitCafe 伺服器上。

    cd project.git
    
    git push --mirror git@gitcafe.com/username/newproject.git
    
  4. 刪除原生程式碼

    cd ..
    
    rm -rf project.git
    
  5. 到新伺服器 GitCafe 上找到 Clone 地址,直接 Clone 到本地就可以了。

    git clone git@gitcafe.com/username/newproject.git
    

這種方式可以保留原版本庫中的所有內容。

參考:
從一個git倉庫遷移到另外一個git倉庫

八、docker相關

1、使用docker自動化部署專案的流程

  • 梳理中

2、docker命令

1 使用docker查所有的映象

docker images

或:

docker image ls

2 使用docker查所有的容器

docker ps -a

3 docker進入容器內部的命令

docker exec -it xxxxxxxxx  /bin/sh 

4 docker 資料卷

管理卷

# docker volume create edc-nginx-vol // 建立一個自定義容器卷
# docker volume ls // 檢視所有容器卷
# docker volume inspect edc-nginx-vol // 檢視指定容器卷詳情資訊

有了自定義容器卷,我們可以建立一個使用這個資料卷的容器,這裡我們以nginx為例:
在這裡插入圖片描述
建立使用指定卷的容器

# docker run -d -it --name=edc-nginx -p 8800:80 -v edc-nginx-vol:/usr/share/nginx/html nginx

其中,-v代表掛載資料卷,這裡使用自定資料卷edc-nginx-vol,並且將資料卷掛載到/usr/share/nginx/html (這個目錄是yum安裝nginx的預設網頁目錄)。

如果沒有通過-v指定,那麼Docker會預設幫我們建立匿名資料捲進行對映和掛載。
  
參考:
你必須知道的Docker資料卷(Volume)

3、DockerFile

1 DockerFile的關鍵標籤

1)FROM(指定基礎映象)
構建指令,Docker中必須使用FROM命令,必須指定且需要在Dockerfile其他指令的前面。後續的指令都依賴於該指令指定的image。FROM指令指定的基礎image可以是官方遠端倉庫中的,也可以位於本地倉庫。

2)MAINTAINER(指定映象建立者資訊)
構建指令,用於將image的製作者相關的資訊寫入到image中,一般輸入名字和電子郵箱即可。當我們對該image執行docker inspect命令時,輸出中有相應的欄位記錄該資訊。

3)RUN(安裝軟體用)
構建指令,在FROM中設定的映象上執行指令碼或命令,在RUN可以執行任何被基礎image支援的命令。如基礎image選擇了ubuntu,那麼軟體管理部分只能使用ubuntu的命令。

4)CMD(設定container啟動時執行的操作)
設定指令,用於容器啟動時指定的操作。該操作可以是執行自定義指令碼,也可以是執行系統命令。
一個Dockerfile中只能有一條CMD命令,多條則只執行最後一條CMD.

5)ENTRYPOINT(設定container啟動時執行的操作)
設定指令,指定容器啟動時執行的命令。
但是一個Dockerfile中只能有一條ENTRYPOINT命令,可以多次設定,但是隻有最後一個有效。

6)EXPOSE(指定容器需要對映到宿主機器的埠)
設定指令,該指令會將容器中的埠對映成宿主機器中的某個埠。

7)ENV(用於設定環境變數)
構建指令,在image中設定一個環境變數。

8)ADD(向映象新增檔案)
構建指令,所有拷貝到container中的檔案和資料夾許可權為0755,uid和gid為0。

9)COPY(向映象新增檔案)
複製本地主機的 <src> (為Dockerfile所在目錄的相對路徑)到容器中的 <dest>

使用COPY新增檔案時,不會解壓縮,也不能使用檔案URL

10)VOLUME(指定掛載點)
設定指令,使容器中的一個目錄具有持久化儲存資料的功能,該目錄可以被容器本身使用,也可以共享給其他容器使用。,當容器關閉後,所有的更改都會丟失。當容器中的應用有持久化資料的需求時可以在Dockerfile中使用該指令。可以將本地資料夾或者其他container的資料夾掛載到container中。

11)WORKDIR(切換目錄)
切換目錄用,設定指令,可以多次切換(相當於cd命令),對RUN,CMD,ENTRYPOINT生效。

更多使用及參考:
DockerFile關鍵字介紹

2 DockerFile的COPY和ADD有什麼區別

Dockerfile中的COPY指令和ADD指令都可以將主機上的資源複製或加入到容器映象中,都是在構建映象的過程中完成的。

COPY指令和ADD指令的唯一區別在於是否支援從遠端URL獲取資源。COPY指令只能從執行docker build所在的主機上讀取資源並複製到映象中。而ADD指令還支援通過URL從遠端伺服器讀取資源並複製到映象中。

滿足同等功能的情況下,推薦使用COPY指令。ADD指令更擅長讀取本地tar檔案並解壓縮。

參考:
Dockerfile中的COPY和ADD指令詳解與比較

九、Linux命令

1、建立單層資料夾以及建立多級資料夾命令

1 建立單層資料夾

建立單層資料夾,不能建立多層,不能重複建立。 比如mkdir abc。

mkdir <folder> 

2 建立多層次資料夾

建立多層次資料夾,可以重複建立同一個資料夾。如mkdir -p abc/a/b/c/

mkdir -p <path> 

2、刪除資料夾

rm -rf 指定目錄* 

3、如何賦給使用者管理員許可權

sudo usermod -G <管理員組> <使用者>

但是這樣設定話每次用到使用root許可權都需要輸入密碼,可以修改/etc/sudoers 配置檔案來設定執行root許可權操作的時候不輸入密碼。
進入root使用者,修改 /etc/sudoers 配置檔案,先檢視root對這個檔案的許可權:

[yaoqi@java-devenv ~]$ ll /etc/sudoers
-r--r----- 1 root root 4187 Nov 17 19:20 /etc/sudoers

root使用者對該檔案只有讀的許可權,需要給root使用者寫的許可權。

[root@java-devenv ~]# chmod +w /etc/sudoers
[root@java-devenv ~]# ll /etc/sudoers
-rw-r-----. 1 root root 4188 Jul  7  2015 /etc/sudoers

執行chmod +w /etc/sudoers 就是給當前使用者對/etc/sudoers檔案增加寫的許可權。然後再用vim 修改該檔案。

vim /etc/sudoers
...
## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)       ALL
## Same thing without a password
 %wheel ALL=(ALL)       NOPASSWD: ALL

##Same thing without a password 下一行的註釋去掉,就可以設定執行root許可權操作的時候不輸入密碼了(註釋中說的很明確了)。
操作完成之後記得收回root的許可權。

[root@java-devenv ~]# chmod -w /etc/sudoers
[root@java-devenv ~]# ll /etc/sudoers
-r--r----- 1 root root 4187 Nov 17 19:20 /etc/sudoers

然後登入新建的使用者,雖然加入了管理員使用者組,但是執行root許可權操作的時候用命令前面需要加上sudo。
比如:在登入了剛剛新建使用者後試試 vim /etc/sudoers 看不到任何東西,sudo vim /etc/sudoers 後才能看到該配置檔案中的內容。

參考:
linux新建使用者並賦管理員許可權

十、前端

1、js中===== 有什麼區別

簡單來說: == 代表相同, === 代表嚴格相同

這麼理解: 當進行==比較時候: 先檢查兩個運算元資料型別,如果相同, 則進行 === 比較, 如果不同, 則願意為你進行一次型別轉換, 轉換成相同型別後再進行比較, 而 === 比較時, 如果型別不同,直接就是false.

運算元1 == 運算元2, 運算元1 === 運算元2

比較過程:

雙等號==

(1)如果兩個值型別相同,再進行三個等號(===)的比較

(2)如果兩個值型別不同,也有可能相等,需根據以下規則進行型別轉換在比較:

  • 1)如果一個是null,一個是undefined,那麼相等
  • 2)如果一個是字串,一個是數值,把字串轉換成數值之後再進行比較

三等號===:
(1)如果型別不同,就一定不相等
(2)如果兩個都是數值,並且是同一個值,那麼相等;如果其中至少一個是NaN,那麼不相等。(判斷一個值是否是NaN,只能使用isNaN( ) 來判斷)
(3)如果兩個都是字串,每個位置的字元都一樣,那麼相等,否則不相等。
(4)如果兩個值都是true,或是false,那麼相等
(5)如果兩個值都引用同一個物件或是函式,那麼相等,否則不相等
(6)如果兩個值都是null,或是undefined,那麼相等

參考:
js中=====區別

2、js的資料型別

8種。Number、String、Boolean、Null、undefined、object、symbol、bigInt。

十一、關於專案

1、專案整體敘述

2、專案中有遇到什麼困難嗎?如何解決的?

這裡主要是面試官想考察你的問題處理思路和是否有固定的處理問題套路。

3、關於專案釋出gitlab上有什麼規範

4、專案開發中API介面安全性如何保證?

Token授權機制
使用者使用使用者名稱密碼登入後伺服器給客戶端返回一個Token(通常是UUID),並將Token-UserId以鍵值對的形式存放在快取伺服器中。服務端接收到請求後進行Token驗證,如果Token不存在,說明請求無效。Token是客戶端訪問服務端的憑證。

時間戳超時機制
使用者每次請求都帶上當前時間的時間戳timestamp,服務端接收到timestamp後跟當前時間進行比對,如果時間差大於一定時間(比如5分鐘),則認為該請求失效。時間戳超時機制是防禦DOS攻擊的有效手段。

簽名機制
將 Token 和 時間戳 加上其他請求引數再用MD5或SHA-1演算法(可根據情況加點鹽)加密,加密後的資料就是本次請求的簽名sign,服務端接收到請求後以同樣的演算法得到簽名,並跟當前的簽名進行比對,如果不一樣,說明引數被更改過,直接返回錯誤標識。簽名機制保證了資料不會被篡改。

輸入引數驗證
在到達應用程式邏輯之前,首先驗證請求引數。進行嚴格的驗證檢查,如果驗證失敗,則立即拒絕請求。在API響應中,傳送相關的錯誤訊息和正確輸入格式的示例,以改善使用者體驗。

一律使用HTTPS
通過始終使用SSL,可以將身份驗證憑據簡化為隨機生成的訪問令牌,該令牌在HTTP基本身份驗證的使用者名稱欄位中提供。它相對簡單易用,並且免費提供許多安全功能。
如果使用HTTP 2來提高效能–甚至可以通過單個連線傳送多個請求,則可以避免以後的請求完全進行TCP和SSL握手。

參考:
API介面安全性設計
REST API Security Essentials

5、專案當中用到的加密演算法?

業務中用到的是MD5RSA

1 MD5加密演算法

MD5 用的是 雜湊函式,它的典型應用是對一段資訊產生 資訊摘要,以 防止被篡改。嚴格來說,MD5 不是一種 加密演算法 而是 摘要演算法。無論是多長的輸入,MD5都會輸出長度為 128bits 的一個串 (通常用 16進位制 表示為 32個字元)。

public static final byte[] computeMD5(byte[] content) {
    try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        return md5.digest(content);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

2 RSA加密演算法

RSA 加密演算法是目前最有影響力的 公鑰加密演算法,並且被普遍認為是目前 最優秀的公鑰方案 之一。RSA 是第一個能同時用於 加密數字簽名 的演算法,它能夠 抵抗 到目前為止已知的 所有密碼攻擊,已被 ISO 推薦為公鑰資料加密標準。

RSA 加密演算法 基於一個十分簡單的數論事實:將兩個大 素數 相乘十分容易,但想要對其乘積進行 因式分解 卻極其困難,因此可以將 乘積 公開作為 加密金鑰

import net.pocrd.annotation.NotThreadSafe;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.Security;
import java.security.Signature;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@NotThreadSafe
public class RsaHelper {
    private static final Logger logger = LoggerFactory.getLogger(RsaHelper.class);
    private RSAPublicKey publicKey;
    private RSAPrivateCrtKey privateKey;

    static {
        Security.addProvider(new BouncyCastleProvider()); //使用bouncycastle作為加密演算法實現
    }

    public RsaHelper(String publicKey, String privateKey) {
        this(Base64Util.decode(publicKey), Base64Util.decode(privateKey));
    }

    public RsaHelper(byte[] publicKey, byte[] privateKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
            if (privateKey != null && privateKey.length > 0) {
                this.privateKey = (RSAPrivateCrtKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public RsaHelper(String publicKey) {
        this(Base64Util.decode(publicKey));
    }

    public RsaHelper(byte[] publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] encrypt(byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }

        if (content == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            int size = publicKey.getModulus().bitLength() / 8 - 11;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 11));
            int left = 0;
            for (int i = 0; i < content.length; ) {
                left = content.length - i;
                if (left > size) {
                    cipher.update(content, i, size);
                    i += size;
                } else {
                    cipher.update(content, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] decrypt(byte[] secret) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }

        if (secret == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            int size = privateKey.getModulus().bitLength() / 8;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size - 12) / (size - 11) * size);
            int left = 0;
            for (int i = 0; i < secret.length; ) {
                left = secret.length - i;
                if (left > size) {
                    cipher.update(secret, i, size);
                    i += size;
                } else {
                    cipher.update(secret, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error("rsa decrypt failed.", e);
        }
        return null;
    }

    public byte[] sign(byte[] content) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        if (content == null) {
            return null;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean verify(byte[] sign, byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        if (sign == null || content == null) {
            return false;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        } catch (Exception e) {
            logger.error("rsa verify failed.", e);
        }
        return false;
    }
}

在專案中還用到了jasypt類包,主要用來對Redis、資料庫的密碼加密。
詳情可以參考Spring Boot中使用 jasypt 處理加密問題
或者官方文件 jasypt-spring-boot

參考:
淺談常見的七種加密演算法及實現

6、說一說你們專案中會用到的依賴

1 google guava

Guava是一個Google開發的基於java的擴充套件專案,提供了很多有用的工具類,可以讓java程式碼更加優雅,更加簡潔。

Guava包括諸多工具類,比如Collections,cache,concurrent,hash,reflect,annotations,eventbus等。

 		<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.2-jre</version>
        </dependency>

參考:
google guava類庫介紹

2 Apache Commons Lang3

Apache Commons Lang庫提供了標準Java庫函式裡所沒有提供的Java核心類的操作方法。Apache Commons Langjava.lang API提供了大量的輔助工具,尤其是在String操作方法,基礎數值方法,物件引用,併發行,建立及序列化,系統屬性方面。
Lang3.0及其後續版本使用的包名為org.apache.commons.lang3,而之前的版本為org.apache.commons.lang,允許其在被使用的同時作為一個較早的版本。

		<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

參考:
Commons.lang 工具

3 commons-codec

commons-codec是Apache開源組織提供的用於摘要運算、編碼解碼的包。常見的編碼解碼工具Base64、MD5、Hex、SHA1、DES等。

	    <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.9</version>
        </dependency>

參考:
commons-codec使用簡介

4 lombok

Lombok是一種 Java™ 實用工具,可用來幫助開發人員消除 Java 的冗長,尤其是對於簡單的 Java 物件(POJO)。它通過註解實現這一目的。它通過註釋實現這一目的。通過在開發環境中實現Lombok,開發人員可以節省構建諸如hashCode()和equals()這樣的方法以及以往用來分類各種accessor和mutator的大量時間。

 		 <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

參考:
十分鐘搞懂Lombok使用與原理

5 commons-io

Apache Commons IO是Apache基金會建立並維護的Java函式庫。它提供了許多類使得開發者的常見任務變得簡單,同時減少重複程式碼,這些程式碼可能遍佈於每個獨立的專案中,你卻不得不重複的編寫。這些類由經驗豐富的開發者維護,對各種問題的邊界條件考慮周到,並持續修復相關bug。

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

參考:
Java IO流學習總結七:Commons IO 2.5-FileUtils

6 Hutool

一個Java基礎工具類,對檔案、流、加密解密、轉碼、正則、執行緒、XML等JDK方法進行封裝,組成各種Util工具類,同時提供以下元件:
在這裡插入圖片描述

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.8</version>
</dependency>

參考:
hutool的github庫
參考:
常用的JAVA依賴庫

7、國際化的專案開發要注意什麼

1 需要解決頁面國際化問題

1、
詳細可參考:
四種方式解決頁面國際化問題——步驟詳解

2 需要處理日期時間的問題

1)日期時間的國際化格式問題處理

對應的關鍵詞:Locale

日期時間的國際化格式指的是在不同的國家和地區對日期時間的顯示方式不同,主要通過不同國家地區不同的語言習慣,對同一個實現的呈現方式不同。在java中需要結合Locale類進行處理:

public class DateTest
{
	public static void main(String[] args)
	{
		Date date = new Date();
		Locale locale = Locale.CHINA;
		DateFormat shortDf = DateFormat
				.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM,
						locale);
		System.out.println("中國格式:" + shortDf.format(date));

		locale = Locale.ENGLISH;
		shortDf = DateFormat
				.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM,
						locale);
		System.out.println("英國格式:" + shortDf.format(date));
	}
}

結果:

中國格式:2020-7-1 17:55:32
英國格式:Jul 1, 2020 5:55:32 PM

2)日期時間的時區問題處理

對應的關鍵詞:TimeZone

日期時間的時區問題,指的是在同一時刻,地球上的各個地區的日期時間不同。全球劃分為24個時區,每個相鄰時區時間相差一個小時(中國為了方便統一,雖然跨越5個時區,但都使用同一個時區時間),也就是說在同一時刻,全球同一時刻對應的當地時間的小時數有可能是0-23點之間的一個值。這裡拿中國上海和英國倫敦舉例:

public class TimeZoneTest
{
	public static void main(String[] args)
	{
		Date date = new Date();
		Locale locale = Locale.CHINA;
		DateFormat shortDf = DateFormat
				.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM,
						locale);
		shortDf.setTimeZone(
				TimeZone.getTimeZone("Asia/Shanghai"));//Asia/Chongqing
		System.out.println(TimeZone.getDefault().getID());
		System.out.println("中國當前日期時間:" + shortDf.format(date));

		locale = Locale.ENGLISH;
		shortDf = DateFormat
				.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM,
						locale);
		shortDf.setTimeZone(TimeZone.getTimeZone("Europe/London"));
		System.out.println("英國當前日期時間:" + shortDf.format(date));
	}
}

結果:

Asia/Shanghai
中國當前日期時間:2020-7-1 17:57:31
英國當前日期時間:Jul 1, 2020 10:57:31 AM

說明同一時刻,中國上海和英國倫敦相差7個小時,也就是相差7個時區。

參考:
java國際化之時區問題處理

8、專案中高併發和高可用的方案?

1 高併發方案

高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發使用者數等。

我們在應對高併發大流量時也會採用類似“抵禦洪水”的方案,歸納起來共有三種方法。

1)Scale-out(橫向擴充套件)

分而治之是一種常見的高併發系統設計方法,採用分散式部署的方式把流量分流開,讓每個伺服器都承擔一部分併發和流量。實際應用方案如:資料庫一主多從,分褲分表,儲存分片。
網際網路分層架構中,各層次水平擴充套件的實踐又有所不同:

(1)反向代理層可以通過“DNS輪詢”的方式來進行水平擴充套件

在這裡插入圖片描述

dns-server對於一個域名配置了多個解析ip,每次DNS解析請求來訪問dns-server,會輪詢返回這些ip。
當nginx成為瓶頸的時候,只要增加伺服器數量,新增nginx服務的部署,增加一個外網ip,就能擴充套件反向代理層的效能,做到理論上的無限高併發

(2)站點層可以通過nginx來進行水平擴充套件

在這裡插入圖片描述

通過修改nginx.conf,可以設定多個web後端。
當web後端成為瓶頸的時候,只要增加伺服器數量,新增web服務的部署,在nginx配置中配置上新的web後端,就能擴充套件站點層的效能,做到理論上的無限高併發。

(3)服務層可以通過服務連線池來進行水平擴充套件

在這裡插入圖片描述

站點層通過RPC-client呼叫下游的服務層RPC-server時,RPC-client中的連線池會建立與下游服務多個連線,當服務成為瓶頸的時候,只要增加伺服器數量,新增服務部署,在RPC-client處建立新的下游服務連線,就能擴充套件服務層效能,做到理論上的無限高併發。如果需要優雅的進行服務層自動擴容,這裡可能需要配置中心裡服務自動發現功能的支援。

(4)資料庫可以按照資料範圍,或者資料雜湊的方式來進行水平擴充套件
1.按照範圍水平拆分

在這裡插入圖片描述
每一個資料服務,儲存一定範圍的資料,上圖為例:
user0庫,儲存uid範圍1-1kw
user1庫,儲存uid範圍1kw-2kw

這個方案的好處是:

(1)規則簡單,service只需判斷一下uid範圍就能路由到對應的儲存服務;
(2)資料均衡性較好;
(3)比較容易擴充套件,可以隨時加一個uid[2kw,3kw]的資料服務;

不足是:

(1)請求的負載不一定均衡,一般來說,新註冊的使用者會比老使用者更活躍,大range的服務請求壓力會更大;

2.按照雜湊水平拆分

雜湊水平拆分
每一個資料庫,儲存某個key值hash後的部分資料,上圖為例:
user0庫,儲存偶數uid資料
user1庫,儲存奇數uid資料

這個方案的好處是:

(1)規則簡單,service只需對uid進行hash能路由到對應的儲存服務;
(2)資料均衡性較好;
(3)請求均勻性較好;

不足是:
(1)不容易擴充套件,擴充套件一個資料服務,hash方法改變時候,可能需要進行資料遷移;

這裡需要注意的是,通過水平拆分來擴充系統效能,與主從同步讀寫分離來擴充資料庫效能的方式有本質的不同。

通過水平拆分擴充套件資料庫效能:

(1)每個伺服器上儲存的資料量是總量的1/n,所以單機的效能也會有提升;
(2)n個伺服器上的資料沒有交集,那個伺服器上資料的並集是資料的全集;
(3)資料水平拆分到了n個伺服器上,理論上讀效能擴充了n倍,寫效能也擴充了n倍(其實遠不止n倍,因為單機的資料量變為了原來的1/n);

通過主從同步讀寫分離擴充套件資料庫效能:

(1)每個伺服器上儲存的資料量是和總量相同; (2)n個伺服器上的資料都一樣,都是全集;
(3)理論上讀效能擴充了n倍,寫仍然是單點,寫效能不變;

2)快取

使用快取來提高系統的效能,就好比用“拓寬河道”的方式抵抗高併發大流量的衝擊。

3)非同步

在某些場景下,未處理完成之前我們可以讓請求先返回,在資料準備好之後再通知請求方,這樣可以在單位時間內處理更多的請求。

參考:
究竟啥才是網際網路架構“高併發”

2 高可用的方案

保證系統高可用,架構設計的核心準則是:冗餘
大部分網際網路公司的系統架構:
大部分網際網路公司的系統架構
1、從客戶端到反向代理Nginx這塊,這個1臺nginx是會可能發生故障的,所以這裡可以再冗餘一臺Nginx,可以利用linux的 keeplived進行探測可用性,當一臺Nginx掛了之後,責會自動轉移到另一臺Nginx機器上來,從而保證高可用。
2、從反向代理到後端服務service這塊,反向代理這塊,目前最受歡迎的是nginx,效能方面表現也很好,nginx能夠自動探測後端服務的可用性,只需在nginx,config配置多臺後端服務就行了。
3、從後端服務到快取這塊,快取這塊推薦使用redis主從同步方案來達到高可用,redis主從同步加上sentine哨兵機制來自動探活redis例項。
4、從後端服務到寫資料庫這塊,這裡可以採用雙主機制,一臺給線上使用,另一臺冗餘,當線上那臺掛了才會階梯過來使用寫功能,同樣是通過linux的keepalived進行自動探活。
5、從後端服務到讀資料庫這塊,這裡同樣是將讀庫部署多臺,例如部署2臺,通過程式碼段增加連線池元件進行路由讀庫和探活。

注:大部分網際網路技術,資料庫層都用了主從同步,讀寫分離架構,所以資料庫層的高可用,又分為“讀庫高可用”與“寫庫高可用”兩類

保證系統高可用的幾個方面

1)超時機制

當併發稍大一點的情況下會出現返回很慢,一直佔用當前資源,使得我們大量的請求阻塞等時需要對此進行設定合理的超時時間,來快速結束這些慢請求,來保證我們系統的可用性。

2)降級

在面對流量劇增的時候,例如,秒殺,大促等這些劇增的大流量,可以將不影響本次業務的流程也砍掉,不去呼叫了,直接走核心業務。其實各大電商都是採取這種方案來保證系統的可用性的。

3)限流

限流是,本來我係統只能抗住10萬併發,然後現在我們運營搞了什麼牛逼的大促,搞的來了10倍的流量,那麼系統就把瞬間不能處理的流量給截斷,直接返回給使用者,使用者可以待會兒再試。所以限流就是為了保證系統的高可用而限制住大流量的情況發生。

參考:
你的系統怎麼保證高可用

9、為什麼需要用分散式服務

1、業務拆分
大型網站為了應對日益複雜的業務場景,通過使用分而治之的手段將整個網站業務分成不同的產品線,
2、分散式服務
3、網站擴充套件性
擴充套件性是指對現有系統影響最小的情況下,系統功能可持續擴充套件或提升的能力。設計網站可擴充套件架構的核心思想是模組化,並在此基礎上,降低模組間的耦合性,提供模組的複用性。模組通過分散式部署,獨立的模組部署在獨立的伺服器上(叢集)從物理上分離模組之間的耦合關係。模組分散式部署以後具體聚合方式主要有分散式訊息佇列和分散式服務。

  • 利用分散式訊息佇列降低系統耦合性

    模組之間不存在直接呼叫,那麼新增模組或者修改模組對其他模組影響最小,這樣系統的可擴充套件性無疑更好一些。
    通過在低耦合的模組之間傳輸事件訊息,以保持模組的鬆散耦合,並藉助事件訊息的通訊完成模組間合作,典型的架構就是生產者消費者模式。最常用的就是分散式訊息佇列

  • 利用分散式服務打造可複用的業務平臺

    分散式服務則通過介面分解系統耦合性,不同子系統通過相同的介面描述進行服務呼叫。
    大型網站分散式服務的需求與特點:
    負載均衡
    失效轉移
    高效的遠端通訊
    整合異構系統
    對應用最小入侵
    版本管理
    實時監控

簡單來說
分散式:不同模組部署在不同伺服器上
作用:分散式解決網站高併發帶來問題

參考:
大型網站為什麼要使用分散式服務
分散式、叢集、微服務、SOA 之間的區別

十二、設計模式

1、常用的設計模式

1 單例模式

1)為什麼要用到單例模式?

(1)資源共享的情況下,避免由於資源操作時導致的效能或損耗等。如上述中的日誌檔案,應用配置。

(2)控制資源的情況下,方便資源之間的互相通訊。如執行緒池等。

參考:
設計模式——Java中為什麼要使用單例模式

2)Spring單例Bean與單例模式的區別

Spring單例Bean與單例模式的區別在於它們關聯的環境不一樣,單例模式是指在一個JVM程式中僅有一個例項,而Spring單例是指一個Spring Bean容器(ApplicationContext)中僅有一個例項。

參考:
Spring單例Bean與單例模式的區別

2 工廠模式

1)簡單工廠模式

2)抽象工廠模式

3 策略模式

4 建造者模式

5 介面卡模式

6 代理模式

2、設計模式的六大原則

1 單一職責原則(Single Responsibility Principle,簡稱SRP )

核心思想:應該有且僅有一個原因引起類的變更
問題描述:假如有類Class1完成職責T1,T2,當職責T1或T2有變更需要修改時,有可能影響到該類的另外一個職責正常工作。
好處:類的複雜度降低、可讀性提高、可維護性提高、擴充套件性提高、降低了變更引起的風險。
需注意:單一職責原則提出了一個編寫程式的標準,用“職責”或“變化原因”來衡量介面或類設計得是否優良,但是“職責”和“變化原因”都是不可以度量的,因專案和環境而異。

2 里氏替換原則(Liskov Substitution Principle,簡稱LSP)

核心思想:在使用基類的的地方可以任意使用其子類,能保證子類完美替換基類。
通俗來講:只要父類能出現的地方子類就能出現。反之,父類則未必能勝任。
好處:增強程式的健壯性,即使增加了子類,原有的子類還可以繼續執行。
需注意:如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係 採用依賴、聚合、組合等關係代替繼承。

3 依賴倒置原則(Dependence Inversion Principle,簡稱DIP)

核心思想:高層模組不應該依賴底層模組,二者都該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象;
說明:高層模組就是呼叫端,低層模組就是具體實現類。抽象就是指介面或抽象類。細節就是實現類。
通俗來講:依賴倒置原則的本質就是通過抽象(介面或抽象類)使個各類或模組的實現彼此獨立,互不影響,實現模組間的鬆耦合。
問題描述:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。這種場景下,類A一般是高層模組,負責複雜的業務邏輯;類B和類C是低層模組,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。
解決方案:將類A修改為依賴介面interface,類B和類C各自實現介面interface,類A通過介面interface間接與類B或者類C發生聯絡,則會大大降低修改類A的機率。
好處:依賴倒置的好處在小型專案中很難體現出來。但在大中型專案中可以減少需求變化引起的工作量。使並行開發更友好。

4 介面隔離原則(Interface Segregation Principle,簡稱ISP)

核心思想:類間的依賴關係應該建立在最小的介面上
通俗來講:建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。
問題描述:類A通過介面interface依賴類B,類C通過介面interface依賴類D,如果介面interface對於類A和類B來說不是最小介面,則類B和類D必須去實現他們不需要的方法。
需注意:介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性,但是如果過小,則會造成介面數量過多,使設計複雜化。所以一定要適度
提高內聚,減少對外互動
。使介面用最少的方法去完成最多的事情
為依賴介面的類定製服務。只暴露給呼叫的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模組提供定製服務,才能建立最小的依賴關係。

5 迪米特法則(Law of Demeter,簡稱LoD)

核心思想:類間解耦。
通俗來講: 一個類對自己依賴的類知道的越少越好。自從我們接觸程式設計開始,就知道了軟體程式設計的總的原則:低耦合,高內聚。無論是程式導向程式設計還是物件導向程式設計,只有使各個模組之間的耦合儘量的低,才能提高程式碼的複用率。低耦合的優點不言而喻,但是怎麼樣程式設計才能做到低耦合呢?那正是迪米特法則要去完成的。

6 開放封閉原則(Open Close Principle,簡稱OCP)

核心思想:儘量通過擴充套件軟體實體來解決需求變化,而不是通過修改已有的程式碼來完成變化
通俗來講: 一個軟體產品在生命週期內,都會發生變化,既然變化是一個既定的事實,我們就應該在設計的時候儘量適應這些變化,以提高專案的穩定性和靈活性。

總結

單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向介面程式設計;介面隔離原則告訴我們在設計介面的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴充套件開放,對修改關閉。

參考
快速理解-設計模式六大原則

十三、Nginx相關

1、Nginx的應用

1 動靜分離

動靜分離的好處:
1)api介面服務化:動靜分離之後,後端應用更為服務化,只需要通過提供api介面即可,可以為多個功能模組甚至是多個平臺的功能使用,可以有效的節省後端人力,更便於功能維護。

2)前後端開發並行:前後端只需要關心介面協議即可,各自的開發相互不干擾,並行開發,並行自測,可以有效的提高開發時間,也可以有些的減少聯調時間

3)減輕後端伺服器壓力,提高靜態資源訪問速度:後端不用再將模板渲染為html返回給使用者端,且靜態伺服器可以採用更為專業的技術提高靜態資源的訪問速度。

2 反向代理

所謂反向代理,很簡單,其實就是在location這一段配置中的root替換成proxy_pass即可。root說明是靜態資源,可以由Nginx進行返回;而proxy_pass說明是動態請求,需要進行轉發,比如代理到Tomcat上。

反向代理的作用

  • 保障應用伺服器的安全(增加一層代理,可以遮蔽危險攻擊,更方便的控制許可權)
  • 實現負載均衡(稍等~下面會講)
  • 實現跨域(號稱是最簡單的跨域方式)

3 負載均衡

在伺服器叢集中,Nginx 可以將接收到的客戶端請求“均勻地”(嚴格講並不一定均勻,可以通過設定權重)分配到這個叢集中所有的伺服器上。這個就叫做負載均衡。

4 正向代理

正向代理,意思是一個位於客戶端和原始伺服器(origin server)之間的伺服器,為了從原始伺服器取得內容,客戶端向代理髮送一個請求並指定目標(原始伺服器),然後代理向原始伺服器轉交請求並將獲得的內容返回給客戶端。客戶端才能使用正向代理。當你需要把你的伺服器作為代理伺服器的時候,可以用Nginx來實現正向代理。

參考:
連前端都看得懂的《Nginx 入門指南》
8分鐘帶你深入淺出搞懂Nginx

相關文章