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

























