後端工程師的後端程式筆記
重點:高流量、並發、鎖的模式,以及實際怎麼寫
目錄
高流量模式
1. 冪等的請求處理
問題:設計一個機制,在可設定的時間窗內,避免同一個 requestId 的請求被重複處理。
最初的設計(V0 — 面試白板版)
java
/**
* Design a mechanism that prevents duplicate processing of requests
* with the same requestId within a configurable time window
*
* Key considerations:
* - Is duplicate? -> check requestId
* - Configurable time window
* - Thread safety
* - Lock timeout
*/
int TIME_WINDOW = 10;
int LOCK_TIME_OUT = 60; // 60 sec
class Request {
String reqId;
String msg;
long timestamp; // unix time
}
class Resp {
String status;
}
public class IdempotentRequestProcessor {
Map<String, Request> processedReq;
IdempotentRequestProcessor() {
this.map = new HashMap(); // NOT thread-safe!
}
public Resp processRequest(Request req) {
// Problem: Race condition here!
if (this.map.contains(req.reqId)) {
if (map.get(req.reqId).timestamp < req.time - TIME_WINDOW) {
return new Resp("duplicated req in time window");
}
}
this.put(req.reqId, req);
return new Resp("req is received");
}
}
V0 的問題:
HashMap不是執行緒安全的- 先檢查再動作(check-then-act)不是原子的(有競態)
- 過期的項目沒有清理機制
- 鎖的逾時沒有實作
解法 V1:ConcurrentHashMap 加上原子檢查
java
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.time.Instant;
@Data
@AllArgsConstructor
class Request {
private String reqId;
private String msg;
private long timestamp; // Unix timestamp in seconds
}
@Data
@AllArgsConstructor
class Resp {
private String status;
private String message;
}
public class IdempotentRequestProcessor {
// Configurable window in seconds
private static final int TIME_WINDOW = 10;
// Stores RequestId -> Timestamp of first successful receipt
private final Map<String, Long> processedReqs = new ConcurrentHashMap<>();
public Resp processRequest(Request req) {
long currentTime = Instant.now().getEpochSecond();
// Check if duplicate within window
Long existingTimestamp = processedReqs.get(req.getReqId());
if (existingTimestamp != null) {
if (currentTime - existingTimestamp < TIME_WINDOW) {
return new Resp("REJECTED", "Duplicate request within time window.");
}
// Outside window - treat as fresh request
}
// Store the request timestamp
processedReqs.put(req.getReqId(), currentTime);
return executeBusinessLogic(req);
}
private Resp executeBusinessLogic(Request req) {
try {
// Simulate processing
return new Resp("SUCCESS", "Request processed: " + req.getReqId());
} catch (Exception e) {
// If fails, remove from map so user can retry
processedReqs.remove(req.getReqId());
return new Resp("ERROR", "Processing failed.");
}
}
// TODO: Add cleanup task (ScheduledExecutor) to prevent infinite growth
}
有改善,但問題還在:
get()之後再put()不是原子的 —— 競態還在!
解法 V2:用 compute() 拿到真正的原子性
java
import java.util.concurrent.*;
class Request {
String reqId;
String msg;
long timestamp; // epoch seconds
public Request(String reqId, String msg, long timestamp) {
this.reqId = reqId;
this.msg = msg;
this.timestamp = timestamp;
}
}
class Resp {
String status;
public Resp(String status) {
this.status = status;
}
}
public class IdempotentRequestProcessor {
private final long TIME_WINDOW_SECONDS = 10;
// reqId -> last processed timestamp
private final ConcurrentHashMap<String, Long> processedReq = new ConcurrentHashMap<>();
public Resp processRequest(Request req) {
long now = req.timestamp;
// Atomic check + update using compute()
Long[] resultHolder = new Long[1];
boolean[] isDuplicate = new boolean[1];
processedReq.compute(req.reqId, (key, oldTimestamp) -> {
if (oldTimestamp == null) {
// First time seeing this request
resultHolder[0] = now;
isDuplicate[0] = false;
return now;
}
if (now - oldTimestamp <= TIME_WINDOW_SECONDS) {
// Duplicate within window - keep old timestamp
isDuplicate[0] = true;
return oldTimestamp;
}
// Outside window - update timestamp
resultHolder[0] = now;
isDuplicate[0] = false;
return now;
});
if (isDuplicate[0]) {
return new Resp("duplicate request");
}
// Process actual request
return process(req);
}
private Resp process(Request req) {
try {
// Simulate business logic
return new Resp("processed: " + req.reqId);
} catch (Exception e) {
return new Resp("failed");
}
}
}
解法 V3:可上線的版本,附清理機制
java
import java.util.concurrent.*;
import java.time.Instant;
public class IdempotentRequestProcessor {
private final long timeWindowSeconds;
private final ConcurrentHashMap<String, Long> processedReqs = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor;
public IdempotentRequestProcessor(long timeWindowSeconds, long cleanupIntervalSeconds) {
this.timeWindowSeconds = timeWindowSeconds;
// Schedule periodic cleanup to prevent memory leak
this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
cleanupExecutor.scheduleAtFixedRate(
this::cleanupExpiredEntries,
cleanupIntervalSeconds,
cleanupIntervalSeconds,
TimeUnit.SECONDS
);
}
public Resp processRequest(Request req) {
long now = Instant.now().getEpochSecond();
boolean[] isDuplicate = {false};
processedReqs.compute(req.getReqId(), (key, oldTimestamp) -> {
if (oldTimestamp == null) {
return now;
}
if (now - oldTimestamp <= timeWindowSeconds) {
isDuplicate[0] = true;
return oldTimestamp;
}
return now;
});
if (isDuplicate[0]) {
return new Resp("REJECTED", "Duplicate request");
}
return executeBusinessLogic(req);
}
private void cleanupExpiredEntries() {
long now = Instant.now().getEpochSecond();
processedReqs.entrySet().removeIf(
entry -> now - entry.getValue() > timeWindowSeconds * 2
);
}
public void shutdown() {
cleanupExecutor.shutdown();
}
private Resp executeBusinessLogic(Request req) {
// Business logic here
return new Resp("SUCCESS", "Processed: " + req.getReqId());
}
}
解法 V4:用 Redis 的分散式版本
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
public class DistributedIdempotentProcessor {
private final Jedis redis;
private final long timeWindowSeconds;
private static final String KEY_PREFIX = "idempotent:";
public DistributedIdempotentProcessor(Jedis redis, long timeWindowSeconds) {
this.redis = redis;
this.timeWindowSeconds = timeWindowSeconds;
}
public Resp processRequest(Request req) {
String key = KEY_PREFIX + req.getReqId();
// SET key value NX EX ttl - sets the key and its TTL in ONE atomic command.
// (bare SETNX has no expiry: SETNX then EXPIRE can crash in between and leak a permanent lock)
String result = redis.set(
key,
String.valueOf(System.currentTimeMillis()),
SetParams.setParams().nx().ex(timeWindowSeconds)
);
if (result == null) {
// Key already exists - duplicate request
return new Resp("REJECTED", "Duplicate request");
}
// First time - process the request
try {
return executeBusinessLogic(req);
} catch (Exception e) {
// On failure, delete the key so a retry can run — but only if we still
// own it. Comparing the stored value first (here via a Lua CAS-delete)
// stops a slow attempt whose TTL already expired from deleting the lock
// a *newer* attempt is now holding. And note "failure" here must mean a
// definite one: after a timeout the side effect may already have
// happened, so that case belongs in reconciliation, not in a retry.
redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] "
+ "then return redis.call('del', KEYS[1]) else return 0 end",
1, key, ownerToken);
return new Resp("ERROR", "Processing failed");
}
}
private Resp executeBusinessLogic(Request req) {
return new Resp("SUCCESS", "Processed: " + req.getReqId());
}
}
冪等處理的重點整理
| 面向 | 單一節點 | 分散式 |
|---|---|---|
| 儲存 | ConcurrentHashMap |
Redis / 資料庫 |
| 原子性 | compute() / computeIfAbsent() |
SET key val NX EX ttl(單一指令;裸的 SETNX 沒有過期時間) |
| 清理 | ScheduledExecutorService |
Redis TTL 自動過期 |
| 失敗處理 | 出錯時從 map 移除 | 出錯時刪掉該 key |
2. 限流
Token Bucket 實作
java
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class TokenBucketRateLimiter {
private final long maxTokens;
private final long refillRate; // tokens per second
private final AtomicReference<State> state;
private static class State {
final long tokens;
final long lastRefillTime;
State(long tokens, long lastRefillTime) {
this.tokens = tokens;
this.lastRefillTime = lastRefillTime;
}
}
public TokenBucketRateLimiter(long maxTokens, long refillRate) {
this.maxTokens = maxTokens;
this.refillRate = refillRate;
this.state = new AtomicReference<>(new State(maxTokens, System.nanoTime()));
}
public boolean tryAcquire() {
while (true) {
State currentState = state.get();
long now = System.nanoTime();
long elapsedNanos = now - currentState.lastRefillTime;
// Calculate tokens to add
long tokensToAdd = (elapsedNanos * refillRate) / 1_000_000_000L;
long newTokens = Math.min(maxTokens, currentState.tokens + tokensToAdd);
if (newTokens < 1) {
return false; // No tokens available
}
// Try to consume one token
State newState = new State(newTokens - 1, now);
if (state.compareAndSet(currentState, newState)) {
return true;
}
// CAS failed - retry
}
}
}
滑動視窗限流器
java
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.Deque;
public class SlidingWindowRateLimiter {
private final int maxRequests;
private final long windowSizeMs;
private final Deque<Long> requestTimestamps = new ConcurrentLinkedDeque<>();
public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs) {
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long windowStart = now - windowSizeMs;
// Remove expired timestamps
while (!requestTimestamps.isEmpty() && requestTimestamps.peekFirst() < windowStart) {
requestTimestamps.pollFirst();
}
if (requestTimestamps.size() < maxRequests) {
requestTimestamps.addLast(now);
return true;
}
return false;
}
}
3. 斷路器(Circuit Breaker)
java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class CircuitBreaker {
private enum State { CLOSED, OPEN, HALF_OPEN }
private volatile State state = State.CLOSED;
private final AtomicInteger failureCount = new AtomicInteger(0);
private final AtomicLong lastFailureTime = new AtomicLong(0);
private final int failureThreshold;
private final long resetTimeoutMs;
private final int halfOpenMaxCalls;
private final AtomicInteger halfOpenCalls = new AtomicInteger(0);
public CircuitBreaker(int failureThreshold, long resetTimeoutMs, int halfOpenMaxCalls) {
this.failureThreshold = failureThreshold;
this.resetTimeoutMs = resetTimeoutMs;
this.halfOpenMaxCalls = halfOpenMaxCalls;
}
public boolean allowRequest() {
switch (state) {
case CLOSED:
return true;
case OPEN:
if (System.currentTimeMillis() - lastFailureTime.get() > resetTimeoutMs) {
state = State.HALF_OPEN;
halfOpenCalls.set(0);
return true;
}
return false;
case HALF_OPEN:
return halfOpenCalls.incrementAndGet() <= halfOpenMaxCalls;
default:
return false;
}
}
public void recordSuccess() {
if (state == State.HALF_OPEN) {
state = State.CLOSED;
}
failureCount.set(0);
}
public void recordFailure() {
lastFailureTime.set(System.currentTimeMillis());
if (state == State.HALF_OPEN) {
state = State.OPEN;
return;
}
if (failureCount.incrementAndGet() >= failureThreshold) {
state = State.OPEN;
}
}
public <T> T execute(SupplierWithException<T> action) throws Exception {
if (!allowRequest()) {
throw new CircuitBreakerOpenException("Circuit breaker is OPEN");
}
try {
T result = action.get();
recordSuccess();
return result;
} catch (Exception e) {
recordFailure();
throw e;
}
}
@FunctionalInterface
public interface SupplierWithException<T> {
T get() throws Exception;
}
public static class CircuitBreakerOpenException extends RuntimeException {
public CircuitBreakerOpenException(String message) {
super(message);
}
}
}
4. 用分散式鎖做請求去重
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.UUID;
public class DistributedLockWithDedup {
private final Jedis redis;
private final String lockPrefix = "lock:";
private final String processedPrefix = "processed:";
public DistributedLockWithDedup(Jedis redis) {
this.redis = redis;
}
/**
* Process request with distributed lock and deduplication
*/
public <T> T processWithLock(String requestId, long lockTimeoutSec,
long dedupWindowSec, RequestProcessor<T> processor) {
// 1. Check if already processed
if (redis.exists(processedPrefix + requestId)) {
throw new DuplicateRequestException("Request already processed");
}
// 2. Acquire distributed lock
String lockKey = lockPrefix + requestId;
String lockValue = UUID.randomUUID().toString();
String acquired = redis.set(
lockKey, lockValue,
SetParams.setParams().nx().ex(lockTimeoutSec)
);
if (acquired == null) {
throw new LockNotAcquiredException("Could not acquire lock");
}
try {
// 3. Double-check after acquiring lock
if (redis.exists(processedPrefix + requestId)) {
throw new DuplicateRequestException("Request already processed");
}
// 4. Process the request
T result = processor.process();
// 5. Mark as processed
redis.setex(processedPrefix + requestId, dedupWindowSec, "1");
return result;
} finally {
// 6. Release lock (only if we own it)
releaseLock(lockKey, lockValue);
}
}
/**
* Lua script for safe lock release (only release if we own the lock)
*/
private void releaseLock(String lockKey, String lockValue) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.eval(script,
java.util.Collections.singletonList(lockKey),
java.util.Collections.singletonList(lockValue)
);
}
@FunctionalInterface
public interface RequestProcessor<T> {
T process();
}
public static class DuplicateRequestException extends RuntimeException {
public DuplicateRequestException(String message) { super(message); }
}
public static class LockNotAcquiredException extends RuntimeException {
public LockNotAcquiredException(String message) { super(message); }
}
}
並發模式
ConcurrentHashMap 的操作
| 方法 | 行為 | 適用場景 |
|---|---|---|
putIfAbsent(k, v) |
key 不存在才插入 | 單純去重 |
computeIfAbsent(k, func) |
不存在時才延遲計算值 | 延遲初始化 |
compute(k, func) |
一律計算(原子的讀-改-寫) | 有條件的更新 |
merge(k, v, func) |
和既有值合併 | 計數、彙總 |
java
// Example: Atomic counter increment
ConcurrentHashMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
// Safe increment
counters.computeIfAbsent("hits", k -> new AtomicLong(0)).incrementAndGet();
// Conditional update
counters.compute("hits", (k, v) -> {
if (v == null) return new AtomicLong(1);
v.incrementAndGet();
return v;
});
原子操作
java
import java.util.concurrent.atomic.*;
// AtomicInteger/Long for simple counters
AtomicLong counter = new AtomicLong(0);
counter.incrementAndGet();
counter.compareAndSet(expected, newValue);
// AtomicReference for object references
AtomicReference<State> stateRef = new AtomicReference<>(initialState);
stateRef.compareAndSet(oldState, newState);
// LongAdder for high-contention counters (better than AtomicLong)
LongAdder adder = new LongAdder();
adder.increment();
adder.sum(); // Get total
鎖的策略
1. 帶逾時的 ReentrantLock
java
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
ReentrantLock lock = new ReentrantLock();
public void processWithTimeout() throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// Critical section
} finally {
lock.unlock();
}
} else {
throw new RuntimeException("Could not acquire lock");
}
}
2. 讀多寫少時用 ReadWriteLock
java
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public Data read() {
rwLock.readLock().lock();
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}
public void write(Data newData) {
rwLock.writeLock().lock();
try {
this.data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
3. 用 StampedLock 做樂觀讀
java
import java.util.concurrent.locks.StampedLock;
StampedLock lock = new StampedLock();
public Data optimisticRead() {
long stamp = lock.tryOptimisticRead();
Data localData = this.data;
// Check if data was modified during read
if (!lock.validate(stamp)) {
// Fall back to regular read lock
stamp = lock.readLock();
try {
localData = this.data;
} finally {
lock.unlockRead(stamp);
}
}
return localData;
}
API 設計模式
冪等鍵(Idempotency Key)模式
java
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(
@RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestBody CreateOrderRequest request) {
// Check if already processed
Optional<Order> existing = orderService.findByIdempotencyKey(idempotencyKey);
if (existing.isPresent()) {
return ResponseEntity.ok(existing.get());
}
// Process and store with idempotency key
Order order = orderService.createOrder(request, idempotencyKey);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
指數退避重試
java
public <T> T executeWithRetry(Supplier<T> action, int maxRetries) {
int attempt = 0;
long backoffMs = 100;
while (true) {
try {
return action.get();
} catch (Exception e) {
attempt++;
if (attempt >= maxRetries) {
throw e;
}
try {
Thread.sleep(backoffMs + (long)(Math.random() * backoffMs));
backoffMs *= 2; // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
}
}
}
後端常用的 Java 設計模式
1. Singleton(執行緒安全)
java
// Double-checked locking with volatile
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// Better: Enum Singleton
public enum SingletonEnum {
INSTANCE;
public void doSomething() { }
}
// Better: Static holder (lazy + thread-safe)
public class SingletonHolder {
private SingletonHolder() {}
private static class Holder {
static final SingletonHolder INSTANCE = new SingletonHolder();
}
public static SingletonHolder getInstance() {
return Holder.INSTANCE;
}
}
2. 用 BlockingQueue 做生產者-消費者
java
import java.util.concurrent.*;
public class ProducerConsumer {
private final BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
private final ExecutorService consumers = Executors.newFixedThreadPool(4);
public void start() {
for (int i = 0; i < 4; i++) {
consumers.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = queue.take(); // Blocks if empty
task.process();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
}
public void submit(Task task) throws InterruptedException {
queue.put(task); // Blocks if full
}
public void shutdown() {
consumers.shutdownNow();
}
}
3. 物件池(Object Pool)模式
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ConnectionPool {
private final BlockingQueue<Connection> pool;
private final int maxSize;
public ConnectionPool(int maxSize) {
this.maxSize = maxSize;
this.pool = new LinkedBlockingQueue<>(maxSize);
// Pre-create connections
for (int i = 0; i < maxSize; i++) {
pool.offer(createConnection());
}
}
public Connection acquire() throws InterruptedException {
return pool.take();
}
public void release(Connection conn) {
if (conn != null && !conn.isClosed()) {
pool.offer(conn);
} else {
// Replace bad connection
pool.offer(createConnection());
}
}
private Connection createConnection() {
// Create new connection
return new Connection();
}
}
面試常見問題
Q1:為什麼要用 ConcurrentHashMap.compute(),而不是 get 加 put?
答:get() 之後接 put() 不是原子的。在這兩個操作之間,另一條執行緒就可能改動這個 map。compute() 把整個讀-改-寫當成一個原子操作完成。
Q2:synchronized 和 ReentrantLock 差在哪?
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 鎖可以逾時 | 不行 | 可以(tryLock) |
| 可被中斷 | 不行 | 可以 |
| 公平性 | 沒有 | 可設定 |
| 多個條件變數 | 不行 | 可以 |
| 試著拿鎖 | 不行 | 可以 |
Q3:什麼時候該用 volatile?
用 volatile 的時機:
- 變數會被多條執行緒存取
- 只有單純的讀或寫(沒有讀-改-寫)
- 不需要複合操作
不夠用的情況:counter++(讀-改-寫要用 AtomicInteger)
Q4:怎麼避免死鎖?
- 鎖的順序:永遠用同樣的順序取鎖
- 鎖的逾時:用帶 timeout 的
tryLock() - 避免嵌套鎖:把鎖的範圍縮到最小
- 用更高階的元件:
ConcurrentHashMap、BlockingQueue
總結:怎麼挑對工具
| 情境 | 解法 |
|---|---|
| 單純的計數器 | AtomicLong 或 LongAdder |
| 有條件的 map 更新 | ConcurrentHashMap.compute() |
| 讀多的快取 | ReadWriteLock 或 StampedLock |
| 請求去重(單節點) | ConcurrentHashMap + TTL 清理 |
| 請求去重(分散式) | Redis SET … NX EX ttl |
| 限流 | Token Bucket/滑動視窗 |
| 容錯 | 斷路器 |
| 生產者-消費者 | BlockingQueue |