大家好,我是小林。
原標(biāo)題:好卷,大二就在美團(tuán)實(shí)習(xí)了
有很多在校的讀者咨詢過(guò)我,在校卷什么對(duì)求職開(kāi)發(fā)崗比較有幫助?很多同學(xué)都會(huì)想去卷一些比賽、證書、績(jī)點(diǎn)這些。
其實(shí)比賽來(lái)說(shuō),無(wú)疑含金量最高的就是 ACM 比賽了,很多大廠都喜歡拿了 ACM 獎(jiǎng)項(xiàng)的同學(xué),ACM 是算法比賽,能拿到比賽名次的,肯定都屬于比較聰明的一類人,成長(zhǎng)性會(huì)很大。但是 ACM 比賽需要花大量的精力去學(xué)算法,而且也不適合每一個(gè)人,也是比較看天賦的。
開(kāi)崗位看重的是你的開(kāi)發(fā)經(jīng)驗(yàn),適合大多數(shù)人的方式,就是卷實(shí)習(xí)了,比如從大二/研一早早就開(kāi)始準(zhǔn)備求職方向的內(nèi)容,然后找實(shí)習(xí)工作,多積累實(shí)習(xí)經(jīng)歷,寫到簡(jiǎn)歷上,自然就能證明你是具備了工程開(kāi)發(fā)的經(jīng)驗(yàn)的。
有了實(shí)習(xí)經(jīng)歷,秋招的時(shí)候,拿到面試的機(jī)會(huì)也會(huì)更大一些,因?yàn)榇蟛糠制髽I(yè)都喜歡招進(jìn)來(lái)就能干活的,你具備了實(shí)習(xí)經(jīng)歷,就證明你是具備上手快的能力的。
前段時(shí)間,正好收到一位訓(xùn)練營(yíng)同學(xué)的喜事,找到美團(tuán)大廠實(shí)習(xí)了,同學(xué)是 26 屆,目前是大二,積累個(gè)半年大廠實(shí)習(xí)經(jīng)歷,等同學(xué)明年秋招,妥妥就是大廠 offer 收割機(jī)了
那大廠實(shí)習(xí)的面試難度到底如何?
今天,就來(lái)跟大家拆解一波美團(tuán)Java 后端實(shí)習(xí)的面經(jīng),主要是考察了Java、MySQL、Redis、網(wǎng)絡(luò)、操作系統(tǒng)這五大塊知識(shí),可以說(shuō)是校招必須掌握的五件套知識(shí)了。
我只能說(shuō),不愧是大廠面試,強(qiáng)度是很大,光是寫面經(jīng)都快 2 萬(wàn)字了
考察的知識(shí)點(diǎn),我給大家羅列了一下:
-
-
- Java:
線程池、線程狀態(tài)、線程同步和鎖、Srping AOP、IoC、動(dòng)態(tài)代理
-
-
-
- MySQL:
索引、事務(wù)、性能優(yōu)化
-
-
-
- Redis:
線程模式、分布式鎖
-
MySQL
講講事務(wù)的特性,怎么保證ACID的?
原子性(Atomicity):一個(gè)事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié),而且事務(wù)在執(zhí)行過(guò)程中發(fā)生錯(cuò)誤,會(huì)被回滾到事務(wù)開(kāi)始前的狀態(tài),就像這個(gè)事務(wù)從來(lái)沒(méi)有執(zhí)行過(guò)一樣,就好比買一件商品,購(gòu)買成功時(shí),則給商家付了錢,商品到手;購(gòu)買失敗時(shí),則商品在商家手中,消費(fèi)者的錢也沒(méi)花出去。
一致性(Consistency):是指事務(wù)操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫(kù)保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉(zhuǎn)賬 200 元,分為兩個(gè)步驟,從 A 的賬戶扣除 200 元和對(duì) B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結(jié)果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會(huì)出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。
隔離性(Isolation):數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致,因?yàn)槎鄠€(gè)事務(wù)同時(shí)使用相同的數(shù)據(jù)時(shí),不會(huì)相互干擾,每個(gè)事務(wù)都有一個(gè)完整的數(shù)據(jù)空間,對(duì)其他并發(fā)事務(wù)是隔離的。也就是說(shuō),消費(fèi)者購(gòu)買商品這個(gè)事務(wù),是不影響其他消費(fèi)者購(gòu)買的。
持久性(Durability):事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。
MySQL InnoDB 引擎通過(guò)什么技術(shù)來(lái)保證事務(wù)的這四個(gè)特性的呢?
- 持久性是通過(guò) redo log (重做日志)來(lái)保證的;原子性是通過(guò) undo log(回滾日志) 來(lái)保證的;隔離性是通過(guò) MVCC(多版本并發(fā)控制) 或鎖機(jī)制來(lái)保證的;一致性則是通過(guò)持久性+原子性+隔離性來(lái)保證;
事務(wù)的隔離級(jí)別有哪些?
讀未提交(read uncommitted),指一個(gè)事務(wù)還沒(méi)提交時(shí),它做的變更就能被其他事務(wù)看到;
讀提交(read committed),指一個(gè)事務(wù)提交之后,它做的變更才能被其他事務(wù)看到;
可重復(fù)讀(repeatable read),指一個(gè)事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的,
MySQL InnoDB 引擎的默認(rèn)隔離級(jí)別;
串行化(serializable);會(huì)對(duì)記錄加上讀寫鎖,在多個(gè)事務(wù)對(duì)這條記錄進(jìn)行讀寫操作時(shí),如果發(fā)生了讀寫沖突的時(shí)候,后訪問(wèn)的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行;
按隔離水平高低排序如下:
針對(duì)不同的隔離級(jí)別,并發(fā)事務(wù)時(shí)可能發(fā)生的現(xiàn)象也會(huì)不同。
也就是說(shuō):
- 在「讀未提交」隔離級(jí)別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀現(xiàn)象;在「讀提交」隔離級(jí)別下,可能發(fā)生不可重復(fù)讀和幻讀現(xiàn)象,但是不可能發(fā)生臟讀現(xiàn)象;在「可重復(fù)讀」隔離級(jí)別下,可能發(fā)生幻讀現(xiàn)象,但是不可能臟讀和不可重復(fù)讀現(xiàn)象;在「串行化」隔離級(jí)別下,臟讀、不可重復(fù)讀和幻讀現(xiàn)象都不可能會(huì)發(fā)生。
講講索引的分類是什么?
MySQL可以按照四個(gè)角度來(lái)分類索引。
-
-
- 按「數(shù)據(jù)結(jié)構(gòu)」分類:
B+tree索引、Hash索引、Full-text索引
-
-
-
- 按「物理存儲(chǔ)」分類:
聚簇索引(主鍵索引)、二級(jí)索引(輔助索引)
-
-
-
- 按「字段特性」分類:
主鍵索引、唯一索引、普通索引、前綴索引
-
-
-
-
- 按「字段個(gè)數(shù)」分類:
單列索引、聯(lián)合索引
-
-
接下來(lái),按照這些角度來(lái)說(shuō)說(shuō)各類索引的特點(diǎn)。
按數(shù)據(jù)結(jié)構(gòu)分類
從數(shù)據(jù)結(jié)構(gòu)的角度來(lái)看,MySQL 常見(jiàn)索引有 B+Tree 索引、HASH 索引、Full-Text 索引。每一種存儲(chǔ)引擎支持的索引類型不一定相同,我在表中總結(jié)了 MySQL 常見(jiàn)的存儲(chǔ)引擎 InnoDB、MyISAM 和 Memory 分別支持的索引類型。
InnoDB 是在 MySQL 5.5 之后成為默認(rèn)的 MySQL 存儲(chǔ)引擎,B+Tree 索引類型也是 MySQL 存儲(chǔ)引擎采用最多的索引類型。在創(chuàng)建表時(shí),InnoDB 存儲(chǔ)引擎會(huì)根據(jù)不同的場(chǎng)景選擇不同的列作為索引:
- 如果有主鍵,默認(rèn)會(huì)使用主鍵作為聚簇索引的索引鍵(key);如果沒(méi)有主鍵,就選擇第一個(gè)不包含 NULL 值的唯一列作為聚簇索引的索引鍵(key);在上面兩個(gè)都沒(méi)有的情況下,InnoDB 將自動(dòng)生成一個(gè)隱式自增 id 列作為聚簇索引的索引鍵(key);
其它索引都屬于輔助索引(Secondary Index),也被稱為二級(jí)索引或非聚簇索引。創(chuàng)建的主鍵索引和二級(jí)索引默認(rèn)使用的是 B+Tree 索引。
按物理存儲(chǔ)分類
從物理存儲(chǔ)的角度來(lái)看,索引分為聚簇索引(主鍵索引)、二級(jí)索引(輔助索引)。這兩個(gè)區(qū)別在前面也提到了:
- 主鍵索引的 B+Tree 的葉子節(jié)點(diǎn)存放的是實(shí)際數(shù)據(jù),所有完整的用戶記錄都存放在主鍵索引的 B+Tree 的葉子節(jié)點(diǎn)里;二級(jí)索引的 B+Tree 的葉子節(jié)點(diǎn)存放的是主鍵值,而不是實(shí)際數(shù)據(jù)。
所以,在查詢時(shí)使用了二級(jí)索引,如果查詢的數(shù)據(jù)能在二級(jí)索引里查詢的到,那么就不需要回表,這個(gè)過(guò)程就是覆蓋索引。如果查詢的數(shù)據(jù)不在二級(jí)索引里,就會(huì)先檢索二級(jí)索引,找到對(duì)應(yīng)的葉子節(jié)點(diǎn),獲取到主鍵值后,然后再檢索主鍵索引,就能查詢到數(shù)據(jù)了,這個(gè)過(guò)程就是回表。
按字段特性分類
從字段特性的角度來(lái)看,索引分為主鍵索引、唯一索引、普通索引、前綴索引。
- 主鍵索引
主鍵索引就是建立在主鍵字段上的索引,通常在創(chuàng)建表的時(shí)候一起創(chuàng)建,一張表最多只有一個(gè)主鍵索引,索引列的值不允許有空值。在創(chuàng)建表時(shí),創(chuàng)建主鍵索引的方式如下:
CREATE?TABLE?table_name??(
??....
??PRIMARY?KEY?(index_column_1)?USING?BTREE
);
- 唯一索引
唯一索引建立在 UNIQUE 字段上的索引,一張表可以有多個(gè)唯一索引,索引列的值必須唯一,但是允許有空值。在創(chuàng)建表時(shí),創(chuàng)建唯一索引的方式如下:
CREATE?TABLE?table_name??(
??....
??UNIQUE?KEY(index_column_1,index_column_2,...)?
);
建表后,如果要?jiǎng)?chuàng)建唯一索引,可以使用這面這條命令:
CREATE?UNIQUE?INDEX?index_name
ON?table_name(index_column_1,index_column_2,...);
- 普通索引
普通索引就是建立在普通字段上的索引,既不要求字段為主鍵,也不要求字段為 UNIQUE。在創(chuàng)建表時(shí),創(chuàng)建普通索引的方式如下:
CREATE?TABLE?table_name??(
??....
??INDEX(index_column_1,index_column_2,...)?
);
建表后,如果要?jiǎng)?chuàng)建普通索引,可以使用這面這條命令:
CREATE?INDEX?index_name
ON?table_name(index_column_1,index_column_2,...);
- 前綴索引
前綴索引是指對(duì)字符類型字段的前幾個(gè)字符建立的索引,而不是在整個(gè)字段上建立的索引,前綴索引可以建立在字段類型為 char、 varchar、binary、varbinary 的列上。使用前綴索引的目的是為了減少索引占用的存儲(chǔ)空間,提升查詢效率。在創(chuàng)建表時(shí),創(chuàng)建前綴索引的方式如下:
CREATE?TABLE?table_name(
????column_list,
????INDEX(column_name(length))
);
建表后,如果要?jiǎng)?chuàng)建前綴索引,可以使用這面這條命令:
CREATE?INDEX?index_name
ON table_name(column_name(length));
按字段個(gè)數(shù)分類
從字段個(gè)數(shù)的角度來(lái)看,索引分為單列索引、聯(lián)合索引(復(fù)合索引)。
- 建立在單列上的索引稱為單列索引,比如主鍵索引;建立在多列上的索引稱為聯(lián)合索引;
通過(guò)將多個(gè)字段組合成一個(gè)索引,該索引就被稱為聯(lián)合索引。比如,將商品表中的 product_no 和 name 字段組合成聯(lián)合索引(product_no, name),創(chuàng)建聯(lián)合索引的方式如下:
CREATE?INDEX?index_product_no_name?ON?product(product_no,?name);
聯(lián)合索引(product_no, name) 的 B+Tree 示意圖如下(圖中葉子節(jié)點(diǎn)之間我畫了單向鏈表,但是實(shí)際上是雙向鏈表,原圖我找不到了,修改不了,偷個(gè)懶我不重畫了,大家腦補(bǔ)成雙向鏈表就行)。
可以看到,聯(lián)合索引的非葉子節(jié)點(diǎn)用兩個(gè)字段的值作為 B+Tree 的 key 值。當(dāng)在聯(lián)合索引查詢數(shù)據(jù)時(shí),先按 product_no 字段比較,在 product_no 相同的情況下再按 name 字段比較。
也就是說(shuō),聯(lián)合索引查詢的 B+Tree 是先按 product_no 進(jìn)行排序,然后再 product_no 相同的情況再按 name 字段排序。因此,使用聯(lián)合索引時(shí),存在最左匹配原則,也就是按照最左優(yōu)先的方式進(jìn)行索引的匹配。在使用聯(lián)合索引進(jìn)行查詢的時(shí)候,如果不遵循「最左匹配原則」,聯(lián)合索引會(huì)失效,這樣就無(wú)法利用到索引快速查詢的特性了。比如,如果創(chuàng)建了一個(gè) (a, b, c) 聯(lián)合索引,如果查詢條件是以下這幾種,就可以匹配上聯(lián)合索引:
- where a=1;where a=1 and b=2 and c=3;where a=1 and b=2;
需要注意的是,因?yàn)橛胁樵儍?yōu)化器,所以 a 字段在 where 子句的順序并不重要。但是,如果查詢條件是以下這幾種,因?yàn)椴环献钭笃ヅ湓瓌t,所以就無(wú)法匹配上聯(lián)合索引,聯(lián)合索引就會(huì)失效:
- where b=2;where c=3;where b=2 and c=3;
上面這些查詢條件之所以會(huì)失效,是因?yàn)?a, b, c) 聯(lián)合索引,是先按 a 排序,在 a 相同的情況再按 b 排序,在 b 相同的情況再按 c 排序。所以,b 和 c 是全局無(wú)序,局部相對(duì)有序的,這樣在沒(méi)有遵循最左匹配原則的情況下,是無(wú)法利用到索引的。
聯(lián)合索引有一些特殊情況,并不是查詢過(guò)程使用了聯(lián)合索引查詢,就代表聯(lián)合索引中的所有字段都用到了聯(lián)合索引進(jìn)行索引查詢,也就是可能存在部分字段用到聯(lián)合索引的 B+Tree,部分字段沒(méi)有用到聯(lián)合索引的 B+Tree 的情況。這種特殊情況就發(fā)生在范圍查詢。聯(lián)合索引的最左匹配原則會(huì)一直向右匹配直到遇到「范圍查詢」就會(huì)停止匹配。也就是范圍查詢的字段可以用到聯(lián)合索引,但是在范圍查詢字段的后面的字段無(wú)法用到聯(lián)合索引。
索引結(jié)構(gòu),為什么innoDB用B+樹(shù)
MySQL 是會(huì)將數(shù)據(jù)持久化在硬盤,而存儲(chǔ)功能是由 MySQL 存儲(chǔ)引擎實(shí)現(xiàn)的,所以討論 MySQL 使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,實(shí)際上是在討論存儲(chǔ)引使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,InnoDB 是 MySQL 默認(rèn)的存儲(chǔ)引擎,它就是采用了 B+ 樹(shù)作為索引的數(shù)據(jù)結(jié)構(gòu)。
要設(shè)計(jì)一個(gè) MySQL 的索引數(shù)據(jù)結(jié)構(gòu),不僅僅考慮數(shù)據(jù)結(jié)構(gòu)增刪改的時(shí)間復(fù)雜度,更重要的是要考慮磁盤 I/0 的操作次數(shù)。因?yàn)樗饕陀涗浂际谴娣旁谟脖P,硬盤是一個(gè)非常慢的存儲(chǔ)設(shè)備,我們?cè)诓樵償?shù)據(jù)的時(shí)候,最好能在盡可能少的磁盤 I/0 的操作次數(shù)內(nèi)完成。
二分查找樹(shù)雖然是一個(gè)天然的二分結(jié)構(gòu),能很好的利用二分查找快速定位數(shù)據(jù),但是它存在一種極端的情況,每當(dāng)插入的元素都是樹(shù)內(nèi)最大的元素,就會(huì)導(dǎo)致二分查找樹(shù)退化成一個(gè)鏈表,此時(shí)查詢復(fù)雜度就會(huì)從 O(logn)降低為 O(n)。
為了解決二分查找樹(shù)退化成鏈表的問(wèn)題,就出現(xiàn)了自平衡二叉樹(shù),保證了查詢操作的時(shí)間復(fù)雜度就會(huì)一直維持在 O(logn) 。但是它本質(zhì)上還是一個(gè)二叉樹(shù),每個(gè)節(jié)點(diǎn)只能有 2 個(gè)子節(jié)點(diǎn),隨著元素的增多,樹(shù)的高度會(huì)越來(lái)越高。
而樹(shù)的高度決定于磁盤 I/O 操作的次數(shù),因?yàn)闃?shù)是存儲(chǔ)在磁盤中的,訪問(wèn)每個(gè)節(jié)點(diǎn),都對(duì)應(yīng)一次磁盤 I/O 操作,也就是說(shuō)樹(shù)的高度就等于每次查詢數(shù)據(jù)時(shí)磁盤 IO 操作的次數(shù),所以樹(shù)的高度越高,就會(huì)影響查詢性能。
B 樹(shù)和 B+ 都是通過(guò)多叉樹(shù)的方式,會(huì)將樹(shù)的高度變矮,所以這兩個(gè)數(shù)據(jù)結(jié)構(gòu)非常適合檢索存于磁盤中的數(shù)據(jù)。
B+Tree vs B Tree:B+Tree 只在葉子節(jié)點(diǎn)存儲(chǔ)數(shù)據(jù),而 B 樹(shù) 的非葉子節(jié)點(diǎn)也要存儲(chǔ)數(shù)據(jù),所以 B+Tree 的單個(gè)節(jié)點(diǎn)的數(shù)據(jù)量更小,在相同的磁盤 I/O 次數(shù)下,就能查詢更多的節(jié)點(diǎn)。另外,B+Tree 葉子節(jié)點(diǎn)采用的是雙鏈表連接,適合 MySQL 中常見(jiàn)的基于范圍的順序查找,而 B 樹(shù)無(wú)法做到這一點(diǎn)。
B+Tree vs 二叉樹(shù):對(duì)于有 N 個(gè)葉子節(jié)點(diǎn)的 B+Tree,其搜索復(fù)雜度為O(logdN),其中 d 表示節(jié)點(diǎn)允許的最大子節(jié)點(diǎn)個(gè)數(shù)為 d 個(gè)。在實(shí)際的應(yīng)用當(dāng)中, d 值是大于100的,這樣就保證了,即使數(shù)據(jù)達(dá)到千萬(wàn)級(jí)別時(shí),B+Tree 的高度依然維持在 3~4 層左右,也就是說(shuō)一次數(shù)據(jù)查詢操作只需要做 3~4 次的磁盤 I/O 操作就能查詢到目標(biāo)數(shù)據(jù)。而二叉樹(shù)的每個(gè)父節(jié)點(diǎn)的兒子節(jié)點(diǎn)個(gè)數(shù)只能是 2 個(gè),意味著其搜索復(fù)雜度為 O(logN),這已經(jīng)比 B+Tree 高出不少,因此二叉樹(shù)檢索到目標(biāo)數(shù)據(jù)所經(jīng)歷的磁盤 I/O 次數(shù)要更多。
B+Tree vs Hash:Hash 在做等值查詢的時(shí)候效率賊快,搜索復(fù)雜度為 O(1)。但是 Hash 表不適合做范圍查詢,它更適合做等值的查詢,這也是 B+Tree 索引要比 Hash 表索引有著更廣泛的適用場(chǎng)景的原因
給你張表,發(fā)現(xiàn)查詢速度很慢,你有那些解決方案
分析查詢語(yǔ)句:使用EXPLAIN命令分析SQL執(zhí)行計(jì)劃,找出慢查詢的原因,比如是否使用了全表掃描,是否存在索引未被利用的情況等,并根據(jù)相應(yīng)情況對(duì)索引進(jìn)行適當(dāng)修改。
創(chuàng)建或優(yōu)化索引:根據(jù)查詢條件創(chuàng)建合適的索引,特別是經(jīng)常用于WHERE子句的字段、Orderby 排序的字段、Join 連表查詢的字典、 group by的字段,并且如果查詢中經(jīng)常涉及多個(gè)字段,考慮創(chuàng)建聯(lián)合索引,使用聯(lián)合索引要符合最左匹配原則,不然會(huì)索引失效
避免索引失效:比如不要用左模糊匹配、函數(shù)計(jì)算、表達(dá)式計(jì)算等等。
查詢優(yōu)化:避免使用SELECT *,只查詢真正需要的列;使用覆蓋索引,即索引包含所有查詢的字段;聯(lián)表查詢最好要以小表驅(qū)動(dòng)大表,并且被驅(qū)動(dòng)表的字段要有索引,當(dāng)然最好通過(guò)冗余字段的設(shè)計(jì),避免聯(lián)表查詢。
分頁(yè)優(yōu)化:針對(duì) limit n,y 深分頁(yè)的查詢優(yōu)化,可以把Limit查詢轉(zhuǎn)換成某個(gè)位置的查詢:select * from tb_sku where id>20000 limit 10,該方案適用于主鍵自增的表,
優(yōu)化數(shù)據(jù)庫(kù)表:如果單表的數(shù)據(jù)超過(guò)了千萬(wàn)級(jí)別,考慮是否需要將大表拆分為小表,減輕單個(gè)表的查詢壓力。也可以將字段多的表分解成多個(gè)表,有些字段使用頻率高,有些低,數(shù)據(jù)量大時(shí),會(huì)由于使用頻率低的存在而變慢,可以考慮分開(kāi)。
使用緩存技術(shù):引入緩存層,如Redis,存儲(chǔ)熱點(diǎn)數(shù)據(jù)和頻繁查詢的結(jié)果,但是要考慮緩存一致性的問(wèn)題,對(duì)于讀請(qǐng)求會(huì)選擇旁路緩存策略,對(duì)于寫請(qǐng)求會(huì)選擇先更新 db,再刪除緩存的策略。
Redis
Redis單線程為什么這么快?
官方使用基準(zhǔn)測(cè)試的結(jié)果是,單線程的 Redis 吞吐量可以達(dá)到 10W/每秒,如下圖所示:
img之所以 Redis 采用單線程(網(wǎng)絡(luò) I/O 和執(zhí)行命令)那么快,有如下幾個(gè)原因:
Redis 的大部分操作都在內(nèi)存中完成,并且采用了高效的數(shù)據(jù)結(jié)構(gòu),因此 Redis 瓶頸可能是機(jī)器的內(nèi)存或者網(wǎng)絡(luò)帶寬,而并非 CPU,既然 CPU 不是瓶頸,那么自然就采用單線程的解決方案了;Redis 采用單線程模型可以避免了多線程之間的競(jìng)爭(zhēng),省去了多線程切換帶來(lái)的時(shí)間和性能上的開(kāi)銷,而且也不會(huì)導(dǎo)致死鎖問(wèn)題。Redis 采用了I/O 多路復(fù)用機(jī)制處理大量的客戶端 Socket 請(qǐng)求,IO 多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè) IO 流,就是我們經(jīng)常聽(tīng)到的 select/epoll 機(jī)制。簡(jiǎn)單來(lái)說(shuō),在 Redis 只運(yùn)行單線程的情況下,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽(tīng) Socket 和已連接 Socket。內(nèi)核會(huì)一直監(jiān)聽(tīng)這些 Socket 上的連接請(qǐng)求或數(shù)據(jù)請(qǐng)求。一旦有請(qǐng)求到達(dá),就會(huì)交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
Redis分布式鎖是怎么實(shí)現(xiàn)的?
分布式鎖是用于分布式環(huán)境下并發(fā)控制的一種機(jī)制,用于控制某個(gè)資源在同一時(shí)刻只能被一個(gè)應(yīng)用所使用。如下圖所示:
Redis 本身可以被多個(gè)客戶端共享訪問(wèn),正好就是一個(gè)共享存儲(chǔ)系統(tǒng),可以用來(lái)保存分布式鎖,而且 Redis 的讀寫性能高,可以應(yīng)對(duì)高并發(fā)的鎖操作場(chǎng)景。Redis 的 SET 命令有個(gè) NX 參數(shù)可以實(shí)現(xiàn)「key不存在才插入」,所以可以用它來(lái)實(shí)現(xiàn)分布式鎖:
- 如果 key 不存在,則顯示插入成功,可以用來(lái)表示加鎖成功;如果 key 存在,則會(huì)顯示插入失敗,可以用來(lái)表示加鎖失敗。
基于 Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖時(shí),對(duì)于加鎖操作,我們需要滿足三個(gè)條件。
- 加鎖包括了讀取鎖變量、檢查鎖變量值和設(shè)置鎖變量值三個(gè)操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項(xiàng)來(lái)實(shí)現(xiàn)加鎖;鎖變量需要設(shè)置過(guò)期時(shí)間,以免客戶端拿到鎖后發(fā)生異常,導(dǎo)致鎖一直無(wú)法釋放,所以,我們?cè)?SET 命令執(zhí)行時(shí)加上 EX/PX 選項(xiàng),設(shè)置其過(guò)期時(shí)間;鎖變量的值需要能區(qū)分來(lái)自不同客戶端的加鎖操作,以免在釋放鎖時(shí),出現(xiàn)誤釋放操作,所以,我們使用 SET 命令設(shè)置鎖變量值時(shí),每個(gè)客戶端設(shè)置的值是一個(gè)唯一值,用于標(biāo)識(shí)客戶端;
滿足這三個(gè)條件的分布式命令如下:
SET?lock_key?unique_value?NX?PX?10000
- lock_key 就是 key 鍵;unique_value 是客戶端生成的唯一的標(biāo)識(shí),區(qū)分來(lái)自不同客戶端的鎖操作;NX 代表只在 lock_key 不存在時(shí),才對(duì) lock_key 進(jìn)行設(shè)置操作;PX 10000 表示設(shè)置 lock_key 的過(guò)期時(shí)間為 10s,這是為了避免客戶端發(fā)生異常而無(wú)法釋放鎖。
而解鎖的過(guò)程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執(zhí)行操作的客戶端就是加鎖的客戶端。所以,解鎖的時(shí)候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。可以看到,解鎖是有兩個(gè)操作,這時(shí)就需要 Lua 腳本來(lái)保證解鎖的原子性,因?yàn)?Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,保證了鎖釋放操作的原子性。
//?釋放鎖時(shí),先比較?unique_value?是否相等,避免鎖的誤釋放
if?redis.call("get",KEYS[1])?==?ARGV[1]?then
????return?redis.call("del",KEYS[1])
else
????return?0
end
這樣一來(lái),就通過(guò)使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上完成了分布式鎖的加鎖和解鎖。
計(jì)網(wǎng)和操作系統(tǒng)
網(wǎng)絡(luò)OSI模型和TCP/IP模型分別介紹一下
OSI七層模型
為了使得多種設(shè)備能通過(guò)網(wǎng)絡(luò)相互通信,和為了解決各種不同設(shè)備在網(wǎng)絡(luò)互聯(lián)中的兼容性問(wèn)題,國(guó)際標(biāo)準(zhǔn)化組織制定了開(kāi)放式系統(tǒng)互聯(lián)通信參考模型,也就是 OSI 網(wǎng)絡(luò)模型,該模型主要有 7 層,分別是應(yīng)用層、表示層、會(huì)話層、傳輸層、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層以及物理層。
每一層負(fù)責(zé)的職能都不同,如下:
- 應(yīng)用層,負(fù)責(zé)給應(yīng)用程序提供統(tǒng)一的接口;表示層,負(fù)責(zé)把數(shù)據(jù)轉(zhuǎn)換成兼容另一個(gè)系統(tǒng)能識(shí)別的格式;會(huì)話層,負(fù)責(zé)建立、管理和終止表示層實(shí)體之間的通信會(huì)話;傳輸層,負(fù)責(zé)端到端的數(shù)據(jù)傳輸;網(wǎng)絡(luò)層,負(fù)責(zé)數(shù)據(jù)的路由、轉(zhuǎn)發(fā)、分片;數(shù)據(jù)鏈路層,負(fù)責(zé)數(shù)據(jù)的封幀和差錯(cuò)檢測(cè),以及 MAC 尋址;物理層,負(fù)責(zé)在物理網(wǎng)絡(luò)中傳輸數(shù)據(jù)幀;
由于 OSI 模型實(shí)在太復(fù)雜,提出的也只是概念理論上的分層,并沒(méi)有提供具體的實(shí)現(xiàn)方案。事實(shí)上,我們比較常見(jiàn),也比較實(shí)用的是四層模型,即 TCP/IP 網(wǎng)絡(luò)模型,Linux 系統(tǒng)正是按照這套網(wǎng)絡(luò)模型來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)協(xié)議棧的。
TCP/IP模型
TCP/IP協(xié)議被組織成四個(gè)概念層,其中有三層對(duì)應(yīng)于ISO參考模型中的相應(yīng)層。ICP/IP協(xié)議族并不包含物理層和數(shù)據(jù)鏈路層,因此它不能獨(dú)立完成整個(gè)計(jì)算機(jī)網(wǎng)絡(luò)系統(tǒng)的功能,必須與許多其他的協(xié)議協(xié)同工作。TCP/IP 網(wǎng)絡(luò)通常是由上到下分成 4 層,分別是應(yīng)用層,傳輸層,網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層。
- 應(yīng)用層 支持 HTTP、SMTP 等最終用戶進(jìn)程傳輸層 處理主機(jī)到主機(jī)的通信(TCP、UDP)網(wǎng)絡(luò)層 尋址和路由數(shù)據(jù)包(IP 協(xié)議)鏈路層 通過(guò)網(wǎng)絡(luò)的物理電線、電纜或無(wú)線信道移動(dòng)比特
TCP和UDP區(qū)別是什么?
- 連接:TCP 是面向連接的傳輸層協(xié)議,傳輸數(shù)據(jù)前先要建立連接;UDP 是不需要連接,即刻傳輸數(shù)據(jù)。服務(wù)對(duì)象:TCP 是一對(duì)一的兩點(diǎn)服務(wù),即一條連接只有兩個(gè)端點(diǎn)。UDP 支持一對(duì)一、一對(duì)多、多對(duì)多的交互通信可靠性:TCP 是可靠交付數(shù)據(jù)的,數(shù)據(jù)可以無(wú)差錯(cuò)、不丟失、不重復(fù)、按序到達(dá)。UDP 是盡最大努力交付,不保證可靠交付數(shù)據(jù)。但是我們可以基于 UDP 傳輸協(xié)議實(shí)現(xiàn)一個(gè)可靠的傳輸協(xié)議,比如 QUIC 協(xié)議擁塞控制、流量控制:TCP 有擁塞控制和流量控制機(jī)制,保證數(shù)據(jù)傳輸?shù)陌踩?。UDP 則沒(méi)有,即使網(wǎng)絡(luò)非常擁堵了,也不會(huì)影響 UDP 的發(fā)送速率。首部開(kāi)銷:TCP 首部長(zhǎng)度較長(zhǎng),會(huì)有一定的開(kāi)銷,首部在沒(méi)有使用「選項(xiàng)」字段時(shí)是 20 個(gè)字節(jié),如果使用了「選項(xiàng)」字段則會(huì)變長(zhǎng)的。UDP 首部只有 8 個(gè)字節(jié),并且是固定不變的,開(kāi)銷較小。傳輸方式:TCP 是流式傳輸,沒(méi)有邊界,但保證順序和可靠。UDP 是一個(gè)包一個(gè)包的發(fā)送,是有邊界的,但可能會(huì)丟包和亂序。
操作系統(tǒng)中進(jìn)程之間的通信有哪些
Linux 內(nèi)核提供了不少進(jìn)程間通信的方式:
- 管道消息隊(duì)列共享內(nèi)存信號(hào)信號(hào)量socket
Linux 內(nèi)核提供了不少進(jìn)程間通信的方式,其中最簡(jiǎn)單的方式就是管道,管道分為「匿名管道」和「命名管道」。
匿名管道顧名思義,它沒(méi)有名字標(biāo)識(shí),匿名管道是特殊文件只存在于內(nèi)存,沒(méi)有存在于文件系統(tǒng)中,shell 命令中的「|」豎線就是匿名管道,通信的數(shù)據(jù)是無(wú)格式的流并且大小受限,通信的方式是單向的,數(shù)據(jù)只能在一個(gè)方向上流動(dòng),如果要雙向通信,需要?jiǎng)?chuàng)建兩個(gè)管道,再來(lái)匿名管道是只能用于存在父子關(guān)系的進(jìn)程間通信,匿名管道的生命周期隨著進(jìn)程創(chuàng)建而建立,隨著進(jìn)程終止而消失。
命名管道突破了匿名管道只能在親緣關(guān)系進(jìn)程間的通信限制,因?yàn)槭褂妹艿赖那疤?,需要在文件系統(tǒng)創(chuàng)建一個(gè)類型為 p 的設(shè)備文件,那么毫無(wú)關(guān)系的進(jìn)程就可以通過(guò)這個(gè)設(shè)備文件進(jìn)行通信。另外,不管是匿名管道還是命名管道,進(jìn)程寫入的數(shù)據(jù)都是緩存在內(nèi)核中,另一個(gè)進(jìn)程讀取數(shù)據(jù)時(shí)候自然也是從內(nèi)核中獲取,同時(shí)通信數(shù)據(jù)都遵循先進(jìn)先出原則,不支持 lseek 之類的文件定位操作。
消息隊(duì)列克服了管道通信的數(shù)據(jù)是無(wú)格式的字節(jié)流的問(wèn)題,消息隊(duì)列實(shí)際上是保存在內(nèi)核的「消息鏈表」,消息隊(duì)列的消息體是可以用戶自定義的數(shù)據(jù)類型,發(fā)送數(shù)據(jù)時(shí),會(huì)被分成一個(gè)一個(gè)獨(dú)立的消息體,當(dāng)然接收數(shù)據(jù)時(shí),也要與發(fā)送方發(fā)送的消息體的數(shù)據(jù)類型保持一致,這樣才能保證讀取的數(shù)據(jù)是正確的。消息隊(duì)列通信的速度不是最及時(shí)的,畢竟每次數(shù)據(jù)的寫入和讀取都需要經(jīng)過(guò)用戶態(tài)與內(nèi)核態(tài)之間的拷貝過(guò)程。
共享內(nèi)存可以解決消息隊(duì)列通信中用戶態(tài)與內(nèi)核態(tài)之間數(shù)據(jù)拷貝過(guò)程帶來(lái)的開(kāi)銷,它直接分配一個(gè)共享空間,每個(gè)進(jìn)程都可以直接訪問(wèn),就像訪問(wèn)進(jìn)程自己的空間一樣快捷方便,不需要陷入內(nèi)核態(tài)或者系統(tǒng)調(diào)用,大大提高了通信的速度,享有最快的進(jìn)程間通信方式之名。但是便捷高效的共享內(nèi)存通信,帶來(lái)新的問(wèn)題,多進(jìn)程競(jìng)爭(zhēng)同個(gè)共享資源會(huì)造成數(shù)據(jù)的錯(cuò)亂。
那么,就需要信號(hào)量來(lái)保護(hù)共享資源,以確保任何時(shí)刻只能有一個(gè)進(jìn)程訪問(wèn)共享資源,這種方式就是互斥訪問(wèn)。信號(hào)量不僅可以實(shí)現(xiàn)訪問(wèn)的互斥性,還可以實(shí)現(xiàn)進(jìn)程間的同步,信號(hào)量其實(shí)是一個(gè)計(jì)數(shù)器,表示的是資源個(gè)數(shù),其值可以通過(guò)兩個(gè)原子操作來(lái)控制,分別是 P 操作和 V 操作。
與信號(hào)量名字很相似的叫信號(hào),它倆名字雖然相似,但功能一點(diǎn)兒都不一樣。信號(hào)是異步通信機(jī)制,信號(hào)可以在應(yīng)用進(jìn)程和內(nèi)核之間直接交互,內(nèi)核也可以利用信號(hào)來(lái)通知用戶空間的進(jìn)程發(fā)生了哪些系統(tǒng)事件,信號(hào)事件的來(lái)源主要有硬件來(lái)源(如鍵盤 Cltr+C )和軟件來(lái)源(如 kill 命令),一旦有信號(hào)發(fā)生,進(jìn)程有三種方式響應(yīng)信號(hào) 1. 執(zhí)行默認(rèn)操作、2. 捕捉信號(hào)、3. 忽略信號(hào)。有兩個(gè)信號(hào)是應(yīng)用進(jìn)程無(wú)法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,這是為了方便我們能在任何時(shí)候結(jié)束或停止某個(gè)進(jìn)程。
前面說(shuō)到的通信機(jī)制,都是工作于同一臺(tái)主機(jī),如果要與不同主機(jī)的進(jìn)程間通信,那么就需要 Socket 通信了。Socket 實(shí)際上不僅用于不同的主機(jī)進(jìn)程間通信,還可以用于本地主機(jī)進(jìn)程間通信,可根據(jù)創(chuàng)建 Socket 的類型不同,分為三種常見(jiàn)的通信方式,一個(gè)是基于 TCP 協(xié)議的通信方式,一個(gè)是基于 UDP 協(xié)議的通信方式,一個(gè)是本地進(jìn)程間通信方式。
TCP的擁塞控制介紹一下?
一般來(lái)說(shuō),計(jì)算機(jī)網(wǎng)絡(luò)都處在一個(gè)共享的環(huán)境。因此也有可能會(huì)因?yàn)槠渌鳈C(jī)之間的通信使得網(wǎng)絡(luò)擁堵。
在網(wǎng)絡(luò)出現(xiàn)擁堵時(shí),如果繼續(xù)發(fā)送大量數(shù)據(jù)包,可能會(huì)導(dǎo)致數(shù)據(jù)包時(shí)延、丟失等,這時(shí) TCP 就會(huì)重傳數(shù)據(jù),但是一重傳就會(huì)導(dǎo)致網(wǎng)絡(luò)的負(fù)擔(dān)更重,于是會(huì)導(dǎo)致更大的延遲以及更多的丟包,這個(gè)情況就會(huì)進(jìn)入惡性循環(huán)被不斷地放大....
所以,TCP 不能忽略網(wǎng)絡(luò)上發(fā)生的事,它被設(shè)計(jì)成一個(gè)無(wú)私的協(xié)議,當(dāng)網(wǎng)絡(luò)發(fā)送擁塞時(shí),TCP 會(huì)自我犧牲,降低發(fā)送的數(shù)據(jù)量。
于是,就有了擁塞控制,控制的目的就是避免「發(fā)送方」的數(shù)據(jù)填滿整個(gè)網(wǎng)絡(luò)。
為了在「發(fā)送方」調(diào)節(jié)所要發(fā)送數(shù)據(jù)的量,定義了一個(gè)叫做「擁塞窗口」的概念。
擁塞窗口 cwnd是發(fā)送方維護(hù)的一個(gè)的狀態(tài)變量,它會(huì)根據(jù)網(wǎng)絡(luò)的擁塞程度動(dòng)態(tài)變化的。發(fā)送窗口 swnd 和接收窗口 rwnd 是約等于的關(guān)系,那么由于加入了擁塞窗口的概念后,此時(shí)發(fā)送窗口的值是swnd = min(cwnd, rwnd),也就是擁塞窗口和接收窗口中的最小值。
擁塞窗口 cwnd 變化的規(guī)則:
- 只要網(wǎng)絡(luò)中沒(méi)有出現(xiàn)擁塞,cwnd 就會(huì)增大;但網(wǎng)絡(luò)中出現(xiàn)了擁塞,cwnd 就減少;
那么怎么知道當(dāng)前網(wǎng)絡(luò)是否出現(xiàn)了擁塞呢?其實(shí)只要「發(fā)送方」沒(méi)有在規(guī)定時(shí)間內(nèi)接收到 ACK 應(yīng)答報(bào)文,也就是發(fā)生了超時(shí)重傳,就會(huì)認(rèn)為網(wǎng)絡(luò)出現(xiàn)了擁塞。
擁塞控制有哪些控制算法?
擁塞控制主要是四個(gè)算法:
- 慢啟動(dòng)擁塞避免擁塞發(fā)生快速恢復(fù)
慢啟動(dòng)
TCP 在剛建立連接完成后,首先是有個(gè)慢啟動(dòng)的過(guò)程,這個(gè)慢啟動(dòng)的意思就是一點(diǎn)一點(diǎn)的提高發(fā)送數(shù)據(jù)包的數(shù)量,如果一上來(lái)就發(fā)大量的數(shù)據(jù),這不是給網(wǎng)絡(luò)添堵嗎?慢啟動(dòng)的算法記住一個(gè)規(guī)則就行:當(dāng)發(fā)送方每收到一個(gè) ACK,擁塞窗口 cwnd 的大小就會(huì)加 1。這里假定擁塞窗口 cwnd 和發(fā)送窗口 swnd 相等,下面舉個(gè)栗子:
- 連接建立完成后,一開(kāi)始初始化 cwnd = 1,表示可以傳一個(gè) MSS 大小的數(shù)據(jù)。當(dāng)收到一個(gè) ACK 確認(rèn)應(yīng)答后,cwnd 增加 1,于是一次能夠發(fā)送 2 個(gè)當(dāng)收到 2 個(gè)的 ACK 確認(rèn)應(yīng)答后, cwnd 增加 2,于是就可以比之前多發(fā)2 個(gè),所以這一次能夠發(fā)送 4 個(gè)當(dāng)這 4 個(gè)的 ACK 確認(rèn)到來(lái)的時(shí)候,每個(gè)確認(rèn) cwnd 增加 1, 4 個(gè)確認(rèn) cwnd 增加 4,于是就可以比之前多發(fā) 4 個(gè),所以這一次能夠發(fā)送 8 個(gè)。
慢啟動(dòng)算法的變化過(guò)程如下圖:
可以看出慢啟動(dòng)算法,發(fā)包的個(gè)數(shù)是指數(shù)性的增長(zhǎng)。那慢啟動(dòng)漲到什么時(shí)候是個(gè)頭呢?有一個(gè)叫慢啟動(dòng)門限 ssthresh (slow start threshold)狀態(tài)變量。
- 當(dāng) cwnd < ssthresh 時(shí),使用慢啟動(dòng)算法。當(dāng) cwnd >= ssthresh 時(shí),就會(huì)使用「擁塞避免算法」。
擁塞避免算法
前面說(shuō)道,當(dāng)擁塞窗口 cwnd 「超過(guò)」慢啟動(dòng)門限 ssthresh 就會(huì)進(jìn)入擁塞避免算法。一般來(lái)說(shuō) ssthresh 的大小是 65535 字節(jié)。那么進(jìn)入擁塞避免算法后,它的規(guī)則是:每當(dāng)收到一個(gè) ACK 時(shí),cwnd 增加 1/cwnd。接上前面的慢啟動(dòng)的栗子,現(xiàn)假定 ssthresh 為 8:
-
-
- 當(dāng) 8 個(gè) ACK 應(yīng)答確認(rèn)到來(lái)時(shí),每個(gè)確認(rèn)增加 1/8,8 個(gè) ACK 確認(rèn) cwnd 一共增加 1,于是這一次能夠發(fā)送 9 個(gè) MSS 大小的數(shù)據(jù),變成了
線性增長(zhǎng)。
-
擁塞避免算法的變化過(guò)程如下圖:
所以,我們可以發(fā)現(xiàn),擁塞避免算法就是將原本慢啟動(dòng)算法的指數(shù)增長(zhǎng)變成了線性增長(zhǎng),還是增長(zhǎng)階段,但是增長(zhǎng)速度緩慢了一些。就這么一直增長(zhǎng)著后,網(wǎng)絡(luò)就會(huì)慢慢進(jìn)入了擁塞的狀況了,于是就會(huì)出現(xiàn)丟包現(xiàn)象,這時(shí)就需要對(duì)丟失的數(shù)據(jù)包進(jìn)行重傳。當(dāng)觸發(fā)了重傳機(jī)制,也就進(jìn)入了「擁塞發(fā)生算法」。
擁塞發(fā)生
當(dāng)網(wǎng)絡(luò)出現(xiàn)擁塞,也就是會(huì)發(fā)生數(shù)據(jù)包重傳,重傳機(jī)制主要有兩種:
- 超時(shí)重傳快速重傳
這兩種使用的擁塞發(fā)送算法是不同的,接下來(lái)分別來(lái)說(shuō)說(shuō)。發(fā)生超時(shí)重傳的擁塞發(fā)生算法當(dāng)發(fā)生了「超時(shí)重傳」,則就會(huì)使用擁塞發(fā)生算法。這個(gè)時(shí)候,ssthresh 和 cwnd 的值會(huì)發(fā)生變化:
- ssthresh 設(shè)為 cwnd/2,cwnd 重置為 1 (是恢復(fù)為 cwnd 初始化值,我這里假定 cwnd 初始化值 1)
擁塞發(fā)生算法的變化如下圖:
接著,就重新開(kāi)始慢啟動(dòng),慢啟動(dòng)是會(huì)突然減少數(shù)據(jù)流的。這真是一旦「超時(shí)重傳」,馬上回到解放前。但是這種方式太激進(jìn)了,反應(yīng)也很強(qiáng)烈,會(huì)造成網(wǎng)絡(luò)卡頓。就好像本來(lái)在秋名山高速漂移著,突然來(lái)個(gè)緊急剎車,輪胎受得了嗎。。。發(fā)生快速重傳的擁塞發(fā)生算法還有更好的方式,前面我們講過(guò)「快速重傳算法」。當(dāng)接收方發(fā)現(xiàn)丟了一個(gè)中間包的時(shí)候,發(fā)送三次前一個(gè)包的 ACK,于是發(fā)送端就會(huì)快速地重傳,不必等待超時(shí)再重傳。TCP 認(rèn)為這種情況不嚴(yán)重,因?yàn)榇蟛糠譀](méi)丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:
- cwnd = cwnd/2 ,也就是設(shè)置為原來(lái)的一半;ssthresh = cwnd;進(jìn)入快速恢復(fù)算法
快速恢復(fù)
快速重傳和快速恢復(fù)算法一般同時(shí)使用,快速恢復(fù)算法是認(rèn)為,你還能收到 3 個(gè)重復(fù) ACK 說(shuō)明網(wǎng)絡(luò)也不那么糟糕,所以沒(méi)有必要像 RTO 超時(shí)那么強(qiáng)烈。正如前面所說(shuō),進(jìn)入快速恢復(fù)之前,cwnd 和 ssthresh 已被更新了:
- cwnd = cwnd/2 ,也就是設(shè)置為原來(lái)的一半;ssthresh = cwnd;
然后,進(jìn)入快速恢復(fù)算法如下:
- 擁塞窗口 cwnd = ssthresh + 3 ( 3 的意思是確認(rèn)有 3 個(gè)數(shù)據(jù)包被收到了);重傳丟失的數(shù)據(jù)包;如果再收到重復(fù)的 ACK,那么 cwnd 增加 1;如果收到新數(shù)據(jù)的 ACK 后,把 cwnd 設(shè)置為第一步中的 ssthresh 的值,原因是該 ACK 確認(rèn)了新的數(shù)據(jù),說(shuō)明從 duplicated ACK 時(shí)的數(shù)據(jù)都已收到,該恢復(fù)過(guò)程已經(jīng)結(jié)束,可以回到恢復(fù)之前的狀態(tài)了,也即再次進(jìn)入擁塞避免狀態(tài);
快速恢復(fù)算法的變化過(guò)程如下圖:
也就是沒(méi)有像「超時(shí)重傳」一夜回到解放前,而是還在比較高的值,后續(xù)呈線性增長(zhǎng)。
cookie和session有什么區(qū)別?
Cookie和Session都是Web開(kāi)發(fā)中用于跟蹤用戶狀態(tài)的技術(shù),但它們?cè)诖鎯?chǔ)位置、數(shù)據(jù)容量、安全性以及生命周期等方面存在顯著差異:
存儲(chǔ)位置:Cookie的數(shù)據(jù)存儲(chǔ)在客戶端(通常是瀏覽器)。當(dāng)瀏覽器向服務(wù)器發(fā)送請(qǐng)求時(shí),會(huì)自動(dòng)附帶Cookie中的數(shù)據(jù)。Session的數(shù)據(jù)存儲(chǔ)在服務(wù)器端。服務(wù)器為每個(gè)用戶分配一個(gè)唯一的Session ID,這個(gè)ID通常通過(guò)Cookie或URL重寫的方式發(fā)送給客戶端,客戶端后續(xù)的請(qǐng)求會(huì)帶上這個(gè)Session ID,服務(wù)器根據(jù)ID查找對(duì)應(yīng)的Session數(shù)據(jù)。
數(shù)據(jù)容量:單個(gè)Cookie的大小限制通常在4KB左右,而且大多數(shù)瀏覽器對(duì)每個(gè)域名的總Cookie數(shù)量也有限制。由于Session存儲(chǔ)在服務(wù)器上,理論上不受數(shù)據(jù)大小的限制,主要受限于服務(wù)器的內(nèi)存大小。
安全性:Cookie相對(duì)不安全,因?yàn)閿?shù)據(jù)存儲(chǔ)在客戶端,容易受到XSS(跨站腳本攻擊)的威脅。不過(guò),可以通過(guò)設(shè)置HttpOnly屬性來(lái)防止JavaScript訪問(wèn),減少XSS攻擊的風(fēng)險(xiǎn),但仍然可能受到CSRF(跨站請(qǐng)求偽造)的攻擊。Session通常認(rèn)為比Cookie更安全,因?yàn)槊舾袛?shù)據(jù)存儲(chǔ)在服務(wù)器端。但仍然需要防范Session劫持(通過(guò)獲取他人的Session ID)和會(huì)話固定攻擊。
生命周期:Cookie可以設(shè)置過(guò)期時(shí)間,過(guò)期后自動(dòng)刪除。也可以設(shè)置為會(huì)話Cookie,即瀏覽器關(guān)閉時(shí)自動(dòng)刪除。Session在默認(rèn)情況下,當(dāng)用戶關(guān)閉瀏覽器時(shí),Session結(jié)束。但服務(wù)器也可以設(shè)置Session的超時(shí)時(shí)間,超過(guò)這個(gè)時(shí)間未活動(dòng),Session也會(huì)失效。
性能:使用Cookie時(shí),因?yàn)閿?shù)據(jù)隨每個(gè)請(qǐng)求發(fā)送到服務(wù)器,可能會(huì)影響網(wǎng)絡(luò)傳輸效率,尤其是在Cookie數(shù)據(jù)較大時(shí)。使用Session時(shí),因?yàn)閿?shù)據(jù)存儲(chǔ)在服務(wù)器端,每次請(qǐng)求都需要查詢服務(wù)器上的Session數(shù)據(jù),這可能會(huì)增加服務(wù)器的負(fù)載,特別是在高并發(fā)場(chǎng)景下。
Java
介紹一下線程池的工作原理
線程池原理
線程池是為了減少頻繁的創(chuàng)建線程和銷毀線程帶來(lái)的性能損耗,線程池的工作原理如下圖:
線程池分為核心線程池,線程池的最大容量,還有等待任務(wù)的隊(duì)列,提交一個(gè)任務(wù),如果核心線程沒(méi)有滿,就創(chuàng)建一個(gè)線程,如果滿了,就是會(huì)加入等待隊(duì)列,如果等待隊(duì)列滿了,就會(huì)增加線程,如果達(dá)到最大線程數(shù)量,如果都達(dá)到最大線程數(shù)量,就會(huì)按照一些丟棄的策略進(jìn)行處理。
線程池的參數(shù)有哪些?
線程池的構(gòu)造函數(shù)有7個(gè)參數(shù):
corePoolSize:線程池核心線程數(shù)量。默認(rèn)情況下,線程池中線程的數(shù)量如果 <= corePoolSize,那么即使這些線程處于空閑狀態(tài),那也不會(huì)被銷毀。
maximumPoolSize:線程池中最多可容納的線程數(shù)量。當(dāng)一個(gè)新任務(wù)交給線程池,如果此時(shí)線程池中有空閑的線程,就會(huì)直接執(zhí)行,如果沒(méi)有空閑的線程且當(dāng)前線程池的線程數(shù)量小于corePoolSize,就會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù),否則就會(huì)將該任務(wù)加入到阻塞隊(duì)列中,如果阻塞隊(duì)列滿了,就會(huì)創(chuàng)建一個(gè)新線程,從阻塞隊(duì)列頭部取出一個(gè)任務(wù)來(lái)執(zhí)行,并將新任務(wù)加入到阻塞隊(duì)列末尾。如果當(dāng)前線程池中線程的數(shù)量等于maximumPoolSize,就不會(huì)創(chuàng)建新線程,就會(huì)去執(zhí)行拒絕策略。
keepAliveTime:當(dāng)線程池中線程的數(shù)量大于corePoolSize,并且某個(gè)線程的空閑時(shí)間超過(guò)了keepAliveTime,那么這個(gè)線程就會(huì)被銷毀。
unit:就是keepAliveTime時(shí)間的單位。
workQueue:工作隊(duì)列。當(dāng)沒(méi)有空閑的線程執(zhí)行新任務(wù)時(shí),該任務(wù)就會(huì)被放入工作隊(duì)列中,等待執(zhí)行。
threadFactory:線程工廠。可以用來(lái)給線程取名字等等
handler:拒絕策略。當(dāng)一個(gè)新任務(wù)交給線程池,如果此時(shí)線程池中有空閑的線程,就會(huì)直接執(zhí)行,如果沒(méi)有空閑的線程,就會(huì)將該任務(wù)加入到阻塞隊(duì)列中,如果阻塞隊(duì)列滿了,就會(huì)創(chuàng)建一個(gè)新線程,從阻塞隊(duì)列頭部取出一個(gè)任務(wù)來(lái)執(zhí)行,并將新任務(wù)加入到阻塞隊(duì)列末尾。如果當(dāng)前線程池中線程的數(shù)量等于maximumPoolSize,就不會(huì)創(chuàng)建新線程,就會(huì)去執(zhí)行拒絕策略
線程池種類有哪些?
- FixedThreadPool:它的核心線程數(shù)和最大線程數(shù)是一樣的,所以可以把它看作是固定線程數(shù)的線程池,它的特點(diǎn)是線程池中的線程數(shù)除了初始階段需要從 0 開(kāi)始增加外,之后的線程數(shù)量就是固定的,就算任務(wù)數(shù)超過(guò)線程數(shù),線程池也不會(huì)再創(chuàng)建更多的線程來(lái)處理任務(wù),而是會(huì)把超出線程處理能力的任務(wù)放到任務(wù)隊(duì)列中進(jìn)行等待。而且就算任務(wù)隊(duì)列滿了,到了本該繼續(xù)增加線程數(shù)的時(shí)候,由于它的最大線程數(shù)和核心線程數(shù)是一樣的,所以也無(wú)法再增加新的線程了。
ExecutorService?executor?=?Executors.newFixedThreadPool(5);
- CachedThreadPool:可以稱作可緩存線程池,它的特點(diǎn)在于線程數(shù)是幾乎可以無(wú)限增加的(實(shí)際最大可以達(dá)到 Integer.MAX_VALUE,為 2^31-1,這個(gè)數(shù)非常大,所以基本不可能達(dá)到),而當(dāng)線程閑置時(shí)還可以對(duì)線程進(jìn)行回收。也就是說(shuō)該線程池的線程數(shù)量不是固定不變的,當(dāng)然它也有一個(gè)用于存儲(chǔ)提交任務(wù)的隊(duì)列,但這個(gè)隊(duì)列是 SynchronousQueue,隊(duì)列的容量為0,實(shí)際不存儲(chǔ)任何任務(wù),它只負(fù)責(zé)對(duì)任務(wù)進(jìn)行中轉(zhuǎn)和傳遞,所以效率比較高。
ExecutorService?executor?=?Executors.newCachedThreadPool();
- SingleThreadExecutor:它會(huì)使用唯一的線程去執(zhí)行任務(wù),原理和 FixedThreadPool 是一樣的,只不過(guò)這里線程只有一個(gè),如果線程在執(zhí)行任務(wù)的過(guò)程中發(fā)生異常,線程池也會(huì)重新創(chuàng)建一個(gè)線程來(lái)執(zhí)行后續(xù)的任務(wù)。這種線程池由于只有一個(gè)線程,所以非常適合用于所有任務(wù)都需要按被提交的順序依次執(zhí)行的場(chǎng)景,而前幾種線程池不一定能夠保障任務(wù)的執(zhí)行順序等于被提交的順序,因?yàn)樗鼈兪嵌嗑€程并行執(zhí)行的。
ExecutorService?executor?=?Executors.newSingleThreadExecutor();
- SingleThreadScheduledExecutor:它實(shí)際和 ScheduledThreadPool 線程池非常相似,它只是 ScheduledThreadPool 的一個(gè)特例,內(nèi)部只有一個(gè)線程。
ExecutorService?executor?=?Executors.newScheduledThreadPool(5);
Java線程的狀態(tài)有哪些?
源自《Java并發(fā)編程藝術(shù)》 java.lang.Thread.State枚舉類中定義了六種線程的狀態(tài),可以調(diào)用線程Thread中的getState()方法獲取當(dāng)前線程的狀態(tài)。
線程狀態(tài) | 解釋 |
---|---|
NEW | 尚未啟動(dòng)的線程狀態(tài),即線程創(chuàng)建,還未調(diào)用start方法 |
RUNNABLE | 就緒狀態(tài)(調(diào)用start,等待調(diào)度)+正在運(yùn)行 |
BLOCKED | 等待監(jiān)視器鎖時(shí),陷入阻塞狀態(tài) |
WAITING | 等待狀態(tài)的線程正在等待另一線程執(zhí)行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待時(shí)間的等待狀態(tài) |
TERMINATED | 線程完成執(zhí)行,終止?fàn)顟B(tài) |
blocked和waiting有啥區(qū)別
觸發(fā)條件:線程進(jìn)入BLOCKED狀態(tài)通常是因?yàn)樵噲D獲取一個(gè)對(duì)象的鎖(monitor lock),但該鎖已經(jīng)被另一個(gè)線程持有。這通常發(fā)生在嘗試進(jìn)入synchronized塊或方法時(shí),如果鎖已被占用,則線程將被阻塞直到鎖可用。線程進(jìn)入WAITING狀態(tài)是因?yàn)樗诘却硪粋€(gè)線程執(zhí)行某些操作,例如調(diào)用Object.wait()方法、Thread.join()方法或LockSupport.park()方法。在這種狀態(tài)下,線程將不會(huì)消耗CPU資源,并且不會(huì)參與鎖的競(jìng)爭(zhēng)。
喚醒機(jī)制:當(dāng)一個(gè)線程被阻塞等待鎖時(shí),一旦鎖被釋放,線程將有機(jī)會(huì)重新嘗試獲取鎖。如果鎖此時(shí)未被其他線程獲取,那么線程可以從BLOCKED狀態(tài)變?yōu)镽UNNABLE狀態(tài)。線程在WAITING狀態(tài)中需要被顯式喚醒。例如,如果線程調(diào)用了Object.wait(),那么它必須等待另一個(gè)線程調(diào)用同一對(duì)象上的Object.notify()或Object.notifyAll()方法才能被喚醒。
synchronized和reentrantlock及其應(yīng)用場(chǎng)景
synchronized 工作原理
synchronized是Java提供的原子性內(nèi)置鎖,這種內(nèi)置的并且使用者看不到的鎖也被稱為監(jiān)視器鎖,
使用synchronized之后,會(huì)在編譯之后在同步的代碼塊前后加上monitorenter和monitorexit字節(jié)碼指令,他依賴操作系統(tǒng)底層互斥鎖實(shí)現(xiàn)。他的作用主要就是實(shí)現(xiàn)原子性操作和解決共享變量的內(nèi)存可見(jiàn)性問(wèn)題。
執(zhí)行monitorenter指令時(shí)會(huì)嘗試獲取對(duì)象鎖,如果對(duì)象沒(méi)有被鎖定或者已經(jīng)獲得了鎖,鎖的計(jì)數(shù)器+1。此時(shí)其他競(jìng)爭(zhēng)鎖的線程則會(huì)進(jìn)入等待隊(duì)列中。執(zhí)行monitorexit指令時(shí)則會(huì)把計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器值為0時(shí),則鎖釋放,處于等待隊(duì)列中的線程再繼續(xù)競(jìng)爭(zhēng)鎖。
synchronized是排它鎖,當(dāng)一個(gè)線程獲得鎖之后,其他線程必須等待該線程釋放鎖后才能獲得鎖,而且由于Java中的線程和操作系統(tǒng)原生線程是一一對(duì)應(yīng)的,線程被阻塞或者喚醒時(shí)時(shí)會(huì)從用戶態(tài)切換到內(nèi)核態(tài),這種轉(zhuǎn)換非常消耗性能。
從內(nèi)存語(yǔ)義來(lái)說(shuō),加鎖的過(guò)程會(huì)清除工作內(nèi)存中的共享變量,再?gòu)闹鲀?nèi)存讀取,而釋放鎖的過(guò)程則是將工作內(nèi)存中的共享變量寫回主內(nèi)存。
實(shí)際上大部分時(shí)候我認(rèn)為說(shuō)到monitorenter就行了,但是為了更清楚的描述,還是再具體一點(diǎn)。
如果再深入到源碼來(lái)說(shuō),synchronized實(shí)際上有兩個(gè)隊(duì)列waitSet和entryList。
- 當(dāng)多個(gè)線程進(jìn)入同步代碼塊時(shí),首先進(jìn)入entryList有一個(gè)線程獲取到monitor鎖后,就賦值給當(dāng)前線程,并且計(jì)數(shù)器+1如果線程調(diào)用wait方法,將釋放鎖,當(dāng)前線程置為null,計(jì)數(shù)器-1,同時(shí)進(jìn)入waitSet等待被喚醒,調(diào)用notify或者notifyAll之后又會(huì)進(jìn)入entryList競(jìng)爭(zhēng)鎖如果線程執(zhí)行完畢,同樣釋放鎖,計(jì)數(shù)器-1,當(dāng)前線程置為null
reentrantlock工作原理
ReentrantLock 的底層實(shí)現(xiàn)主要依賴于 AbstractQueuedSynchronizer(AQS)這個(gè)抽象類。AQS 是一個(gè)提供了基本同步機(jī)制的框架,其中包括了隊(duì)列、狀態(tài)值等。
ReentrantLock 在 AQS 的基礎(chǔ)上通過(guò)內(nèi)部類 Sync 來(lái)實(shí)現(xiàn)具體的鎖操作。
不同的 Sync 子類實(shí)現(xiàn)了公平鎖和非公平鎖的不同邏輯。
可中斷性:ReentrantLock 實(shí)現(xiàn)了可中斷性,這意味著線程在等待鎖的過(guò)程中,可以被其他線程中斷而提前結(jié)束等待。在底層,ReentrantLock 使用了與 LockSupport.park() 和 LockSupport.unpark() 相關(guān)的機(jī)制來(lái)實(shí)現(xiàn)可中斷性。
設(shè)置超時(shí)時(shí)間:ReentrantLock 支持在嘗試獲取鎖時(shí)設(shè)置超時(shí)時(shí)間,即等待一定時(shí)間后如果還未獲得鎖,則放棄鎖的獲取。這是通過(guò)內(nèi)部的 tryAcquireNanos 方法來(lái)實(shí)現(xiàn)的。
公平鎖和非公平鎖:在直接創(chuàng)建 ReentrantLock 對(duì)象時(shí),默認(rèn)情況下是非公平鎖。公平鎖是按照線程等待的順序來(lái)獲取鎖,而非公平鎖則允許多個(gè)線程在同一時(shí)刻競(jìng)爭(zhēng)鎖,不考慮它們申請(qǐng)鎖的順序。公平鎖可以通過(guò)在創(chuàng)建 ReentrantLock 時(shí)傳入 true 來(lái)設(shè)置,例如:
ReentrantLock?fairLock?=?new?ReentrantLock(true);
多個(gè)條件變量
- :ReentrantLock 支持多個(gè)條件變量,每個(gè)條件變量可以與一個(gè) ReentrantLock 關(guān)聯(lián)。這使得線程可以更靈活地進(jìn)行等待和喚醒操作,而不僅僅是基于對(duì)象監(jiān)視器的 wait() 和 notify()。多個(gè)條件變量的實(shí)現(xiàn)依賴于 Condition 接口,例如:
ReentrantLock?lock?=?new?ReentrantLock();
Condition?condition?=?lock.newCondition();
//?使用下面方法進(jìn)行等待和喚醒
condition.await();
condition.signal();
可重入性:ReentrantLock 支持可重入性,即同一個(gè)線程可以多次獲得同一把鎖,而不會(huì)造成死鎖。這是通過(guò)內(nèi)部的 holdCount 計(jì)數(shù)來(lái)實(shí)現(xiàn)的。當(dāng)一個(gè)線程多次獲取鎖時(shí),holdCount 遞增,釋放鎖時(shí)遞減,只有當(dāng) holdCount 為零時(shí),其他線程才有機(jī)會(huì)獲取鎖。
應(yīng)用場(chǎng)景的區(qū)別
synchronized:
簡(jiǎn)單同步需求:當(dāng)你需要對(duì)代碼塊或方法進(jìn)行簡(jiǎn)單的同步控制時(shí),synchronized是一個(gè)很好的選擇。它使用起來(lái)簡(jiǎn)單,不需要額外的資源管理,因?yàn)殒i會(huì)在方法退出或代碼塊執(zhí)行完畢后自動(dòng)釋放。
代碼塊同步:如果你想對(duì)特定代碼段進(jìn)行同步,而不是整個(gè)方法,可以使用synchronized
代碼塊。這可以讓你更精細(xì)地控制同步的范圍,從而減少鎖的持有時(shí)間,提高并發(fā)性能。
內(nèi)置鎖的使用:synchronized
關(guān)鍵字使用對(duì)象的內(nèi)置鎖(也稱為監(jiān)視器鎖),這在需要使用對(duì)象作為鎖對(duì)象的情況下很有用,尤其是在對(duì)象狀態(tài)與鎖保護(hù)的代碼緊密相關(guān)時(shí)。
ReentrantLock:
高級(jí)鎖功能需求:ReentrantLock
提供了synchronized所不具備的高級(jí)功能,如公平鎖、響應(yīng)中斷、定時(shí)鎖嘗試、以及多個(gè)條件變量。當(dāng)你需要這些功能時(shí),ReentrantLock
是更好的選擇。
性能優(yōu)化:在高度競(jìng)爭(zhēng)的環(huán)境中,ReentrantLock
可以提供比synchronized
更好的性能,因?yàn)樗峁┝烁?xì)粒度的控制,如嘗試鎖定和定時(shí)鎖定,可以減少線程阻塞的可能性。
復(fù)雜同步結(jié)構(gòu):當(dāng)你需要更復(fù)雜的同步結(jié)構(gòu),如需要多個(gè)條件變量來(lái)協(xié)調(diào)線程之間的通信時(shí),ReentrantLock
及其配套的Condition
對(duì)象可以提供更靈活的解決方案。
綜上,synchronized
適用于簡(jiǎn)單同步需求和不需要額外鎖功能的場(chǎng)景,而ReentrantLock
適用于需要更高級(jí)鎖功能、性能優(yōu)化或復(fù)雜同步邏輯的情況。選擇哪種同步機(jī)制取決于具體的應(yīng)用需求和性能考慮。
線程池一般是怎么用的?
Java 中的 Executors 類定義了一些快捷的工具方法,來(lái)幫助我們快速創(chuàng)建線程池。《阿里巴巴 Java 開(kāi)發(fā)手冊(cè)》中提到,禁止使用這些方法來(lái)創(chuàng)建線程池,而應(yīng)該手動(dòng) new ThreadPoolExecutor 來(lái)創(chuàng)建線程池。這一條規(guī)則的背后,是大量血淋淋的生產(chǎn)事故,最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因?yàn)橘Y源耗盡導(dǎo)致 OOM 問(wèn)題。所以,不建議使用 Executors 提供的兩種快捷的線程池,原因如下:
- 我們需要根據(jù)自己的場(chǎng)景、并發(fā)情況來(lái)評(píng)估線程池的幾個(gè)核心參數(shù),包括核心線程數(shù)、最大線程數(shù)、線程回收策略、工作隊(duì)列的類型,以及拒絕策略,確保線程池的工作行為符合需求,一般都需要設(shè)置有界的工作隊(duì)列和可控的線程數(shù)。任何時(shí)候,都應(yīng)該為自定義線程池指定有意義的名稱,以方便排查問(wèn)題。當(dāng)出現(xiàn)線程數(shù)量暴增、線程死鎖、線程占用大量 CPU、線程執(zhí)行出現(xiàn)異常等問(wèn)題時(shí),我們往往會(huì)抓取線程棧。此時(shí),有意義的線程名稱,就可以方便我們定位問(wèn)題。
除了建議手動(dòng)聲明線程池以外,我還建議用一些監(jiān)控手段來(lái)觀察線程池的狀態(tài)。線程池這個(gè)組件往往會(huì)表現(xiàn)得任勞任怨、默默無(wú)聞,除非是出現(xiàn)了拒絕策略,否則壓力再大都不會(huì)拋出一個(gè)異常。如果我們能提前觀察到線程池隊(duì)列的積壓,或者線程數(shù)量的快速膨脹,往往可以提早發(fā)現(xiàn)并解決問(wèn)題。
Spring IoC和AOP 介紹一下
Spring IoC和AOP 區(qū)別:
IoC:即控制反轉(zhuǎn)的意思,它是一種創(chuàng)建和獲取對(duì)象的技術(shù)思想,依賴注入(DI)是實(shí)現(xiàn)這種技術(shù)的一種方式。傳統(tǒng)開(kāi)發(fā)過(guò)程中,我們需要通過(guò)new關(guān)鍵字來(lái)創(chuàng)建對(duì)象。使用IoC思想開(kāi)發(fā)方式的話,我們不通過(guò)new關(guān)鍵字創(chuàng)建對(duì)象,而是通過(guò)IoC容器來(lái)幫我們實(shí)例化對(duì)象。通過(guò)IoC的方式,可以大大降低對(duì)象之間的耦合度。
AOP:是面向切面編程,能夠?qū)⒛切┡c業(yè)務(wù)無(wú)關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯封裝起來(lái),以減少系統(tǒng)的重復(fù)代碼,降低模塊間的耦合度。Spring AOP 就是基于動(dòng)態(tài)代理的,如果要代理的對(duì)象,實(shí)現(xiàn)了某個(gè)接口,那么 Spring AOP 會(huì)使用 JDK Proxy,去創(chuàng)建代理對(duì)象,而對(duì)于沒(méi)有實(shí)現(xiàn)接口的對(duì)象,就無(wú)法使用 JDK Proxy 去進(jìn)行代理了,這時(shí)候 Spring AOP 會(huì)使用 Cglib 生成一個(gè)被代理對(duì)象的子類來(lái)作為代理。
在 Spring 框架中,IOC 和 AOP 結(jié)合使用,可以更好地實(shí)現(xiàn)代碼的模塊化和分層管理。例如:
- 通過(guò) IOC 容器管理對(duì)象的依賴關(guān)系,然后通過(guò) AOP 將橫切關(guān)注點(diǎn)統(tǒng)一切入到需要的業(yè)務(wù)邏輯中。使用 IOC 容器管理 Service 層和 DAO 層的依賴關(guān)系,然后通過(guò) AOP 在 Service 層實(shí)現(xiàn)事務(wù)管理、日志記錄等橫切功能,使得業(yè)務(wù)邏輯更加清晰和可維護(hù)。
動(dòng)態(tài)代理是什么?
Java的動(dòng)態(tài)代理是一種在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建代理對(duì)象的機(jī)制,主要用于在不修改原始類的情況下對(duì)方法調(diào)用進(jìn)行攔截和增強(qiáng)。Java動(dòng)態(tài)代理主要分為兩種類型:
基于接口的代理
-
- (JDK動(dòng)態(tài)代理):這種類型的代理要求目標(biāo)對(duì)象必須實(shí)現(xiàn)至少一個(gè)接口。Java動(dòng)態(tài)代理會(huì)創(chuàng)建一個(gè)實(shí)現(xiàn)了相同接口的代理類,然后在運(yùn)行時(shí)動(dòng)態(tài)生成該類的實(shí)例。這種代理的實(shí)現(xiàn)核心是java.lang.reflect.Proxy
- 類和java.lang.reflect.InvocationHandler接口。每一個(gè)動(dòng)態(tài)代理類都必須實(shí)現(xiàn)InvocationHandler接口,并且每個(gè)代理類的實(shí)例都關(guān)聯(lián)到一個(gè)handler。當(dāng)通過(guò)代理對(duì)象調(diào)用一個(gè)方法時(shí),這個(gè)方法的調(diào)用會(huì)被轉(zhuǎn)發(fā)為由InvocationHandler接口的invoke()方法來(lái)進(jìn)行調(diào)用。
基于類的代理
- (CGLIB動(dòng)態(tài)代理):CGLIB(Code Generation Library)是一個(gè)強(qiáng)大的高性能的代碼生成庫(kù),它可以在運(yùn)行時(shí)動(dòng)態(tài)生成一個(gè)目標(biāo)類的子類。CGLIB代理不需要目標(biāo)類實(shí)現(xiàn)接口,而是通過(guò)繼承的方式創(chuàng)建代理類。因此,如果目標(biāo)對(duì)象沒(méi)有實(shí)現(xiàn)任何接口,可以使用CGLIB來(lái)創(chuàng)建動(dòng)態(tài)代理。