Java后端高頻面試題
目錄
- Java集合框架
- Java并發編程
- JVM相關
- MySQL數據庫
- Redis緩存
- Spring框架
Java集合框架
HashMap的數據結構是什么,為什么在JDK8要引入紅黑樹?
HashMap數據結構:
- JDK7:數組 + 鏈表
- JDK8:數組 + 鏈表 + 紅黑樹
引入紅黑樹的原因:
- 性能優化:當鏈表長度過長時,查詢效率從O(1)退化為O(n)
- 閾值設計:當鏈表長度達到或超過8時,考慮轉換為紅黑樹(注意是考慮,不是立即轉換),只有當HashMap的數組容量達到或超過64時,才會真正執行樹化操作。≤6時轉回鏈表
- 平衡性能:紅黑樹保證最壞情況下O(log n)的查詢時間復雜度
HashMap的擴容機制是什么,為什么其數組長度是2的冪次?
擴容機制:
- 觸發條件:當size > threshold(容量 × 負載因子0.75)時觸發擴容
- 擴容過程:數組長度擴大為原來的2倍,重新hash分布元素
- rehash優化:JDK8中元素要么在原位置,要么在原位置+oldCap
2的冪次的原因:
一種更高效的取模運算,只用當length為2的冪次時,才可以用位運算替代
// 計算索引位置:hash & (length - 1)
// 當length為2的冪次時,length-1的二進制全為1
// 例如:length=16時,length-1=15(二進制1111)
// 這樣可以充分利用hash值的所有位,減少hash沖突
為什么在JDK7到8要把頭插改為尾插?
頭插法問題(JDK7):
- 死循環風險:多線程擴容時可能形成環形鏈表
- 線程不安全:并發操作可能導致數據丟失
尾插法優勢(JDK8):
- 避免死循環:保持原有順序,不會形成環
- 更直觀:插入順序更符合邏輯
- 配合紅黑樹:為樹化做準備
為什么它解決問題的方式是用鏈表加紅黑樹?
設計考慮:
- 兼容性:保持原有鏈表結構的簡單性
- 性能平衡:紅黑樹維護成本比AVL樹低
- 動態調整:根據沖突程度動態選擇數據結構
- 空間效率:紅黑樹節點比鏈表節點占用更多空間,只在必要時使用
ArrayList和LinkedList的區別是什么?
特性 | ArrayList | LinkedList |
---|---|---|
底層結構 | 動態數組 | 雙向鏈表 |
隨機訪問 | O(1) | O(n) |
插入刪除(頭尾) | O(n) | O(1) |
插入刪除(中間) | O(n) | O(1) |
內存占用 | 較少 | 較多(存儲指針) |
緩存友好性 | 好 | 差 |
使用建議:
- 頻繁隨機訪問:ArrayList
- 頻繁插入刪除:LinkedList
- 內存敏感:ArrayList
Java并發編程
ConcurrentHashMap是怎么實現的,其在JDK7到8做了什么升級?
JDK7實現:
// 分段鎖(Segment)+ HashEntry數組
// 默認16個Segment,每個Segment管理一部分數據
// 并發度 = Segment數量
JDK8升級:
// Node數組 + CAS + synchronized
// 取消Segment,使用Node數組
// 鎖粒度更細:只鎖鏈表頭節點或紅黑樹根節點
// 并發度 = 數組長度
主要改進:
- 更高并發度:從16提升到數組長度
- 更少內存占用:去除Segment層級
- 更好性能:CAS + 局部鎖
什么是樂觀鎖和悲觀鎖?
悲觀鎖:
- 概念:假設會發生沖突,每次操作都加鎖
- 實現:synchronized、ReentrantLock
- 適用場景:寫多讀少
樂觀鎖:
- 概念:假設不會發生沖突,操作時檢查是否被修改
- 實現:CAS、版本號機制
- 適用場景:讀多寫少
CAS是怎么實現的?
CAS(Compare And Swap):
// 偽代碼
boolean compareAndSwap(int expectedValue, int newValue) {if (currentValue == expectedValue) {currentValue = newValue;return true;}return false;
}
底層實現:
- 硬件支持:CPU提供原子性指令
- 內存屏障:保證可見性和有序性
- 自旋機制:失敗時重試
ABA問題解決:
- 使用版本號(AtomicStampedReference)
- 使用標記位(AtomicMarkableReference)
synchronized和ReentrantLock有什么區別?
特性 | synchronized | ReentrantLock |
---|---|---|
實現方式 | JVM內置 | JDK實現 |
鎖釋放 | 自動 | 手動(finally) |
公平性 | 非公平 | 可選公平/非公平 |
條件等待 | wait/notify | Condition |
中斷響應 | 不可中斷 | 可中斷 |
嘗試獲取鎖 | 不支持 | tryLock() |
原子類是如何實現的?
核心機制:
public class AtomicInteger {private volatile int value;public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
}
實現要點:
- volatile保證可見性
- Unsafe類提供CAS操作
- 自旋重試機制
volatile關鍵字有什么用?
主要作用:
- 保證可見性:修改立即刷新到主內存
- 禁止指令重排序:通過內存屏障實現
- 不保證原子性:復合操作仍需同步
使用場景:
- 狀態標記
- 雙重檢查鎖定
- 單例模式
什么是JMM?
Java內存模型(JMM):
- 定義:規范多線程中變量訪問規則
- 主內存:所有線程共享的內存區域
- 工作內存:每個線程的私有內存區域
核心規則:
- 所有變量存儲在主內存
- 線程對變量的操作在工作內存進行
- 工作內存與主內存同步
什么是指令重排序?
概念:
編譯器和處理器為優化性能,可能改變指令執行順序
類型:
- 編譯器重排序:編譯時優化
- 指令級重排序:CPU執行時優化
- 內存系統重排序:緩存和寫緩沖區優化
影響:
在多線程環境下可能導致程序行為異常
什么是happens-before原則?
核心規則:
- 程序順序規則:單線程內按程序順序執行
- 監視器鎖規則:unlock happens-before lock
- volatile規則:寫 happens-before 讀
- 線程啟動規則:start() happens-before 線程內操作
- 線程終止規則:線程操作 happens-before join()
- 傳遞性:A happens-before B,B happens-before C,則A happens-before C
synchronized的鎖升級流程
升級路徑:
無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖
詳細流程:
- 偏向鎖:單線程訪問,在對象頭記錄線程ID
- 輕量級鎖:多線程競爭不激烈,使用CAS
- 重量級鎖:競爭激烈,使用操作系統互斥量
synchronized是不是可重入鎖,可重入鎖是為了保證什么?
可重入性:
- synchronized是可重入鎖
- 同一線程可以多次獲取同一把鎖
實現機制:
// 鎖記錄中維護獲取次數
// 每次重入計數+1,退出時計數-1
// 計數為0時釋放鎖
保證目的:
- 避免死鎖:防止線程自己阻塞自己
- 支持遞歸調用
- 簡化編程模型
AQS隊列是怎么實現的,其如何實現一個公平鎖?
AQS(AbstractQueuedSynchronizer):
// 核心結構:雙向鏈表 + state狀態
static final class Node {Node prev;Node next;Thread thread;int waitStatus;
}
實現機制:
- 狀態管理:使用int state表示同步狀態
- 隊列管理:FIFO隊列管理等待線程
- 模板方法:子類實現具體的同步語義
公平鎖實現:
protected final boolean tryAcquire(int acquires) {// 檢查隊列中是否有等待的線程if (hasQueuedPredecessors()) {return false;}// 嘗試CAS獲取鎖return compareAndSetState(0, acquires);
}
線程池的核心參數是什么,它提交任務的流程是怎么樣的,核心參數如何計算?
核心參數:
ThreadPoolExecutor(int corePoolSize, // 核心線程數int maximumPoolSize, // 最大線程數long keepAliveTime, // 空閑線程存活時間TimeUnit unit, // 時間單位BlockingQueue<Runnable> workQueue, // 任務隊列ThreadFactory threadFactory, // 線程工廠RejectedExecutionHandler handler // 拒絕策略
)
提交流程:
- 當前線程數 < corePoolSize:創建新線程
- 核心線程已滿:任務入隊
- 隊列已滿且線程數 < maximumPoolSize:創建新線程
- 達到最大線程數:執行拒絕策略
參數計算:
// CPU密集型:核心線程數 = CPU核數 + 1
// IO密集型:核心線程數 = CPU核數 × (1 + IO時間/CPU時間)
JVM相關
接口和抽象類的區別
特性 | 接口 | 抽象類 |
---|---|---|
繼承關系 | implements(可多個) | extends(單個) |
方法實現 | JDK8前只能抽象方法 | 可以有具體實現 |
變量 | public static final | 任意訪問修飾符 |
構造方法 | 不能有 | 可以有 |
設計理念 | 能力契約 | 模板設計 |
什么是單例模式?
定義:
確保一個類只有一個實例,并提供全局訪問點
實現方式:
- 餓漢式:類加載時創建
- 懶漢式:首次使用時創建
- 雙重檢查鎖定:線程安全的懶漢式
- 枚舉實現:最安全的實現
寫一個雙重鎖檢查
public class Singleton {private volatile static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton();}}}return instance;}
}
關鍵點:
- volatile關鍵字:防止指令重排序
- 雙重檢查:減少同步開銷
- synchronized:保證線程安全
JVM調優做過嗎?
調優目標:
- 降低GC頻率:減少Stop-The-World時間
- 提高吞吐量:單位時間處理更多任務
- 降低延遲:減少響應時間
常用參數:
# 堆內存設置
-Xms2g -Xmx2g# 新生代設置
-Xmn800m# 垃圾收集器選擇
-XX:+UseG1GC# GC日志
-XX:+PrintGC -XX:+PrintGCDetails
JVM垃圾回收算法
標記-清除(Mark-Sweep):
- 標記所有需要回收的對象,然后清除
- 優點:簡單
- 缺點:產生內存碎片
復制算法(Copying):
- 將內存分為兩塊,每次只使用一塊
- 優點:無碎片,效率高
- 缺點:內存利用率低
標記-整理(Mark-Compact):
- 標記后將存活對象向一端移動
- 優點:無碎片,內存利用率高
- 缺點:移動對象成本高
分代收集:
- 新生代:復制算法
- 老年代:標記-清除或標記-整理
JVM內存空間如何分配?
堆內存:
- 新生代:Eden + Survivor0 + Survivor1(8:1:1)
- 老年代:長期存活的對象
非堆內存:
- 方法區:類信息、常量池
- 直接內存:NIO使用
- 棧內存:局部變量、方法調用
分配流程:
- 新對象在Eden區分配
- Eden滿時觸發Minor GC
- 存活對象進入Survivor區
- 經過多次GC后進入老年代
什么是逃逸分析?
定義:
分析對象的作用域,判斷對象是否會逃出方法或線程
優化策略:
- 棧上分配:不逃逸的對象可在棧上分配
- 標量替換:將對象分解為基本類型
- 鎖消除:消除不必要的同步
判斷條件:
- 對象是否被返回
- 對象是否被外部引用
- 對象是否在其他線程中使用
如何避免Out Of Memory這個錯誤?
預防措施:
- 合理設置堆內存:-Xms和-Xmx
- 避免內存泄漏:及時釋放資源
- 使用對象池:重用對象
- 選擇合適的數據結構
排查步驟:
- 堆dump分析:使用MAT、jhat
- 監控工具:jstat、jvisualvm
- 代碼review:檢查循環引用、大對象
你項目中遇到過內存泄漏的問題嗎,如何解決?
常見內存泄漏場景:
- 集合類持有對象引用
- 監聽器未正確移除
- 數據庫連接未關閉
- ThreadLocal使用不當
解決方案:
// 使用try-with-resources
try (Connection conn = getConnection()) {// 數據庫操作
}// ThreadLocal及時清理
threadLocal.remove();// 弱引用處理監聽器
WeakHashMap<Object, Listener> listeners;
MySQL數據庫
MySQL的事務隔離級別有哪些?
四個隔離級別:
-
READ UNCOMMITTED(讀未提交)
- 最低級別,可能出現臟讀、不可重復讀、幻讀
-
READ COMMITTED(讀已提交)
- Oracle默認級別,避免臟讀,可能出現不可重復讀、幻讀
-
REPEATABLE READ(可重復讀)
- MySQL默認級別,避免臟讀、不可重復讀,可能出現幻讀
-
SERIALIZABLE(序列化)
- 最高級別,避免所有問題,但性能最差
什么是ACID?
原子性(Atomicity):
- 事務中的所有操作要么全部成功,要么全部失敗
- 通過undo log實現
一致性(Consistency):
- 事務執行前后,數據庫狀態保持一致
- 通過其他三個特性保證
隔離性(Isolation):
- 并發事務之間相互隔離,不受影響
- 通過鎖和MVCC實現
持久性(Durability):
- 事務提交后,對數據的修改永久保存
- 通過redo log實現
在MVCC機制下可重復讀是怎么實現的,它還會幻讀嗎?
MVCC實現原理:
-- 每行記錄包含兩個隱藏字段
-- trx_id: 創建該版本的事務ID
-- roll_pointer: 指向undo log的指針-- ReadView包含:
-- m_ids: 當前活躍事務列表
-- min_trx_id: 最小活躍事務ID
-- max_trx_id: 下一個事務ID
-- creator_trx_id: 創建ReadView的事務ID
可重復讀實現:
- 事務開始時創建ReadView
- 根據ReadView判斷數據版本可見性
- 整個事務期間使用同一個ReadView
幻讀問題:
- 快照讀:通過MVCC避免幻讀
- 當前讀:通過Next-Key Lock避免幻讀
什么是間隙鎖,什么是臨鍵鎖?
間隙鎖(Gap Lock):
- 鎖定記錄之間的間隙,防止插入新記錄
- 只在可重復讀級別下生效
臨鍵鎖(Next-Key Lock):
- 記錄鎖 + 間隙鎖的組合
- 鎖定記錄本身和記錄前面的間隙
示例:
-- 假設索引值:1, 3, 5, 7, 10
-- 查詢條件:WHERE id > 3 AND id < 7
-- Gap Lock: (3,5), (5,7)
-- Next-Key Lock: (3,5], (5,7]
什么是索引的回表查詢,如何避免?
回表查詢:
通過二級索引找到主鍵值,再通過主鍵索引查找完整記錄
避免方法:
- 覆蓋索引:查詢字段都在索引中
- 聯合索引:將常用查詢字段組合成索引
- 主鍵選擇:使用自增主鍵減少回表
示例:
-- 需要回表
SELECT * FROM user WHERE name = 'John';-- 覆蓋索引,無需回表
SELECT id, name FROM user WHERE name = 'John';
MySQL有哪些常見的索引?
按數據結構分類:
- B+Tree索引:InnoDB默認索引類型
- Hash索引:Memory存儲引擎支持
- Full-Text索引:全文檢索
按物理存儲分類:
- 聚簇索引:數據和索引存儲在一起(主鍵索引)
- 非聚簇索引:索引和數據分別存儲(二級索引)
按邏輯分類:
- 主鍵索引:唯一且不為空
- 唯一索引:值唯一但可為空
- 普通索引:無唯一性限制
- 聯合索引:多個字段組合
索引在什么情況下會失效?
常見失效場景:
-- 1. 違反最左前綴原則
-- 索引:(a, b, c)
SELECT * FROM t WHERE b = 1 AND c = 1; -- 失效-- 2. 使用函數或計算
SELECT * FROM t WHERE UPPER(name) = 'JOHN'; -- 失效-- 3. 類型轉換
SELECT * FROM t WHERE id = '123'; -- 可能失效-- 4. 使用NOT、!=、<>
SELECT * FROM t WHERE name != 'John'; -- 失效-- 5. LIKE以通配符開頭
SELECT * FROM t WHERE name LIKE '%John'; -- 失效-- 6. OR條件中有未建索引的字段
SELECT * FROM t WHERE name = 'John' OR age = 25; -- 可能失效
explain關鍵字
主要字段:
EXPLAIN SELECT * FROM user WHERE name = 'John';-- id: 查詢序列號
-- select_type: 查詢類型(SIMPLE、PRIMARY、SUBQUERY等)
-- table: 表名
-- type: 訪問類型(system > const > eq_ref > ref > range > index > ALL)
-- possible_keys: 可能使用的索引
-- key: 實際使用的索引
-- key_len: 索引長度
-- ref: 索引比較的列
-- rows: 掃描的行數
-- Extra: 額外信息
type字段重要性(性能從好到壞):
- system/const: 最優
- eq_ref: 唯一性索引掃描
- ref: 非唯一性索引掃描
- range: 范圍掃描
- index: 索引全掃描
- ALL: 全表掃描(最差)
InnoDB下MySQL索引的數據結構是什么,為什么選它不選別的?
數據結構:B+Tree
選擇原因:
- 減少磁盤IO:樹高度低,通常3-4層
- 范圍查詢友好:葉子節點鏈表結構
- 插入性能好:相比紅黑樹更適合磁盤存儲
- 空間利用率高:非葉子節點只存儲鍵值
對比其他結構:
- Hash:等值查詢快,但不支持范圍查詢
- 二叉樹:樹高度高,IO次數多
- B-Tree:非葉子節點存儲數據,空間利用率低
了解過MySQL的三大日志嗎?
redo log(重做日志):
- 作用:保證事務持久性
- 機制:WAL(Write-Ahead Logging)
- 刷盤策略:innodb_flush_log_at_trx_commit
undo log(回滾日志):
- 作用:保證事務原子性,實現MVCC
- 內容:記錄數據修改前的值
- 清理:事務提交后可以清理
binlog(二進制日志):
- 作用:主從復制、數據恢復
- 格式:STATEMENT、ROW、MIXED
- 位置:MySQL Server層
Redis緩存
Redis有哪些數據類型,他們有哪些主要的應用場景?
基本數據類型:
-
String(字符串)
SET key value GET key
- 應用:緩存、計數器、session存儲
-
Hash(哈希)
HSET user:1 name "John" age 25 HGET user:1 name
- 應用:存儲對象、用戶信息
-
List(列表)
LPUSH list1 "a" "b" "c" RPOP list1
- 應用:消息隊列、最新列表
-
Set(集合)
SADD tags "java" "redis" "mysql" SINTER set1 set2
- 應用:標簽、去重、交集運算
-
Sorted Set(有序集合)
ZADD leaderboard 100 "player1" 200 "player2" ZRANGE leaderboard 0 -1
- 應用:排行榜、優先級隊列
緩存穿透、緩存擊穿、緩存雪崩
緩存穿透:
- 問題:查詢不存在的數據,緩存和數據庫都沒有
- 解決方案:
- 布隆過濾器
- 緩存空值(設置較短過期時間)
緩存擊穿:
- 問題:熱點key過期,大量請求直接打到數據庫
- 解決方案:
- 互斥鎖重建緩存
- 熱點數據永不過期
- 提前異步刷新
緩存雪崩:
- 問題:大量key同時過期或Redis宕機
- 解決方案:
- 過期時間隨機化
- 多級緩存
- 限流降級
Redis和數據庫的一致性問題怎么解決?
一致性策略:
-
Cache Aside(旁路緩存)
// 讀取 data = cache.get(key); if (data == null) {data = db.get(key);cache.set(key, data); }// 更新 db.update(key, data); cache.delete(key); // 刪除緩存
-
延遲雙刪
cache.delete(key); db.update(key, data); Thread.sleep(500); // 延遲 cache.delete(key);
-
使用消息隊列
- 數據庫更新后發送消息
- 消費者異步更新緩存
Redis為什么快,可以說說Redis的IO多路復用模型嗎?
Redis快的原因:
- 內存存儲:數據存儲在內存中
- 單線程模型:避免線程切換和鎖競爭
- 高效數據結構:針對性優化的數據結構
- IO多路復用:高效處理網絡IO
IO多路復用模型:
Client1 ----\
Client2 ------> Redis Server (單線程)
Client3 ----/
工作原理:
- 事件循環:主線程在事件循環中處理IO事件
- 多路復用器:使用epoll/kqueue監聽多個socket
- 非阻塞IO:socket設置為非阻塞模式
- 事件驅動:有數據可讀/寫時觸發事件
用Redis實現一套登錄的機制
實現方案:
// 登錄
public String login(String username, String password) {// 驗證用戶名密碼if (validateUser(username, password)) {// 生成tokenString token = UUID.randomUUID().toString();// 存儲到Redis,設置過期時間String key = "session:" + token;Map<String, String> userInfo = new HashMap<>();userInfo.put("username", username);userInfo.put("loginTime", String.valueOf(System.currentTimeMillis()));redisTemplate.opsForHash().putAll(key, userInfo);redisTemplate.expire(key, 30, TimeUnit.MINUTES);return token;}return null;
}// 驗證登錄狀態
public boolean isLogin(String token) {String key = "session:" + token;return redisTemplate.hasKey(key);
}// 登出
public void logout(String token) {String key = "session:" + token;redisTemplate.delete(key);
}// 續期
public void refreshSession(String token) {String key = "session:" + token;if (redisTemplate.hasKey(key)) {redisTemplate.expire(key, 30, TimeUnit.MINUTES);}
}
用Redis做防抖和節流
防抖實現(Debounce):
public class RedisDebounce {@Autowiredprivate RedisTemplate<String, String> redisTemplate;public boolean debounce(String key, long delayMs) {String debounceKey = "debounce:" + key;// 設置鍵值,如果鍵已存在則不執行Boolean result = redisTemplate.opsForValue().setIfAbsent(debounceKey, "1", delayMs, TimeUnit.MILLISECONDS);return result != null && result;}
}
節流實現(Throttle):
public class RedisThrottle {@Autowiredprivate RedisTemplate<String, String> redisTemplate;// 固定窗口節流public boolean throttle(String key, int maxRequests, long windowMs) {String throttleKey = "throttle:" + key;Long count = redisTemplate.opsForValue().increment(throttleKey);if (count == 1) {redisTemplate.expire(throttleKey, windowMs, TimeUnit.MILLISECONDS);}return count <= maxRequests;}// 滑動窗口節流public boolean slidingWindowThrottle(String key, int maxRequests, long windowMs) {String throttleKey = "sliding:" + key;long now = System.currentTimeMillis();long windowStart = now - windowMs;// 使用Lua腳本保證原子性String luaScript = "redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +"local count = redis.call('zcard', KEYS[1]) " +"if count < tonumber(ARGV[2]) then " +" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +" redis.call('expire', KEYS[1], ARGV[4]) " +" return 1 " +"else " +" return 0 " +"end";Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),Collections.singletonList(throttleKey),String.valueOf(windowStart),String.valueOf(maxRequests),String.valueOf(now),String.valueOf(windowMs / 1000));return result != null && result == 1;}
}
Redis主從同步
主從復制原理:
-
全量同步:
- 從庫發送PSYNC命令
- 主庫執行BGSAVE生成RDB文件
- 主庫將RDB文件發送給從庫
- 從庫加載RDB文件
-
增量同步:
- 主庫將寫命令記錄到復制緩沖區
- 異步發送給從庫執行
配置示例:
# 從庫配置
slaveof 192.168.1.100 6379
# 或
replicaof 192.168.1.100 6379# 只讀模式
slave-read-only yes
主從切換(哨兵模式):
# 哨兵配置
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
Redis的持久化機制
RDB(Redis Database):
- 原理:某個時間點的數據快照
- 觸發條件:
save 900 1 # 900秒內至少1個key變化 save 300 10 # 300秒內至少10個key變化 save 60 10000 # 60秒內至少10000個key變化
- 優點:文件小,恢復快
- 缺點:可能丟失數據
AOF(Append Only File):
- 原理:記錄所有寫操作命令
- 同步策略:
appendfsync always # 每次寫入都同步 appendfsync everysec # 每秒同步(默認) appendfsync no # 操作系統決定何時同步
- 優點:數據安全性高
- 缺點:文件大,恢復慢
混合持久化(RDB + AOF):
aof-use-rdb-preamble yes
- RDB作為基礎數據
- AOF記錄RDB之后的增量操作
Redis中的大key和熱key如何去優化?
大key優化:
-
拆分策略:
// 原來:一個大hash HSET user:info name age email address ...// 拆分后:多個小hash HSET user:basic name age HSET user:contact email phone HSET user:address province city
-
分片存儲:
// 將大list分片存儲 for (int i = 0; i < totalSize; i += batchSize) {String shardKey = key + ":" + (i / batchSize);// 存儲分片數據 }
熱key優化:
-
多級緩存:
// 本地緩存 + Redis緩存 Object data = localCache.get(key); if (data == null) {data = redisCache.get(key);if (data != null) {localCache.put(key, data);} }
-
讀寫分離:
- 讀操作分散到多個從節點
- 熱key復制到多個實例
-
熱key發現:
// 使用監控工具 redis-cli --hotkeys // 或使用應用層統計
Spring框架
Spring Boot設計了哪些設計模式?
主要設計模式:
- 單例模式:Spring Bean默認是單例
- 工廠模式:BeanFactory創建Bean實例
- 代理模式:AOP的實現基礎
- 模板方法模式:JdbcTemplate、RestTemplate
- 觀察者模式:ApplicationEvent事件機制
- 策略模式:不同的Bean創建策略
- 裝飾器模式:BeanWrapper裝飾Bean
- 適配器模式:HandlerAdapter適配不同Controller
什么是IOC?
控制反轉(Inversion of Control):
- 定義:將對象的創建和依賴關系的管理交給容器
- 核心思想:不要主動創建依賴對象,而是被動接收
DI(依賴注入)實現方式:
// 1. 構造器注入
@Component
public class UserService {private final UserRepository userRepository;public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}// 2. Setter注入
@Component
public class UserService {private UserRepository userRepository;@Autowiredpublic void setUserRepository(UserRepository userRepository) {this.userRepository = userRepository;}
}// 3. 字段注入
@Component
public class UserService {@Autowiredprivate UserRepository userRepository;
}
SpringBoot的循環依賴怎么解決?
循環依賴場景:
@Component
public class A {@Autowiredprivate B b;
}@Component
public class B {@Autowiredprivate A a;
}
解決機制(三級緩存):
// 第一級緩存:成品對象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();// 第二級緩存:半成品對象
private final Map<String, Object> earlySingletonObjects = new HashMap<>();// 第三級緩存:對象工廠
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
解決流程:
- 創建A的實例,放入三級緩存
- 填充A的屬性,發現需要B
- 創建B的實例,放入三級緩存
- 填充B的屬性,發現需要A
- 從三級緩存獲取A的實例,放入二級緩存
- B創建完成,放入一級緩存
- A創建完成,放入一級緩存
AOP是怎么實現的?
AOP(面向切面編程)實現原理:
-
JDK動態代理(接口代理):
public class JdkProxyExample implements InvocationHandler {private Object target;public Object invoke(Object proxy, Method method, Object[] args) {// 前置通知System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);// 后置通知System.out.println("After method: " + method.getName());return result;} }
-
CGLIB代理(類代理):
public class CglibProxyExample implements MethodInterceptor {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("Before method: " + method.getName());Object result = proxy.invokeSuper(obj, args);System.out.println("After method: " + method.getName());return result;} }
AOP配置示例:
@Aspect
@Component
public class LoggingAspect {@Pointcut("execution(* com.example.service.*.*(..))")public void serviceMethods() {}@Before("serviceMethods()")public void logBefore(JoinPoint joinPoint) {System.out.println("執行方法: " + joinPoint.getSignature().getName());}@AfterReturning(pointcut = "serviceMethods()", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {System.out.println("方法返回: " + result);}
}
除了JDK的動態代理你還了解過其他的代理模式嗎?
靜態代理:
// 接口
interface UserService {void addUser(String name);
}// 目標類
class UserServiceImpl implements UserService {public void addUser(String name) {System.out.println("添加用戶: " + name);}
}// 代理類
class UserServiceProxy implements UserService {private UserService userService;public UserServiceProxy(UserService userService) {this.userService = userService;}public void addUser(String name) {System.out.println("權限檢查");userService.addUser(name);System.out.println("日志記錄");}
}
字節碼生成代理:
- CGLIB:運行時生成字節碼
- Javassist:編譯時字節碼操作
- ASM:低級別字節碼操作框架
Spring的AOP是運行時代理還是編譯時代理?
Spring AOP是運行時代理:
- 在應用啟動時創建代理對象
- 使用JDK動態代理或CGLIB生成代理類
- 代理對象在運行時攔截方法調用
對比編譯時代理:
- AspectJ:編譯時織入,性能更好
- Spring AOP:運行時代理,使用簡單
配置AspectJ編譯時織入:
<plugin><groupId>org.aspectj</groupId><artifactId>aspectj-maven-plugin</artifactId><configuration><complianceLevel>1.8</complianceLevel></configuration>
</plugin>
事務注解什么時候失效?
常見失效場景:
-
方法不是public:
@Transactional private void updateUser() { // 失效// 更新操作 }
-
自調用問題:
@Service public class UserService {public void method1() {this.method2(); // 失效,沒有通過代理調用}@Transactionalpublic void method2() {// 事務方法} }
-
異常被捕獲:
@Transactional public void updateUser() {try {// 數據庫操作} catch (Exception e) {// 異常被捕獲,事務不回滾} }
-
異常類型不匹配:
@Transactional // 默認只回滾RuntimeException public void updateUser() throws Exception {throw new Exception(); // 不會回滾 }// 正確配置 @Transactional(rollbackFor = Exception.class) public void updateUser() throws Exception {throw new Exception(); // 會回滾 }
-
數據庫引擎不支持:
- MyISAM不支持事務
- 需要使用InnoDB引擎
解決方案:
// 1. 使用ApplicationContext獲取代理對象
@Autowired
private ApplicationContext applicationContext;public void method1() {UserService proxy = applicationContext.getBean(UserService.class);proxy.method2();
}// 2. 使用@EnableAspectJAutoProxy(exposeProxy = true)
public void method1() {UserService proxy = (UserService) AopContext.currentProxy();proxy.method2();
}// 3. 注入自己
@Autowired
private UserService userService;public void method1() {userService.method2();
}
總結
本文涵蓋了Java后端開發中的核心知識點,包括:
- 集合框架:HashMap、ArrayList等核心數據結構的實現原理
- 并發編程:線程安全、鎖機制、線程池等并發處理技術
- JVM調優:內存管理、垃圾回收、性能優化策略
- 數據庫:MySQL事務、索引、查詢優化等數據庫技術
- 緩存技術:Redis數據類型、持久化、集群等緩存方案
- 框架原理:Spring IOC、AOP、事務管理等框架核心
這些知識點不僅是面試的重點,更是日常開發中需要深入理解和靈活運用的核心技術。建議結合實際項目經驗,深入理解每個技術點的適用場景和最佳實踐。