藏文網(wǎng)站建設(shè)計劃網(wǎng)站建設(shè)優(yōu)化公司
文章目錄
- 前言
- 一 專業(yè)技能
- 1. 熟悉GoLang語言
- 1.1 Slice
- 1.2 Map
- 1.3 Channel
- 1.4 Goroutine
- 1.5 GMP調(diào)度
- 1.6 垃圾回收機(jī)制
- 1.7 其他知識點(diǎn)
- 2. 掌握Web框架Gin和微服務(wù)框架Micro
- 2.1 Gin框架
- 2.2 Micro框架
- 2.3 Viper
- 2.4 Swagger
- 2.5 Zap
- 2.6 JWT
- 3. 熟悉使用 MySQL 數(shù)據(jù)庫
- 3.1 索引
- 3.2 事務(wù)
- 3.3 存儲引擎
- 3.4 鎖機(jī)制
- 3.5 其他面試題
- 4. 熟悉計算機(jī)網(wǎng)絡(luò)
- 4.1 TCP/IP協(xié)議
- 4.2 HTTP/HTTPS協(xié)議
- 4.3 TCP三次握手/四次揮手
- 4.4 IP協(xié)議
- 4.5 其他面試題
- 5. 熟悉操作系統(tǒng)
- 5.1 進(jìn)程、線程管理
- 5.2 內(nèi)存管理
- 5.3 進(jìn)程調(diào)度算法
- 5.4 磁盤調(diào)度算法
- 5.5 頁面置換算法
- 5.6 網(wǎng)絡(luò)系統(tǒng)
- 5.7 鎖
- 5.8 其他面試題
- 6. 熟悉Redis的基本使用
- 6.1 基本數(shù)據(jù)結(jié)構(gòu)
- 6.2 數(shù)據(jù)持久化
- 6.3 高可用
- 6.4 緩存
- 7.熟悉使用 Python 語言
- 7.1 Django框架
- 7.2 爬蟲
- 7.3 數(shù)據(jù)分析
- 7.4 機(jī)器學(xué)習(xí)算法
- 7.5 Python語言
- 8. 了解前端技術(shù)
- 8.1 Html/Css
- 8.2 JavaScript
- 8.3 Vue框架
- 二 項目經(jīng)歷
- 1. 基于機(jī)器學(xué)習(xí)的冬奧會智能分析與預(yù)測系統(tǒng)
- 1.1 項目描述
- 1.2 工作內(nèi)容
- 1.3 面試問題
- 2. 基于微服務(wù)的通用賬戶功能系統(tǒng)
- 2.1 項目描述
- 2.2 工作內(nèi)容
- 2.3 面試問題
文章字?jǐn)?shù)大約15.5萬字,閱讀大概需要8.6小時,建議收藏后慢慢閱讀!!!
前言
本篇文章是博主自己找工作時收集總結(jié)的,涵蓋了Go語言、MySQL數(shù)據(jù)庫、計算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、Redis、Python語言等多種面經(jīng),主要是根據(jù)博主自己的簡歷來制作的,編寫不易,先贊后看。
話不多說,開搞!
一 專業(yè)技能
1. 熟悉GoLang語言
1.1 Slice
-
Slice底層實現(xiàn)原理
切片是基于數(shù)組實現(xiàn)的,它的底層是數(shù)組,它自己本身非常小,可以理解為對底層數(shù)組的抽象。因為基于數(shù)組實現(xiàn),所以它的底層的內(nèi)存是連續(xù)分配的,效率非常高,還可以通過索引獲得數(shù)據(jù),可以迭代以及垃圾回收優(yōu)化。 切片本身并不是動態(tài)數(shù)組或者數(shù)組指針。它內(nèi)部實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)通過指針引用底層數(shù)組,設(shè)定相關(guān)屬性將數(shù)據(jù)讀寫操作限定在指定的區(qū)域內(nèi)。切片本身是一 個只讀對象,其工作機(jī)制類似數(shù)組指針的一種封裝。
切片對象非常小,是因為它是只有 3 個字段的數(shù)據(jù)結(jié)構(gòu):
-
指向底層數(shù)組的指針
-
切片的長度
-
切片的容量
-
-
Slice擴(kuò)容機(jī)制
在使用 append 向 slice 追加元素時,若 slice 空間不足則會發(fā)生擴(kuò)容,擴(kuò)容會重新分配一塊更大的內(nèi)存,將原 slice 拷貝到新 slice ,然后返回新 slice。擴(kuò)容后再將數(shù)據(jù)追加進(jìn)去。
擴(kuò)容操作只對容量,擴(kuò)容后的 slice 長度不變,容量變化規(guī)則如下:
- 若 slice 容量小于1024個元素,那么擴(kuò)容的時候slice的cap就翻番,乘以2;一旦元素個數(shù)超過1024個元素,增長因子就變成1.25,即每次增加原來容量的四分之一。
- 若 slice 容量夠用,則將新元素追加進(jìn)去,slice.len++,返回原 slice
- 若 slice 容量不夠用,將 slice 先擴(kuò)容,擴(kuò)容得到新 slice,將新元素追加進(jìn)新 slice,slice.len++,返回新 slice。
-
Slice與數(shù)組區(qū)別
array是固定長度的數(shù)組,使用前必須確定數(shù)組長度,是值類型。
slice是一個引用類型,是一個動態(tài)的指向數(shù)組切片的指針。
slice是一個不定長的,總是指向底層的數(shù)組array的數(shù)據(jù)結(jié)構(gòu),可以動態(tài)擴(kuò)容。創(chuàng)建方式不一樣,Slice使用make創(chuàng)建或者根據(jù)數(shù)組創(chuàng)建。
作為函數(shù)參數(shù)時,數(shù)組傳遞的是數(shù)組的副本,而slice傳遞的是指針。
1.2 Map
-
Map底層實現(xiàn)原理
Golang 中 map 的底層實現(xiàn)是一個散列表,因此實現(xiàn) map 的過程實際上就是實現(xiàn)散表的過程。在這個散列表中,主要出現(xiàn)的結(jié)構(gòu)體有兩個,一個叫 hmap(a header for a go map),一個叫 bmap(a bucket for a Go map,通常叫其 bucket)。
hmap 哈希表
hmap是Go map的底層實現(xiàn),每個hmap內(nèi)都含有多個bmap(buckets桶、oldbuckets舊桶、overflow溢出桶),既每個哈希表都由多個桶組成。-
buckets
buckets是一個指針,指向一個bmap數(shù)組,存儲多個桶。 -
oldbuckets
oldbuckets是一個指針,指向一個bmap數(shù)組,存儲多個舊桶,用于擴(kuò)容。 -
overflow
overflow是一個指針,指向一個元素個數(shù)為2的數(shù)組,數(shù)組的類型是一個指針,指向一個slice,slice的元素是桶(bmap)的地址,這些桶都是溢出桶。為什么有兩個?因為Go map在哈希沖突過多時,會發(fā)生擴(kuò)容操作。[0]表示當(dāng)前使用的溢出桶集合,[1]是在發(fā)生擴(kuò)容時,保存了舊的溢出桶集合。overflow存在的意義在于防止溢出桶被gc。
bmap 哈希桶
bmap是一個隸屬于hmap的結(jié)構(gòu)體,一個桶(bmap)可以存儲8個鍵值對。如果有第9個鍵值對被分配到該桶,那就需要再創(chuàng)建一個桶,通過overflow指針將兩個桶連接起來。在hmap中,多個bmap桶通過overflow指針相連,組成一個鏈表。 -
-
Map進(jìn)行有序的排序
map每次遍歷,都會從一個隨機(jī)值序號的桶,再從其中隨機(jī)的cell開始遍歷,并且擴(kuò)容后,原來桶中的key會落到其他桶中,本身就會造成失序
如果想順序遍歷map,先把key放到切片排序,再按照key的順序遍歷map。
或者可以先把map中的key,通過sort包排序,再遍歷map。
-
map 為什么是不安全的
Go map 默認(rèn)是并發(fā)不安全的,同時對 map 進(jìn)行并發(fā)讀寫的時,程序會 panic,原因如下:Go 官方經(jīng)過長時間的討論,認(rèn)為 map 適配的場景應(yīng)該是簡單的(不需要從多個 gorountine 中進(jìn)行安全訪問的),而不是為了小部分情況(并發(fā)訪問),導(dǎo)致大部分程序付出鎖的代價,因此決定了不支持。
map 在擴(kuò)縮容時,需要進(jìn)行數(shù)據(jù)遷移,遷移的過程并沒有采用鎖機(jī)制防止并發(fā)操作,而是會對某個標(biāo)識位標(biāo)記為 1,表示此時正在遷移數(shù)據(jù)。如果有其他 goroutine 對 map 也進(jìn)行寫操作,當(dāng)它檢測到標(biāo)識位為 1 時,將會直接 panic。
如果想實現(xiàn)map線程安全,有兩種方式:
方式一:使用讀寫鎖
map
+sync.RWMutex
方式二:使用golang提供的
sync.Map
-
Map擴(kuò)容策略
擴(kuò)容時機(jī):
向 map 插入新 key 的時候,會進(jìn)行條件檢測,符合下面這 2 個條件,就會觸發(fā)擴(kuò)容
擴(kuò)容條件:
-
超過負(fù)載 map元素個數(shù) > 6.5(負(fù)載因子) * 桶個數(shù)
-
溢出桶太多
當(dāng)桶總數(shù)<2^15時,如果溢出桶總數(shù)>=桶總數(shù),則認(rèn)為溢出桶過多
當(dāng)桶總數(shù)>215時,如果溢出桶總數(shù)>=215,則認(rèn)為溢出桶過多
擴(kuò)容機(jī)制:
-
雙倍擴(kuò)容:針對條件1,新建一個buckets數(shù)組,新的buckets大小是原來的2倍,然后舊buckets數(shù)據(jù)搬遷到新的buckets。
-
等量擴(kuò)容:針對條件2,并不擴(kuò)大容量,buckets數(shù)量維持不變,重新做一遍類似雙倍擴(kuò)容的搬遷動作,把松散的鍵值對重新排列一次,使得同一個 bucket 中的 key 排列地更緊密,節(jié)省空間,提高 bucket 利用率,進(jìn)而保證更快的存取。
-
漸進(jìn)式擴(kuò)容:
插入修改刪除key的時候,都會嘗試進(jìn)行搬遷桶的工作,每次都會檢查oldbucket是否nil,如果不是nil則每次搬遷2個桶,螞蟻搬家一樣漸進(jìn)式擴(kuò)容
-
-
Map和Slice區(qū)別
- 數(shù)組:數(shù)組是一個由固定長度的特定類型元素組成的序列,一個數(shù)組可以由零個或多個元素組成。聲明方式:var a [3]int
- slice(切片):Slice(切片)代表變長的序列,序列中每個元素都有相同的類型,slice的語法和數(shù)組很像,只是沒有固定長度而已。
- map:在Go語言中,一個map就是一個哈希表的引用,是一個無序的key/value對的集合
-
Map總結(jié)
- map是引用類型
- map遍歷是無序的
- map是非線程安全的
- map的哈希沖突解決方式是鏈表法
- map的擴(kuò)容不是一定會新增空間,也有可能是只是做了內(nèi)存整理
- map的遷移是逐步進(jìn)行的,在每次賦值時,會做至少一次遷移工作
- map中刪除key,有可能導(dǎo)致出現(xiàn)很多空的kv,這會導(dǎo)致遷移操作,如果可以避免,盡量避免
1.3 Channel
-
介紹一下Channel(有緩沖和無緩沖)
Go 語言中,不要通過共享內(nèi)存來通信,而要通過通信來實現(xiàn)內(nèi)存共享。Go 的CSP(Communicating Sequential Process)并發(fā)模型,中文可以叫做通信順序進(jìn)程,是通過 goroutine 和 channel 來實現(xiàn)的。
所以 channel 收發(fā)遵循先進(jìn)先出 FIFO,分為有緩存和無緩存,channel 中大致有 buffer(當(dāng)緩沖區(qū)大小部位 0 時,是個 ring buffer)、sendx 和 recvx 收發(fā)的位置(ring buffer 記錄實現(xiàn))、sendq、recvq 當(dāng)前 channel 因為緩沖區(qū)不足 而阻塞的隊列、使用雙向鏈表存儲、還有一個 mutex 鎖控制并發(fā)、其他原屬等。
// 無緩沖的channel由于沒有緩沖發(fā)送和接收需要同步 ch := make(chan int) //有緩沖channel不要求發(fā)送和接收操作同步 ch := make(chan int, 2)
channel 無緩沖時,發(fā)送阻塞直到數(shù)據(jù)被接收,接收阻塞直到讀到數(shù)據(jù);channel有緩沖時,當(dāng)緩沖滿時發(fā)送阻塞,當(dāng)緩沖空時接收阻塞。
-
Channel實現(xiàn)原理
channel 內(nèi)部維護(hù)了兩個 goroutine 隊列,一個是待發(fā)送數(shù)據(jù)的 goroutine 隊列,另一個是待讀取數(shù)據(jù)的 goroutine 隊列。
每當(dāng)對 channel 的讀寫操作超過了可緩沖的 goroutine 數(shù)量,那么當(dāng)前的 goroutine 就會被掛到對應(yīng)的隊列上,直到有其他 goroutine 執(zhí)行了與之相反的讀寫操作,將它重新喚起。
-
Channel讀寫流程
向 channel 寫數(shù)據(jù):
若等待接收隊列 recvq 不為空,則緩沖區(qū)中無數(shù)據(jù)或無緩沖區(qū),將直接從 recvq 取出 G ,并把數(shù)據(jù)寫入,最后把該 G 喚醒,結(jié)束發(fā)送過程。
若緩沖區(qū)中有空余位置,則將數(shù)據(jù)寫入緩沖區(qū),結(jié)束發(fā)送過程。
若緩沖區(qū)中沒有空余位置,則將發(fā)送數(shù)據(jù)寫入 G,將當(dāng)前 G 加入 sendq ,進(jìn)入睡眠,等待被讀 goroutine 喚醒。
從 channel 讀數(shù)據(jù)
若等待發(fā)送隊列 sendq 不為空,且沒有緩沖區(qū),直接從 sendq 中取出 G ,把 G 中數(shù)據(jù)讀出,最后把 G 喚醒,結(jié)束讀取過程。
如果等待發(fā)送隊列 sendq 不為空,說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把 G 中數(shù)據(jù)寫入緩沖區(qū)尾部,把 G 喚醒,結(jié)束讀取過程。
如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程。
將當(dāng)前 goroutine 加入 recvq ,進(jìn)入睡眠,等待被寫 goroutine 喚醒。
關(guān)閉 channel
1.關(guān)閉 channel 時會將 recvq 中的 G 全部喚醒,本該寫入 G 的數(shù)據(jù)位置為 nil。將 sendq 中的 G 全部喚醒,但是這些 G 會 panic。
panic 出現(xiàn)的場景還有:
- 關(guān)閉值為 nil 的 channel
- 關(guān)閉已經(jīng)關(guān)閉的 channel
- 向已經(jīng)關(guān)閉的 channel 中寫數(shù)據(jù)
-
Channel為什么能做到線程安全
Channel 可以理解是一個先進(jìn)先出的隊列,通過管道進(jìn)行通信,發(fā)送一個數(shù)據(jù)到Channel和從Channel接收一個數(shù)據(jù)都是原子性的。不要通過共享內(nèi)存來通信,而是通過通信來共享內(nèi)存,前者就是傳統(tǒng)的加鎖,后者就是Channel。設(shè)計Channel的主要目的就是在多任務(wù)間傳遞數(shù)據(jù)的,本身就是安全的。
-
Channel是同步進(jìn)行還是異步的(Channel的三種狀態(tài))
Channel是異步進(jìn)行的, channel存在3種狀態(tài):
- nil,未初始化的狀態(tài),只進(jìn)行了聲明,或者手動賦值為nil
- active,正常的channel,可讀或者可寫
- closed,已關(guān)閉,千萬不要誤認(rèn)為關(guān)閉channel后,channel的值是nil
操作 一個零值nil通道 一個非零值但已關(guān)閉的通道 一個非零值且尚未關(guān)閉的通道 關(guān)閉 產(chǎn)生恐慌 產(chǎn)生恐慌 成功關(guān)閉 發(fā)送數(shù)據(jù) 永久阻塞 產(chǎn)生恐慌 阻塞或者成功發(fā)送 接收數(shù)據(jù) 永久阻塞 永不阻塞 阻塞或者成功接收 - 給一個 nil channel 發(fā)送數(shù)據(jù),造成永遠(yuǎn)阻塞
- 從一個 nil channel 接收數(shù)據(jù),造成永遠(yuǎn)阻塞
- 給一個已經(jīng)關(guān)閉的 channel 發(fā)送數(shù)據(jù),引起 panic
- 從一個已經(jīng)關(guān)閉的 channel 接收數(shù)據(jù),如果緩沖區(qū)中為空,則返回一個零值
- 無緩沖的 channel 是同步的,而有緩沖的 channel 是非同步的
- 關(guān)閉一個 nil channel 將會發(fā)生 panic
1.4 Goroutine
-
進(jìn)程、線程和協(xié)程的區(qū)別
-
進(jìn)程: 進(jìn)程是具有一定獨(dú)立功能的程序,進(jìn)程是系統(tǒng)資源分配和調(diào)度的最小單位。每個進(jìn)程都有自己的獨(dú)立內(nèi)存空間,不同進(jìn)程通過進(jìn)程間通信來通信。由于進(jìn)程比較重量,占據(jù)獨(dú)立的內(nèi)存,所以上下文進(jìn)程間的切換開銷(棧、寄存器、虛擬內(nèi)存、文件句柄等)比較大,但相對比較穩(wěn)定安全。
-
線程: 線程是進(jìn)程的一個實體,線程是內(nèi)核態(tài),而且是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位。線程間通信主要通過共享內(nèi)存,上下文切換很快,資源開銷較少,但相比進(jìn)程不夠穩(wěn)定容易丟失數(shù)據(jù)。
-
協(xié)程: 協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調(diào)度完全是由用戶來控制的。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復(fù)先前保存的寄存器上下文和棧,直接操作棧則基本沒有內(nèi)核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
-
線程和協(xié)程的區(qū)別
- 線程切換需要陷入內(nèi)核,然后進(jìn)行上下文切換,而協(xié)程在用戶態(tài)由協(xié)程調(diào)度器完成,不需要陷入內(nèi)核,這樣代價就小了。
- 協(xié)程的切換時間點(diǎn)是由調(diào)度器決定,而不是由系統(tǒng)內(nèi)核決定的,盡管它們的切換點(diǎn)都是時間片超過一定閾值,或者是進(jìn)入I/O或睡眠等狀態(tài)時。
- 基于垃圾回收的考慮,Go實現(xiàn)了垃圾回收,但垃圾回收的必要條件是內(nèi)存位于一致狀態(tài),因此就需要暫停所有的線程。如果交給系統(tǒng)去做,那么會暫停所有的線程使其一致。對于Go語言來說,調(diào)度器知道什么時候內(nèi)存位于一致狀態(tài),所以也就沒有必要暫停所有運(yùn)行的線程。
-
-
介紹一下Goroutine
Goroutine 是一個與其他 goroutines 并行運(yùn)行在同一地址空間的 Go 函數(shù)或方法。
goroutine的概念類似于線程,但 goroutine是由Go的運(yùn)行時(runtime)調(diào)度和管理的。Go程序會智能地將 goroutine 中的任務(wù)合理地分配給每個CPU。它在語言層面已經(jīng)內(nèi)置了調(diào)度和上下文切換的機(jī)制。
goroutine是Go并發(fā)設(shè)計的核心,也叫協(xié)程,它比線程更加輕量,因此可以同時運(yùn)行成千上萬個并發(fā)任務(wù)。在Go語言中,每一個并發(fā)的執(zhí)行單元叫作一個goroutine。我們只需要在調(diào)用的函數(shù)前面添加go關(guān)鍵字,就能使這個函數(shù)以協(xié)程的方式運(yùn)行。
-
context包結(jié)構(gòu)原理和用途
Context(上下文)是Golang應(yīng)用開發(fā)常用的并發(fā)控制技術(shù) ,它可以控制一組呈樹狀結(jié)構(gòu)的goroutine,每個goroutine擁有相同的上下文。Context 是并發(fā)安全的,主要是用于控制多個協(xié)程之間的協(xié)作、取消操作。
Context 只定義了接口,凡是實現(xiàn)該接口的類都可稱為是一種 context。
- 「Deadline」 方法:可以獲取設(shè)置的截止時間,返回值 deadline 是截止時間,到了這個時間,Context 會自動發(fā)起取消請求,返回值 ok 表示是否設(shè)置了截止時間。
- 「Done」 方法:返回一個只讀的 channel ,類型為 struct{}。如果這個 chan 可以讀取,說明已經(jīng)發(fā)出了取消信號,可以做清理操作,然后退出協(xié)程,釋放資源。
- 「Err」 方法:返回Context 被取消的原因。
- 「Value」 方法:獲取 Context 上綁定的值,是一個鍵值對,通過 key 來獲取對應(yīng)的值。
-
goroutine調(diào)度
GPM是Go語言運(yùn)行時(runtime)層面的實現(xiàn),是go語言自己實現(xiàn)的一套調(diào)度系統(tǒng)。區(qū)別于操作系統(tǒng)調(diào)度OS線程。
-
- G很好理解,就是個goroutine的,里面除了存放本goroutine信息外 還有與所在P的綁定等信息。
-
- P管理著一組goroutine隊列,P里面會存儲當(dāng)前goroutine運(yùn)行的上下文環(huán)境(函數(shù)指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調(diào)度(比如把占用CPU時間較長的goroutine暫停、運(yùn)行后續(xù)的goroutine等等)當(dāng)自己的隊列消費(fèi)完了就去全局隊列里取,如果全局隊列里也消費(fèi)完了會去其他P的隊列里搶任務(wù)。
-
- M(machine)是Go運(yùn)行時(runtime)對操作系統(tǒng)內(nèi)核線程的虛擬, M與內(nèi)核線程一般是一一映射的關(guān)系, 一個groutine最終是要放到M上執(zhí)行的;
P與M一般也是一一對應(yīng)的。他們關(guān)系是: P管理著一組G掛載在M上運(yùn)行。當(dāng)一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當(dāng)舊的G阻塞完成或者認(rèn)為其已經(jīng)死掉時 回收舊的M。
P的個數(shù)是通過runtime.GOMAXPROCS設(shè)定(最大256),Go1.5版本之后默認(rèn)為物理線程數(shù)。 在并發(fā)量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
單從線程調(diào)度講,Go語言相比起其他語言的優(yōu)勢在于OS線程是由OS內(nèi)核來調(diào)度的,goroutine則是由Go運(yùn)行時(runtime)自己的調(diào)度器調(diào)度的,這個調(diào)度器使用一個稱為m:n調(diào)度的技術(shù)(復(fù)用/調(diào)度m個goroutine到n個OS線程)。 其一大特點(diǎn)是goroutine的調(diào)度是在用戶態(tài)下完成的, 不涉及內(nèi)核態(tài)與用戶態(tài)之間的頻繁切換,包括內(nèi)存的分配與釋放,都是在用戶態(tài)維護(hù)著一塊大的內(nèi)存池, 不直接調(diào)用系統(tǒng)的malloc函數(shù)(除非內(nèi)存池需要改變),成本比調(diào)度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調(diào)度方面的性能。
-
-
如何避免Goroutine泄露和泄露場景
gorouinte 里有關(guān)于 channel 的操作,如果沒有正確處理 channel 的讀取,會導(dǎo)致 channel 一直阻塞住, goroutine 不能正常結(jié)束
-
waitgroup 用法和原理
waitgroup 內(nèi)部維護(hù)了一個計數(shù)器,當(dāng)調(diào)用
wg.Add(1)
方法時,就會增加對應(yīng)的數(shù)量;當(dāng)調(diào)用wg.Done()
時,計數(shù)器就會減一。直到計數(shù)器的數(shù)量減到 0 時,就會調(diào)用
runtime_Semrelease 喚起之前因為wg.Wait()
而阻塞住的 goroutine。使用方法:
- main 協(xié)程通過調(diào)用 wg.Add(delta int) 設(shè)置 worker 協(xié)程的個數(shù),然后創(chuàng)建 worker 協(xié)程;
- worker 協(xié)程執(zhí)行結(jié)束以后,都要調(diào)用 wg.Done();
- main 協(xié)程調(diào)用 wg.Wait() 且被 block,直到所有 worker 協(xié)程全部執(zhí)行結(jié)束后返回。
實現(xiàn)原理:
-
WaitGroup 主要維護(hù)了 2 個計數(shù)器,一個是請求計數(shù)器 v,一個是等待計數(shù)器 w,二者組成一個 64bit 的值,請求計數(shù)器占高 32bit,等待計數(shù)器占低 32bit。
-
每次 Add 執(zhí)行,請求計數(shù)器 v 加 1,Done 方法執(zhí)行,請求計數(shù)器減 1,v為0 時通過信號量喚醒 Wait()。
1.5 GMP調(diào)度
-
GMP是什么
- G(Goroutine):即Go協(xié)程,每個go關(guān)鍵字都會創(chuàng)建一個協(xié)程。
- M(Machine):工作線程,在Go中稱為Machine,數(shù)量對應(yīng)真實的CPU數(shù)(真正干活的對象)。
- P(Processor):處理器(Go中定義的一個摡念,非CPU),包含運(yùn)行Go代碼的必要資源,用來調(diào)度 G 和 M 之間的關(guān)聯(lián)關(guān)系,其數(shù)量可通過 GOMAXPROCS() 來設(shè)置,默認(rèn)為核心數(shù)。
M必須擁有P才可以執(zhí)行G中的代碼,P含有一個包含多個G的隊列,P可以調(diào)度G交由M執(zhí)行。
優(yōu)先從 P 的本地隊列獲取 goroutine 來執(zhí)行;如果本地隊列沒有,從全局隊列獲取,如果全局隊列也沒有,會從其他的 P 上偷取 goroutine。
-
GMP goroutine調(diào)度策略
- 隊列輪轉(zhuǎn):P 會周期性的將G調(diào)度到M中執(zhí)行,執(zhí)行一段時間后,保存上下文,將G放到隊列尾部,然后從隊列中再取出一個G進(jìn)行調(diào)度。除此之外,P還會周期性的查看全局隊列是否有G等待調(diào)度到M中執(zhí)行。
- 系統(tǒng)調(diào)用:當(dāng)G0即將進(jìn)入系統(tǒng)調(diào)用時,M0將釋放P,進(jìn)而某個空閑的M1獲取P,繼續(xù)執(zhí)行P隊列中剩下的G。M1的來源有可能是M的緩存池,也可能是新建的。
- 當(dāng)G0系統(tǒng)調(diào)用結(jié)束后,如果有空閑的P,則獲取一個P,繼續(xù)執(zhí)行G0。如果沒有,則將G0放入全局隊列,等待被其他的P調(diào)度。然后M0將進(jìn)入緩存池睡眠。
-
調(diào)度器的設(shè)計策略
復(fù)用線程:避免頻繁的創(chuàng)建、銷毀線程,而是對線程的復(fù)用。
- work stealing 機(jī)制
- 當(dāng)本線程無可運(yùn)行的 G 時,嘗試從其他線程綁定的 P 偷取 G,而不是銷毀線程。
- hand off 機(jī)制
- 當(dāng)本線程因為 G 進(jìn)行系統(tǒng)調(diào)用阻塞時,線程釋放綁定的 P,把 P 轉(zhuǎn)移給其他空閑的線程執(zhí)行。
利用并行:GOMAXPROCS 設(shè)置 P 的數(shù)量,最多有 GOMAXPROCS 個線程分布在多個 CPU 上同時運(yùn)行。GOMAXPROCS 也限制了并發(fā)的程度,比如 GOMAXPROCS = 核數(shù)/2,則最多利用了一半的 CPU 核進(jìn)行并行。
搶占:在 coroutine 中要等待一個協(xié)程主動讓出 CPU 才執(zhí)行下一個協(xié)程,在 Go 中,一個 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被餓死,這就是 goroutine 不同于 coroutine 的一個地方。
全局 G 隊列:,當(dāng) M 執(zhí)行 work stealing 從其他 P 偷不到 G 時,它可以從全局 G 隊列獲取 G。
- work stealing 機(jī)制
-
CSP 模型是“以通信的方式來共享內(nèi)存”,不同于傳統(tǒng)的多線程通過共享內(nèi)存來通信。用于描述兩個獨(dú)立的并發(fā)實體通過共享的通訊 channel (管道)進(jìn)行通信的并發(fā)模型。
-
兩種搶占式調(diào)度
協(xié)作式的搶占式調(diào)度
在 1.14 版本之前,程序只能依靠 Goroutine 主動讓出 CPU 資源才能觸發(fā)調(diào)度,存在問題
-
某些 Goroutine 可以長時間占用線程,造成其它 Goroutine 的饑餓
-
垃圾回收需要暫停整個程序(Stop-the-world,STW),最長可能需要幾分鐘的時間,導(dǎo)致整個程序無法工作。
基于信號的搶占式調(diào)度
在任何情況下,Go 運(yùn)行時并行執(zhí)行(注意,不是并發(fā))的 goroutines 數(shù)量是小于等于 P 的數(shù)量的。為了提高系統(tǒng)的性能,P 的數(shù)量肯定不是越小越好,所以官方默認(rèn)值就是 CPU 的核心數(shù),設(shè)置的過小的話,如果一個持有 P 的 M, 由于 P 當(dāng)前執(zhí)行的 G 調(diào)用了 syscall 而導(dǎo)致 M 被阻塞,那么此時關(guān)鍵點(diǎn):GO 的調(diào)度器是遲鈍的,它很可能什么都沒做,直到 M 阻塞了相當(dāng)長時間以后,才會發(fā)現(xiàn)有一個 P/M 被 syscall 阻塞了。然后,才會用空閑的 M 來強(qiáng)這個 P。通過 sysmon 監(jiān)控實現(xiàn)的搶占式調(diào)度,最快在 20us,最慢在 10-20ms 才 會發(fā)現(xiàn)有一個 M 持有 P 并阻塞了。操作系統(tǒng)在 1ms 內(nèi)可以完成很多次線程調(diào)度(一般情況 1ms 可以完成幾十次線程調(diào)度),Go 發(fā)起 IO/syscall 的時候執(zhí)行該 G 的 M 會阻塞然后被 OS 調(diào)度走,P 什么也不干,sysmon 最慢要 10-20ms才能發(fā)現(xiàn)這個阻塞,說不定那時候阻塞已經(jīng)結(jié)束了,寶貴的 P 資源就這么被阻塞的 M 浪費(fèi)。
-
-
GMP 調(diào)度過程中存在哪些阻塞
-
I/O,select
-
block on syscall
-
channel
-
等待鎖
-
runtime.Gosched()
-
-
GMP 調(diào)度流程
-
每個 P 有個局部隊列,局部隊列保存待執(zhí)行的 goroutine(流程 2),當(dāng) M 綁 定的 P 的的局部隊列已經(jīng)滿了之后就會把 goroutine 放到全局隊列(流程 2- 1)
-
每個 P 和一個 M 綁定,M 是真正的執(zhí)行 P 中 goroutine 的實體(流程 3),M 從綁定的 P 中的局部隊列獲取 G 來執(zhí)行
-
當(dāng) M 綁定的 P 的局部隊列為空時,M 會從全局隊列獲取到本地隊列來執(zhí)行G(流程 3.1),當(dāng)從全局隊列中沒有獲取到可執(zhí)行的 G 時候,M 會從其他 P 的局部隊列中偷取 G 來執(zhí)行(流程 3.2),這種從其他 P 偷的方式稱為 work stealing
-
當(dāng) G 因系統(tǒng)調(diào)用(syscall)阻塞時會阻塞 M,此時 P 會和 M 解綁即 hand off,并尋找新的 idle 的 M,若沒有 idle 的 M 就會新建一個 M(流程 5.1)。
-
當(dāng) G 因 channel 或者 network I/O 阻塞時,不會阻塞 M,M 會尋找其他 runnable 的 G;當(dāng)阻塞的 G 恢復(fù)后會重新進(jìn)入 runnable 進(jìn)入 P 隊列等待執(zhí) 行(流程 5.3)
-
1.6 垃圾回收機(jī)制
-
GC 原理
垃圾回收就是對程序中不再使用的內(nèi)存資源進(jìn)行自動回收的操作。
三色標(biāo)記法
- 初始狀態(tài)下所有對象都是白色的。
- 從根節(jié)點(diǎn)開始遍歷所有對象,把遍歷到的對象變成灰色對象
- 遍歷灰色對象,將灰色對象引用的對象也變成灰色對象,然后將遍歷過的灰色對象變成黑色對象。
- 循環(huán)步驟3,直到灰色對象全部變黑色。
- 通過寫屏障(write-barrier)檢測對象有變化,重復(fù)以上操作
- 收集所有白色對象(垃圾)。
STW(Stop The World)
- 為了避免在 GC 的過程中,對象之間的引用關(guān)系發(fā)生新的變更,使得GC的結(jié)果發(fā)生錯誤(如GC過程中新增了一個引用,但是由于未掃描到該引用導(dǎo)致將被引用的對象清除了),停止所有正在運(yùn)行的協(xié)程。
- STW對性能有一些影響,Golang目前已經(jīng)可以做到1ms以下的STW。
-
GC 的觸發(fā)條件
主動觸發(fā)(手動觸發(fā)),通過調(diào)用 runtime.GC 來觸發(fā)GC,此調(diào)用阻塞式地等待當(dāng)前GC運(yùn)行完畢。
被動觸發(fā),分為兩種方式:- 使用步調(diào)(Pacing)算法,其核心思想是控制內(nèi)存增長的比例,每次內(nèi)存分配時檢查當(dāng)前內(nèi)存分配量是否已達(dá)到閾值(環(huán)境變量GOGC):默認(rèn)100%,即當(dāng)內(nèi)存擴(kuò)大一倍時啟用GC。
- 使用系統(tǒng)監(jiān)控,當(dāng)超過兩分鐘沒有產(chǎn)生任何GC時,強(qiáng)制觸發(fā) GC。
-
Golang為什么小對象多了會造成gc壓力
通常小對象過多會導(dǎo)致GC三色法消耗過多的GPU。優(yōu)化思路是,減少對象分配。
-
GC的屏障介紹
寫屏障(Write Barrier)
- 為了避免GC的過程中新修改的引用關(guān)系到GC的結(jié)果發(fā)生錯誤,我們需要進(jìn)行STW。但是STW會影響程序的性能,所以我們要通過寫屏障技術(shù)盡可能地縮短STW的時間。
寫屏障:并發(fā)gc會產(chǎn)生黑色節(jié)點(diǎn)引用白色節(jié)點(diǎn)情況,導(dǎo)致正常的指針變量錯誤的被清除;解決方法為寫屏障;
主要包括強(qiáng)三色不變式和弱三色不變式;
強(qiáng)三色不變:黑色節(jié)點(diǎn)不能引用白色節(jié)點(diǎn),如果引用白色節(jié)點(diǎn)需要將白色節(jié)點(diǎn)置灰(插入寫屏障);
弱三色不變:黑節(jié)點(diǎn)可以引用白節(jié)點(diǎn),但白節(jié)點(diǎn)有其他灰色節(jié)點(diǎn)或遞歸指向存在灰色節(jié)點(diǎn),刪除白色節(jié)點(diǎn)引用時,需要把白色節(jié)點(diǎn)置灰(刪除寫屏障);
棧上變量較小,且頻繁開辟或刪除,不開啟寫屏障;需要之后一次rescan;
stw時機(jī):
插入寫屏障:結(jié)束時需要STW來重新掃描棧,標(biāo)記棧上引用的白色對象的存活;(1.5版本采用)刪除寫屏障:回收精度低,GC開始時STW掃描堆棧來記錄初始快照,這個過程會保護(hù)開始時刻的所有存活對象;
混合寫屏障:1.8版本加入
原因是stw需要耗時;加入混合寫屏障,解決這個問題;
流程:
1.開始標(biāo)記時候,棧上可達(dá)節(jié)點(diǎn)均置黑,之后不進(jìn)行rescan,不用stw;
2.gc時產(chǎn)生的在棧上創(chuàng)建的對象,均置黑;
3.堆空間刪除的對象置灰;
4.堆空間插入的對象置灰;
特點(diǎn)
-
混合寫屏障繼承了插入寫屏障的優(yōu)點(diǎn),起始無需 STW 打快照,直接并發(fā)掃描垃圾即可;
-
混合寫屏障繼承了刪除寫屏障的優(yōu)點(diǎn),賦值器是黑色賦值器,GC 期間,任何在棧上創(chuàng)建的新對象,均為黑色。掃描過一次就不需要掃描了,這樣就消除了插入寫屏障時期最后 STW 的重新掃描棧;
-
混合寫屏障掃描精度繼承了刪除寫屏障,比插入寫屏障更低,隨著帶來的是 GC 過程全程無 STW;
-
混合寫屏障掃描棧雖然沒有 STW,但是掃描某一個具體的棧的時候,還是要停止這個 goroutine 賦值器的工作的哈(針對一個 goroutine 棧來說,是暫停掃的,要么全灰,要么全黑哈,原子狀態(tài)切換)。
-
GC 的流程是什么
當(dāng)前版本的 Go 以 STW 為界限,可以將 GC 劃分為五個階段:
階段說明賦值器狀態(tài) GCMark 標(biāo)記準(zhǔn)備階段,為并發(fā)標(biāo)記做準(zhǔn)備工作,啟動寫屏障 STWGCMark 掃描標(biāo)記階段,與賦值器并發(fā)執(zhí)行,寫屏障開啟并發(fā)
GCMarkTermination 標(biāo)記終止階段,保證一個周期內(nèi)標(biāo)記任務(wù)完成,停止寫屏障 STWGCoff 內(nèi)存清掃階段,將需要回收的內(nèi)存歸還到堆中,寫屏障關(guān)閉并發(fā)
GCoff 內(nèi)存歸還階段,將過多的內(nèi)存歸還給操作系統(tǒng),寫屏障關(guān)閉并發(fā)
-
GC 如何調(diào)優(yōu)
優(yōu)化內(nèi)存的申請速度,盡可能少申請內(nèi)存,復(fù)用已申請的內(nèi)存。三個關(guān)鍵字:控制、減少、復(fù)用。
通過 go tool pprof 和 go tool trace 等工具
- 控制內(nèi)存分配的速度,限制 goroutine 的數(shù)量,從而提高賦值器對 CPU 的利用率。
- 減少并復(fù)用內(nèi)存,例如使用 sync.Pool 來復(fù)用需要頻繁創(chuàng)建臨時對象,例如提前分配足夠的內(nèi)存來降低多余的拷貝。
- 需要時,增大 GOGC 的值,降低 GC 的運(yùn)行頻率。
1.7 其他知識點(diǎn)
-
new和make的區(qū)別
- make 僅用來分配及初始化類型為 slice、map、chan 的數(shù)據(jù)。
- new 可分配任意類型的數(shù)據(jù),根據(jù)傳入的類型申請一塊內(nèi)存,返回指向這塊內(nèi)存的指針,即類型 *Type。
- make 返回引用,即 Type,new 分配的空間被清零, make 分配空間后,會進(jìn)行初始。
-
go的內(nèi)存分配是怎么樣的
Go 的內(nèi)存分配借鑒了 Google 的 TCMalloc 分配算法,其核心思想是內(nèi)存池 + 多級對象管理。內(nèi)存池主要是預(yù)先分配內(nèi)存,減少向系統(tǒng)申請的頻率;多級對象有:mheap、mspan、arenas、mcentral、mcache。它們以 mspan 作為基本分配單位。具體的分配邏輯如下:
- 當(dāng)要分配大于 32K 的對象時,從 mheap 分配。
- 當(dāng)要分配的對象小于等于 32K 大于 16B 時,從 P 上的 mcache 分配,如果 mcache 沒有內(nèi)存,則從 mcentral 獲取,如果 mcentral 也沒有,則向 mheap 申請,如果 mheap 也沒有,則從操作系統(tǒng)申請內(nèi)存。
- 當(dāng)要分配的對象小于等于 16B 時,從 mcache 上的微型分配器上分配。
-
競態(tài)、內(nèi)存逃逸
競態(tài)
資源競爭,就是在程序中,同一塊內(nèi)存同時被多個 goroutine 訪問。我們使用 go build、go run、go test 命令時,添加 -race 標(biāo)識可以檢查代碼中是否存在資源競爭。
解決這個問題,我們可以給資源進(jìn)行加鎖,讓其在同一時刻只能被一個協(xié)程來操作。
- sync.Mutex
- sync.RWMutex
逃逸分析
「逃逸分析」就是程序運(yùn)行時內(nèi)存的分配位置(?;蚨?,是由編譯器來確定的。堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價是分配速度較慢,而且會形成內(nèi)存碎片。
在 Go 里變量的內(nèi)存分配方式則是由編譯器來決定的。如果變量在作用域(比如函數(shù)范圍)之外,還會被引用的話,那么稱之為發(fā)生了逃逸行為,此時將會把對象放到堆上,即使聲明為值類型;如果沒有發(fā)生逃逸行為的話,則會被分配到棧上,即使 new 了一個對象。
逃逸場景:
- 指針逃逸
- 棧空間不足逃逸
- 動態(tài)類型逃逸
- 閉包引用對象逃逸
-
什么是 rune 類型
rune 類型,代表一個 UTF-8 字符,當(dāng)需要處理中文、日文或者其他復(fù)合字符時,則需要用到 rune 類型。rune 類型等價于 int32 類型。
-
go語言觸發(fā)異常的場景有哪些
-
空指針解析
-
下標(biāo)越界
-
除數(shù)為0
-
調(diào)用panic函數(shù)
-
-
go的接口
Go 語言提供了另外一種數(shù)據(jù)類型即接口,它把所有的具有共性的方法定義在一起,任何其他類型只要實現(xiàn)了這些方法就是實現(xiàn)了這個接口。
接口可以讓我們將不同的類型綁定到一組公共的方法上,從而實現(xiàn)多態(tài)和靈活的設(shè)計。
Go 語言中的接口是隱式實現(xiàn)的,也就是說,如果一個類型實現(xiàn)了一個接口定義的所有方法,那么它就自動地實現(xiàn)了該接口。因此,我們可以通過將接口作為參數(shù)來實現(xiàn)對不同類型的調(diào)用,從而實現(xiàn)多態(tài)。
-
相比較于其他語言, Go 有什么優(yōu)勢或者特點(diǎn)
-
Go代碼的設(shè)計是務(wù)實的。每個功能和語法決策都旨在讓程序員的生活更輕松。
-
Golang 針對并發(fā)進(jìn)行了優(yōu)化,并且在規(guī)模上運(yùn)行良好。
-
由于單一的標(biāo)準(zhǔn)代碼格式,Golang 通常被認(rèn)為比其他語言更具可讀性。
-
自動垃圾收集明顯比 Java 或 Python 更有效,因為它與程序同時執(zhí)行。
-
-
defer、panic、recover 三者的用法
defer 函數(shù)調(diào)用的順序是后進(jìn)先出,當(dāng)產(chǎn)生 panic 的時候,會先執(zhí)行 panic 前面的 defer 函數(shù)后才真的拋出異常。一般的,recover 會在 defer 函數(shù)里執(zhí)行并捕獲異常,防止程序崩潰。
-
Go反射
介紹
Go語言提供了一種機(jī)制在運(yùn)行時更新和檢查變量的值、調(diào)用變量的方法和變量支持的內(nèi)在操作,但是在編譯時并不知道這些變量的具體類型,這種機(jī)制被稱為反射。反射也可以讓我們將類型本身作為第一類的值類型處理。
反射是指在程序運(yùn)行期對程序本身進(jìn)行訪問和修改的能力,程序在編譯時變量被轉(zhuǎn)換為內(nèi)存地址,變量名不會被編譯器寫入到可執(zhí)行部分,在運(yùn)行程序時程序無法獲取自身的信息。
Go語言中的反射是由 reflect 包提供支持的,它定義了兩個重要的類型 Type 和 Value 任意接口值在反射中都可以理解為由 reflect.Type 和 reflect.Value 兩部分組成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 兩個函數(shù)來獲取任意對象的 Value 和 Type。
反射三定律
- 反射第一定律:反射可以將interface類型變量轉(zhuǎn)換成反射對象
- 反射第二定律:反射可以將反射對象還原成interface對象
- 反射第三定律:反射對象可修改,value值必須是可設(shè)置的
-
Go語言函數(shù)傳參是值類型還是引用類型
- 在Go語言中只存在值傳遞,要么是值的副本,要么是指針的副本。無論是值類型的變量還是引用類型的變量亦或是指針類型的變量作為參數(shù)傳遞都會發(fā)生值拷貝,開辟新的內(nèi)存空間。
- 另外值傳遞、引用傳遞和值類型、引用類型是兩個不同的概念,不要混淆了。引用類型作為變量傳遞可以影響到函數(shù)外部是因為發(fā)生值拷貝后新舊變量指向了相同的內(nèi)存地址。
-
Go語言中的內(nèi)存對齊
CPU 訪問內(nèi)存時,并不是逐個字節(jié)訪問,而是以字長(word size)為單位訪問。比如 32 位的 CPU ,字長為 4 字節(jié),那么 CPU 訪問內(nèi)存的單位也是 4 字節(jié)。
CPU 始終以字長訪問內(nèi)存,如果不進(jìn)行內(nèi)存對齊,很可能增加 CPU 訪問內(nèi)存的次數(shù),例如:
變量 a、b 各占據(jù) 3 字節(jié)的空間,內(nèi)存對齊后,a、b 占據(jù) 4 字節(jié)空間,CPU 讀取 b 變量的值只需要進(jìn)行一次內(nèi)存訪問。如果不進(jìn)行內(nèi)存對齊,CPU 讀取 b 變量的值需要進(jìn)行 2 次內(nèi)存訪問。第一次訪問得到 b 變量的第 1 個字節(jié),第二次訪問得到 b 變量的后兩個字節(jié)。
內(nèi)存對齊對實現(xiàn)變量的原子性操作也是有好處的,每次內(nèi)存訪問是原子的,如果變量的大小不超過字長,那么內(nèi)存對齊后,對該變量的訪問就是原子的,這個特性在并發(fā)場景下至關(guān)重要。
簡言之:合理的內(nèi)存對齊可以提高內(nèi)存讀寫的性能,并且便于實現(xiàn)變量操作的原子性。
-
空 struct{} 的用途
因為空結(jié)構(gòu)體不占據(jù)內(nèi)存空間,因此被廣泛作為各種場景下的占位符使用。
- 將 map 作為集合(Set)使用時,可以將值類型定義為空結(jié)構(gòu)體,僅作為占位符使用即可。
- 不發(fā)送數(shù)據(jù)的信道(channel)
使用 channel 不需要發(fā)送任何的數(shù)據(jù),只用來通知子協(xié)程(goroutine)執(zhí)行任務(wù),或只用來控制協(xié)程并發(fā)度。 - 結(jié)構(gòu)體只包含方法,不包含任何的字段
-
值傳遞和地址傳遞(引用傳遞)
Go 語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因為拷貝的內(nèi)容有時候是非引用類型(int、string、struct 等這些),這樣就在函數(shù)中就無法修改原內(nèi)容數(shù)據(jù);有的是引用類型(指針、map、slice、chan等 這些),這樣就可以修改原內(nèi)容數(shù)據(jù)。
Golang 的引用類型包括 slice、map 和 channel。它們有復(fù)雜的內(nèi)部結(jié)構(gòu),除了申請內(nèi)存外,還需要初始化相關(guān)屬性。內(nèi)置函數(shù) new 計算類型大小,為其分配零值內(nèi)存,返回指針。而 make 會被編譯器翻譯成具體的創(chuàng)建函數(shù),由其分 配內(nèi)存和初始化成員結(jié)構(gòu),返回對象而非指針。
-
原子操作
一個或者多個操作在 CPU 執(zhí)行過程中不被中斷的特性,稱為原子性 (atomicity)。
這些操作對外表現(xiàn)成一個不可分割的整體,他們要么都執(zhí)行,要么都不執(zhí)行,外界不會看到他們只執(zhí)行到一半的狀態(tài)。而在現(xiàn)實世界中,CPU不可能不中斷的執(zhí)行一系列操作,但如果我們在執(zhí)行多個操作時,能讓他們的中間狀態(tài)對外不可見,那我們就可以宣城他們擁有了“不可分割”的原子性。
在 Go 中,一條普通的賦值語句其實不是一個原子操作。列如,在 32 位機(jī)器上寫 int64 類型的變量就會有中間狀態(tài),因為他會被拆成兩次寫操作(MOV)——寫低 32 位和寫高 32 位。
2. 掌握Web框架Gin和微服務(wù)框架Micro
2.1 Gin框架
-
什么是Gin框架
Gin是一個用Go語言編寫的web框架,,優(yōu)點(diǎn)是封裝比較好,API友好,源碼注釋比較明確,具有快速靈活,容錯方便等特點(diǎn)。它具有運(yùn)行速度快,分組的路由器,良好的崩潰捕獲和錯誤處理,非常好的支持中間件和 json。
-
Gin路由的實現(xiàn)
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前綴的樹結(jié)構(gòu),它基本上是一個緊湊的Trie tree(或者只是Radix Tree)。具有公共前綴的節(jié)點(diǎn)也共享一個公共父節(jié)點(diǎn)。
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前綴的樹結(jié)構(gòu),它基本上是一個緊湊的Trie tree(或者只是Radix Tree)。具有公共前綴的節(jié)點(diǎn)也共享一個公共父節(jié)點(diǎn)。
路由樹是由一個個節(jié)點(diǎn)構(gòu)成的,gin框架路由樹的節(jié)點(diǎn)由
node
結(jié)構(gòu)體表示在gin的路由中,每一個
HTTP Method
(GET、POST、PUT、DELETE…)都對應(yīng)了一棵radix tree
,我們注冊路由的時候會調(diào)用addRoute`函數(shù)注冊路由的邏輯主要有
addRoute
函數(shù)和insertChild
方法。? 路由樹構(gòu)造的詳細(xì)過程:
- 第一次注冊路由,例如注冊search
- 繼續(xù)注冊一條沒有公共前綴的路由,例如blog
- 注冊一條與先前注冊的路由有公共前綴的路由,例如support
路由樹構(gòu)造的詳細(xì)過程:
- 第一次注冊路由,例如注冊search
- 繼續(xù)注冊一條沒有公共前綴的路由,例如blog
- 注冊一條與先前注冊的路由有公共前綴的路由,例如support
路由匹配是由節(jié)點(diǎn)的
getValue
方法實現(xiàn)的。getValue
根據(jù)給定的路徑(鍵)返回nodeValue
值,保存注冊的處理函數(shù)和匹配到的路徑參數(shù)數(shù)據(jù)。gin框架路由使用前綴樹,路由注冊的過程是構(gòu)造前綴樹的過程,路由匹配的過程就是查找前綴樹的過程。
-
Gin的中間件
Gin框架允許開發(fā)者在處理請求的過程中,加入用戶自己的鉤子(Hook)函數(shù)。這個鉤子函數(shù)就叫中間件,中間件適合處理一些公共的業(yè)務(wù)邏輯,比如登錄認(rèn)證、權(quán)限校驗、數(shù)據(jù)分頁、記錄日志、耗時統(tǒng)計等。
gin框架涉及中間件相關(guān)有4個常用的方法,它們分別是
c.Next()
、c.Abort()
、c.Set()
、c.Get()
。gin框架的中間件函數(shù)和處理函數(shù)是以切片形式的調(diào)用鏈條存在的,我們可以順序調(diào)用也可以借助
c.Next()
方法實現(xiàn)嵌套調(diào)用。借助
c.Set()
和c.Get()
方法我們能夠在不同的中間件函數(shù)中傳遞數(shù)據(jù)。gin默認(rèn)中間件
gin.Default()默認(rèn)使用了Logger和Recovery中間件,其中:Logger中間件將日志寫入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中間件會recover任何panic。如果有panic的話,會寫入500響應(yīng)碼。
如果不想使用上面兩個默認(rèn)的中間件,可以使用gin.New()新建一個沒有任何默認(rèn)中間件的路由。
2.2 Micro框架
-
對微服務(wù)的了解
使用一套小服務(wù)來開發(fā)單個應(yīng)用的方式,每個服務(wù)運(yùn)行在獨(dú)立的進(jìn)程里,一般采用輕量級的通訊機(jī)制互聯(lián),并且它們可以通過自動化的方式部署
微服務(wù)特點(diǎn)
- 單一職責(zé),此時項目專注于登錄和注冊
- 輕量級的通信,通信與平臺和語言無關(guān),http是輕量的
- 隔離性,數(shù)據(jù)隔離
- 有自己的數(shù)據(jù)
- 技術(shù)多樣性
-
微服務(wù)架構(gòu)的優(yōu)勢和缺點(diǎn)
優(yōu)點(diǎn)
1、易于開發(fā)和維護(hù)
2、啟動較快
3、局部修改容易部署
4、技術(shù)棧不受限
5、按需伸縮
缺點(diǎn)
1、運(yùn)維要求較高
2、分布式的復(fù)雜性
3、接口調(diào)整成本高
4、重復(fù)勞動
-
RPC協(xié)議
- 遠(yuǎn)程過程調(diào)用(Remote Procedure Call,RPC)是一個計算機(jī)通信協(xié)議
- 該協(xié)議允許運(yùn)行于一臺計算機(jī)的程序調(diào)用另一臺計算機(jī)的子程序,而程序員無需額外地為這個交互作用編程
- 如果涉及的軟件采用面向?qū)ο缶幊?#xff0c;那么遠(yuǎn)程過程調(diào)用亦可稱作遠(yuǎn)程調(diào)用或遠(yuǎn)程方法調(diào)用
RPC調(diào)用流程
微服務(wù)架構(gòu)下數(shù)據(jù)交互一般是對內(nèi) RPC,對外 REST
將業(yè)務(wù)按功能模塊拆分到各個微服務(wù),具有提高項目協(xié)作效率、降低模塊耦合度、提高系統(tǒng)可用性等優(yōu)點(diǎn),但是開發(fā)門檻比較高,比如 RPC 框架的使用、后期的服務(wù)監(jiān)控等工作
一般情況下,我們會將功能代碼在本地直接調(diào)用,微服務(wù)架構(gòu)下,我們需要將這個函數(shù)作為單獨(dú)的服務(wù)運(yùn)行,客戶端通過網(wǎng)絡(luò)調(diào)用流行RPC框架:Dubbo、Motan、Thrift、gRPC
-
gRPC介紹
gRPC由google開發(fā),是一款語言中立、平臺中立、開源的遠(yuǎn)程過程調(diào)用系統(tǒng)
gRPC 是一個高性能、開源、通用的RPC框架,基于HTTP2協(xié)議標(biāo)準(zhǔn)設(shè)計開發(fā),默認(rèn)采用Protocol Buffers數(shù)據(jù)序列化協(xié)議,支持多種開發(fā)語言。gRPC提供了一種簡單的方法來精確的定義服務(wù),并且為客戶端和服務(wù)端自動生成可靠的功能庫。
在gRPC客戶端可以直接調(diào)用不同服務(wù)器上的遠(yuǎn)程程序,使用起來就像調(diào)用本地程序一樣,很容易去構(gòu)建分布式應(yīng)用和服務(wù)。和很多RPC系統(tǒng)一樣,服務(wù)端負(fù)責(zé)實現(xiàn)定義好的接口并處理客戶端的請求,客戶端根據(jù)接口描述直接調(diào)用需要的服務(wù)??蛻舳撕头?wù)端可以分別使用gRPC支持的不同語言實現(xiàn)。
gRPC主要特性
- 強(qiáng)大的IDL
gRPC使用ProtoBuf來定義服務(wù),ProtoBuf是由Google開發(fā)的一種數(shù)據(jù)序列化協(xié)議(類似于XML、JSON、hessian)。ProtoBuf能夠?qū)?shù)據(jù)進(jìn)行序列化,并廣泛應(yīng)用在數(shù)據(jù)存儲、通信協(xié)議等方面。
- 多語言支持
gRPC支持多種語言,并能夠基于語言自動生成客戶端和服務(wù)端功能庫。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它語言的版本正在積極開發(fā)中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等語言,grpc-java已經(jīng)支持Android開發(fā)。
- HTTP2
gRPC基于HTTP2標(biāo)準(zhǔn)設(shè)計,所以相對于其他RPC框架,gRPC帶來了更多強(qiáng)大功能,如雙向流、頭部壓縮、多復(fù)用請求等。這些功能給移動設(shè)備帶來重大益處,如節(jié)省帶寬、降低TCP鏈接次數(shù)、節(jié)省CPU使用和延長電池壽命等。同時,gRPC還能夠提高了云端服務(wù)和Web應(yīng)用的性能。gRPC既能夠在客戶端應(yīng)用,也能夠在服務(wù)器端應(yīng)用,從而以透明的方式實現(xiàn)客戶端和服務(wù)器端的通信和簡化通信系統(tǒng)的構(gòu)建。
-
Protobuf介紹
Google Protocol Buffer( 簡稱 Protobuf) 是 Google 公司內(nèi)部的混合語言數(shù)據(jù)標(biāo)準(zhǔn),目前已經(jīng)正在使用的有超過 48,162 種報文格式定義和超過 12,183 個 .proto 文件。他們用于 RPC 系統(tǒng)和持續(xù)數(shù)據(jù)存儲系統(tǒng)。Protocol Buffers 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,可以用于結(jié)構(gòu)化數(shù)據(jù)串行化、或者說序列化。它很適合做數(shù)據(jù)存儲或RPC數(shù)據(jù)交換格式??梢杂糜诩磿r通訊、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)格式
protobuf的核心內(nèi)容包括:
-
定義消息:消息的結(jié)構(gòu)體,以message標(biāo)識。
-
定義接口:接口路徑和參數(shù),以service標(biāo)識。
通過protobuf提供的機(jī)制,服務(wù)端與服務(wù)端之間只需要關(guān)注接口方法名(service)和參數(shù)(message)即可通信,不需關(guān)注繁瑣的鏈路協(xié)議和字段解析,極大降低服務(wù)端的設(shè)計開發(fā)成本。
-
-
Micro介紹和主要功能
go-micro簡介
- Go Micro是一個插件化的基礎(chǔ)框架,基于此可以構(gòu)建微服務(wù),Micro的設(shè)計哲學(xué)是可插拔的插件化架構(gòu)
- 在架構(gòu)之外,它默認(rèn)實現(xiàn)了consul作為服務(wù)發(fā)現(xiàn),通過http進(jìn)行通信,通過protobuf和json進(jìn)行編解碼
- 是用來構(gòu)建和管理分布式程序的系統(tǒng)
- Runtime (運(yùn)行時) : 用來管理配置,認(rèn)證,網(wǎng)絡(luò)等
- Framework (程序開發(fā)框架) : 用來方便編寫微服務(wù)
- Clients (多語言客戶端) : 支持多語言訪問服務(wù)端
go-micro的主要功能
-
服務(wù)發(fā)現(xiàn):自動服務(wù)注冊和名稱解析。
-
負(fù)載均衡:基于服務(wù)發(fā)現(xiàn)構(gòu)建的客戶端負(fù)載均衡。
-
消息編碼:基于內(nèi)容類型的動態(tài)消息編碼。
-
請求/響應(yīng):基于RPC的請求/響應(yīng),支持雙向流。
-
Async Messaging:PubSub是異步通信和事件驅(qū)動架構(gòu)的一流公民。
-
可插拔接口:Go Micro為每個分布式系統(tǒng)抽象使用Go接口,因此,這些接口是可插拔的,并允許Go Micro與運(yùn)行時無關(guān),可以插入任何基礎(chǔ)技術(shù)
go-micro特性
- api: api 網(wǎng)關(guān)。使用服務(wù)發(fā)現(xiàn)具有動態(tài)請求路由的單個入口點(diǎn). API 網(wǎng)關(guān)允許您在后端構(gòu)建可擴(kuò)展的微服務(wù)體系結(jié)構(gòu),并在前端合并公共 api. micro api 通過發(fā)現(xiàn)和可插拔處理程序提供強(qiáng)大的路由,為 http, grpc, Websocket, 發(fā)布事件等提供服務(wù).
- broker: 允許異步消息的消息代理。微服務(wù)是事件驅(qū)動的體系結(jié)構(gòu),應(yīng)該作為一等公民提供消息傳遞。通知其他服務(wù)的事件,而無需擔(dān)心響應(yīng).
- network: 通過微網(wǎng)絡(luò)服務(wù)構(gòu)建多云網(wǎng)絡(luò)。只需跨任何環(huán)境連接網(wǎng)絡(luò)服務(wù),創(chuàng)建單個平面網(wǎng)絡(luò)即可全局路由. Micro 的網(wǎng)絡(luò)根據(jù)每個數(shù)據(jù)中心中的本地注冊表動態(tài)構(gòu)建路由,確保根據(jù)本地設(shè)置路由查詢.
- new: 服務(wù)模板生成器。創(chuàng)建新的服務(wù)模板以快速入門. Micro 提供用于編寫微服務(wù)的預(yù)定義模板。始終以相同的方式啟動,構(gòu)建相同的服務(wù)以提高工作效率.
- proxy: 建立在 Go Micro 上的透明服務(wù)代理。將服務(wù)發(fā)現(xiàn),負(fù)載平衡,容錯,消息編碼,中間件,監(jiān)視等卸載到單個位置。獨(dú)立運(yùn)行它或與服務(wù)一起運(yùn)行.
- registry: 注冊表提供服務(wù)發(fā)現(xiàn)以查找其他服務(wù),存儲功能豐富的元數(shù)據(jù)和終結(jié)點(diǎn)信息。它是一個服務(wù)資源管理器,允許您在運(yùn)行時集中和動態(tài)地存儲此信息.
- store: 有狀態(tài)是任何系統(tǒng)的必然需求。我們提供密鑰值存儲,提供簡單的狀態(tài)存儲,可在服務(wù)之間共享或長期卸載 m 以保持微服務(wù)無狀態(tài)和水平可擴(kuò)展.
- web: Web 儀表板允許您瀏覽服務(wù),描述其終結(jié)點(diǎn),請求和響應(yīng)格式,甚至直接查詢它們。儀表板還包括內(nèi)置 CLI 的體驗,適用于希望動態(tài)進(jìn)入終端的開發(fā)人員.
-
Micro通信流程
Server監(jiān)聽客戶端的調(diào)用,和Brocker推送過來的信息進(jìn)行處理。并且Server端需要向Register注冊自己的存在或消亡,這樣Client才能知道自己的狀態(tài)
Register服務(wù)的注冊的發(fā)現(xiàn),Client端從Register中得到Server的信息,然后每次調(diào)用都根據(jù)算法選擇一個的Server進(jìn)行通信,當(dāng)然通信是要經(jīng)過編碼/解碼,選擇傳輸協(xié)議等一系列過程的
如果有需要通知所有的Server端可以使用Brocker進(jìn)行信息的推送,Brocker 信息隊列進(jìn)行信息的接收和發(fā)布 -
consul
Consul是用于實現(xiàn)分布式系統(tǒng)的服務(wù)發(fā)現(xiàn)與配置,Consul是分布式的、高可用的、可橫向擴(kuò)展的。
注冊中心Consul關(guān)鍵功能
服務(wù)發(fā)現(xiàn):客戶端可以注冊服務(wù),程序可以輕松找到它們所依賴的服務(wù)
運(yùn)行狀況檢查:Consul客戶端可以提供任意數(shù)量的運(yùn)行狀況檢查
KV 存儲:應(yīng)用程序可以將Consul的層級鍵/值存儲用于任何目的,包括動態(tài)配置,功能標(biāo)記,協(xié)調(diào),領(lǐng)導(dǎo)者選舉等
安全服務(wù)通信:Consul 可以為服務(wù)生成和分發(fā)TLS證書,建立相互的TLS連接
多數(shù)據(jù)中心:Consul 支持多個數(shù)據(jù)中心
注冊中心Consul兩個重要協(xié)議-
Gossip Protocol (八卦協(xié)議)
-
Raft Protocol ( 選舉協(xié)議)
-
-
Jaeger
什么是鏈路追蹤:
分布式鏈路追蹤就是將一次分布式請求還原成調(diào)用鏈路,將一次分布式請求的調(diào)用情況集中展示,比如各個服務(wù)節(jié)點(diǎn)上的耗時、請求具體到達(dá)哪臺機(jī)器上、每個服務(wù)節(jié)點(diǎn)的請求狀態(tài)等等
鏈路追蹤主要功能:-
故障快速定位:可以通過調(diào)用鏈結(jié)合業(yè)務(wù)日志快速定位錯誤信息
-
鏈路性能可視化:各個階段鏈路耗時、服務(wù)依賴關(guān)系可以通過可視化界面展現(xiàn)出來
-
鏈路分析:通過分析鏈路耗時、服務(wù)依賴關(guān)系可以得到用戶的行為路徑,匯總分析應(yīng)用在很多業(yè)務(wù)場景
jaeger鏈路追蹤作用
- 它是用來監(jiān)視和診斷基于微服務(wù)的分布式系統(tǒng)
- 用于服務(wù)依賴性分析,輔助性能優(yōu)化
Jaeger組成
Jaeger Client - 為不同語言實現(xiàn)了符合 OpenTracing 標(biāo)準(zhǔn)的 SDK。應(yīng)用程序通過 API 寫入數(shù)據(jù),client library 把 trace 信息按照應(yīng)用程序指定的采樣策略傳遞給 jaeger-agent。
Agent - 它是一個監(jiān)聽在 UDP 端口上接收 span 數(shù)據(jù)的網(wǎng)絡(luò)守護(hù)進(jìn)程,它會將數(shù)據(jù)批量發(fā)送給 collector。它被設(shè)計成一個基礎(chǔ)組件,部署到所有的宿主機(jī)上。Agent 將 client library 和 collector 解耦,為 client library 屏蔽了路由和發(fā)現(xiàn) collector 的細(xì)節(jié)。
Collector - 接收 jaeger-agent 發(fā)送來的數(shù)據(jù),然后將數(shù)據(jù)寫入后端存儲。Collector 被設(shè)計成無狀態(tài)的組件,因此您可以同時運(yùn)行任意數(shù)量的 jaeger-collector。
Data Store - 后端存儲被設(shè)計成一個可插拔的組件,支持將數(shù)據(jù)寫入 cassandra、elastic search。
Query - 接收查詢請求,然后從后端存儲系統(tǒng)中檢索 trace 并通過 UI 進(jìn)行展示。Query 是無狀態(tài)的,您可以啟動多個實例,把它們部署在 nginx 這樣的負(fù)載均衡器后面。
分布式追蹤系統(tǒng)發(fā)展很快,種類繁多,但核心步驟一般有三個:代碼埋點(diǎn),數(shù)據(jù)存儲、查詢展示
-
-
Prometheus
promethues介紹
- 是一套開源的監(jiān)控&報警&時間序列數(shù)據(jù)庫的組合
- 基本原理是通過HTTP協(xié)議周期性抓取被監(jiān)控組件的狀態(tài)
- 適合Docker、 Kubernetes環(huán)境的監(jiān)控系統(tǒng)
promethues工作流程
-
Prometheus server定期從配置好的jobs/exporters/Pushgateway中拉數(shù)據(jù)
-
Prometheus server記錄數(shù)據(jù)并且根據(jù)報警規(guī)則推送alert數(shù)據(jù)
-
Alertmanager 根據(jù)配置文件,對接收到的警報進(jìn)行處理,發(fā)出告警。
-
在圖形界面中,可視化采集數(shù)據(jù)
promethues重要組件
-
Prometheus Server:用于收集和存儲時間序列數(shù)據(jù)。
-
Client Library:客戶端庫成相應(yīng)的metrics并暴露給Prometheus server
-
Push Gateway:主要用于短期的jobs
-
Exporters: 用于暴露已有的第三方服務(wù)的metrics給Prometheus
-
Alertmanager: 從Prometheus server端接收到alerts后,會進(jìn)行
grafana看板
- 擁有 豐富dashboard和圖表編輯的指標(biāo)分析平臺
- 擁有自己的權(quán)限管理和用戶管理系統(tǒng)
- Grafana 更適合用于數(shù)據(jù)可視化展示
-
熔斷降級、限流、負(fù)載均衡
-
熔斷降級
服務(wù)熔斷也稱服務(wù)隔離或過載保護(hù)。在微服務(wù)應(yīng)用中,服務(wù)存在一定的依賴關(guān)系,形成一定的依賴鏈,如果某個目標(biāo)服務(wù)調(diào)用慢或者有大量超時,造成服務(wù)不可用,間接導(dǎo)致其他的依賴服務(wù)不可用,最嚴(yán)重的可能會阻塞整條依賴鏈,最終導(dǎo)致業(yè)務(wù)系統(tǒng)崩潰(又稱雪崩效應(yīng))。此時,對該服務(wù)的調(diào)用執(zhí)行熔斷,對于后續(xù)請求,不再繼續(xù)調(diào)用該目標(biāo)服務(wù),而是直接返回,從而可以快速釋放資源。等到目標(biāo)服務(wù)情況好轉(zhuǎn)后,則可恢復(fù)其調(diào)用。
關(guān)閉 (Closed):在這種狀態(tài)下,我們需要一個計數(shù)器來記錄調(diào)用失敗的次數(shù)和總的請求次數(shù),如果在某個時間窗口內(nèi),失敗的失敗率達(dá)到預(yù)設(shè)的閾值,則切換到斷開狀態(tài),此時開啟一個超時時間,當(dāng)?shù)竭_(dá)該時間則切換到半關(guān)閉狀態(tài),該超時時間是給了系統(tǒng)一次機(jī)會來修正導(dǎo)致調(diào)用失敗的錯誤,以回到正常的工作狀態(tài)。在關(guān)閉狀態(tài)下,調(diào)用錯誤是基于時間的,在特定的時間間隔內(nèi)會重置,這能夠防止偶然錯誤導(dǎo)致熔斷器進(jìn)去斷開狀態(tài)
打開 (Open):在該狀態(tài)下,發(fā)起請求時會立即返回錯誤,一般會啟動一個超時計時器,當(dāng)計時器超時后,狀態(tài)切換到半打開狀態(tài),也可以設(shè)置一個定時器,定期的探測服務(wù)是否恢復(fù)
半打開 (Half-Open):在該狀態(tài)下,允許應(yīng)用程序一定數(shù)量的請求發(fā)往被調(diào)用服務(wù),如果這些調(diào)用正常,那么可以認(rèn)為被調(diào)用服務(wù)已經(jīng)恢復(fù)正常,此時熔斷器切換到關(guān)閉狀態(tài),同時需要重置計數(shù)。如果這部分仍有調(diào)用失敗的情況,則認(rèn)為被調(diào)用方仍然沒有恢復(fù),熔斷器會切換到關(guān)閉狀態(tài),然后重置計數(shù)器,半打開狀態(tài)能夠有效防止正在恢復(fù)中的服務(wù)被突然大量請求再次打垮。
常見的有三種熔斷降級策略
- 錯誤比例:在所設(shè)定的時間窗口內(nèi),調(diào)用的訪問錯誤比例大于所設(shè)置的閾值,則對接下來訪問的請求進(jìn)行自動熔斷。
- 錯誤計數(shù):在所設(shè)定的時間窗口內(nèi),調(diào)用的訪問錯誤次數(shù)大于所設(shè)置的閾值,則對接下來訪問的請求進(jìn)行自動熔斷。
- 慢調(diào)用比例:在所設(shè)定的時間窗口內(nèi),慢調(diào)用的比例大于所設(shè)置的閾值,則對接下來訪問的請求進(jìn)行自動熔斷。
服務(wù)降級
當(dāng)下游的服務(wù)因為某種原因響應(yīng)過慢,下游服務(wù)主動停掉一些不太重要的業(yè)務(wù),釋放出服務(wù)器資源,增加響應(yīng)速度。
關(guān)于降級,這里有兩種場景:
- 當(dāng)下游的服務(wù)因為某種原因響應(yīng)過慢,下游服務(wù)主動停掉一些不太重要的業(yè)務(wù),釋放出服務(wù)器資源,增加響應(yīng)速度!
- 當(dāng)下游的服務(wù)因為某種原因不可用,上游主動調(diào)用本地的一些降級邏輯,避免卡頓,迅速返回給用戶!
-
限流
在微服務(wù)架構(gòu)下,若大量請求超過微服務(wù)的處理能力時,可能會將服務(wù)打跨,甚至產(chǎn)生雪崩效應(yīng)、影響系統(tǒng)的整體穩(wěn)定性。比如說你的用戶服務(wù)處理能力是1w/s,現(xiàn)在因為異常流量或其他原因,有10w的并發(fā)請求訪問你的服務(wù),那你的服務(wù)肯定扛不住啊。這種情況下,我們可以在流量超出承受閾值時,直接進(jìn)行”限流”、拒絕部分請求,從而保證系統(tǒng)的整體穩(wěn)定性。
限流算法
固定時間窗口
基于固定時間窗口的限流算法是非常簡單的。首先需要選定一個時間起點(diǎn),之后每次接口請求到來都累加計數(shù)器,如果在當(dāng)前時間窗口內(nèi),根據(jù)限流規(guī)則(比如每秒鐘最大允許 100 次接口請求),累加訪問次數(shù)超過限流值,則限流熔斷拒絕接口請求。當(dāng)進(jìn)入下一個時間窗口之后,計數(shù)器清零重新計數(shù)。
滑動時間窗口算法
滑動時間窗口算法是對固定時間窗口算法的一種改進(jìn),流量經(jīng)過滑動時間窗口算法整形之后,可以保證任意時間窗口內(nèi),都不會超過最大允許的限流值,從流量曲線上來看會更加平滑,可以部分解決上面提到的臨界突發(fā)流量問題。對比固定時間窗口限流算法,滑動時間窗口限流算法的時間窗口是持續(xù)滑動的,并且除了需要一個計數(shù)器來記錄時間窗口內(nèi)接口請求次數(shù)之外,還需要記錄在時間窗口內(nèi)每個接口請求到達(dá)的時間點(diǎn),對內(nèi)存的占用會比較多。
漏桶和令牌桶算法
漏桶算法(Leaky Bucket):主要目的是控制數(shù)據(jù)注入到網(wǎng)絡(luò)的速率,平滑網(wǎng)絡(luò)上的突發(fā)流量。漏桶算法提供了一種機(jī)制,通過它,突發(fā)流量可以被整形以便為網(wǎng)絡(luò)提供一個穩(wěn)定的流量。
請求先進(jìn)入到漏桶里,漏桶以一定的速度出水,當(dāng)水請求過大會直接溢出,可以看出漏桶算法能強(qiáng)行限制數(shù)據(jù)的傳輸速率。
令牌桶算法(Token Bucket):是網(wǎng)絡(luò)流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發(fā)送到網(wǎng)絡(luò)上的數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送。
大小固定的令牌桶可自行以恒定的速率源源不斷地產(chǎn)生令牌。如果令牌不被消耗,或者被消耗的速度小于產(chǎn)生的速度,令牌就會不斷地增多,直到把桶填滿。后面再產(chǎn)生的令牌就會從桶中溢出。最后桶中可以保存的最大令牌數(shù)永遠(yuǎn)不會超過桶的大小。
漏桶和令牌桶算法的區(qū)別
令牌桶算法,主要放在服務(wù)端,用來保護(hù)服務(wù)端(自己),主要用來對調(diào)用者頻率進(jìn)行限流,為的是不讓自己被壓垮。所以如果自己本身有處理能力的時候,如果流量突發(fā)(實際消費(fèi)能力強(qiáng)于配置的流量限制=桶大小),那么實際處理速率可以超過配置的限制(桶大小)。
而漏桶算法,主要放在調(diào)用方,這是用來保護(hù)他人,也就是保護(hù)他所調(diào)用的系統(tǒng)。主要場景是,當(dāng)調(diào)用的第三方系統(tǒng)本身沒有保護(hù)機(jī)制,或者有流量限制的時候,我們的調(diào)用速度不能超過他的限制,由于我們不能更改第三方系統(tǒng),所以只有在主調(diào)方控制。這個時候,即使流量突發(fā),也必須舍棄。因為消費(fèi)能力是第三方?jīng)Q定的。自適應(yīng)限流
一般的限流常常需要指定一個固定值(qps)作為限流開關(guān)的閾值,這個值一是靠經(jīng)驗判斷,二是靠通過大量的測試數(shù)據(jù)得出。但這個閾值,在流量激增、系統(tǒng)自動伸縮或者某某commit了一段有毒代碼后就有可能變得不那么合適了。并且一般業(yè)務(wù)方也不太能夠正確評估自己的容量,去設(shè)置一個合適的限流閾值。那么我們就可以考慮用自適應(yīng)限流來解決這個問題。
對于自適應(yīng)限流來說, 一般都是結(jié)合系統(tǒng)的 Load、CPU 使用率以及應(yīng)用的入口 QPS、平均響應(yīng)時間和并發(fā)量等幾個維度的監(jiān)控指標(biāo),通過自適應(yīng)的流控策略, 讓系統(tǒng)的入口流量和系統(tǒng)的負(fù)載達(dá)到一個平衡,讓系統(tǒng)盡可能跑在最大吞吐量的同時保證系統(tǒng)整體的穩(wěn)定。
分布式限流
上面使用的限流算法,都是基本單節(jié)點(diǎn)限流的。但線上業(yè)務(wù)出于各種原因考慮,多是分布式系統(tǒng),單節(jié)點(diǎn)的限流僅能保護(hù)自身節(jié)點(diǎn),但無法保護(hù)應(yīng)用依賴的各種服務(wù),并且在進(jìn)行節(jié)點(diǎn)擴(kuò)容、縮容時也無法準(zhǔn)確控制整個服務(wù)的請求限制。比如說我希望某個接口的QPS的1000次/秒,服務(wù)部署在5臺機(jī)器上,雖然我們可以通過配置每臺節(jié)點(diǎn)200次/秒來限流。但如果節(jié)點(diǎn)收縮或者擴(kuò)容,那么久不能滿足需求了。而且不同服務(wù)的物理配置不一定相同,可能有些節(jié)點(diǎn)處理得比較快,那么配置均值來限流,就不是一個好方法了。
常見的分布式限流策略
網(wǎng)關(guān)層限流:將限流規(guī)則應(yīng)用在所有流量的入口處,比如nigix+lua
中間件限流:將限流信息存儲在分布式環(huán)境中某個中間件里(比如Redis緩存),每個組件都可以從這里獲取到當(dāng)前時刻的流量統(tǒng)計,從而決定是拒絕服務(wù)還是放行流量。 -
負(fù)載均衡
Load balancing,即負(fù)載均衡,是一種計算機(jī)技術(shù),用來在多個計算機(jī)(計算機(jī)集群)、網(wǎng)絡(luò)連接、CPU、磁盤驅(qū)動器或其他資源中分配負(fù)載,以達(dá)到最優(yōu)化資源使用、最大化吞吐率、最小化響應(yīng)時間、同時避免過載的目的。
負(fù)載均衡(Load Balance),意思是將負(fù)載(工作任務(wù),訪問請求)進(jìn)行平衡、分?jǐn)偟蕉鄠€操作單元(服務(wù)器,組件)上進(jìn)行執(zhí)行。是解決高性能,單點(diǎn)故障(高可用),擴(kuò)展性(水平伸縮)的終極解決方案。
負(fù)載均衡算法
1、輪詢法
將請求按順序輪流地分配到后端服務(wù)器上,它均衡地對待后端的每一臺服務(wù)器,而不關(guān)心服務(wù)器實際的連接數(shù)和當(dāng)前的系統(tǒng)負(fù)載。
2、隨機(jī)法
通過系統(tǒng)的隨機(jī)算法,根據(jù)后端服務(wù)器的列表大小值來隨機(jī)選取其中的一臺服務(wù)器進(jìn)行訪問。由概率統(tǒng)計理論可以得知,隨著客戶端調(diào)用服務(wù)端的次數(shù)增多,其實際效果越來越接近于平均分配調(diào)用量到后端的每一臺服務(wù)器,也就是輪詢的結(jié)果。
3、源地址哈希法
源地址哈希的思想是根據(jù)獲取客戶端的IP地址,通過哈希函數(shù)計算得到的一個數(shù)值,用該數(shù)值對服務(wù)器列表的大小進(jìn)行取模運(yùn)算,得到的結(jié)果便是客服端要訪問服務(wù)器的序號。采用源地址哈希法進(jìn)行負(fù)載均衡,同一IP地址的客戶端,當(dāng)后端服務(wù)器列表不變時,它每次都會映射到同一臺后端服務(wù)器進(jìn)行訪問。
4、加權(quán)輪詢法
不同的后端服務(wù)器可能機(jī)器的配置和當(dāng)前系統(tǒng)的負(fù)載并不相同,因此它們的抗壓能力也不相同。給配置高、負(fù)載低的機(jī)器配置更高的權(quán)重,讓其處理更多的請;而配置低、負(fù)載高的機(jī)器,給其分配較低的權(quán)重,降低其系統(tǒng)負(fù)載,加權(quán)輪詢能很好地處理這一問題,并將請求順序且按照權(quán)重分配到后端。
5、加權(quán)隨機(jī)法
與加權(quán)輪詢法一樣,加權(quán)隨機(jī)法也根據(jù)后端機(jī)器的配置,系統(tǒng)的負(fù)載分配不同的權(quán)重。不同的是,它是按照權(quán)重隨機(jī)請求后端服務(wù)器,而非順序。
6、最小連接數(shù)法
最小連接數(shù)算法比較靈活和智能,由于后端服務(wù)器的配置不盡相同,對于請求的處理有快有慢,它是根據(jù)后端服務(wù)器當(dāng)前的連接情況,動態(tài)地選取其中當(dāng)前。
積壓連接數(shù)最少的一臺服務(wù)器來處理當(dāng)前的請求,盡可能地提高后端服務(wù)的利用效率,將負(fù)責(zé)合理地分流到每一臺服務(wù)器。
-
2.3 Viper
-
什么是Viper
Viper是適用于Go應(yīng)用程序的完整配置解決方案。它被設(shè)計用于在應(yīng)用程序中工作,并且可以處理所有類型的配置需求和格式。
特性:
- 設(shè)置默認(rèn)值
- 從
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件讀取配置信息 - 實時監(jiān)控和重新讀取配置文件(可選)
- 從環(huán)境變量中讀取
- 從遠(yuǎn)程配置系統(tǒng)(etcd或Consul)讀取并監(jiān)控配置變化
- 從命令行參數(shù)讀取配置
- 從buffer讀取配置
- 顯式配置值
-
Viper支持什么功能
Viper能夠為你執(zhí)行下列操作:
- 查找、加載和反序列化
JSON
、TOML
、YAML
、HCL
、INI
、envfile
和Java properties
格式的配置文件。 - 提供一種機(jī)制為你的不同配置選項設(shè)置默認(rèn)值。
- 提供一種機(jī)制來通過命令行參數(shù)覆蓋指定選項的值。
- 提供別名系統(tǒng),以便在不破壞現(xiàn)有代碼的情況下輕松重命名參數(shù)。
- 當(dāng)用戶提供了與默認(rèn)值相同的命令行或配置文件時,可以很容易地分辨出它們之間的區(qū)別。
- 建立默認(rèn)值
- 讀取配置文件
- 寫入配置文件
- 監(jiān)控并重新讀取配置文件
- 從io.Reader讀取配置
- 覆蓋設(shè)置
- 注冊和使用別名
- 使用環(huán)境變量
- 使用Flags
- 遠(yuǎn)程Key/Value存儲支持
- 監(jiān)控etcd中的更改-未加密
- 查找、加載和反序列化
2.4 Swagger
-
什么是Swagger
Swagger本質(zhì)上是一種用于描述使用JSON表示的RESTful API的接口描述語言。Swagger與一組開源軟件工具一起使用,以設(shè)計、構(gòu)建、記錄和使用RESTful Web服務(wù)。Swagger包括自動文檔,代碼生成和測試用例生成。
想要使用
gin-swagger
為你的代碼自動生成接口文檔,一般需要下面三個步驟:- 按照swagger要求給接口代碼添加聲明式注釋,具體參照聲明式注釋格式
- 使用swag工具掃描代碼自動生成API接口文檔數(shù)據(jù)
- 使用gin-swagger渲染在線接口文檔頁面
-
Swagger的優(yōu)勢
在前后端分離的項目開發(fā)過程中,如果后端同學(xué)能夠提供一份清晰明了的接口文檔,那么就能極大地提高大家的溝通效率和開發(fā)效率。可是編寫接口文檔歷來都是令人頭痛的,而且后續(xù)接口文檔的維護(hù)也十分耗費(fèi)精力。
最好是有一種方案能夠既滿足我們輸出文檔的需要又能隨代碼的變更自動更新,而Swagger正是那種能幫我們解決接口文檔問題的工具。
2.5 Zap
-
什么是Zap
Zap是在 Go 中實現(xiàn)超快、結(jié)構(gòu)化、分級的日志記錄。
Zap日志能夠提供下面這些功能:
-
1、能夠?qū)⑹录涗浀轿募?#xff0c;也可以在應(yīng)用控制臺輸出
-
2、日志切割-可以根據(jù)文件大小,時間或間隔來切割日志文件
-
3、支持不同的日志級別。例如 INFO、DEBUG、ERROR等
-
4、能夠打印基本信息,如調(diào)用文件/函數(shù)名和行號,日志時間等。
zap的基本配置
Zap提供了兩種類型的日志記錄器—Sugared Logger 和 Logger 。在性能很好但不是很關(guān)鍵的上下文中,使用 SugaredLogger 。它比其他結(jié)構(gòu)化日志記錄包快4-10倍,并且支持結(jié)構(gòu)化和printf風(fēng)格的日志記錄。
在每一微秒和每一次內(nèi)存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,內(nèi)存分配次數(shù)也更少,但它只支持強(qiáng)類型的結(jié)構(gòu)化日志記錄。
這個日志程序中唯一缺少的就是日志切割歸檔功能。添加日志切割歸檔功能,我們將使用第三方庫Lumberjack來實現(xiàn)。
-
2.6 JWT
-
什么是JWT
JWT 英文名是 Json Web Token ,是一種用于通信雙方之間傳遞安全信息的簡潔的、URL安全的表述性聲明規(guī)范,經(jīng)常用在跨域身份驗證。
JWT 以 JSON 對象的形式安全傳遞信息。因為存在數(shù)字簽名,因此所傳遞的信息是安全的。
一個JWT Token就像這樣:
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyODAx0DcyNzQ40DMyMzU4NSwiZ
XhwIjoxNTk0NTQwMjkxLCJpc3MiOiJibHV1YmVsbCJ9.1k_ZrAtYGCeZhK3iupHxP1kgjBJzQTVTtX0iZYFx9wU -
JWT的實現(xiàn)
JWT由.分割的三部分組成,這三部分依次是:
- 頭部(Header)
作用:記錄令牌類型、簽名算法等 例如:{“alg":“HS256”,“type”,"JWT} - 負(fù)載(Payload)
作用:攜帶一些用戶信息 例如{“userId”:“1”,“username”:“mayikt”} - 簽名(Signature)
作用:防止Token被篡改、確保安全性 例如 計算出來的簽名,一個字符串
頭部和負(fù)載以json形式存在,這就是JWT中的JSON,三部分的內(nèi)容都分別單獨(dú)經(jīng)過了Base64編碼,以.拼接成一個JWT Token。
- 頭部(Header)
-
JWT的優(yōu)勢
JWT就是一種基于Token的輕量級認(rèn)證模式,服務(wù)端認(rèn)證通過后,會生成一個JSON對象,經(jīng)過簽名后得到一個Token(令牌)再發(fā)回給用戶,用戶后續(xù)請求只需要帶上這個Token,服務(wù)端解密之后就能獲取該用戶的相關(guān)信息了。
JWT擁有基于Token的會話管理方式所擁有的一切優(yōu)勢,不依賴Cookie,使得其可以防止CSRF攻擊,也能在禁用Cookie的瀏覽器環(huán)境中正常運(yùn)行。
而JWT的最大優(yōu)勢是服務(wù)端不再需要存儲Session,使得服務(wù)端認(rèn)證鑒權(quán)業(yè)務(wù)可以方便擴(kuò)展,避免存儲Session所需要引入的Redis等組件,降低了系統(tǒng)架構(gòu)復(fù)雜度。但這也是JWT最大的劣勢,由于有效期存儲在Token中,JWT Token-旦簽發(fā),就會在有效期內(nèi)-直可用,無法在服務(wù)端廢止,當(dāng)用戶進(jìn)行登出操作,只能依賴客戶端刪除掉本地存儲的JWT Token,如果需要禁用用戶,單純使用JWT就無法做到了。
3. 熟悉使用 MySQL 數(shù)據(jù)庫
3.1 索引
-
為什么使用索引
- 通過創(chuàng)建唯一性索引,可以保證數(shù)據(jù)庫表中每一行數(shù)據(jù)的唯一性。
- 可以大大加快數(shù)據(jù)的檢索速度,這也是創(chuàng)建索引的最主要的原因。
- 幫助服務(wù)器避免排序和臨時表
- 將隨機(jī)IO變?yōu)轫樞騃O。
- 可以加速表和表之間的連接,特別是在實現(xiàn)數(shù)據(jù)的參考完整性方面特別有意義。
-
索引的分類
- 普通索引:僅加速查詢
- 唯一索引:加速查詢 + 列值唯一(可以有null)
- 主鍵索引:加速查詢 + 列值唯一(不可以有null)+ 表中只有一個
- 組合索引:多列值組成一個索引,專門用于組合搜索,其效率大于索引合并
- 全文索引:對文本的內(nèi)容進(jìn)行分詞,進(jìn)行搜索
- 索引合并:使用多個單列索引組合搜索
- 覆蓋索引:select的數(shù)據(jù)列只用從索引中就能夠取得,不必讀取數(shù)據(jù)行,換句話說查詢列要被所建的索引覆蓋
- 聚簇索引:表數(shù)據(jù)是和主鍵一起存儲的,主鍵索引的葉結(jié)點(diǎn)存儲行數(shù)據(jù)(包含了主鍵值),二級索引的葉結(jié)點(diǎn)存儲行的主鍵值。使用的是B+樹作為索引的存儲結(jié)構(gòu),非葉子節(jié)點(diǎn)都是索引關(guān)鍵字,但非葉子節(jié)點(diǎn)中的關(guān)鍵字中不存儲對應(yīng)記錄的具體內(nèi)容或內(nèi)容地址。葉子節(jié)點(diǎn)上的數(shù)據(jù)是主鍵與具體記錄(數(shù)據(jù)內(nèi)容)
-
什么時候需要/不需要創(chuàng)建索引
索引最大的好處是提高查詢速度,但是索引也是有缺點(diǎn)的,比如:
- 需要占用物理空間,數(shù)量越大,占用空間越大;
- 創(chuàng)建索引和維護(hù)索引要耗費(fèi)時間,這種時間隨著數(shù)據(jù)量的增加而增大;
- 會降低表的增刪改的效率,每次增刪改索引,B+ 樹為了維護(hù)索引有序性,需要進(jìn)行動態(tài)維護(hù)。
所以,索引不是萬能鑰匙,它也是根據(jù)場景來使用的。
什么時候適用索引?
- 字段有唯一性限制的,比如商品編碼;
- 經(jīng)常用于
WHERE
查詢條件的字段,這樣能夠提高整個表的查詢速度,如果查詢條件不是一個字段,可以建立聯(lián)合索引。 - 經(jīng)常用于
GROUP BY
和ORDER BY
的字段,這樣在查詢的時候就不需要再去做一次排序了,因為我們都已經(jīng)知道了建立索引之后在 B+Tree 中的記錄都是排序好的。
什么時候不需要創(chuàng)建索引?
WHERE
條件,GROUP BY
,ORDER BY
里用不到的字段,索引的價值是快速定位,如果起不到定位的字段通常是不需要創(chuàng)建索引的,因為索引是會占用物理空間的。- 字段中存在大量重復(fù)數(shù)據(jù),不需要創(chuàng)建索引,比如性別字段,只有男女,如果數(shù)據(jù)庫表中,男女的記錄分布均勻,那么無論搜索哪個值都可能得到一半的數(shù)據(jù)。在這些情況下,還不如不要索引,因為 MySQL 還有一個查詢優(yōu)化器,查詢優(yōu)化器發(fā)現(xiàn)某個值出現(xiàn)在表的數(shù)據(jù)行中的百分比很高的時候,它一般會忽略索引,進(jìn)行全表掃描。
- 表數(shù)據(jù)太少的時候,不需要創(chuàng)建索引;
- 經(jīng)常更新的字段不用創(chuàng)建索引,因為索引字段頻繁修改,由于要維護(hù) B+Tree的有序性,那么需要頻繁的重建索引,這個過程是會影響數(shù)據(jù)庫性能的。
-
優(yōu)化索引的方法
-
前綴索引優(yōu)化;
? 前綴索引顧名思義就是使用某個字段中字符串的前幾個字符建立索引。
? 使用前綴索引是為了減小索引字段大小,可以增加一個索引頁中存儲的索引值,有效提高索引的查詢速度。在一些大字符串的字段作為索引時,使用前綴索引可以幫助我們減小索引項的大小。
-
覆蓋索引優(yōu)化;
? 覆蓋索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的葉子節(jié)點(diǎn)上都能找得到的那些索引,從二級索引中查詢得到記錄,而不需要通過聚簇索引查詢獲得,可以避免回表的操作。
? 使用覆蓋索引的好處就是,不需要查詢出包含整行記錄的所有信息,也就減少了大量的 I/O 操作。
-
主鍵索引最好是自增的;
? 如果我們使用自增主鍵,那么每次插入的新數(shù)據(jù)就會按順序添加到當(dāng)前索引節(jié)點(diǎn)的位置,不需要移動已有的數(shù)據(jù),當(dāng)頁面寫滿,就會自動開辟一個新頁面。因為每次插入一條新記錄,都是追加操作,不需要重新移動數(shù)據(jù),因此這種插入數(shù)據(jù)的方法效率非常高。
? 主鍵字段的長度不要太大,因為主鍵字段長度越小,意味著二級索引的葉子節(jié)點(diǎn)越小(二級索引的葉子節(jié)點(diǎn)存放的數(shù)據(jù)是主鍵值),這樣二級索引占用的空間也就越小。
-
防止索引失效;
? 用上了索引并不意味著查詢的時候會使用到索引,所以我們心里要清楚有哪些情況會導(dǎo)致索引失效,從而避免寫出索引失效的查詢語句,否則這樣的查詢效率是很低的。
? 發(fā)生索引失效的情況:
- 當(dāng)我們使用左或者左右模糊匹配的時候,也就是
like %xx
或者like %xx%
這兩種方式都會造成索引失效; - 當(dāng)我們在查詢條件中對索引列做了計算、函數(shù)、類型轉(zhuǎn)換操作,這些情況下都會造成索引失效;
- 聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配,否則就會導(dǎo)致索引失效。
- 在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會失效。
- 當(dāng)我們使用左或者左右模糊匹配的時候,也就是
-
-
索引使用的注意事項
MySQL 索引通常是被用于提高 WHERE 條件的數(shù)據(jù)行匹配時的搜索速度,在索引的使用過程中,存在一些使用細(xì)節(jié)和注意事項。
函數(shù),運(yùn)算,否定操作符,連接條件,多個單列索引,最左前綴原則,范圍查詢,不會包含有NULL值的列,like 語句不要在列上使用函數(shù)和進(jìn)行運(yùn)算
- 1)不要在列上使用函數(shù),這將導(dǎo)致索引失效而進(jìn)行全表掃描。
- 2)盡量避免使用 != 或 not in或 等否定操作符
- 3)多個單列索引并不是最佳選擇
- 4)復(fù)合索引的最左前綴原則
- 5)覆蓋索引的好處
- 6)范圍查詢對多列查詢的影響
- 7)索引不會包含有NULL值的列
- 8)隱式轉(zhuǎn)換的影響
- 9)like 語句的索引失效問題
-
索引為什么使用B+樹作為索引
主要原因:B+樹只要遍歷葉子節(jié)點(diǎn)就可以實現(xiàn)整棵樹的遍歷,而且在數(shù)據(jù)庫中基于范圍的查詢是非常頻繁的,而B樹只能中序遍歷所有節(jié)點(diǎn),效率太低。
B+tree的磁盤讀寫代價更低,B+tree的查詢效率更加穩(wěn)定 數(shù)據(jù)庫索引采用B+樹而不是B樹的主要原因:B+樹只要遍歷葉子節(jié)點(diǎn)就可以實現(xiàn)整棵樹的遍歷,而且在數(shù)據(jù)庫中基于范圍的查詢是非常頻繁的,而B樹只能中序遍歷所有節(jié)點(diǎn),效率太低。
B+樹的特點(diǎn)
- 所有關(guān)鍵字都出現(xiàn)在葉子結(jié)點(diǎn)的鏈表中(稠密索引),且鏈表中的關(guān)鍵字恰好是有序的;
- 不可能在非葉子結(jié)點(diǎn)命中;
- 非葉子結(jié)點(diǎn)相當(dāng)于是葉子結(jié)點(diǎn)的索引(稀疏索引),葉子結(jié)點(diǎn)相當(dāng)于是存儲(關(guān)鍵字)數(shù)據(jù)的數(shù)據(jù)層;
-
索引失效有哪些
- 當(dāng)我們使用左或者左右模糊匹配的時候,也就是
like %xx
或者like %xx%
這兩種方式都會造成索引失效; - 當(dāng)我們在查詢條件中對索引列使用函數(shù),就會導(dǎo)致索引失效。
- 當(dāng)我們在查詢條件中對索引列進(jìn)行表達(dá)式計算,也是無法走索引的。
- MySQL 在遇到字符串和數(shù)字比較的時候,會自動把字符串轉(zhuǎn)為數(shù)字,然后再進(jìn)行比較。如果字符串是索引列,而條件語句中的輸入?yún)?shù)是數(shù)字的話,那么索引列會發(fā)生隱式類型轉(zhuǎn)換,由于隱式類型轉(zhuǎn)換是通過 CAST 函數(shù)實現(xiàn)的,等同于對索引列使用了函數(shù),所以就會導(dǎo)致索引失效。
- 聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配,否則就會導(dǎo)致索引失效。
- 在 WHERE 子句中,如果在 OR 前的條件列是索引列,而在 OR 后的條件列不是索引列,那么索引會失效。
- 當(dāng)我們使用左或者左右模糊匹配的時候,也就是
-
MyISAM和InnoDB實現(xiàn)B樹索引方式的區(qū)別是什么
-
MyISAM,B+Tree葉節(jié)點(diǎn)的data域存放的是數(shù)據(jù)記錄的地址,在索引檢索的時候,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,則取出其data域的值,然后以data域的值為地址讀取相應(yīng)的數(shù)據(jù)記錄,這被稱為“非聚簇索引”
-
InnoDB,其數(shù)據(jù)文件本身就是索引文件,相比MyISAM,索引文件和數(shù)據(jù)文件是分離的,其表數(shù)據(jù)文件本身就是按B+Tree組織的一個索引結(jié)構(gòu),樹的節(jié)點(diǎn)data域保存了完整的數(shù)據(jù)記錄,這個索引的key是數(shù)據(jù)表的主鍵,因此InnoDB表數(shù)據(jù)文件本身就是主索引,這被稱為“聚簇索引”或者聚集索引,而其余的索引都作為輔助索引,輔助索引的data域存儲相應(yīng)記錄主鍵的值而不是地址,這也是和MyISAM不同的地方。
在根據(jù)主索引搜索時,直接找到key所在的節(jié)點(diǎn)即可取出數(shù)據(jù);在根據(jù)輔助索引查找時,則需要先取出主鍵的值,再走一遍主索引。因此,在設(shè)計表的時候,不建議使用過長的字段為主鍵,也不建議使用非單調(diào)的字段作為主鍵,這樣會造成主索引頻繁分裂。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-1pQCH7oH-1678081583393)(https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E7%B4%A2%E5%BC%95/%E7%B4%A2%E5%BC%95%E6%80%BB%E7%BB%93.drawio.png)]
-
3.2 事務(wù)
-
事務(wù)的四大特性
- 原子性:事務(wù)是最小的執(zhí)行單位,不允許分割。事務(wù)的原子性確保動作要么全部完成,要么完全不起作用;
- 一致性:執(zhí)行事務(wù)前后,數(shù)據(jù)庫從一個一致性狀態(tài)轉(zhuǎn)換到另一個一致性狀態(tài)。
- 隔離性:并發(fā)訪問數(shù)據(jù)庫時,一個用戶的事物不被其他事務(wù)所干擾,各并發(fā)事務(wù)之間數(shù)據(jù)庫是獨(dú)立的;
- 持久性:一個事務(wù)被提交之后。它對數(shù)據(jù)庫中數(shù)據(jù)的改變是持久的,即使數(shù)據(jù)庫 發(fā)生故障也不應(yīng)該對其有任何影響。
-
事務(wù)的臟讀、不可重復(fù)讀、幻讀問題
臟讀:如果一個事務(wù)「讀到」了另一個「未提交事務(wù)修改過的數(shù)據(jù)」,就意味著發(fā)生了「臟讀」現(xiàn)象。
幻讀:在一個事務(wù)內(nèi)多次查詢某個符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
丟棄修改:兩個寫事務(wù)T1 T2同時對A=0進(jìn)行遞增操作,結(jié)果T2覆蓋T1,導(dǎo)致最終結(jié)果是1 而不是2,事務(wù)被覆蓋
不可重復(fù)讀:在一個事務(wù)內(nèi)多次讀取同一個數(shù)據(jù),如果出現(xiàn)前后兩次讀到的數(shù)據(jù)不一樣的情況,就意味著發(fā)生了「不可重復(fù)讀」現(xiàn)象。
-
事務(wù)的隔離級別有哪些
- READ_UNCOMMITTED(未提交讀): 最低的隔離級別,允許讀取尚未提交的數(shù)據(jù)變更,可能會導(dǎo)致臟讀、幻讀或不可重復(fù)讀;
- READ_COMMITTED(提交讀): 允許讀取并發(fā)事務(wù)已經(jīng)提交的數(shù)據(jù),可以阻止臟讀,但是幻讀或不可重復(fù)讀仍有可能發(fā)生;
- REPEATABLE_READ(可重復(fù)讀): 對同一字段的多次讀取結(jié)果都是一致的,除非數(shù)據(jù)是被本身事務(wù)自己所修改,可以阻止臟讀和不可重復(fù)讀,但幻讀仍有可能發(fā)生;
- SERIALIZABLE(串行化): 最高的隔離級別,完全服從 ACID 的隔離級別。所有的事務(wù)依次逐個執(zhí)行,這樣事務(wù)之間就完全不可能產(chǎn)生干擾,也就是說,該級別可以防止臟讀、不可重復(fù)讀以及幻讀。但是這將嚴(yán)重影響程序的性能。通常情況下也不會用到該級別。
隔離級別 臟讀 不可重復(fù)讀 幻影讀 READ-UNCOMMITTED 未提交讀 √ √ √ READ-COMMITTED 提交讀 × √ √ REPEATABLE-READ 重復(fù)讀 × × √ SERIALIZABLE 可串行化讀 × × × MySQL InnoDB 存儲引擎的默認(rèn)支持的隔離級別是 REPEATABLE-READ(可重讀)
這里需要注意的是:與 SQL 標(biāo)準(zhǔn)不同的地方在于InnoDB 存儲引擎在 REPEATABLE-READ(可重讀)事務(wù)隔離級別 下使用的是Next-Key Lock 鎖算法,因此可以避免幻讀的產(chǎn)生,這與其他數(shù)據(jù)庫系統(tǒng)(如 SQL Server)是不同的。所以 說InnoDB 存儲引擎的默認(rèn)支持的隔離級別是 REPEATABLE-READ(可重讀) 已經(jīng)可以完全保證事務(wù)的隔離性要 求,即達(dá)到了 SQL標(biāo)準(zhǔn)的SERIALIZABLE(可串行化)隔離級別。
-
Read View的作用
Read View 有四個重要的字段:
- m_ids :指的是在創(chuàng)建 Read View 時,當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」的事務(wù) id 列表,注意是一個列表,“活躍事務(wù)”指的就是,啟動了但還沒提交的事務(wù)。
- min_trx_id :指的是在創(chuàng)建 Read View 時,當(dāng)前數(shù)據(jù)庫中「活躍事務(wù)」中事務(wù) id 最小的事務(wù),也就是 m_ids 的最小值。
- max_trx_id :這個并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時當(dāng)前數(shù)據(jù)庫中應(yīng)該給下一個事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值 + 1;
- creator_trx_id :指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。
對于使用 InnoDB 存儲引擎的數(shù)據(jù)庫表,它的聚簇索引記錄中都包含下面兩個隱藏列:
- trx_id,當(dāng)一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時,就會把該事務(wù)的事務(wù) id 記錄在 trx_id 隱藏列里;
- roll_pointer,每次對某條聚簇索引記錄進(jìn)行改動時,都會把舊版本的記錄寫入到 undo 日志中,然后這個隱藏列是個指針,指向每一個舊版本記錄,于是就可以通過它找到修改前的記錄。
通過「版本鏈」來控制并發(fā)事務(wù)訪問同一個記錄時的行為就叫 MVCC(多版本并發(fā)控制)。
-
MySQL可重復(fù)讀級別完全解決幻讀了嗎
- 針對快照讀(普通 select 語句),是通過 MVCC 方式解決了幻讀,因為可重復(fù)讀隔離級別下,事務(wù)執(zhí)行過程中看到的數(shù)據(jù),一直跟這個事務(wù)啟動時看到的數(shù)據(jù)是一致的,即使中途有其他事務(wù)插入了一條數(shù)據(jù),是查詢不出來這條數(shù)據(jù)的,所以就很好了避免幻讀問題。
- 針對當(dāng)前讀(select … for update 等語句),是通過 next-key lock(記錄鎖+間隙鎖)方式解決了幻讀,因為當(dāng)執(zhí)行 select … for update 語句的時候,會加上 next-key lock,如果有其他事務(wù)在 next-key lock 鎖范圍內(nèi)插入了一條記錄,那么這個插入語句就會被阻塞,無法成功插入,所以就很好了避免幻讀問題。
對于「讀提交」和「可重復(fù)讀」隔離級別的事務(wù)來說,它們是通過 Read View 來實現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時機(jī)不同:
- 「讀提交」隔離級別是在每個 select 都會生成一個新的 Read View,也意味著,事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會出現(xiàn)不一致,因為可能這期間另外一個事務(wù)修改了該記錄,并提交了事務(wù)。
- 「可重復(fù)讀」隔離級別是啟動事務(wù)時生成一個 Read View,然后整個事務(wù)期間都在用這個 Read View,這樣就保證了在事務(wù)期間讀到的數(shù)據(jù)都是事務(wù)啟動前的記錄。
這兩個隔離級別實現(xiàn)是通過「事務(wù)的 Read View 里的字段」和「記錄中的兩個隱藏列」的比對,來控制并發(fā)事務(wù)訪問同一個記錄時的行為,這就叫 MVCC(多版本并發(fā)控制)。
兩個發(fā)生幻讀場景的例子。
第一個例子:對于快照讀, MVCC 并不能完全避免幻讀現(xiàn)象。因為當(dāng)事務(wù) A 更新了一條事務(wù) B 插入的記錄,那么事務(wù) A 前后兩次查詢的記錄條目就不一樣了,所以就發(fā)生幻讀。
第二個例子:對于當(dāng)前讀,如果事務(wù)開啟后,并沒有執(zhí)行當(dāng)前讀,而是先快照讀,然后這期間如果其他事務(wù)插入了一條記錄,那么事務(wù)后續(xù)使用當(dāng)前讀進(jìn)行查詢的時候,就會發(fā)現(xiàn)兩次查詢的記錄條目就不一樣了,所以就發(fā)生幻讀。
所以,MySQL 可重復(fù)讀隔離級別并沒有徹底解決幻讀,只是很大程度上避免了幻讀現(xiàn)象的發(fā)生。
-
MySQL中為什么要有事務(wù)回滾機(jī)制
在 MySQL 中,恢復(fù)機(jī)制是通過回滾日志(undo log)實現(xiàn)的,所有事務(wù)進(jìn)行的修改都會先記錄到這個回滾日志,然后在對數(shù)據(jù)庫中的對應(yīng)行進(jìn)行寫入。 當(dāng)事務(wù)已經(jīng)被提交后,就無法再次回滾了。
回滾日志作用: 1)能夠在發(fā)生錯誤或者用戶執(zhí)行 ROLLBACK 時提供回滾相關(guān)的信息 2) 在整個系統(tǒng)發(fā)生崩潰、數(shù)據(jù)庫進(jìn)程直接被殺死后,當(dāng)用戶再次啟動數(shù)據(jù)庫進(jìn)程時,還能夠立刻通過查詢回滾日志將之前未完成的事務(wù)進(jìn)行回滾,這也就需要回滾日志必須先于數(shù)據(jù)持久化到磁盤上,是我們需要先寫日志后寫數(shù)據(jù)庫的主要原因。
3.3 存儲引擎
-
InnoDB介紹
InnoDB是事務(wù)型數(shù)據(jù)庫的首選引擎,支持事務(wù)安全表(ACID),支持行鎖定和外鍵,InnoDB是默認(rèn)的MySQL引擎。
InnoDB主要特性有:
-
InnoDB給MySQL提供了具有提交、回滾和崩潰恢復(fù)能力的事物安全(ACID兼容)存儲引擎。
InnoDB鎖定在行級并且也在SELECT語句中提供一個類似Oracle的非鎖定讀。這些功能增加了多用戶部署和性能。在SQL查詢中,可以自由地將InnoDB類型的表和其他MySQL的表類型混合起來,甚至在同一個查詢中也可以混合。
-
InnoDB是為處理巨大數(shù)據(jù)量的最大性能設(shè)計。它的CPU效率可能是任何其他基于磁盤的關(guān)系型數(shù)據(jù)庫引擎鎖不能匹敵的。
-
InnoDB存儲引擎完全與MySQL服務(wù)器整合,InnoDB存儲引擎為在主內(nèi)存中緩存數(shù)據(jù)和索引而維持它自己的緩沖池。InnoDB將它的表和索引在一個邏輯表空間中,表空間可以包含數(shù)個文件(或原始磁盤文件)。這與MyISAM表不同,比如在MyISAM表中每個表被存放在分離的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制為2GB的操作系統(tǒng)上。
-
InnoDB支持外鍵完整性約束,存儲表中的數(shù)據(jù)時,每張表的存儲都按主鍵順序存放,如果沒有顯示在表定義時指定主鍵,InnoDB會為每一行生成一個6字節(jié)的ROWID,并以此作為主鍵。
-
InnoDB被用在眾多需要高性能的大型數(shù)據(jù)庫站點(diǎn)上。InnoDB不創(chuàng)建目錄,使用InnoDB時,MySQL將在MySQL數(shù)據(jù)目錄下創(chuàng)建一個名為ibdata1的10MB大小的自動擴(kuò)展數(shù)據(jù)文件,以及兩個名為 ib_logfile0 和 ib_logfile1 的5MB大小的日志文件。
-
-
MyISAM介紹
MyISAM基于ISAM存儲引擎,并對其進(jìn)行擴(kuò)展。它是在Web、數(shù)據(jù)倉儲和其他應(yīng)用環(huán)境下最常使用的存儲引擎之一。MyISAM擁有較高的插入、查詢速度,但不支持事物。
MyISAM主要特性有:
- 大文件(達(dá)到63位文件長度)在支持大文件的文件系統(tǒng)和操作系統(tǒng)上被支持。
- 當(dāng)把刪除和更新及插入操作混合使用的時候,動態(tài)尺寸的行產(chǎn)生更少碎片。這要通過合并相鄰被刪除的塊,以及若下一個塊被刪除,就擴(kuò)展到下一塊自動完成。
- 每個MyISAM表最大索引數(shù)是64,這可以通過重新編譯來改變。每個索引最大的列數(shù)是16。
- 最大的鍵長度是1000字節(jié),這也可以通過編譯來改變,對于鍵長度超過250字節(jié)的情況,一個超過1024字節(jié)的鍵將被用上。
- BLOB和TEXT列可以被索引。
- NULL被允許在索引的列中,這個值占每個鍵的0~1個字節(jié)。
- 所有數(shù)字鍵值以高字節(jié)優(yōu)先被存儲以允許一個更高的索引壓縮。
- 每個MyISAM類型的表都有一個AUTO_INCREMENT的內(nèi)部列,當(dāng)INSERT和UPDATE操作的時候該列被更新,同時AUTO_INCREMENT列將被刷新。所以說,MyISAM類型表的AUTO_INCREMENT列更新比InnoDB類型的AUTO_INCREMENT更快。
- 可以把數(shù)據(jù)文件和索引文件放在不同目錄。
- 每個字符列可以有不同的字符集。
- 有VARCHAR的表可以固定或動態(tài)記錄長度。
- VARCHAR和CHAR列可以多達(dá)64KB。
-
MEMORY介紹
MEMORY存儲引擎將表中的數(shù)據(jù)存儲到內(nèi)存中,未查詢和引用其他表數(shù)據(jù)提供快速訪問。
MEMORY主要特性有:
- MEMORY表的每個表可以有多達(dá)32個索引,每個索引16列,以及500字節(jié)的最大鍵長度。
- MEMORY存儲引擎執(zhí)行HASH和BTREE縮影。
- 可以在一個MEMORY表中有非唯一鍵值。
- MEMORY表使用一個固定的記錄長度格式。
- MEMORY不支持BLOB或TEXT列。
- MEMORY支持AUTO_INCREMENT列和對可包含NULL值的列的索引。
- MEMORY表在所由客戶端之間共享(就像其他任何非TEMPORARY表)。
- MEMORY表內(nèi)存被存儲在內(nèi)存中,內(nèi)存是MEMORY表和服務(wù)器在查詢處理時的空閑中,創(chuàng)建的內(nèi)部表共享。
- 當(dāng)不再需要MEMORY表的內(nèi)容時,要釋放被MEMORY表使用的內(nèi)存,應(yīng)該執(zhí)行DELETE FROM或TRUNCATE TABLE,或者刪除整個表(使用DROP TABLE)。
-
Archive介紹
archive儲存引擎的應(yīng)用場景就是它的名字的縮影,主要用于歸檔。archive儲存引擎僅支持select和insert,最出眾的是插入快,查詢快,占用空間小。
文件系統(tǒng)存儲特性
- 以zlib對表數(shù)據(jù)進(jìn)行壓縮,磁盤I/O更少(幾Tinnodb表在archive中只需要幾百兆)
- 數(shù)據(jù)存儲在.ARZ為后綴的文件中
- .frm文件
功能特點(diǎn)
- 只支持insert、replace和select
- 支持行級鎖和專用的緩存區(qū),可實現(xiàn)高并發(fā)
- 只允許在自增ID列上加索引
- 支持分區(qū),不支持事務(wù)處理
-
數(shù)據(jù)庫引擎InnoDB與MyISAM的區(qū)別
InnoDB
- 是 MySQL 默認(rèn)的事務(wù)型存儲引擎,只有在需要它不支持的特性時,才考慮使用其它存儲引擎。
- 實現(xiàn)了四個標(biāo)準(zhǔn)的隔離級別,默認(rèn)級別是可重復(fù)讀(REPEATABLE READ)。在可重復(fù)讀隔離級別下,通過多版本并發(fā)控制(MVCC)+ 間隙鎖(Next-Key Locking)防止幻影讀。
- 主索引是聚簇索引,在索引中保存了數(shù)據(jù),從而避免直接讀取磁盤,因此對查詢性能有很大的提升。
- 內(nèi)部做了很多優(yōu)化,包括從磁盤讀取數(shù)據(jù)時采用的可預(yù)測性讀、能夠加快讀操作并且自動創(chuàng)建的自適應(yīng)哈希索引、能夠加速插入操作的插入緩沖區(qū)等。
- 支持真正的在線熱備份。其它存儲引擎不支持在線熱備份,要獲取一致性視圖需要停止對所有表的寫入,而在讀寫混合場景中,停止寫入可能也意味著停止讀取。
MyISAM
- 設(shè)計簡單,數(shù)據(jù)以緊密格式存儲。對于只讀數(shù)據(jù),或者表比較小、可以容忍修復(fù)操作,則依然可以使用它。
- 提供了大量的特性,包括壓縮表、空間數(shù)據(jù)索引等。
- 不支持事務(wù)。
- 不支持行級鎖,只能對整張表加鎖,讀取時會對需要讀到的所有表加共享鎖,寫入時則對表加排它鎖。但在表有讀取操作的同時,也可以往表中插入新的記錄,這被稱為并發(fā)插入(CONCURRENT INSERT)。
總結(jié)
- 事務(wù): InnoDB 是事務(wù)型的,可以使用
Commit
和Rollback
語句。 - 并發(fā): MyISAM 只支持表級鎖,而 InnoDB 還支持行級鎖。
- 外鍵: InnoDB 支持外鍵。
- 備份: InnoDB 支持在線熱備份。
- 崩潰恢復(fù): MyISAM 崩潰后發(fā)生損壞的概率比 InnoDB 高很多,而且恢復(fù)的速度也更慢。
- 其它特性: MyISAM 支持壓縮表和空間數(shù)據(jù)索引。
適用場景: MyISAM適合: 插入不頻繁,查詢非常頻繁,如果執(zhí)行大量的SELECT,MyISAM是更好的選擇, 沒有事務(wù)。 InnoDB適合: 可靠性要求比較高,或者要求事務(wù); 表更新和查詢都相當(dāng)?shù)念l繁, 大量的INSERT或UPDATE
3.4 鎖機(jī)制
-
MySQL有哪些鎖(全局鎖/表級鎖/行級鎖)
全局鎖
MyISAM 只支持表鎖,InnoDB 支持表鎖和行鎖,默認(rèn)為行鎖。
表級鎖:開銷小,加鎖快,不會出現(xiàn)死鎖。鎖定粒度大,發(fā)生鎖沖突的概率最高,并發(fā)量最低。
- 表鎖:表級別的鎖
- 元數(shù)據(jù)鎖:MDL 全稱為 metadata lock,即元數(shù)據(jù)鎖,一般也可稱為字典鎖。MDL 的主要作用是為了管理數(shù)據(jù)庫對象的并發(fā)訪問和確保元數(shù)據(jù)一致性。
- 意向鎖:意向鎖是放置在資源層次結(jié)構(gòu)的一個級別上的鎖,以保護(hù)較低級別資源上的共享鎖或排它鎖。
- AUTO-INC鎖:AUTO-INC 鎖是特殊的表鎖機(jī)制,鎖不是再一個事務(wù)提交后才釋放,而是再執(zhí)行完插入語句后就會立即釋放。
行級鎖:開銷大,加鎖慢,會出現(xiàn)死鎖。鎖粒度小,發(fā)生鎖沖突的概率小,并發(fā)度最高。
- Record lock:單個行記錄上的鎖;
- Gap lock:間隙鎖,鎖定一個范圍,不包括記錄本身;
- Next-key lock:record+gap 鎖定一個范圍,包含記錄本身。
- 插入意向鎖:插入意向鎖名字雖然有意向鎖,但是它并不是意向鎖,它是一種特殊的間隙鎖,屬于行級別鎖。
-
MySQL是怎么加鎖的
MySQL 行級鎖的加鎖規(guī)則。
唯一索引等值查詢:
- 當(dāng)查詢的記錄是「存在」的,在索引樹上定位到這一條記錄后,將該記錄的索引中的 next-key lock 會退化成「記錄鎖」。
- 當(dāng)查詢的記錄是「不存在」的,在索引樹找到第一條大于該查詢記錄的記錄后,將該記錄的索引中的 next-key lock 會退化成「間隙鎖」。
非唯一索引等值查詢:
- 當(dāng)查詢的記錄「存在」時,由于不是唯一索引,所以肯定存在索引值相同的記錄,于是非唯一索引等值查詢的過程是一個掃描的過程,直到掃描到第一個不符合條件的二級索引記錄就停止掃描,然后在掃描的過程中,對掃描到的二級索引記錄加的是 next-key 鎖,而對于第一個不符合條件的二級索引記錄,該二級索引的 next-key 鎖會退化成間隙鎖。同時,在符合查詢條件的記錄的主鍵索引上加記錄鎖。
- 當(dāng)查詢的記錄「不存在」時,掃描到第一條不符合條件的二級索引記錄,該二級索引的 next-key 鎖會退化成間隙鎖。因為不存在滿足查詢條件的記錄,所以不會對主鍵索引加鎖。
非唯一索引和主鍵索引的范圍查詢的加鎖規(guī)則不同之處在于:
- 唯一索引在滿足一些條件的時候,索引的 next-key lock 退化為間隙鎖或者記錄鎖。
- 非唯一索引范圍查詢,索引的 next-key lock 不會退化為間隙鎖和記錄鎖。
-
MySQL記錄鎖+間隙鎖解決幻讀問題
在 MySQL 的可重復(fù)讀隔離級別下,針對當(dāng)前讀的語句會對索引加記錄鎖+間隙鎖,這樣可以避免其他事務(wù)執(zhí)行增、刪、改時導(dǎo)致幻讀的問題。
有一點(diǎn)要注意的是,在執(zhí)行 update、delete、select … for update 等具有加鎖性質(zhì)的語句,一定要檢查語句是否走了索引,如果是全表掃描的話,會對每一個索引加 next-key 鎖,相當(dāng)于把整個表鎖住了,這是挺嚴(yán)重的問題。
-
死鎖的四個必要條件
-
互斥條件:一個資源每次只能被一個進(jìn)程使用;
-
請求與保持條件:一個進(jìn)程因請求資源而阻塞時,對已獲得的資源保持不放;
-
不剝奪條件:進(jìn)程已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪;
-
循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系;
-
-
如何解決MySQL死鎖問題
死鎖是指兩個或多個事務(wù)在同一資源上相互占用,并請求鎖定對方的資源,從而導(dǎo)致惡性循環(huán)的現(xiàn)象。
常見的解決死鎖的方法
- 如果不同程序并發(fā)存取多個表,盡量約定 以相同的順序訪問表,可以大大降低死鎖機(jī)會;
- 在同一個事務(wù)中,盡可能做到 一次鎖定所需要的所有資源,減少死鎖產(chǎn)生概率;
- 對于非常容易產(chǎn)生死鎖的業(yè)務(wù)部分,可以嘗試使用 升級鎖定顆粒度,通過 表級鎖 定來減少死鎖產(chǎn)生的概率。
-
數(shù)據(jù)庫悲觀鎖和樂觀鎖的原理和應(yīng)用場景
悲觀鎖,先獲取鎖,再進(jìn)行業(yè)務(wù)操作,一般就是利用類似 SELECT … FOR UPDATE 這樣的語句,對數(shù)據(jù)加鎖,避免其他事務(wù)意外修改數(shù)據(jù)。 當(dāng)數(shù)據(jù)庫執(zhí)行SELECT … FOR UPDATE時會獲取被select中的數(shù)據(jù)行的行鎖,select for update獲取的行鎖會在當(dāng)前事務(wù)結(jié)束時自動釋放,因此必須在事務(wù)中使用。
樂觀鎖,先進(jìn)行業(yè)務(wù)操作,只在最后實際更新數(shù)據(jù)時進(jìn)行檢查數(shù)據(jù)是否被更新過。Java 并發(fā)包中的 AtomicFieldUpdater 類似,也是利用 CAS 機(jī)制,并不會對數(shù)據(jù)加鎖,而是通過對比數(shù)據(jù)的時間戳或者版本號,來實現(xiàn)樂觀鎖需要的版本判斷。
3.5 其他面試題
-
MySQL的內(nèi)部構(gòu)造一般可以分為哪兩個部分
可以分為服務(wù)層和存儲引擎層兩部分,其中:
服務(wù)層包括連接器、查詢緩存、分析器、優(yōu)化器、執(zhí)行器等,涵蓋MySQL的大多數(shù)核心服務(wù)功能,以及所有的內(nèi)置函數(shù)(如日期、時間、數(shù)學(xué)和加密函數(shù)等),所有跨存儲引擎的功能都在這一層實現(xiàn),比如存儲過程、觸發(fā)器、視圖等。
存儲引擎層負(fù)責(zé)數(shù)據(jù)的存儲和提取。其架構(gòu)模式是插件式的,支持InnoDB、MyISAM、Memory等多個存儲引擎?,F(xiàn)在最常用的存儲引擎是InnoDB,它從MySQL 5.5.5版本開始成為了默認(rèn)的存儲引擎。
-
undo log、redo log、binlog有什么用
redo log是InnoDB引擎特有的,只記錄該引擎中表的修改記錄。binlog是MySQL的Server層實現(xiàn)的,會記錄所有引擎對數(shù)據(jù)庫的修改。
redo log是物理日志,記錄的是在具體某個數(shù)據(jù)頁上做了什么修改;binlog是邏輯日志,記錄的是這個語句的原始邏輯。
redo log是循環(huán)寫的,空間固定會用完;binlog是可以追加寫入的,binlog文件寫到一定大小后會切換到下一個,并不會覆蓋以前的日志。
補(bǔ)充
1、redolog記錄修改內(nèi)容(哪一頁發(fā)生了什么變化),寫于事務(wù)開始前,用于數(shù)據(jù)未落磁盤,但數(shù)據(jù)庫掛了后的數(shù)據(jù)恢復(fù)
2、binlog記錄修改SQL,寫于事務(wù)提交時,可用于讀寫分離
3、undolog記錄修改前記錄,用于回滾和多版本并發(fā)控制 -
什么是Buffer pool
Innodb 存儲引擎設(shè)計了一個緩沖池(*Buffer Pool*),來提高數(shù)據(jù)庫的讀寫性能。
- 當(dāng)讀取數(shù)據(jù)時,如果數(shù)據(jù)存在于 Buffer Pool 中,客戶端就會直接讀取 Buffer Pool 中的數(shù)據(jù),否則再去磁盤中讀取。
- 當(dāng)修改數(shù)據(jù)時,首先是修改 Buffer Pool 中數(shù)據(jù)所在的頁,然后將其頁設(shè)置為臟頁,最后由后臺線程將臟頁寫入到磁盤。
緩存什么
InnoDB 會把存儲的數(shù)據(jù)劃分為若干個「頁」,以頁作為磁盤和內(nèi)存交互的基本單位,一個頁的默認(rèn)大小為 16KB。因此,Buffer Pool 同樣需要按「頁」來劃分。
在 MySQL 啟動的時候,InnoDB 會為 Buffer Pool 申請一片連續(xù)的內(nèi)存空間,然后按照默認(rèn)的
16KB
的大小劃分出一個個的頁, Buffer Pool 中的頁就叫做緩存頁。此時這些緩存頁都是空閑的,之后隨著程序的運(yùn)行,才會有磁盤上的頁被緩存到 Buffer Pool 中。Innodb 通過三種鏈表來管理緩頁:
- Free List (空閑頁鏈表),管理空閑頁;
- Flush List (臟頁鏈表),管理臟頁;
- LRU List,管理臟頁+干凈頁,將最近且經(jīng)常查詢的數(shù)據(jù)緩存在其中,而不常查詢的數(shù)據(jù)就淘汰出去。;
InnoDB 對 LRU 做了一些優(yōu)化,我們熟悉的 LRU 算法通常是將最近查詢的數(shù)據(jù)放到 LRU 鏈表的頭部,而 InnoDB 做 2 點(diǎn)優(yōu)化:
- 將 LRU 鏈表 分為young 和 old 兩個區(qū)域,加入緩沖池的頁,優(yōu)先插入 old 區(qū)域;頁被訪問時,才進(jìn)入 young 區(qū)域,目的是為了解決預(yù)讀失效的問題。
- 當(dāng)**「頁被訪問」且「 old 區(qū)域停留時間超過
innodb_old_blocks_time
閾值(默認(rèn)為1秒)」**時,才會將頁插入到 young 區(qū)域,否則還是插入到 old 區(qū)域,目的是為了解決批量數(shù)據(jù)訪問,大量熱數(shù)據(jù)淘汰的問題。
可以通過調(diào)整
innodb_old_blocks_pct
參數(shù),設(shè)置 young 區(qū)域和 old 區(qū)域比例。 -
DROP、DELETE 與 TRUNCATE 的區(qū)別
三種都可以表示刪除,其中的細(xì)微區(qū)別之處如下:
DROP DELETE TRUNCATE SQL 語句類型 DDL DML DDL 回滾 不可回滾 可回滾 不可回滾 刪除內(nèi)容 從數(shù)據(jù)庫中 刪除表,所有的數(shù)據(jù)行,索引和權(quán)限也會被刪除 表結(jié)構(gòu)還在,刪除表的 全部或者一部分?jǐn)?shù)據(jù)行 表結(jié)構(gòu)還在,刪除表中的 所有數(shù)據(jù) 刪除速度 刪除速度最快 刪除速度慢,需要逐行刪除 刪除速度快 因此,在不再需要一張表的時候,采用 DROP;在想刪除部分?jǐn)?shù)據(jù)行時候,用 DELETE;在保留表而刪除所有數(shù)據(jù)的時候用 TRUNCATE。
-
SQL語法中內(nèi)連接、自連接、外連接(左、右、全)、交叉連接的區(qū)別分別是什么
-
內(nèi)連接:只有兩個元素表相匹配的才能在結(jié)果集中顯示。
-
外連接:
-
左外連接: 左邊為驅(qū)動表,驅(qū)動表的數(shù)據(jù)全部顯示,匹配表的不匹配的不會顯示。
-
右外連接:右邊為驅(qū)動表,驅(qū)動表的數(shù)據(jù)全部顯示,匹配表的不匹配的不會顯示。
-
全外連接:連接的表中不匹配的數(shù)據(jù)全部會顯示出來。
- 交叉連接: 笛卡爾效應(yīng),顯示的結(jié)果是鏈接表數(shù)的乘積。
-
-
MySQL中CHAR和VARCHAR的區(qū)別有哪些
- char的長度是不可變的,用空格填充到指定長度大小,而varchar的長度是可變的。
- char的存取數(shù)度還是要比varchar要快得多
- char的存儲方式是:對英文字符(ASCII)占用1個字節(jié),對一個漢字占用兩個字節(jié)。varchar的存儲方式是:對每個英文字符占用2個字節(jié),漢字也占用2個字節(jié)
-
數(shù)據(jù)庫中的主鍵、超鍵、候選鍵、外鍵是什么
- 超鍵:在關(guān)系中能唯一標(biāo)識元組的屬性集稱為關(guān)系模式的超鍵
- 候選鍵:不含有多余屬性的超鍵稱為候選鍵。也就是在候選鍵中,若再刪除屬性,就不是鍵了!
- 主鍵:用戶選作元組標(biāo)識的一個候選鍵程序主鍵
- 外鍵:如果關(guān)系模式R中屬性K是其它模式的主鍵,那么k在模式R中稱為外鍵。
主鍵為候選鍵的子集,候選鍵為超鍵的子集,而外鍵的確定是相對于主鍵的。
-
MySQL優(yōu)化
- 為搜索字段創(chuàng)建索引
- 避免使用 Select *,列出需要查詢的字段
- 垂直分割分表
- 選擇正確的存儲引擎
-
SQL語句執(zhí)行流程
Server層按順序執(zhí)行sql的步驟為:
- 客戶端請求->
- 連接器(驗證用戶身份,給予權(quán)限) ->
- 查詢緩存(存在緩存則直接返回,不存在則執(zhí)行后續(xù)操作)->
- 分析器(對SQL進(jìn)行詞法分析和語法分析操作) ->
- 優(yōu)化器(主要對執(zhí)行的sql優(yōu)化選擇最優(yōu)的執(zhí)行方案方法) ->
- 執(zhí)行器(執(zhí)行時會先看用戶是否有執(zhí)行權(quán)限,有才去使用這個引擎提供的接口)->
- 去引擎層獲取數(shù)據(jù)返回(如果開啟查詢緩存則會緩存查詢結(jié)果)
簡單概括:
- 連接器:管理連接、權(quán)限驗證;
- 查詢緩存:命中緩存則直接返回結(jié)果;
- 分析器:對SQL進(jìn)行詞法分析、語法分析;(判斷查詢的SQL字段是否存在也是在這步)
- 優(yōu)化器:執(zhí)行計劃生成、選擇索引;
- 執(zhí)行器:操作引擎、返回結(jié)果;
- 存儲引擎:存儲數(shù)據(jù)、提供讀寫接口。
-
數(shù)據(jù)庫三范式是什么
- 第一范式:強(qiáng)調(diào)的是列的原子性,即數(shù)據(jù)庫表的每一列都是不可分割的原子數(shù)據(jù)項;
- 第二范式:要求實體的屬性完全依賴于主關(guān)鍵字。所謂完全依賴是指不能存在僅依賴主關(guān)鍵字一部分的屬性;
- 第三范式:任何非主屬性不依賴于其它非主屬性。
-
對MVCC的了解
數(shù)據(jù)庫并發(fā)場景:
- 讀-讀:不存在任何問題,也不需要并發(fā)控制;
- 讀-寫:有線程安全問題,可能會造成事務(wù)隔離性問題,可能遇到臟讀,幻讀,不可重復(fù)讀;
- 寫-寫:有線程安全問題,可能會存在更新丟失問題。
多版本并發(fā)控制(MVCC)是一種用來解決讀-寫沖突的無鎖并發(fā)控制,也就是為事務(wù)分配單向增長的時間戳,為每個修改保存一個版本,版本與事務(wù)時間戳關(guān)聯(lián),讀操作只讀該事務(wù)開始前的數(shù)據(jù)庫的快照。
MVCC 可以為數(shù)據(jù)庫解決以下問題:
- 在并發(fā)讀寫數(shù)據(jù)庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數(shù)據(jù)庫并發(fā)讀寫的性能;
- 同時還可以解決臟讀,幻讀,不可重復(fù)讀等事務(wù)隔離問題,但不能解決更新丟失問題。
-
主從復(fù)制中涉及到哪三個線程?
主要涉及三個線程:binlog 線程、I/O 線程和 SQL 線程。
- binlog 線程 :負(fù)責(zé)將主服務(wù)器上的數(shù)據(jù)更改寫入二進(jìn)制日志(Binary log)中。
- I/O 線程 :負(fù)責(zé)從主服務(wù)器上讀取二進(jìn)制日志,并寫入從服務(wù)器的重放日志(Relay log)中。
- SQL 線程 :負(fù)責(zé)讀取重放日志并重放其中的 SQL 語句。
-
數(shù)據(jù)庫如何保證持久性
主要是利用Innodb的redo log。重寫日志, 正如之前說的,MySQL是先把磁盤上的數(shù)據(jù)加載到內(nèi)存中,在內(nèi)存中對數(shù)據(jù)進(jìn)行修改,再寫回到磁盤上。如果此時突然宕機(jī),內(nèi)存中的數(shù)據(jù)就會丟失。 怎么解決這個問題? 簡單啊,事務(wù)提交前直接把數(shù)據(jù)寫入磁盤就行啊。 這么做有什么問題?
- 只修改一個頁面里的一個字節(jié),就要將整個頁面刷入磁盤,太浪費(fèi)資源了。畢竟一個頁面16kb大小,你只改其中一點(diǎn)點(diǎn)東西,就要將16kb的內(nèi)容刷入磁盤,聽著也不合理。
- 畢竟一個事務(wù)里的SQL可能牽涉到多個數(shù)據(jù)頁的修改,而這些數(shù)據(jù)頁可能不是相鄰的,也就是屬于隨機(jī)IO。顯然操作隨機(jī)IO,速度會比較慢。
于是,決定采用redo log解決上面的問題。當(dāng)做數(shù)據(jù)修改的時候,不僅在內(nèi)存中操作,還會在redo log中記錄這次操作。當(dāng)事務(wù)提交的時候,會將redo log日志進(jìn)行刷盤(redo log一部分在內(nèi)存中,一部分在磁盤上)。當(dāng)數(shù)據(jù)庫宕機(jī)重啟的時候,會將redo log中的內(nèi)容恢復(fù)到數(shù)據(jù)庫中,再根據(jù)undo log和binlog內(nèi)容決定回滾數(shù)據(jù)還是提交數(shù)據(jù)。
采用redo log的好處?
其實好處就是將redo log進(jìn)行刷盤比對數(shù)據(jù)頁刷盤效率高,具體表現(xiàn)如下:
- redo log體積小,畢竟只記錄了哪一頁修改了啥,因此體積小,刷盤快。
- redo log是一直往末尾進(jìn)行追加,屬于順序IO。效率顯然比隨機(jī)IO來的快。
-
數(shù)據(jù)庫如何保證原子性
主要是利用 Innodb 的undo log。 undo log名為回滾日志,是實現(xiàn)原子性的關(guān)鍵,當(dāng)事務(wù)回滾時能夠撤銷所有已經(jīng)成功執(zhí)行的 SQL語句,他需要記錄你要回滾的相應(yīng)日志信息。 例如
- 當(dāng)你delete一條數(shù)據(jù)的時候,就需要記錄這條數(shù)據(jù)的信息,回滾的時候,insert這條舊數(shù)據(jù)
- 當(dāng)你update一條數(shù)據(jù)的時候,就需要記錄之前的舊值,回滾的時候,根據(jù)舊值執(zhí)行update操作
- 當(dāng)年insert一條數(shù)據(jù)的時候,就需要這條記錄的主鍵,回滾的時候,根據(jù)主鍵執(zhí)行delete操作
undo log記錄了這些回滾需要的信息,當(dāng)事務(wù)執(zhí)行失敗或調(diào)用了rollback,導(dǎo)致事務(wù)需要回滾,便可以利用undo log中的信息將數(shù)據(jù)回滾到修改之前的樣子。
-
數(shù)據(jù)庫如何保證一致性
- 從數(shù)據(jù)庫層面,數(shù)據(jù)庫通過原子性、隔離性、持久性來保證一致性。也就是說ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔離性)、D(持久性)是手段,是為了保證一致性,數(shù)據(jù)庫提供的手段。數(shù)據(jù)庫必須要實現(xiàn)AID三大特性,才有可能實現(xiàn)一致性。例如,原子性無法保證,顯然一致性也無法保證。
- 從應(yīng)用層面,通過代碼判斷數(shù)據(jù)庫數(shù)據(jù)是否有效,然后決定回滾還是提交數(shù)據(jù)!
-
數(shù)據(jù)庫高并發(fā)的解決方案
- 在web服務(wù)框架中加入緩存。在服務(wù)器與數(shù)據(jù)庫層之間加入緩存層,將高頻訪問的數(shù)據(jù)存入緩存中,減少數(shù)據(jù)庫的讀取負(fù)擔(dān)。
- 增加數(shù)據(jù)庫索引,進(jìn)而提高查詢速度。(不過索引太多會導(dǎo)致速度變慢,并且數(shù)據(jù)庫的寫入會導(dǎo)致索引的更新,也會導(dǎo)致速度變慢)
- 主從讀寫分離,讓主服務(wù)器負(fù)責(zé)寫,從服務(wù)器負(fù)責(zé)讀。
- 將數(shù)據(jù)庫進(jìn)行拆分,使得數(shù)據(jù)庫的表盡可能小,提高查詢的速度。
- 使用分布式架構(gòu),分散計算壓力。
-
數(shù)據(jù)庫結(jié)構(gòu)優(yōu)化的手段
- 范式優(yōu)化: 比如消除冗余(節(jié)省空間。。)
- 反范式優(yōu)化:比如適當(dāng)加冗余等(減少join)
- 限定數(shù)據(jù)的范圍: 務(wù)必禁止不帶任何限制數(shù)據(jù)范圍條件的查詢語句。比如:我們當(dāng)用戶在查詢訂單歷史的時候,我們可以控制在一個月的范圍內(nèi)。
- 讀/寫分離: 經(jīng)典的數(shù)據(jù)庫拆分方案,主庫負(fù)責(zé)寫,從庫負(fù)責(zé)讀;
- 拆分表:分區(qū)將數(shù)據(jù)在物理上分隔開,不同分區(qū)的數(shù)據(jù)可以制定保存在處于不同磁盤上的數(shù)據(jù)文件里。這樣,當(dāng)對這個表進(jìn)行查詢時,只需要在表分區(qū)中進(jìn)行掃描,而不必進(jìn)行全表掃描,明顯縮短了查詢時間,另外處于不同磁盤的分區(qū)也將對這個表的數(shù)據(jù)傳輸分散在不同的磁盤I/O,一個精心設(shè)置的分區(qū)可以將數(shù)據(jù)傳輸對磁盤I/O競爭均勻地分散開。對數(shù)據(jù)量大的時時表可采取此方法??砂丛伦詣咏ū矸謪^(qū)。
-
關(guān)系型和非關(guān)系型數(shù)據(jù)庫的區(qū)別
非關(guān)系型數(shù)據(jù)庫也叫NOSQL,采用鍵值對的形式進(jìn)行存儲。
它的讀寫性能很高,易于擴(kuò)展,可分為內(nèi)存性數(shù)據(jù)庫以及文檔型數(shù)據(jù)庫,比如 Redis,Mongodb,HBase等等。
合使用非關(guān)系型數(shù)據(jù)庫的場景:
- 日志系統(tǒng)、地理位置存儲、數(shù)據(jù)量巨大、高可用
- 關(guān)系型數(shù)據(jù)庫的優(yōu)點(diǎn)
- 容易理解。因為它采用了關(guān)系模型來組織數(shù)據(jù)。
- 可以保持?jǐn)?shù)據(jù)的一致性。
- 數(shù)據(jù)更新的開銷比較小。
- 支持復(fù)雜查詢(帶where子句的查詢)
- 非關(guān)系型數(shù)據(jù)庫的優(yōu)點(diǎn)
- 不需要經(jīng)過SQL層的解析,讀寫效率高。
- 基于鍵值對,數(shù)據(jù)的擴(kuò)展性很好。
- 可以支持多種類型數(shù)據(jù)的存儲,如圖片,文檔等等
-
數(shù)據(jù)庫為什么要進(jìn)行分庫和分表
分庫與分表的目的在于,減小數(shù)據(jù)庫的單庫單表負(fù)擔(dān),提高查詢性能,縮短查詢時間。
通過分表,可以減少數(shù)據(jù)庫的單表負(fù)擔(dān),將壓力分散到不同的表上,同時因為不同的表上的數(shù)據(jù)量少了,起到提高查詢性能,縮短查詢時間的作用,此外,可以很大的緩解表鎖的問題。 分表策略可以歸納為垂直拆分和水平拆分: 水平分表:取模分表就屬于隨機(jī)分表,而時間維度分表則屬于連續(xù)分表。 如何設(shè)計好垂直拆分,我的建議:將不常用的字段單獨(dú)拆分到另外一張擴(kuò)展表. 將大文本的字段單獨(dú)拆分到另外一張擴(kuò)展表, 將不經(jīng)常修改的字段放在同一張表中,將經(jīng)常改變的字段放在另一張表中。 對于海量用戶場景,可以考慮取模分表,數(shù)據(jù)相對比較均勻,不容易出現(xiàn)熱點(diǎn)和并發(fā)訪問的瓶頸。
庫內(nèi)分表,僅僅是解決了單表數(shù)據(jù)過大的問題,但并沒有把單表的數(shù)據(jù)分散到不同的物理機(jī)上,因此并不能減輕 MySQL 服務(wù)器的壓力,仍然存在同一個物理機(jī)上的資源競爭和瓶頸,包括 CPU、內(nèi)存、磁盤 IO、網(wǎng)絡(luò)帶寬等。
分庫與分表帶來的分布式困境與應(yīng)對之策 數(shù)據(jù)遷移與擴(kuò)容問題----一般做法是通過程序先讀出數(shù)據(jù),然后按照指定的分表策略再將數(shù)據(jù)寫入到各個分表中。 分頁與排序問題----需要在不同的分表中將數(shù)據(jù)進(jìn)行排序并返回,并將不同分表返回的結(jié)果集進(jìn)行匯總和再次排序,最后再返回給用戶。
4. 熟悉計算機(jī)網(wǎng)絡(luò)
4.1 TCP/IP協(xié)議
-
TCP/IP網(wǎng)絡(luò)模型有哪幾層,分別有什么作用
應(yīng)用層
最上層的,也是我們能直接接觸到的就是應(yīng)用層(Application Layer),我們電腦或手機(jī)使用的應(yīng)用軟件都是在應(yīng)用層實現(xiàn)。
所以,應(yīng)用層只需要專注于為用戶提供應(yīng)用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。
應(yīng)用層是不用去關(guān)心數(shù)據(jù)是如何傳輸?shù)?#xff0c;而且應(yīng)用層是工作在操作系統(tǒng)中的用戶態(tài),傳輸層及以下則工作在內(nèi)核態(tài)。
傳輸層
應(yīng)用層的數(shù)據(jù)包會傳給傳輸層,傳輸層(Transport Layer)是為應(yīng)用層提供網(wǎng)絡(luò)支持的。
在傳輸層會有兩個傳輸協(xié)議,分別是 TCP 和 UDP。
TCP 的全稱叫傳輸控制協(xié)議(Transmission Control Protocol),大部分應(yīng)用使用的正是 TCP 傳輸層協(xié)議,比如 HTTP 應(yīng)用層協(xié)議。TCP 相比 UDP 多了很多特性,比如流量控制、超時重傳、擁塞控制等,這些都是為了保證數(shù)據(jù)包能可靠地傳輸給對方。
UDP 相對來說就很簡單,簡單到只負(fù)責(zé)發(fā)送數(shù)據(jù)包,不保證數(shù)據(jù)包是否能抵達(dá)對方,但它實時性相對更好,傳輸效率也高。當(dāng)然,UDP 也可以實現(xiàn)可靠傳輸,把 TCP 的特性在應(yīng)用層上實現(xiàn)就可以,不過要實現(xiàn)一個商用的可靠 UDP 傳輸協(xié)議,也不是一件簡單的事情。
應(yīng)用需要傳輸?shù)臄?shù)據(jù)可能會非常大,如果直接傳輸就不好控制,因此當(dāng)傳輸層的數(shù)據(jù)包大小超過 MSS(TCP 最大報文段長度) ,就要將數(shù)據(jù)包分塊,這樣即使中途有一個分塊丟失或損壞了,只需要重新發(fā)送這一個分塊,而不用重新發(fā)送整個數(shù)據(jù)包。在 TCP 協(xié)議中,我們把每個分塊稱為一個 TCP 段(TCP Segment)。
網(wǎng)絡(luò)層
我們不希望傳輸層協(xié)議處理太多的事情,只需要服務(wù)好應(yīng)用即可,讓其作為應(yīng)用間數(shù)據(jù)傳輸?shù)拿浇?#xff0c;幫助實現(xiàn)應(yīng)用到應(yīng)用的通信,而實際的傳輸功能就交給下一層,也就是網(wǎng)絡(luò)層(Internet Layer)。
網(wǎng)絡(luò)層最常使用的是 IP 協(xié)議(Internet Protocol),IP 協(xié)議會將傳輸層的報文作為數(shù)據(jù)部分,再加上 IP 包頭組裝成 IP 報文,如果 IP 報文大小超過 MTU(以太網(wǎng)中一般為 1500 字節(jié))就會再次進(jìn)行分片,得到一個即將發(fā)送到網(wǎng)絡(luò)的 IP 報文。
IP 協(xié)議的尋址作用是告訴我們?nèi)ネ乱粋€目的地該朝哪個方向走,路由則是根據(jù)「下一個目的地」選擇路徑。尋址更像在導(dǎo)航,路由更像在操作方向盤。
網(wǎng)絡(luò)接口層
生成了 IP 頭部之后,接下來要交給網(wǎng)絡(luò)接口層(Link Layer)在 IP 頭部的前面加上 MAC 頭部,并封裝成數(shù)據(jù)幀(Data frame)發(fā)送到網(wǎng)絡(luò)上。
以太網(wǎng)在判斷網(wǎng)絡(luò)包目的地時和 IP 的方式不同,因此必須采用相匹配的方式才能在以太網(wǎng)中將包發(fā)往目的地,而 MAC 頭部就是干這個用的,所以,在以太網(wǎng)進(jìn)行通訊要用到 MAC 地址。
MAC 頭部是以太網(wǎng)使用的頭部,它包含了接收方和發(fā)送方的 MAC 地址等信息,我們可以通過 ARP 協(xié)議獲取對方的 MAC 地址。
所以說,網(wǎng)絡(luò)接口層主要為網(wǎng)絡(luò)層提供「鏈路級別」傳輸?shù)姆?wù),負(fù)責(zé)在以太網(wǎng)、WiFi 這樣的底層網(wǎng)絡(luò)上發(fā)送原始數(shù)據(jù)包,工作在網(wǎng)卡這個層次,使用 MAC 地址來標(biāo)識網(wǎng)絡(luò)上的設(shè)備。
-
輸入網(wǎng)址到網(wǎng)頁顯示,期間發(fā)生了什么
- 根據(jù)域名,進(jìn)行DNS域名解析;
- 拿到解析的IP地址,建立TCP連接;
- 向IP地址,發(fā)送HTTP請求;
- 服務(wù)器處理請求;
- 返回響應(yīng)結(jié)果;
- 關(guān)閉TCP連接;
- 瀏覽器解析HTML;
- 瀏覽器布局渲染;
背后有哪些技術(shù)
1、查瀏覽器緩存,看看有沒有已經(jīng)緩存好的,如果沒有
2 、檢查本機(jī)host文件,
3、調(diào)用API,Linux下Socket函數(shù) gethostbyname
4、向DNS服務(wù)器發(fā)送DNS請求,查詢本地DNS服務(wù)器,這其中用的是UDP的協(xié)議
5、如果在一個子網(wǎng)內(nèi)采用ARP地址解析協(xié)議進(jìn)行ARP查詢?nèi)绻辉谝粋€子網(wǎng)那就需要對默認(rèn)網(wǎng)關(guān)進(jìn)行DNS查詢,如果還找不到會一直向上找根DNS服務(wù)器,直到最終拿到IP地址(全球400多個根DNS服務(wù)器,由13個不同的組織管理)
6、這個時候我們就有了服務(wù)器的IP地址 以及默認(rèn)的端口號了,http默認(rèn)是80 https是 443 端口號,會,首先嘗試http然后調(diào)用Socket建立TCP連接,
7、經(jīng)過三次握手成功建立連接后,開始傳送數(shù)據(jù),如果正是http協(xié)議的話,就返回就完事了,
8、如果不是http協(xié)議,服務(wù)器會返回一個5開頭的的重定向消息,告訴我們用的是https,那就是說IP沒變,但是端口號從80變成443了,好了,再四次揮手,完事,
9、再來一遍,這次除了上述的端口號從80變成443之外,還會采用SSL的加密技術(shù)來保證傳輸數(shù)據(jù)的安全性,保證數(shù)據(jù)傳輸過程中不被修改或者替換之類的,
10、這次依然是三次握手,溝通好雙方使用的認(rèn)證算法,加密和檢驗算法,在此過程中也會檢驗對方的CA安全證書。
11、確認(rèn)無誤后,開始通信,然后服務(wù)器就會返回你所要訪問的網(wǎng)址的一些數(shù)據(jù),在此過程中會將界面進(jìn)行渲染,牽涉到ajax技術(shù)之類的,直到最后我們看到色彩斑斕的網(wǎng)頁
4.2 HTTP/HTTPS協(xié)議
-
什么是HTTP協(xié)議
HTTP 是超文本傳輸協(xié)議,也就是HyperText Transfer Protocol。
HTTP 的名字「超文本協(xié)議傳輸」,它可以拆成三個部分:
- 超文本
- 傳輸
- 協(xié)議
-
GET和POST
GET 用于獲取資源,而 POST 用于傳輸實體主體。
-
get是獲取數(shù)據(jù),post是修改數(shù)據(jù)
-
get把請求的數(shù)據(jù)放在url上, 以?分割URL和傳輸數(shù)據(jù),參數(shù)之間以&相連,所以get不太安全。而post把數(shù)據(jù)放在HTTP的包體內(nèi)(request body 相對安全)
-
get提交的數(shù)據(jù)最大是2k( 限制實際上取決于瀏覽器), post理論上沒有限制。
-
GET產(chǎn)生一個TCP數(shù)據(jù)包,瀏覽器會把http header和data一并發(fā)送出去,服務(wù)器響應(yīng)200(返回數(shù)據(jù)); POST產(chǎn)生兩個TCP數(shù)據(jù)包,瀏覽器先發(fā)送header,服務(wù)器響應(yīng)100 continue,瀏覽器再發(fā)送data,服務(wù)器響應(yīng)200 ok(返回數(shù)據(jù))。
-
GET請求會被瀏覽器主動緩存,而POST不會,除非手動設(shè)置。
-
本質(zhì)區(qū)別:GET是冪等的,而POST不是冪等的
這里的冪等性:冪等性是指一次和多次請求某一個資源應(yīng)該具有同樣的副作用。簡單來說意味著對同一URL的多個請求應(yīng)該返回同樣的結(jié)果。
正因為它們有這樣的區(qū)別,所以不應(yīng)該且不能用get請求做數(shù)據(jù)的增刪改這些有副作用的操作。因為get請求是冪等的,在網(wǎng)絡(luò)不好的隧道中會嘗試重試。如果用get請求增數(shù)據(jù),會有重復(fù)操作的風(fēng)險,而這種重復(fù)操作可能會導(dǎo)致副作用(瀏覽器和操作系統(tǒng)并不知道你會用get請求去做增操作)。
-
-
HTTP緩存技術(shù)
對于一些具有重復(fù)性的 HTTP 請求,比如每次請求得到的數(shù)據(jù)都一樣的,我們可以把這對「請求-響應(yīng)」的數(shù)據(jù)都緩存在本地,那么下次就直接讀取本地的數(shù)據(jù),不必在通過網(wǎng)絡(luò)獲取服務(wù)器的響應(yīng)了,這樣的話 HTTP/1.1 的性能肯定肉眼可見的提升。
所以,避免發(fā)送 HTTP 請求的方法就是通過緩存技術(shù),HTTP 緩存有兩種實現(xiàn)方式,分別是強(qiáng)制緩存和協(xié)商緩存。
強(qiáng)制緩存
強(qiáng)緩存指的是只要瀏覽器判斷緩存沒有過期,則直接使用瀏覽器的本地緩存,決定是否使用緩存的主動性在于瀏覽器這邊。
強(qiáng)緩存是利用下面這兩個 HTTP 響應(yīng)頭部(Response Header)字段實現(xiàn)的,它們都用來表示資源在客戶端緩存的有效期:
Cache-Control
, 是一個相對時間;Expires
,是一個絕對時間;
如果 HTTP 響應(yīng)頭部同時有 Cache-Control 和 Expires 字段的話,Cache-Control 的優(yōu)先級高于 Expires 。
Cache-control 選項更多一些,設(shè)置更加精細(xì),所以建議使用 Cache-Control 來實現(xiàn)強(qiáng)緩存。具體的實現(xiàn)流程如下:
- 當(dāng)瀏覽器第一次請求訪問服務(wù)器資源時,服務(wù)器會在返回這個資源的同時,在 Response 頭部加上 Cache-Control,Cache-Control 中設(shè)置了過期時間大小;
- 瀏覽器再次請求訪問服務(wù)器中的該資源時,會先通過請求資源的時間與 Cache-Control 中設(shè)置的過期時間大小,來計算出該資源是否過期,如果沒有,則使用該緩存,否則重新請求服務(wù)器;
- 服務(wù)器再次收到請求后,會再次更新 Response 頭部的 Cache-Control。
協(xié)商緩存
協(xié)商緩存就是與服務(wù)端協(xié)商之后,通過協(xié)商結(jié)果來判斷是否使用本地緩存。
協(xié)商緩存可以基于兩種頭部來實現(xiàn)。
第一種:請求頭部中的
If-Modified-Since
字段與響應(yīng)頭部中的Last-Modified
字段實現(xiàn),這兩個字段的意思是:- 響應(yīng)頭部中的
Last-Modified
:標(biāo)示這個響應(yīng)資源的最后修改時間; - 請求頭部中的
If-Modified-Since
:當(dāng)資源過期了,發(fā)現(xiàn)響應(yīng)頭中具有 Last-Modified 聲明,則再次發(fā)起請求的時候帶上 Last-Modified 的時間,服務(wù)器收到請求后發(fā)現(xiàn)有 If-Modified-Since 則與被請求資源的最后修改時間進(jìn)行對比(Last-Modified),如果最后修改時間較新(大),說明資源又被改過,則返回最新資源,HTTP 200 OK;如果最后修改時間較舊(小),說明資源無新修改,響應(yīng) HTTP 304 走緩存。
第二種:請求頭部中的
If-None-Match
字段與響應(yīng)頭部中的ETag
字段,這兩個字段的意思是:- 響應(yīng)頭部中
Etag
:唯一標(biāo)識響應(yīng)資源; - 請求頭部中的
If-None-Match
:當(dāng)資源過期時,瀏覽器發(fā)現(xiàn)響應(yīng)頭里有 Etag,則再次向服務(wù)器發(fā)起請求時,會將請求頭 If-None-Match 值設(shè)置為 Etag 的值。服務(wù)器收到請求后進(jìn)行比對,如果資源沒有變化返回 304,如果資源變化了返回 200。
第一種實現(xiàn)方式是基于時間實現(xiàn)的,第二種實現(xiàn)方式是基于一個唯一標(biāo)識實現(xiàn)的,相對來說后者可以更加準(zhǔn)確地判斷文件內(nèi)容是否被修改,避免由于時間篡改導(dǎo)致的不可靠問題。
當(dāng)使用 ETag 字段實現(xiàn)的協(xié)商緩存的過程:
-
當(dāng)瀏覽器第一次請求訪問服務(wù)器資源時,服務(wù)器會在返回這個資源的同時,在 Response 頭部加上 ETag 唯一標(biāo)識,這個唯一標(biāo)識的值是根據(jù)當(dāng)前請求的資源生成的;
-
當(dāng)瀏覽器再次請求訪問服務(wù)器中的該資源時,首先會先檢查強(qiáng)制緩存是否過期:
- 如果沒有過期,則直接使用本地緩存;
- 如果緩存過期了,會在 Request 頭部加上 If-None-Match 字段,該字段的值就是 ETag 唯一標(biāo)識;
-
服務(wù)器再次收到請求后,
會根據(jù)請求中的 If-None-Match 值與當(dāng)前請求的資源生成的唯一標(biāo)識進(jìn)行比較:
- 如果值相等,則返回 304 Not Modified,不會返回資源;
- 如果不相等,則返回 200 狀態(tài)碼和返回資源,并在 Response 頭部加上新的 ETag 唯一標(biāo)識;
-
如果瀏覽器收到 304 的請求響應(yīng)狀態(tài)碼,則會從本地緩存中加載資源,否則更新資源。
-
HTTP 如何實現(xiàn)長連接?在什么時候會超時?
通過在頭部(請求和響應(yīng)頭)設(shè)置 Connection: keep-alive,HTTP1.0協(xié)議支持,但是默認(rèn)關(guān)閉,從HTTP1.1協(xié)議以后,連接默認(rèn)都是長連接
1、HTTP 一般會有 httpd 守護(hù)進(jìn)程,里面可以設(shè)置 keep-alive timeout,當(dāng) tcp 鏈接閑置超過這個時間就會關(guān)閉,也可以在 HTTP 的 header 里面設(shè)置超時時間
2、TCP 的 keep-alive 包含三個參數(shù),支持在系統(tǒng)內(nèi)核的 net.ipv4 里面設(shè)置:當(dāng) TCP 鏈接之后,閑置了 tcp_keepalive_time,則會發(fā)生偵測包,如果沒有收到對方的 ACK,那么會每隔 tcp_keepalive_intvl 再發(fā)一次,直到發(fā)送了 tcp_keepalive_probes,就會丟棄該鏈接。
(1)tcp_keepalive_intvl = 15
(2)tcp_keepalive_probes = 5
(3)tcp_keepalive_time = 1800實際上 HTTP 沒有長短鏈接,只有 TCP 有,TCP 長連接可以復(fù)用一個 TCP 鏈接來發(fā)起多次 HTTP 請求,這樣可以減少資源消耗,比如一次請求 HTML,可能需要請求后續(xù)的 JS/CSS/圖片等
-
HTTP 1.0/1.1/2.0/3.0的介紹、優(yōu)點(diǎn)和缺點(diǎn)
HTTP/1.0
1996年5月,HTTP/1.0 版本發(fā)布,為了提高系統(tǒng)的效率,HTTP/1.0規(guī)定瀏覽器與服務(wù)器只保持短暫的連接,瀏覽器的每次請求都需要與服務(wù)器建立一個TCP連接,服務(wù)器完成請求處理后立即斷開TCP連接,服務(wù)器不跟蹤每個客戶也不記錄過去的請求。
HTTP/1.0中瀏覽器與服務(wù)器只保持短暫的連接,連接無法復(fù)用。也就是說每個TCP連接只能發(fā)送一個請求。發(fā)送數(shù)據(jù)完畢,連接就關(guān)閉,如果還要請求其他資源,就必須再新建一個連接。
我們知道TCP連接的建立需要三次握手,是很耗費(fèi)時間的一個過程。所以,HTTP/1.0版本的性能比較差。
HTTP/1.1
HTTP 最突出的優(yōu)點(diǎn)是「簡單、靈活和易于擴(kuò)展、應(yīng)用廣泛和跨平臺」。
HTTP 協(xié)議里有優(yōu)缺點(diǎn)一體的雙刃劍,分別是「無狀態(tài)、明文傳輸」,同時還有一大缺點(diǎn)「不安全」。
為了解決HTTP/1.0存在的缺陷,HTTP/1.1于1999年誕生。相比較于HTTP/1.0來說,最主要的改進(jìn)就是引入了持久連接。所謂的持久連接即TCP連接默認(rèn)不關(guān)閉,可以被多個請求復(fù)用。
客戶端和服務(wù)器發(fā)現(xiàn)對方一段時間沒有活動,就可以主動關(guān)閉連接?;蛘呖蛻舳嗽谧詈笠粋€請求時,主動告訴服務(wù)端要關(guān)閉連接。
HTTP/1.1版還引入了管道機(jī)制(pipelining),即在同一個TCP連接里面,客戶端可以同時發(fā)送多個請求。這樣就進(jìn)一步改進(jìn)了HTTP協(xié)議的效率。
有了持久連接和管道,大大的提升了HTTP的效率。但是服務(wù)端還是順序執(zhí)行的,效率還有提升的空間。
HTTP/2
HTTP/2 是 HTTP 協(xié)議自 1999 年 HTTP 1.1 發(fā)布后的首個更新,主要基于 SPDY 協(xié)議。
HTTP/2 為了解決HTTP/1.1中仍然存在的效率問題,HTTP/2 采用了多路復(fù)用。即在一個連接里,客戶端和瀏覽器都可以同時發(fā)送多個請求或回應(yīng),而且不用按照順序一一對應(yīng)。能這樣做有一個前提,就是HTTP/2進(jìn)行了二進(jìn)制分幀,即 HTTP/2 會將所有傳輸?shù)男畔⒎指顬楦〉南⒑蛶?#xff08;frame),并對它們采用二進(jìn)制格式的編碼。
而這個負(fù)責(zé)拆分、組裝請求和二進(jìn)制幀的一層就叫做二進(jìn)制分幀層。
除此之外,還有一些其他的優(yōu)化,比如做Header壓縮、服務(wù)端推送等。
Header壓縮就是壓縮老板和員工之間的對話。
服務(wù)端推送就是員工事先把一些老板可能詢問的事情提現(xiàn)發(fā)送到老板的手機(jī)(緩存)上。這樣老板想要知道的時候就可以直接讀取短信(緩存)了。
HTTP/2 相比 HTTP/1.1 性能上的改進(jìn):
- 頭部壓縮
- 二進(jìn)制格式
- 并發(fā)傳輸
- 服務(wù)器主動推送資源
HTTP/2 有什么缺陷?
HTTP/2 通過 Stream 的并發(fā)能力,解決了 HTTP/1 隊頭阻塞的問題,看似很完美了,但是 HTTP/2 還是存在“隊頭阻塞”的問題,只不過問題不是在 HTTP 這一層面,而是在 TCP 這一層。
HTTP/2 是基于 TCP 協(xié)議來傳輸數(shù)據(jù)的,TCP 是字節(jié)流協(xié)議,TCP 層必須保證收到的字節(jié)數(shù)據(jù)是完整且連續(xù)的,這樣內(nèi)核才會將緩沖區(qū)里的數(shù)據(jù)返回給 HTTP 應(yīng)用,那么當(dāng)「前 1 個字節(jié)數(shù)據(jù)」沒有到達(dá)時,后收到的字節(jié)數(shù)據(jù)只能存放在內(nèi)核緩沖區(qū)里,只有等到這 1 個字節(jié)數(shù)據(jù)到達(dá)時,HTTP/2 應(yīng)用層才能從內(nèi)核中拿到數(shù)據(jù),這就是 HTTP/2 隊頭阻塞問題。
HTTP3.0
HTTP/2 雖然具有多個流并發(fā)傳輸?shù)哪芰?#xff0c;但是傳輸層是 TCP 協(xié)議,于是存在以下缺陷:
- 隊頭阻塞,HTTP/2 多個請求跑在一個 TCP 連接中,如果序列號較低的 TCP 段在網(wǎng)絡(luò)傳輸中丟失了,即使序列號較高的 TCP 段已經(jīng)被接收了,應(yīng)用層也無法從內(nèi)核中讀取到這部分?jǐn)?shù)據(jù),從 HTTP 視角看,就是多個請求被阻塞了;
- TCP 和 TLS 握手時延,TCP 三次握手和 TLS 四次握手,共有 3-RTT 的時延;
- 連接遷移需要重新連接,移動設(shè)備從 4G 網(wǎng)絡(luò)環(huán)境切換到 WiFi 時,由于 TCP 是基于四元組來確認(rèn)一條 TCP 連接的,那么網(wǎng)絡(luò)環(huán)境變化后,就會導(dǎo)致 IP 地址或端口變化,于是 TCP 只能斷開連接,然后再重新建立連接,切換網(wǎng)絡(luò)環(huán)境的成本高;
HTTP/3 就將傳輸層從 TCP 替換成了 UDP,并在 UDP 協(xié)議上開發(fā)了 QUIC 協(xié)議,來保證數(shù)據(jù)的可靠傳輸。
QUIC 協(xié)議的特點(diǎn):
- 無隊頭阻塞,QUIC 連接上的多個 Stream 之間并沒有依賴,都是獨(dú)立的,也不會有底層協(xié)議限制,某個流發(fā)生丟包了,只會影響該流,其他流不受影響;
- 建立連接速度快,因為 QUIC 內(nèi)部包含 TLS 1.3,因此僅需 1 個 RTT 就可以「同時」完成建立連接與 TLS 密鑰協(xié)商,甚至在第二次連接的時候,應(yīng)用數(shù)據(jù)包可以和 QUIC 握手信息(連接信息 + TLS 信息)一起發(fā)送,達(dá)到 0-RTT 的效果。
- 連接遷移,QUIC 協(xié)議沒有用四元組的方式來“綁定”連接,而是通過「連接 ID 」來標(biāo)記通信的兩個端點(diǎn),客戶端和服務(wù)器可以各自選擇一組 ID 來標(biāo)記自己,因此即使移動設(shè)備的網(wǎng)絡(luò)變化后,導(dǎo)致 IP 地址變化了,只要仍保有上下文信息(比如連接 ID、TLS 密鑰等),就可以“無縫”地復(fù)用原連接,消除重連的成本;
另外 HTTP/3 的 QPACK 通過兩個特殊的單向流來同步雙方的動態(tài)表,解決了 HTTP/2 的 HPACK 隊頭阻塞問題。
-
HTTPS的介紹
HTTPS 并不是新協(xié)議,而是讓 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是說 HTTPS 使用了隧道進(jìn)行通信。通過使用 SSL,HTTPS 具有了加密(防竊聽)、認(rèn)證(防偽裝)和完整性保護(hù)(防篡改)。
-
HTTPS是如何保證數(shù)據(jù)傳輸?shù)陌踩?#xff0c;整體的流程是什么?(SSL是怎么工作保證安全的)
(1)客戶端向服務(wù)器端發(fā)起SSL連接請求;
(2) 服務(wù)器把公鑰發(fā)送給客戶端,并且服務(wù)器端保存著唯一的私鑰
(3)客戶端用公鑰對雙方通信的對稱秘鑰進(jìn)行加密,并發(fā)送給服務(wù)器端
(4)服務(wù)器利用自己唯一的私鑰對客戶端發(fā)來的對稱秘鑰進(jìn)行解密,
(5)進(jìn)行數(shù)據(jù)傳輸,服務(wù)器和客戶端雙方用公有的相同的對稱秘鑰對數(shù)據(jù)進(jìn)行加密解密,可以保證在數(shù)據(jù)收發(fā)過程中的安全,即是第三方獲得數(shù)據(jù)包,也無法對其進(jìn)行加密,解密和篡改。
因為數(shù)字簽名、摘要是證書防偽非常關(guān)鍵的武器。 “摘要”就是對傳輸?shù)膬?nèi)容,通過hash算法計算出一段固定長度的串。通過發(fā)送方的私鑰對這段摘要進(jìn)行加密,加密后得到的結(jié)果就是“數(shù)字簽名”
SSL/TLS協(xié)議的基本思路是采用公鑰加密法,也就是說,客戶端先向服務(wù)器端索要公鑰,然后用公鑰加密信息,服務(wù)器收到密文后,用自己的私鑰解密。
補(bǔ)充:SSL/TLS的四次握手,目前網(wǎng)上的主流答案都在重復(fù)阮一峰老師的博客,屬于TLS 1.0版本的答案,使用RSA密鑰交換算法。但是現(xiàn)在TLS 1.2已經(jīng)成為主流,使用ECDHE算法,如果面試可以說出這個版本的答案,應(yīng)該會更好。
-
什么是SSL/TLS
SSL代表安全套接字層。它是一種用于加密和驗證應(yīng)用程序(如瀏覽器)和Web服務(wù)器之間發(fā)送的數(shù)據(jù)的協(xié)議。 身份驗證 , 加密Https的加密機(jī)制是一種共享密鑰加密和公開密鑰加密并用的混合加密機(jī)制。
SSL/TLS協(xié)議作用:認(rèn)證用戶和服務(wù),加密數(shù)據(jù),維護(hù)數(shù)據(jù)的完整性的應(yīng)用層協(xié)議加密和解密需要兩個不同的密鑰,故被稱為非對稱加密;加密和解密都使用同一個密鑰的
對稱加密:優(yōu)點(diǎn)在于加密、解密效率通常比較高 ,HTTPS 是基于非對稱加密的, 公鑰是公開的。
-
TLS 握手過程
TLS 協(xié)議是如何解決 HTTP 的風(fēng)險的呢?
- 信息加密: HTTP 交互信息是被加密的,第三方就無法被竊取;
- 校驗機(jī)制:校驗信息傳輸過程中是否有被第三方篡改過,如果被篡改過,則會有警告提示;
- 身份證書:證明淘寶是真的淘寶網(wǎng);
TLS 的握手過程,其中每一個「框」都是一個記錄(record),記錄是 TLS 收發(fā)數(shù)據(jù)的基本單位,類似于 TCP 里的 segment。多個記錄可以組合成一個 TCP 包發(fā)送,所以通常經(jīng)過「四個消息」就可以完成 TLS 握手,也就是需要 2個 RTT 的時延,然后就可以在安全的通信環(huán)境里發(fā)送 HTTP 報文,實現(xiàn) HTTPS 協(xié)議。
HTTPS 是應(yīng)用層協(xié)議,需要先完成 TCP 連接建立,然后走 TLS 握手過程后,才能建立通信安全的連接。
-
HTTPS RSA握手過程
HTTPS 采用混合的加密機(jī)制,使用非對稱密鑰加密用于傳輸對稱密鑰來保證傳輸過程的安全性,之后使用對稱密鑰加密進(jìn)行通信來保證通信過程的效率。
確保傳輸安全過程(其實就是rsa原理):
- Client給出協(xié)議版本號、一個客戶端生成的隨機(jī)數(shù)(Client random),以及客戶端支持的加密方法。
- Server確認(rèn)雙方使用的加密方法,并給出數(shù)字證書、以及一個服務(wù)器生成的隨機(jī)數(shù)(Server random)。
- Client確認(rèn)數(shù)字證書有效,然后生成呀一個新的隨機(jī)數(shù)(Premaster secret),并使用數(shù)字證書中的公鑰,加密這個隨機(jī)數(shù),發(fā)給Server。
- Server使用自己的私鑰,獲取Client發(fā)來的隨機(jī)數(shù)(Premaster secret)。
- Client和Server根據(jù)約定的加密方法,使用前面的三個隨機(jī)數(shù),生成”對話密鑰”(session key),用來加密接下來的整個對話過程。
使用 RSA 密鑰協(xié)商算法的最大問題是不支持前向保密。因為客戶端傳遞隨機(jī)數(shù)(用于生成對稱加密密鑰的條件之一)給服務(wù)端時使用的是公鑰加密的,服務(wù)端收到后,會用私鑰解密得到隨機(jī)數(shù)。所以一旦服務(wù)端的私鑰泄漏了,過去被第三方截獲的所有 TLS 通訊密文都會被破解。
為了解決這個問題,后面就出現(xiàn)了 ECDHE 密鑰協(xié)商算法。
-
HTTPS ECDHE握手過程
ECDHE 算法是在 DHE 算法的基礎(chǔ)上利用了 ECC 橢圓曲線特性,可以用更少的計算量計算出公鑰,以及最終的會話密鑰。
TLS 第一次握手
客戶端首先會發(fā)一個「Client Hello」消息,消息里面有客戶端使用的 TLS 版本號、支持的密碼套件列表,以及生成的隨機(jī)數(shù)(*Client Random*)。
TLS 第二次握手
服務(wù)端收到客戶端的「打招呼」,同樣也要回禮,會返回「Server Hello」消息,消息面有服務(wù)器確認(rèn)的 TLS 版本號,也給出了一個隨機(jī)數(shù)(*Server Random*),然后從客戶端的密碼套件列表選擇了一個合適的密碼套件。
接著,服務(wù)端為了證明自己的身份,發(fā)送「Certificate」消息,會把證書也發(fā)給客戶端。
這一步就和 RSA 握手過程有很大的區(qū)別了,因為服務(wù)端選擇了 ECDHE 密鑰協(xié)商算法,所以會在發(fā)送完證書后,發(fā)送「Server Key Exchange」消息。
這個過程服務(wù)器做了三件事:
- 選擇了名為 x25519 的橢圓曲線,選好了橢圓曲線相當(dāng)于橢圓曲線基點(diǎn) G 也定好了,這些都會公開給客戶端;
- 生成隨機(jī)數(shù)作為服務(wù)端橢圓曲線的私鑰,保留到本地;
- 根據(jù)基點(diǎn) G 和私鑰計算出服務(wù)端的橢圓曲線公鑰,這個會公開給客戶端。
為了保證這個橢圓曲線的公鑰不被第三方篡改,服務(wù)端會用 RSA 簽名算法給服務(wù)端的橢圓曲線公鑰做個簽名。
隨后,就是「Server Hello Done」消息,服務(wù)端跟客戶端表明:“這些就是我提供的信息,打招呼完畢”。
至此,TLS 兩次握手就已經(jīng)完成了,目前客戶端和服務(wù)端通過明文共享了這幾個信息:Client Random、Server Random 、使用的橢圓曲線、橢圓曲線基點(diǎn) G、服務(wù)端橢圓曲線的公鑰,這幾個信息很重要,是后續(xù)生成會話密鑰的材料。
TLS 第三次握手
客戶端收到了服務(wù)端的證書后,自然要校驗證書是否合法,如果證書合法,那么服務(wù)端到身份就是沒問題的。校驗證書的過程會走證書鏈逐級驗證,確認(rèn)證書的真實性,再用證書的公鑰驗證簽名,這樣就能確認(rèn)服務(wù)端的身份了,確認(rèn)無誤后,就可以繼續(xù)往下走。
客戶端會生成一個隨機(jī)數(shù)作為客戶端橢圓曲線的私鑰,然后再根據(jù)服務(wù)端前面給的信息,生成客戶端的橢圓曲線公鑰,然后用「Client Key Exchange」消息發(fā)給服務(wù)端。
算好會話密鑰后,客戶端會發(fā)一個「Change Cipher Spec」消息,告訴服務(wù)端后續(xù)改用對稱算法加密通信。
接著,客戶端會發(fā)「Encrypted Handshake Message」消息,把之前發(fā)送的數(shù)據(jù)做一個摘要,再用對稱密鑰加密一下,讓服務(wù)端做個驗證,驗證下本次生成的對稱密鑰是否可以正常使用。
TLS 第四次握手
最后,服務(wù)端也會有一個同樣的操作,發(fā)「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果雙方都驗證加密和解密沒問題,那么握手正式完成。于是,就可以正常收發(fā)加密的 HTTP 請求和響應(yīng)了。
-
RSA 和 ECDHE 握手過程的區(qū)別
- RSA 密鑰協(xié)商算法「不支持」前向保密,ECDHE 密鑰協(xié)商算法「支持」前向保密;
- 使用了 RSA 密鑰協(xié)商算法,TLS 完成四次握手后,才能進(jìn)行應(yīng)用數(shù)據(jù)傳輸,而對于 ECDHE 算法,客戶端可以不用等服務(wù)端的最后一次 TLS 握手,就可以提前發(fā)出加密的 HTTP 數(shù)據(jù),節(jié)省了一個消息的往返時間(這個是 RFC 文檔規(guī)定的,具體原因文檔沒有說明,所以這點(diǎn)我也不太明白);
- 使用 ECDHE, 在 TLS 第 2 次握手中,會出現(xiàn)服務(wù)器端發(fā)出的「Server Key Exchange」消息,而 RSA 握手過程沒有該消息;
-
FIN_WAIT_2,CLOSE_WAIT狀態(tài)和TIME_WAIT狀態(tài)
- FIN_WAIT_2:
- 半關(guān)閉狀態(tài)。
- 發(fā)送斷開請求一方還有接收數(shù)據(jù)能力,但已經(jīng)沒有發(fā)送數(shù)據(jù)能力。
- CLOSE_WAIT狀態(tài):
- 被動關(guān)閉連接一方接收到FIN包會立即回應(yīng)ACK包表示已接收到斷開請求。
- 被動關(guān)閉連接一方如果還有剩余數(shù)據(jù)要發(fā)送就會進(jìn)入CLOSE_WAIT狀態(tài)。
- TIME_WAIT狀態(tài):
- 又叫2MSL等待狀態(tài)。
- 如果客戶端直接進(jìn)入CLOSED狀態(tài),如果服務(wù)端沒有接收到最后一次ACK包會在超時之后重新再發(fā)FIN包,此時因為客戶端已經(jīng)CLOSED,所以服務(wù)端就不會收到ACK而是收到RST。所以TIME_WAIT狀態(tài)目的是防止最后一次握手?jǐn)?shù)據(jù)沒有到達(dá)對方而觸發(fā)重傳FIN準(zhǔn)備的。
- 在2MSL時間內(nèi),同一個socket不能再被使用,否則有可能會和舊連接數(shù)據(jù)混淆(如果新連接和舊連接的socket相同的話)。
- FIN_WAIT_2:
-
對稱密鑰加密和非對稱密鑰加密
對稱秘鑰加密
對稱密鑰加密(Symmetric-Key Encryption),加密和解密使用同一密鑰。
- 優(yōu)點(diǎn):運(yùn)算速度快
- 缺點(diǎn):無法安全地將密鑰傳輸給通信方
非對稱秘鑰加密
非對稱密鑰加密,又稱公開密鑰加密(Public-Key Encryption),加密和解密使用不同的密鑰。
公開密鑰所有人都可以獲得,通信發(fā)送方獲得接收方的公開密鑰之后,就可以使用公開密鑰進(jìn)行加密,接收方收到通信內(nèi)容后使用私有密鑰解密。
非對稱密鑰除了用來加密,還可以用來進(jìn)行簽名。因為私有密鑰無法被其他人獲取,因此通信發(fā)送方使用其私有密鑰進(jìn)行簽名,通信接收方使用發(fā)送方的公開密鑰對簽名進(jìn)行解密,就能判斷這個簽名是否正確。
- 優(yōu)點(diǎn):可以更安全地將公開密鑰傳輸給通信發(fā)送方;
- 缺點(diǎn):運(yùn)算速度慢。
-
HTTPS如何優(yōu)化
對于硬件優(yōu)化的方向,因為 HTTPS 是屬于計算密集型,應(yīng)該選擇計算力更強(qiáng)的 CPU,而且最好選擇支持 AES-NI 特性的 CPU,這個特性可以在硬件級別優(yōu)化 AES 對稱加密算法,加快應(yīng)用數(shù)據(jù)的加解密。
對于軟件優(yōu)化的方向,如果可以,把軟件升級成較新的版本,比如將 Linux 內(nèi)核 2.X 升級成 4.X,將 openssl 1.0.1 升級到 1.1.1,因為新版本的軟件不僅會提供新的特性,而且還會修復(fù)老版本的問題。
對于協(xié)議優(yōu)化的方向:
- 密鑰交換算法應(yīng)該選擇 ECDHE 算法,而不用 RSA 算法,因為 ECDHE 算法具備前向安全性,而且客戶端可以在第三次握手之后,就發(fā)送加密應(yīng)用數(shù)據(jù),節(jié)省了 1 RTT。
- 將 TLS1.2 升級 TLS1.3,因為 TLS1.3 的握手過程只需要 1 RTT,而且安全性更強(qiáng)。
對于證書優(yōu)化的方向:
- 服務(wù)器應(yīng)該選用 ECDSA 證書,而非 RSA 證書,因為在相同安全級別下,ECC 的密鑰長度比 RSA 短很多,這樣可以提高證書傳輸?shù)男?#xff1b;
- 服務(wù)器應(yīng)該開啟 OCSP Stapling 功能,由服務(wù)器預(yù)先獲得 OCSP 的響應(yīng),并把響應(yīng)結(jié)果緩存起來,這樣 TLS 握手的時候就不用再訪問 CA 服務(wù)器,減少了網(wǎng)絡(luò)通信的開銷,提高了證書驗證的效率;
對于重連 HTTPS 時,我們可以使用一些技術(shù)讓客戶端和服務(wù)端使用上一次 HTTPS 連接使用的會話密鑰,直接恢復(fù)會話,而不用再重新走完整的 TLS 握手過程。
常見的會話重用技術(shù)有 Session ID 和 Session Ticket,用了會話重用技術(shù),當(dāng)再次重連 HTTPS 時,只需要 1 RTT 就可以恢復(fù)會話。對于 TLS1.3 使用 Pre-shared Key 會話重用技術(shù),只需要 0 RTT 就可以恢復(fù)會話。
這些會話重用技術(shù)雖然好用,但是存在一定的安全風(fēng)險,它們不僅不具備前向安全,而且有重放攻擊的風(fēng)險,所以應(yīng)當(dāng)對會話密鑰設(shè)定一個合理的過期時間。
-
WebSocket協(xié)議
websocket是一種瀏覽器與服務(wù)器進(jìn)行全雙工通信的網(wǎng)絡(luò)技術(shù),屬于 應(yīng)用層協(xié)議。它 基于TCP傳輸協(xié)議,并 復(fù)用HTTP 的握手通道,用來彌補(bǔ)HTTP協(xié)議在持久通信能力上的不足。
- ws 默認(rèn)端口:80
- wss 默認(rèn)端口:443
- Websocket 通過HTTP協(xié)議握手。
websocket的特點(diǎn)有哪些?
- 節(jié)省資源開銷,HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了;
- 更強(qiáng)的實時性,由于協(xié)議是全雙工通信,所以服務(wù)器可以主動給客戶端推送數(shù)據(jù),相對于HTTP請求需要等待客戶端發(fā)起請求服務(wù)端才能響應(yīng),延遲明顯更少;
- 保持連接狀態(tài),能夠記錄用戶狀態(tài),通信時可以省略部分狀態(tài)信息,不像HTTP每次都需要攜帶用戶認(rèn)證信息;
- 更好的二進(jìn)制支持,Websocket定義了二進(jìn)制幀,相對HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。
-
HTTP如何實現(xiàn)長連接
HTTP長連接
- 瀏覽器向服務(wù)器進(jìn)行一次HTTP會話訪問后,并不會直接關(guān)閉這個連接,而是會默認(rèn)保持一段時間,那么下一次瀏覽器繼續(xù)訪問的時候就會再次利用到這個連接。
- 在
HTTP/1.1
版本中,默認(rèn)的連接都是長連接,我們可以通過Connection: keep-alive
字段進(jìn)行指定。
TCP保活機(jī)制
-
為什么要有?;顧C(jī)制?
- 第一點(diǎn)自然是我們這篇文章的主題,通過?;顧C(jī)制,我們可以保證通訊雙方的連接不被釋放掉
- 第二點(diǎn)就是在另一些情況下,如果客戶端或者服務(wù)器發(fā)生了錯誤或者宕機(jī),那么就可以依靠這種?;顧C(jī)制探測出網(wǎng)絡(luò)通信出現(xiàn)了問題,進(jìn)而可以釋放掉這種錯誤連接。
-
?;顧C(jī)制
首先?;顧C(jī)制的工作原理就是,通過在服務(wù)器端設(shè)置一個保活定時器,當(dāng)定時器開始工作后就定時的向網(wǎng)絡(luò)通信的另一端發(fā)出?;钐綔y的TCP報文,如果接收到了ACK報文,那么就證明對方存活,可以繼續(xù)保有連接;否則就證明網(wǎng)絡(luò)存在故障。
上面只是在原理層面簡單的介紹,根據(jù)文獻(xiàn)[1],我們可以了解到詳細(xì)的內(nèi)容:
- 如果一個給定的連接在兩個小時之內(nèi)沒有任何動作,則服務(wù)器就向客戶發(fā)送一個探查報文段。客戶主機(jī)必須處于以下 4個狀態(tài)之一。
- 狀態(tài)1:客戶主機(jī)依然正常運(yùn)行,并從服務(wù)器可達(dá)。客戶的TCP響應(yīng)正常,而服務(wù)器也知道對方是正常工作的。服務(wù)器在兩小時以后將?;疃〞r器復(fù)位。如果在兩個小時定時器到時間之前有應(yīng)用程序的通信量通過此連接,則定時器在交換數(shù)據(jù)后的未來2小時再復(fù)位。
- 狀態(tài)2:客戶主機(jī)已經(jīng)崩潰,并且關(guān)閉或者正在重新啟動。在任何一種情況下,客戶的TCP都沒有響應(yīng)。服務(wù)器將不能夠收到對探查的響應(yīng),并在75秒后超時。服務(wù)器總共發(fā)送10個這樣的探查,每個間隔75秒。如果服務(wù)器沒有收到一個響應(yīng),它就認(rèn)為客戶主機(jī)已經(jīng)關(guān)閉并終止連接。
- 狀態(tài)3:客戶主機(jī)崩潰并已經(jīng)重新啟動。這時服務(wù)器將收到一個對其保活探查的響應(yīng),但是這個響應(yīng)是一個復(fù)位,使得服務(wù)器終止這個連接。
- 狀態(tài)4:客戶主機(jī)正常運(yùn)行,但是從服務(wù)器不可達(dá)。這與狀態(tài)2相同,因為TCP不能夠區(qū)分狀態(tài)4與狀態(tài)2之間的區(qū)別,它所能發(fā)現(xiàn)的就是沒有收到探查的響應(yīng)。
-
HTTP和HTTPS的區(qū)別
Http協(xié)議運(yùn)行在TCP之上,明文傳輸,客戶端與服務(wù)器端都無法驗證對方的身份;Https是身披SSL(Secure Socket Layer)外殼的Http,運(yùn)行于SSL上,SSL運(yùn)行于TCP之上,是添加了加密和認(rèn)證機(jī)制的HTTP。二者之間存在如下不同:
1、端口不同:Http與Https使用不同的連接方式,用的端口也不一樣,前者是80,后者是443;
2、資源消耗:和HTTP通信相比,Https通信會由于加減密處理消耗更多的CPU和內(nèi)存資源;
3、開銷:Https通信需要證書,而證書一般需要向認(rèn)證機(jī)構(gòu)購買;
4、安全性:HTTP 的連接很簡單,是無狀態(tài)的;HTTPS 協(xié)議是由 TLS+HTTP 協(xié)議構(gòu)建的可進(jìn)行加密傳輸、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議,比 HTTP 協(xié)議安全
Https的加密機(jī)制是一種共享密鑰加密和公開密鑰加密并用的混合加密機(jī)制。websocket應(yīng)用場景有哪些?
- 即時通信、直播、游戲、在線協(xié)同工具(騰訊文檔等)、實時數(shù)據(jù)拉取和推送地圖
-
HTTP請求方法有多少
客戶端發(fā)送的 請求報文 第一行為請求行,包含了方法字段。
根據(jù) HTTP 標(biāo)準(zhǔn),HTTP 請求可以使用多種請求方法。
HTTP1.0 定義了三種請求方法: GET, POST 和 HEAD方法。
HTTP1.1 新增六種請求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
序 號 方法 描述 1 GET 請求指定的頁面信息,并返回實體主體。 2 HEAD 類似于 GET 請求,只不過返回的響應(yīng)中沒有具體的內(nèi)容,用于獲取報頭 3 POST 向指定資源提交數(shù)據(jù)進(jìn)行處理請求(例如提交表單或者上傳文件)。數(shù)據(jù)被包含在請求體中。POST 請求可能會導(dǎo)致新的資源的建立和/或已有資源的修改。 4 PUT 從客戶端向服務(wù)器傳送的數(shù)據(jù)取代指定的文檔的內(nèi)容。 5 DELETE 請求服務(wù)器刪除指定的頁面。 6 CONNECT HTTP/1.1 協(xié)議中預(yù)留給能夠?qū)⑦B接改為管道方式的代理服務(wù)器。 7 OPTIONS 允許客戶端查看服務(wù)器的性能。 8 TRACE 回顯服務(wù)器收到的請求,主要用于測試或診斷。 9 PATCH 是對 PUT 方法的補(bǔ)充,用來對已知資源進(jìn)行局部更新 。
4.3 TCP三次握手/四次揮手
-
TCP三次握手和四次揮手的過程
三次握手過程
- 初始狀態(tài):客戶端處于
closed(關(guān)閉)
狀態(tài),服務(wù)器處于listen(監(jiān)聽)
狀態(tài)。 - 第一次握手:客戶端發(fā)送請求報文將
SYN = 1
同步序列號和初始化序列號seq = x
發(fā)送給服務(wù)端,發(fā)送完之后客戶端處于SYN_Send
狀態(tài)。(驗證了客戶端的發(fā)送能力和服務(wù)端的接收能力) - 第二次握手:服務(wù)端受到
SYN
請求報文之后,如果同意連接,會以自己的同步序列號SYN(服務(wù)端) = 1
、初始化序列號seq = y
和確認(rèn)序列號(期望下次收到的數(shù)據(jù)包)ack = x+ 1
以及確認(rèn)號ACK = 1
報文作為應(yīng)答,服務(wù)器為SYN_Receive
狀態(tài)。(問題來了,兩次握手之后,站在客戶端角度上思考:我發(fā)送和接收都o(jì)k,服務(wù)端的發(fā)送和接收也都o(jì)k。但是站在服務(wù)端的角度思考:哎呀,我服務(wù)端接收ok,但是我不清楚我的發(fā)送ok不ok呀,而且我還不知道你接受能力如何呢?所以老哥,你需要給我三次握手來傳個話告訴我一聲。你要是不告訴我,萬一我認(rèn)為你跑了,然后我可能出于安全性的考慮繼續(xù)給你發(fā)一次,看看你回不回我。) - 第三次握手: 客戶端接收到服務(wù)端的
SYN + ACK
之后,知道可以下次可以發(fā)送了下一序列的數(shù)據(jù)包了,然后發(fā)送同步序列號ack = y + 1
和數(shù)據(jù)包的序列號seq = x + 1
以及確認(rèn)號ACK = 1
確認(rèn)包作為應(yīng)答,客戶端轉(zhuǎn)為established
狀態(tài)。(分別站在雙方的角度上思考,各自ok)
為什么需要三次握手,兩次不行嗎
弄清這個問題,我們需要先弄明白三次握手的目的是什么,能不能只用兩次握手來達(dá)到同樣的目的。
- 第一次握手:客戶端發(fā)送網(wǎng)絡(luò)包,服務(wù)端收到了。 這樣服務(wù)端就能得出結(jié)論:客戶端的發(fā)送能力、服務(wù)端的接收能力是正常的。
- 第二次握手:服務(wù)端發(fā)包,客戶端收到了。 這樣客戶端就能得出結(jié)論:服務(wù)端的接收、發(fā)送能力,客戶端的接收、發(fā)送能力是正常的。不過此時服務(wù)器并不能確認(rèn)客戶端的接收能力是否正常。
- 第三次握手:客戶端發(fā)包,服務(wù)端收到了。 這樣服務(wù)端就能得出結(jié)論:客戶端的接收、發(fā)送能力正常,服務(wù)器自己的發(fā)送、接收能力也正常。
三個方面分析三次握手的原因:
- 三次握手才可以阻止重復(fù)歷史連接的初始化(主要原因)
- 三次握手才可以同步雙方的初始序列號
- 三次握手才可以避免資源浪費(fèi)
TCP 建立連接時,通過三次握手能防止歷史連接的建立,能減少雙方不必要的資源開銷,能幫助雙方同步初始化序列號。序列號能夠保證數(shù)據(jù)包不重復(fù)、不丟棄和按序傳輸。
不使用「兩次握手」和「四次握手」的原因:
- 「兩次握手」:無法防止歷史連接的建立,會造成雙方資源的浪費(fèi),也無法可靠的同步雙方序列號;
- 「四次握手」:三次握手就已經(jīng)理論上最少可靠連接建立,所以不需要使用更多的通信次數(shù)。
四次揮手過程
- 初始化狀態(tài):客戶端和服務(wù)端都在連接狀態(tài),接下來開始進(jìn)行四次分手?jǐn)嚅_連接操作。
- 第一次分手:第一次分手無論是客戶端還是服務(wù)端都可以發(fā)起,因為 TCP 是全雙工的。
假如客戶端發(fā)送的數(shù)據(jù)已經(jīng)發(fā)送完畢,發(fā)送FIN = 1 告訴服務(wù)端,客戶端所有數(shù)據(jù)已經(jīng)全發(fā)完了,服務(wù)端你可以關(guān)閉接收了,但是如果你們服務(wù)端有數(shù)據(jù)要發(fā)給客戶端,客戶端照樣可以接收的。此時客戶端處于FIN = 1等待服務(wù)端確認(rèn)釋放連接狀態(tài)。
- 第二次分手:服務(wù)端接收到客戶端的釋放請求連接之后,知道客戶端沒有數(shù)據(jù)要發(fā)給自己了,然后服務(wù)端發(fā)送ACK = 1告訴客戶端收到你發(fā)給我的信息,此時服務(wù)端處于 CLOSE_WAIT 等待關(guān)閉狀態(tài)。(服務(wù)端先回應(yīng)給客戶端一聲,我知道了,但服務(wù)端的發(fā)送數(shù)據(jù)能力即將等待關(guān)閉,于是接下來第三次就來了。)
- 第三次分手:此時服務(wù)端向客戶端把所有的數(shù)據(jù)發(fā)送完了,然后發(fā)送一個FIN = 1,用于告訴客戶端,服務(wù)端的所有數(shù)據(jù)發(fā)送完畢,客戶端你也可以關(guān)閉接收數(shù)據(jù)連接了。此時服務(wù)端狀態(tài)處于LAST_ACK狀態(tài),來等待確認(rèn)客戶端是否收到了自己的請求。(服務(wù)端等客戶端回復(fù)是否收到呢,不收到的話,服務(wù)端不知道客戶端是不是掛掉了還是咋回事呢,所以服務(wù)端不敢關(guān)閉自己的接收能力,于是第四次就來了。)
- 第四次分手:此時如果客戶端收到了服務(wù)端發(fā)送完的信息之后,就發(fā)送ACK = 1,告訴服務(wù)端,客戶端已經(jīng)收到了你的信息。有一個 2 MSL 的延遲等待。
為什么需要揮手四次
因為當(dāng)服務(wù)端收到客戶端的SYN連接請求報文后,可以直接發(fā)送SYN+ACK報文。其中ACK報文是用來應(yīng)答的,SYN報文是用來同步的。但是關(guān)閉連接時,當(dāng)服務(wù)端收到FIN報文時,很可能并不會立即關(guān)閉SOCKET,所以只能先回復(fù)一個ACK報文,告訴客戶端,“你發(fā)的FIN報文收到了”。只有等到我服務(wù)端所有的報文都發(fā)送完了,我才能發(fā)送FIN報文,因此不能一起發(fā)送。故需要四次揮手。
- 初始狀態(tài):客戶端處于
-
TCP協(xié)議介紹
TCP(Transmission Control Protocol 傳輸控制協(xié)議)是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。
特點(diǎn)
- TCP是面向連接的。
- 每一條TCP連接只能有兩個端點(diǎn),每一條TCP連接只能是點(diǎn)對點(diǎn)的(一對一);
- TCP提供可靠交付的服務(wù)。通過TCP連接傳送的數(shù)據(jù),無差錯、不丟失、不重復(fù)、并且按序到達(dá);
- TCP提供全雙工通信。TCP允許通信雙方的應(yīng)用進(jìn)程在任何時候都能發(fā)送數(shù)據(jù)。TCP連接的兩端都設(shè)有發(fā)送緩存和接收緩存,用來臨時存放雙方通信的數(shù)據(jù);
- 面向字節(jié)流。TCP中的“流”(stream)指的是流入進(jìn)程或從進(jìn)程流出的字節(jié)序列?!懊嫦蜃止?jié)流”的含義是:雖然應(yīng)用程序和TCP的交互是一次一個數(shù)據(jù)塊(大小不等),但TCP把應(yīng)用程序交下來的數(shù)據(jù)僅僅看成是一連串的無結(jié)構(gòu)的字節(jié)流。
-
TCP三次握手、四次揮手丟失,會發(fā)生什么
三次握手丟失
第一次握手丟失了,會發(fā)生什么?
如果客戶端遲遲收不到服務(wù)端的 SYN-ACK 報文(第二次握手),就會觸發(fā)「超時重傳」機(jī)制,重傳 SYN 報文,而且重傳的 SYN 報文的序列號都是一樣的。每次超時的時間是上一次的 2 倍。
第二次握手丟失了,會發(fā)生什么?
因為第二次握手報文里是包含對客戶端的第一次握手的 ACK 確認(rèn)報文,所以,如果客戶端遲遲沒有收到第二次握手,那么客戶端就覺得可能自己的 SYN 報文(第一次握手)丟失了,于是客戶端就會觸發(fā)超時重傳機(jī)制,重傳 SYN 報文。
然后,因為第二次握手中包含服務(wù)端的 SYN 報文,所以當(dāng)客戶端收到后,需要給服務(wù)端發(fā)送 ACK 確認(rèn)報文(第三次握手),服務(wù)端才會認(rèn)為該 SYN 報文被客戶端收到了。
那么,如果第二次握手丟失了,服務(wù)端就收不到第三次握手,于是服務(wù)端這邊會觸發(fā)超時重傳機(jī)制,重傳 SYN-ACK 報文。
因此,當(dāng)?shù)诙挝帐謥G失了,客戶端和服務(wù)端都會重傳
第三次握手丟失了,會發(fā)生什么?
客戶端收到服務(wù)端的 SYN-ACK 報文后,就會給服務(wù)端回一個 ACK 報文,也就是第三次握手,此時客戶端狀態(tài)進(jìn)入到
ESTABLISH
狀態(tài)。因為這個第三次握手的 ACK 是對第二次握手的 SYN 的確認(rèn)報文,所以當(dāng)?shù)谌挝帐謥G失了,如果服務(wù)端那一方遲遲收不到這個確認(rèn)報文,就會觸發(fā)超時重傳機(jī)制,重傳 SYN-ACK 報文,直到收到第三次握手,或者達(dá)到最大重傳次數(shù)。
注意,ACK 報文是不會有重傳的,當(dāng) ACK 丟失了,就由對方重傳對應(yīng)的報文。
揮手四次丟失
第一次揮手丟失了,會發(fā)生什么?
如果第一次揮手丟失了,那么客戶端遲遲收不到被動方的 ACK 的話,也就會觸發(fā)超時重傳機(jī)制,重傳 FIN 報文,重發(fā)次數(shù)由
tcp_orphan_retries
參數(shù)控制。當(dāng)客戶端重傳 FIN 報文的次數(shù)超過
tcp_orphan_retries
后,就不再發(fā)送 FIN 報文,則會在等待一段時間(時間為上一次超時時間的 2 倍),如果還是沒能收到第二次揮手,那么直接進(jìn)入到close
狀態(tài)。第二次揮手丟失了,會發(fā)生什么?
當(dāng)服務(wù)端收到客戶端的第一次揮手后,就會先回一個 ACK 確認(rèn)報文,此時服務(wù)端的連接進(jìn)入到
CLOSE_WAIT
狀態(tài)。在前面我們也提了,ACK 報文是不會重傳的,所以如果服務(wù)端的第二次揮手丟失了,客戶端就會觸發(fā)超時重傳機(jī)制,重傳 FIN 報文,直到收到服務(wù)端的第二次揮手,或者達(dá)到最大的重傳次數(shù)。
第三次揮手丟失了,會發(fā)生什么?
當(dāng)服務(wù)端(被動關(guān)閉方)收到客戶端(主動關(guān)閉方)的 FIN 報文后,內(nèi)核會自動回復(fù) ACK,同時連接處于
CLOSE_WAIT
狀態(tài),顧名思義,它表示等待應(yīng)用進(jìn)程調(diào)用 close 函數(shù)關(guān)閉連接。此時,內(nèi)核是沒有權(quán)利替代進(jìn)程關(guān)閉連接,必須由進(jìn)程主動調(diào)用 close 函數(shù)來觸發(fā)服務(wù)端發(fā)送 FIN 報文。
服務(wù)端處于 CLOSE_WAIT 狀態(tài)時,調(diào)用了 close 函數(shù),內(nèi)核就會發(fā)出 FIN 報文,同時連接進(jìn)入 LAST_ACK 狀態(tài),等待客戶端返回 ACK 來確認(rèn)連接關(guān)閉。
如果遲遲收不到這個 ACK,服務(wù)端就會重發(fā) FIN 報文,重發(fā)次數(shù)仍然由
tcp_orphan_retrie
s 參數(shù)控制,這與客戶端重發(fā) FIN 報文的重傳次數(shù)控制方式是一樣的。第四次揮手丟失了,會發(fā)生什么?
當(dāng)客戶端收到服務(wù)端的第三次揮手的 FIN 報文后,就會回 ACK 報文,也就是第四次揮手,此時客戶端連接進(jìn)入
TIME_WAIT
狀態(tài)。在 Linux 系統(tǒng),TIME_WAIT 狀態(tài)會持續(xù) 2MSL 后才會進(jìn)入關(guān)閉狀態(tài)。
然后,服務(wù)端(被動關(guān)閉方)沒有收到 ACK 報文前,還是處于 LAST_ACK 狀態(tài)。
如果第四次揮手的 ACK 報文沒有到達(dá)服務(wù)端,服務(wù)端就會重發(fā) FIN 報文,重發(fā)次數(shù)仍然由前面介紹過的
tcp_orphan_retries
參數(shù)控制。 -
2MSL等待狀態(tài)
2MSL等待狀態(tài)
TIME_WAIT狀態(tài)也成為2MSL等待狀態(tài)。每個具體TCP實現(xiàn)必須選擇一個報文段最大生存時間MSL(Maximum Segment Lifetime),它是任何報文段被丟棄前在網(wǎng)絡(luò)內(nèi)的最長時間。這個時間是有限的,因為TCP報文段以IP數(shù)據(jù)報在網(wǎng)絡(luò)內(nèi)傳輸,而IP數(shù)據(jù)報則有限制其生存時間的TTL字段。
對一個具體實現(xiàn)所給定的MSL值,處理的原則是:當(dāng)TCP執(zhí)行一個主動關(guān)閉,并發(fā)回最后一個ACK,該連接必須在TIME_WAIT狀態(tài)停留的時間為2倍的MSL。這樣可讓TCP再次發(fā)送最后的ACK以防這個ACK丟失(另一端超時并重發(fā)最后的FIN)。
這種2MSL等待的另一個結(jié)果是這個TCP連接在2MSL等待期間,定義這個連接的插口(客戶的IP地址和端口號,服務(wù)器的IP地址和端口號)不能被使用。這個連接只能在2MSL結(jié)束后才能再被使用。
為什么TIME_WAIT狀態(tài)需要經(jīng)過2MSL才能返回到CLOSE狀態(tài)
理論上,四個報文都發(fā)送完畢,就可以直接進(jìn)入CLOSE狀態(tài)了,但是可能網(wǎng)絡(luò)是不可靠的,有可能最后一個ACK丟失。所以TIME_WAIT狀態(tài)就是用來重發(fā)可能丟失的ACK報文。
客戶端給服務(wù)端發(fā)送的ACK = 1丟失,服務(wù)端等待 1MSL沒收到,然后重新發(fā)送消息需要1MSL。如果再次接收到服務(wù)端的消息,則重啟2MSL計時器,發(fā)送確認(rèn)請求??蛻舳酥恍璧却?MSL,如果沒有再次收到服務(wù)端的消息,就說明服務(wù)端已經(jīng)接收到自己確認(rèn)消息;此時雙方都關(guān)閉的連接,TCP 四次分手完畢
-
TIME_WAIT介紹
MSL
是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網(wǎng)絡(luò)上存在的最長時間,超過這個時間報文將被丟棄。因為 TCP 報文基于是 IP 協(xié)議的,而 IP 頭中有一個TTL
字段,是 IP 數(shù)據(jù)報可以經(jīng)過的最大路由數(shù),每經(jīng)過一個處理他的路由器此值就減 1,當(dāng)此值為 0 則數(shù)據(jù)報將被丟棄,同時發(fā)送 ICMP 報文通知源主機(jī)。MSL 與 TTL 的區(qū)別: MSL 的單位是時間,而 TTL 是經(jīng)過路由跳數(shù)。所以 MSL 應(yīng)該要大于等于 TTL 消耗為 0 的時間,以確保報文已被自然消亡。
TTL 的值一般是 64,Linux 將 MSL 設(shè)置為 30 秒,意味著 Linux 認(rèn)為數(shù)據(jù)報文經(jīng)過 64 個路由器的時間不會超過 30 秒,如果超過了,就認(rèn)為報文已經(jīng)消失在網(wǎng)絡(luò)中了。
TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是: 網(wǎng)絡(luò)中可能存在來自發(fā)送方的數(shù)據(jù)包,當(dāng)這些發(fā)送方的數(shù)據(jù)包被接收方處理后又會向?qū)Ψ桨l(fā)送響應(yīng),所以一來一回需要等待 2 倍的時間。
為什么需要 TIME_WAIT 狀態(tài)?
主動發(fā)起關(guān)閉連接的一方,才會有
TIME-WAIT
狀態(tài)。需要 TIME-WAIT 狀態(tài),主要是兩個原因:
- 防止歷史連接中的數(shù)據(jù),被后面相同四元組的連接錯誤的接收;
- 保證「被動關(guān)閉連接」的一方,能被正確的關(guān)閉;
-
TIME_WAIT 過多有什么危害?
過多的 TIME-WAIT 狀態(tài)主要的危害有兩種:
- 第一是占用系統(tǒng)資源,比如文件描述符、內(nèi)存資源、CPU 資源、線程資源等;
- 第二是占用端口資源,端口資源也是有限的,一般可以開啟的端口為
32768~61000
,也可以通過net.ipv4.ip_local_port_range
參數(shù)指定范圍。
-
如何優(yōu)化 TIME_WAIT?
這里給出優(yōu)化 TIME-WAIT 的幾個方式,都是有利有弊:
- 打開 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 選項;
- net.ipv4.tcp_max_tw_buckets
- 程序中使用 SO_LINGER ,應(yīng)用強(qiáng)制使用 RST 關(guān)閉。
-
什么是半連接隊列
服務(wù)器第一次收到客戶端的 SYN 之后,就會處于 SYN_RCVD 狀態(tài),此時雙方還沒有完全建立其連接,服務(wù)器會把此種狀態(tài)下請求連接放在一個隊列里,我們把這種隊列稱之為半連接隊列。
當(dāng)然還有一個全連接隊列,就是已經(jīng)完成三次握手,建立起連接的就會放在全連接隊列中。如果隊列滿了就有可能會出現(xiàn)丟包現(xiàn)象。
這里在補(bǔ)充一點(diǎn)關(guān)于SYN-ACK 重傳次數(shù)的問題: 服務(wù)器發(fā)送完SYN-ACK包,如果未收到客戶確認(rèn)包,服務(wù)器進(jìn)行首次重傳,等待一段時間仍未收到客戶確認(rèn)包,進(jìn)行第二次重傳。如果重傳次數(shù)超過系統(tǒng)規(guī)定的最大重傳次數(shù),系統(tǒng)將該連接信息從半連接隊列中刪除。 注意,每次重傳等待的時間不一定相同,一般會是指數(shù)增長,例如間隔時間為 1s,2s,4s,8s…
-
常見TCP的連接狀態(tài)有哪些
- CLOSED:初始狀態(tài)。
- LISTEN:服務(wù)器處于監(jiān)聽狀態(tài)。
- SYN_SEND:客戶端socket執(zhí)行CONNECT連接,發(fā)送SYN包,進(jìn)入此狀態(tài)。
- SYN_RECV:服務(wù)端收到SYN包并發(fā)送服務(wù)端SYN包,進(jìn)入此狀態(tài)。
- ESTABLISH:表示連接建立??蛻舳税l(fā)送了最后一個ACK包后進(jìn)入此狀態(tài),服務(wù)端接收到ACK包后進(jìn)入此狀態(tài)。
- FIN_WAIT_1:終止連接的一方(通常是客戶機(jī))發(fā)送了FIN報文后進(jìn)入。等待對方FIN。
- CLOSE_WAIT:(假設(shè)服務(wù)器)接收到客戶機(jī)FIN包之后等待關(guān)閉的階段。在接收到對方的FIN包之后,自然是需要立即回復(fù)ACK包的,表示已經(jīng)知道斷開請求。但是本方是否立即斷開連接(發(fā)送FIN包)取決是否還有數(shù)據(jù)需要發(fā)送給客戶端,若有,則在發(fā)送FIN包之前均為此狀態(tài)。
- FIN_WAIT_2:此時是半連接狀態(tài),即有一方要求關(guān)閉連接,等待另一方關(guān)閉??蛻舳私邮盏椒?wù)器的ACK包,但并沒有立即接收到服務(wù)端的FIN包,進(jìn)入FIN_WAIT_2狀態(tài)。
- LAST_ACK:服務(wù)端發(fā)動最后的FIN包,等待最后的客戶端ACK響應(yīng),進(jìn)入此狀態(tài)。
- TIME_WAIT:客戶端收到服務(wù)端的FIN包,并立即發(fā)出ACK包做最后的確認(rèn),在此之后的2MSL時間稱為TIME_WAIT狀態(tài)。
-
TCP頭部中有哪些信息
- 序號(32bit):傳輸方向上字節(jié)流的字節(jié)編號。初始時序號會被設(shè)置一個隨機(jī)的初始值(ISN),之后每次發(fā)送數(shù)據(jù)時,序號值 = ISN + 數(shù)據(jù)在整個字節(jié)流中的偏移。假設(shè)A -> B且ISN = 1024,第一段數(shù)據(jù)512字節(jié)已經(jīng)到B,則第二段數(shù)據(jù)發(fā)送時序號為1024 + 512。用于解決網(wǎng)絡(luò)包亂序問題。
- 確認(rèn)號(32bit):接收方對發(fā)送方TCP報文段的響應(yīng),其值是收到的序號值 + 1。
- 首部長(4bit):標(biāo)識首部有多少個4字節(jié) * 首部長,最大為15,即60字節(jié)。
- 標(biāo)志位(6bit):
- URG:標(biāo)志緊急指針是否有效。
- ACK:標(biāo)志確認(rèn)號是否有效(確認(rèn)報文段)。用于解決丟包問題。
- PSH:提示接收端立即從緩沖讀走數(shù)據(jù)。
- RST:表示要求對方重新建立連接(復(fù)位報文段)。
- SYN:表示請求建立一個連接(連接報文段)。
- FIN:表示關(guān)閉連接(斷開報文段)。
- 窗口(16bit):接收窗口。用于告知對方(發(fā)送方)本方的緩沖還能接收多少字節(jié)數(shù)據(jù)。用于解決流控。
- 校驗和(16bit):接收端用CRC檢驗整個報文段有無損壞。
-
RTO,RTT和超時重傳分別是什么
- 超時重傳:發(fā)送端發(fā)送報文后若長時間未收到確認(rèn)的報文則需要重發(fā)該報文。可能有以下幾種情況:
- 發(fā)送的數(shù)據(jù)沒能到達(dá)接收端,所以對方?jīng)]有響應(yīng)。
- 接收端接收到數(shù)據(jù),但是ACK報文在返回過程中丟失。
- 接收端拒絕或丟棄數(shù)據(jù)。
- RTO:從上一次發(fā)送數(shù)據(jù),因為長期沒有收到ACK響應(yīng),到下一次重發(fā)之間的時間。就是重傳間隔。
- 通常每次重傳RTO是前一次重傳間隔的兩倍,計量單位通常是RTT。例:1RTT,2RTT,4RTT,8RTT…
- 重傳次數(shù)到達(dá)上限之后停止重傳。
- RTT:數(shù)據(jù)從發(fā)送到接收到對方響應(yīng)之間的時間間隔,即數(shù)據(jù)報在網(wǎng)絡(luò)中一個往返用時。大小不穩(wěn)定。
- 超時重傳:發(fā)送端發(fā)送報文后若長時間未收到確認(rèn)的報文則需要重發(fā)該報文。可能有以下幾種情況:
-
TCP重傳
TCP 實現(xiàn)可靠傳輸?shù)姆绞街?#xff0c;是通過序列號與確認(rèn)應(yīng)答。
在 TCP 中,當(dāng)發(fā)送端的數(shù)據(jù)到達(dá)接收主機(jī)時,接收端主機(jī)會返回一個確認(rèn)應(yīng)答消息,表示已收到消息。
常見的重傳機(jī)制:
- 超時重傳
- 快速重傳
- SACK
- D-SACK
超時重傳
重傳機(jī)制的其中一個方式,就是在發(fā)送數(shù)據(jù)時,設(shè)定一個定時器,當(dāng)超過指定的時間后,沒有收到對方的
ACK
確認(rèn)應(yīng)答報文,就會重發(fā)該數(shù)據(jù),也就是我們常說的超時重傳。TCP 會在以下兩種情況發(fā)生超時重傳:
- 數(shù)據(jù)包丟失
- 確認(rèn)應(yīng)答丟失
RTT
指的是數(shù)據(jù)發(fā)送時刻到接收到確認(rèn)的時刻的差值,也就是包的往返時間。超時重傳時間是以
RTO
(Retransmission Timeout 超時重傳時間)表示。- 當(dāng)超時時間 RTO 較大時,重發(fā)就慢,丟了老半天才重發(fā),沒有效率,性能差;
- 當(dāng)超時時間 RTO 較小時,會導(dǎo)致可能并沒有丟就重發(fā),于是重發(fā)的就快,會增加網(wǎng)絡(luò)擁塞,導(dǎo)致更多的超時,更多的超時導(dǎo)致更多的重發(fā)。
根據(jù)上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應(yīng)該略大于報文往返 RTT 的值。
快速重傳
TCP 還有另外一種快速重傳(Fast Retransmit)機(jī)制,它不以時間為驅(qū)動,而是以數(shù)據(jù)驅(qū)動重傳。
快速重傳的工作方式是當(dāng)收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。
快速重傳機(jī)制只解決了一個問題,就是超時時間的問題,但是它依然面臨著另外一個問題。就是重傳的時候,是重傳一個,還是重傳所有的問題。
SACK 方法
還有一種實現(xiàn)重傳機(jī)制的方式叫:
SACK
( Selective Acknowledgment), 選擇性確認(rèn)。這種方式需要在 TCP 頭部「選項」字段里加一個
SACK
的東西,它可以將已收到的數(shù)據(jù)的信息發(fā)送給「發(fā)送方」,這樣發(fā)送方就可以知道哪些數(shù)據(jù)收到了,哪些數(shù)據(jù)沒收到,知道了這些信息,就可以只重傳丟失的數(shù)據(jù)。Duplicate SACK
Duplicate SACK 又稱
D-SACK
,其主要使用了 SACK 來告訴「發(fā)送方」有哪些數(shù)據(jù)被重復(fù)接收了。D-SACK
有這么幾個好處:- 可以讓「發(fā)送方」知道,是發(fā)出去的包丟了,還是接收方回應(yīng)的 ACK 包丟了;
- 可以知道是不是「發(fā)送方」的數(shù)據(jù)包被網(wǎng)絡(luò)延遲了;
- 可以知道網(wǎng)絡(luò)中是不是把「發(fā)送方」的數(shù)據(jù)包給復(fù)制了;
在 Linux 下可以通過
net.ipv4.tcp_dsack
參數(shù)開啟/關(guān)閉這個功能(Linux 2.4 后默認(rèn)打開)。 -
TCP滑動窗口
TCP 利用滑動窗口實現(xiàn)流量控制的機(jī)制?;瑒哟翱?#xff08;Sliding window)是一種流量控制技術(shù)。早期的網(wǎng)絡(luò)通信中,通信雙方不會考慮網(wǎng)絡(luò)的擁擠情況直接發(fā)送數(shù)據(jù)。由于大家不知道網(wǎng)絡(luò)擁塞狀況,同時發(fā)送數(shù)據(jù),導(dǎo)致中間節(jié)點(diǎn)阻塞掉包,誰也發(fā)不了數(shù)據(jù),就有了滑動窗口機(jī)制來解決此問題。
TCP 中采用滑動窗口來進(jìn)行傳輸控制,滑動窗口的大小意味著接收方還有多大的緩沖區(qū)可以用于接收數(shù)據(jù)。發(fā)送方可以通過滑動窗口的大小來確定應(yīng)該發(fā)送多少字節(jié)的數(shù)據(jù)。當(dāng)滑動窗口為 0 時,發(fā)送方一般不能再發(fā)送數(shù)據(jù)報,但有兩種情況除外,一種情況是可以發(fā)送緊急數(shù)據(jù),例如,允許用戶終止在遠(yuǎn)端機(jī)上的運(yùn)行進(jìn)程。另一種情況是發(fā)送方可以發(fā)送一個 1 字節(jié)的數(shù)據(jù)報來通知接收方重新聲明它希望接收的下一字節(jié)及發(fā)送方的滑動窗口大小。
-
TCP流量控制
TCP 利用滑動窗口實現(xiàn)流量控制。流量控制是為了控制發(fā)送方發(fā)送速率,保證接收方來得及接收。接收方發(fā)送的確認(rèn)報文中的窗口字段可以用來控制發(fā)送方窗口大小,從而影響發(fā)送方的發(fā)送速率。將窗口字段設(shè)置為 0,則發(fā)送方不能發(fā)送數(shù)據(jù)。
- 目的是接收方通過TCP頭窗口字段告知發(fā)送方本方可接收的最大數(shù)據(jù)量,用以解決發(fā)送速率過快導(dǎo)致接收方不能接收的問題。所以流量控制是點(diǎn)對點(diǎn)控制。
- TCP是雙工協(xié)議,雙方可以同時通信,所以發(fā)送方接收方各自維護(hù)一個發(fā)送窗和接收窗。
- 發(fā)送窗:用來限制發(fā)送方可以發(fā)送的數(shù)據(jù)大小,其中發(fā)送窗口的大小由接收端返回的TCP報文段中窗口字段來控制,接收方通過此字段告知發(fā)送方自己的緩沖(受系統(tǒng)、硬件等限制)大小。
- 接收窗:用來標(biāo)記可以接收的數(shù)據(jù)大小。
- TCP是流數(shù)據(jù),發(fā)送出去的數(shù)據(jù)流可以被分為以下四部分:已發(fā)送且被確認(rèn)部分 | 已發(fā)送未被確認(rèn)部分 | 未發(fā)送但可發(fā)送部分 | 不可發(fā)送部分,其中發(fā)送窗 = 已發(fā)送未確認(rèn)部分 + 未發(fā)但可發(fā)送部分。接收到的數(shù)據(jù)流可分為:已接收 | 未接收但準(zhǔn)備接收 | 未接收不準(zhǔn)備接收。接收窗 = 未接收但準(zhǔn)備接收部分。
- 發(fā)送窗內(nèi)數(shù)據(jù)只有當(dāng)接收到接收端某段發(fā)送數(shù)據(jù)的ACK響應(yīng)時才移動發(fā)送窗,左邊緣緊貼剛被確認(rèn)的數(shù)據(jù)。接收窗也只有接收到數(shù)據(jù)且最左側(cè)連續(xù)時才移動接收窗口。
-
TCP擁塞控制
擁塞控制目的是防止數(shù)據(jù)過多注入到網(wǎng)絡(luò)中導(dǎo)致網(wǎng)絡(luò)資源(路由器、交換機(jī)等)過載。因為擁塞控制涉及網(wǎng)絡(luò)鏈路全局,所以屬于全局控制??刂茡砣褂脫砣翱凇?/p>
擁塞控制主要是四個算法:1)慢啟動,2)擁塞避免,3)擁塞發(fā)生,4)快速恢復(fù)。
慢熱啟動算法 – Slow Start
所謂慢啟動,也就是TCP連接剛建立,一點(diǎn)一點(diǎn)地提速,試探一下網(wǎng)絡(luò)的承受能力,以免直接擾亂了網(wǎng)絡(luò)通道的秩序。
慢啟動算法:
- 連接建好的開始先初始化擁塞窗口cwnd大小為1,表明可以傳一個MSS大小的數(shù)據(jù)。
- 每當(dāng)收到一個ACK,cwnd大小加一,呈線性上升。
- 每當(dāng)過了一個往返延遲時間RTT(Round-Trip Time),cwnd大小直接翻倍,乘以2,呈指數(shù)讓升。
- 還有一個ssthresh(slow start threshold),是一個上限,當(dāng)cwnd >= ssthresh時,就會進(jìn)入“擁塞避免算法”
擁塞避免算法 – Congestion Avoidance
如同前邊說的,當(dāng)擁塞窗口大小cwnd大于等于慢啟動閾值ssthresh后,就進(jìn)入擁塞避免算法。算法如下:
- 收到一個ACK,則cwnd = cwnd + 1 / cwnd
- 每當(dāng)過了一個往返延遲時間RTT,cwnd大小加一。
過了慢啟動閾值后,擁塞避免算法可以避免窗口增長過快導(dǎo)致窗口擁塞,而是緩慢的增加調(diào)整到網(wǎng)絡(luò)的最佳值。
擁塞發(fā)生狀態(tài)時的算法
一般來說,TCP擁塞控制默認(rèn)認(rèn)為網(wǎng)絡(luò)丟包是由于網(wǎng)絡(luò)擁塞導(dǎo)致的,所以一般的TCP擁塞控制算法以丟包為網(wǎng)絡(luò)進(jìn)入擁塞狀態(tài)的信號。對于丟包有兩種判定方式,一種是超時重傳RTO[Retransmission Timeout]超時,另一個是收到三個重復(fù)確認(rèn)ACK。
超時重傳是TCP協(xié)議保證數(shù)據(jù)可靠性的一個重要機(jī)制,其原理是在發(fā)送一個數(shù)據(jù)以后就開啟一個計時器,在一定時間內(nèi)如果沒有得到發(fā)送數(shù)據(jù)報的ACK報文,那么就重新發(fā)送數(shù)據(jù),直到發(fā)送成功為止。
但是如果發(fā)送端接收到3個以上的重復(fù)ACK,TCP就意識到數(shù)據(jù)發(fā)生丟失,需要重傳。這個機(jī)制不需要等到重傳定時器超時,所以叫 做快速重傳,而快速重傳后沒有使用慢啟動算法,而是擁塞避免算法,所以這又叫做快速恢復(fù)算法。
超時重傳RTO[Retransmission Timeout]超時,TCP會重傳數(shù)據(jù)包。TCP認(rèn)為這種情況比較糟糕,反應(yīng)也比較強(qiáng)烈:
- 由于發(fā)生丟包,將慢啟動閾值ssthresh設(shè)置為當(dāng)前cwnd的一半,即ssthresh = cwnd / 2.
- cwnd重置為1
- 進(jìn)入慢啟動過程
最為早期的TCP Tahoe算法就只使用上述處理辦法,但是由于一丟包就一切重來,導(dǎo)致cwnd又重置為1,十分不利于網(wǎng)絡(luò)數(shù)據(jù)的穩(wěn)定傳遞。
所以,TCP Reno算法進(jìn)行了優(yōu)化。當(dāng)收到三個重復(fù)確認(rèn)ACK時,TCP開啟快速重傳Fast Retransmit算法,而不用等到RTO超時再進(jìn)行重傳:
- cwnd大小縮小為當(dāng)前的一半
- ssthresh設(shè)置為縮小后的cwnd大小
- 然后進(jìn)入快速恢復(fù)算法Fast Recovery。
快速恢復(fù)算法 – Fast Recovery
TCP Tahoe是早期的算法,所以沒有快速恢復(fù)算法,而Reno算法有。在進(jìn)入快速恢復(fù)之前,cwnd和ssthresh已經(jīng)被更改為原有cwnd的一半??焖倩謴?fù)算法的邏輯如下:
- cwnd = cwnd + 3 MSS,加3 MSS的原因是因為收到3個重復(fù)的ACK。
- 重傳DACKs指定的數(shù)據(jù)包。
- 如果再收到DACKs,那么cwnd大小增加一。
- 如果收到新的ACK,表明重傳的包成功了,那么退出快速恢復(fù)算法。將cwnd設(shè)置為ssthresh,然后進(jìn)入擁塞避免算法。
-
流量控制和擁塞控制的區(qū)別
- 流量控制屬于通信雙方協(xié)商;擁塞控制涉及通信鏈路全局。
- 流量控制需要通信雙方各維護(hù)一個發(fā)送窗、一個接收窗,對任意一方,接收窗大小由自身決定,發(fā)送窗大小由接收方響應(yīng)的TCP報文段中窗口值確定;擁塞控制的擁塞窗口大小變化由試探性發(fā)送一定數(shù)據(jù)量數(shù)據(jù)探查網(wǎng)絡(luò)狀況后而自適應(yīng)調(diào)整。
- 實際最終發(fā)送窗口 = min{流控發(fā)送窗口,擁塞窗口}。
-
TCP 協(xié)議如何保證可靠傳輸
- 確認(rèn)和重傳:接收方收到報文就會確認(rèn),發(fā)送方發(fā)送一段時間后沒有收到確認(rèn)就會重傳。
- 數(shù)據(jù)校驗:TCP報文頭有校驗和,用于校驗報文是否損壞。
- 對失序數(shù)據(jù)包重排序:既然 TCP 報文段作為 IP 數(shù)據(jù)報來傳輸,而 IP 數(shù)據(jù)報的到達(dá)可能會失序,因此 TCP 報文段的到達(dá)也可能會失序。TCP 將對失序數(shù)據(jù)進(jìn)行重新排序,才交給應(yīng)用層;
- 流量控制:當(dāng)接收方來不及處理發(fā)送方的數(shù)據(jù),能通過滑動窗口,提示發(fā)送方降低發(fā)送的速率,防止包丟失。
- 擁塞控制:當(dāng)網(wǎng)絡(luò)擁塞時,通過擁塞窗口,減少數(shù)據(jù)的發(fā)送,防止包丟失。
- **丟棄重復(fù)數(shù)據(jù):**對于重復(fù)數(shù)據(jù),能夠丟棄重復(fù)數(shù)據(jù);
- **應(yīng)答機(jī)制:**當(dāng) TCP 收到發(fā)自 TCP 連接另一端的數(shù)據(jù),它將發(fā)送一個確認(rèn)。這個確認(rèn)不是立即發(fā)送,通常將推遲幾分之一秒;
-
如何優(yōu)化TCP
從三個角度來闡述提升 TCP 的策略,分別是:
- TCP 三次握手的性能提升;
- TCP 四次揮手的性能提升;
- TCP 數(shù)據(jù)傳輸?shù)男阅芴嵘?#xff1b;
TCP 三次握手的性能提升
客戶端的優(yōu)化
當(dāng)客戶端發(fā)起 SYN 包時,可以通過
tcp_syn_retries
控制其重傳的次數(shù)。服務(wù)端的優(yōu)化
當(dāng)服務(wù)端 SYN 半連接隊列溢出后,會導(dǎo)致后續(xù)連接被丟棄,可以通過
netstat -s
觀察半連接隊列溢出的情況,如果 SYN 半連接隊列溢出情況比較嚴(yán)重,可以通過tcp_max_syn_backlog、somaxconn、backlog
參數(shù)來調(diào)整 SYN 半連接隊列的大小。服務(wù)端回復(fù) SYN+ACK 的重傳次數(shù)由
tcp_synack_retries
參數(shù)控制。如果遭受 SYN 攻擊,應(yīng)把tcp_syncookies
參數(shù)設(shè)置為 1,表示僅在 SYN 隊列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。服務(wù)端收到客戶端返回的 ACK,會把連接移入 accpet 隊列,等待進(jìn)行調(diào)用 accpet() 函數(shù)取出連接。
可以通過
ss -lnt
查看服務(wù)端進(jìn)程的 accept 隊列長度,如果 accept 隊列溢出,系統(tǒng)默認(rèn)丟棄 ACK,如果可以把tcp_abort_on_overflow
設(shè)置為 1 ,表示用 RST 通知客戶端連接建立失敗。如果 accpet 隊列溢出嚴(yán)重,可以通過 listen 函數(shù)的
backlog
參數(shù)和somaxconn
系統(tǒng)參數(shù)提高隊列大小,accept 隊列長度取決于 min(backlog, somaxconn)。繞過三次握手
TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請求減少了 1 個 RTT 的時間,Linux 下可以通過
tcp_fastopen
開啟該功能,同時必須保證服務(wù)端和客戶端同時支持。TCP 四次揮手的性能提升
客戶端和服務(wù)端雙方都可以主動斷開連接,通常先關(guān)閉連接的一方稱為主動方,后關(guān)閉連接的一方稱為被動方。
針對 TCP 四次揮手的優(yōu)化,我們需要根據(jù)主動方和被動方四次揮手狀態(tài)變化來調(diào)整系統(tǒng) TCP 內(nèi)核參數(shù)。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-vIOGjqUw-1678081583394)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/39.jpg)]
主動方的優(yōu)化
主動發(fā)起 FIN 報文斷開連接的一方,如果遲遲沒收到對方的 ACK 回復(fù),則會重傳 FIN 報文,重傳的次數(shù)由
tcp_orphan_retries
參數(shù)決定。當(dāng)主動方收到 ACK 報文后,連接就進(jìn)入 FIN_WAIT2 狀態(tài),根據(jù)關(guān)閉的方式不同,優(yōu)化的方式也不同:
- 如果這是 close 函數(shù)關(guān)閉的連接,那么它就是孤兒連接。如果
tcp_fin_timeout
秒內(nèi)沒有收到對方的 FIN 報文,連接就直接關(guān)閉。同時,為了應(yīng)對孤兒連接占用太多的資源,tcp_max_orphans
定義了最大孤兒連接的數(shù)量,超過時連接就會直接釋放。 - 反之是 shutdown 函數(shù)關(guān)閉的連接,則不受此參數(shù)限制;
當(dāng)主動方接收到 FIN 報文,并返回 ACK 后,主動方的連接進(jìn)入 TIME_WAIT 狀態(tài)。這一狀態(tài)會持續(xù) 1 分鐘,為了防止 TIME_WAIT 狀態(tài)占用太多的資源,
tcp_max_tw_buckets
定義了最大數(shù)量,超過時連接也會直接釋放。當(dāng) TIME_WAIT 狀態(tài)過多時,還可以通過設(shè)置
tcp_tw_reuse
和tcp_timestamps
為 1 ,將 TIME_WAIT 狀態(tài)的端口復(fù)用于作為客戶端的新連接,注意該參數(shù)只適用于客戶端。被動方的優(yōu)化
被動關(guān)閉的連接方應(yīng)對非常簡單,它在回復(fù) ACK 后就進(jìn)入了 CLOSE_WAIT 狀態(tài),等待進(jìn)程調(diào)用 close 函數(shù)關(guān)閉連接。因此,出現(xiàn)大量 CLOSE_WAIT 狀態(tài)的連接時,應(yīng)當(dāng)從應(yīng)用程序中找問題。
當(dāng)被動方發(fā)送 FIN 報文后,連接就進(jìn)入 LAST_ACK 狀態(tài),在未等到 ACK 時,會在
tcp_orphan_retries
參數(shù)的控制下重發(fā) FIN 報文。TCP 數(shù)據(jù)傳輸?shù)男阅芴嵘?/strong>
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-yvueg1Zy-1678081583395)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/49.jpg)]
TCP 可靠性是通過 ACK 確認(rèn)報文實現(xiàn)的,又依賴滑動窗口提升了發(fā)送速度也兼顧了接收方的處理能力。
可是,默認(rèn)的滑動窗口最大值只有 64 KB,不滿足當(dāng)今的高速網(wǎng)絡(luò)的要求,要想提升發(fā)送速度必須提升滑動窗口的上限,在 Linux 下是通過設(shè)置
tcp_window_scaling
為 1 做到的,此時最大值可高達(dá) 1GB。滑動窗口定義了網(wǎng)絡(luò)中飛行報文的最大字節(jié)數(shù),當(dāng)它超過帶寬時延積時,網(wǎng)絡(luò)過載,就會發(fā)生丟包。而當(dāng)它小于帶寬時延積時,就無法充分利用網(wǎng)絡(luò)帶寬。因此,滑動窗口的設(shè)置,必須參考帶寬時延積。
內(nèi)核緩沖區(qū)決定了滑動窗口的上限,緩沖區(qū)可分為:發(fā)送緩沖區(qū) tcp_wmem 和接收緩沖區(qū) tcp_rmem。
Linux 會對緩沖區(qū)動態(tài)調(diào)節(jié),我們應(yīng)該把緩沖區(qū)的上限設(shè)置為帶寬時延積。發(fā)送緩沖區(qū)的調(diào)節(jié)功能是自動打開的,而接收緩沖區(qū)需要把 tcp_moderate_rcvbuf 設(shè)置為 1 來開啟。其中,調(diào)節(jié)的依據(jù)是 TCP 內(nèi)存范圍 tcp_mem。
但需要注意的是,如果程序中的 socket 設(shè)置 SO_SNDBUF 和 SO_RCVBUF,則會關(guān)閉緩沖區(qū)的動態(tài)整功能,所以不建議在程序設(shè)置它倆,而是交給內(nèi)核自動調(diào)整比較好。
-
UDP協(xié)議
提供無連接的,盡最大努力的數(shù)據(jù)傳輸服務(wù)(不保證數(shù)據(jù)傳輸?shù)目煽啃?/strong>)。
特點(diǎn)
(1)UDP是無連接的傳輸層協(xié)議;
(2)UDP使用盡最大努力交付,不保證可靠交付;
(3)UDP是面向報文的,對應(yīng)用層交下來的報文,不合并,不拆分,保留原報文的邊界;
(4)UDP沒有擁塞控制,因此即使網(wǎng)絡(luò)出現(xiàn)擁塞也不會降低發(fā)送速率;
(5)UDP支持一對一 一對多 多對多的交互通信;
(6)UDP的首部開銷小,只有8字節(jié).
-
UDP 如何實現(xiàn)可靠傳輸
UDP不屬于連接協(xié)議,具有資源消耗少,處理速度快的優(yōu)點(diǎn),所以通常音頻,視頻和普通數(shù)據(jù)在傳送時,使用UDP較多,因為即使丟失少量的包,也不會對接受結(jié)果產(chǎn)生較大的影響。
傳輸層無法保證數(shù)據(jù)的可靠傳輸,只能通過應(yīng)用層來實現(xiàn)了。實現(xiàn)的方式可以參照tcp可靠性傳輸?shù)姆绞?#xff0c;只是實現(xiàn)不在傳輸層,實現(xiàn)轉(zhuǎn)移到了應(yīng)用層。
最簡單的方式是在應(yīng)用層模仿傳輸層TCP的可靠性傳輸。下面不考慮擁塞處理,可靠UDP的簡單設(shè)計。
- 1、添加seq/ack機(jī)制,確保數(shù)據(jù)發(fā)送到對端
- 2、添加發(fā)送和接收緩沖區(qū),主要是用戶超時重傳。
- 3、添加超時重傳機(jī)制。
詳細(xì)說明:送端發(fā)送數(shù)據(jù)時,生成一個隨機(jī)seq=x,然后每一片按照數(shù)據(jù)大小分配seq。數(shù)據(jù)到達(dá)接收端后接收端放入緩存,并發(fā)送一個ack=x的包,表示對方已經(jīng)收到了數(shù)據(jù)。發(fā)送端收到了ack包后,刪除緩沖區(qū)對應(yīng)的數(shù)據(jù)。時間到后,定時任務(wù)檢查是否需要重傳數(shù)據(jù)。
目前有如下開源程序利用udp實現(xiàn)了可靠的數(shù)據(jù)傳輸。分別為*RUDP、RTP、UDT*。
-
TCP和UDP的區(qū)別
(1)TCP是可靠傳輸,UDP是不可靠傳輸;
(2)TCP面向連接,UDP無連接;
(3)TCP傳輸數(shù)據(jù)有序,UDP不保證數(shù)據(jù)的有序性;
(4)TCP不保存數(shù)據(jù)邊界,UDP保留數(shù)據(jù)邊界;
(5)TCP傳輸速度相對UDP較慢;
(6)TCP有流量控制和擁塞控制,UDP沒有;
(7)TCP是重量級協(xié)議,UDP是輕量級協(xié)議;
(8)TCP首部較長20字節(jié),UDP首部較短8字節(jié);
TCP應(yīng)用場景:
效率要求相對低,但對準(zhǔn)確性要求相對高的場景。因為傳輸中需要對數(shù)據(jù)確認(rèn)、重發(fā)、排序等操作,相比之下效率沒有UDP高。舉幾個例子:文件傳輸(準(zhǔn)確高要求高、但是速度可以相對慢)、接受郵件、遠(yuǎn)程登錄。
UDP應(yīng)用場景:
效率要求相對高,對準(zhǔn)確性要求相對低的場景。舉幾個例子:QQ聊天、在線視頻、網(wǎng)絡(luò)語音電話(即時通訊,速度要求高,但是出現(xiàn)偶爾斷續(xù)不是太大問題,并且此處完全不可以使用重發(fā)機(jī)制)、廣播通信(廣播、多播)
基于TCP和UDP的常用協(xié)議
HTTP、HTTPS、FTP、TELNET、SMTP(簡單郵件傳輸協(xié)議)協(xié)議基于可靠的TCP協(xié)議。DNS、DHCP、TFTP、SNMP(簡單網(wǎng)絡(luò)管理協(xié)議)、RIP基于不可靠的UDP協(xié)議
-
TCP黏包和拆包
TCP黏包
TCP粘包是指發(fā)送方發(fā)送的若干包數(shù)據(jù)到接收方接收時粘成一包,從接收緩沖區(qū)看,后一包數(shù)據(jù)的頭緊接著前一包數(shù)據(jù)的尾。
- 由TCP連接復(fù)用造成的粘包問題。
- 因為TCP默認(rèn)會使用Nagle算法,此算法會導(dǎo)致粘包問題。
- 只有上一個分組得到確認(rèn),才會發(fā)送下一個分組;
- 收集多個小分組,在一個確認(rèn)到來時一起發(fā)送。
- 數(shù)據(jù)包過大造成的粘包問題。
- 流量控制,擁塞控制也可能導(dǎo)致粘包。
- 接收方不及時接收緩沖區(qū)的包,造成多個包接收
解決:
- Nagle算法問題導(dǎo)致的,需要結(jié)合應(yīng)用場景適當(dāng)關(guān)閉該算法
- 尾部標(biāo)記序列。通過特殊標(biāo)識符表示數(shù)據(jù)包的邊界,例如\n\r,\t,或者一些隱藏字符。
- 頭部標(biāo)記分步接收。在TCP報文的頭部加上表示數(shù)據(jù)長度。
- 應(yīng)用層發(fā)送數(shù)據(jù)時定長發(fā)送。
- 特殊字符控制;
-
URI和 URL之間的區(qū)別
URL,即統(tǒng)一資源定位符 (Uniform Resource Locator ),URL 其實就是我們平時上網(wǎng)時輸入的網(wǎng)址,它標(biāo)識一個互聯(lián)網(wǎng)資源,并指定對其進(jìn)行操作或獲取該資源的方法。例如 https://leetcode-cn.com/problemset/all/ 這個 URL,標(biāo)識一個特定資源并表示該資源的某種形式是可以通過 HTTP 協(xié)議從相應(yīng)位置獲得。
而 URI 則是統(tǒng)一資源標(biāo)識符,URL 是 URI 的一個子集,兩者都定義了資源是什么,而 URL 還定義了如何能訪問到該資源。URI 是一種語義上的抽象概念,可以是絕對的,也可以是相對的,而URL則必須提供足夠的信息來定位,是絕對的。簡單地說,只要能唯一標(biāo)識資源的就是 URI,在 URI 的基礎(chǔ)上給出其資源的訪問方式的就是 URL。
4.4 IP協(xié)議
-
IP協(xié)議的作用
網(wǎng)絡(luò)層的主要作用是:實現(xiàn)主機(jī)與主機(jī)之間的通信,也叫點(diǎn)對點(diǎn)(end to end)通信。
IP協(xié)議的作用主要是在相互連接的網(wǎng)絡(luò)之間傳遞IP數(shù)據(jù)包,主要功能有兩方面,分別是尋址與路由和分段與重組,而在IP協(xié)議當(dāng)中,最重要的就是TTL(IP允許通過的最大網(wǎng)段數(shù)量)字段(八位),規(guī)定該數(shù)據(jù)包能穿過幾個路由之后才會被拋棄。
-
IPV4和IPV6
IPv4 的地址是 32 位的,大約可以提供 42 億個地址,但是早在 2011 年 IPv4 地址就已經(jīng)被分配完了。
IPv6 不僅僅只是可分配的地址變多了,它還有非常多的亮點(diǎn)。
- IPv6 可自動配置,即使沒有 DHCP 服務(wù)器也可以實現(xiàn)自動分配IP地址,真是便捷到即插即用啊。
- IPv6 包頭包首部長度采用固定的值
40
字節(jié),去掉了包頭校驗和,簡化了首部結(jié)構(gòu),減輕了路由器負(fù)荷,大大提高了傳輸?shù)男阅?/strong>。 - IPv6 有應(yīng)對偽造 IP 地址的網(wǎng)絡(luò)安全功能以及防止線路竊聽的功能,大大提升了安全性。
IPv6 地址的結(jié)構(gòu)
IPv6 類似 IPv4,也是通過 IP 地址的前幾位標(biāo)識 IP 地址的種類。
IPv6 的地址主要有以下類型地址:
- 單播地址,用于一對一的通信
- 組播地址,用于一對多的通信
- 任播地址,用于通信最近的節(jié)點(diǎn),最近的節(jié)點(diǎn)是由路由協(xié)議決定
- 沒有廣播地址
IPv6 單播地址類型
對于一對一通信的 IPv6 地址,主要劃分了三類單播地址,每類地址的有效范圍都不同。
- 在同一鏈路單播通信,不經(jīng)過路由器,可以使用鏈路本地單播地址,IPv4 沒有此類型
- 在內(nèi)網(wǎng)里單播通信,可以使用唯一本地地址,相當(dāng)于 IPv4 的私有 IP
- 在互聯(lián)網(wǎng)通信,可以使用全局單播地址,相當(dāng)于 IPv4 的公有 IP
IPv6 相比 IPv4 的首部改進(jìn):
- 取消了首部校驗和字段。 因為在數(shù)據(jù)鏈路層和傳輸層都會校驗,因此 IPv6 直接取消了 IP 的校驗。
- 取消了分片/重新組裝相關(guān)字段。 分片與重組是耗時的過程,IPv6 不允許在中間路由器進(jìn)行分片與重組,這種操作只能在源與目標(biāo)主機(jī),這將大大提高了路由器轉(zhuǎn)發(fā)的速度。
- 取消選項字段。 選項字段不再是標(biāo)準(zhǔn) IP 首部的一部分了,但它并沒有消失,而是可能出現(xiàn)在 IPv6 首部中的「下一個首部」指出的位置上。刪除該選項字段使的 IPv6 的首部成為固定長度的
40
字節(jié)。
-
IPV4 地址不夠如何解決
目前主要有以下兩種方式:
1、其實我們平時上網(wǎng),電腦的 IP 地址都是屬于私有地址,我無法出網(wǎng)關(guān),我們的數(shù)據(jù)都是通過網(wǎng)關(guān)來中轉(zhuǎn)的,這個其實 NAT 協(xié)議,可以用來暫緩 IPV4 地址不夠。
2、IPv6 :作為接替 IPv4 的下一代互聯(lián)網(wǎng)協(xié)議,其可以實現(xiàn) 2 的 128 次方個地址,這個數(shù)量級,即使給地球上每一顆沙子都分配一個IP地址,該協(xié)議能從根本上解決 IPv4 地址不夠用的問題。
-
IP地址和MAC地址有什么區(qū)別?各自的用處?
簡單著說,IP 地址主要用來網(wǎng)絡(luò)尋址用的,就是大致定位你在哪里,而 MAC 地址,則是身份的唯一象征,通過 MAC 來唯一確認(rèn)這人是不是就是你,MAC 地址不具備尋址的功能。
-
網(wǎng)絡(luò)層常見協(xié)議
議 名稱 作用 IP 網(wǎng)際協(xié)議 IP協(xié)議不但定義了數(shù)據(jù)傳輸時的基本單元和格式,還定義了數(shù)據(jù)報的遞交方法和路由選擇 ICMP Internet控制報文協(xié)議 ICMP就是一個“錯誤偵測與回報機(jī)制”,其目的就是讓我們能夠檢測網(wǎng)路的連線狀況﹐也能確保連線的準(zhǔn)確性,是ping和traceroute的工作協(xié)議 RIP 路由信息協(xié)議 使用“跳數(shù)”(即metric)來衡量到達(dá)目標(biāo)地址的路由距離 IGMP Internet組管理協(xié)議 用于實現(xiàn)組播、廣播等通信 -
什么是DNS和工作原理
DNS(Domain Name System,域名系統(tǒng)),因特網(wǎng)上作為域名和IP地址相互映射的一個分布式數(shù)據(jù)庫,能夠使用戶更方便的訪問互聯(lián)網(wǎng),而不用去記住能夠被機(jī)器直接讀取的IP數(shù)串。
通過主機(jī)名,最終得到該主機(jī)名對應(yīng)的IP地址的過程叫做域名解析(或主機(jī)名解析)。
工作原理
將主機(jī)域名轉(zhuǎn)換為ip地址,屬于應(yīng)用層協(xié)議,使用UDP傳輸。
過程: 總結(jié): 瀏覽器緩存,系統(tǒng)緩存,路由器緩存,IPS服務(wù)器緩存,根域名服務(wù)器緩存,頂級域名服務(wù)器緩存,主域名服務(wù)器緩存。 一、主機(jī)向本地域名服務(wù)器的查詢一般都是采用遞歸查詢。 二、本地域名服務(wù)器向根域名服務(wù)器的查詢的迭代查詢。
- 當(dāng)用戶輸入域名時,瀏覽器先檢查自己的緩存中是否包含這個域名映射的ip地址,有解析結(jié)束。 2)若沒命中,則檢查操作系統(tǒng)緩存(如Windows的hosts)中有沒有解析過的結(jié)果,有解析結(jié)束。 3)若無命中,則請求本地域名服務(wù)器解析(LDNS)。 4)若LDNS沒有命中就直接跳到根域名服務(wù)器請求解析。根域名服務(wù)器返回給LDNS一個 主域名服務(wù)器地址。 5)此時LDNS再發(fā)送請求給上一步返回的gTLD( 通用頂級域), 接受請求的gTLD查找并返回這個域名對應(yīng)的Name Server的地址 6)Name Server根據(jù)映射關(guān)系表找到目標(biāo)ip,返回給LDNS
- LDNS緩存這個域名和對應(yīng)的ip, 把解析的結(jié)果返回給用戶,用戶根據(jù)TTL值緩存到本地系統(tǒng)緩存中,域名解析過程至此結(jié)束
-
DNS負(fù)載均衡是什么策略
當(dāng)一個網(wǎng)站有足夠多的用戶的時候,假如每次請求的資源都位于同一臺機(jī)器上面,那么這臺機(jī)器隨時可能會崩掉。處理辦法就是用DNS負(fù)載均衡技術(shù),它的原理是在DNS服務(wù)器中為同一個主機(jī)名配置多個IP地址,在應(yīng)答DNS查詢時,DNS服務(wù)器對每個查詢將以DNS文件中主機(jī)記錄的IP地址按順序返回不同的解析結(jié)果,將客戶端的訪問引導(dǎo)到不同的機(jī)器上去,使得不同的客戶端訪問不同的服務(wù)器,從而達(dá)到負(fù)載均衡的目的。例如可以根據(jù)每臺機(jī)器的負(fù)載量,該機(jī)器離用戶地理位置的距離等等。
-
數(shù)據(jù)鏈路層常見協(xié)議
議 名稱 作用 ARP 地址解析協(xié)議 根據(jù)IP地址獲取物理地址 RARP 反向地址轉(zhuǎn)換協(xié)議 根據(jù)物理地址獲取IP地址 PPP 點(diǎn)對點(diǎn)協(xié)議 主要是用來通過撥號或?qū)>€方式建立點(diǎn)對點(diǎn)連接發(fā)送數(shù)據(jù),使其成為各種主機(jī)、網(wǎng)橋和路由器之間簡單連接的一種共通的解決方案 -
ARP和RARP協(xié)議
RARP
概括: 反向地址轉(zhuǎn)換協(xié)議,網(wǎng)絡(luò)層協(xié)議,RARP與ARP工作方式相反。 RARP使只知道自己硬件地址的主機(jī)能夠知道其IP地址。RARP發(fā)出要反向解釋的物理地址并希望返回其IP地址,應(yīng)答包括能夠提供所需信息的RARP服務(wù)器發(fā)出的IP地址。
原理: (1)網(wǎng)絡(luò)上的每臺設(shè)備都會有一個獨(dú)一無二的硬件地址,通常是由設(shè)備廠商分配的MAC地址。主機(jī)從網(wǎng)卡上讀取MAC地址,然后在網(wǎng)絡(luò)上發(fā)送一個RARP請求的廣播數(shù)據(jù)包,請求RARP服務(wù)器回復(fù)該主機(jī)的IP地址。
(2)RARP服務(wù)器收到了RARP請求數(shù)據(jù)包,為其分配IP地址,并將RARP回應(yīng)發(fā)送給主機(jī)。
(3)PC1收到RARP回應(yīng)后,就使用得到的IP地址進(jìn)行通訊。
ARP
網(wǎng)絡(luò)層的 ARP 協(xié)議完成了 IP 地址與物理地址的映射。首先,每臺主機(jī)都會在自己的 ARP 緩沖區(qū)中建立一個 ARP 列表,以表示 IP 地址和 MAC 地址的對應(yīng)關(guān)系。當(dāng)源主機(jī)需要將一個數(shù)據(jù)包要發(fā)送到目的主機(jī)時,會首先檢查自己 ARP 列表中是否存在該 IP 地址對應(yīng)的 MAC 地址:如果有,就直接將數(shù)據(jù)包發(fā)送到這個 MAC 地址;如果沒有,就向本地網(wǎng)段發(fā)起一個 ARP 請求的廣播包,查詢此目的主機(jī)對應(yīng)的 MAC 地址。
此 ARP 請求數(shù)據(jù)包里包括源主機(jī)的 IP 地址、硬件地址、以及目的主機(jī)的 IP 地址。網(wǎng)絡(luò)中所有的主機(jī)收到這個 ARP 請求后,會檢查數(shù)據(jù)包中的目的 IP 是否和自己的 IP 地址一致。如果不相同就忽略此數(shù)據(jù)包;如果相同,該主機(jī)首先將發(fā)送端的 MAC 地址和 IP 地址添加到自己的 ARP 列表中,如果 ARP 表中已經(jīng)存在該 IP 的信息,則將其覆蓋,然后給源主機(jī)發(fā)送一個 ARP 響應(yīng)數(shù)據(jù)包,告訴對方自己是它需要查找的 MAC 地址;源主機(jī)收到這個 ARP 響應(yīng)數(shù)據(jù)包后,將得到的目的主機(jī)的 IP 地址和 MAC 地址添加到自己的 ARP 列表中,并利用此信息開始數(shù)據(jù)的傳輸。如果源主機(jī)一直沒有收到 ARP 響應(yīng)數(shù)據(jù)包,表示 ARP 查詢失敗
-
DHCP/NAT協(xié)議
DHCP
DHCP 在生活中我們是很常見的了,我們的電腦通常都是通過 DHCP 動態(tài)獲取 IP 地址,大大省去了配 IP 信息繁瑣的過程。
DHCP 客戶端進(jìn)程監(jiān)聽的是 68 端口號,DHCP 服務(wù)端進(jìn)程監(jiān)聽的是 67 端口號。
這 4 個步驟:
- 客戶端首先發(fā)起 DHCP 發(fā)現(xiàn)報文(DHCP DISCOVER) 的 IP 數(shù)據(jù)報,由于客戶端沒有 IP 地址,也不知道 DHCP 服務(wù)器的地址,所以使用的是 UDP 廣播通信,其使用的廣播目的地址是 255.255.255.255(端口 67) 并且使用 0.0.0.0(端口 68) 作為源 IP 地址。DHCP 客戶端將該 IP 數(shù)據(jù)報傳遞給鏈路層,鏈路層然后將幀廣播到所有的網(wǎng)絡(luò)中設(shè)備。
- DHCP 服務(wù)器收到 DHCP 發(fā)現(xiàn)報文時,用 DHCP 提供報文(DHCP OFFER) 向客戶端做出響應(yīng)。該報文仍然使用 IP 廣播地址 255.255.255.255,該報文信息攜帶服務(wù)器提供可租約的 IP 地址、子網(wǎng)掩碼、默認(rèn)網(wǎng)關(guān)、DNS 服務(wù)器以及 IP 地址租用期。
- 客戶端收到一個或多個服務(wù)器的 DHCP 提供報文后,從中選擇一個服務(wù)器,并向選中的服務(wù)器發(fā)送 DHCP 請求報文(DHCP REQUEST進(jìn)行響應(yīng),回顯配置的參數(shù)。
- 最后,服務(wù)端用 DHCP ACK 報文對 DHCP 請求報文進(jìn)行響應(yīng),應(yīng)答所要求的參數(shù)。
一旦客戶端收到 DHCP ACK 后,交互便完成了,并且客戶端能夠在租用期內(nèi)使用 DHCP 服務(wù)器分配的 IP 地址。
DHCP 交互中,全程都是使用 UDP 廣播通信。
NAT
網(wǎng)絡(luò)地址轉(zhuǎn)換 NAT 的,緩解了 IPv4 地址耗盡的問題。簡單的來說 NAT 就是同個公司、家庭、教室內(nèi)的主機(jī)對外部通信時,把私有 IP 地址轉(zhuǎn)換成公有 IP 地址。
由于 NAT/NAPT 都依賴于自己的轉(zhuǎn)換表,因此會有以下的問題:
- 外部無法主動與 NAT 內(nèi)部服務(wù)器建立連接,因為 NAPT 轉(zhuǎn)換表沒有轉(zhuǎn)換記錄。
- 轉(zhuǎn)換表的生成與轉(zhuǎn)換操作都會產(chǎn)生性能開銷。
- 通信過程中,如果 NAT 路由器重啟了,所有的 TCP 連接都將被重置。
如何解決 NAT 潛在的問題呢?
解決的方法主要有兩種方法。
第一種就是改用 IPv6、第二種 NAT 穿透技術(shù)
-
ICMP/IGMP協(xié)議
ICMP
ICMP 全稱是 Internet Control Message Protocol,也就是互聯(lián)網(wǎng)控制報文協(xié)議。
ICMP
主要的功能包括:確認(rèn) IP 包是否成功送達(dá)目標(biāo)地址、報告發(fā)送過程中 IP 包被廢棄的原因和改善網(wǎng)絡(luò)設(shè)置等。在
IP
通信中如果某個IP
包因為某種原因未能達(dá)到目標(biāo)地址,那么這個具體的原因?qū)?strong>由 ICMP 負(fù)責(zé)通知。ICMP 大致可以分為兩大類:
- 一類是用于診斷的查詢消息,也就是「查詢報文類型」
- 另一類是通知出錯原因的錯誤消息,也就是「差錯報文類型」
IGMP
我們知道了組播地址,也就是 D 類地址,既然是組播,那就說明是只有一組的主機(jī)能收到數(shù)據(jù)包,不在一組的主機(jī)不能收到數(shù)組包,怎么管理是否是在一組呢?那么,就需要
IGMP
協(xié)議了。IGMP 是因特網(wǎng)組管理協(xié)議,工作在主機(jī)(組播成員)和最后一跳路由之間
- IGMP 報文向路由器申請加入和退出組播組,默認(rèn)情況下路由器是不會轉(zhuǎn)發(fā)組播包到連接中的主機(jī),除非主機(jī)通過 IGMP 加入到組播組,主機(jī)申請加入到組播組時,路由器就會記錄 IGMP 路由器表,路由器后續(xù)就會轉(zhuǎn)發(fā)組播包到對應(yīng)的主機(jī)了。
- IGMP 報文采用 IP 封裝,IP 頭部的協(xié)議號為 2,而且 TTL 字段值通常為 1,因為 IGMP 是工作在主機(jī)與連接的路由器之間。
常規(guī)查詢與響應(yīng)工作機(jī)制
- 路由器會周期性發(fā)送目的地址為
224.0.0.1
(表示同一網(wǎng)段內(nèi)所有主機(jī)和路由器) IGMP 常規(guī)查詢報文。 - 主機(jī)1 和 主機(jī) 3 收到這個查詢,隨后會啟動「報告延遲計時器」,計時器的時間是隨機(jī)的,通常是 0~10 秒,計時器超時后主機(jī)就會發(fā)送 IGMP 成員關(guān)系報告報文(源 IP 地址為自己主機(jī)的 IP 地址,目的 IP 地址為組播地址)。如果在定時器超時之前,收到同一個組內(nèi)的其他主機(jī)發(fā)送的成員關(guān)系報告報文,則自己不再發(fā)送,這樣可以減少網(wǎng)絡(luò)中多余的 IGMP 報文數(shù)量。
- 路由器收到主機(jī)的成員關(guān)系報文后,就會在 IGMP 路由表中加入該組播組,后續(xù)網(wǎng)絡(luò)中一旦該組播地址的數(shù)據(jù)到達(dá)路由器,它會把數(shù)據(jù)包轉(zhuǎn)發(fā)出去。
離開組播組工作機(jī)制
離開組播組的情況一,網(wǎng)段中仍有該組播組:
- 主機(jī) 1 要離開組 224.1.1.1,發(fā)送 IGMPv2 離組報文,報文的目的地址是 224.0.0.2(表示發(fā)向網(wǎng)段內(nèi)的所有路由器)
- 路由器 收到該報文后,以 1 秒為間隔連續(xù)發(fā)送 IGMP 特定組查詢報文(共計發(fā)送 2 個),以便確認(rèn)該網(wǎng)絡(luò)是否還有 224.1.1.1 組的其他成員。
- 主機(jī) 3 仍然是組 224.1.1.1 的成員,因此它立即響應(yīng)這個特定組查詢。路由器知道該網(wǎng)絡(luò)中仍然存在該組播組的成員,于是繼續(xù)向該網(wǎng)絡(luò)轉(zhuǎn)發(fā) 224.1.1.1 的組播數(shù)據(jù)包。
離開組播組的情況一,網(wǎng)段中仍有該組播組:
- 主機(jī) 1 要離開組播組 224.1.1.1,發(fā)送 IGMP 離組報文。
- 路由器收到該報文后,以 1 秒為間隔連續(xù)發(fā)送 IGMP 特定組查詢報文(共計發(fā)送 2 個)。此時在該網(wǎng)段內(nèi),組 224.1.1.1 已經(jīng)沒有其他成員了,因此沒有主機(jī)響應(yīng)這個查詢。
- 一定時間后,路由器認(rèn)為該網(wǎng)段中已經(jīng)沒有 224.1.1.1 組播組成員了,將不會再向這個網(wǎng)段轉(zhuǎn)發(fā)該組播地址的數(shù)據(jù)包。
-
ICMP 有哪些應(yīng)用
ICMP 主要有兩個應(yīng)用,一個是 Ping,一個是 Traceroute。
1. Ping
Ping 是 ICMP 的一個重要應(yīng)用,主要用來測試兩臺主機(jī)之間的連通性。
Ping 的原理是通過向目的主機(jī)發(fā)送 ICMP Echo 請求報文,目的主機(jī)收到之后會發(fā)送 Echo 回答報文。Ping 會根據(jù)時間和成功響應(yīng)的次數(shù)估算出數(shù)據(jù)包往返時間以及丟包率。
2. Traceroute
Traceroute 是 ICMP 的另一個應(yīng)用,用來跟蹤一個分組從源點(diǎn)到終點(diǎn)的路徑。
Traceroute 發(fā)送的 IP 數(shù)據(jù)報封裝的是無法交付的 UDP 用戶數(shù)據(jù)報,并由目的主機(jī)發(fā)送終點(diǎn)不可達(dá)差錯報告報文。
- 源主機(jī)向目的主機(jī)發(fā)送一連串的 IP 數(shù)據(jù)報。第一個數(shù)據(jù)報 P1 的生存時間 TTL 設(shè)置為 1,當(dāng) P1 到達(dá)路徑上的第一個路由器 R1 時,R1 收下它并把 TTL 減 1,此時 TTL 等于 0,R1 就把 P1 丟棄,并向源主機(jī)發(fā)送一個 ICMP 時間超過差錯報告報文;
- 源主機(jī)接著發(fā)送第二個數(shù)據(jù)報 P2,并把 TTL 設(shè)置為 2。P2 先到達(dá) R1,R1 收下后把 TTL 減 1 再轉(zhuǎn)發(fā)給 R2,R2 收下后也把 TTL 減 1,由于此時 TTL 等于 0,R2 就丟棄 P2,并向源主機(jī)發(fā)送一個 ICMP 時間超過差錯報文。
- 不斷執(zhí)行這樣的步驟,直到最后一個數(shù)據(jù)報剛剛到達(dá)目的主機(jī),主機(jī)不轉(zhuǎn)發(fā)數(shù)據(jù)報,也不把 TTL 值減 1。但是因為數(shù)據(jù)報封裝的是無法交付的 UDP,因此目的主機(jī)要向源主機(jī)發(fā)送 ICMP 終點(diǎn)不可達(dá)差錯報告報文。
- 之后源主機(jī)知道了到達(dá)目的主機(jī)所經(jīng)過的路由器 IP 地址以及到達(dá)每個路由器的往返時間。
4.5 其他面試題
-
應(yīng)用層常見協(xié)議知道多少
協(xié)議 名稱 默認(rèn)端口 底層協(xié)議 HTTP 超文本傳輸協(xié)議 80 TCP HTTPS 超文本傳輸安全協(xié)議 443 TCP Telnet 遠(yuǎn)程登錄服務(wù)的標(biāo)準(zhǔn)協(xié)議 23 TCP FTP 文件傳輸協(xié)議 20傳輸和21連接 TCP TFTP 簡單文件傳輸協(xié)議 69 UDP SMTP 簡單郵件傳輸協(xié)議(發(fā)送用) 25 TCP POP 郵局協(xié)議(接收用) 110 TCP DNS 域名解析服務(wù) 53 服務(wù)器間進(jìn)行域傳輸?shù)臅r候用TCP 客戶端查詢DNS服務(wù)器時用 UDP -
常見的HTTP狀態(tài)碼有哪些
狀態(tài)碼 類別 含義 1XX Informational(信息性狀態(tài)碼) 接收的請求正在處理 2XX Success(成功狀態(tài)碼) 請求正常處理完畢 3XX Redirection(重定向狀態(tài)碼) 需要進(jìn)行附加操作以完成請求 4XX Client Error(客戶端錯誤狀態(tài)碼) 服務(wù)器無法處理請求 5XX Server Error(服務(wù)器錯誤狀態(tài)碼) 服務(wù)器處理請求出 1xx 信息
100 Continue :表明到目前為止都很正常,客戶端可以繼續(xù)發(fā)送請求或者忽略這個響應(yīng)。
2xx 成功
- 200 OK
- 204 No Content :請求已經(jīng)成功處理,但是返回的響應(yīng)報文不包含實體的主體部分。一般在只需要從客戶端往服務(wù)器發(fā)送信息,而不需要返回數(shù)據(jù)時使用。
- 206 Partial Content :表示客戶端進(jìn)行了范圍請求,響應(yīng)報文包含由 Content-Range 指定范圍的實體內(nèi)容。
3xx 重定向
- 301 Moved Permanently :永久性重定向
- 302 Found :臨時性重定向
- 303 See Other :和 302 有著相同的功能,但是 303 明確要求客戶端應(yīng)該采用 GET 方法獲取資源。
- 304 Not Modified :如果請求報文首部包含一些條件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不滿足條件,則服務(wù)器會返回 304 狀態(tài)碼。
- 307 Temporary Redirect :臨時重定向,與 302 的含義類似,但是 307 要求瀏覽器不會把重定向請求的 POST 方法改成 GET 方法。
4xx 客戶端錯誤
- 400 Bad Request :請求報文中存在語法錯誤。
- 401 Unauthorized :該狀態(tài)碼表示發(fā)送的請求需要有認(rèn)證信息(BASIC 認(rèn)證、DIGEST 認(rèn)證)。如果之前已進(jìn)行過一次請求,則表示用戶認(rèn)證失敗。
- 403 Forbidden :請求被拒絕。
- 404 Not Found
5xx 服務(wù)器錯誤
- 500 Internal Server Error :服務(wù)器正在執(zhí)行請求時發(fā)生錯誤。
- 503 Service Unavailable :服務(wù)器暫時處于超負(fù)載或正在進(jìn)行停機(jī)維護(hù),現(xiàn)在無法處理請求。
-
OSI的七層模型和功能
簡要概括
- 物理層:底層數(shù)據(jù)傳輸,如網(wǎng)線;網(wǎng)卡標(biāo)準(zhǔn)。
- 數(shù)據(jù)鏈路層:定義數(shù)據(jù)的基本格式,如何傳輸,如何標(biāo)識;如網(wǎng)卡MAC地址。
- 網(wǎng)絡(luò)層:定義IP編址,定義路由功能;如不同設(shè)備的數(shù)據(jù)轉(zhuǎn)發(fā)。
- 傳輸層:端到端傳輸數(shù)據(jù)的基本功能;如 TCP、UDP。
- 會話層:控制應(yīng)用程序之間會話能力;如不同軟件數(shù)據(jù)分發(fā)給不同軟件。
- 表示層:數(shù)據(jù)格式標(biāo)識,基本壓縮加密功能。
- 應(yīng)用層:各種應(yīng)用軟件,包括 Web 應(yīng)用。
說明
- 在四層,既傳輸層數(shù)據(jù)被稱作段(Segments);
- 三層網(wǎng)絡(luò)層數(shù)據(jù)被稱做包(Packages);
- 二層數(shù)據(jù)鏈路層時數(shù)據(jù)被稱為幀(Frames);
- 一層物理層時數(shù)據(jù)被稱為比特流(Bits)。
總結(jié)
- 網(wǎng)絡(luò)七層模型是一個標(biāo)準(zhǔn),而非實現(xiàn)。
- 網(wǎng)絡(luò)四層模型是一個實現(xiàn)的應(yīng)用模型。
- 網(wǎng)絡(luò)四層模型由七層模型簡化合并而來。
-
網(wǎng)絡(luò)五層模型,每一層的職責(zé)
- 物理層
一臺計算機(jī)與另一臺計算機(jī)要進(jìn)行通信,第一件要做的事是什么?當(dāng)然是要把這臺計算機(jī)與另外的其他計算機(jī)連起來啊,這樣,我們才能把數(shù)據(jù)傳輸過去。例如可以通過光纖啊,電纜啊,雙絞線啊等介質(zhì)把他們連接起來,然后才能進(jìn)行通信。
物理層負(fù)責(zé)把兩臺計算機(jī)連起來,然后在計算機(jī)之間通過高低電頻來傳送0,1這樣的電信號。
- 數(shù)據(jù)鏈路層
物理層它只是單純著負(fù)責(zé)把計算機(jī)連接起來,并且在計算機(jī)之間傳輸0,1這樣的電信號。如果這些0,1組合的傳送毫無規(guī)則的話,計算機(jī)是解讀不了的。
因此,我們需要制定一套規(guī)則來進(jìn)行0,1的傳送。例如多少個電信號為一組啊,每一組信號應(yīng)該如何標(biāo)識才能讓計算機(jī)讀懂啊等等。
于是,有了以太網(wǎng)協(xié)議。
1. 以太網(wǎng)協(xié)議
以太網(wǎng)協(xié)議規(guī)定,一組電信號構(gòu)成一個數(shù)據(jù)包,我們把這個數(shù)據(jù)包稱之為幀。每一個楨由標(biāo)頭(Head)和數(shù)據(jù)(Data)兩部分組成。
幀的大小一般為 64 – 1518 個字節(jié)。假如需要傳送的數(shù)據(jù)很大的話,就分成多個楨來進(jìn)行傳送。
對于表頭和數(shù)據(jù)這兩個部分,他們存放的都是一些什么數(shù)據(jù)呢?我猜你瞇著眼睛都能想到他們應(yīng)該放什么數(shù)據(jù)。 毫無疑問,我們至少得知道這個楨是誰發(fā)送,發(fā)送給誰的等這些信息吧?所以標(biāo)頭部分主要是一些說明數(shù)據(jù),例如發(fā)送者,接收者等信息。而數(shù)據(jù)部分則是這個數(shù)據(jù)包具體的,想給接守者的內(nèi)容。
把一臺計算的的數(shù)據(jù)通過物理層和鏈路層發(fā)送給另一臺計算機(jī),究竟是誰發(fā)給誰的,計算機(jī)與計算機(jī)之間如何區(qū)分,,你總得給他們一個唯一的標(biāo)識吧?
于是,MAC 地址出現(xiàn)了。
2. MAC 地址
連入網(wǎng)絡(luò)的每一個計算機(jī)都會有網(wǎng)卡接口,每一個網(wǎng)卡都會有一個唯一的地址,這個地址就叫做 MAC 地址。計算機(jī)之間的數(shù)據(jù)傳送,就是通過 MAC 地址來唯一尋找、傳送的。
MAC地址 由 48 個二進(jìn)制位所構(gòu)成,在網(wǎng)卡生產(chǎn)時就被唯一標(biāo)識了。
3. 廣播與ARP協(xié)議
(1). 廣播
如圖,假如計算機(jī) A 知道了計算機(jī) B 的 MAC 地址,然后計算機(jī) A 想要給計算機(jī) B 傳送數(shù)據(jù),雖然計算機(jī) A 知道了計算機(jī) B 的 MAC 地址,可是它要怎么給它傳送數(shù)據(jù)呢?計算機(jī) A 不僅連著計算機(jī) B,而且計算機(jī) A 也還連著其他的計算機(jī)。 雖然計算機(jī) A 知道計算機(jī) B 的 MAC 地址,可是計算機(jī) A 卻不知道知道計算機(jī) B 是分布在哪邊路線上,為了解決這個問題,于是,有了廣播的出現(xiàn)。
在同一個子網(wǎng)中,計算機(jī) A 要向計算機(jī) B 發(fā)送一個數(shù)據(jù)包,這個數(shù)據(jù)包會包含接收者的 MAC 地址。當(dāng)發(fā)送時,計算機(jī) A 是通過廣播的方式發(fā)送的,這時同一個子網(wǎng)中的計算機(jī) C, D 也會收到這個數(shù)據(jù)包的,然后收到這個數(shù)據(jù)包的計算機(jī),會把數(shù)據(jù)包的 MAC 地址取出來,與自身的 MAC 地址對比,如果兩者相同,則接受這個數(shù)據(jù)包,否則就丟棄這個數(shù)據(jù)包。這種發(fā)送方式我們稱之為廣播,就像我們平時在廣場上通過廣播的形式呼叫某個人一樣,如果這個名字是你,你就理會一下,如果不是你,你就當(dāng)作聽不見。
(2). ARP 協(xié)議。
那么問題來了,計算機(jī) A 是如何知道計算機(jī) B 的 MAC 地址的呢?這個時候就得由 ARP 協(xié)議這個家伙來解決了,不過 ARP 協(xié)議會涉及到IP地址,我們下面才會扯到IP地址。因此我們先放著,就當(dāng)作是有這么一個 ARP 協(xié)議,通過它我們可以知道子網(wǎng)中其他計算機(jī)的 MAC 地址。
3. 網(wǎng)絡(luò)層
上面我們有說到子網(wǎng)這個關(guān)鍵詞,實際上我們所處的網(wǎng)絡(luò),是由無數(shù)個子網(wǎng)絡(luò)構(gòu)成的。廣播的時候,也只有同一個子網(wǎng)里面的計算機(jī)能夠收到。
假如沒有子網(wǎng)這種劃分的話,計算機(jī) A 通過廣播的方式發(fā)一個數(shù)據(jù)包給計算機(jī) B , 其他所有計算機(jī)也都能收到這個數(shù)據(jù)包,然后進(jìn)行對比再舍棄。世界上有那么多它計算機(jī),每一臺計算機(jī)都能收到其他所有計算機(jī)的數(shù)據(jù)包,那就不得了了。那還不得奔潰。 因此產(chǎn)生了子網(wǎng)這么一個東西。
那么問題來了,我們?nèi)绾螀^(qū)分哪些 MAC 地址是屬于同一個子網(wǎng)的呢?假如是同一個子網(wǎng),那我們就用廣播的形式把數(shù)據(jù)傳送給對方,如果不是同一個子網(wǎng)的,我們就會把數(shù)據(jù)發(fā)給網(wǎng)關(guān),讓網(wǎng)關(guān)進(jìn)行轉(zhuǎn)發(fā)。
為了解決這個問題,于是,有了 IP 協(xié)議。
1. IP協(xié)議
IP協(xié)議,它所定義的地址,我們稱之為IP地址。IP協(xié)議有兩種版本,一種是 IPv4,另一種是 IPv6。不過我們目前大多數(shù)用的還是 IPv4,我們現(xiàn)在也只討論 IPv4 這個版本的協(xié)議。
這個 IP 地址由 32 位的二進(jìn)制數(shù)組成,我們一般把它分成4段的十進(jìn)制表示,地址范圍為0.0.0.0~255.255.255.255。
每一臺想要聯(lián)網(wǎng)的計算機(jī)都會有一個IP地址。這個IP地址被分為兩部分,前面一部分代表網(wǎng)絡(luò)部分,后面一部分代表主機(jī)部分。并且網(wǎng)絡(luò)部分和主機(jī)部分所占用的二進(jìn)制位數(shù)是不固定的。
可是問題來了,你怎么知道網(wǎng)絡(luò)部分是占幾位,主機(jī)部分又是占幾位呢?也就是說,單單從兩臺計算機(jī)的IP地址,我們是無法判斷他們的是否處于同一個子網(wǎng)中的。
這就引申出了另一個關(guān)鍵詞————子網(wǎng)掩碼。子網(wǎng)掩碼和IP地址一樣也是 32 位二進(jìn)制數(shù),不過它的網(wǎng)絡(luò)部分規(guī)定全部為 1,主機(jī)部分規(guī)定全部為 0.也就是說,假如上面那兩個IP地址的網(wǎng)絡(luò)部分為 24 位,主機(jī)部分為 8 位的話,那他們的子網(wǎng)掩碼都為 11111111.11111111.11111111.00000000,即255.255.255.0。
那有了子網(wǎng)掩碼,如何來判端IP地址是否處于同一個子網(wǎng)中呢。顯然,知道了子網(wǎng)掩碼,相當(dāng)于我們知道了網(wǎng)絡(luò)部分是幾位,主機(jī)部分是幾位。我們只需要把 IP 地址與它的子網(wǎng)掩碼做與(and)運(yùn)算,然后把各自的結(jié)果進(jìn)行比較就行了,如果比較的結(jié)果相同,則代表是同一個子網(wǎng),否則不是同一個子網(wǎng)。
2. ARP協(xié)議
有了上面IP協(xié)議的知識,我們回來講一下ARP協(xié)議。
有了兩臺計算機(jī)的IP地址與子網(wǎng)掩碼,我們就可以判斷出它們是否處于同一個子網(wǎng)之中了。
假如他們處于同一個子網(wǎng)之中,計算機(jī)A要給計算機(jī)B發(fā)送數(shù)據(jù)時。我們可以通過ARP協(xié)議來得到計算機(jī)B的MAC地址。
ARP協(xié)議也是通過廣播的形式給同一個子網(wǎng)中的每臺電腦發(fā)送一個數(shù)據(jù)包(當(dāng)然,這個數(shù)據(jù)包會包含接收方的IP地址)。對方收到這個數(shù)據(jù)包之后,會取出IP地址與自身的對比,如果相同,則把自己的MAC地址回復(fù)給對方,否則就丟棄這個數(shù)據(jù)包。這樣,計算機(jī)A就能知道計算機(jī)B的MAC地址了。
可能有人會問,知道了MAC地址之后,發(fā)送數(shù)據(jù)是通過廣播的形式發(fā)送,詢問對方的MAC地址也是通過廣播的形式來發(fā)送,那其他計算機(jī)怎么知道你是要傳送數(shù)據(jù)還是要詢問MAC地址呢?其實在詢問MAC地址的數(shù)據(jù)包中,在對方的MAC地址這一欄中,填的是一個特殊的MAC地址,其他計算機(jī)看到這個特殊的MAC地址之后,就能知道廣播想干嘛了。
假如兩臺計算機(jī)的IP不是處于同一個子網(wǎng)之中,這個時候,我們就會把數(shù)據(jù)包發(fā)送給網(wǎng)關(guān),然后讓網(wǎng)關(guān)讓我們進(jìn)行轉(zhuǎn)發(fā)傳送
3. DNS服務(wù)器
這里再說一個問題,我們是如何知道對方計算機(jī)的IP地址的呢?這個問題可能有人會覺得很白癡,心想,當(dāng)然是計算機(jī)的操作者來進(jìn)行輸入了。這沒錯,當(dāng)我們想要訪問某個網(wǎng)站的時候,我們可以輸入IP來進(jìn)行訪問,但是我相信絕大多數(shù)人是輸入一個網(wǎng)址域名的,例如訪問百度是輸入 www.baidu.com 這個域名。其實當(dāng)我們輸入這個域名時,會有一個叫做DNS服務(wù)器的家伙來幫我們解析這個域名,然后返回這個域名對應(yīng)的IP給我們的。
因此,網(wǎng)絡(luò)層的功能就是讓我們在茫茫人海中,能夠找到另一臺計算機(jī)在哪里,是否屬于同一個子網(wǎng)等。
4. 傳輸層
通過物理層、數(shù)據(jù)鏈路層以及網(wǎng)絡(luò)層的互相幫助,我們已經(jīng)把數(shù)據(jù)成功從計算機(jī)A傳送到計算機(jī)B了,可是,計算機(jī)B里面有各種各樣的應(yīng)用程序,計算機(jī)該如何知道這些數(shù)據(jù)是給誰的呢?
這個時候,**端口(Port)**這個家伙就上場了,也就是說,我們在從計算機(jī)A傳數(shù)據(jù)給計算表B的時候,還得指定一個端口,以供特定的應(yīng)用程序來接受處理。
傳輸層的功能就是建立端口到端口的通信。相比網(wǎng)絡(luò)層的功能是建立主機(jī)到主機(jī)的通信。
也就是說,只有有了IP和端口,我們才能進(jìn)行準(zhǔn)確著通信。這個時候可能有人會說,我輸入IP地址的時候并沒有指定一個端口啊。其實呢,對于有些傳輸協(xié)議,已經(jīng)有設(shè)定了一些默認(rèn)端口了。例如http的傳輸默認(rèn)端口是80,這些端口信息也會包含在數(shù)據(jù)包里的。
傳輸層最常見的兩大協(xié)議是 TCP 協(xié)議和 UDP 協(xié)議,其中 TCP 協(xié)議與 UDP 最大的不同就是 TCP 提供可靠的傳輸,而 UDP 提供的是不可靠傳輸。
5. 應(yīng)用層
雖然我們收到了傳輸層傳來的數(shù)據(jù),可是這些傳過來的數(shù)據(jù)五花八門,有html格式的,有mp4格式的,各種各樣。你確定你能看的懂?
因此我們需要指定這些數(shù)據(jù)的格式規(guī)則,收到后才好解讀渲染。例如我們最常見的 Http 數(shù)據(jù)包中,就會指定該數(shù)據(jù)包是 什么格式的文件了。
-
一次完整的HTTP請求過程包括哪些內(nèi)容
第一種回答
- 建立起客戶機(jī)和服務(wù)器連接。
- 建立連接后,客戶機(jī)發(fā)送一個請求給服務(wù)器。
- 服務(wù)器收到請求給予響應(yīng)信息。
- 客戶端瀏覽器將返回的內(nèi)容解析并呈現(xiàn),斷開連接。
第二種回答
域名解析 --> 發(fā)起TCP的3次握手 --> 建立TCP連接后發(fā)起http請求 --> 服務(wù)器響應(yīng)http請求,瀏覽器得到html代碼 --> 瀏覽器解析html代碼,并請求html代碼中的資源(如js、css、圖片等) --> 瀏覽器對頁面進(jìn)行渲染呈現(xiàn)給用戶。
-
談?wù)勀銓νV沟却齾f(xié)議的理解
停止等待協(xié)議是為了實現(xiàn)可靠傳輸?shù)?#xff0c;它的基本原理就是每發(fā)完一個分組就停止發(fā)送,等待對方確認(rèn)。在收到確認(rèn)后再發(fā)下一個分組;在停止等待協(xié)議中,若接收方收到重復(fù)分組,就丟棄該分組,但同時還要發(fā)送確認(rèn)。主要包括以下幾種情況:無差錯情況、出現(xiàn)差錯情況(超時重傳)、確認(rèn)丟失和確認(rèn)遲到。
-
談?wù)勀銓?ARQ 協(xié)議的理解
自動重傳請求 ARQ 協(xié)議
停止等待協(xié)議中超時重傳是指只要超過一段時間仍然沒有收到確認(rèn),就重傳前面發(fā)送過的分組(認(rèn)為剛才發(fā)送過的分組丟失了)。因此每發(fā)送完一個分組需要設(shè)置一個超時計時器,其重傳時間應(yīng)比數(shù)據(jù)在分組傳輸?shù)钠骄禃r間更長一些。這種自動重傳方式常稱為自動重傳請求 ARQ。
連續(xù) ARQ 協(xié)議
連續(xù) ARQ 協(xié)議可提高信道利用率。發(fā)送方維持一個發(fā)送窗口,凡位于發(fā)送窗口內(nèi)的分組可以連續(xù)發(fā)送出去,而不需要等待對方確認(rèn)。接收方一般采用累計確認(rèn),對按序到達(dá)的最后一個分組發(fā)送確認(rèn),表明到這個分組為止的所有分組都已經(jīng)正確收到了。
-
Cookie是什么與用途
Cookie 是服務(wù)器發(fā)送到用戶瀏覽器并保存在本地的一小塊數(shù)據(jù),它會在瀏覽器之后向同一服務(wù)器再次發(fā)起請求時被攜帶上,用于告知服務(wù)端兩個請求是否來自同一瀏覽器。由于之后每次請求都會需要攜帶 Cookie 數(shù)據(jù),因此會帶來額外的性能開銷.。
- 會話狀態(tài)管理(如用戶登錄狀態(tài)、購物車、游戲分?jǐn)?shù)或其它需要記錄的信息)
- 個性化設(shè)置(如用戶自定義設(shè)置、主題等)
- 瀏覽器行為跟蹤(如跟蹤分析用戶行為等)
-
Session知識總結(jié)
Session 存儲在服務(wù)器端,存儲在服務(wù)器端的信息更加安全。
Session 可以存儲在服務(wù)器上的文件、數(shù)據(jù)庫或者內(nèi)存中。也可以將 Session 存儲在 Redis 這種內(nèi)存型數(shù)據(jù)庫中,效率會更高。
使用 Session 維護(hù)用戶登錄狀態(tài)的過程如下:
- 用戶進(jìn)行登錄時,用戶提交包含用戶名和密碼的表單,放入 HTTP 請求報文中;
- 服務(wù)器驗證該用戶名和密碼,如果正確則把用戶信息存儲到 Redis 中,它在 Redis 中的 Key 稱為 Session ID;
- 服務(wù)器返回的響應(yīng)報文的 Set-Cookie 首部字段包含了這個 Session ID,客戶端收到響應(yīng)報文之后將該 Cookie 值存入瀏覽器中;
- 客戶端之后對同一個服務(wù)器進(jìn)行請求時會包含該 Cookie 值,服務(wù)器收到之后提取出 Session ID,從 Redis 中取出用戶信息,繼續(xù)之前的業(yè)務(wù)操作。
工作原理
session 的工作原理是客戶端登錄完成之后,服務(wù)器會創(chuàng)建對應(yīng)的 session,session 創(chuàng)建完之后,會把 session 的 id 發(fā)送給客戶端,客戶端再存儲到瀏覽器中。這樣客戶端每次訪問服務(wù)器時,都會帶著 sessionid,服務(wù)器拿到 sessionid 之后,在內(nèi)存找到與之對應(yīng)的 session 這樣就可以正常工作了。
-
Cookie與Session的對比
Cookie和Session都是客戶端與服務(wù)器之間保持狀態(tài)的解決方案 1,存儲的位置不同,cookie:存放在客戶端,session:存放在服務(wù)端。Session存儲的數(shù)據(jù)比較安全 2,存儲的數(shù)據(jù)類型不同 兩者都是key-value的結(jié)構(gòu),但針對value的類型是有差異的 cookie:value只能是字符串類型,session:value是Object類型 3,存儲的數(shù)據(jù)大小限制不同 cookie:大小受瀏覽器的限制,很多是是4K的大小, session:理論上受當(dāng)前內(nèi)存的限制, 4,生命周期的控制 cookie的生命周期當(dāng)瀏覽器關(guān)閉的時候,就消亡了 (1)cookie的生命周期是累計的,從創(chuàng)建時,就開始計時,20分鐘后,cookie生命周期結(jié)束, (2)session的生命周期是間隔的,從創(chuàng)建時,開始計時如在20分鐘,沒有訪問session,那么session生命周期被銷毀
-
使用 Session 的過程是怎樣的
過程如下:
- 用戶進(jìn)行登錄時,用戶提交包含用戶名和密碼的表單,放入 HTTP 請求報文中;
- 服務(wù)器驗證該用戶名和密碼,如果正確則把用戶信息存儲到 Redis 中,它在 Redis 中的 Key 稱為 Session ID;
- 服務(wù)器返回的響應(yīng)報文的 Set-Cookie 首部字段包含了這個 Session ID,客戶端收到響應(yīng)報文之后將該 Cookie 值存入瀏覽器中;
- 客戶端之后對同一個服務(wù)器進(jìn)行請求時會包含該 Cookie 值,服務(wù)器收到之后提取出 Session ID,從 Redis 中取出用戶信息,繼續(xù)之前的業(yè)務(wù)操作。
注意:Session ID 的安全性問題,不能讓它被惡意攻擊者輕易獲取,那么就不能產(chǎn)生一個容易被猜到的 Session ID 值。此外,還需要經(jīng)常重新生成 Session ID。在對安全性要求極高的場景下,例如轉(zhuǎn)賬等操作,除了使用 Session 管理用戶狀態(tài)之外,還需要對用戶進(jìn)行重新驗證,比如重新輸入密碼,或者使用短信驗證碼等方式。
-
?;钣嫊r器的作用
除時間等待計時器外,TCP 還有一個?;钣嫊r器(keepalive timer)。設(shè)想這樣的場景:客戶已主動與服務(wù)器建立了 TCP 連接。但后來客戶端的主機(jī)突然發(fā)生故障。顯然,服務(wù)器以后就不能再收到客戶端發(fā)來的數(shù)據(jù)。應(yīng)當(dāng)有措施使服務(wù)器不要再白白等待下去。這就需要使用保活計時器了。
服務(wù)器每收到一次客戶的數(shù)據(jù),就重新設(shè)置?;钣嫊r器,時間的設(shè)置通常是兩個小時。若兩個小時都沒有收到客戶端的數(shù)據(jù),服務(wù)端就發(fā)送一個探測報文段,以后則每隔 75 秒鐘發(fā)送一次。若連續(xù)發(fā)送 10個 探測報文段后仍然無客戶端的響應(yīng),服務(wù)端就認(rèn)為客戶端出了故障,接著就關(guān)閉這個連接。
-
DDos攻擊、Sql注入、SYN攻擊、XSS攻擊、CSRF攻擊
DDos攻擊
客戶端向服務(wù)端發(fā)送請求鏈接數(shù)據(jù)包,服務(wù)端向客戶端發(fā)送確認(rèn)數(shù)據(jù)包,客戶端不向服務(wù)端發(fā)送確認(rèn)數(shù)據(jù)包,服務(wù)器一直等待來自客戶端的確認(rèn) 沒有徹底根治的辦法,除非不使用TCP DDos 預(yù)防: 1)限制同時打開SYN半鏈接的數(shù)目 2)縮短SYN半鏈接的Time out 時間 3)關(guān)閉不必要的服務(wù)
SQL注入攻擊
攻擊者在HTTP請求中注入惡意的SQL代碼,服務(wù)器使用參數(shù)構(gòu)建數(shù)據(jù)庫SQL命令時,惡意SQL被一起構(gòu)造,并在數(shù)據(jù)庫中執(zhí)行。 用戶登錄,輸入用戶名 lianggzone,密碼 ‘ or ‘1’=’1 ,如果此時使用參數(shù)構(gòu)造的方式,就會出現(xiàn) select * from user where name = ‘lianggzone’ and password = ‘’ or ‘1’=‘1’ 不管用戶名和密碼是什么內(nèi)容,使查詢出來的用戶列表不為空。如何防范SQL注入攻擊使用預(yù)編譯的PrepareStatement是必須的,但是一般我們會從兩個方面同時入手。 Web端 1)有效性檢驗。 2)限制字符串輸入的長度。 服務(wù)端 1)不用拼接SQL字符串。 2)使用預(yù)編譯的PrepareStatement。 3)有效性檢驗。(為什么服務(wù)端還要做有效性檢驗?第一準(zhǔn)則,外部都是不可信的,防止攻擊者繞過Web端請求) 4)過濾SQL需要的參數(shù)中的特殊字符。比如單引號、雙引號。
SYN攻擊
服務(wù)器端的資源分配是在二次握手時分配的,而客戶端的資源是在完成三次握手時分配的,所以服務(wù)器容易受到SYN洪泛攻擊。SYN攻擊就是Client在短時間內(nèi)偽造大量不存在的IP地址,并向Server不斷地發(fā)送SYN包,Server則回復(fù)確認(rèn)包,等待Client確認(rèn),由于源地址不存在,因此Server需要不斷重發(fā)直至超時,這些偽造的SYN包將長時間占用未連接隊列,導(dǎo)致正常的SYN請求因為隊列滿而被丟棄,從而引起網(wǎng)絡(luò)擁塞甚至系統(tǒng)癱瘓。SYN 攻擊是一種典型的 DoS/DDoS 攻擊。
常見的防御 SYN 攻擊的方法有如下幾種:
- 縮短超時(SYN Timeout)時間
- 增加最大半連接數(shù)
- 過濾網(wǎng)關(guān)防護(hù)
- SYN cookies技術(shù)
XSS攻擊
跨站點(diǎn)腳本攻擊,指攻擊者通過篡改網(wǎng)頁,嵌入惡意腳本程序,在用戶瀏覽網(wǎng)頁時,控制用戶瀏覽器進(jìn)行惡意操作的一種攻擊方式。如何防范XSS攻擊 1)前端,服務(wù)端,同時需要字符串輸入的長度限制。 2)前端,服務(wù)端,同時需要對HTML轉(zhuǎn)義處理。將其中的”<”,”>”等特殊字符進(jìn)行轉(zhuǎn)義編碼。 防 XSS 的核心是必須對輸入的數(shù)據(jù)做過濾處理。
CSRF攻擊
跨站點(diǎn)請求偽造,指攻擊者通過跨站請求,以合法的用戶的身份進(jìn)行非法操作??梢赃@么理解CSRF攻擊:攻擊者盜用你的身份,以你的名義向第三方網(wǎng)站發(fā)送惡意請求。CRSF能做的事情包括利用你的身份發(fā)郵件,發(fā)短信,進(jìn)行交易轉(zhuǎn)賬,甚至盜取賬號信息
如何防范CSRF攻擊
安全框架,例如Spring Security。 token機(jī)制。在HTTP請求中進(jìn)行token驗證,如果請求中沒有token或者token內(nèi)容不正確,則認(rèn)為CSRF攻擊而拒絕該請求。 驗證碼。通常情況下,驗證碼能夠很好的遏制CSRF攻擊,但是很多情況下,出于用戶體驗考慮,驗證碼只能作為一種輔助手段,而不是最主要的解決方案。 referer識別。在HTTP Header中有一個字段Referer,它記錄了HTTP請求的來源地址。如果Referer是其他網(wǎng)站,就有可能是CSRF攻擊,則拒絕該請求。但是,服務(wù)器并非都能取到Referer。很多用戶出于隱私保護(hù)的考慮,限制了Referer的發(fā)送。在某些情況下,瀏覽器也不會發(fā)送Referer,例如HTTPS跳轉(zhuǎn)到HTTP。 1)驗證請求來源地址; 2)關(guān)鍵操作添加驗證碼; 3)在請求地址添加 token 并驗證。
-
服務(wù)器出現(xiàn)大量close_wait的連接的原因是什么?有什么解決方法
close_wait狀態(tài)是在TCP四次揮手的時候收到FIN但是沒有發(fā)送自己的FIN時出現(xiàn)的,服務(wù)器出現(xiàn)大量close_wait狀態(tài)的原因有兩種:
- 服務(wù)器內(nèi)部業(yè)務(wù)處理占用了過多時間,都沒能處理完業(yè)務(wù);或者還有數(shù)據(jù)需要發(fā)送;或者服務(wù)器的業(yè)務(wù)邏輯有問題,沒有執(zhí)行close()方法
- 服務(wù)器的父進(jìn)程派生出子進(jìn)程,子進(jìn)程繼承了socket,收到FIN的時候子進(jìn)程處理但父進(jìn)程沒有處理該信號,導(dǎo)致socket的引用不為0無法回收
處理方法:
- 停止應(yīng)用程序
- 修改程序里的bug
-
MTU和MSS分別是什么
MTU:maximum transmission unit,最大傳輸單元,由硬件規(guī)定,如以太網(wǎng)的MTU為1500字節(jié)。
MSS:maximum segment size,最大分節(jié)大小,為TCP數(shù)據(jù)包每次傳輸?shù)淖畲髷?shù)據(jù)分段大小,一般由發(fā)送端向?qū)Χ薚CP通知對端在每個分節(jié)中能發(fā)送的最大TCP數(shù)據(jù)。MSS值為MTU值減去IPv4 Header(20 Byte)和TCP header(20 Byte)得到。
-
Ping命令基于什么協(xié)議?原理是什么
ping是基于網(wǎng)絡(luò)層的ICMP協(xié)議實現(xiàn)的。通過向?qū)Ψ桨l(fā)送一個ICMP回送請求報文,如果對方主機(jī)可達(dá)的話會收到該報文,并響應(yīng)一個ICMP回送回答報文。
擴(kuò)展:ICMP報文的介紹。ICMP報文分為兩個種類:
- ICMP差錯報告報文,常見的有
- 終點(diǎn)不可達(dá)
- 時間超過
- 參數(shù)問題
- 改變路由
- ICMP詢問報文
- 回送請求和回答:向特定主機(jī)發(fā)出回送請求報文,收到回送請求報文的主機(jī)響應(yīng)回送回答報文。
- 時間戳請求和回答:詢問對方當(dāng)前的時間,返回的是一個32位的時間戳。
- ICMP差錯報告報文,常見的有
5. 熟悉操作系統(tǒng)
5.1 進(jìn)程、線程管理
-
進(jìn)程和線程基礎(chǔ)知識
進(jìn)程:進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨(dú)立單位,是系統(tǒng)中的并發(fā)執(zhí)行的單位。
線程:線程是進(jìn)程的一個實體,也是 CPU 調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位,有時又被稱為輕權(quán)進(jìn)程或輕量級進(jìn)程。
-
進(jìn)程
運(yùn)行中的程序,就被稱為「進(jìn)程」(Process)。
在一個進(jìn)程的活動期間至少具備三種基本狀態(tài),即運(yùn)行狀態(tài)、就緒狀態(tài)、阻塞狀態(tài)。
- 運(yùn)行狀態(tài)(Running):該時刻進(jìn)程占用 CPU;
- 就緒狀態(tài)(Ready):可運(yùn)行,由于其他進(jìn)程處于運(yùn)行狀態(tài)而暫時停止運(yùn)行;
- 阻塞狀態(tài)(Blocked):該進(jìn)程正在等待某一事件發(fā)生(如等待輸入/輸出操作的完成)而暫時停止運(yùn)行,這時,即使給它CPU控制權(quán),它也無法運(yùn)行;
當(dāng)然,進(jìn)程還有另外兩個基本狀態(tài):
- 創(chuàng)建狀態(tài)(new):進(jìn)程正在被創(chuàng)建時的狀態(tài);
- 結(jié)束狀態(tài)(Exit):進(jìn)程正在從系統(tǒng)中消失時的狀態(tài);
掛起狀態(tài)可以分為兩種:
- 阻塞掛起狀態(tài):進(jìn)程在外存(硬盤)并等待某個事件的出現(xiàn);
- 就緒掛起狀態(tài):進(jìn)程在外存(硬盤),但只要進(jìn)入內(nèi)存,即刻立刻運(yùn)行;
PCB 進(jìn)程控制塊 是進(jìn)程存在的唯一標(biāo)識
PCB 具體包含什么信息呢?
進(jìn)程描述信息:
- 進(jìn)程標(biāo)識符:標(biāo)識各個進(jìn)程,每個進(jìn)程都有一個并且唯一的標(biāo)識符;
- 用戶標(biāo)識符:進(jìn)程歸屬的用戶,用戶標(biāo)識符主要為共享和保護(hù)服務(wù);
進(jìn)程控制和管理信息:
- 進(jìn)程當(dāng)前狀態(tài),如 new、ready、running、waiting 或 blocked 等;
- 進(jìn)程優(yōu)先級:進(jìn)程搶占 CPU 時的優(yōu)先級;
資源分配清單:
- 有關(guān)內(nèi)存地址空間或虛擬地址空間的信息,所打開文件的列表和所使用的 I/O 設(shè)備信息。
CPU 相關(guān)信息:
- CPU 中各個寄存器的值,當(dāng)進(jìn)程被切換時,CPU 的狀態(tài)信息都會被保存在相應(yīng)的 PCB 中,以便進(jìn)程重新執(zhí)行時,能從斷點(diǎn)處繼續(xù)執(zhí)行。
-
線程
線程是進(jìn)程當(dāng)中的一條執(zhí)行流程。
同一個進(jìn)程內(nèi)多個線程之間可以共享代碼段、數(shù)據(jù)段、打開的文件等資源,但每個線程各自都有一套獨(dú)立的寄存器和棧,這樣可以確保線程的控制流是相對獨(dú)立的。
線程的實現(xiàn)
主要有三種線程的實現(xiàn)方式:
- 用戶線程(*User Thread*):在用戶空間實現(xiàn)的線程,不是由內(nèi)核管理的線程,是由用戶態(tài)的線程庫來完成線程的管理;
- 內(nèi)核線程(*Kernel Thread*):在內(nèi)核中實現(xiàn)的線程,是由內(nèi)核管理的線程;
- 輕量級進(jìn)程(*LightWeight Process*):在內(nèi)核中來支持用戶線程;
第一種關(guān)系是多對一的關(guān)系,也就是多個用戶線程對應(yīng)同一個內(nèi)核線程
第二種是一對一的關(guān)系,也就是一個用戶線程對應(yīng)一個內(nèi)核線程
第三種是多對多的關(guān)系,也就是多個用戶線程對應(yīng)到多個內(nèi)核線程
用戶線程的整個線程管理和調(diào)度,操作系統(tǒng)是不直接參與的,而是由用戶級線程庫函數(shù)來完成線程的管理,包括線程的創(chuàng)建、終止、同步和調(diào)度等。
線程的優(yōu)點(diǎn):
- 一個進(jìn)程中可以同時存在多個線程;
- 各個線程之間可以并發(fā)執(zhí)行;
- 各個線程之間可以共享地址空間和文件等資源;
線程的缺點(diǎn):
- 當(dāng)進(jìn)程中的一個線程崩潰時,會導(dǎo)致其所屬進(jìn)程的所有線程崩潰(這里是針對 C/C++ 語言,Java語言中的線程奔潰不會造成進(jìn)程崩潰
內(nèi)核線程是由操作系統(tǒng)管理的,線程對應(yīng)的 TCB 自然是放在操作系統(tǒng)里的,這樣線程的創(chuàng)建、終止和管理都是由操作系統(tǒng)負(fù)責(zé)。
-
-
進(jìn)程/線程上下文切換
-
進(jìn)程
一個進(jìn)程切換到另一個進(jìn)程運(yùn)行,稱為進(jìn)程的上下文切換。
進(jìn)程的上下文切換到底是切換什么呢?
進(jìn)程是由內(nèi)核管理和調(diào)度的,所以進(jìn)程的切換只能發(fā)生在內(nèi)核態(tài)。
所以,進(jìn)程的上下文切換不僅包含了虛擬內(nèi)存、棧、全局變量等用戶空間的資源,還包括了內(nèi)核堆棧、寄存器等內(nèi)核空間的資源。
通常,會把交換的信息保存在進(jìn)程的 PCB,當(dāng)要運(yùn)行另外一個進(jìn)程的時候,我們需要從這個進(jìn)程的 PCB 取出上下文,然后恢復(fù)到 CPU 中,這使得這個進(jìn)程可以繼續(xù)執(zhí)行
發(fā)生進(jìn)程上下文切換有哪些場景?
- 為了保證所有進(jìn)程可以得到公平調(diào)度,CPU 時間被劃分為一段段的時間片,這些時間片再被輪流分配給各個進(jìn)程。這樣,當(dāng)某個進(jìn)程的時間片耗盡了,進(jìn)程就從運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài),系統(tǒng)從就緒隊列選擇另外一個進(jìn)程運(yùn)行;
- 進(jìn)程在系統(tǒng)資源不足(比如內(nèi)存不足)時,要等到資源滿足后才可以運(yùn)行,這個時候進(jìn)程也會被掛起,并由系統(tǒng)調(diào)度其他進(jìn)程運(yùn)行;
- 當(dāng)進(jìn)程通過睡眠函數(shù) sleep 這樣的方法將自己主動掛起時,自然也會重新調(diào)度;
- 當(dāng)有優(yōu)先級更高的進(jìn)程運(yùn)行時,為了保證高優(yōu)先級進(jìn)程的運(yùn)行,當(dāng)前進(jìn)程會被掛起,由高優(yōu)先級進(jìn)程來運(yùn)行;
- 發(fā)生硬件中斷時,CPU 上的進(jìn)程會被中斷掛起,轉(zhuǎn)而執(zhí)行內(nèi)核中的中斷服務(wù)程序;
-
線程
-
線程上下文切換的是什么?
這還得看線程是不是屬于同一個進(jìn)程:
- 當(dāng)兩個線程不是屬于同一個進(jìn)程,則切換的過程就跟進(jìn)程上下文切換一樣;
- 當(dāng)兩個線程是屬于同一個進(jìn)程,因為虛擬內(nèi)存是共享的,所以在切換時,虛擬內(nèi)存這些資源就保持不動,只需要切換線程的私有數(shù)據(jù)、寄存器等不共享的數(shù)據(jù);
所以,線程的上下文切換相比進(jìn)程,開銷要小很多。
-
-
-
進(jìn)程/線程間通信方式
進(jìn)程間通信(IPC,InterProcess Communication)是指在不同進(jìn)程之間傳播或交換信息。IPC 的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams 等。其中 Socket 和 Streams 支持不同主機(jī)上的兩個進(jìn)程 IPC。
管道
- 它是半雙工的,具有固定的讀端和寫端;
- 它只能用于父子進(jìn)程或者兄弟進(jìn)程之間的進(jìn)程的通信;
- 它可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的 read、write 等函數(shù)。但是它不是普通的文件,并不屬于其他任何文件系統(tǒng),并且只存在于內(nèi)存中。
命名管道
- FIFO 可以在無關(guān)的進(jìn)程之間交換數(shù)據(jù),與無名管道不同;
- FIFO 有路徑名與之相關(guān)聯(lián),它以一種特殊設(shè)備文件形式存在于文件系統(tǒng)中。
消息隊列
- 消息隊列,是消息的鏈接表,存放在內(nèi)核中。一個消息隊列由一個標(biāo)識符 ID 來標(biāo)識;
- 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優(yōu)先級;
- 消息隊列獨(dú)立于發(fā)送與接收進(jìn)程。進(jìn)程終止時,消息隊列及其內(nèi)容并不會被刪除;
- 消息隊列可以實現(xiàn)消息的隨機(jī)查詢,消息不一定要以先進(jìn)先出的次序讀取,也可以按消息的類型讀取。
信號量
- 信號量(semaphore)是一個計數(shù)器。用于實現(xiàn)進(jìn)程間的互斥與同步,而不是用于存儲進(jìn)程間通信數(shù)據(jù);
- 信號量用于進(jìn)程間同步,若要在進(jìn)程間傳遞數(shù)據(jù)需要結(jié)合共享內(nèi)存;
- 信號量基于操作系統(tǒng)的 PV 操作,程序?qū)π盘柫康牟僮鞫际窃硬僮?#xff1b;
- 每次對信號量的 PV 操作不僅限于對信號量值加 1 或減 1,而且可以加減任意正整數(shù);
- 支持信號量組。
共享內(nèi)存
- 共享內(nèi)存(Shared Memory),指兩個或多個進(jìn)程共享一個給定的存儲區(qū);
- 共享內(nèi)存是最快的一種 IPC,因為進(jìn)程是直接對內(nèi)存進(jìn)行存取。
Socket通信
前面說到的通信機(jī)制,都是工作于同一臺主機(jī),如果要與不同主機(jī)的進(jìn)程間通信,那么就需要 Socket 通信了。Socket 實際上不僅用于不同的主機(jī)進(jìn)程間通信,還可以用于本地主機(jī)進(jìn)程間通信,可根據(jù)創(chuàng)建 Socket 的類型不同,分為三種常見的通信方式,一個是基于 TCP 協(xié)議的通信方式,一個是基于 UDP 協(xié)議的通信方式,一個是本地進(jìn)程間通信方式。
以上,就是進(jìn)程間通信的主要機(jī)制了。你可能會問了,那線程通信間的方式呢?
同個進(jìn)程下的線程之間都是共享進(jìn)程的資源,只要是共享變量都可以做到線程間通信,比如全局變量,所以對于線程間關(guān)注的不是通信方式,而是關(guān)注多線程競爭共享資源的問題,信號量也同樣可以在線程間實現(xiàn)互斥與同步:
- 互斥的方式,可保證任意時刻只有一個線程訪問共享資源;
- 同步的方式,可保證線程 A 應(yīng)在線程 B 之前執(zhí)行;
-
線程、進(jìn)程崩潰發(fā)生什么
一般來說如果線程是因為非法訪問內(nèi)存引起的崩潰,那么進(jìn)程肯定會崩潰,為什么系統(tǒng)要讓進(jìn)程崩潰呢,這主要是因為在進(jìn)程中,各個線程的地址空間是共享的,既然是共享,那么某個線程對地址的非法訪問就會導(dǎo)致內(nèi)存的不確定性,進(jìn)而可能會影響到其他線程,這種操作是危險的,操作系統(tǒng)會認(rèn)為這很可能導(dǎo)致一系列嚴(yán)重的后果,于是干脆讓整個進(jìn)程崩潰
崩潰機(jī)制
- CPU 執(zhí)行正常的進(jìn)程指令
- 調(diào)用 kill 系統(tǒng)調(diào)用向進(jìn)程發(fā)送信號
- 進(jìn)程收到操作系統(tǒng)發(fā)的信號,CPU 暫停當(dāng)前程序運(yùn)行,并將控制權(quán)轉(zhuǎn)交給操作系統(tǒng)
- 調(diào)用 kill 系統(tǒng)調(diào)用向進(jìn)程發(fā)送信號(假設(shè)為 11,即 SIGSEGV,一般非法訪問內(nèi)存報的都是這個錯誤)
- 操作系統(tǒng)根據(jù)情況執(zhí)行相應(yīng)的信號處理程序(函數(shù)),一般執(zhí)行完信號處理程序邏輯后會讓進(jìn)程退出
注意上面的第五步,如果進(jìn)程沒有注冊自己的信號處理函數(shù),那么操作系統(tǒng)會執(zhí)行默認(rèn)的信號處理程序(一般最后會讓進(jìn)程退出),但如果注冊了,則會執(zhí)行自己的信號處理函數(shù),這樣的話就給了進(jìn)程一個垂死掙扎的機(jī)會,它收到 kill 信號后,可以調(diào)用 exit() 來退出,但也可以使用 sigsetjmp,siglongjmp 這兩個函數(shù)來恢復(fù)進(jìn)程的執(zhí)行
-
守護(hù)進(jìn)程、僵尸進(jìn)程和孤兒進(jìn)程
守護(hù)進(jìn)程
指在后臺運(yùn)行的,沒有控制終端與之相連的進(jìn)程。它獨(dú)立于控制終端,周期性地執(zhí)行某種任務(wù)。Linux的大多數(shù)服務(wù)器就是用守護(hù)進(jìn)程的方式實現(xiàn)的,如web服務(wù)器進(jìn)程http等
創(chuàng)建守護(hù)進(jìn)程要點(diǎn):
(1)讓程序在后臺執(zhí)行。方法是調(diào)用fork()產(chǎn)生一個子進(jìn)程,然后使父進(jìn)程退出。
(2)調(diào)用setsid()創(chuàng)建一個新對話期??刂平K端、登錄會話和進(jìn)程組通常是從父進(jìn)程繼承下來的,守護(hù)進(jìn)程要擺脫它們,不受它們的影響,方法是調(diào)用setsid()使進(jìn)程成為一個會話組長。setsid()調(diào)用成功后,進(jìn)程成為新的會話組長和進(jìn)程組長,并與原來的登錄會話、進(jìn)程組和控制終端脫離。
(3)禁止進(jìn)程重新打開控制終端。經(jīng)過以上步驟,進(jìn)程已經(jīng)成為一個無終端的會話組長,但是它可以重新申請打開一個終端。為了避免這種情況發(fā)生,可以通過使進(jìn)程不再是會話組長來實現(xiàn)。再一次通過fork()創(chuàng)建新的子進(jìn)程,使調(diào)用fork的進(jìn)程退出。
(4)關(guān)閉不再需要的文件描述符。子進(jìn)程從父進(jìn)程繼承打開的文件描述符。如不關(guān)閉,將會浪費(fèi)系統(tǒng)資源,造成進(jìn)程所在的文件系統(tǒng)無法卸下以及引起無法預(yù)料的錯誤。首先獲得最高文件描述符值,然后用一個循環(huán)程序,關(guān)閉0到最高文件描述符值的所有文件描述符。
(5)將當(dāng)前目錄更改為根目錄。
(6)子進(jìn)程從父進(jìn)程繼承的文件創(chuàng)建屏蔽字可能會拒絕某些許可權(quán)。為防止這一點(diǎn),使用unmask(0)將屏蔽字清零。
(7)處理SIGCHLD信號。對于服務(wù)器進(jìn)程,在請求到來時往往生成子進(jìn)程處理請求。如果子進(jìn)程等待父進(jìn)程捕獲狀態(tài),則子進(jìn)程將成為僵尸進(jìn)程(zombie),從而占用系統(tǒng)資源。如果父進(jìn)程等待子進(jìn)程結(jié)束,將增加父進(jìn)程的負(fù)擔(dān),影響服務(wù)器進(jìn)程的并發(fā)性能。在Linux下可以簡單地將SIGCHLD信號的操作設(shè)為SIG_IGN。這樣,子進(jìn)程結(jié)束時不會產(chǎn)生僵尸進(jìn)程。
孤兒進(jìn)程
如果父進(jìn)程先退出,子進(jìn)程還沒退出,那么子進(jìn)程的父進(jìn)程將變?yōu)閕nit進(jìn)程。(注:任何一個進(jìn)程都必須有父進(jìn)程)。
一個父進(jìn)程退出,而它的一個或多個子進(jìn)程還在運(yùn)行,那么那些子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號為1)所收養(yǎng),并由init進(jìn)程對它們完成狀態(tài)收集工作。
僵尸進(jìn)程
如果子進(jìn)程先退出,父進(jìn)程還沒退出,那么子進(jìn)程必須等到父進(jìn)程捕獲到了子進(jìn)程的退出狀態(tài)才真正結(jié)束,否則這個時候子進(jìn)程就成為僵尸進(jìn)程。
設(shè)置僵尸進(jìn)程的目的是維護(hù)子進(jìn)程的信息,以便父進(jìn)程在以后某個時候獲取。這些信息至少包括進(jìn)程ID,進(jìn)程的終止?fàn)顟B(tài),以及該進(jìn)程使用的CPU時間,所以當(dāng)終止子進(jìn)程的父進(jìn)程調(diào)用wait或waitpid時就可以得到這些信息。如果一個進(jìn)程終止,而該進(jìn)程有子進(jìn)程處于僵尸狀態(tài),那么它的所有僵尸子進(jìn)程的父進(jìn)程ID將被重置為1(init進(jìn)程)。繼承這些子進(jìn)程的init進(jìn)程將清理它們(也就是說init進(jìn)程將wait它們,從而去除它們的僵尸狀態(tài))。
如何避免僵尸進(jìn)程
- 通過signal(SIGCHLD, SIG_IGN)通知內(nèi)核對子進(jìn)程的結(jié)束不關(guān)心,由內(nèi)核回收。如果不想讓父進(jìn)程掛起,可以在父進(jìn)程中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父進(jìn)程忽略SIGCHLD信號,該信號是子進(jìn)程退出的時候向父進(jìn)程發(fā)送的。
- 父進(jìn)程調(diào)用wait/waitpid等函數(shù)等待子進(jìn)程結(jié)束,如果尚無子進(jìn)程退出wait會導(dǎo)致父進(jìn)程阻塞。waitpid可以通過傳遞WNOHANG使父進(jìn)程不阻塞立即返回。
- 如果父進(jìn)程很忙可以用signal注冊信號處理函數(shù),在信號處理函數(shù)調(diào)用wait/waitpid等待子進(jìn)程退出。
- 通過兩次調(diào)用fork。父進(jìn)程首先調(diào)用fork創(chuàng)建一個子進(jìn)程然后waitpid等待子進(jìn)程退出,子進(jìn)程再fork一個孫進(jìn)程后退出。這樣子進(jìn)程退出后會被父進(jìn)程等待回收,而對于孫子進(jìn)程其父進(jìn)程已經(jīng)退出所以孫進(jìn)程成為一個孤兒進(jìn)程,孤兒進(jìn)程由init進(jìn)程接管,孫進(jìn)程結(jié)束后,init會等待回收。
第一種方法忽略SIGCHLD信號,這常用于并發(fā)服務(wù)器的性能的一個技巧因為并發(fā)服務(wù)器常常fork很多子進(jìn)程,子進(jìn)程終結(jié)之后需要服務(wù)器進(jìn)程去wait清理資源。如果將此信號的處理方式設(shè)為忽略,可讓內(nèi)核把僵尸子進(jìn)程轉(zhuǎn)交給init進(jìn)程去處理,省去了大量僵尸進(jìn)程占用系統(tǒng)資源。
-
進(jìn)程和線程的比較
線程是調(diào)度的基本單位,而進(jìn)程則是資源擁有的基本單位。
線程與進(jìn)程的比較如下:
- 進(jìn)程是資源(包括內(nèi)存、打開的文件等)分配的單位,線程是 CPU 調(diào)度的單位;
- 進(jìn)程擁有一個完整的資源平臺,而線程只獨(dú)享必不可少的資源,如寄存器和棧;
- 線程同樣具有就緒、阻塞、執(zhí)行三種基本狀態(tài),同樣具有狀態(tài)之間的轉(zhuǎn)換關(guān)系;
- 線程能減少并發(fā)執(zhí)行的時間和空間開銷;
對于,線程相比進(jìn)程能減少開銷,體現(xiàn)在:
- 線程的創(chuàng)建時間比進(jìn)程快,因為進(jìn)程在創(chuàng)建的過程中,還需要資源管理信息,比如內(nèi)存管理信息、文件管理信息,而線程在創(chuàng)建的過程中,不會涉及這些資源管理信息,而是共享它們;
- 線程的終止時間比進(jìn)程快,因為線程釋放的資源相比進(jìn)程少很多;
- 同一個進(jìn)程內(nèi)的線程切換比進(jìn)程切換快,因為線程具有相同的地址空間(虛擬內(nèi)存共享),這意味著同一個進(jìn)程的線程都具有同一個頁表,那么在切換的時候不需要切換頁表。而對于進(jìn)程之間的切換,切換的時候要把頁表給切換掉,而頁表的切換過程開銷是比較大的;
- 由于同一進(jìn)程的各線程間共享內(nèi)存和文件資源,那么在線程之間數(shù)據(jù)傳遞的時候,就不需要經(jīng)過內(nèi)核了,這就使得線程之間的數(shù)據(jù)交互效率更高了;
所以,不管是時間效率,還是空間效率線程比進(jìn)程都要高。
1、線程啟動速度快,輕量級
2、線程的系統(tǒng)開銷小
3、線程使用有一定難度,需要處理數(shù)據(jù)一致性問題
4、同一線程共享的有堆、全局變量、靜態(tài)變量、指針,引用、文件等,而獨(dú)自占有棧
- 進(jìn)程是資源分配的最小單位,而線程是 CPU 調(diào)度的最小單位;
- 創(chuàng)建進(jìn)程或撤銷進(jìn)程,系統(tǒng)都要為之分配或回收資源,操作系統(tǒng)開銷遠(yuǎn)大于創(chuàng)建或撤銷線程時的開銷;
- 不同進(jìn)程地址空間相互獨(dú)立,同一進(jìn)程內(nèi)的線程共享同一地址空間。一個進(jìn)程的線程在另一個進(jìn)程內(nèi)是不可見的;
- 進(jìn)程間不會相互影響,而一個線程掛掉將可能導(dǎo)致整個進(jìn)程掛掉;
5.2 內(nèi)存管理
-
物理地址、邏輯地址、虛擬內(nèi)存的概念
- 物理地址:它是地址轉(zhuǎn)換的最終地址,進(jìn)程在運(yùn)行時執(zhí)行指令和訪問數(shù)據(jù)最后都要通過物理地址從主存中存取,是內(nèi)存單元真正的地址。
- 邏輯地址:是指計算機(jī)用戶看到的地址。例如:當(dāng)創(chuàng)建一個長度為 100 的整型數(shù)組時,操作系統(tǒng)返回一個邏輯上的連續(xù)空間:指針指向數(shù)組第一個元素的內(nèi)存地址。由于整型元素的大小為 4 個字節(jié),故第二個元素的地址時起始地址加 4,以此類推。事實上,邏輯地址并不一定是元素存儲的真實地址,即數(shù)組元素的物理地址(在內(nèi)存條中所處的位置),并非是連續(xù)的,只是操作系統(tǒng)通過地址映射,將邏輯地址映射成連續(xù)的,這樣更符合人們的直觀思維。
- 虛擬內(nèi)存:是計算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。它使得應(yīng)用程序認(rèn)為它擁有連續(xù)的可用的內(nèi)存(一個連續(xù)完整的地址空間),而實際上,它通常是被分隔成多個物理內(nèi)存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進(jìn)行數(shù)據(jù)交換。
-
虛擬內(nèi)存有什么好處
- 第一,虛擬內(nèi)存可以使得進(jìn)程對運(yùn)行內(nèi)存超過物理內(nèi)存大小,因為程序運(yùn)行符合局部性原理,CPU 訪問內(nèi)存會有很明顯的重復(fù)訪問的傾向性,對于那些沒有被經(jīng)常使用到的內(nèi)存,我們可以把它換出到物理內(nèi)存之外,比如硬盤上的 swap 區(qū)域。
- 第二,由于每個進(jìn)程都有自己的頁表,所以每個進(jìn)程的虛擬內(nèi)存空間就是相互獨(dú)立的。進(jìn)程也沒有辦法訪問其他進(jìn)程的頁表,所以這些頁表是私有的,這就解決了多進(jìn)程之間地址沖突的問題。
- 第三,頁表里的頁表項中除了物理地址之外,還有一些標(biāo)記屬性的比特,比如控制一個頁的讀寫權(quán)限,標(biāo)記該頁是否存在等。在內(nèi)存訪問方面,操作系統(tǒng)提供了更好的安全性。
-
內(nèi)存管理
內(nèi)存管理的概念就是操作系統(tǒng)對內(nèi)存的劃分和動態(tài)分配。
內(nèi)存管理功能:
內(nèi)存空間的分配與回收:由操作系統(tǒng)完成主存儲器空間的分配和管理,是程序員擺脫存儲分配的麻煩,提高編程效率。
地址轉(zhuǎn)換:將邏輯地址轉(zhuǎn)換成相應(yīng)的物理地址。
內(nèi)存空間的擴(kuò)充:利用虛擬存儲技術(shù)或自動覆蓋技術(shù),從邏輯上擴(kuò)充主存。
存儲保護(hù):保證各道作業(yè)在各自的存儲空間內(nèi)運(yùn)行,互不干擾。
創(chuàng)建進(jìn)程首先要將程序和數(shù)據(jù)裝入內(nèi)存。將用戶源程序變?yōu)榭稍趦?nèi)存中執(zhí)行的程序,通常需要以下幾個步驟:編譯:由編譯程序?qū)⒂脩粼创a編譯成若干目標(biāo)模塊(把高級語言翻譯成機(jī)器語言)
鏈接:由鏈接程序?qū)⒕幾g后形成的一組目標(biāo)模塊及所需的庫函數(shù)連接在一起,形成一個完整的裝入模塊(由目標(biāo)模塊生成裝入模塊,鏈接后形成完整的邏輯地址)
裝入:由裝入程序?qū)⒀b入模塊裝入內(nèi)存運(yùn)行,裝入后形成物理地址
程序的鏈接有以下三種方式:靜態(tài)鏈接:在程序運(yùn)行之前,先將各目標(biāo)模塊及它們所需的庫函數(shù)連接成一個完整的可執(zhí)行文件(裝入模塊),之后不再拆開。
裝入時動態(tài)鏈接:將各目標(biāo)模塊裝入內(nèi)存時,邊裝入邊鏈接的鏈接方式。
運(yùn)行時動態(tài)鏈接:在程序執(zhí)行中需要該目標(biāo)模塊時,才對它進(jìn)行鏈接。其優(yōu)點(diǎn)是便于修改和更新,便于實現(xiàn)對目標(biāo)模塊的共享。
內(nèi)存的裝入模塊在裝入內(nèi)存時,有以下三種方式:重定位:根據(jù)內(nèi)存的當(dāng)前情況,將裝入模塊裝入內(nèi)存的適當(dāng)位置,裝入時對目標(biāo)程序中的指令和數(shù)據(jù)的修改過程稱為重定位。
靜態(tài)重定位:地址的變換通常是在裝入時一次完成的。一個作業(yè)裝入內(nèi)存時,必須給它分配要求的全部內(nèi)存空間,若沒有足夠的內(nèi)存,則不能裝入該作業(yè)。此外,作業(yè)一旦裝入內(nèi)存,整個運(yùn)行期間就不能在內(nèi)存中移動,也不能再申請內(nèi)存空間。
動態(tài)重定位:需要重定位寄存器的支持??梢詫⒊绦蚍峙涞讲贿B續(xù)的存儲區(qū)中;在程序運(yùn)行之前可以只裝入它的部分代碼即可投入運(yùn)行,然后在程序運(yùn)行期間,根據(jù)需要動態(tài)申請分配內(nèi)存。
內(nèi)存分配前,需要保護(hù)操作系統(tǒng)不受用戶進(jìn)程的影響,同時保護(hù)用戶進(jìn)程不受其他用戶進(jìn)程的影響。內(nèi)存保護(hù)可采取如下兩種方法:在CPU中設(shè)置一對上、下限寄存器,存放用戶作業(yè)在主存中的上限和下限地址,每當(dāng)CPU要訪問一個地址時,分別和兩個寄存器的值相比,判斷有無越界。
采用重定位寄存器(或基址寄存器)和界地址寄存器(又稱限長存儲器)來實現(xiàn)這種保護(hù)。重定位寄存器包含最小的物理地址值,界地址寄存器含邏輯地址的最大值。每個邏輯地址值必須小于界地址寄存器;內(nèi)存管理機(jī)構(gòu)動態(tài)得將邏輯地址與界地址寄存器進(jìn)行比較,若未發(fā)生地址越界,則加上重定位寄存器的值后映射成物理地址,再送交內(nèi)存單元。 -
常見的內(nèi)存分配方式
(1) 從靜態(tài)存儲區(qū)域分配。內(nèi)存在程序編譯的時候就已經(jīng)分配好,這塊內(nèi)存在程序的整個運(yùn)行期間都存在。例如全局變量,static變量。
(2) 在棧上創(chuàng)建。在執(zhí)行函數(shù)時,函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
(3) 從堆上分配,亦稱動態(tài)內(nèi)存分配。程序在運(yùn)行的時候用malloc或new申請任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時用free或delete釋放內(nèi)存。動態(tài)內(nèi)存的生存期由我們決定,使用非常靈活,但問題也最多。
常見內(nèi)存分配內(nèi)存錯誤
(1)內(nèi)存分配未成功,卻使用了它。
(2)內(nèi)存分配雖然成功,但是尚未初始化就引用它。
(3)內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。
(4)忘記了釋放內(nèi)存,造成內(nèi)存泄露。
(5)釋放了內(nèi)存卻繼續(xù)使用它。常見于以下有三種情況:
- 程序中的對象調(diào)用關(guān)系過于復(fù)雜,實在難以搞清楚某個對象究竟是否已經(jīng)釋放了內(nèi)存,此時應(yīng)該重新設(shè)計數(shù)據(jù)結(jié)構(gòu),從根本上解決對象管理的混亂局面。
- 函數(shù)的return語句寫錯了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因為該內(nèi)存在函數(shù)體結(jié)束時被自動銷毀。
- 使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。
-
malloc如何分配內(nèi)存
從操作系統(tǒng)層面上看,malloc是通過兩個系統(tǒng)調(diào)用來實現(xiàn)的: brk和mmap
- brk是將進(jìn)程數(shù)據(jù)段(.data)的最高地址指針向高處移動,這一步可以擴(kuò)大進(jìn)程在運(yùn)行時的堆大小
- mmap是在進(jìn)程的虛擬地址空間中尋找一塊空閑的虛擬內(nèi)存,這一步可以獲得一塊可以操作的堆內(nèi)存。
通常,分配的內(nèi)存小于128k時,使用brk調(diào)用來獲得虛擬內(nèi)存,大于128k時就使用mmap來獲得虛擬內(nèi)存。
進(jìn)程先通過這兩個系統(tǒng)調(diào)用獲取或者擴(kuò)大進(jìn)程的虛擬內(nèi)存,獲得相應(yīng)的虛擬地址,在訪問這些虛擬地址的時候,通過缺頁中斷,讓內(nèi)核分配相應(yīng)的物理內(nèi)存,這樣內(nèi)存分配才算完成。
-
如何避免預(yù)讀失效和緩存污染
預(yù)讀失效
這些被提前加載進(jìn)來的頁,并沒有被訪問,相當(dāng)于這個預(yù)讀工作是白做了,這個就是預(yù)讀失效。
傳統(tǒng)的 LRU 算法法無法避免下面這兩個問題:
- 預(yù)讀失效導(dǎo)致緩存命中率下降;
- 緩存污染導(dǎo)致緩存命中率下降;
為了避免「預(yù)讀失效」造成的影響,Linux 和 MySQL 對傳統(tǒng)的 LRU 鏈表做了改進(jìn):
- Linux 操作系統(tǒng)實現(xiàn)兩個了 LRU 鏈表:活躍 LRU 鏈表(active list)和非活躍 LRU 鏈表(inactive list)。
- MySQL Innodb 存儲引擎是在一個 LRU 鏈表上劃分來 2 個區(qū)域:young 區(qū)域 和 old 區(qū)域。
緩存污染
如果這些大量的數(shù)據(jù)在很長一段時間都不會被訪問的話,那么整個活躍 LRU 鏈表(或者 young 區(qū)域)就被污染了。
為了避免「緩存污染」造成的影響,Linux 操作系統(tǒng)和 MySQL Innodb 存儲引擎分別提高了升級為熱點(diǎn)數(shù)據(jù)的門檻:
-
Linux 操作系統(tǒng):在內(nèi)存頁被訪問第二次的時候,才將頁從 inactive list 升級到 active list 里。
-
MySQL Innodb:在內(nèi)存頁被訪問
第二次
的時候,并不會馬上將該頁從 old 區(qū)域升級到 young 區(qū)域,因為還要進(jìn)行
停留在 old 區(qū)域的時間判斷:
- 如果第二次的訪問時間與第一次訪問的時間在 1 秒內(nèi)(默認(rèn)值),那么該頁就不會被從 old 區(qū)域升級到 young 區(qū)域;
- 如果第二次的訪問時間與第一次訪問的時間超過 1 秒,那么該頁就會從 old 區(qū)域升級到 young 區(qū)域;
通過提高了進(jìn)入 active list (或者 young 區(qū)域)的門檻后,就很好了避免緩存污染帶來的影響。
-
物理內(nèi)存管理
操作系統(tǒng)物理內(nèi)存管理主要包括程序裝入、交換技術(shù)、連續(xù)分配管理方式和非連續(xù)分配管理方式(分頁、分段、段分頁)。
連續(xù)分配管理方式
連續(xù)內(nèi)存分配
內(nèi)存碎片
當(dāng)給程序分配空間時,可能會出現(xiàn)一些無法被利用的空閑碎片空間。
1.外部碎片:分配單元之間無法被利用的內(nèi)存碎片
2.內(nèi)部碎片:分配給任務(wù)內(nèi)存大小大于任務(wù)所需內(nèi)存大小時,多出來的內(nèi)存碎片。分區(qū)的動態(tài)分配
連續(xù)內(nèi)存分配情況:將應(yīng)用程序從硬盤加載到內(nèi)存中,給內(nèi)存分配一塊內(nèi)存。應(yīng)用程序運(yùn)行訪問數(shù)據(jù),數(shù)據(jù)的連續(xù)內(nèi)存空間。內(nèi)存分配算法:
具體問題具體分析適配算法。-
首次適配:
定義:使用第一塊內(nèi)存大小大于需求大小的可用空閑塊
實現(xiàn)方法:需要有一個按地址排序的空閑塊列表。尋找第一個滿足內(nèi)存需求的空閑塊。對于回收,要考慮空閑塊與相鄰空閑塊合并問題。
優(yōu)點(diǎn):簡單,易于產(chǎn)生更大的空閑塊,想著地址空間結(jié)尾分配
缺點(diǎn):產(chǎn)生外部碎片,具有不確定性 -
最優(yōu)適配:
定義:尋找整個空閑塊中,最滿足分配請求的空閑塊,內(nèi)存差值最小。
實現(xiàn)方法:需要有一個按尺寸排序的空閑塊列表,尋找最滿足分配的內(nèi)存塊。對于回收,要考慮空閑塊與相鄰空閑塊合并問題。
優(yōu)點(diǎn):避免分割大空閑塊,最小化外部碎片的產(chǎn)生,簡單
缺點(diǎn):外部碎片很細(xì),有很多微小碎片,不利于后續(xù)分配管理。 -
最差適配:
定義:找到差距最大的內(nèi)存空閑塊。
實現(xiàn):需要有一個按尺寸排序的空閑塊列表,尋找差距最大的內(nèi)存塊。對于回收,要考慮空閑塊與相鄰空閑塊合并問題。
優(yōu)點(diǎn):避免產(chǎn)生微小外部碎片,分配效率高,適用于分配中大快
缺點(diǎn):對于大塊請求帶來一定影響
減少內(nèi)存碎片方法
-
緊致:壓縮式碎片整理
調(diào)整運(yùn)行程序的位置。
1.重定位的時機(jī)。不能在程序運(yùn)行時進(jìn)行,可以在程序等待時拷貝。
2.內(nèi)存拷貝開銷很大。 -
swaping:交換式碎片整理
把硬盤作為一個備份。把等待的程序包括數(shù)據(jù)(主存)挪到硬盤上。當(dāng)硬盤上程序需要執(zhí)行時再拷貝到主存上。
1.交換那個程序,減小開銷
2.交換的時機(jī)
非連續(xù)內(nèi)存分配
連續(xù)內(nèi)存分配和非連續(xù)內(nèi)存分配
連續(xù)內(nèi)存分配缺點(diǎn):1.分配給一個程序的物理內(nèi)存是連續(xù)的,內(nèi)存利用率較低,由外碎片內(nèi)碎片的問題。
非連續(xù)內(nèi)存分配優(yōu)點(diǎn):1.程序物理地址是非連續(xù)的 2.更好的內(nèi)存利用和管理 3.允許共享代碼與數(shù)據(jù) 4.支持動態(tài)加載和動態(tài)鏈接
非連續(xù)內(nèi)存分配缺點(diǎn):建立虛擬地址到物理地址的映射,軟件開銷太大,可以用硬件支持
-》硬件兩種管理方案:分段和分頁
分段
分段地址空間
對于一段程序內(nèi)存可以分為:程序(主程序+子程序+共享庫)+變量(棧、堆、共享數(shù)據(jù)段)
分段:更好的分離與共享,將邏輯地址空間分散到多個物理地址空間
邏輯地址是連續(xù)的,將具有不同功能的映射到物理空間中,這些段大小不一,位置不一硬件實現(xiàn)分段尋址機(jī)制
一維邏輯地址有不同段組成,首先將邏輯地址分為兩段:段尋址(段號)+段偏移尋址(addr)
通過段號在段表找到邏輯內(nèi)存的段起始地址,看段起始地址是否滿足段大小限制,不滿足返回內(nèi)存異常,滿足將邏輯地址加偏移量是物理地址。段表:
1.存儲邏輯地址段段號到物理地址段號之間的映射關(guān)系
2.存儲段大小,起始地址
段表的建立:操作系統(tǒng)在尋址前建立。分頁
分頁地址空間
需要頁號和頁地址偏移。相比分段,分頁頁幀大小固定不變。
可以劃分物理內(nèi)存至固定大小的幀,將邏輯地址的頁也劃分至相同內(nèi)存大小。大小是2的冪。
建立方案
頁幀(Frame):物理內(nèi)存被分割為大小相等的幀
一個內(nèi)存物理地址是一個二元組(f,o)
物理地址 = 2^S*f+o
f是幀號(F位,2F個幀),o是幀內(nèi)偏移(S位,每幀2S字節(jié)),頁(Page):邏輯地址空間分割為大小相等的頁
頁內(nèi)偏移大小 = 幀內(nèi)偏移大小(頁幀大小和頁大小一致)
頁號大小和幀號大小可能不一致一個邏輯地址是一個二元組(p,o)
邏輯地址 = 2^S*p+o
p:頁號(P位,2P個頁),o:頁內(nèi)偏移(S位,每頁2S個字節(jié))頁尋址機(jī)制
CPU尋址(邏輯地址),邏輯地址包含兩部分(p,o),首先把p(頁號)作為索引,再加上頁表基址查頁表(pagetable)中對應(yīng)幀號(物理地址中f),知道幀號加上頁內(nèi)偏移就是物理地址(f,o)。頁表:以頁號為索引的對應(yīng)的幀號(物理地址),為此需要知道頁表的基址位置(頁號從哪個地址開始查)。
頁表的建立:操作系統(tǒng)初始化時,enable分頁機(jī)制前就需要建立好分頁與分段:
分頁:相比分段,分頁頁內(nèi)存固定,導(dǎo)致頁內(nèi)偏移大小范圍是固定的。不需要想分段一樣考慮分頁大小不一致的問題。
邏輯地址和物理地址空間
1.總邏輯頁大小和總物理幀大小不一致,一般邏輯地址空間大于物理地址空間。
2.邏輯地址空間連續(xù),物理地址空間不連續(xù)。減少內(nèi)外碎片。
頁表
頁表結(jié)構(gòu)
頁表是個數(shù)組,索引是頁號,對應(yīng)的數(shù)組項內(nèi)容里有幀號。分頁機(jī)制性能問題
1.時間開銷:訪問一個內(nèi)存單元需要兩次內(nèi)存訪問頁表不能放在CPU里,只能放在內(nèi)存里,CPU先做內(nèi)存尋址找頁表基址,再進(jìn)行頁表訪問,進(jìn)行兩次內(nèi)存訪問,訪問速度很慢
2空間代價:頁表占用空間
1.64位計算機(jī),每頁1KB,頁表大小?2^54個數(shù)的頁表,很大
2.多個程序有多個頁表解決方法
時間上:緩存 ——快表,TLB,Translation Look-aside Buffer
TLB:位于CPU中的一塊緩存區(qū)域,存放常用的頁號-幀號對,采用關(guān)聯(lián)內(nèi)存的方式實現(xiàn),具有快速訪問功能。
-CPU尋址時會先通過頁號在TLB查找,看是否存在頁號的Key,對應(yīng)得到幀號,進(jìn)而得到物理地址(減少對物理地址的訪問)。TLB未命中,把該項更新到TLB中(x86CPU這個過程是硬件完成,對于mps是操作系統(tǒng)完成)。
編寫程序時,把訪問的地址寫在一個頁號里。
空間上:間接訪問(多級頁表機(jī)制),以時間換空間
二級頁表:對于邏輯地址(p1,p2,o)
CPU尋址時先通過p1查找一級頁表,一級頁表中存放的是二級頁表的p2的起始地址,再在二級頁表對應(yīng)起始地址查找偏移p2,對應(yīng)存放的就是幀號。提高時間開銷,但一級頁表中不存在頁表項就不需要占用二級頁表項的內(nèi)存,節(jié)省空間。
多級頁表頁號分為K個部分,建立頁表“樹”
-
-
快表
快表,又稱聯(lián)想寄存器(TLB) ,是一種訪問速度比內(nèi)存快很多的高速緩沖存儲器,用來存放當(dāng)前訪問的若干頁表項,以加速地址變換的過程。與此對應(yīng),內(nèi)存中的頁表常稱為慢表。
地址變換過程 訪問一個邏輯地址的訪存次數(shù) 基本地址變換機(jī)構(gòu) ①算頁號、頁內(nèi)偏移量 ②檢查頁號合法性 ③查頁表,找到頁面存放的內(nèi)存塊號 ④根據(jù)內(nèi)存塊號與頁內(nèi)偏移量得到物理地址 ⑤訪問目標(biāo)內(nèi)存單元 兩次訪存 具有快表的地址變換機(jī)構(gòu) ①算頁號、頁內(nèi)偏移量 ②檢查頁號合法性 ③查快表。若命中,即可知道頁面存放的內(nèi)存塊號,可直接進(jìn)行⑤;若未命中則進(jìn)行④ ④查頁表,找到頁面存放的內(nèi)存塊號,并且將頁表項復(fù)制到快表中 ⑤根據(jù)內(nèi)存塊號與頁內(nèi)偏移量得到物理地址 ⑥訪問目標(biāo)內(nèi)存單元 快表命中,只需一次訪存 快表未命中,需要兩次訪存 -
內(nèi)存交換技術(shù)
交換(對換)技術(shù)的設(shè)計思想:內(nèi)存空間緊張時,系統(tǒng)將內(nèi)存中某些進(jìn)程暫時換出外存,把外存中某些已具備運(yùn)行條件的進(jìn)程換入內(nèi)存(進(jìn)程在內(nèi)存與磁盤間動態(tài)調(diào)度)
換入:把準(zhǔn)備好競爭CPU運(yùn)行的程序從輔存移到內(nèi)存。 換出:把處于等待狀態(tài)(或CPU調(diào)度原則下被剝奪運(yùn)行權(quán)力)的程序從內(nèi)存移到輔存,把內(nèi)存空間騰出來。
**交換時機(jī):**內(nèi)存交換通常在許多進(jìn)程運(yùn)行且內(nèi)存吃緊時進(jìn)行,而系統(tǒng)負(fù)荷降低就暫停。例如:在發(fā)現(xiàn)許多進(jìn)程運(yùn)行時經(jīng)常發(fā)生缺頁,就說明內(nèi)存緊張,此時可以換出一些進(jìn)程;如果缺頁率明顯下降,就可以暫停換出。
關(guān)鍵點(diǎn)
- 交換需要備份存儲,通常是快速磁盤,它必須足夠大,并且提供對這些內(nèi)存映像的直接訪問。
- 為了有效使用CPU,需要每個進(jìn)程的執(zhí)行時間比交換時間長,而影響交換時間的主要是轉(zhuǎn)移時間,轉(zhuǎn)移時間與所交換的空間內(nèi)存成正比。
- 如果換出進(jìn)程,比如確保該進(jìn)程的內(nèi)存空間成正比。
- 交換空間通常作為磁盤的一整塊,且獨(dú)立于文件系統(tǒng),因此使用就可能很快。
- 交換通常在有許多進(jìn)程運(yùn)行且內(nèi)存空間吃緊時開始啟動,而系統(tǒng)負(fù)荷降低就暫停。
- 普通交換使用不多,但交換的策略的某些變種在許多系統(tǒng)中(如UNIX系統(tǒng))仍然發(fā)揮作用。
-
分頁與分段的區(qū)別
- 段是信息的邏輯單位,它是根據(jù)用戶的需要劃分的,因此段對用戶是可見的 ;頁是信息的物理單位,是為了管理主存的方便而劃分的,對用戶是透明的;
- 段的大小不固定,有它所完成的功能決定;頁大大小固定,由系統(tǒng)決定;
- 段向用戶提供二維地址空間;頁向用戶提供的是一維地址空間;
- 段是信息的邏輯單位,便于存儲保護(hù)和信息的共享,頁的保護(hù)和共享受到限制。
5.3 進(jìn)程調(diào)度算法
-
進(jìn)程調(diào)度算法詳細(xì)介紹
選擇一個進(jìn)程運(yùn)行這一功能是在操作系統(tǒng)中完成的,通常稱為調(diào)度程序(scheduler)。
調(diào)度時機(jī)
? 在進(jìn)程的生命周期中,當(dāng)進(jìn)程從一個運(yùn)行狀態(tài)到另外一狀態(tài)變化的時候,其實會觸發(fā)一次調(diào)度。
比如,以下狀態(tài)的變化都會觸發(fā)操作系統(tǒng)的調(diào)度:
- 從就緒態(tài) -> 運(yùn)行態(tài):當(dāng)進(jìn)程被創(chuàng)建時,會進(jìn)入到就緒隊列,操作系統(tǒng)會從就緒隊列選擇一個進(jìn)程運(yùn)行;
- 從運(yùn)行態(tài) -> 阻塞態(tài):當(dāng)進(jìn)程發(fā)生 I/O 事件而阻塞時,操作系統(tǒng)必須選擇另外一個進(jìn)程運(yùn)行;
- 從運(yùn)行態(tài) -> 結(jié)束態(tài):當(dāng)進(jìn)程退出結(jié)束后,操作系統(tǒng)得從就緒隊列選擇另外一個進(jìn)程運(yùn)行;
因為,這些狀態(tài)變化的時候,操作系統(tǒng)需要考慮是否要讓新的進(jìn)程給 CPU 運(yùn)行,或者是否讓當(dāng)前進(jìn)程從 CPU 上退出來而換另一個進(jìn)程運(yùn)行。
另外,如果硬件時鐘提供某個頻率的周期性中斷,那么可以根據(jù)如何處理時鐘中斷 ,把調(diào)度算法分為兩類:
- 非搶占式調(diào)度算法挑選一個進(jìn)程,然后讓該進(jìn)程運(yùn)行直到被阻塞,或者直到該進(jìn)程退出,才會調(diào)用另外一個進(jìn)程,也就是說不會理時鐘中斷這個事情。
- 搶占式調(diào)度算法挑選一個進(jìn)程,然后讓該進(jìn)程只運(yùn)行某段時間,如果在該時段結(jié)束時,該進(jìn)程仍然在運(yùn)行時,則會把它掛起,接著調(diào)度程序從就緒隊列挑選另外一個進(jìn)程。這種搶占式調(diào)度處理,需要在時間間隔的末端發(fā)生時鐘中斷,以便把 CPU 控制返回給調(diào)度程序進(jìn)行調(diào)度,也就是常說的時間片機(jī)制。
調(diào)度原則
- CPU 利用率:調(diào)度程序應(yīng)確保 CPU 是始終匆忙的狀態(tài),這可提高 CPU 的利用率;
- 系統(tǒng)吞吐量:吞吐量表示的是單位時間內(nèi) CPU 完成進(jìn)程的數(shù)量,長作業(yè)的進(jìn)程會占用較長的 CPU 資源,因此會降低吞吐量,相反,短作業(yè)的進(jìn)程會提升系統(tǒng)吞吐量;
- 周轉(zhuǎn)時間:周轉(zhuǎn)時間是進(jìn)程運(yùn)行+阻塞時間+等待時間的總和,一個進(jìn)程的周轉(zhuǎn)時間越小越好;
- 等待時間:這個等待時間不是阻塞狀態(tài)的時間,而是進(jìn)程處于就緒隊列的時間,等待的時間越長,用戶越不滿意;
- 響應(yīng)時間:用戶提交請求到系統(tǒng)第一次產(chǎn)生響應(yīng)所花費(fèi)的時間,在交互式系統(tǒng)中,響應(yīng)時間是衡量調(diào)度算法好壞的主要標(biāo)準(zhǔn)。
調(diào)度算法
? 調(diào)度算法是指:根據(jù)系統(tǒng)的資源分配策略所規(guī)定的資源分配算法。常用的調(diào)度算法有:先來先服務(wù)調(diào)度算法、時間片輪轉(zhuǎn)調(diào)度法、短作業(yè)優(yōu)先調(diào)度算法、最短剩余時間優(yōu)先、高響應(yīng)比優(yōu)先調(diào)度算法、優(yōu)先級調(diào)度算法等等。
- 先來先服務(wù)調(diào)度算法
先來先服務(wù)調(diào)度算法是一種最簡單的調(diào)度算法,也稱為先進(jìn)先出或嚴(yán)格排隊方案。當(dāng)每個進(jìn)程就緒后,它加入就緒隊列。當(dāng)前正運(yùn)行的進(jìn)程停止執(zhí)行,選擇在就緒隊列中存在時間最長的進(jìn)程運(yùn)行。該算法既可以用于作業(yè)調(diào)度,也可以用于進(jìn)程調(diào)度。先來先服務(wù)比較適合于常作業(yè)(進(jìn)程),而不利于段作業(yè)(進(jìn)程)。
- 時間片輪轉(zhuǎn)調(diào)度算法
時間片輪轉(zhuǎn)調(diào)度算法主要適用于分時系統(tǒng)。在這種算法中,系統(tǒng)將所有就緒進(jìn)程按到達(dá)時間的先后次序排成一個隊列,進(jìn)程調(diào)度程序總是選擇就緒隊列中第一個進(jìn)程執(zhí)行,即先來先服務(wù)的原則,但僅能運(yùn)行一個時間片。
- 短作業(yè)優(yōu)先調(diào)度算法
短作業(yè)優(yōu)先調(diào)度算法是指對短作業(yè)優(yōu)先調(diào)度的算法,從后備隊列中選擇一個或若干個估計運(yùn)行時間最短的作業(yè),將它們調(diào)入內(nèi)存運(yùn)行。 短作業(yè)優(yōu)先調(diào)度算法是一個非搶占策略,他的原則是下一次選擇預(yù)計處理時間最短的進(jìn)程,因此短進(jìn)程將會越過長作業(yè),跳至隊列頭。
- 最短剩余時間優(yōu)先調(diào)度算法
最短剩余時間是針對最短進(jìn)程優(yōu)先增加了搶占機(jī)制的版本。在這種情況下,進(jìn)程調(diào)度總是選擇預(yù)期剩余時間最短的進(jìn)程。當(dāng)一個進(jìn)程加入到就緒隊列時,他可能比當(dāng)前運(yùn)行的進(jìn)程具有更短的剩余時間,因此只要新進(jìn)程就緒,調(diào)度程序就能可能搶占當(dāng)前正在運(yùn)行的進(jìn)程。像最短進(jìn)程優(yōu)先一樣,調(diào)度程序正在執(zhí)行選擇函數(shù)是必須有關(guān)于處理時間的估計,并且存在長進(jìn)程饑餓的危險。
- 高響應(yīng)比優(yōu)先調(diào)度算法
高響應(yīng)比優(yōu)先調(diào)度算法主要用于作業(yè)調(diào)度,該算法是對 先來先服務(wù)調(diào)度算法和短作業(yè)優(yōu)先調(diào)度算法的一種綜合平衡,同時考慮每個作業(yè)的等待時間和估計的運(yùn)行時間。在每次進(jìn)行作業(yè)調(diào)度時,先計算后備作業(yè)隊列中每個作業(yè)的響應(yīng)比,從中選出響應(yīng)比最高的作業(yè)投入運(yùn)行。
- 優(yōu)先級調(diào)度算法
優(yōu)先級調(diào)度算法每次從后備作業(yè)隊列中選擇優(yōu)先級最髙的一個或幾個作業(yè),將它們調(diào)入內(nèi)存,分配必要的資源,創(chuàng)建進(jìn)程并放入就緒隊列。在進(jìn)程調(diào)度中,優(yōu)先級調(diào)度算法每次從就緒隊列中選擇優(yōu)先級最高的進(jìn)程,將處理機(jī)分配給它,使之投入運(yùn)行。
5.4 磁盤調(diào)度算法
-
磁盤調(diào)度算法詳細(xì)介紹
常見的磁盤調(diào)度算法有:
- 先來先服務(wù)算法
- 最短尋道時間優(yōu)先算法
- 掃描算法
- 循環(huán)掃描算法
- LOOK 與 C-LOOK 算法
先來先服務(wù)
? 先來先服務(wù)(First-Come,First-Served,FCFS),顧名思義,先到來的請求,先被服務(wù)。
最短尋道時間優(yōu)先
? 最短尋道時間優(yōu)先(Shortest Seek First,SSF)算法的工作方式是,優(yōu)先選擇從當(dāng)前磁頭位置所需尋道時間最短的請求
掃描算法
? 最短尋道時間優(yōu)先算法會產(chǎn)生饑餓的原因在于:磁頭有可能再一個小區(qū)域內(nèi)來回得移動。
? 為了防止這個問題,可以規(guī)定:磁頭在一個方向上移動,訪問所有未完成的請求,直到磁頭到達(dá)該方向上的最后的磁道,才調(diào)換方向,這就是掃描(*Scan*)算法。
? 這種算法也叫做電梯算法,比如電梯保持按一個方向移動,直到在那個方向上沒有請求為止,然后改變方向。
循環(huán)掃描算法
? 掃描算法使得每個磁道響應(yīng)的頻率存在差異,那么要優(yōu)化這個問題的話,可以總是按相同的方向進(jìn)行掃描,使得每個磁道的響應(yīng)頻率基本一致。
? 循環(huán)掃描(Circular Scan, CSCAN )規(guī)定:只有磁頭朝某個特定方向移動時,才處理磁道訪問請求,而返回時直接快速移動至最靠邊緣的磁道,也就是復(fù)位磁頭,這個過程是很快的,并且返回中途不處理任何請求,該算法的特點(diǎn),就是磁道只響應(yīng)一個方向上的請求。
LOOK 與 C-LOOK算法
? 掃描算法和循環(huán)掃描算法,都是磁頭移動到磁盤「最始端或最末端」才開始調(diào)換方向。
那這其實是可以優(yōu)化的,優(yōu)化的思路就是磁頭在移動到「最遠(yuǎn)的請求」位置,然后立即反向移動。
針對 SCAN 算法的優(yōu)化叫 LOOK 算法,它的工作方式,磁頭在每個方向上僅僅移動到最遠(yuǎn)的請求位置,然后立即反向移動,而不需要移動到磁盤的最始端或最末端,反向移動的途中會響應(yīng)請求。
針對C-SCAN 算法的優(yōu)化叫 C-LOOK,它的工作方式,磁頭在每個方向上僅僅移動到最遠(yuǎn)的請求位置,然后立即反向移動,而不需要移動到磁盤的最始端或最末端,反向移動的途中不會響應(yīng)請求。
5.5 頁面置換算法
-
頁面置換算法詳細(xì)介紹
請求調(diào)頁,也稱按需調(diào)頁,即對不在內(nèi)存中的“頁”,當(dāng)進(jìn)程執(zhí)行時要用時才調(diào)入,否則有可能到程序結(jié)束時也不會調(diào)入。而內(nèi)存中給頁面留的位置是有限的,在內(nèi)存中以幀為單位放置頁面。為了防止請求調(diào)頁的過程出現(xiàn)過多的內(nèi)存頁面錯誤(即需要的頁面當(dāng)前不在內(nèi)存中,需要從硬盤中讀數(shù)據(jù),也即需要做頁面的替換)而使得程序執(zhí)行效率下降,我們需要設(shè)計一些頁面置換算法,頁面按照這些算法進(jìn)行相互替換時,可以盡量達(dá)到較低的錯誤率。常用的頁面置換算法如下:
- 先進(jìn)先出置換算法(FIFO)
先進(jìn)先出,即淘汰最早調(diào)入的頁面。
- 最佳置換算法(OPT)
選未來最遠(yuǎn)將使用的頁淘汰,是一種最優(yōu)的方案,可以證明缺頁數(shù)最小。
- 最近最久未使用(LRU)算法
即選擇最近最久未使用的頁面予以淘汰
- 時鐘(Clock)置換算法
時鐘置換算法也叫最近未用算法 NRU(Not RecentlyUsed)。該算法為每個頁面設(shè)置一位訪問位,將內(nèi)存中的所有頁面都通過鏈接指針鏈成一個循環(huán)隊列。
5.6 網(wǎng)絡(luò)系統(tǒng)
-
什么是零拷貝
為了提高文件傳輸?shù)男阅?#xff0c;于是就出現(xiàn)了零拷貝技術(shù),它通過一次系統(tǒng)調(diào)用(
sendfile
方法)合并了磁盤讀取與網(wǎng)絡(luò)發(fā)送兩個操作,降低了上下文切換次數(shù)。另外,拷貝數(shù)據(jù)都是發(fā)生在內(nèi)核中的,天然就降低了數(shù)據(jù)拷貝的次數(shù)。Kafka 和 Nginx 都有實現(xiàn)零拷貝技術(shù),這將大大提高文件傳輸?shù)男阅堋?/p>
零拷貝技術(shù)是基于 PageCache 的,PageCache 會緩存最近訪問的數(shù)據(jù),提升了訪問緩存數(shù)據(jù)的性能,同時,為了解決機(jī)械硬盤尋址慢的問題,它還協(xié)助 I/O 調(diào)度算法實現(xiàn)了 IO 合并與預(yù)讀,這也是順序讀比隨機(jī)讀性能好的原因。這些優(yōu)勢,進(jìn)一步提升了零拷貝的性能。
-
I/O多路復(fù)用
既然為每個請求分配一個進(jìn)程/線程的方式不合適,那有沒有可能只使用一個進(jìn)程來維護(hù)多個 Socket 呢?答案是有的,那就是 I/O 多路復(fù)用技術(shù)。
一個進(jìn)程雖然任一時刻只能處理一個請求,但是處理每個請求的事件時,耗時控制在 1 毫秒以內(nèi),這樣 1 秒內(nèi)就可以處理上千個請求,把時間拉長來看,多個請求復(fù)用了一個進(jìn)程,這就是多路復(fù)用,這種思想很類似一個 CPU 并發(fā)多個進(jìn)程,所以也叫做時分多路復(fù)用。
我們熟悉的 select/poll/epoll 內(nèi)核提供給用戶態(tài)的多路復(fù)用系統(tǒng)調(diào)用,進(jìn)程可以通過一個系統(tǒng)調(diào)用函數(shù)從內(nèi)核中獲取多個事件。
select/poll/epoll 是如何獲取網(wǎng)絡(luò)事件的呢?在獲取事件時,先把所有連接(文件描述符)傳給內(nèi)核,再由內(nèi)核返回產(chǎn)生了事件的連接,然后在用戶態(tài)中再處理這些連接對應(yīng)的請求即可。
-
select/poll/epoll
select 和 poll 并沒有本質(zhì)區(qū)別,它們內(nèi)部都是使用「線性結(jié)構(gòu)」來存儲進(jìn)程關(guān)注的 Socket 集合。
在使用的時候,首先需要把關(guān)注的 Socket 集合通過 select/poll 系統(tǒng)調(diào)用從用戶態(tài)拷貝到內(nèi)核態(tài),然后由內(nèi)核檢測事件,當(dāng)有網(wǎng)絡(luò)事件產(chǎn)生時,內(nèi)核需要遍歷進(jìn)程關(guān)注 Socket 集合,找到對應(yīng)的 Socket,并設(shè)置其狀態(tài)為可讀/可寫,然后把整個 Socket 集合從內(nèi)核態(tài)拷貝到用戶態(tài),用戶態(tài)還要繼續(xù)遍歷整個 Socket 集合找到可讀/可寫的 Socket,然后對其處理。
很明顯發(fā)現(xiàn),select 和 poll 的缺陷在于,當(dāng)客戶端越多,也就是 Socket 集合越大,Socket 集合的遍歷和拷貝會帶來很大的開銷,因此也很難應(yīng)對 C10K。
epoll 是解決 C10K 問題的利器,通過兩個方面解決了 select/poll 的問題。
- epoll 在內(nèi)核里使用「紅黑樹」來關(guān)注進(jìn)程所有待檢測的 Socket,紅黑樹是個高效的數(shù)據(jù)結(jié)構(gòu),增刪改一般時間復(fù)雜度是 O(logn),通過對這棵黑紅樹的管理,不需要像 select/poll 在每次操作時都傳入整個 Socket 集合,減少了內(nèi)核和用戶空間大量的數(shù)據(jù)拷貝和內(nèi)存分配。
- epoll 使用事件驅(qū)動的機(jī)制,內(nèi)核里維護(hù)了一個「鏈表」來記錄就緒事件,只將有事件發(fā)生的 Socket 集合傳遞給應(yīng)用程序,不需要像 select/poll 那樣輪詢掃描整個集合(包含有和無事件的 Socket ),大大提高了檢測的效率。
而且,epoll 支持邊緣觸發(fā)和水平觸發(fā)的方式,而 select/poll 只支持水平觸發(fā),一般而言,邊緣觸發(fā)的方式會比水平觸發(fā)的效率高。
-
高性能網(wǎng)絡(luò)模式:Reactor和Proactor
常見的 Reactor 實現(xiàn)方案有三種。
第一種方案單 Reactor 單進(jìn)程 / 線程,不用考慮進(jìn)程間通信以及數(shù)據(jù)同步的問題,因此實現(xiàn)起來比較簡單,這種方案的缺陷在于無法充分利用多核 CPU,而且處理業(yè)務(wù)邏輯的時間不能太長,否則會延遲響應(yīng),所以不適用于計算機(jī)密集型的場景,適用于業(yè)務(wù)處理快速的場景,比如 Redis(6.0之前 ) 采用的是單 Reactor 單進(jìn)程的方案。
第二種方案單 Reactor 多線程,通過多線程的方式解決了方案一的缺陷,但它離高并發(fā)還差一點(diǎn)距離,差在只有一個 Reactor 對象來承擔(dān)所有事件的監(jiān)聽和響應(yīng),而且只在主線程中運(yùn)行,在面對瞬間高并發(fā)的場景時,容易成為性能的瓶頸的地方。
第三種方案多 Reactor 多進(jìn)程 / 線程,通過多個 Reactor 來解決了方案二的缺陷,主 Reactor 只負(fù)責(zé)監(jiān)聽事件,響應(yīng)事件的工作交給了從 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多線程」的方案,Nginx 則采用了類似于 「多 Reactor 多進(jìn)程」的方案。
Reactor 可以理解為「來了事件操作系統(tǒng)通知應(yīng)用進(jìn)程,讓應(yīng)用進(jìn)程來處理」,而 Proactor 可以理解為「來了事件操作系統(tǒng)來處理,處理完再通知應(yīng)用進(jìn)程」。
因此,真正的大殺器還是 Proactor,它是采用異步 I/O 實現(xiàn)的異步網(wǎng)絡(luò)模型,感知的是已完成的讀寫事件,而不需要像 Reactor 感知到事件后,還需要調(diào)用 read 來從內(nèi)核中獲取數(shù)據(jù)。
不過,無論是 Reactor,還是 Proactor,都是一種基于「事件分發(fā)」的網(wǎng)絡(luò)編程模式,區(qū)別在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式則是基于「已完成」的 I/O 事件。
-
一致性哈希介紹
一致性哈希是指將「存儲節(jié)點(diǎn)」和「數(shù)據(jù)」都映射到一個首尾相連的哈希環(huán)上,如果增加或者移除一個節(jié)點(diǎn),僅影響該節(jié)點(diǎn)在哈希環(huán)上順時針相鄰的后繼節(jié)點(diǎn),其它數(shù)據(jù)也不會受到影響。
但是一致性哈希算法不能夠均勻的分布節(jié)點(diǎn),會出現(xiàn)大量請求都集中在一個節(jié)點(diǎn)的情況,在這種情況下進(jìn)行容災(zāi)與擴(kuò)容時,容易出現(xiàn)雪崩的連鎖反應(yīng)。
為了解決一致性哈希算法不能夠均勻的分布節(jié)點(diǎn)的問題,就需要引入虛擬節(jié)點(diǎn),對一個真實節(jié)點(diǎn)做多個副本。不再將真實節(jié)點(diǎn)映射到哈希環(huán)上,而是將虛擬節(jié)點(diǎn)映射到哈希環(huán)上,并將虛擬節(jié)點(diǎn)映射到實際節(jié)點(diǎn),所以這里有「兩層」映射關(guān)系。
引入虛擬節(jié)點(diǎn)后,可以會提高節(jié)點(diǎn)的均衡度,還會提高系統(tǒng)的穩(wěn)定性。所以,帶虛擬節(jié)點(diǎn)的一致性哈希方法不僅適合硬件配置不同的節(jié)點(diǎn)的場景,而且適合節(jié)點(diǎn)規(guī)模會發(fā)生變化的場景。
5.7 鎖
-
什么是死鎖和產(chǎn)生死鎖原因
死鎖,是指多個進(jìn)程在運(yùn)行過程中因爭奪資源而造成的一種僵局,當(dāng)進(jìn)程處于這種僵持狀態(tài)時,若無外力作用,它們都將無法再向前推進(jìn)。 如下圖所示:如果此時有一個線程 A,已經(jīng)持有了鎖 A,但是試圖獲取鎖 B,線程 B 持有鎖 B,而試圖獲取鎖 A,這種情況下就會產(chǎn)生死鎖。
產(chǎn)生死鎖原因
? 由于系統(tǒng)中存在一些不可剝奪資源,而當(dāng)兩個或兩個以上進(jìn)程占有自身資源,并請求對方資源時,會導(dǎo)致每個進(jìn)程都無法向前推進(jìn),這就是死鎖。
- 競爭資源
例如:系統(tǒng)中只有一臺打印機(jī),可供進(jìn)程 A 使用,假定 A 已占用了打印機(jī),若 B 繼續(xù)要求打印機(jī)打印將被阻塞。
系統(tǒng)中的資源可以分為兩類:
- 可剝奪資源:是指某進(jìn)程在獲得這類資源后,該資源可以再被其他進(jìn)程或系統(tǒng)剝奪,CPU 和主存均屬于可剝奪性資源;
- 不可剝奪資源,當(dāng)系統(tǒng)把這類資源分配給某進(jìn)程后,再不能強(qiáng)行收回,只能在進(jìn)程用完后自行釋放,如磁帶機(jī)、打印機(jī)等。
- 進(jìn)程推進(jìn)順序不當(dāng)
例如:進(jìn)程 A 和 進(jìn)程 B 互相等待對方的數(shù)據(jù)。
死鎖產(chǎn)生的必要條件?
- 互斥條件:進(jìn)程要求對所分配的資源進(jìn)行排它性控制,即在一段時間內(nèi)某資源僅為一進(jìn)程所占用。
- 請求和保持條件:當(dāng)進(jìn)程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進(jìn)程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
- 環(huán)路等待條件:在發(fā)生死鎖時,必然存在一個進(jìn)程–資源的環(huán)形鏈。
-
如何避免死鎖
預(yù)防死鎖、避免死鎖、檢測死鎖、解除死鎖
預(yù)防死鎖
- 破壞請求條件:一次性分配所有資源,這樣就不會再有請求了;
- 破壞請保持條件:只要有一個資源得不到分配,也不給這個進(jìn)程分配其他的資源:
- 破壞不可剝奪條件:當(dāng)某進(jìn)程獲得了部分資源,但得不到其它資源,則釋放已占有的資源;
- 破壞環(huán)路等待條件:系統(tǒng)給每類資源賦予一個編號,每一個進(jìn)程按編號遞增的順序請求資源,釋放則相反。
解除死鎖
- 資源剝奪:掛起某些死鎖進(jìn)程,并搶占它的資源,將這些資源分配給其他死鎖進(jìn)程(但應(yīng)該防止被掛起的進(jìn)程長時間得不到資源);
- 撤銷進(jìn)程:強(qiáng)制撤銷部分、甚至全部死鎖進(jìn)程并剝奪這些進(jìn)程的資源(撤銷的原則可以按進(jìn)程優(yōu)先級和撤銷進(jìn)程代價的高低進(jìn)行);
- 進(jìn)程回退:讓一個或多個進(jìn)程回退到足以避免死鎖的地步。進(jìn)程回退時自愿釋放資源而不是被剝奪。要求系統(tǒng)保持進(jìn)程的歷史信息,設(shè)置還原點(diǎn)。
-
什么是悲觀鎖、樂觀鎖
悲觀鎖做事比較悲觀,它認(rèn)為多線程同時修改共享資源的概率比較高,于是很容易出現(xiàn)沖突,所以訪問共享資源前,先要上鎖。
樂觀鎖做事比較樂觀,它假定沖突的概率很低,它的工作方式是:先修改完共享資源,再驗證這段時間內(nèi)有沒有發(fā)生沖突,如果沒有其他線程在修改資源,那么操作完成,如果發(fā)現(xiàn)有其他線程已經(jīng)修改過這個資源,就放棄本次操作。
可見,樂觀鎖的心態(tài)是,不管三七二十一,先改了資源再說。另外,你會發(fā)現(xiàn)樂觀鎖全程并沒有加鎖,所以它也叫無鎖編程。
互斥鎖、自旋鎖、讀寫鎖,都是屬于悲觀鎖。
-
哲學(xué)家進(jìn)餐問題
生產(chǎn)者-消費(fèi)者問題描述:
- 生產(chǎn)者在生成數(shù)據(jù)后,放在一個緩沖區(qū)中;
- 消費(fèi)者從緩沖區(qū)取出數(shù)據(jù)處理;
- 任何時刻,只能有一個生產(chǎn)者或消費(fèi)者可以訪問緩沖區(qū);
我們對問題分析可以得出:
- 任何時刻只能有一個線程操作緩沖區(qū),說明操作緩沖區(qū)是臨界代碼,需要互斥;
- 緩沖區(qū)空時,消費(fèi)者必須等待生產(chǎn)者生成數(shù)據(jù);緩沖區(qū)滿時,生產(chǎn)者必須等待消費(fèi)者取出數(shù)據(jù)。說明生產(chǎn)者和消費(fèi)者需要同步。
那么我們需要三個信號量,分別是:
- 互斥信號量
mutex
:用于互斥訪問緩沖區(qū),初始化值為 1; - 資源信號量
fullBuffers
:用于消費(fèi)者詢問緩沖區(qū)是否有數(shù)據(jù),有數(shù)據(jù)則讀取數(shù)據(jù),初始化值為 0(表明緩沖區(qū)一開始為空); - 資源信號量
emptyBuffers
:用于生產(chǎn)者詢問緩沖區(qū)是否有空位,有空位則生成數(shù)據(jù),初始化值為 n (緩沖區(qū)大小);
如果消費(fèi)者線程一開始執(zhí)行
P(fullBuffers)
,由于信號量fullBuffers
初始值為 0,則此時fullBuffers
的值從 0 變?yōu)?-1,說明緩沖區(qū)里沒有數(shù)據(jù),消費(fèi)者只能等待。接著,輪到生產(chǎn)者執(zhí)行
P(emptyBuffers)
,表示減少 1 個空槽,如果當(dāng)前沒有其他生產(chǎn)者線程在臨界區(qū)執(zhí)行代碼,那么該生產(chǎn)者線程就可以把數(shù)據(jù)放到緩沖區(qū),放完后,執(zhí)行V(fullBuffers)
,信號量fullBuffers
從 -1 變成 0,表明有「消費(fèi)者」線程正在阻塞等待數(shù)據(jù),于是阻塞等待的消費(fèi)者線程會被喚醒。消費(fèi)者線程被喚醒后,如果此時沒有其他消費(fèi)者線程在讀數(shù)據(jù),那么就可以直接進(jìn)入臨界區(qū),從緩沖區(qū)讀取數(shù)據(jù)。最后,離開臨界區(qū)后,把空槽的個數(shù) + 1。
-
生產(chǎn)者、消費(fèi)者問題
哲學(xué)家就餐的問題描述:
5
個老大哥哲學(xué)家,閑著沒事做,圍繞著一張圓桌吃面;- 巧就巧在,這個桌子只有
5
支叉子,每兩個哲學(xué)家之間放一支叉子; - 哲學(xué)家圍在一起先思考,思考中途餓了就會想進(jìn)餐;
- 這些哲學(xué)家要兩支叉子才愿意吃面,也就是需要拿到左右兩邊的叉子才進(jìn)餐;
- 吃完后,會把兩支叉子放回原處,繼續(xù)思考;
一、讓偶數(shù)編號的哲學(xué)家「先拿左邊的叉子后拿右邊的叉子」,奇數(shù)編號的哲學(xué)家「先拿右邊的叉子后拿左邊的叉子」。
在 P 操作時,根據(jù)哲學(xué)家的編號不同,拿起左右兩邊叉子的順序不同。另外,V 操作是不需要分支的,因為 V 操作是不會阻塞的。
二、用一個數(shù)組 state 來記錄每一位哲學(xué)家的三個狀態(tài),分別是在進(jìn)餐狀態(tài)、思考狀態(tài)、饑餓狀態(tài)(正在試圖拿叉子)。
那么,一個哲學(xué)家只有在兩個鄰居都沒有進(jìn)餐時,才可以進(jìn)入進(jìn)餐狀態(tài)。
上面的程序使用了一個信號量數(shù)組,每個信號量對應(yīng)一位哲學(xué)家,這樣在所需的叉子被占用時,想進(jìn)餐的哲學(xué)家就被阻塞。
-
讀者寫者問題
讀者只會讀取數(shù)據(jù),不會修改數(shù)據(jù),而寫者即可以讀也可以修改數(shù)據(jù)。
讀者-寫者的問題描述:
- 「讀-讀」允許:同一時刻,允許多個讀者同時讀
- 「讀-寫」互斥:沒有寫者時讀者才能讀,沒有讀者時寫者才能寫
- 「寫-寫」互斥:沒有其他寫者時,寫者才能寫
使用信號量的方式來嘗試解決:
- 信號量
wMutex
:控制寫操作的互斥信號量,初始值為 1 ; - 讀者計數(shù)
rCount
:正在進(jìn)行讀操作的讀者個數(shù),初始化為 0; - 信號量
rCountMutex
:控制對 rCount 讀者計數(shù)器的互斥修改,初始值為 1;
這種實現(xiàn),是讀者優(yōu)先的策略,因為只要有讀者正在讀的狀態(tài),后來的讀者都可以直接進(jìn)入,如果讀者持續(xù)不斷進(jìn)入,則寫者會處于饑餓狀態(tài)。
那既然有讀者優(yōu)先策略,自然也有寫者優(yōu)先策略:
- 只要有寫者準(zhǔn)備要寫入,寫者應(yīng)盡快執(zhí)行寫操作,后來的讀者就必須阻塞;
- 如果有寫者持續(xù)不斷寫入,則讀者就處于饑餓;
在方案一的基礎(chǔ)上新增如下變量:
- 信號量
rMutex
:控制讀者進(jìn)入的互斥信號量,初始值為 1; - 信號量
wDataMutex
:控制寫者寫操作的互斥信號量,初始值為 1; - 寫者計數(shù)
wCount
:記錄寫者數(shù)量,初始值為 0; - 信號量
wCountMutex
:控制 wCount 互斥修改,初始值為 1;
注意,這里
rMutex
的作用,開始有多個讀者讀數(shù)據(jù),它們?nèi)窟M(jìn)入讀者隊列,此時來了一個寫者,執(zhí)行了P(rMutex)
之后,后續(xù)的讀者由于阻塞在rMutex
上,都不能再進(jìn)入讀者隊列,而寫者到來,則可以全部進(jìn)入寫者隊列,因此保證了寫者優(yōu)先。同時,第一個寫者執(zhí)行了
P(rMutex)
之后,也不能馬上開始寫,必須等到所有進(jìn)入讀者隊列的讀者都執(zhí)行完讀操作,通過V(wDataMutex)
喚醒寫者的寫操作。既然讀者優(yōu)先策略和寫者優(yōu)先策略都會造成饑餓的現(xiàn)象,那么我們就來實現(xiàn)一下公平策略。
公平策略:
- 優(yōu)先級相同;
- 寫者、讀者互斥訪問;
- 只能一個寫者訪問臨界區(qū);
- 可以有多個讀者同時訪問臨界資源;
5.8 其他面試題
-
對并發(fā)和并行的理解
- 并行是指兩個或者多個事件在同一時刻發(fā)生;而并發(fā)是指兩個或多個事件在同一時間間隔發(fā)生;
- 并行是在不同實體上的多個事件,并發(fā)是在同一實體上的多個事件;
-
什么是用戶態(tài)和內(nèi)核態(tài)
用戶態(tài)和內(nèi)核態(tài)是操作系統(tǒng)的兩種運(yùn)行狀態(tài)。
內(nèi)核態(tài)
:處于內(nèi)核態(tài)的 CPU 可以訪問任意的數(shù)據(jù),包括外圍設(shè)備,比如網(wǎng)卡、硬盤等,處于內(nèi)核態(tài)的 CPU 可以從一個程序切換到另外一個程序,并且占用 CPU 不會發(fā)生搶占情況,一般處于特權(quán)級 0 的狀態(tài)我們稱之為內(nèi)核態(tài)。用戶態(tài)
:處于用戶態(tài)的 CPU 只能受限的訪問內(nèi)存,并且不允許訪問外圍設(shè)備,用戶態(tài)下的 CPU 不允許獨(dú)占,也就是說 CPU 能夠被其他程序獲取。
-
兩大局部性原理是什么
主要分為時間局部性和空間局部性。
**時間局部性:**如果執(zhí)行了程序中的某條指令,那么不久后這條指令很有可能再次執(zhí)行;如果某個數(shù)據(jù)被訪問過,不久之后該數(shù)據(jù)很可能再次被訪問。(因為程序中存在大量的循環(huán))
**空間局部性:**一旦程序訪問了某個存儲單元,在不久之后,其附近的存儲單元也很有可能被訪問。(因為很多數(shù)據(jù)在內(nèi)存中都是連續(xù)存放的,并且程序的指令也是順序地在內(nèi)存中存放的)
-
異常和中斷是什么,有什么區(qū)別
中斷
當(dāng)我們在敲擊鍵盤的同時就會產(chǎn)生中斷,當(dāng)硬盤讀寫完數(shù)據(jù)之后也會產(chǎn)生中斷,所以,我們需要知道,中斷是由硬件設(shè)備產(chǎn)生的,而它們從物理上說就是電信號,之后,它們通過中斷控制器發(fā)送給CPU,接著CPU判斷收到的中斷來自于哪個硬件設(shè)備(這定義在內(nèi)核中),最后,由CPU發(fā)送給內(nèi)核,有內(nèi)核處理中斷。
異常
CPU處理程序的時候一旦程序不在內(nèi)存中,會產(chǎn)生缺頁異常;當(dāng)運(yùn)行除法程序時,當(dāng)除數(shù)為0時,又會產(chǎn)生除0異常。所以,大家也需要記住的是,異常是由CPU產(chǎn)生的,同時,它會發(fā)送給內(nèi)核,要求內(nèi)核處理這些異常。
相同點(diǎn)
- 最后都是由CPU發(fā)送給內(nèi)核,由內(nèi)核去處理
- 處理程序的流程設(shè)計上是相似的
不同點(diǎn)
- 產(chǎn)生源不相同,異常是由CPU產(chǎn)生的,而中斷是由硬件設(shè)備產(chǎn)生的
- 內(nèi)核需要根據(jù)是異常還是中斷調(diào)用不同的處理程序
- 中斷不是時鐘同步的,這意味著中斷可能隨時到來;異常由于是CPU產(chǎn)生的,所以它是時鐘同步的
- 當(dāng)處理中斷時,處于中斷上下文中;處理異常時,處于進(jìn)程上下文中
-
原子操作
**處理器使用基于對緩存加鎖或總線加鎖的方式來實現(xiàn)多處理器之間的原子操作。**首先處理器會自動保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存中讀取或者寫入一個字節(jié)是原子的,意思是當(dāng)一個處理器讀取一個字節(jié)時,其他處理器不能訪問這個字節(jié)的內(nèi)存地址。Pentium 6和最新的處理器能自動保證單處理器對同一個緩存行里進(jìn)行16/32/64位的操作是原子的,但是復(fù)雜的內(nèi)存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。但是,處理器提供總線鎖定和緩存鎖定兩個機(jī)制來保證復(fù)雜內(nèi)存操作的原子性。
(1)使用總線鎖保證原子性 第一個機(jī)制是通過總線鎖保證原子性。
? 所謂總線鎖就是使用處理器提供的一個LOCK#信號,當(dāng)一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨(dú)占共享內(nèi)存。
(2)使用緩存鎖保證原子性 第二個機(jī)制是通過緩存鎖定來保證原子性。
-
服務(wù)器高并發(fā)解決方案
- 應(yīng)用數(shù)據(jù)與靜態(tài)資源分離 將靜態(tài)資源(圖片,視頻,js,css等)單獨(dú)保存到專門的靜態(tài)資源服務(wù)器中,在客戶端訪問的時候從靜態(tài)資源服務(wù)器中返回靜態(tài)資源,從主服務(wù)器中返回應(yīng)用數(shù)據(jù)。
- 客戶端緩存 因為效率最高,消耗資源最小的就是純靜態(tài)的html頁面,所以可以把網(wǎng)站上的頁面盡可能用靜態(tài)的來實現(xiàn),在頁面過期或者有數(shù)據(jù)更新之后再將頁面重新緩存。或者先生成靜態(tài)頁面,然后用ajax異步請求獲取動態(tài)數(shù)據(jù)。
- 集群和分布式 (集群是所有的服務(wù)器都有相同的功能,請求哪臺都可以,主要起分流作用)
(分布式是將不同的業(yè)務(wù)放到不同的服務(wù)器中,處理一個請求可能需要使用到多臺服務(wù)器,起到加快請求處理的速度。)
可以使用服務(wù)器集群和分布式架構(gòu),使得原本屬于一個服務(wù)器的計算壓力分散到多個服務(wù)器上。同時加快請求處理的速度。 - 反向代理 在訪問服務(wù)器的時候,服務(wù)器通過別的服務(wù)器獲取資源或結(jié)果返回給客戶端。
-
抖動你知道是什么嗎?它也叫顛簸現(xiàn)象
剛剛換出的頁面馬上又要換入內(nèi)存,剛剛換入的頁面馬上又要換出外存,這種頻繁的頁面調(diào)度行為稱為抖動,或顛簸。產(chǎn)生抖動的主要原因是進(jìn)程頻繁訪問的頁面數(shù)目高于可用的物理塊數(shù)(分配給進(jìn)程的物理塊不夠)
為進(jìn)程分配的物理塊太少,會使進(jìn)程發(fā)生抖動現(xiàn)象。為進(jìn)程分配的物理塊太多,又會降低系統(tǒng)整體的并發(fā)度,降低某些資源的利用率 為了研究為應(yīng)該為每個進(jìn)程分配多少個物理塊,Denning 提出了進(jìn)程工作集” 的概念
6. 熟悉Redis的基本使用
6.1 基本數(shù)據(jù)結(jié)構(gòu)
-
什么是Redis
Redis是一個數(shù)據(jù)庫,不過與傳統(tǒng)數(shù)據(jù)庫不同的是Redis的數(shù)據(jù)庫是存在內(nèi)存中,所以讀寫速度非???/strong>,因此 Redis被廣泛應(yīng)用于緩存方向。
除此之外,Redis也經(jīng)常用來做分布式鎖,Redis提供了多種數(shù)據(jù)類型來支持不同的業(yè)務(wù)場景。除此之外,Redis 支持事務(wù)持久化、LUA腳本、LRU驅(qū)動事件、多種集群方案。
-
Redis有哪幾種數(shù)據(jù)類型
Redis 提供了豐富的數(shù)據(jù)類型,常見的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
Redis 五種數(shù)據(jù)類型的應(yīng)用場景:
- String 類型的應(yīng)用場景:緩存對象、常規(guī)計數(shù)、分布式鎖、共享 session 信息等。
- List 類型的應(yīng)用場景:消息隊列(但是有兩個問題:1. 生產(chǎn)者需要自行實現(xiàn)全局唯一 ID;2. 不能以消費(fèi)組形式消費(fèi)數(shù)據(jù))等。
- Hash 類型:緩存對象、購物車等。
- Set 類型:聚合計算(并集、交集、差集)場景,比如點(diǎn)贊、共同關(guān)注、抽獎活動等。
- Zset 類型:排序場景,比如排行榜、電話和姓名排序等。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-AZ9MTye1-1678081583396)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/%E4%BA%94%E7%A7%8D%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B.png)]
隨著 Redis 版本的更新,后面又支持四種數(shù)據(jù)類型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 后續(xù)版本又支持四種數(shù)據(jù)類型,它們的應(yīng)用場景如下:
- BitMap(2.2 版新增):二值狀態(tài)統(tǒng)計的場景,比如簽到、判斷用戶登陸狀態(tài)、連續(xù)簽到用戶總數(shù)等;
- HyperLogLog(2.8 版新增):海量數(shù)據(jù)基數(shù)統(tǒng)計的場景,比如百萬級網(wǎng)頁 UV 計數(shù)等;
- GEO(3.2 版新增):存儲地理位置信息的場景,比如滴滴叫車;
- Stream(5.0 版新增):消息隊列,相比于基于 List 類型實現(xiàn)的消息隊列,有這兩個特有的特性:自動生成全局唯一消息ID,支持以消費(fèi)組形式消費(fèi)數(shù)據(jù)。
-
詳細(xì)介紹Redis的五種基本數(shù)據(jù)類型
String 類型內(nèi)部實現(xiàn)
String 類型的底層的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)主要是 SDS(簡單動態(tài)字符串)。 SDS 和我們認(rèn)識的 C 字符串不太一樣,之所以沒有使用 C 語言的字符串表示,因為 SDS 相比于 C 的原生字符串:
- SDS 不僅可以保存文本數(shù)據(jù),還可以保存二進(jìn)制數(shù)據(jù)。因為 SDS 使用 len 屬性的值而不是空字符來判斷字符串是否結(jié)束,并且 SDS 的所有 API 都會以處理二進(jìn)制的方式來處理 SDS 存放在 buf[] 數(shù)組里的數(shù)據(jù)。所以 SDS 不光能存放文本數(shù)據(jù),而且能保存圖片、音頻、視頻、壓縮文件這樣的二進(jìn)制數(shù)據(jù)。
- SDS 獲取字符串長度的時間復(fù)雜度是 O(1)。因為 C 語言的字符串并不記錄自身長度,所以獲取長度的復(fù)雜度為 O(n);而 SDS 結(jié)構(gòu)里用 len 屬性記錄了字符串長度,所以復(fù)雜度為 O(1)。
- Redis 的 SDS API 是安全的,拼接字符串不會造成緩沖區(qū)溢出。因為 SDS 在拼接字符串之前會檢查 SDS 空間是否滿足要求,如果空間不夠會自動擴(kuò)容,所以不會導(dǎo)致緩沖區(qū)溢出的問題。
List 類型內(nèi)部實現(xiàn)
List 類型的底層數(shù)據(jù)結(jié)構(gòu)是由雙向鏈表或壓縮列表實現(xiàn)的:
- 如果列表的元素個數(shù)小于 512 個(默認(rèn)值,可由 list-max-ziplist-entries 配置),列表每個元素的值都小于 64 字節(jié)(默認(rèn)值,可由 list-max-ziplist-value 配置),Redis 會使用壓縮列表作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu);
- 如果列表的元素不滿足上面的條件,Redis 會使用雙向鏈表作為 List 類型的底層數(shù)據(jù)結(jié)構(gòu);
但是在 Redis 3.2 版本之后,List 數(shù)據(jù)類型底層數(shù)據(jù)結(jié)構(gòu)就只由 quicklist 實現(xiàn)了,替代了雙向鏈表和壓縮列表。
Hash 類型內(nèi)部實現(xiàn)
Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或哈希表實現(xiàn)的:
- 如果哈希類型元素個數(shù)小于 512 個(默認(rèn)值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字節(jié)(默認(rèn)值,可由 hash-max-ziplist-value 配置)的話,Redis 會使用壓縮列表作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu);
- 如果哈希類型元素不滿足上面條件,Redis 會使用哈希表作為 Hash 類型的底層數(shù)據(jù)結(jié)構(gòu)。
在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)了。
Set 類型內(nèi)部實現(xiàn)
Set 類型的底層數(shù)據(jù)結(jié)構(gòu)是由哈希表或整數(shù)集合實現(xiàn)的:
- 如果集合中的元素都是整數(shù)且元素個數(shù)小于 512 (默認(rèn)值,set-maxintset-entries配置)個,Redis 會使用整數(shù)集合作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu);
- 如果集合中的元素不滿足上面條件,則 Redis 使用哈希表作為 Set 類型的底層數(shù)據(jù)結(jié)構(gòu)。
ZSet 類型內(nèi)部實現(xiàn)
Zset 類型的底層數(shù)據(jù)結(jié)構(gòu)是由壓縮列表或跳表實現(xiàn)的:
- 如果有序集合的元素個數(shù)小于 128 個,并且每個元素的值小于 64 字節(jié)時,Redis 會使用壓縮列表作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu);
- 如果有序集合的元素不滿足上面的條件,Redis 會使用跳表作為 Zset 類型的底層數(shù)據(jù)結(jié)構(gòu);
在 Redis 7.0 中,壓縮列表數(shù)據(jù)結(jié)構(gòu)已經(jīng)廢棄了,交由 listpack 數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)了。
-
Redis數(shù)據(jù)結(jié)構(gòu)詳解
- 簡單動態(tài)字符串(Simple Dynamic String,SDS)
Redis沒有直接使用C語言傳統(tǒng)的字符串,而是自己構(gòu)建了一種名為簡單動態(tài)字符串(Simple dynamic string,SDS)的抽象類型,并將SDS用作Redis的默認(rèn)字符串表示。
其實SDS等同于C語言中的char * ,但它可以存儲任意二進(jìn)制數(shù)據(jù),不能像C語言字符串那樣以字符’\0’來標(biāo)識字符串的結(jié) 束,因此它必然有個長度字段。
優(yōu)點(diǎn)
- 獲取字符串長度的復(fù)雜度為O(1)。
- 杜絕緩沖區(qū)溢出。
- 減少修改字符串長度時所需要的內(nèi)存重分配次數(shù)。
- 二進(jìn)制安全。
- 兼容部分C字符串函數(shù)。
它具有很常規(guī)的 set/get 操作,value 可以是String也可以是數(shù)字,一般做一些復(fù)雜的計數(shù)功能的緩存。
- 鏈表
當(dāng)有一個列表鍵包含了數(shù)量比較多的元素,又或者列表中包含的元素都是比較長的額字符串時,Redis就會使用鏈表作為列表建的底層實現(xiàn)。
特性
- 鏈表被廣泛用于實現(xiàn)Redis的各種功能,比如列表建、發(fā)布與訂閱、慢查詢、監(jiān)視器等。
- 每個鏈表節(jié)點(diǎn)由一個listNode結(jié)構(gòu)來表示,每個節(jié)點(diǎn)都有一個指向前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的指針,所以Redis的鏈表實現(xiàn)是雙端鏈表。
- 每個鏈表使用一個list結(jié)構(gòu)表示,這個結(jié)構(gòu)帶有表頭節(jié)點(diǎn)指針、表尾節(jié)點(diǎn)指針,以及鏈表長度等信息。
- 因為鏈表表頭的前置節(jié)點(diǎn)和表尾節(jié)點(diǎn)的后置節(jié)點(diǎn)都指向NULL,所以Redis的鏈表實現(xiàn)是無環(huán)鏈表。
- 通過為鏈表設(shè)置不同的類型特定函數(shù),Redis的鏈表可以用于保存各種不同類型的值。
- 字典
字典的底層是哈希表,類似 C++中的 map ,也就是鍵值對。
- 哈希表
哈希算法
當(dāng)字典被用作數(shù)據(jù)庫的底層實現(xiàn),或者哈希鍵的底層實現(xiàn)時,Redis使用MurmurHash算法。這種算法的優(yōu)點(diǎn)在于即使輸入的鍵是規(guī)律的,算法仍能給出一個個很好的隨機(jī)分布性,并且算法的計算速度非??臁?/p>
哈希沖突的解決方式
Redis的哈希表使用鏈地址法來解決鍵沖突,每個哈希表節(jié)點(diǎn)都有一個next指針,多個哈希表節(jié)點(diǎn)可以用這個單向鏈表連接起來,這就解決了鍵沖突的問題。
特性
-
字典被廣泛用于實現(xiàn)Redis的各種功能,其中包括數(shù)據(jù)庫和哈希鍵。
-
Redis中的字典使用哈希表作為底層結(jié)構(gòu)實現(xiàn),每個字典帶有兩個哈希表,一個平時使用,另一個僅在進(jìn)行rehash時使用。
-
Redis使用MurmurHash2算法來計算鍵的哈希值。
-
哈希表使用鏈地址法來解決鍵沖突。
-
跳躍表
Redis 只有 Zset 對象的底層實現(xiàn)用到跳表,跳表的優(yōu)勢是能支持平均 O(logN) 復(fù)雜度的節(jié)點(diǎn)查找。
zset 結(jié)構(gòu)體里有兩個數(shù)據(jù)結(jié)構(gòu):一個是跳表,一個是哈希表。這樣的好處是既能進(jìn)行高效的范圍查詢,也能進(jìn)行高效單點(diǎn)查詢。
查找一個跳表節(jié)點(diǎn)的過程時,跳表會從頭節(jié)點(diǎn)的最高層開始,逐一遍歷每一層。在遍歷某一層的跳表節(jié)點(diǎn)時,會用跳表節(jié)點(diǎn)中的 SDS 類型的元素和元素的權(quán)重來進(jìn)行判斷,共有兩個判斷條件:
- 如果當(dāng)前節(jié)點(diǎn)的權(quán)重「小于」要查找的權(quán)重時,跳表就會訪問該層上的下一個節(jié)點(diǎn)。
- 如果當(dāng)前節(jié)點(diǎn)的權(quán)重「等于」要查找的權(quán)重時,并且當(dāng)前節(jié)點(diǎn)的 SDS 類型數(shù)據(jù)「小于」要查找的數(shù)據(jù)時,跳表就會訪問該層上的下一個節(jié)點(diǎn)。
如果上面兩個條件都不滿足,或者下一個節(jié)點(diǎn)為空時,跳表就會使用目前遍歷到的節(jié)點(diǎn)的 level 數(shù)組里的下一層指針,然后沿著下一層指針繼續(xù)查找,這就相當(dāng)于跳到了下一層接著查找。
特性
- 跳躍表是有序集合的底層實現(xiàn)之一
- Redis的跳躍表實現(xiàn)由zskiplist和zskiplistNode兩個結(jié)構(gòu)組成,其中zskiplist用于保存跳躍表信息(比如表頭節(jié)點(diǎn)、表尾節(jié)點(diǎn)、長度),而zskiplistNode則用于表示跳躍表節(jié)點(diǎn)
- 每個跳躍表節(jié)點(diǎn)的層高都是1至32之間的隨機(jī)數(shù)
- 在同一個跳躍表中,多個節(jié)點(diǎn)可以包含相同的分值,但每個節(jié)點(diǎn)的成員對象必須是唯一的。
- 跳躍表中的節(jié)點(diǎn)按照分值大小進(jìn)行排序,當(dāng)分值相同時,節(jié)點(diǎn)按照成員對象的大小進(jìn)行排序。
- 跳表是一種實現(xiàn)起來很簡單,單層多指針的鏈表,它查找效率很高,堪比優(yōu)化過的二叉平衡樹,且比平衡樹的實現(xiàn)。
- 壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現(xiàn)之一。當(dāng)一個列表鍵只包含少量列表項,并且每個列表項要么就是小整數(shù)值,要么就是長度比較短的字符串,那么Redis就會使用壓縮列表來做列表鍵的底層實現(xiàn)。
特性
看他的名字就能看出來,是為了節(jié)省內(nèi)存造的列表結(jié)構(gòu)。
- quicklist
其實 quicklist 就是「雙向鏈表 + 壓縮列表」組合,因為一個 quicklist 就是一個鏈表,而鏈表中的每個元素又是一個壓縮列表。
quicklist 解決辦法,通過控制每個鏈表節(jié)點(diǎn)中的壓縮列表的大小或者元素個數(shù),來規(guī)避連鎖更新的問題。因為壓縮列表元素越少或越小,連鎖更新帶來的影響就越小,從而提供了更好的訪問性能。
- listpack
listpack,目的是替代壓縮列表,它最大特點(diǎn)是 listpack 中每個節(jié)點(diǎn)不再包含前一個節(jié)點(diǎn)的長度了,壓縮列表每個節(jié)點(diǎn)正因為需要保存前一個節(jié)點(diǎn)的長度字段,就會有連鎖更新的隱患。
listpack 采用了壓縮列表的很多優(yōu)秀的設(shè)計,比如還是用一塊連續(xù)的內(nèi)存空間來緊湊地保存數(shù)據(jù),并且為了節(jié)省內(nèi)存的開銷,listpack 節(jié)點(diǎn)會采用不同的編碼方式保存不同大小的數(shù)據(jù)。
listpack 沒有壓縮列表中記錄前一個節(jié)點(diǎn)長度的字段了,listpack 只記錄當(dāng)前節(jié)點(diǎn)的長度,當(dāng)我們向 listpack 加入一個新元素的時候,不會影響其他節(jié)點(diǎn)的長度字段的變化,從而避免了壓縮列表的連鎖更新問題。
- 整數(shù)集合
整數(shù)集合是 Set 對象的底層實現(xiàn)之一。當(dāng)一個 Set 對象只包含整數(shù)值元素,并且元素數(shù)量不大時,就會使用整數(shù)集這個數(shù)據(jù)結(jié)構(gòu)作為底層實現(xiàn)。整數(shù)集合本質(zhì)上是一塊連續(xù)內(nèi)存空間。
整數(shù)集合會有一個升級規(guī)則,就是當(dāng)我們將一個新元素加入到整數(shù)集合里面,如果新元素的類型(int32_t)比整數(shù)集合現(xiàn)有所有元素的類型(int16_t)都要長時,整數(shù)集合需要先進(jìn)行升級,也就是按新元素的類型(int32_t)擴(kuò)展 contents 數(shù)組的空間大小,然后才能將新元素加入到整數(shù)集合里,當(dāng)然升級的過程中,也要維持整數(shù)集合的有序性。
整數(shù)集合升級的過程不會重新分配一個新類型的數(shù)組,而是在原本的數(shù)組上擴(kuò)展空間,然后在將每個元素按間隔類型大小分割,如果 encoding 屬性值為 INTSET_ENC_INT16,則每個元素的間隔就是 16 位。
整數(shù)集合升級的好處是節(jié)省內(nèi)存資源。
-
Redis的線程模式
Redis 單線程指的是「接收客戶端請求->解析請求 ->進(jìn)行數(shù)據(jù)讀寫等操作->發(fā)送數(shù)據(jù)給客戶端」這個過程是由一個線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。
但是,Redis 程序并不是單線程的,Redis 在啟動的時候,是會啟動后臺線程(BIO)的
關(guān)閉文件、AOF 刷盤、釋放內(nèi)存
Redis 單線程模式是怎樣的
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-IMjNfraQ-1678081583397)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/redis%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B.drawio.png)]
圖中的藍(lán)色部分是一個事件循環(huán),是由主線程負(fù)責(zé)的,可以看到網(wǎng)絡(luò) I/O 和命令處理都是單線程。 Redis 初始化的時候,會做下面這幾件事情:
- 首先,調(diào)用 epoll_create() 創(chuàng)建一個 epoll 對象和調(diào)用 socket() 創(chuàng)建一個服務(wù)端 socket
- 然后,調(diào)用 bind() 綁定端口和調(diào)用 listen() 監(jiān)聽該 socket;
- 然后,將調(diào)用 epoll_ctl() 將 listen socket 加入到 epoll,同時注冊「連接事件」處理函數(shù)。
初始化完后,主線程就進(jìn)入到一個事件循環(huán)函數(shù),主要會做以下事情:
- 首先,先調(diào)用處理發(fā)送隊列函數(shù),看是發(fā)送隊列里是否有任務(wù),如果有發(fā)送任務(wù),則通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)送完,就會注冊寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。
- 接著,調(diào)用 epoll_wait 函數(shù)等待事件的到來:
- 如果是連接事件到來,則會調(diào)用連接事件處理函數(shù),該函數(shù)會做這些事情:調(diào)用 accpet 獲取已連接的 socket -> 調(diào)用 epoll_ctl 將已連接的 socket 加入到 epoll -> 注冊「讀事件」處理函數(shù);
- 如果是讀事件到來,則會調(diào)用讀事件處理函數(shù),該函數(shù)會做這些事情:調(diào)用 read 獲取客戶端發(fā)送的數(shù)據(jù) -> 解析命令 -> 處理命令 -> 將客戶端對象添加到發(fā)送隊列 -> 將執(zhí)行結(jié)果寫到發(fā)送緩存區(qū)等待發(fā)送;
- 如果是寫事件到來,則會調(diào)用寫事件處理函數(shù),該函數(shù)會做這些事情:通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)送完,就會繼續(xù)注冊寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。
-
使用Redis的好處有哪些
1、訪問速度快,因為數(shù)據(jù)存在內(nèi)存中,類似于Java中的HashMap或者C++中的哈希表(如unordered_map/unordered_set),這兩者的優(yōu)勢就是查找和操作的時間復(fù)雜度都是O(1)
2、數(shù)據(jù)類型豐富,支持String,list,set,sorted set,hash這五種數(shù)據(jù)結(jié)構(gòu)
3、支持事務(wù),Redis中的操作都是原子性,換句話說就是對數(shù)據(jù)的更改要么全部執(zhí)行,要么全部不執(zhí)行,這就是原子性的定義
4、特性豐富:Redis可用于緩存,消息,按key設(shè)置過期時間,過期后將會自動刪除。
-
Memcached與Redis的區(qū)別都有哪些
1、存儲方式
- Memecache把數(shù)據(jù)全部存在內(nèi)存之中,斷電后會掛掉,沒有持久化功能,數(shù)據(jù)不能超過內(nèi)存大小。
- Redis有部份存在硬盤上,這樣能保證數(shù)據(jù)的持久性。
2、數(shù)據(jù)支持類型
- Memcache對數(shù)據(jù)類型支持相對簡單,只有String這一種類型
- Redis有復(fù)雜的數(shù)據(jù)類型。Redis不僅僅支持簡單的k/v類型的數(shù)據(jù),同時還提供 list,set,zset,hash等數(shù)據(jù)結(jié)構(gòu)的存儲。
3、使用底層模型不同
- 它們之間底層實現(xiàn)方式 以及與客戶端之間通信的應(yīng)用協(xié)議不一樣。
- Redis直接自己構(gòu)建了VM 機(jī)制 ,因為一般的系統(tǒng)調(diào)用系統(tǒng)函數(shù)的話,會浪費(fèi)一定的時間去移動和請求。
4、集群模式:Memcached沒有原生的集群模式,需要依靠客戶端來實現(xiàn)往集群中分片寫入數(shù)據(jù);但是 Redis 目前 是原生支持 cluster 模式的.
5、Memcached是多線程,非阻塞IO復(fù)用的網(wǎng)絡(luò)模型;Redis使用單線程的多路 IO 復(fù)用模型。
6、Value 值大小不同:Redis 最大可以達(dá)到 512MB;Memcached 只有 1MB。
-
單線程的Redis為什么這么快
-
Redis的全部操作都是純內(nèi)存的操作;
-
Redis采用單線程,有效避免了頻繁的上下文切換;
-
采用了非阻塞I/O多路復(fù)用機(jī)制。
-
-
Hash 沖突怎么辦
Redis 通過鏈?zhǔn)焦?/strong>解決沖突:也就是同一個 桶里面的元素使用鏈表保存。但是當(dāng)鏈表過長就會導(dǎo)致查找性能變差可能,所以 Redis 為了追求快,使用了兩個全局哈希表。用于 rehash 操作,增加現(xiàn)有的哈希桶數(shù)量,減少哈希沖突。
開始默認(rèn)使用 「hash 表 1 」保存鍵值對數(shù)據(jù),「hash 表 2」 此刻沒有分配空間。當(dāng)數(shù)據(jù)越來越多觸發(fā) rehash 操作,則執(zhí)行以下操作:
- 給 「hash 表 2 」分配更大的空間;
- 將 「hash 表 1 」的數(shù)據(jù)重新映射拷貝到 「hash 表 2」 中;
- 釋放 「hash 表 1」 的空間。
值得注意的是,將 hash 表 1 的數(shù)據(jù)重新映射到 hash 表 2 的過程中并不是一次性的,這樣會造成 Redis 阻塞,無法提供服務(wù)。
而是采用了漸進(jìn)式 rehash,每次處理客戶端請求的時候,先從「 hash 表 1」 中第一個索引開始,將這個位置的 所有數(shù)據(jù)拷貝到 「hash 表 2」 中,就這樣將 rehash 分散到多次請求過程中,避免耗時阻塞。
-
Redis的過期刪除策略
我們都知道,Redis是key-value數(shù)據(jù)庫,我們可以設(shè)置Redis中緩存的key的過期時間。Redis的過期策略就是指當(dāng)Redis中緩存的key過期了,Redis如何處理。
過期策略通常有以下三種:
- 定時過期:每個設(shè)置過期時間的key都需要創(chuàng)建一個定時器,到過期時間就會立即清除。該策略可以立即清除過期的數(shù)據(jù),對內(nèi)存很友好;但是會占用大量的CPU資源去處理過期的數(shù)據(jù),從而影響緩存的響應(yīng)時間和吞吐量。
- 惰性過期:只有當(dāng)訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節(jié)省CPU資源,卻對內(nèi)存非常不友好。極端情況可能出現(xiàn)大量的過期key沒有再次被訪問,從而不會被清除,占用大量內(nèi)存。
- 定期清除:每隔一定的時間,會掃描一定數(shù)量的數(shù)據(jù)庫的expires字典中一定數(shù)量的key,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調(diào)整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。
(expires字典會保存所有設(shè)置了過期時間的key的過期時間數(shù)據(jù),其中,key是指向鍵空間中的某個鍵的指針,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵。)
Redis中同時使用了惰性過期和定期過期兩種過期策略。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-yPoc3v2v-1678081583397)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/%E8%BF%87%E6%9C%9F%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5.jpg)]
-
Redis的內(nèi)存淘汰策略
Redis 內(nèi)存淘汰策略共有八種,這八種策略大體分為「不進(jìn)行數(shù)據(jù)淘汰」和「進(jìn)行數(shù)據(jù)淘汰」兩類策略。
1、不進(jìn)行數(shù)據(jù)淘汰的策略
noeviction(Redis3.0之后,默認(rèn)的內(nèi)存淘汰策略) :它表示當(dāng)運(yùn)行內(nèi)存超過最大設(shè)置內(nèi)存時,不淘汰任何數(shù)據(jù),這時如果有新的數(shù)據(jù)寫入,則會觸發(fā) OOM,但是如果沒用數(shù)據(jù)寫入的話,只是單純的查詢或者刪除操作的話,還是可以正常工作。
2、進(jìn)行數(shù)據(jù)淘汰的策略
針對「進(jìn)行數(shù)據(jù)淘汰」這一類策略,又可以細(xì)分為「在設(shè)置了過期時間的數(shù)據(jù)中進(jìn)行淘汰」和「在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰」這兩類策略。
在設(shè)置了過期時間的數(shù)據(jù)中進(jìn)行淘汰:
- volatile-random:隨機(jī)淘汰設(shè)置了過期時間的任意鍵值;
- volatile-ttl:優(yōu)先淘汰更早過期的鍵值。
- volatile-lru(Redis3.0 之前,默認(rèn)的內(nèi)存淘汰策略):淘汰所有設(shè)置了過期時間的鍵值中,最久未使用的鍵值;
- volatile-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略):淘汰所有設(shè)置了過期時間的鍵值中,最少使用的鍵值;
在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰:
- allkeys-random:隨機(jī)淘汰任意鍵值;
- allkeys-lru:淘汰整個鍵值中最久未使用的鍵值;
- allkeys-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略):淘汰整個鍵值中最少使用的鍵值。
Redis 是如何實現(xiàn) LRU 算法的?
Redis 實現(xiàn)的是一種近似 LRU 算法,目的是為了更好的節(jié)約內(nèi)存,它的實現(xiàn)方式是在 Redis 的對象結(jié)構(gòu)體中添加一個額外的字段,用于記錄此數(shù)據(jù)的最后一次訪問時間。
當(dāng) Redis 進(jìn)行內(nèi)存淘汰時,會使用隨機(jī)采樣的方式來淘汰數(shù)據(jù),它是隨機(jī)取 5 個值(此值可配置),然后淘汰最久沒有使用的那個。
Redis 實現(xiàn)的 LRU 算法的優(yōu)點(diǎn):
- 不用為所有的數(shù)據(jù)維護(hù)一個大鏈表,節(jié)省了空間占用;
- 不用在每次數(shù)據(jù)訪問時都移動鏈表項,提升了緩存的性能;
什么是 LFU 算法?
LFU 全稱是 Least Frequently Used 翻譯為最近最不常用,LFU 算法是根據(jù)數(shù)據(jù)訪問次數(shù)來淘汰數(shù)據(jù)的,它的核心思想是“如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也更高”。
所以, LFU 算法會記錄每個數(shù)據(jù)的訪問次數(shù)。當(dāng)一個數(shù)據(jù)被再次訪問時,就會增加該數(shù)據(jù)的訪問次數(shù)。這樣就解決了偶爾被訪問一次之后,數(shù)據(jù)留存在緩存中很長一段時間的問題,相比于 LRU 算法也更合理一些。
Redis 是如何實現(xiàn) LFU 算法的?
LFU 算法相比于 LRU 算法的實現(xiàn),多記錄了「數(shù)據(jù)的訪問頻次」的信息。
訪問頻次(訪問頻率)的 logc 會隨時間推移而衰減的。
- 先按照上次訪問距離當(dāng)前的時長,來對 logc 進(jìn)行衰減;
- 然后,再按照一定概率增加 logc 的值
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-MVmju7O5-1678081583398)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/%E5%86%85%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5.jpg)]
6.2 數(shù)據(jù)持久化
-
Redis如何實現(xiàn)持久化
Redis是一個支持持久化的內(nèi)存數(shù)據(jù)庫,通過持久化機(jī)制把內(nèi)存中的數(shù)據(jù)同步到硬盤文件來保證數(shù)據(jù)持久化。當(dāng)Redis重啟后通過把硬盤文件重新加載到內(nèi)存,就能達(dá)到恢復(fù)數(shù)據(jù)的目的。
很多時候我們需要持久化數(shù)據(jù)也就是將內(nèi)存中的數(shù)據(jù)寫入到硬盤里面,大部分原因是為了之后重用數(shù)據(jù)(比如重啟機(jī) 器、機(jī)器故障之后回復(fù)數(shù)據(jù)),或者是為了防止系統(tǒng)故障而將數(shù)據(jù)備份到一個遠(yuǎn)程位置。
Redis 共有三種數(shù)據(jù)持久化的方式:
- AOF 日志:每執(zhí)行一條寫操作命令,就把該命令以追加的方式寫入到一個文件里;
- RDB 快照:將某一時刻的內(nèi)存數(shù)據(jù),以二進(jìn)制的方式寫入磁盤;
- 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的優(yōu)點(diǎn);
-
AOF日志實現(xiàn)
AOF(append-only file)持久化
Redis 在執(zhí)行完一條寫操作命令后,就會把該命令以追加的方式寫入到一個文件里,然后 Redis 重啟時,會讀取該文件記錄的命令,然后逐一執(zhí)行命令的方式來進(jìn)行數(shù)據(jù)恢復(fù)。
Redis 提供了 3 種寫回硬盤的策略,控制的就是上面說的第三步的過程。 在 Redis.conf 配置文件中的 appendfsync 配置項可以有以下 3 種參數(shù)可填:
- Always,這個單詞的意思是「總是」,所以它的意思是每次寫操作命令執(zhí)行完后,同步將 AOF 日志數(shù)據(jù)寫回硬盤;
- Everysec,這個單詞的意思是「每秒」,所以它的意思是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),然后每隔一秒將緩沖區(qū)里的內(nèi)容寫回到硬盤;
- No,意味著不由 Redis 控制寫回硬盤的時機(jī),轉(zhuǎn)交給操作系統(tǒng)控制寫回的時機(jī),也就是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),再由操作系統(tǒng)決定何時將緩沖區(qū)內(nèi)容寫回硬盤。
AOF 日志過大,會觸發(fā)AOF 重寫機(jī)制
AOF 重寫機(jī)制是在重寫時,讀取當(dāng)前數(shù)據(jù)庫中的所有鍵值對,然后將每一個鍵值對用一條命令記錄到「新的 AOF 文件」,等到全部記錄完后,就將新的 AOF 文件替換掉現(xiàn)有的 AOF 文件。
Redis 提供了 AOF 重寫機(jī)制,它會直接掃描數(shù)據(jù)中所有的鍵值對數(shù)據(jù),然后為每一個鍵值對生成一條寫操作命令,接著將該命令寫入到新的 AOF 文件,重寫完成后,就替換掉現(xiàn)有的 AOF 日志。重寫的過程是由后臺子進(jìn)程完成的,這樣可以使得主進(jìn)程可以繼續(xù)正常處理命令。
-
RDB快照實現(xiàn)
快照(snapshotting)持久化(RDB持久化)
將某一時刻的內(nèi)存數(shù)據(jù),以二進(jìn)制的方式寫入磁盤;RDB 快照就是記錄某一個瞬間的內(nèi)存數(shù)據(jù),記錄的是實際數(shù)據(jù),而 AOF 文件記錄的是命令操作的日志,而不是實際的數(shù)據(jù)。
因此在 Redis 恢復(fù)數(shù)據(jù)時, RDB 恢復(fù)數(shù)據(jù)的效率會比 AOF 高些,因為直接將 RDB 文件讀入內(nèi)存就可以,不需要像 AOF 那樣還需要額外執(zhí)行操作命令的步驟才能恢復(fù)數(shù)據(jù)。
RDB 在執(zhí)行快照的時候,數(shù)據(jù)能修改嗎?
可以的,執(zhí)行 bgsave 過程中,Redis 依然可以繼續(xù)處理操作命令的,也就是數(shù)據(jù)是能被修改的,關(guān)鍵的技術(shù)就在于寫時復(fù)制技術(shù)(Copy-On-Write, COW)。
執(zhí)行 bgsave 命令的時候,會通過 fork() 創(chuàng)建子進(jìn)程,此時子進(jìn)程和父進(jìn)程是共享同一片內(nèi)存數(shù)據(jù)的,因為創(chuàng)建子進(jìn)程的時候,會復(fù)制父進(jìn)程的頁表,但是頁表指向的物理內(nèi)存還是一個,此時如果主線程執(zhí)行讀操作,則主線程和 bgsave 子進(jìn)程互相不影響。
-
混合持久化
混合持久化方式 Redis 4.0 對于持久化機(jī)制的優(yōu)化
Redis 4.0 開始支持 RDB 和 AOF 的混合持久化(默認(rèn)關(guān)閉,可以通過配置項 aof-use-rdb-preamble 開啟)。
使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量數(shù)據(jù),后半部分是 AOF 格式的增量數(shù)據(jù)。
如果把混合持久化打開,AOF 重寫的時候就直接把 RDB 的內(nèi)容寫到 AOF 文件開頭。這樣做的好處是可以結(jié)合 RDB 和 AOF 的優(yōu)點(diǎn), 快速加載同時避免丟失過多的數(shù)據(jù)。當(dāng)然缺點(diǎn)也是有的, AOF 里面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。
-
大Key對Redis持久化有什么影響
當(dāng) AOF 寫回策略配置了 Always 策略,如果寫入是一個大 Key,主線程在執(zhí)行 fsync() 函數(shù)的時候,阻塞的時間會比較久,因為當(dāng)寫入的數(shù)據(jù)量很大的時候,數(shù)據(jù)同步到硬盤這個過程是很耗時的。
AOF 重寫機(jī)制和 RDB 快照(bgsave 命令)的過程,都會分別通過
fork()
函數(shù)創(chuàng)建一個子進(jìn)程來處理任務(wù)。會有兩個階段會導(dǎo)致阻塞父進(jìn)程(主線程):- 創(chuàng)建子進(jìn)程的途中,由于要復(fù)制父進(jìn)程的頁表等數(shù)據(jù)結(jié)構(gòu),阻塞的時間跟頁表的大小有關(guān),頁表越大,阻塞的時間也越長;
- 創(chuàng)建完子進(jìn)程后,如果父進(jìn)程修改了共享數(shù)據(jù)中的大 Key,就會發(fā)生寫時復(fù)制,這期間會拷貝物理內(nèi)存,由于大 Key 占用的物理內(nèi)存會很大,那么在復(fù)制物理內(nèi)存這一過程,就會比較耗時,所以有可能會阻塞父進(jìn)程。
大 key 除了會影響持久化之外,還會有以下的影響。
- 客戶端超時阻塞。由于 Redis 執(zhí)行命令是單線程處理,然后在操作大 key 時會比較耗時,那么就會阻塞 Redis,從客戶端這一視角看,就是很久很久都沒有響應(yīng)。
- 引發(fā)網(wǎng)絡(luò)阻塞。每次獲取大 key 產(chǎn)生的網(wǎng)絡(luò)流量較大,如果一個 key 的大小是 1 MB,每秒訪問量為 1000,那么每秒會產(chǎn)生 1000MB 的流量,這對于普通千兆網(wǎng)卡的服務(wù)器來說是災(zāi)難性的。
- 阻塞工作線程。如果使用 del 刪除大 key 時,會阻塞工作線程,這樣就沒辦法處理后續(xù)的命令。
- 內(nèi)存分布不均。集群模型在 slot 分片均勻情況下,會出現(xiàn)數(shù)據(jù)和查詢傾斜情況,部分有大 key 的 Redis 節(jié)點(diǎn)占用內(nèi)存多,QPS 也會比較大。
如何避免大 Key 呢?
最好在設(shè)計階段,就把大 key 拆分成一個一個小 key。或者,定時檢查 Redis 是否存在大 key ,如果該大 key 是可以刪除的,不要使用 DEL 命令刪除,因為該命令刪除過程會阻塞主線程,而是用 unlink 命令(Redis 4.0+)刪除大 key,因為該命令的刪除過程是異步的,不會阻塞主線程。
6.3 高可用
-
主從復(fù)制模式介紹
Redis多副本,采用主從(replication)部署結(jié)構(gòu),相較于單副本而言最大的特點(diǎn)就是主從實例間數(shù)據(jù)實時同步,并且提供數(shù)據(jù)持久化和備份策略。主從實例部署在不同的物理服務(wù)器上,根據(jù)公司的基礎(chǔ)環(huán)境配置,可以實現(xiàn)同時對外提供服務(wù)和讀寫分離策略。
優(yōu)點(diǎn):
- 高可靠性:一方面,采用雙機(jī)主備架構(gòu),能夠在主庫出現(xiàn)故障時自動進(jìn)行主備切換,從庫提升為主庫提供服務(wù),保證服務(wù)平穩(wěn)運(yùn)行;另一方面,開啟數(shù)據(jù)持久化功能和配置合理的備份策略,能有效的解決數(shù)據(jù)誤操作和數(shù)據(jù)異常丟失的問題;
- 讀寫分離策略:從節(jié)點(diǎn)可以擴(kuò)展主庫節(jié)點(diǎn)的讀能力,有效應(yīng)對大并發(fā)量的讀操作。
缺點(diǎn):
- 故障恢復(fù)復(fù)雜,如果沒有RedisHA系統(tǒng)(需要開發(fā)),當(dāng)主庫節(jié)點(diǎn)出現(xiàn)故障時,需要手動將一個從節(jié)點(diǎn)晉升為主節(jié)點(diǎn),同時需要通知業(yè)務(wù)方變更配置,并且需要讓其它從庫節(jié)點(diǎn)去復(fù)制新主庫節(jié)點(diǎn),整個過程需要人為干預(yù),比較繁瑣;
- 主庫的寫能力受到單機(jī)的限制,可以考慮分片;
- 主庫的存儲能力受到單機(jī)的限制,可以考慮Pika;
- 原生復(fù)制的弊端在早期的版本中也會比較突出,如:Redis復(fù)制中斷后,Slave會發(fā)起psync,此時如果同步不成功,則會進(jìn)行全量同步,主庫執(zhí)行全量備份的同時可能會造成毫秒或秒級的卡頓;又由于COW機(jī)制,導(dǎo)致極端情況下的主庫內(nèi)存溢出,程序異常退出或宕機(jī);主庫節(jié)點(diǎn)生成備份文件導(dǎo)致服務(wù)器磁盤IO和CPU(壓縮)資源消耗;發(fā)送數(shù)GB大小的備份文件導(dǎo)致服務(wù)器出口帶寬暴增,阻塞請求,建議升級到最新版本。
-
主從復(fù)制是怎么實現(xiàn)的
主從復(fù)制共有三種模式:全量復(fù)制、基于長連接的命令傳播、增量復(fù)制。
主從服務(wù)器第一次同步的時候,就是采用全量復(fù)制,此時主服務(wù)器會兩個耗時的地方,分別是生成 RDB 文件和傳輸 RDB 文件。為了避免過多的從服務(wù)器和主服務(wù)器進(jìn)行全量復(fù)制,可以把一部分從服務(wù)器升級為「經(jīng)理角色」,讓它也有自己的從服務(wù)器,通過這樣可以分?jǐn)傊鞣?wù)器的壓力。
第一次同步完成后,主從服務(wù)器都會維護(hù)著一個長連接,主服務(wù)器在接收到寫操作命令后,就會通過這個連接將寫命令傳播給從服務(wù)器,來保證主從服務(wù)器的數(shù)據(jù)一致性。
如果遇到網(wǎng)絡(luò)斷開,增量復(fù)制就可以上場了,不過這個還跟 repl_backlog_size 這個大小有關(guān)系。
如果它配置的過小,主從服務(wù)器網(wǎng)絡(luò)恢復(fù)時,可能發(fā)生「從服務(wù)器」想讀的數(shù)據(jù)已經(jīng)被覆蓋了,那么這時就會導(dǎo)致主服務(wù)器采用全量復(fù)制的方式。所以為了避免這種情況的頻繁發(fā)生,要調(diào)大這個參數(shù)的值,以降低主從服務(wù)器斷開后全量同步的概率。
-
集群模式的工作原理是什么
基本通信原理
集群元數(shù)據(jù)的維護(hù)有兩種方式:集中式、Gossip 協(xié)議。Redis cluster 節(jié)點(diǎn)間采用 gossip 協(xié)議進(jìn)行通信。
集中式是將集群元數(shù)據(jù)(節(jié)點(diǎn)信息、故障等等)集中存儲在某個節(jié)點(diǎn)上。集中式元數(shù)據(jù)集中存儲的一個典型代表,就是大數(shù)據(jù)領(lǐng)域的
storm
。它是分布式的大數(shù)據(jù)實時計算引擎,是集中式的元數(shù)據(jù)存儲的結(jié)構(gòu),底層基于 zookeeper(分布式協(xié)調(diào)的中間件)對所有元數(shù)據(jù)進(jìn)行存儲維護(hù)。Redis 維護(hù)集群元數(shù)據(jù)采用另一個方式,
gossip
協(xié)議,所有節(jié)點(diǎn)都持有一份元數(shù)據(jù),不同的節(jié)點(diǎn)如果出現(xiàn)了元數(shù)據(jù)的變更,就不斷將元數(shù)據(jù)發(fā)送給其它的節(jié)點(diǎn),讓其它節(jié)點(diǎn)也進(jìn)行元數(shù)據(jù)的變更。集中式的好處在于,元數(shù)據(jù)的讀取和更新,時效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,就立即更新到集中式的存儲中,其它節(jié)點(diǎn)讀取的時候就可以感知到;不好在于,所有的元數(shù)據(jù)的更新壓力全部集中在一個地方,可能會導(dǎo)致元數(shù)據(jù)的存儲有壓力。
gossip 好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個地方,更新請求會陸陸續(xù)續(xù)打到所有節(jié)點(diǎn)上去更新,降低了壓力;不好在于,元數(shù)據(jù)的更新有延時,可能導(dǎo)致集群中的一些操作會有一些滯后。
- 10000 端口:每個節(jié)點(diǎn)都有一個專門用于節(jié)點(diǎn)間通信的端口,就是自己提供服務(wù)的端口號+10000,比如 7001,那么用于節(jié)點(diǎn)間通信的就是 17001 端口。每個節(jié)點(diǎn)每隔一段時間都會往另外幾個節(jié)點(diǎn)發(fā)送
ping
消息,同時其它幾個節(jié)點(diǎn)接收到ping
之后返回pong
。 - 交換的信息:信息包括故障信息,節(jié)點(diǎn)的增加和刪除,hash slot 信息等等。
gossip 協(xié)議
gossip 協(xié)議包含多種消息,包含
ping
,pong
,meet
,fail
等等。- meet:某個節(jié)點(diǎn)發(fā)送 meet 給新加入的節(jié)點(diǎn),讓新節(jié)點(diǎn)加入集群中,然后新節(jié)點(diǎn)就會開始與其它節(jié)點(diǎn)進(jìn)行通信。
其實內(nèi)部就是發(fā)送了一個 gossip meet 消息給新加入的節(jié)點(diǎn),通知那個節(jié)點(diǎn)去加入我們的集群。
- ping:每個節(jié)點(diǎn)都會頻繁給其它節(jié)點(diǎn)發(fā)送 ping,其中包含自己的狀態(tài)還有自己維護(hù)的集群元數(shù)據(jù),互相通過 ping 交換元數(shù)據(jù)。
- pong:返回 ping 和 meet,包含自己的狀態(tài)和其它信息,也用于信息廣播和更新。
- fail:某個節(jié)點(diǎn)判斷另一個節(jié)點(diǎn) fail 之后,就發(fā)送 fail 給其它節(jié)點(diǎn),通知其它節(jié)點(diǎn)說,某個節(jié)點(diǎn)宕機(jī)啦。
ping 消息深入
ping 時要攜帶一些元數(shù)據(jù),如果很頻繁,可能會加重網(wǎng)絡(luò)負(fù)擔(dān)。
每個節(jié)點(diǎn)每秒會執(zhí)行 10 次 ping,每次會選擇 5 個最久沒有通信的其它節(jié)點(diǎn)。當(dāng)然如果發(fā)現(xiàn)某個節(jié)點(diǎn)通信延時達(dá)到了
cluster_node_timeout / 2
,那么立即發(fā)送 ping,避免數(shù)據(jù)交換延時過長,落后的時間太長了。比如說,兩個節(jié)點(diǎn)之間都 10 分鐘沒有交換數(shù)據(jù)了,那么整個集群處于嚴(yán)重的元數(shù)據(jù)不一致的情況,就會有問題。所以cluster_node_timeout
可以調(diào)節(jié),如果調(diào)得比較大,那么會降低 ping 的頻率。每次 ping,會帶上自己節(jié)點(diǎn)的信息,還有就是帶上 1/10 其它節(jié)點(diǎn)的信息,發(fā)送出去,進(jìn)行交換。至少包含
3
個其它節(jié)點(diǎn)的信息,最多包含總節(jié)點(diǎn)數(shù)減 2
個其它節(jié)點(diǎn)的信息。分布式尋址算法
- hash 算法(大量緩存重建)
- 一致性 hash 算法(自動緩存遷移)+ 虛擬節(jié)點(diǎn)(自動負(fù)載均衡)
- Redis cluster 的 hash slot 算法
hash 算法
來了一個 key,首先計算 hash 值,然后對節(jié)點(diǎn)數(shù)取模。然后打在不同的 master 節(jié)點(diǎn)上。一旦某一個 master 節(jié)點(diǎn)宕機(jī),所有請求過來,都會基于最新的剩余 master 節(jié)點(diǎn)數(shù)去取模,嘗試去取數(shù)據(jù)。這會導(dǎo)致大部分的請求過來,全部無法拿到有效的緩存,導(dǎo)致大量的流量涌入數(shù)據(jù)庫。
一致性 hash 算法
一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環(huán),整個空間按順時針方向組織,下一步將各個 master 節(jié)點(diǎn)(使用服務(wù)器的 ip 或主機(jī)名)進(jìn)行 hash。這樣就能確定每個節(jié)點(diǎn)在其哈希環(huán)上的位置。
來了一個 key,首先計算 hash 值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,遇到的第一個 master 節(jié)點(diǎn)就是 key 所在位置。
在一致性哈希算法中,如果一個節(jié)點(diǎn)掛了,受影響的數(shù)據(jù)僅僅是此節(jié)點(diǎn)到環(huán)空間前一個節(jié)點(diǎn)(沿著逆時針方向行走遇到的第一個節(jié)點(diǎn))之間的數(shù)據(jù),其它不受影響。增加一個節(jié)點(diǎn)也同理。
燃鵝,一致性哈希算法在節(jié)點(diǎn)太少時,容易因為節(jié)點(diǎn)分布不均勻而造成緩存熱點(diǎn)的問題。為了解決這種熱點(diǎn)問題,一致性 hash 算法引入了虛擬節(jié)點(diǎn)機(jī)制,即對每一個節(jié)點(diǎn)計算多個 hash,每個計算結(jié)果位置都放置一個虛擬節(jié)點(diǎn)。這樣就實現(xiàn)了數(shù)據(jù)的均勻分布,負(fù)載均衡。
Redis cluster 的 hash slot 算法
Redis cluster 有固定的
16384
個 hash slot,對每個key
計算CRC16
值,然后對16384
取模,可以獲取 key 對應(yīng)的 hash slot。Redis cluster 中每個 master 都會持有部分 slot,比如有 3 個 master,那么可能每個 master 持有 5000 多個 hash slot。hash slot 讓 node 的增加和移除很簡單,增加一個 master,就將其他 master 的 hash slot 移動部分過去,減少一個 master,就將它的 hash slot 移動到其他 master 上去。移動 hash slot 的成本是非常低的??蛻舳说?api,可以對指定的數(shù)據(jù),讓他們走同一個 hash slot,通過
hash tag
來實現(xiàn)。任何一臺機(jī)器宕機(jī),另外兩個節(jié)點(diǎn),不影響的。因為 key 找的是 hash slot,不是機(jī)器。
Redis cluster 的高可用與主備切換原理
Redis cluster 的高可用的原理,幾乎跟哨兵是類似的。
判斷節(jié)點(diǎn)宕機(jī)
如果一個節(jié)點(diǎn)認(rèn)為另外一個節(jié)點(diǎn)宕機(jī),那么就是
pfail
,主觀宕機(jī)。如果多個節(jié)點(diǎn)都認(rèn)為另外一個節(jié)點(diǎn)宕機(jī)了,那么就是fail
,客觀宕機(jī),跟哨兵的原理幾乎一樣,sdown,odown。在
cluster-node-timeout
內(nèi),某個節(jié)點(diǎn)一直沒有返回pong
,那么就被認(rèn)為pfail
。如果一個節(jié)點(diǎn)認(rèn)為某個節(jié)點(diǎn)
pfail
了,那么會在gossip ping
消息中,ping
給其他節(jié)點(diǎn),如果超過半數(shù)的節(jié)點(diǎn)都認(rèn)為pfail
了,那么就會變成fail
。從節(jié)點(diǎn)過濾
對宕機(jī)的 master node,從其所有的 slave node 中,選擇一個切換成 master node。
檢查每個 slave node 與 master node 斷開連接的時間,如果超過了
cluster-node-timeout * cluster-slave-validity-factor
,那么就沒有資格切換成master
。從節(jié)點(diǎn)選舉
每個從節(jié)點(diǎn),都根據(jù)自己對 master 復(fù)制數(shù)據(jù)的 offset,來設(shè)置一個選舉時間,offset 越大(復(fù)制數(shù)據(jù)越多)的從節(jié)點(diǎn),選舉時間越靠前,優(yōu)先進(jìn)行選舉。
所有的 master node 開始 slave 選舉投票,給要進(jìn)行選舉的 slave 進(jìn)行投票,如果大部分 master node
(N/2 + 1)
都投票給了某個從節(jié)點(diǎn),那么選舉通過,那個從節(jié)點(diǎn)可以切換成 master。從節(jié)點(diǎn)執(zhí)行主備切換,從節(jié)點(diǎn)切換為主節(jié)點(diǎn)。
與哨兵比較
整個流程跟哨兵相比,非常類似,所以說,Redis cluster 功能強(qiáng)大,直接集成了 replication 和 sentinel 的功能。
- 10000 端口:每個節(jié)點(diǎn)都有一個專門用于節(jié)點(diǎn)間通信的端口,就是自己提供服務(wù)的端口號+10000,比如 7001,那么用于節(jié)點(diǎn)間通信的就是 17001 端口。每個節(jié)點(diǎn)每隔一段時間都會往另外幾個節(jié)點(diǎn)發(fā)送
-
哨兵模式的作用
哨兵的介紹
sentinel,中文名是哨兵。哨兵是 Redis 集群架構(gòu)中非常重要的一個組件,主要有以下功能:
- 集群監(jiān)控:負(fù)責(zé)監(jiān)控 Redis master 和 slave 進(jìn)程是否正常工作。
- 消息通知:如果某個 Redis 實例有故障,那么哨兵負(fù)責(zé)發(fā)送消息作為報警通知給管理員。
- 故障轉(zhuǎn)移:如果 master node 掛掉了,會自動轉(zhuǎn)移到 slave node 上。
- 配置中心:如果故障轉(zhuǎn)移發(fā)生了,通知 client 客戶端新的 master 地址。
哨兵用于實現(xiàn) Redis 集群的高可用,本身也是分布式的,作為一個哨兵集群去運(yùn)行,互相協(xié)同工作。
- 故障轉(zhuǎn)移時,判斷一個 master node 是否宕機(jī)了,需要大部分的哨兵都同意才行,涉及到了分布式選舉的問題。
- 即使部分哨兵節(jié)點(diǎn)掛掉了,哨兵集群還是能正常工作的,因為如果一個作為高可用機(jī)制重要組成部分的故障轉(zhuǎn)移系統(tǒng)本身是單點(diǎn)的,那就很坑爹了。
哨兵的核心知識
- 哨兵至少需要 3 個實例,來保證自己的健壯性。
- 哨兵 + Redis 主從的部署架構(gòu),是不保證數(shù)據(jù)零丟失的,只能保證 Redis 集群的高可用性。
- 對于哨兵 + Redis 主從這種復(fù)雜的部署架構(gòu),盡量在測試環(huán)境和生產(chǎn)環(huán)境,都進(jìn)行充足的測試和演練。
哨兵集群必須部署 2 個以上節(jié)點(diǎn),如果哨兵集群僅僅部署了 2 個哨兵實例,quorum = 1。
配置
quorum=1
,如果 master 宕機(jī), s1 和 s2 中只要有 1 個哨兵認(rèn)為 master 宕機(jī)了,就可以進(jìn)行切換,同時 s1 和 s2 會選舉出一個哨兵來執(zhí)行故障轉(zhuǎn)移。但是同時這個時候,需要 majority,也就是大多數(shù)哨兵都是運(yùn)行的。如果此時僅僅是 M1 進(jìn)程宕機(jī)了,哨兵 s1 正常運(yùn)行,那么故障轉(zhuǎn)移是 OK 的。但是如果是整個 M1 和 S1 運(yùn)行的機(jī)器宕機(jī)了,那么哨兵只有 1 個,此時就沒有 majority 來允許執(zhí)行故障轉(zhuǎn)移,雖然另外一臺機(jī)器上還有一個 R1,但是故障轉(zhuǎn)移不會執(zhí)行。
配置
quorum=2
,如果 M1 所在機(jī)器宕機(jī)了,那么三個哨兵還剩下 2 個,S2 和 S3 可以一致認(rèn)為 master 宕機(jī)了,然后選舉出一個來執(zhí)行故障轉(zhuǎn)移,同時 3 個哨兵的 majority 是 2,所以還剩下的 2 個哨兵運(yùn)行著,就可以允許執(zhí)行故障轉(zhuǎn)移。 -
Redis哨兵是怎么工作的
- 每個Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他 Sentinel 實例發(fā)送一個 PING 命令。
- 如果一個實例(instance)距離最后一次有效回復(fù) PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 則這個實例會被當(dāng)前 Sentinel 標(biāo)記為主觀下線。
- 如果一個Master被標(biāo)記為主觀下線,則正在監(jiān)視這個Master的所有 Sentinel 要以每秒一次的頻率確認(rèn)Master的確進(jìn)入了主觀下線狀態(tài)。
- 當(dāng)有足夠數(shù)量的 Sentinel(大于等于配置文件指定的值)在指定的時間范圍內(nèi)確認(rèn)Master的確進(jìn)入了主觀下線狀態(tài), 則Master會被標(biāo)記為客觀下線 。
- 當(dāng)Master被 Sentinel 標(biāo)記為客觀下線時,Sentinel 向下線的 Master 的所有 Slave 發(fā)送 INFO 命令的頻率會從 10 秒一次改為每秒一次 (在一般情況下, 每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有Master,Slave發(fā)送 INFO 命令 )。
- 若沒有足夠數(shù)量的 Sentinel 同意 Master 已經(jīng)下線, Master 的客觀下線狀態(tài)就會變成主觀下線。若 Master 重新向 Sentinel 的 PING 命令返回有效回復(fù), Master 的主觀下線狀態(tài)就會被移除。
- sentinel節(jié)點(diǎn)會與其他sentinel節(jié)點(diǎn)進(jìn)行“溝通”,投票選舉一個sentinel節(jié)點(diǎn)進(jìn)行故障處理,在從節(jié)點(diǎn)中選取一個主節(jié)點(diǎn),其他從節(jié)點(diǎn)掛載到新的主節(jié)點(diǎn)上自動復(fù)制新主節(jié)點(diǎn)的數(shù)據(jù)。
-
哨兵主備切換的數(shù)據(jù)丟失問題
導(dǎo)致數(shù)據(jù)丟失的兩種情況
主備切換的過程,可能會導(dǎo)致數(shù)據(jù)丟失:
- 異步復(fù)制導(dǎo)致的數(shù)據(jù)丟失
因為 master->slave 的復(fù)制是異步的,所以可能有部分?jǐn)?shù)據(jù)還沒復(fù)制到 slave,master 就宕機(jī)了,此時這部分?jǐn)?shù)據(jù)就丟失了。
- 腦裂導(dǎo)致的數(shù)據(jù)丟失
腦裂,也就是說,某個 master 所在機(jī)器突然脫離了正常的網(wǎng)絡(luò),跟其他 slave 機(jī)器不能連接,但是實際上 master 還運(yùn)行著。此時哨兵可能就會認(rèn)為 master 宕機(jī)了,然后開啟選舉,將其他 slave 切換成了 master。這個時候,集群里就會有兩個 master ,也就是所謂的腦裂。
此時雖然某個 slave 被切換成了 master,但是可能 client 還沒來得及切換到新的 master,還繼續(xù)向舊 master 寫數(shù)據(jù)。因此舊 master 再次恢復(fù)的時候,會被作為一個 slave 掛到新的 master 上去,自己的數(shù)據(jù)會清空,重新從新的 master 復(fù)制數(shù)據(jù)。而新的 master 并沒有后來 client 寫入的數(shù)據(jù),因此,這部分?jǐn)?shù)據(jù)也就丟失了。
數(shù)據(jù)丟失問題的解決方案
進(jìn)行如下配置:
表示,要求至少有 1 個 slave,數(shù)據(jù)復(fù)制和同步的延遲不能超過 10 秒。
如果說一旦所有的 slave,數(shù)據(jù)復(fù)制和同步的延遲都超過了 10 秒鐘,那么這個時候,master 就不會再接收任何請求了。
- 減少異步復(fù)制數(shù)據(jù)的丟失
有了
min-slaves-max-lag
這個配置,就可以確保說,一旦 slave 復(fù)制數(shù)據(jù)和 ack 延時太長,就認(rèn)為可能 master 宕機(jī)后損失的數(shù)據(jù)太多了,那么就拒絕寫請求,這樣可以把 master 宕機(jī)時由于部分?jǐn)?shù)據(jù)未同步到 slave 導(dǎo)致的數(shù)據(jù)丟失降低的可控范圍內(nèi)。- 減少腦裂的數(shù)據(jù)丟失
如果一個 master 出現(xiàn)了腦裂,跟其他 slave 丟了連接,那么上面兩個配置可以確保說,如果不能繼續(xù)給指定數(shù)量的 slave 發(fā)送數(shù)據(jù),而且 slave 超過 10 秒沒有給自己 ack 消息,那么就直接拒絕客戶端的寫請求。因此在腦裂場景下,最多就丟失 10 秒的數(shù)據(jù)。
-
哨兵集群的自動發(fā)現(xiàn)機(jī)制
哨兵互相之間的發(fā)現(xiàn),是通過 Redis 的
pub/sub
系統(tǒng)實現(xiàn)的,每個哨兵都會往__sentinel__:hello
這個 channel 里發(fā)送一個消息,這時候所有其他哨兵都可以消費(fèi)到這個消息,并感知到其他的哨兵的存在。每隔兩秒鐘,每個哨兵都會往自己監(jiān)控的某個 master+slaves 對應(yīng)的
__sentinel__:hello
channel 里發(fā)送一個消息,內(nèi)容是自己的 host、ip 和 runid 還有對這個 master 的監(jiān)控配置。每個哨兵也會去監(jiān)聽自己監(jiān)控的每個 master+slaves 對應(yīng)的
__sentinel__:hello
channel,然后去感知到同樣在監(jiān)聽這個 master+slaves 的其他哨兵的存在。每個哨兵還會跟其他哨兵交換對
master
的監(jiān)控配置,互相進(jìn)行監(jiān)控配置的同步。 -
Redis 如何實現(xiàn)服務(wù)高可用
要想設(shè)計一個高可用的 Redis 服務(wù),一定要從 Redis 的多服務(wù)節(jié)點(diǎn)來考慮,比如 Redis 的主從復(fù)制、哨兵模式、切片集群。
主從復(fù)制
主從復(fù)制是 Redis 高可用服務(wù)的最基礎(chǔ)的保證,實現(xiàn)方案就是將從前的一臺 Redis 服務(wù)器,同步數(shù)據(jù)到多臺從 Redis 服務(wù)器上,即一主多從的模式,且主從服務(wù)器之間采用的是「讀寫分離」的方式。
主服務(wù)器可以進(jìn)行讀寫操作,當(dāng)發(fā)生寫操作時自動將寫操作同步給從服務(wù)器,而從服務(wù)器一般是只讀,并接受主服務(wù)器同步過來寫操作命令,然后執(zhí)行這條命令。
也就是說,所有的數(shù)據(jù)修改只在主服務(wù)器上進(jìn)行,然后將最新的數(shù)據(jù)同步給從服務(wù)器,這樣就使得主從服務(wù)器的數(shù)據(jù)是一致的。
注意,主從服務(wù)器之間的命令復(fù)制是異步進(jìn)行的。
具體來說,在主從服務(wù)器命令傳播階段,主服務(wù)器收到新的寫命令后,會發(fā)送給從服務(wù)器。但是,主服務(wù)器并不會等到從服務(wù)器實際執(zhí)行完命令后,再把結(jié)果返回給客戶端,而是主服務(wù)器自己在本地執(zhí)行完命令后,就會向客戶端返回結(jié)果了。如果從服務(wù)器還沒有執(zhí)行主服務(wù)器同步過來的命令,主從服務(wù)器間的數(shù)據(jù)就不一致了。
所以,無法實現(xiàn)強(qiáng)一致性保證(主從數(shù)據(jù)時時刻刻保持一致),數(shù)據(jù)不一致是難以避免的。
哨兵模式
在使用 Redis 主從服務(wù)的時候,會有一個問題,就是當(dāng) Redis 的主從服務(wù)器出現(xiàn)故障宕機(jī)時,需要手動進(jìn)行恢復(fù)。
為了解決這個問題,Redis 增加了哨兵模式(Redis Sentinel),因為哨兵模式做到了可以監(jiān)控主從服務(wù)器,并且提供主從節(jié)點(diǎn)故障轉(zhuǎn)移的功能。
切片集群模式
當(dāng) Redis 緩存數(shù)據(jù)量大到一臺服務(wù)器無法緩存時,就需要使用 Redis 切片集群(Redis Cluster )方案,它將數(shù)據(jù)分布在不同的服務(wù)器上,以此來降低系統(tǒng)對單主節(jié)點(diǎn)的依賴,從而提高 Redis 服務(wù)的讀寫性能。
Redis Cluster 方案采用哈希槽(Hash Slot),來處理數(shù)據(jù)和節(jié)點(diǎn)之間的映射關(guān)系。在 Redis Cluster 方案中,一個切片集群共有 16384 個哈希槽,這些哈希槽類似于數(shù)據(jù)分區(qū),每個鍵值對都會根據(jù)它的 key,被映射到一個哈希槽中,具體執(zhí)行過程分為兩大步:
- 根據(jù)鍵值對的 key,按照 CRC16 算法 (opens new window)計算一個 16 bit 的值。
- 再用 16bit 值對 16384 取模,得到 0~16383 范圍內(nèi)的模數(shù),每個模數(shù)代表一個相應(yīng)編號的哈希槽。
接下來的問題就是,這些哈希槽怎么被映射到具體的 Redis 節(jié)點(diǎn)上的呢?有兩種方案:
- 平均分配: 在使用 cluster create 命令創(chuàng)建 Redis 集群時,Redis 會自動把所有哈希槽平均分布到集群節(jié)點(diǎn)上。比如集群中有 9 個節(jié)點(diǎn),則每個節(jié)點(diǎn)上槽的個數(shù)為 16384/9 個。
- 手動分配: 可以使用 cluster meet 命令手動建立節(jié)點(diǎn)間的連接,組成集群,再使用 cluster addslots 命令,指定每個節(jié)點(diǎn)上的哈希槽個數(shù)。
然后在集群運(yùn)行的過程中,key1 和 key2 計算完 CRC16 值后,對哈希槽總個數(shù) 4 進(jìn)行取模,再根據(jù)各自的模數(shù)結(jié)果,就可以被映射到哈希槽 1(對應(yīng)節(jié)點(diǎn)1) 和 哈希槽 2(對應(yīng)節(jié)點(diǎn)2)。
需要注意的是,在手動分配哈希槽時,需要把 16384 個槽都分配完,否則 Redis 集群無法正常工作
6.4 緩存
-
緩存雪崩、緩存穿透、緩存預(yù)熱、緩存更新、緩存擊穿、緩存降級全搞定!
緩存雪崩
緩存雪崩指的是緩存同一時間大面積的失效,所以,后面的請求都會落到數(shù)據(jù)庫上,造成數(shù)據(jù)庫短時間內(nèi)承受大量請求而崩掉。
看不懂?那我說人話。
我們可以簡單的理解為:由于原有緩存失效,新緩存未到期間(例如:我們設(shè)置緩存時采用了相同的過期時間,在同一時刻出現(xiàn)大面積的緩存過期),所有原本應(yīng)該訪問緩存的請求都去查詢數(shù)據(jù)庫了,而對數(shù)據(jù)庫CPU和內(nèi)存造成巨大壓力,嚴(yán)重的會造成數(shù)據(jù)庫宕機(jī),從而形成一系列連鎖反應(yīng),造成整個系統(tǒng)崩潰。
解決辦法
- 事前:盡量保證整個 Redis 集群的高可用性,發(fā)現(xiàn)機(jī)器宕機(jī)盡快補(bǔ)上,選擇合適的內(nèi)存淘汰策略。
- 事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL崩掉, 通過加鎖或者隊列來控制讀數(shù)據(jù)庫寫緩存的線程數(shù)量。比如對某個key只允許一個線程查詢數(shù)據(jù)和寫緩存,其他線程等待。
- 事后:利用 Redis 持久化機(jī)制保存的數(shù)據(jù)盡快恢復(fù)緩存
緩存穿透
一般是黑客故意去請求緩存中不存在的數(shù)據(jù),導(dǎo)致所有的請求都落到數(shù)據(jù)庫上,造成數(shù)據(jù)庫短時間內(nèi)承受大量 請求而崩掉。
這也看不懂?那我再換個說法好了。
緩存穿透是指查詢一個一定不存在的數(shù)據(jù),由于緩存不命中,接著查詢數(shù)據(jù)庫也無法查詢出結(jié)果,因此也不會寫入到緩存中,這將會導(dǎo)致每個查詢都會去請求數(shù)據(jù)庫,造成緩存穿透。
解決辦法
1、布隆過濾器
這是最常見的一種解決方法了,它是將所有可能存在的數(shù)據(jù)哈希到一個足夠大的bitmap中,一個一定不存在的數(shù)據(jù)會被 這個bitmap攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓 力。
對所有可能查詢的參數(shù)以hash形式存儲,在控制層先進(jìn)行校驗,不符合則丟棄,從而避免了對底層存儲系統(tǒng)的查詢壓力;
這里稍微科普一下布隆過濾器。
布隆過濾器是引入了k(k>1)k(k>1)個相互獨(dú)立的哈希函數(shù),保證在給定的空間、誤判率下,完成元素判重的過程。 它的優(yōu)點(diǎn)是空間效率和查詢時間都遠(yuǎn)遠(yuǎn)超過一般的算法,缺點(diǎn)是有一定的誤識別率和刪除困難。
該算法的核心思想就是利用多個不同的Hash函數(shù)來解決“沖突”。Hash存在一個沖突(碰撞)的問題,用同一個Hash得到的兩個URL的值有可能相同。為了減少沖突,我們可以多引入幾個Hash,如果通過其中的一個Hash值我們得出某元素不在集合中,那么該元素肯定不在集合中。只有在所有的Hash函數(shù)告訴我們該元素在集合中時,才能確定該元素存在于集合中。這便是布隆過濾器的基本思想,一般用于在大數(shù)據(jù)量的集合中判定某元素是否存在。
2、緩存空對象
當(dāng)存儲層不命中后,即使返回的空對象也將其緩存起來,同時會設(shè)置一個過期時間,之后再訪問這個數(shù)據(jù)將會從緩存中獲取,保護(hù)了后端數(shù)據(jù)源;如果一個查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存 在,還是系統(tǒng)故障),我們?nèi)匀话堰@個空結(jié)果進(jìn)行緩存,但它的過期時間會很短,最長不超過五分鐘。
但是這種方法會存在兩個問題:
1、如果空值能夠被緩存起來,這就意味著緩存需要更多的空間存儲更多的鍵,因為這當(dāng)中可能會有很多的空值的鍵;
2、即使對空值設(shè)置了過期時間,還是會存在緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致,這對于需要保持一致性的業(yè)務(wù)會有影響。
我們可以從適用場景和維護(hù)成本兩方面對這兩匯總方法進(jìn)行一個簡單比較:
適用場景:緩存空對象適用于1、數(shù)據(jù)命中不高 2、數(shù)據(jù)頻繁變化且實時性較高 ;而布隆過濾器適用1、數(shù)據(jù)命中不高 2、數(shù)據(jù)相對固定即實時性較低
維護(hù)成本:緩存空對象的方法適合1、代碼維護(hù)簡單 2、需要較多的緩存空間 3、數(shù)據(jù)會出現(xiàn)不一致的現(xiàn)象;布隆過濾器適合 1、代碼維護(hù)較復(fù)雜 2、緩存空間要少一些
緩存預(yù)熱
緩存預(yù)熱是指系統(tǒng)上線后,將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。這樣就可以避免在用戶請求的時候,先查詢數(shù)據(jù)庫,然后再將數(shù)據(jù)緩存的問題。用戶會直接查詢事先被預(yù)熱的緩存數(shù)據(jù)!
解決思路 1、直接寫個緩存刷新頁面,上線時手工操作下; 2、數(shù)據(jù)量不大,可以在項目啟動的時候自動進(jìn)行加載; 3、定時刷新緩存;
緩存更新
除了緩存服務(wù)器自帶的緩存失效策略之外(Redis默認(rèn)的有6中策略可供選擇),我們還可以根據(jù)具體的業(yè)務(wù)需求進(jìn)行自定義的緩存淘汰,常見的策略有兩種:定時刪除和惰性刪除,其中: (1)定時刪除:定時去清理過期的緩存; (2)惰性刪除:當(dāng)有用戶請求過來時,再判斷這個請求所用到的緩存是否過期,過期的話就去底層系統(tǒng)得到新數(shù)據(jù)并更新緩存。 兩者各有優(yōu)劣,第一種的缺點(diǎn)是維護(hù)大量緩存的key是比較麻煩的,第二種的缺點(diǎn)就是每次用戶請求過來都要判斷緩存失效,邏輯相對比較復(fù)雜!具體用哪種方案,大家可以根據(jù)自己的應(yīng)用場景來權(quán)衡。
緩存擊穿
緩存擊穿,是指一個key非常熱點(diǎn),在不停的扛著大并發(fā),大并發(fā)集中對這一個點(diǎn)進(jìn)行訪問,當(dāng)這個key在失效瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請求數(shù)據(jù)庫,就像在一個屏障上鑿開一個洞。
比如常見的電商項目中,某些貨物成為“爆款”了,可以對一些主打商品的緩存直接設(shè)置為永不過期。即便某些商品自己發(fā)酵成了爆款,也是直接設(shè)為永不過期就好了。mutex key互斥鎖基本上是用不上的,有個詞叫做大道至簡。
緩存降級
當(dāng)訪問量劇增、服務(wù)出現(xiàn)問題(如響應(yīng)時間慢或不響應(yīng))或非核心服務(wù)影響到核心流程的性能時,仍然需要保證服務(wù)還是可用的,即使是有損服務(wù)。系統(tǒng)可以根據(jù)一些關(guān)鍵數(shù)據(jù)進(jìn)行自動降級,也可以配置開關(guān)實現(xiàn)人工降級。 降級的最終目的是保證核心服務(wù)可用,即使是有損的。而且有些服務(wù)是無法降級的(如加入購物車、結(jié)算)。 以參考日志級別設(shè)置預(yù)案: (1)一般:比如有些服務(wù)偶爾因為網(wǎng)絡(luò)抖動或者服務(wù)正在上線而超時,可以自動降級; (2)警告:有些服務(wù)在一段時間內(nèi)成功率有波動(如在95~100%之間),可以自動降級或人工降級,并發(fā)送告警; (3)錯誤:比如可用率低于90%,或者數(shù)據(jù)庫連接池被打爆了,或者訪問量突然猛增到系統(tǒng)能承受的最大閥值,此時可以根據(jù)情況自動降級或者人工降級; (4)嚴(yán)重錯誤:比如因為特殊原因數(shù)據(jù)錯誤了,此時需要緊急人工降級。
服務(wù)降級的目的,是為了防止Redis服務(wù)故障,導(dǎo)致數(shù)據(jù)庫跟著一起發(fā)生雪崩問題。因此,對于不重要的緩存數(shù)據(jù),可以采取服務(wù)降級策略,例如一個比較常見的做法就是,Redis出現(xiàn)問題,不去數(shù)據(jù)庫查詢,而是直接返回默認(rèn)值給用戶。
-
數(shù)據(jù)庫和緩存如何保證一致性
一般來說,就是如果你的系統(tǒng)不是嚴(yán)格要求緩存+數(shù)據(jù)庫必須一致性的話,緩存可以稍微的跟數(shù)據(jù)庫偶爾有不一致的 情況,最好不要做這個方案,最好將讀請求和寫請求串行化,串到一個內(nèi)存隊列里去,這樣就可以保證一定不會出現(xiàn)不一致的情況。
串行化之后,就會導(dǎo)致系統(tǒng)的吞吐量會大幅度的降低,用比正常情況下多幾倍的機(jī)器去支撐線上的一個請求。
最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,就是 預(yù)留緩存模式Cache Aside Pattern。
- 讀的時候,先讀緩存,緩存沒有的話,就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)。
- 更新的時候,先刪除緩存,然后再更新數(shù)據(jù)庫,這樣讀的時候就會發(fā)現(xiàn)緩存中沒有數(shù)據(jù)而直接去數(shù)據(jù)庫中拿數(shù)據(jù)了。(因為要刪除,狗日的編輯器可能會背著你做一些優(yōu)化,要徹底將緩存中的數(shù)據(jù)進(jìn)行刪除才行)
在高并發(fā)的業(yè)務(wù)場景下,數(shù)據(jù)庫的性能瓶頸往往都是用戶并發(fā)訪問過大。所以,一般都使用Redis做一個緩沖操作,讓請求先訪問到Redis,而不是直接去訪問MySQL等數(shù)據(jù)庫,從而減少網(wǎng)絡(luò)請求的延遲響應(yīng)。
-
如何保證緩存與數(shù)據(jù)庫的雙寫一致性
你只要用緩存,就可能會涉及到緩存與數(shù)據(jù)庫雙存儲雙寫,你只要是雙寫,就一定會有數(shù)據(jù)一致性的問題,那么你如何解決一致性問題?
Cache Aside Pattern
最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,就是 Cache Aside Pattern。
- 讀的時候,先讀緩存,緩存沒有的話,就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)。
- 更新的時候,先更新數(shù)據(jù)庫,然后再刪除緩存。
為什么是刪除緩存,而不是更新緩存?
原因很簡單,很多時候,在復(fù)雜點(diǎn)的緩存場景,緩存不單單是數(shù)據(jù)庫中直接取出來的值。
比如可能更新了某個表的一個字段,然后其對應(yīng)的緩存,是需要查詢另外兩個表的數(shù)據(jù)并進(jìn)行運(yùn)算,才能計算出緩存最新的值的。
另外更新緩存的代價有時候是很高的。是不是說,每次修改數(shù)據(jù)庫的時候,都一定要將其對應(yīng)的緩存更新一份?也許有的場景是這樣,但是對于比較復(fù)雜的緩存數(shù)據(jù)計算的場景,就不是這樣了。如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。但是問題在于,這個緩存到底會不會被頻繁訪問到?
其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都重新做復(fù)雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。
最初級的緩存不一致問題及解決方案
問題:先更新數(shù)據(jù)庫,再刪除緩存。如果刪除緩存失敗了,那么會導(dǎo)致數(shù)據(jù)庫中是新數(shù)據(jù),緩存中是舊數(shù)據(jù),數(shù)據(jù)就出現(xiàn)了不一致。
解決思路 1:先刪除緩存,再更新數(shù)據(jù)庫。如果數(shù)據(jù)庫更新失敗了,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存中是空的,那么數(shù)據(jù)不會不一致。因為讀的時候緩存沒有,所以去讀了數(shù)據(jù)庫中的舊數(shù)據(jù),然后更新到緩存中。
解決思路 2:延時雙刪。依舊是先更新數(shù)據(jù)庫,再刪除緩存,唯一不同的是,我們把這個刪除的動作,在不久之后再執(zhí)行一次,比如 5s 之后。
刪除的動作,可以有多種選擇,比如:1. 使用
DelayQueue
,會隨著 JVM 進(jìn)程的死亡,丟失更新的風(fēng)險;2. 放在MQ
,但編碼復(fù)雜度為增加。總之,我們需要綜合各種因素去做設(shè)計,選擇一個最合理的解決方案。比較復(fù)雜的數(shù)據(jù)不一致問題分析
數(shù)據(jù)發(fā)生了變更,先刪除了緩存,然后要去修改數(shù)據(jù)庫,此時還沒修改。一個請求過來,去讀緩存,發(fā)現(xiàn)緩存空了,去查詢數(shù)據(jù)庫,查到了修改前的舊數(shù)據(jù),放到了緩存中。隨后數(shù)據(jù)變更的程序完成了數(shù)據(jù)庫的修改。完了,數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一樣了…
為什么上億流量高并發(fā)場景下,緩存會出現(xiàn)這個問題?
只有在對一個數(shù)據(jù)在并發(fā)的進(jìn)行讀寫的時候,才可能會出現(xiàn)這種問題。其實如果說你的并發(fā)量很低的話,特別是讀并發(fā)很低,每天訪問量就 1 萬次,那么很少的情況下,會出現(xiàn)剛才描述的那種不一致的場景。但是問題是,如果每天的是上億的流量,每秒并發(fā)讀是幾萬,每秒只要有數(shù)據(jù)更新的請求,就可能會出現(xiàn)上述的數(shù)據(jù)庫+緩存不一致的情況。
解決方案如下:
更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標(biāo)識,將操作路由之后,發(fā)送到一個 jvm 內(nèi)部隊列中。讀取數(shù)據(jù)的時候,如果發(fā)現(xiàn)數(shù)據(jù)不在緩存中,那么將重新執(zhí)行“讀取數(shù)據(jù)+更新緩存”的操作,根據(jù)唯一標(biāo)識路由之后,也發(fā)送到同一個 jvm 內(nèi)部隊列中。
一個隊列對應(yīng)一個工作線程,每個工作線程串行拿到對應(yīng)的操作,然后一條一條的執(zhí)行。這樣的話,一個數(shù)據(jù)變更的操作,先刪除緩存,然后再去更新數(shù)據(jù)庫,但是還沒完成更新。此時如果一個讀請求過來,沒有讀到緩存,那么可以先將緩存更新的請求發(fā)送到隊列中,此時會在隊列中積壓,然后同步等待緩存更新完成。
這里有一個優(yōu)化點(diǎn),一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發(fā)現(xiàn)隊列中已經(jīng)有一個更新緩存的請求了,那么就不用再放個更新請求操作進(jìn)去了,直接等待前面的更新操作請求完成即可。
待那個隊列對應(yīng)的工作線程完成了上一個操作的數(shù)據(jù)庫的修改之后,才會去執(zhí)行下一個操作,也就是緩存更新的操作,此時會從數(shù)據(jù)庫中讀取最新的值,然后寫入緩存中。
如果請求還在等待時間范圍內(nèi),不斷輪詢發(fā)現(xiàn)可以取到值了,那么就直接返回;如果請求等待的時間超過一定時長,那么這一次直接從數(shù)據(jù)庫中讀取當(dāng)前的舊值。
高并發(fā)的場景下,該解決方案要注意的問題:
- 讀請求長時阻塞
由于讀請求進(jìn)行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內(nèi)返回。
該解決方案,最大的風(fēng)險點(diǎn)在于說,可能數(shù)據(jù)更新很頻繁,導(dǎo)致隊列中積壓了大量更新操作在里面,然后讀請求會發(fā)生大量的超時,最后導(dǎo)致大量的請求直接走數(shù)據(jù)庫。務(wù)必通過一些模擬真實的測試,看看更新數(shù)據(jù)的頻率是怎樣的。
另外一點(diǎn),因為一個隊列中,可能會積壓針對多個數(shù)據(jù)項的更新操作,因此需要根據(jù)自己的業(yè)務(wù)情況進(jìn)行測試,可能需要部署多個服務(wù),每個服務(wù)分?jǐn)傄恍?shù)據(jù)的更新操作。如果一個內(nèi)存隊列里居然會擠壓 100 個商品的庫存修改操作,每個庫存修改操作要耗費(fèi) 10ms 去完成,那么最后一個商品的讀請求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到數(shù)據(jù),這個時候就導(dǎo)致讀請求的長時阻塞。
一定要做根據(jù)實際業(yè)務(wù)系統(tǒng)的運(yùn)行情況,去進(jìn)行一些壓力測試,和模擬線上環(huán)境,去看看最繁忙的時候,內(nèi)存隊列可能會擠壓多少更新操作,可能會導(dǎo)致最后一個更新操作對應(yīng)的讀請求,會 hang 多少時間,如果讀請求在 200ms 返回,如果你計算過后,哪怕是最繁忙的時候,積壓 10 個更新操作,最多等待 200ms,那還可以的。
如果一個內(nèi)存隊列中可能積壓的更新操作特別多,那么你就要加機(jī)器,讓每個機(jī)器上部署的服務(wù)實例處理更少的數(shù)據(jù),那么每個內(nèi)存隊列中積壓的更新操作就會越少。
其實根據(jù)之前的項目經(jīng)驗,一般來說,數(shù)據(jù)的寫頻率是很低的,因此實際上正常來說,在隊列中積壓的更新操作應(yīng)該是很少的。像這種針對讀高并發(fā)、讀緩存架構(gòu)的項目,一般來說寫請求是非常少的,每秒的 QPS 能到幾百就不錯了。
- 讀請求并發(fā)量過高
這里還必須做好壓力測試,確保恰巧碰上上述情況的時候,還有一個風(fēng)險,就是突然間大量讀請求會在幾十毫秒的延時 hang 在服務(wù)上,看服務(wù)能不能扛的住,需要多少機(jī)器才能扛住最大的極限情況的峰值。
但是因為并不是所有的數(shù)據(jù)都在同一時間更新,緩存也不會同一時間失效,所以每次可能也就是少數(shù)數(shù)據(jù)的緩存失效了,然后那些數(shù)據(jù)對應(yīng)的讀請求過來,并發(fā)量應(yīng)該也不會特別大。
- 多服務(wù)實例部署的請求路由
可能這個服務(wù)部署了多個實例,那么必須保證說,執(zhí)行數(shù)據(jù)更新操作,以及執(zhí)行緩存更新操作的請求,都通過 Nginx 服務(wù)器路由到相同的服務(wù)實例上。
比如說,對同一個商品的讀寫請求,全部路由到同一臺機(jī)器上??梢宰约喝プ龇?wù)間的按照某個請求參數(shù)的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
- 熱點(diǎn)商品的路由問題,導(dǎo)致請求的傾斜
萬一某個商品的讀寫請求特別高,全部打到相同的機(jī)器的相同的隊列里面去了,可能會造成某臺機(jī)器的壓力過大。就是說,因為只有在商品數(shù)據(jù)更新的時候才會清空緩存,然后才會導(dǎo)致讀寫并發(fā),所以其實要根據(jù)業(yè)務(wù)系統(tǒng)去看,如果更新頻率不是太高的話,這個問題的影響并不是特別大,但是的確可能某些機(jī)器的負(fù)載會高一些。
-
常見的數(shù)據(jù)優(yōu)化方案
一、緩存雙淘汰法
- 先淘汰緩存
- 再寫數(shù)據(jù)庫
- 往消息總線esb發(fā)送一個淘汰消息,發(fā)送立即返回。寫請求的處理時間幾乎沒有增加,這個方法淘汰了緩存兩次。因此被稱為“緩存雙淘汰法“,而在消息總線下游,有一個異步淘汰緩存的消費(fèi)者,在拿到淘汰消息在1s后淘汰緩存,這樣,即使在一秒內(nèi)有臟數(shù)據(jù)入緩存,也能夠被淘汰掉。
二、異步淘汰緩存
上述的步驟,都是在業(yè)務(wù)線里面執(zhí)行,新增一個線下的讀取binlog異步淘汰緩存模塊,讀取binlog總的數(shù)據(jù),然后進(jìn)行異步淘汰。
這里簡單提供一個思路
1.思路:
MySQL binlog增量發(fā)布訂閱消費(fèi)+消息隊列+增量數(shù)據(jù)更新到Redis
1)讀請求走Redis:熱數(shù)據(jù)基本都在Redis
2)寫請求走M(jìn)ySQL: 增刪改都操作MySQL
3)更新Redis數(shù)據(jù):MySQ的數(shù)據(jù)操作binlog,來更新到Redis
2.Redis更新
1)數(shù)據(jù)操作主要分為兩塊:
- 一個是全量(將全部數(shù)據(jù)一次寫入到Redis)
- 一個是增量(實時更新)
這里說的是增量,指的是mysql的update、insert、delate變更數(shù)據(jù)。這樣一旦MySQL中產(chǎn)生了新的寫入、更新、刪除等操作,就可以把binlog相關(guān)的消息推送至Redis,Redis再根據(jù)binlog中的記錄,對Redis進(jìn)行更新,就無需在從業(yè)務(wù)線去操作緩存內(nèi)容。
7.熟悉使用 Python 語言
7.1 Django框架
-
Django的優(yōu)點(diǎn)和缺點(diǎn)
-
什么MVC、MTV模式
M:Model,模型,和數(shù)據(jù)庫進(jìn)行交互
V:View,視圖,負(fù)責(zé)產(chǎn)生 Html 頁面
C:Controller,控制器,接收請求,進(jìn)行處理,與 M 和 V 進(jìn)行交互,返回應(yīng)答。
M:Model,模型,和 MVC 中的 M 功能相同,和數(shù)據(jù)庫進(jìn)行交互
V:view,視圖,和 MVC 中的 C 功能相同,接收請求,進(jìn)行處理,與 M 和 T 進(jìn)行交互,返回應(yīng)答
T:Template,模板,和 MVC 中的 V 功能相同,產(chǎn)生 Html 頁面
-
Django中間件
Django 在中間件中預(yù)置了六個方法,這六個方法的區(qū)別在于不同的階段執(zhí)行,對輸入或輸出進(jìn)行干預(yù),方法如下:
1.初始化:無需任何參數(shù),服務(wù)器響應(yīng)第一個請求的時候調(diào)用一次,用于確定是否啟用當(dāng)前中間件。
2.處理請求前:在每個請求上調(diào)用,返回 None 或 HttpResponse 對象。
3.處理視圖前:在每個請求上調(diào)用,返回 None 或 HttpResponse 對象。
4.處理模板響應(yīng)前:在每個請求上調(diào)用,返回實現(xiàn)了 render 方法的響應(yīng)對象。
5.處理響應(yīng)后:所有響應(yīng)返回瀏覽器之前被調(diào)用,在每個請求上調(diào)用,返回 HttpResponse 對象。
6.異常處理:當(dāng)視圖拋出異常時調(diào)用,在每個請求上調(diào)用,返回一個 HttpResponse 對象。
-
Django如何提升性能
對一個后端開發(fā)程序員來說,提升性能指標(biāo)主要有兩個一個是并發(fā)數(shù),另一個是響應(yīng)時間網(wǎng)站性能的優(yōu)化一般包括 web 前端性能優(yōu)化,應(yīng)用服務(wù)器性能優(yōu)化,存儲服務(wù)器優(yōu)化。
(1)合理的使用緩存技術(shù),對一些常用到的動態(tài)數(shù)據(jù),比如首頁做一個緩存,或者某些常用的數(shù)據(jù)做個緩存,設(shè)置一定得過期時間,這樣減少了對數(shù)據(jù)庫的壓力,提升網(wǎng)站性能。
(2) 使用 celery 消息隊列,將耗時的操作扔到隊列里,讓 worker 去監(jiān)聽隊列里的任務(wù),實現(xiàn)異步操 作,比如發(fā)郵件,發(fā)短信。
(3) 就是代碼上的一些優(yōu)化,補(bǔ)充:nginx 部署項目也是項目優(yōu)化,可以配置合適的配置參數(shù),提升效率,增加并發(fā)量。
(4) 如果太多考慮安全因素,服務(wù)器磁盤用固態(tài)硬盤讀寫,遠(yuǎn)遠(yuǎn)大于機(jī)械硬盤,這個技術(shù)現(xiàn)在沒有普及,主要是固態(tài)硬盤技術(shù)上還不是完全成熟, 相信以后會大量普及。
(5) 另外還可以搭建服務(wù)器集群,將并發(fā)訪問請求,分散到多臺服務(wù)器上處理。
7.2 爬蟲
-
你用過的爬蟲框架或者模塊有哪些?談?wù)勊麄兊膮^(qū)別或者優(yōu)缺點(diǎn)?
(1) Python 自帶:urllib、urllib2
urllib 和 urllib2 模塊都做與請求 URL 相關(guān)的操作,但他們提供不同的功能。
urllib2:urllib2.urlopen 可以接受一個 Request 對象或者 url,(在接受 Request 對象時候,并以此可以來設(shè)置一個 URL 的 headers),urllib.urlopen 只接收一個 url。
urllib 有 urlencode,urllib2 沒有,因此總是 urllib,urllib2 常會一起使用的原因。
(2) 第三方:requests
request 是一個 HTTP 庫, 它只是用來,進(jìn)行請求,對于 HTTP 請求,他是一個強(qiáng)大的庫,下載,解析全部自己處理,靈活性更高,高并發(fā)與分布式部署也非常靈活,對于功能可以更好實現(xiàn)
(3) 框架: Scrapy
scrapy 是封裝起來的框架,它包含了下載器,解析器,日志及異常處理,基于多線程,twisted 的方式處理,對于固定單個網(wǎng)站的爬取開發(fā),有優(yōu)勢,但是對于多網(wǎng)站爬取,并發(fā)及分布式處理方面,不夠靈活,不便調(diào)整與括展。
Scrapy 優(yōu)點(diǎn)
scrapy 是異步的
采取可讀性更強(qiáng)的 xpath 代替正則
強(qiáng)大的統(tǒng)計和 log 系統(tǒng)
同時在不同的 url 上爬行
支持 shell 方式,方便獨(dú)立調(diào)試
寫 middleware,方便寫一些統(tǒng)一的過濾器
通過管道的方式存入數(shù)據(jù)庫
Scrapy 缺點(diǎn)
基于 python 的爬蟲框架,擴(kuò)展性比較差
基于 twisted 框架,運(yùn)行中的 exception 是不會干掉 reactor,并且異步框架出錯后是不會停掉其他任務(wù)的,數(shù)據(jù)出錯后難以察覺。
-
你做過什么爬蟲案例
- 爬取網(wǎng)易云音樂
- 爬取豆瓣電影TOP100
- 爬蟲淘寶商品信息
-
常見的反爬蟲和應(yīng)對方法?
(1) 通過 Headers 反爬蟲
從用戶請求的 Headers 反爬蟲是最常見的反爬蟲策略。很多網(wǎng)站都會對 Headers 的 User-Agent 進(jìn)行檢測,還有一部分網(wǎng)站會對 Referer 進(jìn)行檢測(一些資源網(wǎng)站的防盜鏈就是檢測 Referer)。如果遇到了這類反爬蟲機(jī)制,可以直接在爬蟲中添加 Headers,將瀏覽器的 User-Agent 復(fù)制到爬蟲的 Headers 中;或者將 Referer 值修改為目標(biāo)網(wǎng)站域名。對于檢測 Headers 的反爬蟲,在爬蟲中修改或者添加 Headers 就能很好的繞過。
(2) 基于用戶行為反爬蟲
還有一部分網(wǎng)站是通過檢測用戶行為,例如同一 IP 短時間內(nèi)多次訪問同一頁面,或者同一賬戶短時間內(nèi)多次進(jìn)行相同操作。
大多數(shù)網(wǎng)站都是前一種情況,對于這種情況,使用 IP 代理就可以解決。可以專門寫一個爬蟲,爬取網(wǎng)上公開的代理 ip,檢測后全部保存起來。這樣的代理 ip 爬蟲經(jīng)常會用到,最好自己準(zhǔn)備一個。有了大量代理 ip 后可以每請求幾次更換一個 ip,這在 requests 或者 urllib2 中很容易做到,這樣就能很容易的繞過第一種反爬蟲。
對于第二種情況,可以在每次請求后隨機(jī)間隔幾秒再進(jìn)行下一次請求。有些有邏輯漏洞的網(wǎng)站,可以通過請求幾次,退出登錄,重新登錄,繼續(xù)請求來繞過同一賬號短時間內(nèi)不能多次進(jìn)行相同請求的限制。
(3) 動態(tài)頁面的反爬蟲
上述的幾種情況大多都是出現(xiàn)在靜態(tài)頁面,還有一部分網(wǎng)站,我們需要爬取的數(shù)據(jù)是通過 ajax 請求得到,或者通過 JavaScript 生成的。首先用 Fiddler 對網(wǎng)絡(luò)請求進(jìn)行分析。如果能夠找到 ajax 請求,也能分析出具體的參數(shù)和響應(yīng)的具體含義,我們就能采用上面的方法,直接利用 requests 或者 urllib2模擬 ajax 請求,對響應(yīng)的 json 進(jìn)行分析得到需要的數(shù)據(jù)。
能夠直接模擬 ajax 請求獲取數(shù)據(jù)固然是極好的,但是有些網(wǎng)站把 ajax 請求的所有參數(shù)全部加密了。我們根本沒辦法構(gòu)造自己所需要的數(shù)據(jù)的請求。這種情況下就用 selenium+phantomJS,調(diào)用瀏覽器內(nèi)核,并利用 phantomJS 執(zhí)行 js 來模擬人為操作以及觸發(fā)頁面中的 js 腳本。從填寫表單到點(diǎn)擊按鈕再到滾動頁面,全部都可以模擬,不考慮具體的請求和響應(yīng)過程,只是完完整整的把人瀏覽頁面獲取數(shù)據(jù)的過程模擬一遍。
用這套框架幾乎能繞過大多數(shù)的反爬蟲,因為它不是在偽裝成瀏覽器來獲取數(shù)據(jù)(上述的通過添加Headers 一定程度上就是為了偽裝成瀏覽器),它本身就是瀏覽器,phantomJS 就是一個沒有界面的瀏覽器,只是操控這個瀏覽器的不是人。利 selenium+phantomJS 能干很多事情,例如識別點(diǎn)觸式(12306)或者滑動式的驗證碼,對頁面表單進(jìn)行暴力破解等。
-
驗證碼的解決
(1) 圖形驗證碼
干擾、雜色不是特別多的圖片可以使用開源庫 Tesseract 進(jìn)行識別,太過復(fù)雜的需要借助第三方打碼平臺。
(2) 滑塊驗證碼
點(diǎn)擊和拖動滑塊驗證碼可以借助 selenium、無圖形界面瀏覽器(chromedirver 或者 phantomjs)和pillow 包來模擬人的點(diǎn)擊和滑動操作,pillow 可以根據(jù)色差識別需要滑動的位置。
7.3 數(shù)據(jù)分析
-
實現(xiàn)數(shù)據(jù)分析的方法
使用numpy和pandas進(jìn)行數(shù)據(jù)處理,使用matplotlib庫實現(xiàn)數(shù)據(jù)可視化
-
數(shù)據(jù)分析項目
7.4 機(jī)器學(xué)習(xí)算法
-
常見的機(jī)器學(xué)習(xí)算法有哪些
KNN算法、線性回歸法、決策樹算法、隨機(jī)森林算法
-
什么是機(jī)器學(xué)習(xí)
簡單的說,機(jī)器學(xué)習(xí)就是讓機(jī)器從數(shù)據(jù)中學(xué)習(xí),進(jìn)而得到一個更加符合現(xiàn)實規(guī)律的模型,通過對模型的使用使得機(jī)器比以往表現(xiàn)的更好,這就是機(jī)器學(xué)習(xí)。
對上面這句話的理解:
數(shù)據(jù):從現(xiàn)實生活抽象出來的一些事物或者規(guī)律的特征進(jìn)行數(shù)字化得到。
學(xué)習(xí):在數(shù)據(jù)的基礎(chǔ)上讓機(jī)器重復(fù)執(zhí)行一套特定的步驟(學(xué)習(xí)算法)進(jìn)行事物特征的萃取,得到一個更加逼近于現(xiàn)實的描述(這個描述是一個模型它的本身可能就是一個函數(shù))。我們把大概能夠描述現(xiàn)實的這個函數(shù)稱作我們學(xué)到的模型。
更好:我們通過對模型的使用就能更好的解釋世界,解決與模型相關(guān)的問題。
-
解釋有監(jiān)督和無監(jiān)督機(jī)器學(xué)習(xí)之間的區(qū)別?
監(jiān)督學(xué)習(xí)需要訓(xùn)練標(biāo)記的數(shù)據(jù)。換句話說,監(jiān)督學(xué)習(xí)使用了基本事實,這意味著我們對輸出和樣本已有知識。這里的目標(biāo)是學(xué)習(xí)一個近似輸入和輸出之間關(guān)系的函數(shù)。
另一方面,無監(jiān)督學(xué)習(xí)不使用標(biāo)記的輸出。此處的目標(biāo)是推斷數(shù)據(jù)集中的自然結(jié)構(gòu)。
-
KNN算法介紹
鄰近算法,或者說K最鄰近(KNN,K-NearestNeighbor)分類算法是數(shù)據(jù)挖掘分類技術(shù)中最簡單的方法之一。所謂K最近鄰,就是K個最近的鄰居的意思,說的是每個樣本都可以用它最接近的K個鄰近值來代表。近鄰算法就是將數(shù)據(jù)集合中每一個記錄進(jìn)行分類的方法 。
k近鄰法是一種基本的分類和回歸方法,是監(jiān)督學(xué)習(xí)方法里的一種常用方法。k近鄰算法假設(shè)給定一個訓(xùn)練數(shù)據(jù)集,其中的實例類別已定。分類時,對新的實例,根據(jù)其k個最近鄰的訓(xùn)練實例類別,通過多數(shù)表決等方式進(jìn)行預(yù)測。
k近鄰法三要素:距離度量、k值的選擇和分類決策規(guī)則。常用的距離度量是歐氏距離及更一般的pL距離。k值小時,k近鄰模型更復(fù)雜,容易發(fā)生過擬合;k值大時,k近鄰模型更簡單,又容易欠擬合。因此k值得選擇會對分類結(jié)果產(chǎn)生重大影響。k值的選擇反映了對近似誤差與估計誤差之間的權(quán)衡,通常由交叉驗證選擇最優(yōu)的k。
優(yōu)點(diǎn)
-
簡單,易于理解,易于實現(xiàn),無需估計參數(shù),無需訓(xùn)練;
-
適合對稀有事件進(jìn)行分類;
-
特別適合于多分類問題(multi-modal,對象具有多個類別標(biāo)簽), kNN比SVM的表現(xiàn)要好。
缺點(diǎn)
- 該算法在分類時有個主要的不足是,當(dāng)樣本不平衡時,如一個類的樣本容量很大,而其他類樣本容量很小時,有可能導(dǎo)致當(dāng)輸入一個新樣本時,該樣本的K個鄰居中大容量類的樣本占多數(shù) 。
- 該方法的另一個不足之處是計算量較大,因為對每一個待分類的文本都要計算它到全體已知樣本的距離,才能求得它的K個最近鄰點(diǎn) 。
-
-
線性回歸法介紹
線性回歸(Linear regression)是利用回歸方程(函數(shù))對一個或多個自變量(特征值)和因變量(目標(biāo)值)之間關(guān)系進(jìn)行建模的一種分析方式。
- 特點(diǎn):只有一個自變量的情況稱為單變量回歸,多于一個自變量情況的叫做多元回歸
線性回歸當(dāng)中主要有兩種模型,一種是線性關(guān)系,另一種是非線性關(guān)系。
-
PCA算法介紹
PCA(principal components analysis)即主成分分析技術(shù),又稱主分量分析,旨在利用降維的思想,把多指標(biāo)轉(zhuǎn)化為少數(shù)幾個綜合指標(biāo)。
PCA算法優(yōu)點(diǎn):
1、使得數(shù)據(jù)集更易使用;
2、降低算法的計算開銷;
3、去除噪聲;
4、使得結(jié)果容易理解;
5、完全無參數(shù)限制。PCA算法缺點(diǎn):
1、主成分解釋其含義往往具有一定的模糊性,不如原始樣本完整
2、貢獻(xiàn)率小的主成分往往可能含有對樣本差異的重要信息,也就是可能對于區(qū)分樣本的類別(標(biāo)簽)更有用
3、特征值矩陣的正交向量空間是否唯一有待討論
4、無監(jiān)督學(xué)習(xí)PCA算法求解步驟:
-
去除平均值
-
計算協(xié)方差矩陣
-
計算協(xié)方差矩陣的特征值和特征向量
-
將特征值排序
-
保留前N個最大的特征值對應(yīng)的特征向量
-
將原始特征轉(zhuǎn)換到上面得到的N個特征向量構(gòu)建的新空間中(最后兩步,實現(xiàn)了特征壓縮)**
PCA是一種常用的數(shù)據(jù)分析方法。PCA通過線性變換將原始數(shù)據(jù)變換為一組各維度線性無關(guān)的表示,可用于識別和提取數(shù)據(jù)的主要特征分量,通過將數(shù)據(jù)坐標(biāo)軸旋轉(zhuǎn)到數(shù)據(jù)角度上那些最重要的方向(方差最大);然后通過特征值分析,確定出需要保留的主成分個數(shù),舍棄其他非主成分,從而實現(xiàn)數(shù)據(jù)的降維。降維使數(shù)據(jù)變得更加簡單高效,從而實現(xiàn)提升數(shù)據(jù)處理速度的目的,節(jié)省大量的時間和成本。降維也成為了應(yīng)用非常廣泛的數(shù)據(jù)預(yù)處理方法。PCA算法已經(jīng)被廣泛的應(yīng)用于高維數(shù)據(jù)集的探索與可視化,還可以用于數(shù)據(jù)壓縮,數(shù)據(jù)預(yù)處理,圖像,語音,通信的分析處理等領(lǐng)域。
-
-
支持向量機(jī)-SVM介紹
SVM(Support Vector Machine)指的是支持向量機(jī),是常見的一種判別方法。在機(jī)器學(xué)習(xí)領(lǐng)域,是一個有監(jiān)督的學(xué)習(xí)模型,通常用來進(jìn)行模式識別、分類以及回歸分析。它在解決小樣本、非線性及高維模式識別中表現(xiàn)出許多特有的優(yōu)勢,并能夠推廣應(yīng)用到函數(shù)擬合等其他其他問題中。
支持向量機(jī)(Support Vector Machine, SVM)是一類按監(jiān)督學(xué)習(xí)(supervised learning)方式對數(shù)據(jù)進(jìn)行二元分類(binary classification)的廣義線性分類器(generalized linear classifier),其決策邊界是對學(xué)習(xí)樣本求解的最大邊距超平面(maximum-margin hyperplane)。- SVM的主要特點(diǎn):
SVM主要思想是針對兩類分類問題,在高維空間中尋找一個超平面作為兩類的分割,以保證最小的分類錯誤率。
SVM考慮尋找一個滿足分類要求的超平面,并且使訓(xùn)練集中的點(diǎn)距離分類面盡可能的遠(yuǎn),即尋找一個分類面使它兩側(cè)的空白區(qū)域(margin)最大。
過兩類樣本中離分類面最近的點(diǎn)且平行于最優(yōu)分類面的超平面上的訓(xùn)練樣本就叫做支持向量。
最優(yōu)分類面就是要求分類線不但能將兩類正確分開(訓(xùn)練錯誤率為0),且使分類間隔最大。
- SVM的主要特點(diǎn):
-
隨機(jī)森林算法介紹
隨機(jī)森林指的是利用多棵樹對樣本進(jìn)行訓(xùn)練并預(yù)測的一種分類器。
隨機(jī)森林的優(yōu)點(diǎn)有 :
1)對于很多種資料,它可以產(chǎn)生高準(zhǔn)確度的分類器;
2)它可以處理大量的輸入變數(shù);
3)它可以在決定類別時,評估變數(shù)的重要性;
4)在建造森林時,它可以在內(nèi)部對于一般化后的誤差產(chǎn)生不偏差的估計;
5)它包含一個好方法可以估計遺失的資料,如果有很大一部分的資料遺失,仍可以維持準(zhǔn)確度;
6)學(xué)習(xí)過程是很快速的。
算法過程
1、一個樣本容量為N的樣本,有放回的抽取N次,每次抽取1個,最終形成了N個樣本。這選擇好了的N個樣本用來訓(xùn)練一個決策樹,作為決策樹根節(jié)點(diǎn)處的樣本。
2、當(dāng)每個樣本有M個屬性時,在決策樹的每個節(jié)點(diǎn)需要分裂時,隨機(jī)從這M個屬性中選取出m個屬性,滿足條件m << M。然后從這m個屬性中采用某種策略(比如說信息增益)來選擇1個屬性作為該節(jié)點(diǎn)的分裂屬性。
3、決策樹形成過程中每個節(jié)點(diǎn)都要按照步驟2來分裂(很容易理解,如果下一次該節(jié)點(diǎn)選出來的那一個屬性是剛剛其父節(jié)點(diǎn)分裂時用過的屬性,則該節(jié)點(diǎn)已經(jīng)達(dá)到了葉子節(jié)點(diǎn),無須繼續(xù)分裂了),一直到不能夠再分裂為止。注意整個決策樹形成過程中沒有進(jìn)行剪枝。
4、按照步驟1~3建立大量的決策樹,這樣就構(gòu)成了隨機(jī)森林了。
數(shù)據(jù)的隨機(jī)選取
首先,從原始的數(shù)據(jù)集中采取有放回的抽樣,構(gòu)造子數(shù)據(jù)集,子數(shù)據(jù)集的數(shù)據(jù)量是和原始數(shù)據(jù)集相同的。不同子數(shù)據(jù)集的元素可以重復(fù),同一個子數(shù)據(jù)集中的元素也可以重復(fù)。第二,利用子數(shù)據(jù)集來構(gòu)建子決策樹,將這個數(shù)據(jù)放到每個子決策樹中,每個子決策樹輸出一個結(jié)果。最后,如果有了新的數(shù)據(jù)需要通過隨機(jī)森林得到分類結(jié)果,就可以通過對子決策樹的判斷結(jié)果的投票,得到隨機(jī)森林的輸出結(jié)果了。待選特征的隨機(jī)選取
與數(shù)據(jù)集的隨機(jī)選取類似,隨機(jī)森林中的子樹的每一個分裂過程并未用到所有的待選特征,而是從所有的待選特征中隨機(jī)選取一定的特征,之后再在隨機(jī)選取的特征中選取最優(yōu)的特征。這樣能夠使得隨機(jī)森林中的決策樹都能夠彼此不同,提升系統(tǒng)的多樣性,從而提升分類性能。
7.5 Python語言
-
Python 的內(nèi)存管理機(jī)制及調(diào)優(yōu)手段
內(nèi)存管理機(jī)制:引用計數(shù)、垃圾回收、內(nèi)存池。
引用計數(shù)
引用計數(shù)是一種非常高效的內(nèi)存管理手段, 當(dāng)一個 Python 對象被引用時其引用計數(shù)增加 1, 當(dāng)其不再被一個變量引用時則計數(shù)減 1. 當(dāng)引用計數(shù)等于 0 時對象被刪除。
垃圾回收
(1) 引用計數(shù)
引用計數(shù)也是一種垃圾收集機(jī)制,而且也是一種最直觀,最簡單的垃圾收集技術(shù)。當(dāng) Python 的某
個對象的引用計數(shù)降為 0 時,說明沒有任何引用指向該對象,該對象就成為要被回收的垃圾了。比如某個新建對象,它被分配給某個引用,對象的引用計數(shù)變?yōu)?1。如果引用被刪除,對象的引用計數(shù)為 0,那么該對象就可以被垃圾回收。不過如果出現(xiàn)循環(huán)引用的話,引用計數(shù)機(jī)制就不再起有效的作用了(2)標(biāo)記清除
如果兩個對象的引用計數(shù)都為 1,但是僅僅存在他們之間的循環(huán)引用,那么這兩個對象都是需要被
回收的,也就是說,它們的引用計數(shù)雖然表現(xiàn)為非 0,但實際上有效的引用計數(shù)為 0。所以先將循環(huán)引用摘掉,就會得出這兩個對象的有效計數(shù)。(3) 分代回收
從前面“標(biāo)記-清除”這樣的垃圾收集機(jī)制來看,這種垃圾收集機(jī)制所帶來的額外操作實際上與系統(tǒng)
中總的內(nèi)存塊的數(shù)量是相關(guān)的,當(dāng)需要回收的內(nèi)存塊越多時,垃圾檢測帶來的額外操作就越多,而垃圾回收帶來的額外操作就越少;反之,當(dāng)需回收的內(nèi)存塊越少時,垃圾檢測就將比垃圾回收帶來更少的額外操作。內(nèi)存池
(1) Python 的內(nèi)存機(jī)制呈現(xiàn)金字塔形狀,-1,-2 層主要有操作系統(tǒng)進(jìn)行操作
(2) 第 0 層是 C 中的 malloc,free 等內(nèi)存分配和釋放函數(shù)進(jìn)行操作
(3)第 1 層和第 2 層是內(nèi)存池,有 Python 的接口函數(shù) PyMem_Malloc 函數(shù)實現(xiàn),當(dāng)對象小于
256K 時有該層直接分配內(nèi)存(4) 第 3 層是最上層,也就是我們對 Python 對象的直接操作
Python 在運(yùn)行期間會大量地執(zhí)行 malloc 和 free 的操作,頻繁地在用戶態(tài)和核心態(tài)之間進(jìn)行切換,這將嚴(yán)重影響 Python 的執(zhí)行效率。為了加速 Python 的執(zhí)行效率,Python 引入了一個內(nèi)存池機(jī)制,用于管理對小塊內(nèi)存的申請和釋放。
Python 內(nèi)部默認(rèn)的小塊內(nèi)存與大塊內(nèi)存的分界點(diǎn)定在 256 個字節(jié),當(dāng)申請的內(nèi)存小于 256 字節(jié)時,PyObject_Malloc 會在內(nèi)存池中申請內(nèi)存;當(dāng)申請的內(nèi)存大于 256 字節(jié)時,PyObject_Malloc 的行為將蛻化為 malloc 的行為。當(dāng)然,通過修改 Python 源代碼,我們可以改變這個默認(rèn)值,從而改變 Python 的默認(rèn)內(nèi)存管理行為。
-
Python 函數(shù)調(diào)用的時候參數(shù)的傳遞方式是值傳遞還是引用傳遞
Python 的參數(shù)傳遞有:位置參數(shù)、默認(rèn)參數(shù)、可變參數(shù)、關(guān)鍵字參數(shù)。函數(shù)的傳值到底是值傳遞還是引用傳遞,要分情況:
不可變參數(shù)用值傳遞
像整數(shù)和字符串這樣的不可變對象,是通過拷貝進(jìn)行傳遞的,因為你無論如何都不可能在原處改變不可變對象
可變參數(shù)是引用傳遞的
比如像列表,字典這樣的對象是通過引用傳遞、和 C 語言里面的用指針傳遞數(shù)組很相似,可變對象能在函數(shù)內(nèi)部改變。
-
什么是裝飾器?
「裝飾器」作為 Python 高級語言特性中的重要部分,是修改函數(shù)的一種超級便捷的方式,適當(dāng)使用能夠有效提高代碼的可讀性和可維護(hù)性,非常的便利靈活。
「裝飾器」本質(zhì)上就是一個函數(shù),這個函數(shù)的特點(diǎn)是可以接受其它的函數(shù)當(dāng)作它的參數(shù),并將其替換成一個新的函數(shù)(即返回給另一個函數(shù))。
裝飾器本質(zhì)上是一個 Python 函數(shù),它可以在讓其他函數(shù)在不需要做任何代碼的變動的前提下增加額外的功能。裝飾器的返回值也是一個函數(shù)的對象,它經(jīng)常用于有切面需求的場景。 比如:插入日志、性能測試、事務(wù)處理、緩存、權(quán)限的校驗等場景 有了裝飾器就可以抽離出大量的與函數(shù)功能本身無關(guān)的雷同代碼并發(fā)并繼續(xù)使用。
8. 了解前端技術(shù)
8.1 Html/Css
8.2 JavaScript
8.3 Vue框架
-
MVC和MVVM
MVVM是Model-View-ViewModel的縮寫。MVVM是一種設(shè)計思想。Model 層代表數(shù)據(jù)模型,也可以在Model中定義數(shù)據(jù)修改和操作的業(yè)務(wù)邏輯;View 代表UI 組件,它負(fù)責(zé)將數(shù)據(jù)模型轉(zhuǎn)化成UI 展現(xiàn)出來,ViewModel 是一個同步View 和 Model的對象。
在MVVM架構(gòu)下,View 和 Model 之間并沒有直接的聯(lián)系,而是通過ViewModel進(jìn)行交互,Model 和 ViewModel 之間的交互是雙向的, 因此View 數(shù)據(jù)的變化會同步到Model中,而Model 數(shù)據(jù)的變化也會立即反應(yīng)到View 上。
ViewModel 通過雙向數(shù)據(jù)綁定把 View 層和 Model 層連接起來,而View 和 Model 之間的同步工作完全是自動的,無需人為干涉,因此開發(fā)者只需關(guān)注業(yè)務(wù)邏輯,不需要手動操作DOM, 不需要關(guān)注數(shù)據(jù)狀態(tài)的同步問題,復(fù)雜的數(shù)據(jù)狀態(tài)維護(hù)完全由 MVVM 來統(tǒng)一管理。
mvc和mvvm其實區(qū)別并不大,都是一種設(shè)計思想。主要就是mvc中Controller演變成mvvm中的viewModel。mvvm主要解決了mvc中大量的DOM 操作使頁面渲染性能降低,加載速度變慢,影響用戶體驗。
區(qū)別:vue數(shù)據(jù)驅(qū)動,通過數(shù)據(jù)來顯示視圖層而不是節(jié)點(diǎn)操作。
場景:數(shù)據(jù)操作比較多、頻繁的場景,更加便捷。
-
Vue 生命周期的理解
總共分為8個階段創(chuàng)建前/后,載入前/后,更新前/后,銷毀前/后。
**創(chuàng)建前后:**在beforeCreated階段,vue實例的掛載元素el和數(shù)據(jù)對象data都為undefined,還未初始化。在created階段,vue實例的數(shù)據(jù)對象data有了,el和數(shù)據(jù)對象data都為undefined,還未初始化。在created階段,vue實例的數(shù)據(jù)對象data有了,el還沒有。
**載入前后:**在beforeMount階段,vue實例的$el和data都初始化了,但還是掛載之前為虛擬的dom節(jié)點(diǎn),data.message還未替換。在mounted階段,vue實例掛載完成,data.message成功渲染。
**更新前后:**當(dāng)data變化時,會觸發(fā)beforeUpdate和updated方法。
**銷毀前后:**在執(zhí)行destroy方法后,對data的改變不會再觸發(fā)周期函數(shù),說明此時vue實例已經(jīng)解除了事件監(jiān)聽以及和dom的綁定,但是dom結(jié)構(gòu)依然存在
-
Vue的優(yōu)點(diǎn)
- 輕量級框架:只關(guān)注視圖層,是一個構(gòu)建數(shù)據(jù)的視圖集合,大小只有幾十
kb
; - 簡單易學(xué):國人開發(fā),中文文檔,不存在語言障礙 ,易于理解和學(xué)習(xí);
- 雙向數(shù)據(jù)綁定:保留了
angular
的特點(diǎn),在數(shù)據(jù)操作方面更為簡單; - 組件化:保留了
react
的優(yōu)點(diǎn),實現(xiàn)了html
的封裝和重用 - 在構(gòu)建單頁面應(yīng)用方面有著獨(dú)特的優(yōu)勢;
- 視圖,數(shù)據(jù),結(jié)構(gòu)分離:使數(shù)據(jù)的更改更為簡單,不需要進(jìn)行邏輯代碼的修改,只需要操作數(shù)據(jù)就能完成相關(guān)操作;
- 虛擬DOM:
dom
操作是非常耗費(fèi)性能的, 不再使用原生的dom
操作節(jié)點(diǎn),極大解放dom
操作,但具體操作的還是dom
不過是換了另一種方式; - 運(yùn)行速度更快:相比較于
react
而言,同樣是操作虛擬dom
,就性能而言,vue
存在很大的優(yōu)勢。
- 輕量級框架:只關(guān)注視圖層,是一個構(gòu)建數(shù)據(jù)的視圖集合,大小只有幾十
二 項目經(jīng)歷
1. 基于機(jī)器學(xué)習(xí)的冬奧會智能分析與預(yù)測系統(tǒng)
1.1 項目描述
本項目是我校的省級創(chuàng)新創(chuàng)業(yè)項目,負(fù)責(zé)后端開發(fā)與數(shù)據(jù)庫設(shè)計,對往屆冬奧會數(shù)據(jù)處理與分析,前端通過可視化展示給用戶,根據(jù)往屆冬奧會信息進(jìn)行預(yù)測模型訓(xùn)練,預(yù)測各個國家在下一屆冬奧會所獲得的獎牌情況。
1.2 工作內(nèi)容
-
從冬奧會多個官網(wǎng)收集數(shù)據(jù),進(jìn)行處理,存儲到MySQL數(shù)據(jù)庫,進(jìn)行數(shù)據(jù)庫表結(jié)構(gòu)設(shè)計。
-
首頁數(shù)據(jù)緩存到Redis數(shù)據(jù)庫,增加系統(tǒng)可維護(hù)性,提高網(wǎng)頁的響應(yīng)速度,提高并發(fā)訪問量。
-
使用隨機(jī)森林算法建立預(yù)測獎牌模型,進(jìn)行各個國家獎牌預(yù)測,并將結(jié)果數(shù)據(jù)緩存到Redis中。
-
使用Viper配置項目參數(shù),增加項目可擴(kuò)展性,使用Swagger編寫注釋,方便前端理解接口和代碼調(diào)試。
-
項目使用JWT跨域認(rèn)證,增加用戶數(shù)據(jù)安全性,注冊使用Email進(jìn)行身份驗證,保障了賬號的真實性。
-
使用Zap日志進(jìn)行日志收集,提高日志收集效率,能夠收集程序運(yùn)行多種基本信息,便于系統(tǒng)調(diào)試。
1.3 面試問題
-
介紹一下項目
本項目是我校的省級創(chuàng)新創(chuàng)業(yè)項目,我負(fù)責(zé)后端開發(fā)與數(shù)據(jù)庫設(shè)計,項目主要包括兩部分,第一為冬奧會的數(shù)據(jù)分析與圖表展示,第二為使用隨機(jī)森林算法根據(jù)往屆冬奧會信息進(jìn)行預(yù)測模型訓(xùn)練,預(yù)測各個國家在下一屆冬奧會所獲得的獎牌情況。采用前后端分離開發(fā),前端采用Vue+Echarts,后端采用GoLang的gin框架進(jìn)行開發(fā),預(yù)測算法機(jī)器學(xué)習(xí)中的隨機(jī)森林算法,數(shù)據(jù)庫采用Mysql,數(shù)據(jù)來源為2022北京冬奧組委官網(wǎng)、國際奧林匹克官網(wǎng)等網(wǎng)站收集獎牌及視頻數(shù)據(jù),百度百科收集運(yùn)動員個人信息數(shù)據(jù),聚會數(shù)據(jù)網(wǎng)收集預(yù)測因素數(shù)據(jù)。
-
項目重點(diǎn)難點(diǎn)有什么
預(yù)測使用的方法為隨機(jī)森林。
預(yù)測模型建立時考慮了9種可能對預(yù)測產(chǎn)生影響的因素,分別為(1).參加冬奧會男子數(shù)(2).參加冬奧會女子數(shù)(3).是否為主辦方(4).國家人均gdp(5).國家總gdp(6).國家人口總數(shù) (7).國家社會制度 (8).獲得獎牌排行 (9).獲得獎牌占總獎牌數(shù)的比率。
首先用RandomForestRegressor按照不同的權(quán)重對9種因素進(jìn)行模型訓(xùn)練,然后使用同種方法對每一種因素進(jìn)行訓(xùn)練,并預(yù)測出2026年的值,再將2026年的數(shù)據(jù)帶入總的預(yù)測模型,并得出2026年冬奧會各個國家獲得的獎牌,并將2022年的獎牌進(jìn)行數(shù)據(jù)檢驗,最后將獲得的數(shù)據(jù)進(jìn)行保存。
-
項目使用了哪些技術(shù)
本項目采用前后端分離開發(fā),前端采用Vue+Echarts,后端采用GoLang的gin框架進(jìn)行開發(fā),預(yù)測算法機(jī)器學(xué)習(xí)中的隨機(jī)森林算法,使用的語言為Python,在開發(fā)過程中我們使用了一下技術(shù):
- 使用zap日志庫進(jìn)行系統(tǒng)日志收集
- 使用配置信息viper進(jìn)行系統(tǒng)參數(shù)配置
- 使用swagger編寫接口規(guī)范和接口文檔
- 使用JWT跨域認(rèn)證,實現(xiàn)Token機(jī)制
- 使用雪花算法生成用戶ID,在并發(fā)操作下,保證了數(shù)據(jù)的唯一性,用戶不知道系統(tǒng)內(nèi)部信息
- 使用go-redis,首頁數(shù)據(jù)緩存到Redis數(shù)據(jù)庫,郵箱驗證碼和Token保存在Redis中
- 使用gorm開發(fā),進(jìn)行MySQL數(shù)據(jù)庫操作。
-
項目有哪些亮眼的設(shè)計
- 采取隨機(jī)森林算法
- 首頁數(shù)據(jù)緩存到Redis數(shù)據(jù)庫,提高網(wǎng)頁的響應(yīng)速度,提高并發(fā)訪問量
- 使用zap日志庫,增加日志文件的詳細(xì)內(nèi)容,系統(tǒng)出問題快速定位解決
- 使用Viper進(jìn)行參數(shù)配置,使系統(tǒng)參數(shù)集中管理,方便參數(shù)的維護(hù)與修改
- 使用swagger編寫接口文檔,開發(fā)更加規(guī)范,降低了前后端交互的困難,并且支持在線測試接口
- 使用雪花算法生成用戶分布式ID,使ID不會重復(fù),有序并且生成速度很快
-
從項目中學(xué)到了什么
- 對項目開發(fā)的規(guī)范流程有了具體的掌握
- 使用多種中間件完成項目開發(fā),加快項目開發(fā)效率,使項目更加健壯
- 使用機(jī)器算法完成預(yù)測功能,對機(jī)器學(xué)習(xí)有了更加深入的理解
2. 基于微服務(wù)的通用賬戶功能系統(tǒng)
2.1 項目描述
本項目是基于Micro框架開發(fā)的通用的賬戶功能系統(tǒng),包括賬戶的基本操作如賬號增刪改查和權(quán)限操作,使用配置中心、鏈路追蹤、監(jiān)控等相關(guān)技術(shù),詳情查看博客:https://juejin.cn/column/7179162138446364732
2.2 工作內(nèi)容
-
使用ProtoBuf進(jìn)行參數(shù)傳輸和定義相關(guān)接口,定義了13個相關(guān)的賬戶功能接口。
-
使用Redis緩存Email驗證碼和存儲Token,提高用戶注冊效率,實現(xiàn)定時清除Token機(jī)制。
-
使用Docker部署多個微服務(wù)插件,如Consul,Jaeger,Prometheus等,降低開發(fā)難度,提高開發(fā)效率。
-
客戶端使用負(fù)載均衡,大大提高項目的并發(fā)量,使用Prometheus進(jìn)行項目監(jiān)控,及時發(fā)現(xiàn)并解決問題。
2.3 面試問題
-
介紹一下項目
項目是基于Micro框架開發(fā)的通用的賬戶功能系統(tǒng),采用Mysql+Redis+Docker進(jìn)行項目開發(fā),包括賬戶的基本操作如賬號增刪改查和權(quán)限操作,使用配置中心、鏈路追蹤、監(jiān)控等相關(guān)技術(shù),使用ProtoBuf進(jìn)行數(shù)據(jù)傳輸和Api接口定義,相關(guān)插件使用Docker進(jìn)行安裝,避免花費(fèi)大量精力放在安裝部署方面。
-
項目重點(diǎn)難點(diǎn)有什么
- 對ProtoBuf需要定義的參數(shù)和接口進(jìn)行細(xì)致的思考
- 使用Docker、Docker Compose進(jìn)行插件安裝和快速啟動
- 對多個微服務(wù)插件的使用和配置
-
項目使用了哪些技術(shù)
- 使用go語言進(jìn)行項目開發(fā)
- 使用go-micro微服務(wù)框架進(jìn)行服務(wù)端開發(fā)
- 使用Docker安裝部署中間件,加快開發(fā)效率
- 使用ProtoBuf行數(shù)據(jù)傳輸和Api接口定義
- 使用Gorm與Mysql數(shù)據(jù)庫進(jìn)行交互
- 使用Zap日志庫進(jìn)行日志記錄
-
項目有哪些亮眼的設(shè)計
- 使用Docker進(jìn)行相關(guān)插件的安裝,提高了開發(fā)效率,跨系統(tǒng)移植更加簡單
- 使用了許多微服務(wù)插件(如Consul、Jaeger、Prometheus)完善項目功能
- 使用ProtoBuf行數(shù)據(jù)傳輸和Api接口定義
- 使用Zap、Markflie、Redis進(jìn)行項目開發(fā),使項目更加完善。
-
從項目中學(xué)到了什么
-
學(xué)到了微服務(wù)開發(fā)流程:
-
創(chuàng)建項目(Docker或go-micro)
-
編寫proto文件,并生成.go文件
-
編寫domain數(shù)據(jù)庫方面,包含(model層,repository層,service層)等
-
編寫Handle層,實現(xiàn)proto定義接口
-
編寫common層,配置,mysql,公共函數(shù),jaeger(鏈路追蹤)等
-
編寫main函數(shù),完成項目閉環(huán)
-
-
Docker、Docker Compose的基本使用
-
Proto的使用、命令、編寫
-
對微服務(wù)組件有了更加深入的了解
-
對微服務(wù)的作用、使用和開發(fā)有了更加深入的了解
-
對中間件整合到項目中更加熟悉
-