每次面完騰訊,都是一把汗......
大家好,我是小林。
今天給大家分享一位 Java 后端同學(xué)的騰訊面經(jīng),問的問題還是比較多的,接近 30 個問題,再加上寫算法,一場面試下來,時長有 1 小時+。
面試的強度還是很大,很多同學(xué)跟我反饋,每次面完騰訊都一把汗,甚至有同學(xué)被騰訊面了 2 小時+。
考察的知識還是比較多的,我這里簡單給在大家列了一下:
- 操作系統(tǒng):進程&線程、進程隔離性
- 數(shù)據(jù)結(jié)構(gòu):排序算法、排序穩(wěn)定性、歸并排序、快速排序
- MySQL:存儲引擎、聚簇索引、B+樹、索引失效、事務(wù)隔離級別、臟讀、幻讀
- Redis:數(shù)據(jù)類型、String 底層實現(xiàn)、熱 key
- Java:ArrayList、Vector、HashMap
- MQ:消息隊列選型、消息可靠性、消息確認(rèn)機制、Kafka 、RocketMQ
總體考察的范圍,就是編程語言+計算機基礎(chǔ)+后端組件。
操作系統(tǒng)
進程和線程的區(qū)別
圖片
- 本質(zhì)區(qū)別:進程是操作系統(tǒng)資源分配的基本單位,而線程是任務(wù)調(diào)度和執(zhí)行的基本單位
- 在開銷方面:每個進程都有獨立的代碼和數(shù)據(jù)空間(程序上下文),程序之間的切換會有較大的開銷;線程可以看做輕量級的進程,同一類線程共享代碼和數(shù)據(jù)空間,每個線程都有自己獨立的運行棧和程序計數(shù)器(PC),線程之間切換的開銷小
- 穩(wěn)定性方面:進程中某個線程如果崩潰了,可能會導(dǎo)致整個進程都崩潰。而進程中的子進程崩潰,并不會影響其他進程。
- 內(nèi)存分配方面:系統(tǒng)在運行的時候會為每個進程分配不同的內(nèi)存空間;而對線程而言,除了CPU外,系統(tǒng)不會為線程分配內(nèi)存(線程所使用的資源來自其所屬進程的資源),線程組之間只能共享資源
- 包含關(guān)系:沒有線程的進程可以看做是單線程的,如果一個進程內(nèi)有多個線程,則執(zhí)行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,所以線程也被稱為輕權(quán)進程或者輕量級進程
為什么進程崩潰不會對其他進程產(chǎn)生很大影響
主要是因為:
- 進程隔離性:每個進程都有自己獨立的內(nèi)存空間,當(dāng)一個進程崩潰時,其內(nèi)存空間會被操作系統(tǒng)回收,不會影響其他進程的內(nèi)存空間。這種進程間的隔離性保證了一個進程崩潰不會直接影響其他進程的執(zhí)行。
- 進程獨立性:每個進程都是獨立運行的,它們之間不會共享資源,如文件、網(wǎng)絡(luò)連接等。因此,一個進程的崩潰通常不會對其他進程的資源產(chǎn)生影響。
數(shù)據(jù)結(jié)構(gòu)
知道哪些排序算法,時間復(fù)雜度
圖片
- 冒泡排序:通過相鄰元素的比較和交換,每次將最大(或最?。┑脑刂鸩健懊芭荨钡阶詈螅ɑ蜃钋埃r間復(fù)雜度:最好情況下O(n),最壞情況下O(n^2),平均情況下O(n^2)。,空間復(fù)雜度:O(1)。
- 插入排序:將待排序元素逐個插入到已排序序列的合適位置,形成有序序列。時間復(fù)雜度:最好情況下O(n),最壞情況下O(n^2),平均情況下O(n^2),空間復(fù)雜度:O(1)。
- 選擇排序:通過不斷選擇未排序部分的最?。ɑ蜃畲螅┰?,并將其放置在已排序部分的末尾(或開頭)。時間復(fù)雜度:最好情況下O(n^2),最壞情況下O(n^2),平均情況下O(n^2)。空間復(fù)雜度:O(1)。
- 快速排序:通過選擇一個基準(zhǔn)元素,將數(shù)組劃分為兩個子數(shù)組,使得左子數(shù)組的元素都小于(或等于)基準(zhǔn)元素,右子數(shù)組的元素都大于(或等于)基準(zhǔn)元素,然后對子數(shù)組進行遞歸排序。時間復(fù)雜度:最好情況下O(nlogn),最壞情況下O(n^2),平均情況下O(nlogn)。空間復(fù)雜度:最好情況下O(logn),最壞情況下O(n)。
- 歸并排序:將數(shù)組不斷分割為更小的子數(shù)組,然后將子數(shù)組進行合并,合并過程中進行排序。時間復(fù)雜度:最好情況下O(nlogn),最壞情況下O(nlogn),平均情況下O(nlogn)??臻g復(fù)雜度:O(n)。
- 堆排序:通過將待排序元素構(gòu)建成一個最大堆(或最小堆),然后將堆頂元素與末尾元素交換,再重新調(diào)整堆,重復(fù)該過程直到排序完成。時間復(fù)雜度:最好情況下O(nlogn),最壞情況下O(nlogn),平均情況下O(nlogn)??臻g復(fù)雜度:O(1)。
歸并排序和快速排序的使用場景
- 歸并排序是穩(wěn)定排序算法,適合排序穩(wěn)定的場景;
- 快速排序是不穩(wěn)定排序算法,不適合排序穩(wěn)定的場景,快速排序是目前基于比較的內(nèi)部排序中被認(rèn)為是最好的方法,當(dāng)待排序的關(guān)鍵字是隨機分布時,快速排序的平均時間最短;
排序穩(wěn)定是什么意思?
圖片
排序穩(wěn)定指的是在排序過程中,對于具有相同排序關(guān)鍵字的元素,在排序后它們的相對位置保持不變。
換句話說,如果在排序前兩個元素 A 和 B 的值相等,并且 A 在 B 的前面,那么在排序后 A 仍然在 B 的前面,這樣的排序就是穩(wěn)定排序。穩(wěn)定排序保持了相同元素之間的順序關(guān)系,適用于需要保持原始順序的場景。
穩(wěn)定和不穩(wěn)定排序算法有什么特點?
穩(wěn)定排序算法的特點:
- 相同元素的相對位置不會改變,排序后仍然保持原始順序。
- 適用于需要保持元素間相對順序關(guān)系的場景,如按照年齡排序后按姓名排序。
不穩(wěn)定排序算法的特點:
- 相同元素的相對位置可能會改變,排序后不保證原始順序。
- 可能會更快,但不適用于需要保持元素間相對順序關(guān)系的場景。
MySQL
MySQL 的存儲引擎有哪些?為什么常用InnoDB?
MySQL 的存儲引擎常用的主要有 3 個:
- InnoDB存儲引擎:支持事務(wù)處理,支持外鍵,支持崩潰修復(fù)能力和并發(fā)控制。如果需要對事務(wù)的完整性要求比較高(比如銀行),要求實現(xiàn)并發(fā)控制(比如售票),那選擇InnoDB有很大的優(yōu)勢。如果需要頻繁的更新、刪除操作的數(shù)據(jù)庫,也可以選擇InnoDB,因為支持事務(wù)的提交(commit)和回滾(rollback)。
- MyISAM存儲引擎:插入數(shù)據(jù)快,空間和內(nèi)存使用比較低。如果表主要是用于插入新記錄和讀出記錄,那么選擇MyISAM能實現(xiàn)處理高效率。如果應(yīng)用的完整性、并發(fā)性要求比 較低,也可以使用。如果數(shù)據(jù)表主要用來插入和查詢記錄,則MyISAM引擎能提供較高的處理效率
- MEMORY存儲引擎:所有的數(shù)據(jù)都在內(nèi)存中,數(shù)據(jù)的處理速度快,但是安全性不高。如果需要很快的讀寫速度,對數(shù)據(jù)的安全性要求較低,可以選擇MEMOEY。它對表的大小有要求,不能建立太大的表。所以,這類數(shù)據(jù)庫只使用在相對較小的數(shù)據(jù)庫表。如果只是臨時存放數(shù)據(jù),數(shù)據(jù)量不大,并且不需要較高的數(shù)據(jù)安全性,可以選擇將數(shù)據(jù)保存在內(nèi)存中的Memory引擎,MySQL中使用該引擎作為臨時表,存放查詢的中間結(jié)果
常用InnoDB的原因是支持事務(wù),且最小鎖的粒度是行級鎖。
B+ 樹和 B 樹的比較
B 樹和 B+ 都是通過多叉樹的方式,會將樹的高度變矮,所以這兩個數(shù)據(jù)結(jié)構(gòu)非常適合檢索存于磁盤中的數(shù)據(jù)。
但是 MySQL 默認(rèn)的存儲引擎 InnoDB 采用的是 B+ 作為索引的數(shù)據(jù)結(jié)構(gòu),原因有:
- B+ 樹的非葉子節(jié)點不存放實際的記錄數(shù)據(jù),僅存放索引,因此數(shù)據(jù)量相同的情況下,相比存儲即存索引又存記錄的 B 樹,B+樹的非葉子節(jié)點可以存放更多的索引,因此 B+ 樹可以比 B 樹更「矮胖」,查詢底層節(jié)點的磁盤 I/O次數(shù)會更少。
- B+ 樹有大量的冗余節(jié)點(所有非葉子節(jié)點都是冗余索引),這些冗余索引讓 B+ 樹在插入、刪除的效率都更高,比如刪除根節(jié)點的時候,不會像 B 樹那樣會發(fā)生復(fù)雜的樹的變化;
- B+ 樹葉子節(jié)點之間用鏈表連接了起來,有利于范圍查詢,而 B 樹要實現(xiàn)范圍查詢,因此只能通過樹的遍歷來完成范圍查詢,這會涉及多個節(jié)點的磁盤 I/O 操作,范圍查詢效率不如 B+ 樹。
除了聚簇索引,還有什么索引?
還有二級索引、聯(lián)合索引、前綴索引、唯一索引等。
二級索引存放的有哪些數(shù)據(jù)?
索引又可以分成聚簇索引和非聚簇索引(二級索引),它們區(qū)別就在于葉子節(jié)點存放的是什么數(shù)據(jù):
- 聚簇索引的葉子節(jié)點存放的是實際數(shù)據(jù),所有完整的用戶記錄都存放在聚簇索引的葉子節(jié)點;
- 二級索引的葉子節(jié)點存放的是主鍵值,而不是實際數(shù)據(jù)。
索引失效的情況
索引失效的情況:
- 當(dāng)我們使用左或者左右模糊匹配的時候,也就是 like %xx 或者 like %xx%這兩種方式都會造成索引失效;
- 當(dāng)我們在查詢條件中對索引列使用函數(shù),就會導(dǎo)致索引失效。
- 當(dāng)我們在查詢條件中對索引列進行表達式計算,也是無法走索引的。
- MySQL 在遇到字符串和數(shù)字比較的時候,會自動把字符串轉(zhuǎn)為數(shù)字,然后再進行比較。如果字符串是索引列,而條件語句中的輸入?yún)?shù)是數(shù)字的話,那么索引列會發(fā)生隱式類型轉(zhuǎn)換,由于隱式類型轉(zhuǎn)換是通過 CAST 函數(shù)實現(xiàn)的,等同于對索引列使用了函數(shù),所以就會導(dǎo)致索引失效。
- 聯(lián)合索引要能正確使用需要遵循最左匹配原則,也就是按照最左優(yōu)先的方式進行索引的匹配,否則就會導(dǎo)致索引失效。
事務(wù)隔離級別有哪些?
四個隔離級別如下:
- 讀未提交,指一個事務(wù)還沒提交時,它做的變更就能被其他事務(wù)看到;
- 讀提交,指一個事務(wù)提交之后,它做的變更才能被其他事務(wù)看到;
- 可重復(fù)讀,指一個事務(wù)執(zhí)行過程中看到的數(shù)據(jù),一直跟這個事務(wù)啟動時看到的數(shù)據(jù)是一致的,MySQL InnoDB 引擎的默認(rèn)隔離級別;
- 串行化;會對記錄加上讀寫鎖,在多個事務(wù)對這條記錄進行讀寫操作時,如果發(fā)生了讀寫沖突的時候,后訪問的事務(wù)必須等前一個事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行;
按隔離水平高低排序如下:
圖片
什么情況下會出現(xiàn)幻讀?
在一個事務(wù)內(nèi)多次查詢某個符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
舉個栗子。
假設(shè)有 A 和 B 這兩個事務(wù)同時在處理,事務(wù) A 先開始從數(shù)據(jù)庫查詢賬戶余額大于 100 萬的記錄,發(fā)現(xiàn)共有 5 條,然后事務(wù) B 也按相同的搜索條件也是查詢出了 5 條記錄。
圖片
接下來,事務(wù) A 插入了一條余額超過 100 萬的賬號,并提交了事務(wù),此時數(shù)據(jù)庫超過 100 萬余額的賬號個數(shù)就變?yōu)?6。
然后事務(wù) B 再次查詢賬戶余額大于 100 萬的記錄,此時查詢到的記錄數(shù)量有 6 條,發(fā)現(xiàn)和前一次讀到的記錄數(shù)量不一樣了,就感覺發(fā)生了幻覺一樣,這種現(xiàn)象就被稱為幻讀。
事務(wù)的 MVCC 是怎么實現(xiàn)的?
對于「讀提交」和「可重復(fù)讀」隔離級別的事務(wù)來說,它們是通過 Read View 來實現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時機不同:
- 「讀提交」隔離級別是在每個 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ā)控制)。
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
圖片
一個事務(wù)去訪問記錄的時候,除了自己的更新記錄總是可見之外,還有這幾種情況:
- 如果記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個版本的記錄是在創(chuàng)建 Read View 前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)可見。
- 如果記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個版本的記錄是在創(chuàng)建 Read View 后才啟動的事務(wù)生成的,所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中:
- 如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒提交事務(wù)),所以該版本的記錄對當(dāng)前事務(wù)不可見。
- 如果記錄的 trx_id 不在 m_ids列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對當(dāng)前事務(wù)可見。
這種通過「版本鏈」來控制并發(fā)事務(wù)訪問同一個記錄時的行為就叫 MVCC(多版本并發(fā)控制)。
事務(wù)之間怎么避免臟讀的?
針對不同的隔離級別,并發(fā)事務(wù)時可能發(fā)生的現(xiàn)象也會不同。
圖片
也就是說:
- 在「讀未提交」隔離級別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀現(xiàn)象;
- 在「讀提交」隔離級別下,可能發(fā)生不可重復(fù)讀和幻讀現(xiàn)象,但是不可能發(fā)生臟讀現(xiàn)象;
- 在「可重復(fù)讀」隔離級別下,可能發(fā)生幻讀現(xiàn)象,但是不可能臟讀和不可重復(fù)讀現(xiàn)象;
- 在「串行化」隔離級別下,臟讀、不可重復(fù)讀和幻讀現(xiàn)象都不可能會發(fā)生。
所以,要解決臟讀現(xiàn)象,就要升級到「讀提交」以上的隔離級別,這樣事務(wù)只能讀到其他事務(wù)已經(jīng)提交完成的數(shù)據(jù),而不會讀到未提交事務(wù)的數(shù)據(jù),就避免臟讀的問題。
Redis
Redis 支持哪幾種數(shù)據(jù)類型?
Redis 提供了豐富的數(shù)據(jù)類型,常見的有五種數(shù)據(jù)類型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
圖片
圖片
隨著 Redis 版本的更新,后面又支持了四種數(shù)據(jù)類型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五種數(shù)據(jù)類型的應(yīng)用場景:
- String 類型的應(yīng)用場景:緩存對象、常規(guī)計數(shù)、分布式鎖、共享 session 信息等。
- List 類型的應(yīng)用場景:消息隊列(但是有兩個問題:1. 生產(chǎn)者需要自行實現(xiàn)全局唯一 ID;2. 不能以消費組形式消費數(shù)據(jù))等。
- Hash 類型:緩存對象、購物車等。
- Set 類型:聚合計算(并集、交集、差集)場景,比如點贊、共同關(guān)注、抽獎活動等。
- Zset 類型:排序場景,比如排行榜、電話和姓名排序等。
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,支持以消費組形式消費數(shù)據(jù)。
熱 key 是什么?怎么解決?
Redis熱key是指被頻繁訪問的key,可能會導(dǎo)致單個key的訪問量過大,影響系統(tǒng)性能。解決方法包括:
- 開啟內(nèi)存淘汰機制,并選擇使用LRU算法來淘汰不常用的key,保證內(nèi)存中存儲的是最熱門的數(shù)據(jù)。
- 設(shè)置key的過期時間,確保key在一段時間后自動刪除,防止長時間占用內(nèi)存。
- 對熱點key進行分片,將數(shù)據(jù)分散存儲在不同的節(jié)點上,減輕單個key的壓力。
String 是使用什么存儲的?為什么不用 c 語言中的字符串?
Redis 的 String 字符串是用 SDS 數(shù)據(jù)結(jié)構(gòu)存儲的。
下圖就是 Redis 5.0 的 SDS 的數(shù)據(jù)結(jié)構(gòu):
圖片
結(jié)構(gòu)中的每個成員變量分別介紹下:
- len,記錄了字符串長度。這樣獲取字符串長度的時候,只需要返回這個成員變量值就行,時間復(fù)雜度只需要 O(1)。
- alloc,分配給字符數(shù)組的空間長度。這樣在修改字符串的時候,可以通過 alloc - len 計算出剩余的空間大小,可以用來判斷空間是否滿足修改需求,如果不滿足的話,就會自動將 SDS 的空間擴展至執(zhí)行修改所需的大小,然后才執(zhí)行實際的修改操作,所以使用 SDS 既不需要手動修改 SDS 的空間大小,也不會出現(xiàn)前面所說的緩沖區(qū)溢出的問題。
- flags,用來表示不同類型的 SDS。一共設(shè)計了 5 種類型,分別是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在說明區(qū)別之處。
- buf[],字符數(shù)組,用來保存實際數(shù)據(jù)。不僅可以保存字符串,也可以保存二進制數(shù)據(jù)。
總的來說,Redis 的 SDS 結(jié)構(gòu)在原本字符數(shù)組之上,增加了三個元數(shù)據(jù):len、alloc、flags,用來解決 C 語言字符串的缺陷。
O(1)復(fù)雜度獲取字符串長度
C 語言的字符串長度獲取 strlen 函數(shù),需要通過遍歷的方式來統(tǒng)計字符串長度,時間復(fù)雜度是 O(N)。
而 Redis 的 SDS 結(jié)構(gòu)因為加入了 len 成員變量,那么獲取字符串長度的時候,直接返回這個成員變量的值就行,所以復(fù)雜度只有 O(1)。
二進制安全
因為 SDS 不需要用 “\0” 字符來標(biāo)識字符串結(jié)尾了,而是有個專門的 len 成員變量來記錄長度,所以可存儲包含 “\0” 的數(shù)據(jù)。但是 SDS 為了兼容部分 C 語言標(biāo)準(zhǔn)庫的函數(shù), SDS 字符串結(jié)尾還是會加上 “\0” 字符。
因此, SDS 的 API 都是以處理二進制的方式來處理 SDS 存放在 buf[] 里的數(shù)據(jù),程序不會對其中的數(shù)據(jù)做任何限制,數(shù)據(jù)寫入的時候時什么樣的,它被讀取時就是什么樣的。
通過使用二進制安全的 SDS,而不是 C 字符串,使得 Redis 不僅可以保存文本數(shù)據(jù),也可以保存任意格式的二進制數(shù)據(jù)。
不會發(fā)生緩沖區(qū)溢出
C 語言的字符串標(biāo)準(zhǔn)庫提供的字符串操作函數(shù),大多數(shù)(比如 strcat 追加字符串函數(shù))都是不安全的,因為這些函數(shù)把緩沖區(qū)大小是否滿足操作需求的工作交由開發(fā)者來保證,程序內(nèi)部并不會判斷緩沖區(qū)大小是否足夠用,當(dāng)發(fā)生了緩沖區(qū)溢出就有可能造成程序異常結(jié)束。
所以,Redis 的 SDS 結(jié)構(gòu)里引入了 alloc 和 len 成員變量,這樣 SDS API 通過 alloc - len 計算,可以算出剩余可用的空間大小,這樣在對字符串做修改操作的時候,就可以由程序內(nèi)部判斷緩沖區(qū)大小是否足夠用。
而且,當(dāng)判斷出緩沖區(qū)大小不夠用時,Redis 會自動將擴大 SDS 的空間大小,以滿足修改所需的大小。
Java
編譯型語言和解釋型語言的區(qū)別?
編譯型語言和解釋型語言的區(qū)別在于:
- 編譯型語言:在程序執(zhí)行之前,整個源代碼會被編譯成機器碼或者字節(jié)碼,生成可執(zhí)行文件。執(zhí)行時直接運行編譯后的代碼,速度快,但跨平臺性較差。
- 解釋型語言:在程序執(zhí)行時,逐行解釋執(zhí)行源代碼,不生成獨立的可執(zhí)行文件。通常由解釋器動態(tài)解釋并執(zhí)行代碼,跨平臺性好,但執(zhí)行速度相對較慢。
- 典型的編譯型語言如C、C++,典型的解釋型語言如Python、JavaScript。
動態(tài)數(shù)組的實現(xiàn)有哪些?
ArrayList和Vector都支持動態(tài)擴容,都屬于動態(tài)數(shù)組。
ArrayList 和 Vector 的比較
- 線程安全性:Vector是線程安全的,ArrayList不是線程安全的。
- 擴容策略:ArrayList在底層數(shù)組不夠用時在原來的基礎(chǔ)上擴展0.5倍,Vector是擴展1倍。
HashMap 的擴容條件是什么?
Java7 的 HashMap 擴容必須滿足兩個條件:
- 當(dāng)前數(shù)據(jù)存儲的數(shù)量(即size())大小必須大于等于閾值
- 當(dāng)前加入的數(shù)據(jù)是否發(fā)生了hash沖突
因為上面這兩個條件,所以存在下面這些情況:
- 第一種情況,就是hashmap在存值的時候(默認(rèn)大小為16,負(fù)載因子0.75,閾值12),可能達到最后存滿16個值的時候,再存入第17個值才會發(fā)生擴容現(xiàn)象,因為前16個值,每個值在底層數(shù)組中分別占據(jù)一個位置,并沒有發(fā)生hash碰撞。
- 第二種情況,有可能存儲更多值(超多16個值,最多可以存26個值)都還沒有擴容。原理:前11個值全部hash碰撞,存到數(shù)組的同一個位置(雖然hash沖突,但是這時元素個數(shù)小于閾值12,并沒有同時滿足擴容的兩個條件。所以不會擴容),后面所有存入的15個值全部分散到數(shù)組剩下的15個位置(這時元素個數(shù)大于等于閾值,但是每次存入的元素并沒有發(fā)生hash碰撞,也沒有同時滿足擴容的兩個條件,所以也不會擴容),前面11+15=26,所以在存入第27個值的時候才同時滿足上面兩個條件,這時候才會發(fā)生擴容現(xiàn)象。
Java8 不再像 Java7中那樣需要滿足兩個條件,Java8中擴容只需要滿足一個條件:
- 當(dāng)前存放新值的時候已有元素的個數(shù)大于等于閾值
MQ
用的什么消息隊列,消息隊列怎么選型的?
項目用的是 RocketMQ 消息隊列。選擇RocketMQ的原因是:
- 開發(fā)語言優(yōu)勢。RocketMQ 使用 Java 語言開發(fā),比起使用 Erlang 開發(fā)的 RabbitMQ 來說,有著更容易上手的閱讀體驗和受眾。在遇到 RocketMQ 較為底層的問題時,大部分熟悉 Java 的同學(xué)都可以深入閱讀其源碼,分析、排查問題。
- 社區(qū)氛圍活躍。RocketMQ 是阿里巴巴開源且內(nèi)部在大量使用的消息隊列,說明 RocketMQ 是的確經(jīng)得起殘酷的生產(chǎn)環(huán)境考驗的,并且能夠針對線上環(huán)境復(fù)雜的需求場景提供相應(yīng)的解決方案。
- 特性豐富。根據(jù) RocketMQ 官方文檔的列舉,其高級特性達到了 12 種,例如順序消息、事務(wù)消息、消息過濾、定時消息等。順序消息、事務(wù)消息、消息過濾、定時消息。RocketMQ 豐富的特性,能夠為我們在復(fù)雜的業(yè)務(wù)場景下盡可能多地提供思路及解決方案。
有沒有消息積壓的問題
導(dǎo)致消息積壓突然增加,最粗粒度的原因,只有兩種:要么是發(fā)送變快了,要么是消費變慢了。
要解決積壓的問題,可以通過擴容消費端的實例數(shù)來提升總體的消費能力。
如果短時間內(nèi)沒有足夠的服務(wù)器資源進行擴容,沒辦法的辦法是,將系統(tǒng)降級,通過關(guān)閉一些不重要的業(yè)務(wù),減少發(fā)送方發(fā)送的數(shù)據(jù)量,最低限度讓系統(tǒng)還能正常運轉(zhuǎn),服務(wù)一些重要業(yè)務(wù)。
RocketMQ 消息可靠性怎么保證?
使用一個消息隊列,其實就分為三大塊:生產(chǎn)者、中間件、消費者,所以要保證消息就是保證三個環(huán)節(jié)都不能丟失數(shù)據(jù)。
圖片
- 消息生產(chǎn)階段:生產(chǎn)者會不會丟消息,取決于生產(chǎn)者對于異常情況的處理是否合理。從消息被生產(chǎn)出來,然后提交給 MQ 的過程中,只要能正常收到 ( MQ 中間件) 的 ack 確認(rèn)響應(yīng),就表示發(fā)送成功,所以只要處理好返回值和異常,如果返回異常則進行消息重發(fā),那么這個階段是不會出現(xiàn)消息丟失的。
- 消息存儲階段:RabbitMQ 或 Kafka 這類專業(yè)的隊列中間件,在使用時是部署一個集群,生產(chǎn)者在發(fā)布消息時,隊列中間件通常會寫「多個節(jié)點」,也就是有多個副本,這樣一來,即便其中一個節(jié)點掛了,也能保證集群的數(shù)據(jù)不丟失。
- 消息消費階段:消費者接收消息+消息處理之后,才回復(fù) ack 的話,那么消息階段的消息不會丟失。不能收到消息就回 ack,否則可能消息處理中途掛掉了,消息就丟失了。
Kafka 和 RocketMQ 消息確認(rèn)機制有什么不同?
Kafka的消息確認(rèn)機制有三種:0,1,-1:
- ACK=0:這是最不可靠的模式。生產(chǎn)者在發(fā)送消息后不會等待來自服務(wù)器的確認(rèn)。這意味著消息可能會在發(fā)送之后丟失,而生產(chǎn)者將無法知道它是否成功到達服務(wù)器。
- ACK=1:這是默認(rèn)模式,也是一種折衷方式。在這種模式下,生產(chǎn)者會在消息發(fā)送后等待來自分區(qū)領(lǐng)導(dǎo)者(leader)的確認(rèn),但不會等待所有副本(replicas)的確認(rèn)。這意味著只要消息被寫入分區(qū)領(lǐng)導(dǎo)者,生產(chǎn)者就會收到確認(rèn)。如果分區(qū)領(lǐng)導(dǎo)者成功寫入消息,但在同步到所有副本之前宕機,消息可能會丟失。
- ACK=-1:這是最可靠的模式。在這種模式下,生產(chǎn)者會在消息發(fā)送后等待所有副本的確認(rèn)。只有在所有副本都成功寫入消息后,生產(chǎn)者才會收到確認(rèn)。這確保了消息的可靠性,但會導(dǎo)致更長的延遲。
RocketMQ 提供了三種消息發(fā)送方式:同步發(fā)送、異步發(fā)送和單向發(fā)送:
- 同步發(fā)送:是指消息發(fā)送方發(fā)出一條消息后,會在收到服務(wù)端同步響應(yīng)之后才發(fā)下一條消息的通訊方式。應(yīng)用場景非常廣泛,例如重要通知郵件、報名短信通知、營銷短信系統(tǒng)等。
- 異步發(fā)送:是指發(fā)送方發(fā)出一條消息后,不等服務(wù)端返回響應(yīng),接著發(fā)送下一條消息的通訊方式,但是需要實現(xiàn)異步發(fā)送回調(diào)接口(SendCallback)。消息發(fā)送方在發(fā)送了一條消息后,不需要等待服務(wù)端響應(yīng)即可發(fā)送第二條消息。發(fā)送方通過回調(diào)接口接收服務(wù)端響應(yīng),并處理響應(yīng)結(jié)果。適用于鏈路耗時較長,對響應(yīng)時間較為敏感的業(yè)務(wù)場景,例如,視頻上傳后通知啟動轉(zhuǎn)碼服務(wù),轉(zhuǎn)碼完成后通知推送轉(zhuǎn)碼結(jié)果等。
- 單向發(fā)送:發(fā)送方只負(fù)責(zé)發(fā)送消息,不等待服務(wù)端返回響應(yīng)且沒有回調(diào)函數(shù)觸發(fā),即只發(fā)送請求不等待應(yīng)答。此方式發(fā)送消息的過程耗時非常短,一般在微秒級別。適用于某些耗時非常短,但對可靠性要求并不高的場景,例如日志收集。
Kafka 和 RocketMQ 的 broker 架構(gòu)有什么區(qū)別
- Kafka 的 broker 架構(gòu):Kafka 的 broker 架構(gòu)采用了分布式的設(shè)計,每個 Kafka broker 是一個獨立的服務(wù)實例,負(fù)責(zé)存儲和處理一部分消息數(shù)據(jù)。Kafka 的 topic 被分區(qū)存儲在不同的 broker 上,實現(xiàn)了水平擴展和高可用性。
- RocketMQ 的 broker 架構(gòu):RocketMQ 的 broker 架構(gòu)也是分布式的,但是每個 RocketMQ broker 有主從之分,一個主節(jié)點和多個從節(jié)點組成一個 broker 集群。主節(jié)點負(fù)責(zé)消息的寫入和消費者的拉取,從節(jié)點負(fù)責(zé)消息的復(fù)制和消費者的負(fù)載均衡,提高了消息的可靠性和可用性。
其他
- 手撕:字符串乘法,輸出 2 的 1000 次方
- 反問:流程,業(yè)務(wù)