- 什么是RabbitMQ,特點是什么
- 怎么理解保障消息的一致性
- String、StringBuffer、StringBuilder
- 解釋一下線程安全
- 先操作數據庫再刪緩存還是先刪緩存再操作數據庫
- 這種辦法能杜絕數據不一致問題嗎
- 解釋一下AOP
- 介紹Redis的特點(Redis比較快)
- Redis為什么快
- 解釋一下Redis的數據類型以及各個數據類型的功能
- list是怎么實現的
- Redis是單線程的嗎,為什么
- 解釋一下垃圾回收機制
- 筆試:三數之和
如果問到項目中使用的技術,我們的回答應該包括這幾個層面
- 這個技術是什么?
- 解決了什么問題?
- 為什么不用其他的?
- 此技術對當前項目當前架構的適配性
涉及到項目內容的回答請結合自己項目作答,這里不舉例,感謝理解。
1. 什么是RabbitMQ,特點是什么
RabbitMQ 是一個 基于 AMQP(高級消息隊列協議)實現的開源消息中間件,它的核心功能是消息的可靠投遞和異步解耦。
- 它支持生產者(Producer)發送消息、消息存儲與路由、消費者(Consumer)訂閱與消費。
- 提供 可靠性(持久化、確認機制)、靈活的路由策略(Exchange + Binding) 和 消息堆積削峰能力。
特點:(結合項目中的使用引導面試官)
- 可靠性: RabbitMQ 使用一些機制來保證可靠性, 如持久化、傳輸確認及發布確認等。
- 靈活的路由 : 在消息進入隊列之前,通過交換器來路由消息。對于典型的路由功能, RabbitMQ 己經提供了一些內置的交換器來實現。針對更復雜的路由功能,可以將多個交換器綁定在一起, 也可以通過插件機制來實現自己的交換器。
- 擴展性: 多個 RabbitMQ 節點可以組成一個集群,也可以根據實際業務情況動態地擴展 集群中節點。
- 高可用性 : 隊列可以在集群中的機器上設置鏡像,使得在部分節點出現問題的情況下隊 列仍然可用。
- 多種協議: RabbitMQ 除了原生支持 AMQP 協議,還支持 STOMP, MQTT 等多種消息 中間件協議。
- 多語言客戶端 :RabbitMQ 幾乎支持所有常用語言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。
- 管理界面 : RabbitMQ 提供了一個易用的用戶界面,使得用戶可以監控和管理消息、集 群中的節點等。
- 插件機制 : RabbitMQ 提供了許多插件 , 以實現從多方面進行擴展,當然也可以編寫自己的插件。
2. 怎么理解保障消息的一致性
在 RabbitMQ 中,保障消息一致性主要是指 保證消息在生產者、隊列和消費者之間不會丟失或重復。
答案可以從以下幾個方面組織:
- 消息可靠投遞:RabbitMQ 提供多種機制確保消息可靠送達:
- Publisher Confirm(生產者確認):生產者發送消息后,可以等待 RabbitMQ 返回確認(ack),確保消息已經成功入隊。
- Mandatory 標記 + Return 回調:當消息無法路由到隊列時,生產者能收到通知,避免消息丟失。
- 隊列持久化:
- 隊列和消息都可以設置持久化,即使 RabbitMQ 宕機重啟,消息仍然存在磁盤上。
- 搭配 消息確認機制(ack),消費者處理完消息才通知 RabbitMQ 刪除,這樣避免消息處理失敗導致丟失。
- 事務機制:RabbitMQ 支持事務模式( txSelect、txCommit、txRollback ),可以保證一批消息要么全部成功,要么全部失敗,但性能開銷非常大。一般我們不會使用。
3. String、StringBuffer、StringBuilder的區別
- 可變性:
String
是不可變的(Immutable),創建之后內容修改,每次修改都會創建一個新對象。StringBuffer
和StringBuilder
是可變的(mutable),允許修改儲存的內容而不會創建新的對象。 - 線程安全性:
String
因為不可變,所以天然線程安全。StringBuilder
是不是線程安全的,適用于單線程環境。StringBuffer
是線程安全的,其方法通過synchronized
關鍵字實現同步,適用于多線程環境。 - 性能:
String
的性能最低,尤其是涉及大量字符串的修改時會產生大量臨時對象,增加內存開銷和垃圾回收壓力。StringBuffer
性能最高,因為沒有線程安全的開銷,適合單線程下的字符串操作。StringBuilder
性能略低于StringBuffer
,因為他的線程安全機制引入了部分開銷。 - 使用場景:如果字符串內容固定或不常變化,使用
String
。如果需要頻繁修改并且在單線程下,使用StringBuilder
。如果在頻繁修改并且在多線程環境下,使用StringBuffer
。
對比總結如下:
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
不可變性 | 不可變 | 可變 | 可變 |
線程安全 | 安全 | 不安全 | 安全 |
性能 | 低(頻繁修改時) | 高(單線程) | 中(多線程安全) |
使用場景 | 靜態字符串 | 單線程動態字符串 | 多線程動態字符串 |
4. 解釋一下線程安全
線程安全是指在多線程環境下,程序的執行結果是正確的、可預期的,不會因為線程的并發執行而出現數據錯誤或狀態不一致。
線程不安全往往出現在多個線程同時讀寫共享變量時,可能導致競態條件。
常見的解決方式有:
- synchronized關鍵字: 可以使用
synchronized
關鍵字來同步代碼塊或方法,確保同一時刻只有一個線程可以訪問這些代碼。對象鎖是通過synchronized
關鍵字鎖定對象的監視器(monitor)來實現的。 - volatile關鍵字:volatile關鍵字用于變量,確保所有線程看到的是該變量的最新值,而不是可能存儲在本地寄存器中的副本。
- Lock接口和ReentrantLock類:
java.util.concurrent.locks.Lock
接口提供了比synchronized
更強大的鎖定機制,ReentrantLock
是一個實現該接口的例子,提供了更靈活的鎖管理和更高的性能。 - 原子類:Java并發庫(
java.util.concurrent.atomic
)提供了原子類,如AtomicInteger
、AtomicLong
等,這些類提供了原子操作,可以用于更新基本類型的變量而無需額外的同步。 - 線程局部變量:
ThreadLocal
類可以為每個線程提供獨立的變量副本,這樣每個線程都擁有自己的變量,消除了競爭條件。 - 并發集合:使用
java.util.concurrent
包中的線程安全集合,如ConcurrentHashMap
、ConcurrentLinkedQueue
等,這些集合內部已經實現了線程安全的邏輯。 - JUC 工具類: 使用
<font style="background-color:rgba(27, 31, 35, 0.05);">java.util.concurrent</font>
包中的一些工具類可以用于控制線程間的同步和協作。例如:<font style="background-color:rgba(27, 31, 35, 0.05);">Semaphore</font>
和<font style="background-color:rgba(27, 31, 35, 0.05);">CyclicBarrier</font>
等。
5. 先操作數據庫再刪緩存還是先刪緩存再操作數據庫
結論是,推薦先操作數據庫,再刪緩存(Cache Aside Pattern):
如果先刪緩存再操作數據庫:
當前線程一旦刪掉緩存,其他線程查緩存未命中,去查數據庫把舊數據寫到緩存中。隨后當前線程再更新數據庫,這就導致數據不一致了。而且這種情況是非常容易出現的。
如果先操作數據庫再刪緩存:
在已經保證數據庫中是最新數據的情況下,這種情況下即使出現了緩存不一致,下次緩存失效或者淘汰時再查數據庫更新緩存,依舊可以保證數據的一致性。
話雖然這么說,但是也不代表這種方法就能杜絕數據不一致問題,只是概率要小于前者。
6. 這種辦法能杜絕數據不一致問題嗎
不能完全杜絕。
比如,線程一查緩存未命中,去查數據庫準備把數據寫入緩存。突然,線程二更新了數據庫,并刪除了緩存。恰好這時,線程一把數據庫舊值寫入了緩存,這就導致數據不一致了。
所以在極端情況下,數據不一致問題還是會出現的,還是得看業務場景對不一致數據的容忍程度。一般情況下,推薦使用這種操作,并根據業務場景進一步優化。
7. 解釋一下AOP
Spring AOP是Spring框架中的一個重要模塊,用于實現面向切面編程。
在 AOP 中最小的單元是“切面”。一個“切面”可以包含很多種類型和對象,對它們進行模塊化管理,例如事務管理。
在面向切面編程的思想里面,把功能分為兩種
- 核心業務:登陸、注冊、增、刪、改、查、都叫核心業務
- 周邊功能:日志、事務管理這些次要的為周邊業務
在面向切面編程中,核心業務功能和周邊功能是分別獨立進行開發,兩者不是耦合的,然后把切面功能和核心業務功能 “編織” 在一起,這就叫AOP。
AOP能夠將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任(例如事務處理、日志管理、權限控制等)封裝起來,便于減少系統的重復代碼,降低模塊間的耦合度,并有利于未來的可拓展性和可維護性。
在 AOP 中有以下幾個概念:
- AspectJ:切面,只是一個概念,沒有具體的接口或類與之對應,是 Join point,Advice 和 Pointcut 的一個統稱。
- Join point:連接點,指程序執行過程中的一個點,例如方法調用、異常處理等。在 Spring AOP 中,僅支持方法級別的連接點。
- Advice:通知,即我們定義的一個切面中的橫切邏輯,有“around”,“before”和“after”三種類型。在很多的 AOP 實現框架中,Advice 通常作為一個攔截器,也可以包含許多個攔截器作為一條鏈路圍繞著 Join point 進行處理。
- Pointcut:切點,用于匹配連接點,一個 AspectJ 中包含哪些 Join point 需要由 Pointcut 進行篩選。
- Introduction:引介,讓一個切面可以聲明被通知的對象實現任何他們沒有真正實現的額外的接口。例如可以讓一個代理對象代理兩個目標類。
- Weaving:織入,在有了連接點、切點、通知以及切面,如何將它們應用到程序中呢?沒錯,就是織入,在切點的引導下,將通知邏輯插入到目標方法上,使得我們的通知邏輯在方法調用時得以執行。
- AOP proxy:AOP 代理,指在 AOP 實現框架中實現切面協議的對象。在 Spring AOP 中有兩種代理,分別是 JDK 動態代理和 CGLIB 動態代理。
- Target object:目標對象,就是被代理的對象。
當然,我們面試過程中肯定不能就這么說,這么說就扯遠了。面試官可不是想來聽碎碎念的,我們在適當解釋之后就要引到自己項目中的實現了
8. 介紹Redis的特點(Redis比較快)、為什么快?
這里就是回答的時候,一定要按照自己項目中實際使用的說,甚至很多常用的功能都可以不說,想方設法引導面試官往下問,給他做局。
- 完全基于內存操作,讀寫速度非常快。
- 使用單線程,避免了線程切換和鎖競爭的開銷(全都是單線程嗎)。
- 基于非阻塞的 IO 多路復用機制(下一個提問點)
- C 語言實現,優化過的數據結構,基于幾種基礎的數據結構,redis 做了大量優化,性能極高
我們可以看到,這里幾乎全是提問點,面試時我們應該怎么做呢?更熟悉哪里,就在哪部分直接展開說,也不去列其他的點了,除非他問我們還有嗎?當然這種方法有點小聰明了,我們還是最好把知識都掌握住,除非實在記不清別的了。
10. Redis的數據類型及功能
- String 類型的應用場景:緩存對象、常規計數、分布式鎖、共享 session 信息等。
- List 類型的應用場景:消息隊列(但是有兩個問題:1. 生產者需要自行實現全局唯一 ID;2. 不能以消費組形式消費數據)等。
- Hash 類型:緩存對象、購物車等。
- Set 類型:聚合計算(并集、交集、差集)場景,比如點贊、共同關注、抽獎活動等。
- Zset 類型:排序場景,比如排行榜、電話和姓名排序等。
Redis 后續版本又支持四種數據類型,它們的應用場景如下:
- BitMap(2.2 版新增):二值狀態統計的場景,比如簽到、判斷用戶登陸狀態、連續簽到用戶總數等;
- HyperLogLog(2.8 版新增):海量數據基數統計的場景,比如百萬級網頁 UV 計數等;
- GEO(3.2 版新增):存儲地理位置信息的場景,比如滴滴叫車;
- Stream(5.0 版新增):消息隊列,相比于基于 List 類型實現的消息隊列,有這兩個特有的特性:自動生成全局唯一消息ID,支持以消費組形式消費數據。
難道要一個一個介紹?當然不是,還是那句話,引導面試官向自己項目中涉及的數據結構以及實現方式。肯定不是考察面試者背誦能力,而是實踐中使用的功底。
11. Redis的List是怎么實現的
List 類型的底層數據結構是由雙向鏈表或壓縮列表實現的:
- 如果列表的元素個數小于 512 個(默認值,可由 list-max-ziplist-entries 配置),列表每個元素的值都小于 64 字節(默認值,可由 list-max-ziplist-value 配置),Redis 會使用壓縮列表作為 List 類型的底層數據結構;
- 如果列表的元素不滿足上面的條件,Redis 會使用雙向鏈表作為 List 類型的底層數據結構;
- 但是在 Redis 3.2 版本之后,List 數據類型底層數據結構就只由 quicklist 實現了,替代了雙向鏈表和壓縮列表。
12. Redis是單線程的嗎,為什么
Redis 單線程指的是「接收客戶端請求->解析請求 ->進行數據讀寫等操作->發送數據給客戶端」
這個過程是由一個線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。
但是,Redis 程序不是單線程的,Redis 在啟動時會啟動后臺線程(BIO)。
用來處理關閉文件、AOF 刷盤、釋放內存這些任務。
Redis 為「關閉文件、AOF 刷盤、釋放內存」這些任務創建單獨的線程來處理,是因為這些任務的操作都是很耗時的,如果把這些任務都放在主線程來處理,那么 Redis 主線程就很容易發生阻塞,這樣就無法處理后續的請求了。
回答這個問題又涉及到了文件、AOF、內存,這絕對還有下文。再問 Redis 持久化…內存淘汰。。
可以看到這些內容都是串在一起的,有些東西說出來就必被問,這下說啥也逃不掉了,所以還是得多提升自己!
13. 解釋一下垃圾回收機制
垃圾回收(Garbage Collection, GC)是自動管理內存的一種機制,它負責自動釋放不再被程序引用的對象所占用的內存,這種機制減少了內存泄漏和內存管理錯誤的可能性。垃圾回收可以通過多種方式觸發,具體如下:
- 內存不足時:當JVM檢測到堆內存不足,無法為新的對象分配內存時,會自動觸發垃圾回收。
- 手動請求:雖然垃圾回收是自動的,開發者可以通過調用
<font style="background-color:rgba(27, 31, 35, 0.05);">System.gc()</font>
或<font style="background-color:rgba(27, 31, 35, 0.05);">Runtime.getRuntime().gc()</font>
建議 JVM 進行垃圾回收。不過這只是一個建議,并不能保證立即執行。 - JVM參數:啟動 Java 應用時可以通過 JVM 參數來調整垃圾回收的行為,比如:
<font style="background-color:rgba(27, 31, 35, 0.05);">-Xmx</font>
(最大堆大小)、<font style="background-color:rgba(27, 31, 35, 0.05);">-Xms</font>
(初始堆大小)等。 - 對象數量或內存使用達到閾值:垃圾收集器內部實現了一些策略,以監控對象的創建和內存使用,達到某個閾值時觸發垃圾回收。
14. 筆試:三數之和
題目:給定一個包含n個整數的數組nums,判斷是否存在三個元素a,b,c,使得a+b+c=0。找出所有不重復的三元組。
思路:排序 + 雙指針(避免三重循環,降低時間復雜度)
步驟:
- 排序數組,便于去重和雙指針操作
- 固定第一個數nums[i],用雙指針left=i+1、right=n-1尋找nums[left]+nums[right] = -nums[i]
- 跳過重復元素,避免結果重復
代碼實現:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class ThreeSum {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> result = new ArrayList<>();if (nums == null || nums.length < 3) return result;Arrays.sort(nums); // 排序for (int i = 0; i < nums.length; i++) {if (nums[i] > 0) break; // 第一個數大于0,三數之和必大于0if (i > 0 && nums[i] == nums[i-1]) continue; // 去重int left = i + 1;int right = nums.length - 1;while (left < right) {int sum = nums[i] + nums[left] + nums[right];if (sum == 0) {result.add(Arrays.asList(nums[i], nums[left], nums[right]));// 去重while (left < right && nums[left] == nums[left+1]) left++;while (left < right && nums[right] == nums[right-1]) right--;left++;right--;} else if (sum < 0) {left++; // 和太小,左指針右移} else {right--; // 和太大,右指針左移}}}return result;}
}