後端面試題總整理
Last updated: Apr 3, 2026Table of Contents
- 目錄
- 1. 資料庫相關
- 1.1 資料庫隔離級別 (Transaction Isolation Levels)
- 1.2 MySQL 鎖機制
- 1.3 SQL 查詢優化與執行計畫
- 2. 快取相關
- 2.1 快取三大問題
- 2.2 Redis 分散式鎖
- 2.3 Spring Boot + Redis 快取實踐
- 3. 分散式系統
- 3.1 全域事務 (Global Transaction)
- 3.2 ZooKeeper 分散式鎖
- 4. 網路與協定
- 4.1 TCP vs UDP
- 4.2 HTTP 相關
- 4.3 JWT (JSON Web Token)
- 5. 應用架構
- 5.1 Middleware (中介層)
- 5.2 ORM (Object-Relational Mapping)
- 5.3 MVC vs MVVM
- 6. 常見面試題
- 6.1 如何建立全域事務?
- 6.2 建立分散式 Redis 鎖時的考量?
- 6.3 如何優化 SQL 查詢?如何使用執行計畫?
- 6.4 Spring Boot Cloud 主要組件有哪些?
- 6.5 如何確保資料庫資料一致性?
- 6.6 如何使用 Redis 快取和資料庫(MySQL),當 Redis 可能失效時?
- 6.7 Interface、Class 在編譯時、執行時的使用,以及與 Bytecode 的關係?
- 參考資料
後端面試題總整理
目錄
1. 資料庫相關
1.1 資料庫隔離級別 (Transaction Isolation Levels)
ACID 特性
資料庫事務需滿足 ACID 特性:
- Atomicity (原子性):事務中的所有操作要麼全部完成,要麼全部不完成
- Consistency (一致性):事務執行前後,資料庫都保持一致性狀態
- Isolation (隔離性):並行執行的事務之間互不干擾
- Durability (持久性):事務提交後,對資料庫的改變是永久性的
四種隔離級別
從最寬鬆到最嚴格:Read Uncommitted → Read Committed → Repeatable Read → Serializable
| 隔離級別 | Dirty Read (髒讀) |
Non-repeatable Read (不可重複讀) |
Phantom Read (幻讀) |
說明 |
|---|---|---|---|---|
| Read Uncommitted | ✅ 可能發生 | ✅ 可能發生 | ✅ 可能發生 | 可以讀取其他事務尚未提交的資料,最不安全但速度最快 |
| Read Committed | ❌ 已避免 | ✅ 可能發生 | ✅ 可能發生 | 只能讀取已提交的資料 (PostgreSQL 預設) |
| Repeatable Read | ❌ 已避免 | ❌ 已避免 | ✅ 可能發生 | 同一事務中相同查詢會得到相同結果 (MySQL InnoDB 預設) |
| Serializable | ❌ 已避免 | ❌ 已避免 | ❌ 已避免 | 最嚴格的隔離級別,事務串行執行,性能最差 |
異常現象說明
- Dirty Read (髒讀):讀取到其他事務尚未提交的資料
- Non-repeatable Read (不可重複讀):同一事務中兩次讀取同一資料得到不同結果
- Phantom Read (幻讀):同一事務中重新執行查詢,會出現新增或減少的資料列
不同資料庫的預設隔離級別
- MySQL InnoDB:
Repeatable Read - PostgreSQL:
Read Committed
設定隔離級別 (MySQL)
-- 設定 Session 級別
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
-- 執行查詢
COMMIT;
-- 設定全域級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查看當前隔離級別
SELECT @@session.transaction_isolation; -- Session 級別
SELECT @@global.transaction_isolation; -- 全域級別
使用場景建議
| 使用情境 | 建議的隔離級別 |
|---|---|
| 分析報表 / 資料查詢 | Read Committed |
| 高並發服務 | Read Committed / Repeatable Read + 重試機制 |
| 金融交易 (如銀行轉帳) | Repeatable Read / Serializable |
| 庫存系統 / 訂票系統 | Serializable 或加鎖 |
1.2 MySQL 鎖機制
鎖的分類
MySQL 中的鎖主要分為三個層級:
- 全域鎖 (Global Lock)
- 表級鎖 (Table-Level Lock)
- 行級鎖 (Row-Level Lock)
1.2.1 全域鎖
- 使用場景:資料庫備份
- 效果:資料庫中所有表變為唯讀狀態
-- 加全域鎖
FLUSH TABLES WITH READ LOCK;
-- 解鎖
UNLOCK TABLES;
1.2.2 表級鎖
類型:
- 表鎖 (Table Lock)
- 元數據鎖 (Metadata Lock, MDL)
- 意向鎖 (Intention Lock)
- AUTO-INC 鎖
表鎖特性:
- 讀讀共享、讀寫互斥、寫寫互斥
- 粒度大,開銷小,不會產生死鎖
- 併發性能較差,適合 MyISAM 引擎
表鎖類型:
| 鎖類型 | 說明 |
|---|---|
| 讀鎖 | 持有讀鎖可以讀表但不能寫表;允許多個執行緒持有讀鎖;其他執行緒加寫鎖會被阻塞 |
| 寫鎖 | 持有寫鎖的執行緒可讀可寫;只有持有寫鎖的執行緒能訪問該表,其他執行緒被阻塞 |
-- 加讀鎖
LOCK TABLES authors READ;
-- 加寫鎖
LOCK TABLES comment WRITE;
-- 解鎖當前 Session 所有表鎖
UNLOCK TABLES;
意向鎖:
- 在使用 InnoDB 引擎的表中對某些記錄加行級鎖之前,需要先在表級加意向鎖
- 意向鎖的目的是為了快速判斷表裡是否有記錄被加鎖
- 意向鎖之間不會衝突,只會與表級的共享鎖和獨佔鎖衝突
1.2.3 行級鎖
類型:
- Record Lock (記錄鎖)
- Gap Lock (間隙鎖)
- Next-Key Lock (記錄鎖 + 間隙鎖)
特性:
- InnoDB 支援行級鎖,MyISAM 不支援
- 讀讀共享、讀寫互斥、寫寫互斥
- 粒度小,開銷大,可能產生死鎖
- 併發性能好
Record Lock (記錄鎖):
-- 對讀取的記錄加共享鎖 (S鎖)
SELECT * FROM authors WHERE id = 1 LOCK IN SHARE MODE;
-- 對讀取的記錄加獨佔鎖 (X鎖)
SELECT * FROM authors WHERE id = 1 FOR UPDATE;
Gap Lock (間隙鎖):
- 鎖定兩個索引之間的間隙,防止其他事務在此範圍內插入或修改記錄
- 可以避免幻讀現象
Next-Key Lock:
- 概念上等於 Record Lock + Gap Lock
- 建立一個前開後閉的索引間隙,避免其他事務在此間隙插入資料
1.2.4 死鎖 (Deadlock)
根本原因:兩個或多個事務加鎖順序不一致
處理方式:
- 設定鎖等待超時時間
- 使用死鎖檢測機制
- 確保事務按相同順序獲取鎖
InnoDB vs MyISAM
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 預設版本 | MySQL 5.5+ | MySQL 5.1 及之前 |
| 事務支援 | ✅ 支援 | ❌ 不支援 |
| 行級鎖 | ✅ 支援 | ❌ 不支援,僅支援表鎖 |
| 外鍵 | ✅ 支援 | ❌ 不支援 |
| 崩潰恢復 | ✅ 自動恢復 | ❌ 不支援 |
| 儲存空間 | 較大 | 較小,可壓縮 |
| 適用場景 | 高並發、事務處理 | 大量查詢、篩選 |
1.3 SQL 查詢優化與執行計畫
什麼是執行計畫?
執行計畫 (Execution Plan) 是資料庫引擎執行 SQL 查詢的具體步驟,包括:
- JOIN 順序
- 索引使用情況
- 表掃描方式
- 預估成本
取得執行計畫
-- MySQL / PostgreSQL
EXPLAIN SELECT * FROM orders WHERE status = 'shipped';
-- PostgreSQL - 包含實際執行時間
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'shipped';
-- MySQL - JSON 格式,更詳細
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE status = 'shipped';
執行計畫關鍵指標
| 指標 | 說明 | 理想值 / 需注意的問題 |
|---|---|---|
| type | 訪問方式 (ALL, index, ref, eq_ref) | ✅ 優先選擇 ref 或 eq_ref,避免 ALL (全表掃描) |
| key | 使用的索引 | ❗ 若為 NULL,表示未使用索引 |
| rows | 預估掃描的行數 | ✅ 越低越好 |
| filtered | 傳遞到下一階段的行數百分比 | ❗ 低於 30% 表示過濾效果差 |
| Extra | 額外資訊 | ❗ “Using temporary” 或 “filesort” 表示性能差 |
優化步驟
1. 識別昂貴的操作
- 全表掃描 (Full Table Scan) 而非索引掃描
- 大數據集上的嵌套迴圈連接 (Nested Loop Join)
- 排序操作 (Sort) 或 Hash 聚合
- 使用臨時表或磁碟
2. 檢查索引使用
- 確保 WHERE 條件和 JOIN 使用索引欄位
- 複合索引的欄位順序要與查詢條件匹配
- 添加缺失的索引
- 移除未使用或低效的索引
3. 避免 SELECT *
- 只查詢需要的欄位,減少 I/O
4. 分析 JOIN 順序
- 將過濾條件推到 JOIN 之前執行
- 使用 CTE (Common Table Expression) 隔離昂貴的分支
5. 重構 WHERE/HAVING 子句
- 確保謂詞是 Sargable (Search Argument-able)
- 避免
WHERE YEAR(date_col) = 2023,改用WHERE date_col BETWEEN '2023-01-01' AND '2023-12-31' - 使用顯式 JOIN 而非相關子查詢
6. 檢查 GROUP BY / ORDER BY
- 為 GROUP BY、ORDER BY 使用的欄位建立索引
- 添加覆蓋索引 (Covering Index)
優化範例
問題查詢:
SELECT o.id, o.order_date, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'shipped' AND c.region = 'US'
ORDER BY o.order_date DESC
LIMIT 100;
執行 EXPLAIN 發現問題:
customers表進行全表掃描 (type=ALL, key=NULL)orders表使用 filesort
優化方案:
-- 添加索引
CREATE INDEX idx_customers_region ON customers(region);
CREATE INDEX idx_orders_status_date ON orders(status, order_date DESC);
-- 可選:覆蓋索引
CREATE INDEX idx_orders_covering ON orders(status, order_date DESC, customer_id, id);
常見問題與修復
| 症狀 | 修復方案 |
|---|---|
| type=ALL | 為 WHERE / JOIN 欄位添加索引 |
| Extra 有 filesort | 為 ORDER BY 欄位添加索引 |
| Extra 有 Using temporary | 重寫查詢或添加索引 |
| rows 數量過高 | 重寫邏輯或拆分為子查詢 |
| filtered < 30% | 改進 WHERE 條件或在應用層預過濾 |
2. 快取相關
2.1 快取三大問題
快取在高並發系統中扮演重要角色,但也會遇到三大經典問題:
- 快取穿透 (Cache Penetration)
- 快取擊穿 (Hotspot Invalid)
- 快取雪崩 (Cache Avalanche)
2.1.1 快取雪崩 (Cache Avalanche)
現象:
- 在某個時刻,所有的快取同時過期或 Redis 服務失效
- 導致大量請求直接打到資料庫
- 當流量巨大時,資料庫可能被打掛
解決方案:
- ✅ 隨機過期時間:為每個快取 key 設定隨機的過期時間,避免同時失效
- ✅ 互斥鎖:快取不存在時使用鎖機制,只允許一個執行緒查詢資料庫
- ✅ 後台定期更新快取:在快取過期前自動更新
- ✅ 服務熔斷或請求限流:避免資料庫過載
- ✅ Redis 叢集:提高可用性
2.1.2 快取擊穿 (Hotspot Invalid)
現象:
- 某個熱門的快取 key 過期
- 高並發集中在此 key,流量直接打到資料庫
解決方案:
- ✅ 將熱點 key 設為永不過期
- ✅ 互斥鎖:在應用層加鎖,確保只有一個執行緒查詢資料庫並更新快取
- 缺點:會降低系統吞吐量,阻礙其他執行緒
2.1.3 快取穿透 (Cache Penetration)
現象:
- 客戶端請求的資料既不存在於快取,也不存在於資料庫
- 每次請求都會穿過快取直接打到資料庫
- 例如:查詢
id=-1的資料,但資料庫從id=1開始
解決方案:
- ✅ 過濾非法請求:在應用層驗證請求參數的合法性
- ✅ 快取空值或預設值:將查詢不到的資料也快取起來(設定較短的過期時間)
- ✅ 布隆過濾器 (Bloom Filter):判斷請求的 key 是否可能存在於資料集中
- 存在則查詢 Redis
- 不存在則直接返回預設訊息
2.2 Redis 分散式鎖
在分散式系統中,Redis 鎖用於確保同一時刻只有一個程序能訪問特定資源,防止競態條件 (race condition) 和資料不一致。
核心概念
- 原子性:Redis 支援原子操作,鎖的獲取和釋放不可中斷
- 過期時間:設定 TTL (Time To Live),避免死鎖
- 分散式:可跨多個實例或微服務使用
2.2.1 SETNX (Set if Not Exists)
最簡單的鎖實現方式:
import redis
r = redis.Redis()
lock_key = "lock:my_resource"
# 嘗試獲取鎖
if r.setnx(lock_key, 1):
try:
# 取得鎖,執行臨界區程式碼
pass
finally:
# 釋放鎖
r.delete(lock_key)
else:
# 未取得鎖,稍後重試
pass
問題:如果程式在釋放鎖之前崩潰,會造成死鎖
2.2.2 SET with Expiration (推薦)
使用 SET 命令搭配 NX 和 EX 選項,原子地設定鎖和過期時間:
import redis
r = redis.Redis()
lock_key = "lock:my_resource"
lock_timeout = 10 # 鎖在 10 秒後自動過期
# 嘗試獲取鎖
if r.set(lock_key, 1, nx=True, ex=lock_timeout):
try:
# 取得鎖,執行臨界區程式碼
pass
finally:
# 釋放鎖
r.delete(lock_key)
else:
# 未取得鎖,稍後重試
pass
2.2.3 Redlock 演算法
用於多個 Redis 實例的分散式鎖,提供容錯能力:
from redis import Redis
from redlock import Redlock
# 連接多個 Redis 實例
redis_instances = [Redis(host='localhost', port=6379) for _ in range(5)]
dlock = Redlock(redis_instances)
lock_key = "lock:my_resource"
lock = dlock.lock(lock_key, 10000) # 鎖在 10 秒後過期
if lock:
try:
# 執行任務
pass
finally:
dlock.unlock(lock)
else:
# 未取得鎖
pass
工作原理:
- 嘗試在多個 Redis 節點 (如 5 個) 上獲取鎖
- 當大多數節點 (如 3 個) 成功獲取鎖時,才認為鎖獲取成功
- 提高可靠性,即使部分節點失效也能正常工作
最佳實踐
- 設定合理的過期時間:避免死鎖,但要足夠完成任務
- 確保釋放鎖:使用 try-finally 確保鎖被釋放
- 重試邏輯:使用指數退避策略 (exponential backoff)
- 原子性和隔離性:確保被鎖保護的操作是原子的
- 超時和失敗處理:考慮使用 Lua 腳本確保鎖和操作的原子性
2.3 Spring Boot + Redis 快取實踐
快取模式
| 模式 | 說明 |
|---|---|
| Read-Through | 應用先查快取,若未命中則查資料庫,並填充快取 |
| Write-Through | 寫入時同時更新快取和資料庫 |
| Write-Behind (Async) | 先寫快取,稍後非同步寫入資料庫(風險較高) |
| Cache-Aside (Lazy Load) | 應用讀取快取,未命中時查資料庫並更新快取(最常見) |
Spring Boot 快取實現 (Cache-Aside)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 查詢時先查快取,未命中則查資料庫並快取結果
@Cacheable(value = "user", key = "#userId")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
// 更新資料後清除快取
@CacheEvict(value = "user", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
// 更新資料並同時更新快取
@CachePut(value = "user", key = "#user.id")
public User saveUser(User user) {
return userRepository.save(user);
}
}
快取一致性策略
| 策略 | 實現方式 | 優點 | 缺點 |
|---|---|---|---|
寫入時清除 (@CacheEvict) |
更新資料庫後刪除快取項 | 簡單、一致性好 | 下次讀取需重新載入快取 |
寫入時更新 (@CachePut) |
同時寫入資料庫和快取 | 讀取快速 | 可能部分失敗導致不一致 |
| 訊息佇列 (如 Kafka) | 透過事件流非同步同步 | 解耦、可擴展 | 複雜度高、可能有延遲 |
| 雙寫事務 | 事務性地更新兩者 | 可靠 | 回滾管理困難 |
常見問題與解決方案
1. 快取風暴 (Cache Stampede)
問題:快取過期時大量請求同時打到資料庫
解決方案:
- 使用隨機化的 TTL
- 使用互斥鎖 (SETNX)
- 使用 Redisson 或 Caffeine 等函式庫
2. 快取不一致
問題:資料庫已更新,但快取未更新
解決方案:
- 使用
@CacheEvict或@CachePut - 避免並行更新,使用鎖或防抖
3. 冷啟動 (Cold Start)
問題:重啟後快取為空
解決方案:
- 啟動時預熱快取 (可選的批次載入器)
4. 序列化格式
Redis 需要快速的序列化,建議使用 JSON 或 String 而非 Java 原生序列化:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
3. 分散式系統
3.1 全域事務 (Global Transaction)
在分散式系統中,全域事務用於確保跨多個服務或資料庫的操作具有 ACID 特性。
3.1.1 兩階段提交 (2PC - Two-Phase Commit)
適用場景:強一致性的分散式資料庫
工作流程:
- 階段 1 - 準備 (Prepare):協調者詢問所有參與節點是否準備好提交
- 階段 2 - 提交 (Commit):如果所有節點回覆「是」,協調者發出提交指令;否則回滾
優點:
- ✅ 強一致性 (ACID)
- ✅ 中央協調
缺點:
- ❌ 阻塞協定,可能造成鎖競爭
- ❌ 單點故障(協調者失效)
- ❌ 在網路分區時可用性差
- ❌ 不適合雲原生 / 微服務架構
3.1.2 Saga 模式 (推薦用於微服務)
適用場景:現代微服務和事件驅動架構
工作原理:
- 一系列本地事務,每個服務更新自己的資料並發布事件觸發下一步
- 如果某一步失敗,執行補償事務 (Compensating Transaction) 回滾之前的步驟
範例(訂票流程):
- 步驟 1:預訂飯店 → 步驟 2:預訂機票 → 步驟 3:付款
- 如果步驟 2 失敗,執行補償動作取消步驟 1 的飯店預訂
實現方式:
| 方式 | 說明 | 範例工具 |
|---|---|---|
| 編排式 Saga (Orchestration) | 中央服務控制流程 | Netflix Conductor, Temporal |
| 編舞式 Saga (Choreography) | 服務對事件做出反應,自行管理轉換 | Apache Kafka + EventBridge |
優點:
- ✅ 最終一致性
- ✅ 高可用性和回應性
- ✅ 適合鬆散耦合的系統
缺點:
- ❌ 補償邏輯複雜
- ❌ 難以除錯和追蹤
注意事項:
- 確保每個服務的操作具有冪等性 (idempotency)
- 使用相關 ID (correlation ID) 進行跨系統追蹤
- 應用超時和斷路器 (circuit breaker) 提高韌性
3.1.3 TCC (Try-Confirm-Cancel)
適用場景:資源可以預留的系統(如庫存、訂票)
工作流程:
- Try:預留資源(如鎖定庫存)
- Confirm:確認操作,提交事務
- Cancel:取消操作,釋放資源
優點:
- ✅ 對操作有細粒度控制
- ✅ 顯式的補償模型
缺點:
- ❌ 所有服務必須實現 try/confirm/cancel 邏輯
- ❌ 實現複雜度高
3.1.4 最終一致性 + 事件驅動架構
適用場景:可以容忍一致性延遲的系統
優點:
- ✅ 非同步
- ✅ 高度可擴展
- ✅ 服務解耦
缺點:
- ❌ 失敗處理複雜
- ❌ 需要冪等操作
- ❌ 難以除錯和追蹤
3.1.5 可靠訊息 + Outbox 模式
適用場景:確保事件/訊息以事務方式傳遞
優點:
- ✅ 確保資料庫和訊息的原子性
- ✅ 服務解耦
缺點:
- ❌ 需要額外基礎設施 (Kafka, Debezium 等)
- ❌ 運維和基礎設施開銷較高
模式比較表
| 模式 | 一致性 | 可擴展性 | 複雜度 | 失敗處理 | 理想使用場景 |
|---|---|---|---|---|---|
| Saga | 最終一致 | ✅ 高 | ⚠️ 中等 | 需要補償邏輯 | 長時間運行、可分解的業務流程 |
| 2PC | 強一致 (ACID) | ❌ 低 | ⚠️ 高 | 協調者是瓶頸 | 需要嚴格一致性的金融操作 |
| TCC | 強一致 | ⚠️ 中等 | ❌ 高 | 顯式取消邏輯 | 訂票系統、可預留資源 |
| 最終一致性 (EDA) | 最終一致 | ✅ 非常高 | ⚠️ 中等 | 重試邏輯、冪等性 | 容忍延遲的微服務 |
| Outbox 模式 | 最終一致 | ✅ 高 | ⚠️ 中等 | 可靠訊息傳遞 | 確保訊息和資料庫一致性 |
選擇建議
| 需求 | 最佳選擇 |
|---|---|
| 需要強一致性 | 2PC, TCC |
| 高可擴展性 | Saga, Outbox |
| 需要資源預留 | TCC |
| 容錯能力 | Saga, Outbox |
| 簡單性 > 一致性 | 手動對帳 |
3.2 ZooKeeper 分散式鎖
ZooKeeper 是一個分散式協調服務,常用於實現分散式鎖、配置管理、服務發現等功能。
基本概念:
- 使用臨時順序節點 (Ephemeral Sequential Node) 實現鎖
- 客戶端創建節點,序號最小的獲得鎖
- 其他客戶端監聽前一個節點,等待鎖釋放
優點:
- 支援阻塞式鎖
- 自動釋放(客戶端斷線時自動刪除臨時節點)
- 避免羊群效應 (thundering herd)
缺點:
- 需要部署和維護 ZooKeeper 叢集
- 性能不如 Redis
4. 網路與協定
4.1 TCP vs UDP
TCP (Transmission Control Protocol)
特性:
- 面向連接的協定
- 提供可靠的資料傳輸
- 錯誤檢查和重傳功能
- 確保資料不會丟失
三次握手 (Three-Way Handshake):
- Client 向 Server 發送連接請求封包 (SYN)
- Server 接收並確認,回傳確認封包 (SYN-ACK)
- Client 收到後再回傳確認封包 (ACK),連接建立
特性:
- 建立連接後,所有封包都會加上序號
- 保證完整性、順序性、重傳處理
- 使用滑動窗口提升傳輸效率
適用場景:
- 網頁瀏覽 (HTTP/HTTPS)
- 電子郵件 (SMTP, IMAP)
- 檔案傳輸 (FTP)
- 需要資料完整性的應用
UDP (User Datagram Protocol)
特性:
- 面向非連接的協定
- 不保證資料傳輸的可靠性
- 傳輸速度快,通訊引擎簡單
- 無需確認機制,表頭資料較少
適用場景:
- 串流服務 (影片、音訊)
- 線上遊戲
- DNS 查詢
- 即時通訊
TCP vs UDP 比較
| 特性 | TCP | UDP |
|---|---|---|
| 連接 | 面向連接 | 無連接 |
| 可靠性 | 可靠 | 不可靠 |
| 速度 | 較慢 | 較快 |
| 順序 | 保證順序 | 不保證順序 |
| 錯誤檢查 | 有 | 有但簡單 |
| 重傳 | 支援 | 不支援 |
| 表頭大小 | 較大 (20 bytes) | 較小 (8 bytes) |
| 使用場景 | 需要完整性的應用 | 需要速度的應用 |
4.2 HTTP 相關
HTTP, TCP, Socket 關係
- TCP/IP:傳輸控制協定/網路協定,指一系列協定族
- HTTP:基於 TCP 的應用層協定,用於 Web 伺服器與瀏覽器之間傳輸超文本
- Socket:TCP/IP 網路的 API,隱藏了複雜的 TCP/IP 協定細節
關係總結:
- 需要 IP 協定來連接網路
- TCP 是一種安全傳輸資料的機制
- HTTP 使用 TCP 協定來傳輸資料
- Socket 可以用來建立 TCP 連接
HTTP 長連接與短連接
短連接(HTTP/1.0 預設):
- 瀏覽器和伺服器每進行一次 HTTP 操作,就建立一次連接
- 任務結束後立即中斷連接
- 每個資源(HTML、CSS、JS、圖片)都需要獨立的連接
長連接(HTTP/1.1 預設):
- 使用 Keep-Alive 保持連接
- 網頁開啟完成後,TCP 連接不會關閉
- 客戶端再次訪問相同伺服器時,繼續使用已建立的連接
- 有保持時間限制,可在伺服器配置中設定
實質:HTTP 協定的長短連接,實質上是 TCP 協定的長短連接
HTTP 304 (Not Modified)
- 含義:已讀取過的資源,由瀏覽器快取 (cache) 中讀取
- 作用:節省頻寬,加快頁面載入速度
- 觸發條件:
- 客戶端發送帶有
If-Modified-Since或If-None-Match的請求 - 伺服器判斷資源未修改,返回 304 狀態碼
- 瀏覽器使用本地快取的版本
- 客戶端發送帶有
4.3 JWT (JSON Web Token)
什麼是 JWT?
JWT 全名為 JSON Web Token,是一種基於 JSON 的開放標準(RFC 7519)。
結構:由三部分組成,以 . 分隔
- Header:包含 token 類型和加密演算法
- Payload:包含聲明 (claims),如用戶 ID、過期時間等
- Signature:使用 Header 中指定的演算法 (HMAC、RSA、ECDSA) 對 Header 和 Payload 進行簽名
範例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 的優點
- ✅ 無狀態:伺服器不需要存儲 session
- ✅ 可擴展:適合分散式系統和微服務
- ✅ 跨域支援:可在多個網域間使用
- ✅ 自包含:Token 本身包含用戶資訊
JWT 的缺點
- ❌ Cross-site 攻擊:可能遭受 XSS 攻擊
- ❌ Local storage 不安全:存儲在 Local Storage 中容易被竊取
- ❌ 無法單獨銷毀:Token 在過期前無法撤銷,除非使用黑名單機制
使用時機
- ✅ Token 生命期較短:讓擁有此 Token 的用戶能在時間內完成特定操作(如登入、下載檔案)
- ✅ Token 僅單次使用:任何 Token 只用於一次後就會被拋棄,不存在於任何持久化狀態
安全建議
- 使用 HTTPS 傳輸 JWT
- 設定合理的過期時間
- 敏感資訊不要放在 Payload 中(因為 Base64 可解碼)
- 考慮使用 Refresh Token 機制
- 實現 Token 黑名單(如 Redis)用於撤銷
5. 應用架構
5.1 Middleware (中介層)
什麼是 Middleware?
Middleware 是位於傳入請求和核心應用邏輯之間的程式碼層,用於攔截、修改或處理請求和回應,而不改變主要業務邏輯。
流程:
Client → Middleware → App → Middleware → Response → Client
為什麼使用 Middleware?
Middleware 讓你可以將常見關注點從核心應用邏輯中解耦:
| 使用案例 | 說明 |
|---|---|
| ✅ 認證 (Authentication) | 驗證 token、session、API key |
| ✅ 日誌 (Logging) | 記錄請求/回應詳情 |
| ✅ 速率限制 (Rate Limiting) | 基於 IP/用戶防止濫用 |
| ✅ CORS / Headers | 添加/修改 header 處理跨域請求 |
| ✅ 錯誤處理 (Error Handling) | 捕獲並回應異常 |
| ✅ 請求解析/驗證 | 確保資料乾淨且安全 |
Flask 範例
from flask import Flask, request
app = Flask(__name__)
# 請求前的日誌記錄
@app.before_request
def log_request():
print(f"[{request.method}] {request.path}")
# 請求後添加自訂 header
@app.after_request
def add_custom_header(response):
response.headers['X-App-Version'] = '1.0.0'
return response
@app.route("/hello")
def hello():
return "Hello, World!"
Flask 認證 Middleware 範例
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# 模擬的 token 儲存(生產環境應使用資料庫或外部認證服務)
VALID_API_TOKENS = {
"token123": "user_a",
"token456": "user_b"
}
@app.before_request
def authenticate():
# 定義不需要認證的公開路徑
public_paths = ['/health', '/login']
if request.path in public_paths:
return # 跳過認證檢查
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
abort(401, description="Missing or malformed Authorization header")
token = auth_header.split("Bearer ")[1]
user = VALID_API_TOKENS.get(token)
if not user:
abort(401, description="Invalid or expired token")
# 將用戶資訊附加到全域 request context
request.user = user
@app.route("/health")
def health_check():
return {"status": "ok"}
@app.route("/protected")
def protected_resource():
return {"message": f"Hello, {request.user}. You accessed a protected route."}
if __name__ == "__main__":
app.run(debug=True)
Express (Node.js) 範例
const express = require('express');
const app = express();
// 日誌 middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // 傳遞控制權給下一個 middleware 或路由
});
// 認證 middleware
app.use((req, res, next) => {
if (!req.headers['authorization']) {
return res.status(401).send('Unauthorized');
}
next();
});
app.get('/api/data', (req, res) => {
res.send({ message: 'Protected data' });
});
核心概念
- Middleware 通常是無狀態的,獨立處理每個請求
- Middleware 可以短路請求(如在到達路由前返回 401)
- 通常是堆疊或組合的,形成管道
- 可以是自訂的或來自框架/函式庫
各框架的 Middleware
| 框架 | Middleware 術語 |
|---|---|
| Express (Node.js) | app.use() handlers |
| Flask (Python) | @before_request, WSGI middleware |
| Django | MIDDLEWARE setting in settings.py |
| ASP.NET Core | Middleware pipeline with UseXXX() |
| Ruby on Rails | Rack middleware |
5.2 ORM (Object-Relational Mapping)
什麼是 ORM?
ORM 是一種程式設計技術,用於在關聯式資料庫和物件導向程式語言之間建立映射關係。
核心概念:
- 將資料庫表映射為類別 (Class)
- 將表中的行映射為物件實例 (Object Instance)
- 將欄位映射為物件屬性 (Attribute)
優點:
- ✅ 減少 SQL 程式碼,提高開發效率
- ✅ 資料庫無關性,易於切換資料庫
- ✅ 型別安全,減少 SQL 注入風險
- ✅ 易於維護和測試
缺點:
- ❌ 性能可能不如原生 SQL(特別是複雜查詢)
- ❌ 學習曲線(需要理解 ORM 框架)
- ❌ 可能產生低效的 SQL(N+1 問題)
常見 ORM 框架:
- Java: Hibernate, JPA, MyBatis
- Python: SQLAlchemy, Django ORM
- Node.js: Sequelize, TypeORM, Prisma
- .NET: Entity Framework
5.3 MVC vs MVVM
MVC (Model-View-Controller)
架構:
- Model:資料和業務邏輯
- View:使用者介面
- Controller:處理用戶輸入,協調 Model 和 View
流程:
User → Controller → Model → Controller → View → User
優點:
- ✅ 職責分離
- ✅ 易於測試
- ✅ 適合傳統 Web 應用
缺點:
- ❌ Controller 可能變得臃腫
- ❌ View 和 Model 可能存在耦合
MVVM (Model-View-ViewModel)
架構:
- Model:資料和業務邏輯
- View:使用者介面
- ViewModel:View 的抽象,處理 View 的展示邏輯和狀態
流程:
User → View ↔ ViewModel ↔ Model
特點:
- View 和 ViewModel 之間通過資料綁定 (Data Binding) 自動同步
優點:
- ✅ View 和業務邏輯完全分離
- ✅ 易於單元測試(ViewModel 可獨立測試)
- ✅ 適合現代前端框架(Vue.js, Angular, React)
缺點:
- ❌ 學習曲線較陡
- ❌ 過度使用資料綁定可能影響性能
6. 常見面試題
6.1 如何建立全域事務?
回答要點:
- 說明 2PC 適用於強一致性需求
- 說明 Saga 適用於微服務架構
- 提到 TCC、Outbox 模式等替代方案
- 強調需考慮一致性、可用性、性能的權衡
詳見 3.1 全域事務
6.2 建立分散式 Redis 鎖時的考量?
回答要點:
- 原子性:使用
SET key value NX EX timeout確保鎖的獲取和過期時間設定是原子的 - 過期時間:設定合理的 TTL,避免死鎖
- 鎖的釋放:確保在 finally 塊中釋放鎖
- 重試策略:使用指數退避 (exponential backoff)
- 唯一標識:使用 UUID 作為鎖的值,釋放時驗證是否為自己持有的鎖
- Redlock 演算法:多 Redis 實例場景下使用 Redlock 提高可靠性
6.3 如何優化 SQL 查詢?如何使用執行計畫?
回答要點:
- 使用
EXPLAIN或EXPLAIN ANALYZE查看執行計畫 - 識別昂貴的操作:全表掃描、filesort、using temporary
- 確保 WHERE 和 JOIN 使用索引
- 避免
SELECT *,只查詢需要的欄位 - 分析 JOIN 順序,將過濾條件前置
- 為 ORDER BY 和 GROUP BY 欄位建立索引
6.4 Spring Boot Cloud 主要組件有哪些?
常見組件:
- Eureka:服務註冊與發現
- Ribbon:客戶端負載平衡
- Feign:聲明式 REST 客戶端
- Hystrix:斷路器,實現服務降級和熔斷
- Zuul / Gateway:API 閘道
- Config Server:集中式配置管理
- Sleuth + Zipkin:分散式追蹤
6.5 如何確保資料庫資料一致性?
回答要點:
- 事務隔離級別:根據需求選擇合適的隔離級別
- 樂觀鎖 vs 悲觀鎖:
- 悲觀鎖:
SELECT ... FOR UPDATE - 樂觀鎖:版本號或時間戳
- 悲觀鎖:
- 分散式事務:使用 2PC、Saga、TCC 等模式
- 冪等性:確保操作可重複執行而不影響結果
- 補償機制:失敗時執行補償邏輯
6.6 如何使用 Redis 快取和資料庫(MySQL),當 Redis 可能失效時?
回答要點:
- 快取模式:使用 Cache-Aside 模式
- 快取一致性:
- 寫入時使用
@CacheEvict清除快取 - 或使用
@CachePut同時更新快取
- 寫入時使用
- 快取失效場景:
- 快取穿透:使用布隆過濾器或快取空值
- 快取擊穿:熱點資料設為永不過期或使用互斥鎖
- 快取雪崩:使用隨機過期時間或 Redis 叢集
- 降級策略:Redis 不可用時直接查詢資料庫
詳見 2.1 快取三大問題、2.3 Spring Boot + Redis 快取實踐
6.7 Interface、Class 在編譯時、執行時的使用,以及與 Bytecode 的關係?
回答要點:
編譯時 (Compile Time):
- Interface 定義契約,Class 實現契約
- 編譯器檢查型別安全、方法簽名等
- Java 編譯器將
.java檔案編譯為.classbytecode 檔案
執行時 (Runtime):
- JVM 載入 bytecode 並執行
- 使用多型 (Polymorphism) 時,JVM 根據實際物件類型調用對應方法
- Interface 在執行時通過虛擬方法表 (Virtual Method Table) 實現動態綁定
與 Bytecode 的關係:
- Interface 編譯後產生
.class檔案,包含方法簽名但無實現 - Class 編譯後產生包含實現的 bytecode
- Bytecode 指令如
invokevirtual、invokeinterface用於方法調用
參考資料
- Redis 快取雪崩、擊穿、穿透
- 小林 coding - 緩存問題
- ExplainThis - 反向代理
- MySQL Lock
- 鎖的介紹與死鎖分析
- Database Transaction Isolation
- MySQL 事務隔離級別
- 後端面試題
最後更新日期: 2026-02-14