java進階面試題

JVM相關:

jvm中一次完整的GC流程(從ygc到fgc)是怎樣的,重點講講對象如何晉升到老年代,幾種主要的jvm參數等

新生代GC ygc(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
老年代GC fgc(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上
大對象直接進入老年代
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。
爲了避免爲大對象分配內存時由於分配擔保機制帶來的複製而降低效率。
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別那些對象應放在新生代,那些對象應放在老年代中。爲了做到這一點,
虛擬機給每個對象一個對象年齡(Age)計數器。
如果對象在 Eden 出生並經過第一次 Minor GC 後仍然能夠存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲1.
對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認爲15歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,
可以通過參數 -XX:MaxTenuringThreshold 來設置。

比較重要的jvm參數

 -Xss:每個線程的棧大小
  -Xms:初始堆大小,默認物理內存的1/64
  -Xmx:最大堆大小,默認物理內存的1/4
  -Xmn:新生代大小
  -XX:NewSize:設置新生代初始大小
  -XX:NewRatio:默認2表示新生代佔年老代的1/2,佔整個堆內存的1/3。
  -XX:SurvivorRatio:默認8表示一個survivor區佔用1/8的Eden內存,即1/10的新生代內存。
 -XX:MetaspaceSize:設置元空間大小
  -XX:MaxMetaspaceSize:設置元空間最大允許大小,默認不受限制,JVM Metaspace會進行動態擴展。
垃圾回收統計信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps 
  -Xloggc:filename
收集器設置
  -XX:+UseSerialGC:設置串行收集器
  -XX:+UseParallelGC:設置並行收集器
  -XX:+UseParallelOldGC:老年代使用並行回收收集器
  -XX:+UseParNewGC:在新生代使用並行收集器
  -XX:+UseParalledlOldGC:設置並行老年代收集器
  -XX:+UseConcMarkSweepGC:設置CMS併發收集器
  -XX:+UseG1GC:設置G1收集器
  -XX:ParallelGCThreads:設置用於垃圾回收的線程數
並行收集器設置
  -XX:ParallelGCThreads:設置並行收集器收集時使用的CPU數。並行收集線程數。
  -XX:MaxGCPauseMillis:設置並行收集最大暫停時間
  -XX:GCTimeRatio:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)
CMS收集器設置
  -XX:+UseConcMarkSweepGC:設置CMS併發收集器
  -XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況。
  -XX:ParallelGCThreads:設置併發收集器新生代收集方式爲並行收集時,使用的CPU數。並行收集線程數。
  -XX:CMSFullGCsBeforeCompaction:設定進行多少次CMS垃圾回收後,進行一次內存壓縮
  -XX:+CMSClassUnloadingEnabled:允許對類元數據進行回收
  -XX:UseCMSInitiatingOccupancyOnly:表示只在到達閥值的時候,才進行CMS回收
  -XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況
  -XX:ParallelCMSThreads:設定CMS的線程數量
  -XX:CMSInitiatingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發
  -XX:+UseCMSCompactAtFullCollection:設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理	
G1收集器設置 
  -XX:+UseG1GC:使用G1收集器
  -XX:ParallelGCThreads:指定GC工作的線程數量
  -XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區
  -XX:GCTimeRatio:吞吐量大小,0-100的整數(默認9),值爲n則系統將花費不超過1/(1+n)的時間用於垃圾收集
  -XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
  -XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%)
  -XX:G1MaxNewSizePercent:新生代內存最大空間
  -XX:TargetSurvivorRatio:Survivor填充容量(默認50%)
  -XX:MaxTenuringThreshold:最大任期閾值(默認15)
  -XX:InitiatingHeapOccupancyPercen:老年代佔用空間超過整堆比IHOP閾值(默認45%),超過則執行混合收集
  -XX:G1HeapWastePercent:堆廢物百分比(默認5%)
  -XX:G1MixedGCCountTarget:參數混合週期的最大總次數(默認8)
  • 你知道哪幾種垃圾收集器,各自的優缺點,重點講下cms
1.Serial(串行)收集器收集器
Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它只會使用一條
垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束
新生代採用複製算法,老年代採用標記-整理算法。

2.ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。
新生代採用複製算法,老年代採用標記-整理算法。

3.Parallel Scavenge收集器
Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用於運行用戶
代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,
可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
新生代採用複製算法,老年代採用標記-整理算法。
4.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用,
它是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
初始標記: 暫停所有的其他線程(STW),並記錄下直接與root相連的對象,速度很快 ;
併發標記: 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象
。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,
這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
併發清除: 開啓用戶線程,同時GC線程開始對未標記的區域做清掃。
從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:
對CPU資源敏感(會和服務搶資源);
無法處理浮動垃圾(在java業務程序線程與垃圾收集線程併發執行過程中又產生的垃圾,這種浮動垃圾只能等到下一次gc再清理了);
它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。

5.G1收集器
G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵.
G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。
這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率。

Serial(串行)收集器收集器

image

ParNew收集器

image

Parallel Scavenge收集器

image

CMS收集器

image

G1收集器

image

  • 當出現了內存溢出,你怎麼排錯
jmap -dump:format=b,file=eureka.hprof 13988
也可以設置內存溢出自動導出dump文件(內存很大的時候,可能會導不出來)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./   (路徑)
用jstack查找死鎖,見如下示例,也可以用jvisualvm查看死鎖
jstack找出佔用cpu最高的堆棧信息
1,使用命令top -p <pid> ,顯示你的java進程的內存情況,pid是你的java進程號,比如4977
2,按H,獲取每個線程的內存情況 
3,找到內存和cpu佔用最高的線程tid,比如4977 
4,轉爲十六進制得到 0x1371 ,此爲線程id的十六進制表示
5,執行 jstack 4977|grep -A 10 1371,得到線程堆棧信息中1371這個線程所在行的後面10行 
6,查看對應的堆棧信息找出可能存在問題的代碼
  • JVM內存模型的相關知識瞭解多少
本地方法棧(線程私有): 
登記native方法,在Execution Engine執行時加載本地方法庫
程序計數器(線程私有): 
就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼)
,由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。
方法區(線程共享):
類的所有字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在此定義。簡單說,
所有定義的方法的信息都保存在該區域,靜態變量+常量+類信息(構造方法/接口定義)+運行時常量池都存在方法區中,
雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
Java棧(線程私有):
Java線程執行方法的內存模型,一個線程對應一個棧,每個方法在執行的同時都會創建一個棧幀(用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息)
不存在垃圾回收問題,只要線程一結束該棧就釋放,生命週期和線程一致
堆(線程共享):
虛擬機啓動時創建,用於存放對象實例,幾乎所有的對象(包含常量池)都在堆上分配內存,當對象無法再該空間申請到內存時將拋出OutOfMemoryError異常。
同時也是垃圾收集器管理的主要區域。可通過 -Xmx –Xms 參數來分別指定最大堆和最小堆

image

image

image

image

  • 簡單說說你瞭解的類加載器
啓動類加載器:負責加載JRE的核心類庫,如jre目標下的rt.jar,charsets.jar等
擴展類加載器:負責加載JRE擴展目錄ext中JAR類包
系統類加載器:負責加載ClassPath路徑下的類包
用戶自定義加載器:負責加載用戶自定義路徑下的類包
全盤負責委託機制:當一個ClassLoader加載一個類時,除非顯示的使用另一個ClassLoader,該類所依賴和引用的類也由這個ClassLoader載入
雙親委派機制:指先委託父類加載器尋找目標類,在找不到的情況下在自己的路徑中查找並載入目標類
雙親委派模式優勢
沙箱安全機制:自己寫的String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改
避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再   加載一次

image

  • JAVA的反射機制
JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,
都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
java通常是先有類再有對象,有對象我就可以調用方法或者屬性。反射其實是通過Class對象來調用類裏面的方法。通過反射可以調用私有方法和私有屬性。

網絡:

http1.0和http1.1有什麼區別


1、HTTP 1.1支持長連接(PersistentConnection)和請求的流水線(Pipelining)處理

HTTP 1.0規定瀏覽器與服務器只保持短暫的連接,瀏覽器的每次請求都需要與服務器建立一個TCP連接,服務器完成請求處理後立即斷開TCP連接,服務器不跟蹤每個客戶也不記錄過去的請求。

HTTP 1.1則支持持久連接Persistent Connection, 並且默認使用persistent connection. 在同一個tcp的連接中可以傳送多個HTTP請求和響應. 多個請求和響應可以重疊,多個請求和響應可以同時進行. 更加多的請求頭和響應頭(比如HTTP1.0沒有host的字段).

在1.0時的會話方式:

  1. 建立連接
  2. 發出請求信息
  3. 回送響應信息
  4. 關掉連接

HTTP 1.1的持續連接,也需要增加新的請求頭來幫助實現,例如,Connection請求頭的值爲Keep-Alive時,客戶端通知服務器返回本次請求結果後保持連接;Connection請求頭的值爲close時,客戶端通知服務器返回本次請求結果後關閉連接。HTTP 1.1還提供了與身份認證、狀態管理和Cache緩存等機制相關的請求頭和響應頭。

請求的流水線(Pipelining)處理,在一個TCP連接上可以傳送多個HTTP請求和響應,減少了建立和關閉連接的消耗和延遲。例如:一個包含有許多圖像的網頁文件的多個請求和應答可以在一個連接中傳輸,但每個單獨的網頁文件的請求和應答仍然需要使用各自的連接。 HTTP 1.1還允許客戶端不用等待上一次請求結果返回,就可以發出下一次請求,但服務器端必須按照接收到客戶端請求的先後順序依次回送響應結果,以保證客戶端能夠區分出每次請求的響應內容。

2.HTTP 1.1增加host字段

在HTTP1.0中認爲每臺服務器都綁定一個唯一的IP地址,因此,請求消息中的URL並沒有傳遞主機名(hostname)。但隨着虛擬主機技術的發展,在一臺物理服務器上可以存在多個虛擬主機(Multi-homed Web Servers),並且它們共享一個IP地址。

HTTP1.1的請求消息和響應消息都應支持Host頭域,且請求消息中如果沒有Host頭域會報告一個錯誤(400 Bad Request)。此外,服務器應該接受以絕對路徑標記的資源請求。

3、100(Continue) Status(節約帶寬)

HTTP/1.1加入了一個新的狀態碼100(Continue)。客戶端事先發送一個只帶頭域的請求,如果服務器因爲權限拒絕了請求,就回送響應碼401(Unauthorized);如果服務器接收此請求就回送響應碼100,客戶端就可以繼續發送帶實體的完整請求了。100 (Continue) 狀態代碼的使用,允許客戶端在發request消息body之前先用request header試探一下server,看server要不要接收request body,再決定要不要發request body。

4、HTTP/1.1中引入了Chunked transfer-coding來解決上面這個問題,發送方將消息分割成若干個任意大小的數據塊,每個數據塊在發送時都會附上塊的長度,最後用一個零長度的塊作爲消息結束的標誌。這種方法允許發送方只緩衝消息的一個片段,避免緩衝整個消息帶來的過載。

5、HTTP/1.1在1.0的基礎上加入了一些cache的新特性,當緩存對象的Age超過Expire時變爲stale對象,cache不需要直接拋棄stale對象,而是與源服務器進行重新激活(revalidation)。


TCP三次握手和四次揮手的流程,爲什麼斷開連接要4次,如果握手只有兩次,會出現什麼

TCP連接建立過程:
1、客戶端向服務器發送SYN(同步序列編號 Synchronize Sequence Numbers),其中seq=x(seq 序號 ack 確認號)。
2、服務器收到SYN報文段後,發送SYN+ACK,其中seq=y,確認號=x+1。
3、客戶端收到SYN+ACK報文段後,發送ACK,確認號=y+1。服務器收到ACK報文段後,連接建立。
TCP連接斷開過程:
1、客戶端TCP模塊在收到應用程序的通知後,發送FIN,seq=x。
2、服務器收到FIN報文段,發送ACK,確認號=x+1,並且通知應用程序客戶端關閉了連接。客戶端收到ACK報文段。
3、服務器端的應用程序通知TCP關閉連接,服務器端TCP發送FIN+ACK,seq=y,確認號=x+1(這裏ACK只是一般性的捎帶ACK,TCP總是這樣,以增強健壯性,反正也不費力氣,從原理上說,對連接斷開不是必須的)。
4、客戶端收到FIN+ACK報文段後,發送ACK,確認號y+1。服務器收到ACK報文段後,連接斷開。
爲什麼不去掉第二步,直接進行第三步呢?
因爲,第二步中,服務器端通知應用程序並獲得反饋信息可能需要可觀的時間,這可能涉及人機交互操作,也可能服務器應用層暫時還不想關閉連接。第二步結束後,服務器還可以繼續通過這條連接發送數據給客戶端,客戶端已經不能發送數據了,但是仍然可以回覆ACK。第二步中服務器立即發送確認是爲了防止客戶端重傳FIN報文。

當然也有可能雙方同時來連接,這個時候通信雙方是對等的,比如BGP peer之間就是對等的,翻譯成中文爲BGP對等體,也算是貼切。如果雙發使用非監聽端口發起連接,這樣最終需要斷開其中一條多餘的連接。如果雙發都使用自己監聽的端口發起連接,那就成了四次握手,正好建立起一條雙向連接。

也有可能服務器在不想繼續爲客戶端提供服務了,主動斷開連接。不過這種情況對於TCP而言仍然是普通的四次揮手。

還有可能客戶端和服務器同時斷開連接,仍然是發送四個報文,你可以把它看做是四次揮手,但是它不再是普通的四次揮手。因爲這4個報文在時間上有重疊。就TCP狀態機的遷移而言,也和普通的四次揮手不同。將不再進入FIN-WAIT-2狀態。

*SYN:同步標誌
 同步序列編號(Synchronize Sequence Numbers)欄有效。該標誌僅在三次握手建立TCP連接時有效。它提示TCP連接的服務端檢查序列編號,該序列編號爲TCP連接初始端(一般是客戶端)的初始序列編號。在這裏,可以把TCP序列編號看作是一個範圍從0到4,294,967,295的32位計數器。通過TCP連接交換的數據中每一個字節都經過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個字節的序列編號。
*ACK:確認標誌
 確認編號(Acknowledgement Number)欄有效。大多數情況下該標誌位是置位的。TCP報頭內的確認編號欄內包含的確認編號(w+1,Figure-1)爲下一個預期的序列編號,同時提示遠端系統已經成功接收所有數據。
*RST:復位標誌
 復位標誌有效。用於復位相應的TCP連接。
*URG:緊急標誌
 緊急(The urgent pointer) 標誌有效。緊急標誌置位,
*PSH:推標誌
 該標誌置位時,接收端不將該數據進行隊列處理,而是儘可能快將數據轉由應用處理。在處理 telnet 或 rlogin 等交互模式的連接時,該標誌總是置位的。
*FIN:結束標誌
 帶有該標誌置位的數據包用來結束一個TCP回話,但對應端口仍處於開放狀態,準備接收後續數據

TIME_WAIT和CLOSE_WAIT的區別

TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。
TIME_WAIT:
是主動關閉鏈接時形成的,等待2MSL時間,約4分鐘。主要是防止最後一個ACK丟失。由於TIME_WAIT 的時間會非常長,因此server端應儘量減少主動關閉連接。
CLOSE_WAIT:
是被動關閉連接是形成的。根據TCP狀態機,服務器端收到客戶端發送的FIN,則按照TCP實現發送ACK,因此進入CLOSE_WAIT狀態。
但如果服務器端不執行close(),就不能由CLOSE_WAIT遷移到LAST_ACK,則系統中會存在很多CLOSE_WAIT狀態的連接。
此時,可能是系統忙於處理讀、寫操作,而未將已收到FIN的連接,進行close。此時,recv/read已收到FIN的連接socket,會返回0。

說說你知道的幾種HTTP響應碼

http狀態返回代碼 1xx(臨時響應)
表示臨時響應並需要請求者繼續執行操作的狀態代碼。

http狀態返回代碼 代碼   說明
100   (繼續)請求者應當繼續提出請求。服務器返回此代碼表示已收到請求的第一部分,正在等待其餘部分。 
101   (切換協議)請求者已要求服務器切換協議,服務器已確認並準備切換。

http狀態返回代碼 2xx (成功)
表示成功處理了請求的狀態代碼。

http狀態返回代碼 代碼   說明
200   (成功)  服務器已成功處理了請求。通常,這表示服務器提供了請求的網頁。
201   (已創建)  請求成功並且服務器創建了新的資源。
202   (已接受)  服務器已接受請求,但尚未處理。
203   (非授權信息)  服務器已成功處理了請求,但返回的信息可能來自另一來源。
204   (無內容)  服務器成功處理了請求,但沒有返回任何內容。
205   (重置內容)服務器成功處理了請求,但沒有返回任何內容。
206   (部分內容)  服務器成功處理了部分 GET 請求。

http狀態返回代碼 3xx (重定向)
表示要完成請求,需要進一步操作。通常,這些狀態代碼用來重定向。

http狀態返回代碼 代碼   說明
300   (多種選擇)  針對請求,服務器可執行多種操作。服務器可根據請求者 (user agent) 選擇一項操作,或提供操作列表供請求者選擇。
301   (永久移動)  請求的網頁已永久移動到新位置。服務器返回此響應(對 GET 或 HEAD 請求的響應)時,會自動將請求者轉到新位置。
302   (臨時移動)  服務器目前從不同位置的網頁響應請求,但請求者應繼續使用原有位置來進行以後的請求。
303   (查看其他位置)請求者應當對不同的位置使用單獨的 GET 請求來檢索響應時,服務器返回此代碼。

304   (未修改)自從上次請求後,請求的網頁未修改過。服務器返回此響應時,不會返回網頁內容。
305   (使用代理)請求者只能使用代理訪問請求的網頁。如果服務器返回此響應,還表示請求者應使用代理。
307   (臨時重定向)  服務器目前從不同位置的網頁響應請求,但請求者應繼續使用原有位置來進行以後的請求。

http狀態返回代碼 4xx(請求錯誤)
這些狀態代碼表示請求可能出錯,妨礙了服務器的處理。

http狀態返回代碼 代碼   說明
400   (錯誤請求)服務器不理解請求的語法。
401   (未授權)請求要求身份驗證。對於需要登錄的網頁,服務器可能返回此響應。
403   (禁止)服務器拒絕請求。
404   (未找到)服務器找不到請求的網頁。
405   (方法禁用)禁用請求中指定的方法。
406   (不接受)無法使用請求的內容特性響應請求的網頁。
407   (需要代理授權)此狀態代碼與 401(未授權)類似,但指定請求者應當授權使用代理。
408   (請求超時)  服務器等候請求時發生超時。
409   (衝突)  服務器在完成請求時發生衝突。服務器必須在響應中包含有關衝突的信息。
410   (已刪除)  如果請求的資源已永久刪除,服務器就會返回此響應。
411   (需要有效長度)服務器不接受不含有效內容長度標頭字段的請求。
412   (未滿足前提條件)服務器未滿足請求者在請求中設置的其中一個前提條件。
413   (請求實體過大)服務器無法處理請求,因爲請求實體過大,超出服務器的處理能力。
414   (請求的 URI 過長)請求的 URI(通常爲網址)過長,服務器無法處理。
415   (不支持的媒體類型)請求的格式不受請求頁面的支持。
416   (請求範圍不符合要求)如果頁面無法提供請求的範圍,則服務器會返回此狀態代碼。
417   (未滿足期望值)服務器未滿足"期望"請求標頭字段的要求。

http狀態返回代碼 5xx(服務器錯誤)
這些狀態代碼表示服務器在嘗試處理請求時發生內部錯誤。這些錯誤可能是服務器本身的錯誤,而不是請求出錯。

http狀態返回代碼 代碼   說明
500   (服務器內部錯誤)  服務器遇到錯誤,無法完成請求。
501   (尚未實施)服務器不具備完成請求的功能。例如,服務器無法識別請求方法時可能會返回此代碼。
502   (錯誤網關)服務器作爲網關或代理,從上游服務器收到無效響應。
503   (服務不可用)服務器目前無法使用(由於超載或停機維護)。通常,這只是暫時狀態。
504   (網關超時)  服務器作爲網關或代理,但是沒有及時從上游服務器收到請求。
505   (HTTP 版本不受支持)服務器不支持請求中所用的 HTTP 協議版本。 

一些常見的http狀態返回代碼爲:

200 - 服務器成功返回網頁
404 - 請求的網頁不存在
503 - 服務不可用

架構設計與分佈式:

tomcat如何調優,各種參數的意義

    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="500" minSpareThreads="20" maxSpareThreads="50" maxIdleTime="60000"/>
<Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               URIEncoding="UTF-8"
               connectionTimeout="30000"
               enableLookups="false"
               disableUploadTimeout="false"
               connectionUploadTimeout="150000"
               acceptCount="300"
               keepAliveTimeout="120000"
               maxKeepAliveRequests="1"
               compression="on"
               compressionMinSize="2048"
               compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png" 
               redirectPort="8443" />

maxThreads :Tomcat 使用線程來處理接收的每個請求,這個值表示 Tomcat 可創建的最大的線程數,默認值是 200

minSpareThreads:最小空閒線程數,Tomcat 啓動時的初始化的線程數,表示即使沒有人使用也開這麼多空線程等待,默認值是 10。

maxSpareThreads:最大備用線程數,一旦創建的線程超過這個值,Tomcat 就會關閉不再需要的 socket 線程。

上邊配置的參數,最大線程 500(一般服務器足以),要根據自己的實際情況合理設置,設置越大會耗費內存和 CPU,因爲 CPU 疲於線程上下文切換,沒有精力提供請求服務了,最小空閒線程數 20,線程最大空閒時間 60 秒,當然允許的最大線程連接數還受制於操作系統的內核參數設置,設置多大要根據自己的需求與環境。當然線程可以配置在“tomcatThreadPool”中,也可以直接配置在“Connector”中,但不可以重複配置。

URIEncoding:指定 Tomcat 容器的 URL 編碼格式,語言編碼格式這塊倒不如其它 WEB 服務器軟件配置方便,需要分別指定。

connnectionTimeout: 網絡連接超時,單位:毫秒,設置爲 0 表示永不超時,這樣設置有隱患的。通常可設置爲 30000 毫秒,可根據檢測實際情況,適當修改。

enableLookups: 是否反查域名,以返回遠程主機的主機名,取值爲:true 或 false,如果設置爲false,則直接返回IP地址,爲了提高處理能力,應設置爲 false。

disableUploadTimeout:上傳時是否使用超時機制。

connectionUploadTimeout:上傳超時時間,畢竟文件上傳可能需要消耗更多的時間,這個根據你自己的業務需要自己調,以使Servlet有較長的時間來完成它的執行,需要與上一個參數一起配合使用纔會生效。

acceptCount:指定當所有可以使用的處理請求的線程數都被使用時,可傳入連接請求的最大隊列長度,超過這個數的請求將不予處理,默認爲100個。

keepAliveTimeout:長連接最大保持時間(毫秒),表示在下次請求過來之前,Tomcat 保持該連接多久,默認是使用 connectionTimeout 時間,-1 爲不限制超時。

maxKeepAliveRequests:表示在服務器關閉之前,該連接最大支持的請求數。超過該請求數的連接也將被關閉,1表示禁用,-1表示不限制個數,默認100個,一般設置在100~200之間。

compression:是否對響應的數據進行 GZIP 壓縮,off:表示禁止壓縮;on:表示允許壓縮(文本將被壓縮)、force:表示所有情況下都進行壓縮,默認值爲off,壓縮數據後可以有效的減少頁面的大小,一般可以減小1/3左右,節省帶寬。

compressionMinSize:表示壓縮響應的最小值,只有當響應報文大小大於這個值的時候纔會對報文進行壓縮,如果開啓了壓縮功能,默認值就是2048。

compressableMimeType:壓縮類型,指定對哪些類型的文件進行數據壓縮。

noCompressionUserAgents=“gozilla, traviata”: 對於以下的瀏覽器,不啓用壓縮。

如果已經對代碼進行了動靜分離,靜態頁面和圖片等數據就不需要 Tomcat 處理了,那麼也就不需要配置在 Tomcat 中配置壓縮了。

-server:啓用jdk的server版本。
-Xms:虛擬機初始化時的最小堆內存。
-Xmx:虛擬機可使用的最大堆內存。 #-Xms與-Xmx設成一樣的值,避免JVM因爲頻繁的GC導致性能大起大落
-XX:PermSize:設置非堆內存初始值,默認是物理內存的1/64。
-XX:MaxNewSize:新生代佔整個堆內存的最大值。
-XX:MaxPermSize:Perm(俗稱方法區)佔整個堆內存的最大值,也稱內存最大永久保留區域。
  • 常見的緩存策略有哪些,你們項目中用到了什麼緩存系統,如何設計的,Redis的使用要注意什麼,持久化方式,內存設置,集羣,淘汰策略等
一、什麼是緩存
1、Cache是高速緩衝存儲器 一種特殊的存儲器子系統,其中複製了頻繁使用的數據以利於快速訪問
2、凡是位於速度相差較大的兩種硬件/軟件之間的,用於協調兩者數據傳輸速度差異的結構,均可稱之爲 Cache

二、緩存的分類
1、基於web應用的系統架構圖



2、在系統架構的不同層級之間,爲了加快訪問速度,都可以存在緩存

操作系統磁盤緩存->減少磁盤機械操作
數據庫緩存->減少文件系統I/O
應用程序緩存->減少對數據庫的查詢 
Web服務器緩存->減少應用服務器請求
客戶端瀏覽器緩存->減少對網站的訪問
三、操作系統緩存
1、文件系統提供的Disk Cache:操作系統會把經常訪問到的文件內容放入到內存當中,由文件系統來管理
2、當應用程序通過文件系統訪問磁盤文件的時候,操作系統從Disk Cache當中讀取文件內容,加速了文件讀取速度
3、Disk Cache由操作系統來自動管理,一般不用人工干預,但應當保證物理內存充足,以便於操作系統可以使用盡量多的內存充當Disk Cache,加速文件讀取速度
4、特殊的應用程序對文件系統Disk Cache有很高的要求,會繞開文件系統Disk Cache,直接訪問磁盤分區,自己實現Disk 
5、Cache策略

Oracle的raw device(裸設備) – 直接拋棄文件系統
MySQL的InnoDB: innodb_flush_method = O_DIRECT

四、數據庫緩存
1、重要性

數據庫通常是企業應用系統最核心的部分
數據庫保存的數據量通常非常龐大
數據庫查詢操作通常很頻繁,有時還很複雜
以上原因造成數據庫查詢會引起非常頻繁的磁盤I/O讀取操作,迫使CPU掛起等待,數據庫性能極度低下
2、緩存策略
     a、Query Cache

以SQL作爲key值緩存查詢結果集
一旦查詢涉及的表記錄被修改,緩存就會被自動刪除
設置合適的Query Cache會極大提高數據庫性能
Query Cache並非越大越好,過大的Qquery Cache會浪費內存。
MySQL: query_cache_size= 128M
     b、Data Buffer

data buffer是數據庫數據在內存中的容器
data buffer的命中率直接決定了數據庫的性能
data buffer越大越好,多多益善
MySQL的InnoDB buffer:innodb_buffer_pool_size = 2G
MySQL建議buffer pool開大到服務器物理內存60-80%
五、應用程序緩存
1、對象緩存

由O/R Mapping框架例如Hibernate提供,透明性訪問,細顆粒度緩存數據庫查詢結果,無需業務代碼顯式編程,是最省事的緩存策略
當軟件結構按照O/R Mapping框架的要求進行針對性設計,使用對象緩存將會極大降低Web系統對於數據庫的訪問請求
良好的設計數據庫結構和利用對象緩存,能夠提供極高的性能,對象緩存適合OLTP(聯機事務處理)應用
2、查詢緩存

對數據庫查詢結果集進行緩存,類似數據庫的Query Cache
適用於一些耗時,但是時效性要求比較低的場景。查詢緩存和對象緩存適用的場景不一樣,是互爲補充的
當查詢結果集涉及的表記錄被修改以後,需要注意清理緩存
3、頁面緩存
     a、作用

針對頁面的緩存技術不但可以減輕數據庫服務器壓力,還可以減輕應用服務器壓力
好的頁面緩存可以極大提高頁面渲染速度
頁面緩存的難點在於如何清理過期的緩存
    b、分類
         I、動態頁面靜態化

利用模板技術將訪問過一次的動態頁面生成靜態html,同時修改頁面鏈接,下一次請求直接訪問靜態鏈接頁面
動態頁面靜態化技術的廣泛應用於互聯網CMS/新聞類Web應用,但也有BBS應用使用該技術,例如Discuz!
無法進行權限驗證,無法顯示個性化信息
可以使用AJAX請求彌補動態頁面靜態化的某些缺點
        II、Servlet緩存

針對URL訪問返回的頁面結果進行緩存,適用於粗粒度的頁面緩存,例如新聞發佈
可以進行權限的檢查
OScache提供了簡單的Servlet緩存(通過web.xml中的配置)
也可以自己編程實現Servlet緩存
        III、頁面內部緩存

針對動態頁面的局部片斷內容進行緩存,適用於一些個性化但不經常更新的頁面(例如博客)
OSCache提供了簡單的頁面緩存
可以自行擴展JSP Tag實現頁面局部緩存

六、web服務器端緩存

基於代理服務器模式的Web服務器端緩存,如squid/nginx
Web服務器緩存技術被用來實現CDN(內容分發網絡 content delivery network)
被國內主流門戶網站大量採用
不需要編程,但僅限於新聞發佈類網站,頁面實時性要求不高
七、基於ajax的瀏覽器緩存

使用AJAX調用的時候,將數據庫在瀏覽器端緩存
只要不離開當前頁面,不刷新當前頁面,就可以直接讀取緩存數據
只適用於使用AJAX技術的頁面
1.使用key值前綴來作命名空間
雖然說Redis支持多個數據庫(默認32個,可以配置更多),但是除了默認的0號庫以外,其它的都需要通過一個額外請求才能使用。所以用前綴作爲命名空間可能會更明智一點。

另外,在使用前綴作爲命名空間區隔不同key的時候,最好在程序中使用全局配置來實現,直接在代碼裏寫前綴的做法要嚴格避免,這樣可維護性實在太差了。

2.創建一個類似 ”registry” 的key用於標記key使用情況
爲了更好的管理你的key值的使用,比如哪一類key值是屬於哪個業務的,你通常會在內部wiki或者什麼地方創建一個文檔,通過查詢這個文檔,
我們能夠知道Redis中的key都是什麼作用。
與之結合,一個推薦的做法是,在Redis裏面保存一個registry值,這個值的名字可以類似於 __key_registry__ 這樣的,這個key對應的value就是你文檔的位置,
這樣我們在使用Redis的時候,就能通過直接查詢這個值獲取到當前Redis的使用情況了。

3.注意垃圾回收
Redis是一個提供持久化功能的內存數據庫,如果你不指定上面值的過期時間,並且也不進行定期的清理工作,那麼你的Redis內存佔用會越來越大,
當有一天它超過了系統可用內存,那麼swap上場,離性能陡降的時間就不遠了。所以在Redis中保存數據時,一定要預先考慮好數據的生命週期,
這有很多方法可以實現。

比如你可以採用Redis自帶的過期時間爲你的數據設定過期時間。但是自動過期有一個問題,很有可能導致你還有大量內存可用時,
就讓key過期去釋放內存,或者是內存已經不足了key還沒有過期。

如果你想更精準的控制你的數據過期,你可以用一個ZSET來維護你的數據更新程度,你可以用時間戳作爲score值,每次更新操作時更新一下score,
這樣你就得到了一個按更新時間排序序列串,你可以輕鬆地找到最老的數據,並且從最老的數據開始進行刪除,一直刪除到你的空間足夠爲止。

4.設計好你的Sharding機制
Redis目前並不支持Sharding,但是當你的數據量超過單機內存時,你不得不考慮Sharding的事(注意:Slave不是用來做Sharding操作的,
只是數據的一個備份和讀寫分離而已)。

所以你可能需要考慮好數據量大了後的分片問題,比如你可以在只有一臺機器的時候就在程序上設定一致性hash機制,
雖然剛開始所有數據都hash到一臺機器,但是當你機器越加越多的時候,你就只需要遷移少量的數據就能完成了。

5.不要有個錘子看哪都是釘子
當你使用Redis構建你的服務的時候,一定要記住,你只是找了一個合適的工具來實現你需要的功能。而不是說你在用Redis構建一個服務,
這是很不同的,你把Redis當作你很多工具中的一個,只在合適使用的時候再使用它,在不合適的時候選擇其它的方法。

redis持久化機制

RDB快照(snapshot)
在默認情況下, Redis 將內存數據庫快照保存在名字爲 dump.rdb 的二進制文件中。
你可以對 Redis 進行設置, 讓它在“ N 秒內數據集至少有 M 個改動”這一條件被滿足時, 自動保存一次數據集。
比如說, 以下設置會讓 Redis 在滿足“ 60 秒內有至少有 1000 個鍵被改動”這一條件時, 自動保存一次數據集:
# save 60 1000

AOF(append-only file)
快照功能並不是非常耐久(durable): 如果 Redis 因爲某些原因而造成故障停機, 那麼服務器將丟失最近寫入、且仍未保存到快照中的那些數據。從 1.1 版本開始, Redis 增加了一種完全耐久的持久化方式: AOF 持久化,將修改的每一條指令記錄進文件
你可以通過修改配置文件來打開 AOF 功能:
# appendonly yes

從現在開始, 每當 Redis 執行一個改變數據集的命令時(比如 SET), 這個命令就會被追加到 AOF 文件的末尾。
這樣的話, 當 Redis 重新啓時, 程序就可以通過重新執行 AOF 文件中的命令來達到重建數據集的目的。
你可以配置 Redis 多久纔將數據 fsync 到磁盤一次。
有三個選項:
每次有新命令追加到 AOF 文件時就執行一次 fsync :非常慢,也非常安全。
每秒 fsync 一次:足夠快(和使用 RDB 持久化差不多),並且在故障時只會丟失 1 秒鐘的數據。
從不 fsync :將數據交給操作系統來處理。更快,也更不安全的選擇。
推薦(並且也是默認)的措施爲每秒 fsync 一次, 這種 fsync 策略可以兼顧速度和安全性。

RDB 和 AOF ,我應該用哪一個?
如果你非常關心你的數據, 但仍然可以承受數分鐘以內的數據丟失, 那麼你可以只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化, 但我們並不推薦這種方式: 因爲定時生成 RDB 快照(snapshot)非常便於進行數據庫備份, 並且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快。

Redis 4.0 混合持久化
重啓 Redis 時,我們很少使用 rdb 來恢復內存狀態,因爲會丟失大量數據。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啓動需要花費很長的時間。 Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。AOF在重寫(aof文件裏可能有太多沒用指令,所以aof會定期根據內存的最新數據生成aof文件)時將重寫這一刻之前的內存rdb快照文件的內容和增量的 AOF修改內存數據的命令日誌文件存在一起,都寫入新的aof文件,新的文件一開始不叫appendonly.aof,等到重寫完新的AOF文件纔會進行改名,原子的覆蓋原有的AOF文件,完成新舊兩個AOF文件的替換;
AOF根據配置規則在後臺自動重寫,也可以人爲執行命令bgrewriteaof重寫AOF。 於是在 Redis 重啓的時候,可以先加載 rdb 的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量文件重放,重啓效率因此大幅得到提升。

開啓混合持久化:
# aof-use-rdb-preamble yes    
混合持久化aof文件結構

redis內存設置

Redis設置最大佔用內存,打開redis配置文件,找到如下段落,設置maxmemory參數,maxmemory是bytes字節類型,注意轉換。
一般推薦Redis設置內存爲最大物理內存的四分之三 修改如下所示:

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory 268435456

redis集羣

在redis3.0以前的版本要實現集羣一般是藉助哨兵sentinel工具來監控master節點的狀態,如果master節點異常,則會做主從切換,將某一臺slave作爲master,哨兵的配置略微複雜,並且性能和高可用性等各方面表現一般,特別是在主從切換的瞬間存在訪問瞬斷的情況,而且哨兵模式只有一個主節點對外提供服務,沒法支持很高的併發,且單個主節點內存也不宜設置得過大,否則會導致持久化文件過大,影響數據恢復或主從同步的效率

下載地址:http://redis.io/download
安裝步驟:
# 安裝gcc
yum install gcc

# 把下載好的redis-5.0.2.tar.gz放在/usr/local文件夾下,並解壓
wget http://download.redis.io/releases/redis-5.0.2.tar.gz
tar xzf redis-5.0.2.tar.gz
cd redis-5.0.2

# 進入到解壓好的redis-5.0.2目錄下,進行編譯與安裝
make & make install

# 啓動並指定配置文件
src/redis-server redis.conf(注意要使用後臺啓動,所以修改redis.conf裏的daemonize改爲yes)

# 驗證啓動是否成功 
ps -ef | grep redis 

# 進入redis客戶端 
/usr/local/redis/bin/redis-cli 

# 退出客戶端
quit

# 退出redis服務: 
(1)pkill redis-server 
(2)kill 進程號                       
(3)src/redis-cli shutdown 

redis集羣搭建  systemctl stop firewalld.service 關閉防火牆
redis集羣需要至少要三個master節點,我們這裏搭建三個master節點,並且給每個master再搭建一個slave節點,
總共6個redis節點,這裏用三臺機器部署6個redis實例,每臺機器一主一從,搭建集羣的步驟如下:
第一步:在第一臺機器的/usr/local下創建文件夾redis-cluster,然後在其下面分別創建2個文件夾如下
(1)mkdir -p /usr/local/redis-cluster
(2)mkdir 8001、 mkdir 8004

第一步:把之前的redis.conf配置文件copy到8001下,修改如下內容:
(1)daemonize yes
(2)port 8001(分別對每個機器的端口號進行設置)
(3)dir /usr/local/redis-cluster/8001/(指定數據文件存放位置,必須要指定不同的目錄位置,不然會丟失數據)
(4)cluster-enabled yes(啓動集羣模式)
(5)cluster-config-file nodes-8001.conf(集羣節點信息文件,這裏800x最好和port對應上)
(6)cluster-node-timeout 5000
   (7)  # bind 127.0.0.1(去掉bind綁定訪問ip信息)
   (8)  protected-mode  no   (關閉保護模式)
 (9)appendonly yes
如果要設置密碼需要增加如下配置:
 (10)requirepass zhuge     (設置redis訪問密碼)
 (11)masterauth zhuge      (設置集羣節點間訪問密碼,跟上面一致)

第三步:把修改後的配置文件,copy到8002,修改第2、3、5項裏的端口號,可以用批量替換:
:%s/源字符串/目的字符串/g 

第四步:另外兩臺機器也需要做上面幾步操作,第二臺機器用8002和8005,第三臺機器用8003和8006

第五步:分別啓動6個redis實例,然後檢查是否啓動成功
(1)/usr/local/redis-5.0.2/src/redis-server /usr/local/redis-cluster/800*/redis.conf
(2)ps -ef | grep redis 查看是否啓動成功

第六步:用redis-cli創建整個redis集羣(redis5以前的版本集羣是依靠ruby腳本redis-trib.rb實現)
(1)/usr/local/redis-5.0.2/src/redis-cli -a zhuge --cluster create --cluster-replicas 1
192.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 
192.168.0.61:8004 192.168.0.62:8005 192.168.0.63:8006 
代表爲每個創建的主服務器節點創建一個從服務器節點

第七步:驗證集羣:
(1)連接任意一個客戶端即可:./redis-cli -c -h -p (-a訪問服務端密碼,-c表示集羣模式,指定ip地址和端口號
)如:/usr/local/redis-5.0.2/src/redis-cli -a zhuge -c -h 192.168.0.61 -p 800*
(2)進行驗證: cluster info(查看集羣信息)、cluster nodes(查看節點列表)
(3)進行數據操作驗證
(4)關閉集羣則需要逐個進行關閉,使用命令:
/usr/local/redis/bin/redis-cli -a zhuge -c -h 192.168.0.60 -p 800* shutdown

redis淘汰策略

當 Redis 內存超出物理內存限制時,內存的數據會開始和磁盤產生頻繁的交換 (swap)。交換會讓 Redis 的性能急劇下降,
對於訪問量比較頻繁的 Redis 來說,這樣龜速的存取效率基本上等於不可用。
在生產環境中我們是不允許 Redis 出現交換行爲的,爲了限制最大使用內存,Redis 提供了配置參數 maxmemory 來限制內存超出期望大小。 
當實際內存超出 maxmemory 時,Redis 提供了幾種可選策略 (maxmemory-policy) 來讓用戶自己決定該如何騰出新的空間以繼續提供讀寫服務。

noeviction 不會繼續服務寫請求 (DEL 請求可以繼續服務),讀請求可以繼續進行。這樣可以保證不會丟失數據,但是會讓線上的業務不能持續進行。這是默認的淘汰策略。
volatile-lru 嘗試淘汰設置了過期時間的 key,最少使用的 key 優先被淘汰。沒有設置過期時間的 key 不會被淘汰,這樣可以保證需要持久化的數據不會突然丟失。
volatile-ttl 跟上面一樣,除了淘汰的策略不是 LRU,而是 key 的剩餘壽命 ttl 的值,ttl 越小越優先被淘汰。
volatile-random 跟上面一樣,不過淘汰的 key 是過期 key 集合中隨機的 key。
allkeys-lru 區別於 volatile-lru,這個策略要淘汰的 key 對象是全體的 key 集合,而不只是過期的 key 集合。這意味着沒有設置過期時間的 key 也會被淘汰。
allkeys-random 跟上面一樣,不過淘汰的策略是隨機的 key。

volatile-xxx 策略只會針對帶過期時間的 key 進行淘汰,allkeys-xxx 策略會對所有的 key 進行淘汰。如果你只是拿 Redis 做緩存,
那應該使用 allkeys-xxx,客戶端寫緩存時不必攜帶過期時間。如果你還想同時使用 Redis 的持久化功能,
那就使用 volatile-xxx 策略,這樣可以保留沒有設置過期時間的 key,它們是永久的 key 不會被 LRU 算法淘汰

如何防止緩存雪崩

在正常情況下,一旦miss就去查DB是沒有問題的。但是如果大量緩存集中在某一時間段失效,將導致所有請求都去訪問後端的DB,DB壓力會很大,甚至被壓垮,造成雪崩。
場景一
電商系統的某個大促活動的首頁,首頁有很多新上架的商品。活動開始前,技術團隊對緩存做了預熱,由於是腳本化預熱,
這些商品的Cache數據幾乎都是同時創建好,並且過期時間都設置爲5分鐘。這就會導致這大量的商品數據在5分鐘後集中失效。

場景二
cache系統剛上線(或者剛從崩潰中恢復過來),沒有對cache進行預熱。cache中什麼也沒有,這時瞬時大流量過來也會產生雪崩。

解決思路:
cache過期時間均勻分佈
針對上面的場景一,可以對cache的過期時間做一個均勻分佈的處理。比如1-5分鐘內,隨機分佈。
排斥鎖
,可以考慮使用排斥鎖(mutex)。即第一個線程過來讀取cache,發現沒有,就去訪問DB。
後續線程再過來就需要等待第一個線程讀取DB成功,cache裏的value變得可用,後續線程返回新的value。僞代碼如下:
使用了分佈式鎖,這當然是考慮到在分佈式環境下,讀請求會落到集羣中的不同應用服務機器上。分佈式鎖可以選用zookeeper或基於redis的setnx這類原子性操作來實現
加鎖時需要用到經典的double-check lock。

排斥鎖方案對緩存過期是零容忍的:cache一旦過期,後續所有讀操作就必須返回新的value。如果我們稍微放寬點限制:
在cache過期時間T到達後,允許短時間內部分讀請求返回舊值,我們就能提出兼顧吞吐率的方案。實際上既然用了cache,
系統就默許了容忍cache和DB的數據短時間的不一致。
限制放寬後,下面我們提出一個優化思路。時間T到達後,cache中的key和value不會被清掉,而只是被標記爲過期(邏輯上過期,物理上不過期)
,然後程序異步去刷新cache。而後續部分讀線程在前面的線程刷新cache成功之前,暫時獲取cache中舊的value返回。一旦cache刷新成功,
後續所有線程就能直接獲取cache中新的value。可以看到,這個思路很大程度上減少了排斥鎖的使用(雖然並沒有完全消除排斥鎖)。 
signKey:
既然存放數據的cache不會被清掉,那麼就通過別的key也就是代碼中的signKey來標記過期。signKey的過期時間一到,就代表實際key邏輯過期。
異步刷新cache時也用到了排斥鎖:
這是因爲同一時間多個讀線程進來都發現signKey已過期,就都要去異步刷新cache,所以這裏有必要加上排斥鎖。
但注意到isExpired方法中(35-41行),signKey一旦過期,馬上把過期時間延後1分鐘,這是爲了讓後續進來的線程先返回舊的value。
這樣只有極少一部分讀線程去刷新cache。因此需要加排斥鎖的線程也並不多。

分佈式集羣下如何做到唯一序列號

1、通過應用程序生成一個GUID,然後和數據一起插入切分後的集羣。
優點是維護簡單,實現也容易。缺點是應用的計算成本較大,且GUID的長度比較長,佔用數據庫存儲空間較大,涉及到應用的開發。
說明:主要優勢是簡單,缺點是浪費存儲空間。
2、通過獨立的應用程序事先在數據庫中生成一系列唯一的 ID,各應用程序通過接口或者自己去讀取再和數據一起插入到切分後的集羣中。
優點是全局唯一主鍵簡單,維護相對容易。
缺點是實現複雜,需要應用開發。
說明:ID表要頻繁查和頻繁更新,插入數據時,影響性能。
3、通過【中心數據庫服務器】利用數據庫自身的自增類型(如 MySQL的 auto_increment 字段),或者自增對象(如 Oracle 的 Sequence)等先生成一個唯一 ID 再和數據一起插入切分後的集羣。優點是?好像沒有特別明顯的優點。缺點是實現較爲複雜,且整體可用性維繫在這個中心數據庫服務器上,一旦這裏crash 了,所有的集羣都無法進行插入操作,涉及到應用開發。
說明:不推薦。
4、通過集羣編號加集羣內的自增(auto_increment類型)【兩個字段】共同組成唯一主鍵。優點是實現簡單,維護也比較簡單,對應用透明。
缺點是引用關聯操作相對比較複雜,需要兩個字段,主鍵佔用空間較大,在使用 InnoDB 的時候這一點的副作用很明顯。
說明:雖然是兩個字段,但是這方式存儲空間最小,僅僅多了一個smallint兩個字節。
5、通過設置每個集羣中自增 ID 起始點(auto_increment_offset),將各個集羣的ID進行絕對的分段來實現全局唯一。當遇到某個集羣數據增長過快後,通過命令調整下一個 ID 起始位置跳過可能存在的衝突。優點是實現簡單,且比較容易根據 ID 大小直接判斷出數據處在哪個集羣,對應用透明。缺點是維護相對較複雜,需要高度關注各個集羣 ID 增長狀況。
說明:段滿了,調整太麻煩。
6、通過設置每個集羣中自增 ID 起始點(auto_increment_offset)以及 ID 自增步長(auto_increment_increment),讓目前每個集羣的起始點錯開 1,步長選擇大於將來基本不可能達到的切分集羣數,達到將 ID 相對分段的效果來滿足全局唯一的效果。優點是實現簡單,後期維護簡單,對應用透明。缺點是第一次設置相對較爲複雜。
說明:避免重合需要多種方案結合

設計一個秒殺系統,30分鐘沒付款就自動關閉交易

Redis是一個分佈式緩存系統,支持多種數據結構,我們可以利用Redis輕鬆實現一個強大的秒殺系統。

我們可以採用Redis 最簡單的key-value數據結構,用一個原子類型的變量值(AtomicInteger)作爲key,把用戶id作爲value,
庫存數量便是原子變量的最大值。對於每個用戶的秒殺,我們使用 RPUSH key value插入秒殺請求, 當插入的秒殺請求數達到上限時,停止所有後續插入。
然後我們可以在臺啓動多個工作線程,使用 LPOP key 讀取秒殺成功者的用戶id,然後再操作數據庫做最終的下訂單減庫存操作。
當然,上面Redis也可以替換成消息中間件如ActiveMQ、RabbitMQ等,
也可以將緩存和消息中間件 組合起來,緩存系統負責接收記錄用戶請求,消息中間件負責將緩存中的請求同步到數據庫。

如何做一個分佈式鎖

使用redis的SETNX
如返回1,則該客戶端獲得鎖,把 lock.foo 的鍵值設置爲時間值表示該鍵已被鎖定,該客戶端最後可以通過 DEL lock.foo 來釋放該鎖。
如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。

用過哪些MQ,怎麼用的,和其他mq比較有什麼優缺點,MQ的連接是線程安全的嗎

RabbitMQ

RabbitMQ 2007年發佈,是一個在AMQP(高級消息隊列協議)基礎上完成的,可複用的企業消息系統,是當前最主流的消息中間件之一。

主要特性:

可靠性: 提供了多種技術可以讓你在性能和可靠性之間進行權衡。這些技術包括持久性機制、投遞確認、發佈者證實和高可用性機制;
靈活的路由: 消息在到達隊列前是通過交換機進行路由的。RabbitMQ爲典型的路由邏輯提供了多種內置交換機類型。如果你有更復雜的路由需求,
可以將這些交換機組合起來使用,你甚至可以實現自己的交換機類型,並且當做RabbitMQ的插件來使用;
消息集羣:在相同局域網中的多個RabbitMQ服務器可以聚合在一起,作爲一個獨立的邏輯代理來使用;
隊列高可用:隊列可以在集羣中的機器上進行鏡像,以確保在硬件問題下還保證消息安全;
多種協議的支持:支持多種消息隊列協議;
服務器端用Erlang語言編寫,支持只要是你能想到的所有編程語言;
管理界面: RabbitMQ有一個易用的用戶界面,使得用戶可以監控和管理消息Broker的許多方面;
跟蹤機制:如果消息異常,RabbitMQ提供消息跟蹤機制,使用者可以找出發生了什麼;
插件機制:提供了許多插件,來從多方面進行擴展,也可以編寫自己的插件;
使用RabbitMQ需要:
ErLang語言包
RabbitMQ安裝包
RabbitMQ可以運行在Erlang語言所支持的平臺之上:
Solaris
BSD
Linux
MacOSX
TRU64
Windows NT/2000/XP/Vista/Windows 7/Windows 8
Windows Server 2003/2008/2012
Windows 95, 98
VxWorks
優點:
由於erlang語言的特性,mq 性能較好,高併發;
健壯、穩定、易用、跨平臺、支持多種語言、文檔齊全;
有消息確認機制和持久化機制,可靠性高;
高度可定製的路由;
管理界面較豐富,在互聯網公司也有較大規模的應用;
社區活躍度高;
缺點:
儘管結合erlang語言本身的併發優勢,性能較好,但是不利於做二次開發和維護;
實現了代理架構,意味着消息在發送到客戶端之前可以在中央節點上排隊。此特性使得RabbitMQ易於使用和部署,但是使得其運行速度較慢,
因爲中央節點增加了延遲,消息封裝後也比較大;
需要學習比較複雜的接口和協議,學習和維護成本較高;

RocketMQ

RocketMQ出自 阿里公司的開源產品,用 Java 語言實現,在設計時參考了 Kafka,並做出了自己的一些改進,消息可靠性上比 Kafka 更好。RocketMQ在阿里集團被廣泛應用在訂單,交易,充值,流計算,消息推送,日誌流式處理,binglog分發等場景。

主要特性:

是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分佈式特點;
Producer、Consumer、隊列都可以分佈式;
Producer向一些隊列輪流發送消息,隊列集合稱爲Topic,Consumer如果做廣播消費,則一個consumer實例消費這個Topic對應的所有隊列,如果做集羣消費,則多個Consumer實例平均消費這個topic對應的隊列集合;
能夠保證嚴格的消息順序;
提供豐富的消息拉取模式;
高效的訂閱者水平擴展能力;
實時的消息訂閱機制;
億級消息堆積能力;
較少的依賴;
使用RocketMQ需要:

Java JDK
安裝git、Maven
RocketMQ安裝包
RocketMQ可以運行在Java語言所支持的平臺之上。

優點:

單機支持 1 萬以上持久化隊列
RocketMQ 的所有消息都是持久化的,先寫入系統 PAGECACHE,然後刷盤,可以保證內存與磁盤都有一份數據,
訪問時,直接從內存讀取。
模型簡單,接口易用(JMS 的接口很多場合並不太實用);
性能非常好,可以大量堆積消息在broker中;
支持多種消費,包括集羣消費、廣播消費等。
各個環節分佈式擴展設計,主從HA;
開發度較活躍,版本更新很快。
缺點:

支持的客戶端語言不多,目前是java及c++,其中c++不成熟;
RocketMQ社區關注度及成熟度也不及前兩者;
沒有web管理界面,提供了一個CLI(命令行界面)管理工具帶來查詢、管理和診斷各種問題;
沒有在 mq 核心中去實現JMS等接口;

Kafka

Apache Kafka是一個分佈式消息發佈訂閱系統。它最初由LinkedIn公司基於獨特的設計實現爲一個分佈式的提交日誌系統( a distributed commit log),,之後成爲Apache項目的一部分。Kafka系統快速、可擴展並且可持久化。它的分區特性,可複製和可容錯都是其不錯的特性。

主要特性:

快速持久化,可以在O(1)的系統開銷下進行消息持久化;
高吞吐,在一臺普通的服務器上既可以達到10W/s的吞吐速率;
.完全的分佈式系統,Broker、Producer、Consumer都原生自動支持分佈式,自動實現負載均衡;
支持同步和異步複製兩種HA;
支持數據批量發送和拉取;
zero-copy:減少IO操作步驟;
數據遷移、擴容對用戶透明;
無需停機即可擴展機器;
其他特性:嚴格的消息順序、豐富的消息拉取模型、高效訂閱者水平擴展、實時的消息訂閱、億級的消息堆積能力、定期刪除機制;
使用Kafka需要:

Java JDK
Kafka安裝包
優點:

客戶端語言豐富,支持java、.net、php、ruby、python、go等多種語言;
性能卓越,單機寫入TPS約在百萬條/秒,消息大小10個字節;
提供完全分佈式架構, 並有replica機制, 擁有較高的可用性和可靠性, 理論上支持消息無限堆積;
支持批量操作;
消費者採用Pull方式獲取消息, 消息有序, 通過控制能夠保證所有消息被消費且僅被消費一次;
有優秀的第三方Kafka Web管理界面Kafka-Manager;
在日誌領域比較成熟,被多家公司和多個開源項目使用;
缺點:

Kafka單機超過64個隊列/分區,Load會發生明顯的飆高現象,隊列越多,load越高,發送消息響應時間變長
使用短輪詢方式,實時性取決於輪詢間隔時間;
消費失敗不支持重試;
支持消息順序,但是一臺代理宕機後,就會產生消息亂序;
社區更新較慢;

image

  • MQ系統的數據如何保證不丟失
主要ack機制。包括副本集的建立
  • 分佈式事務的原理,如何使用分佈式事務
    image
    image
  • 什麼是一致性hash

image

此時對象Object A、B、D不受影響,只有對象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一臺服務器,則受影響的數據僅僅是新服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它數據也不會受到影響。

綜上所述,一致性Hash算法對於節點的增減都只需重定位環空間中的一小部分數據,具有較好的容錯性和可擴展性。
--------------------- 
作者:Java後端技術 
來源:CSDN 
原文:https://blog.csdn.net/bntX2jSQfEHy7/article/details/79549368 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

說說你知道的幾種HASH算法,簡單的也可以

  1. 加法Hash;
所謂的加法Hash就是把輸入元素一個一個的加起來構成最後的結果。標準的加法Hash的構造如下:

 static int additiveHash(String key, int prime)
 {
  int hash, i;
  for (hash = key.length(), i = 0; i < key.length(); i++)
   hash += key.charAt(i);
  return (hash % prime);
 }
 這裏的prime是任意的質數,看得出,結果的值域爲[0,prime-1]。
  1. 位運算Hash;
這類型Hash函數通過利用各種位運算(常見的是移位和異或)來充分的混合輸入元素。比如,標準的旋轉Hash的構造如下:

 static int rotatingHash(String key, int prime)
 {
   int hash, i;
   for (hash=key.length(), i=0; i<key.length(); ++i)
     hash = (hash<<4)^(hash>>28)^key.charAt(i);
   return (hash % prime);
 }

先移位,然後再進行各種位運算是這種類型Hash函數的主要特點。比如,以上的那段計算hash的代碼還可以有如下幾種變形:
1.     hash = (hash<<5)^(hash>>27)^key.charAt(i);
2.     hash += key.charAt(i);
        hash += (hash << 10);
        hash ^= (hash >> 6);
3.     if((i&1) == 0)
        {
         hash ^= (hash<<7) ^ key.charAt(i) ^ (hash>>3);
        }
        else
        {
         hash ^= ~((hash<<11) ^ key.charAt(i) ^ (hash >>5));
        }
4.     hash += (hash<<5) + key.charAt(i);
5.     hash = key.charAt(i) + (hash<<6) + (hash>>16) – hash;
6.     hash ^= ((hash<<5) + key.charAt(i) + (hash>>2));
  1. 乘法Hash;
這種類型的Hash函數利用了乘法的不相關性(乘法的這種性質,最有名的莫過於平方取頭尾的隨機數生成算法,雖然這種算法效果並不好)。比如,

 static int bernstein(String key)
 {
   int hash = 0;
   int i;
   for (i=0; i<key.length(); ++i) hash = 33*hash + key.charAt(i);
   return hash;
 }

jdk5.0裏面的String類的hashCode()方法也使用乘法Hash。不過,它使用的乘數是31。推薦的乘數還有:131, 1313, 13131, 131313等等。

使用這種方式的著名Hash函數還有:
 //  32位FNV算法
 int M_SHIFT = 0;
    public int FNVHash(byte[] data)
    {
        int hash = (int)2166136261L;
        for(byte b : data)
            hash = (hash * 16777619) ^ b;
        if (M_SHIFT == 0)
            return hash;
        return (hash ^ (hash >> M_SHIFT)) & M_MASK;
}

以及改進的FNV算法:
    public static int FNVHash1(String data)
    {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for(int i=0;i<data.length();i++)
            hash = (hash ^ data.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        return hash;
}

除了乘以一個固定的數,常見的還有乘以一個不斷改變的數,比如:
    static int RSHash(String str)
    {
        int b    = 378551;
        int a    = 63689;
        int hash = 0;

       for(int i = 0; i < str.length(); i++)
       {
          hash = hash * a + str.charAt(i);
          a    = a * b;
       }
       return (hash & 0x7FFFFFFF);
}

雖然Adler32算法的應用沒有CRC32廣泛,不過,它可能是乘法Hash裏面最有名的一個了。關於它的介紹,大家可以去看RFC 1950規範。
  1. 除法Hash;
除法和乘法一樣,同樣具有表面上看起來的不相關性。不過,因爲除法太慢,這種方式幾乎找不到真正的應用。需要注意的是,
我們在前面看到的hash的 結果除以一個prime的目的只是爲了保證結果的範圍。如果你不需要它限制一個範圍的話,
可以使用如下的代碼替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。
  1. 查表Hash;
查表Hash最有名的例子莫過於CRC系列算法。雖然CRC系列算法本身並不是查表,但是,查表是它的一種最快的實現方式。
查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他們的表格都是隨機生成的。
  1. 混合Hash;
混合Hash算法利用了以上各種方式。各種常見的Hash算法,比如MD5、
Tiger都屬於這個範圍。它們一般很少在面向查找的Hash函數裏面使用。


什麼是paxos算法

image

image

  • redis和memcached 的內存管理的區別等等
Redis支持服務器端的數據操作:Redis相比Memcached來說,擁有更多的數據結構和並支持更豐富的數據操作,通常在Memcached裏,你需要將數據拿到客戶端來進行類似的修改再set回去。這大大增加了網絡IO的次數和數據體積。在Redis中,這些複雜的操作通常和一般的GET/SET一樣高效。所以,如果需要緩存能夠支持更復雜的結構和操作,那麼Redis會是不錯的選擇。
內存使用效率對比:使用簡單的key-value存儲的話,Memcached的內存利用率更高,而如果Redis採用hash結構來做key-value存儲,由於其組合式的壓縮,其內存利用率會高於Memcached。
性能對比:由於Redis只使用單核,而Memcached可以使用多核,所以平均每一個核上Redis在存儲小數據時比Memcached性能更高。而在100k以上的數據中,Memcached性能要高於Redis,雖然Redis最近也在存儲大數據的性能上進行優化,但是比起Memcached,還是稍有遜色。

1、數據類型支持不同

與Memcached僅支持簡單的key-value結構的數據記錄不同,Redis支持的數據類型要豐富得多。
最爲常用的數據類型主要由五種:String、Hash、List、Set和Sorted Set。Redis內部使用一個redisObject對象來表示所有的key和value。
redisObject最主要的信息如圖所示:

image

type代表一個value對象具體是何種數據類型,encoding是不同數據類型在redis內部的存儲方式,
比如:type=string代表value存儲的是一個普通字符串,那麼對應的encoding可以是raw或者是int,
如果是int則代表實際redis內部是按數值型類存儲和表示這個字符串的,
當然前提是這個字符串本身可以用數值表示,比如:”123″ “456”這樣的字符串。只有打開了Redis的虛擬內存功能,
vm字段字段纔會真正的分配內存,該功能默認是關閉狀態的。

1)String

常用命令:set/get/decr/incr/mget等;
應用場景:String是最常用的一種數據類型,普通的key/value存儲都可以歸爲此類;
實現方式:String在redis內部存儲默認就是一個字符串,被redisObject所引用,當遇到incr、decr等操作時會轉成數值型進行計算,此時redisObject的encoding字段爲int。

2)Hash

常用命令:hget/hset/hgetall等
應用場景:我們要存儲一個用戶信息對象數據,其中包括用戶ID、用戶姓名、年齡和生日,通過用戶ID我們希望獲取該用戶的姓名或者年齡或者生日;
實現方式:Redis的Hash實際是內部存儲的Value爲一個HashMap,並提供了直接存取這個Map成員的接口。如圖所示,Key是用戶ID, value是一個Map。
這個Map的key是成員的屬性名,value是屬性值。
這樣對數據的修改和存取都可以直接通過其內部Map的Key(Redis裏稱內部Map的key爲field), 
也就是通過 key(用戶ID) + field(屬性標籤) 就可以操作對應屬性數據。
當前HashMap的實現有兩種方式:當HashMap的成員比較少時Redis爲了節省內存會採用類似一維數組的方式來緊湊存儲,
而不會採用真正的HashMap結構,這時對應的value的redisObject的encoding爲zipmap,當成員數量增大時會自動轉成真正的HashMap,此時encoding爲ht。
hash

image
3)List

常用命令:lpush/rpush/lpop/rpop/lrange等;
應用場景:Redis list的應用場景非常多,也是Redis最重要的數據結構之一,
比如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現;
實現方式:Redis list的實現爲一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,
不過帶來了部分額外的內存開銷,Redis內部的很多實現,包括髮送緩衝隊列等也都是用的這個數據結構。

4)Set

常用命令:sadd/spop/smembers/sunion等;
應用場景:Redis set對外提供的功能與list類似是一個列表的功能,特殊之處在於set是可以自動排重的,當你需要存儲一個列表數據,又不希望出現重複數據時,
set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是list所不能提供的;
實現方式:set 的內部實現是一個 value永遠爲null的HashMap,實際就是通過計算hash的方式來快速排重的,這也是set能提供判斷一個成員是否在集合內的原因。

5)Sorted Set

常用命令:zadd/zrange/zrem/zcard等;
應用場景:Redis sorted set的使用場景與set類似,區別是set不是自動有序的,而sorted set可以通過用戶額外提供一個優先級(score)的參數來爲成員排序,
並且是插入有序的,即自動排序。當你需要一個有序的並且不重複的集合列表,
那麼可以選擇sorted set數據結構,比如twitter 的public timeline可以以發表時間作爲score來存儲,這樣獲取時就是自動按時間排好序的。
實現方式:Redis sorted set的內部使用HashMap和跳躍表(SkipList)來保證數據的存儲和有序,HashMap裏放的是成員到score的映射,而跳躍表裏存放的是所有的成員,
排序依據是HashMap裏存的score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。

2、內存管理機制不同

在Redis中,並不是所有的數據都一直存儲在內存中的。這是和Memcached相比一個最大的區別。當物理內存用完時,Redis可以將一些很久沒用到的value交換到磁盤。Redis只會緩存所有的key的信息,如果Redis發現內存的使用量超過了某一個閥值,將觸發swap的操作,Redis根據“swappability = age*log(size_in_memory)”計算出哪些key對應的value需要swap到磁盤。然後再將這些key對應的value持久化到磁盤中,同時在內存中清除。這種特性使得Redis可以保持超過其機器本身內存大小的數據。當然,機器本身的內存必須要能夠保持所有的key,畢竟這些數據是不會進行swap操作的。同時由於Redis將內存中的數據swap到磁盤中的時候,提供服務的主線程和進行swap操作的子線程會共享這部分內存,所以如果更新需要swap的數據,Redis將阻塞這個操作,直到子線程完成swap操作後纔可以進行修改。當從Redis中讀取數據的時候,如果讀取的key對應的value不在內存中,那麼Redis就需要從swap文件中加載相應數據,然後再返回給請求方。 這裏就存在一個I/O線程池的問題。在默認的情況下,Redis會出現阻塞,即完成所有的swap文件加載後纔會相應。這種策略在客戶端的數量較小,進行批量操作的時候比較合適。但是如果將Redis應用在一個大型的網站應用程序中,這顯然是無法滿足大併發的情況的。所以Redis運行我們設置I/O線程池的大小,對需要從swap文件中加載相應數據的讀取請求進行併發操作,減少阻塞的時間。
對於像Redis和Memcached這種基於內存的數據庫系統來說,內存管理的效率高低是影響系統性能的關鍵因素。傳統C語言中的malloc/free函數是最常用的分配和釋放內存的方法,但是這種方法存在着很大的缺陷:首先,對於開發人員來說不匹配的malloc和free容易造成內存泄露;其次頻繁調用會造成大量內存碎片無法回收重新利用,降低內存利用率;最後作爲系統調用,其系統開銷遠遠大於一般函數調用。所以,爲了提高內存的管理效率,高效的內存管理方案都不會直接使用malloc/free調用。Redis和Memcached均使用了自身設計的內存管理機制,但是實現方法存在很大的差異,下面將會對兩者的內存管理機制分別進行介紹。
Memcached默認使用Slab Allocation機制管理內存,其主要思想是按照預先規定的大小,將分配的內存分割成特定長度的塊以存儲相應長度的key-value數據記錄,以完全解決內存碎片問題。Slab Allocation機制只爲存儲外部數據而設計,也就是說所有的key-value數據都存儲在Slab Allocation系統裏,而Memcached的其它內存請求則通過普通的malloc/free來申請,因爲這些請求的數量和頻率決定了它們不會對整個系統的性能造成影響Slab Allocation的原理相當簡單。 如圖所示,它首先從操作系統申請一大塊內存,並將其分割成各種尺寸的塊Chunk,並把尺寸相同的塊分成組Slab Class。其中,Chunk就是用來存儲key-value數據的最小單位。每個Slab Class的大小,可以在Memcached啓動的時候通過制定Growth Factor來控制。假定圖中Growth Factor的取值爲1.25,如果第一組Chunk的大小爲88個字節,第二組Chunk的大小就爲112個字節,依此類推。

image

當Memcached接收到客戶端發送過來的數據時首先會根據收到數據的大小選擇一個最合適的Slab Class,然後通過查詢Memcached保存着的該Slab Class內空閒Chunk的列表就可以找到一個可用於存儲數據的Chunk。當一條數據庫過期或者丟棄時,該記錄所佔用的Chunk就可以回收,重新添加到空閒列表中。從以上過程我們可以看出Memcached的內存管理制效率高,而且不會造成內存碎片,但是它最大的缺點就是會導致空間浪費。因爲每個Chunk都分配了特定長度的內存空間,所以變長數據無法充分利用這些空間。如圖 所示,將100個字節的數據緩存到128個字節的Chunk中,剩餘的28個字節就浪費掉了。

image

Redis的內存管理主要通過源碼中zmalloc.h和zmalloc.c兩個文件來實現的。Redis爲了方便內存的管理,在分配一塊內存之後,會將這塊內存的大小存入內存塊的頭部。如圖所示,real_ptr是redis調用malloc後返回的指針。redis將內存塊的大小size存入頭部,size所佔據的內存大小是已知的,爲size_t類型的長度,然後返回ret_ptr。當需要釋放內存的時候,ret_ptr被傳給內存管理程序。通過ret_ptr,程序可以很容易的算出real_ptr的值,然後將real_ptr傳給free釋放內存。

image

Redis通過定義一個數組來記錄所有的內存分配情況,這個數組的長度爲ZMALLOC_MAX_ALLOC_STAT。數組的每一個元素代表當前程序所分配的內存塊的個數,且內存塊的大小爲該元素的下標。在源碼中,這個數組爲zmalloc_allocations。zmalloc_allocations[16]代表已經分配的長度爲16bytes的內存塊的個數。zmalloc.c中有一個靜態變量used_memory用來記錄當前分配的內存總大小。所以,總的來看,Redis採用的是包裝的mallc/free,相較於Memcached的內存管理方法來說,要簡單很多。

3、數據持久化支持

Redis雖然是基於內存的存儲系統,但是它本身是支持內存數據的持久化的,而且提供兩種主要的持久化策略:RDB快照和AOF日誌。而memcached是不支持數據持久化操作的。

1)RDB快照

Redis支持將當前數據的快照存成一個數據文件的持久化機制,即RDB快照。但是一個持續寫入的數據庫如何生成快照呢?Redis藉助了fork命令的copy on write機制。在生成快照時,將當前進程fork出一個子進程,然後在子進程中循環所有的數據,將數據寫成爲RDB文件。我們可以通過Redis的save指令來配置RDB快照生成的時機,比如配置10分鐘就生成快照,也可以配置有1000次寫入就生成快照,也可以多個規則一起實施。這些規則的定義就在Redis的配置文件中,你也可以通過Redis的CONFIG SET命令在Redis運行時設置規則,不需要重啓Redis。
Redis的RDB文件不會壞掉,因爲其寫操作是在一個新進程中進行的,當生成一個新的RDB文件時,Redis生成的子進程會先將數據寫到一個臨時文件中,然後通過原子性rename系統調用將臨時文件重命名爲RDB文件,這樣在任何時候出現故障,Redis的RDB文件都總是可用的。同時,Redis的RDB文件也是Redis主從同步內部實現中的一環。RDB有他的不足,就是一旦數據庫出現問題,那麼我們的RDB文件中保存的數據並不是全新的,從上次RDB文件生成到Redis停機這段時間的數據全部丟掉了。在某些業務下,這是可以忍受的。

2)AOF日誌

AOF日誌的全稱是append only file,它是一個追加寫入的日誌文件。與一般數據庫的binlog不同的是,AOF文件是可識別的純文本,它的內容就是一個個的Redis標準命令。只有那些會導致數據發生修改的命令纔會追加到AOF文件。每一條修改數據的命令都生成一條日誌,AOF文件會越來越大,所以Redis又提供了一個功能,叫做AOF rewrite。其功能就是重新生成一份AOF文件,新的AOF文件中一條記錄的操作只會有一次,而不像一份老文件那樣,可能記錄了對同一個值的多次操作。其生成過程和RDB類似,也是fork一個進程,直接遍歷數據,寫入新的AOF臨時文件。在寫入新文件的過程中,所有的寫操作日誌還是會寫到原來老的AOF文件中,同時還會記錄在內存緩衝區中。當重完操作完成後,會將所有緩衝區中的日誌一次性寫入到臨時文件中。然後調用原子性的rename命令用新的AOF文件取代老的AOF文件。
AOF是一個寫文件操作,其目的是將操作日誌寫到磁盤上,所以它也同樣會遇到我們上面說的寫操作的流程。在Redis中對AOF調用write寫入後,通過appendfsync選項來控制調用fsync將其寫到磁盤上的時間,下面appendfsync的三個設置項,安全強度逐漸變強。
appendfsync no 
當設置appendfsync爲no的時候,Redis不會主動調用fsync去將AOF日誌內容同步到磁盤,所以這一切就完全依賴於操作系統的調試了。
對大多數Linux操作系統,是每30秒進行一次fsync,將緩衝區中的數據寫到磁盤上。
appendfsync everysec 
當設置appendfsync爲everysec的時候,Redis會默認每隔一秒進行一次fsync調用,將緩衝區中的數據寫到磁盤。
但是當這一次的fsync調用時長超過1秒時。Redis會採取延遲fsync的策略,再等一秒鐘。也就是在兩秒後再進行fsync,
這一次的fsync就不管會執行多長時間都會進行。這時候由於在fsync時文件描述符會被阻塞,所以當前的寫操作就會阻塞。
所以結論就是,在絕大多數情況下,Redis會每隔一秒進行一次fsync。在最壞的情況下,兩秒鐘會進行一次fsync操作。
這一操作在大多數數據庫系統中被稱爲group commit,就是組合多次寫操作的數據,一次性將日誌寫到磁盤。
appednfsync always 
當設置appendfsync爲always時,每一次寫操作都會調用一次fsync,這時數據是最安全的,當然,由於每次都會執行fsync,所以其性能也會受到影響。
對於一般性的業務需求,建議使用RDB的方式進行持久化,原因是RDB的開銷並相比AOF日誌要低很多,對於那些無法忍數據丟失的應用,建議使用AOF日誌。

4、集羣管理的不同

Memcached是全內存的數據緩衝系統,Redis雖然支持數據的持久化,但是全內存畢竟纔是其高性能的本質。作爲基於內存的存儲系統來說,機器物理內存的大小就是系統能夠容納的最大數據量。如果需要處理的數據量超過了單臺機器的物理內存大小,就需要構建分佈式集羣來擴展存儲能力。
Memcached本身並不支持分佈式,因此只能在客戶端通過像一致性哈希這樣的分佈式算法來實現Memcached的分佈式存儲。下圖給出了Memcached的分佈式存儲實現架構。當客戶端向Memcached集羣發送數據之前,首先會通過內置的分佈式算法計算出該條數據的目標節點,然後數據會直接發送到該節點上存儲。但客戶端查詢數據時,同樣要計算出查詢數據所在的節點,然後直接向該節點發送查詢請求以獲取數據。

image

相較於Memcached只能採用客戶端實現分佈式存儲,Redis更偏向於在服務器端構建分佈式存儲。最新版本的Redis已經支持了分佈式存儲功能。Redis Cluster是一個實現了分佈式且允許單點故障的Redis高級版本,它沒有中心節點,具有線性可伸縮的功能。下圖給出Redis Cluster的分佈式存儲架構,其中節點與節點之間通過二進制協議進行通信,節點與客戶端之間通過ascii協議進行通信。在數據的放置策略上,Redis Cluster將整個key的數值域分成4096個哈希槽,每個節點上可以存儲一個或多個哈希槽,也就是說當前Redis Cluster支持的最大節點數就是4096。Redis Cluster使用的分佈式算法也很簡單:crc16( key ) % HASH_SLOTS_NUMBER。

image

爲了保證單點故障下的數據可用性,Redis Cluster引入了Master節點和Slave節點。在Redis Cluster中,每個Master節點都會有對應的兩個用於冗餘的Slave節點。這樣在整個集羣中,任意兩個節點的宕機都不會導致數據的不可用。當Master節點退出後,集羣會自動選擇一個Slave節點成爲新的Master節點。

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章