高性能網站離不開多層緩存:首頁排行榜、熱門話題、最新註冊/登錄用戶等「讀多寫少」數據,若每次查數據庫將成為瓶頸。本文展示一種零侵入、聲明式、實時一致性的緩存更新方案:領域模型封裝緩存邏輯 + Spring AOP 切面攔截寫操作自動更新緩存,並深入分析高併發下的數據結構選型與一致性權衡。
┌─────────────────────────────────────┐
│ Controller / Service │ 業務邏輯純淨,無緩存代碼
├─────────────────────────────────────┤
│ Spring AOP 切面層 │ @AfterReturning 攔截寫方法
├─────────────────────────────────────┤
│ Cache Manager (單例) │ 線程安全容器 + 排序策略
├─────────────────────────────────────┤
│ Domain Model (POJO) │ 實現 Comparable/Comparator
└─────────────────────────────────────┘
public final class WebCacheUtils {
// 使用 CopyOnWriteArrayList 實現讀多寫少場景的高併發安全遍歷
private static final List RECENT_REGISTERED = new CopyOnWriteArrayList<>();
private static final List RECENT_LOGINED = new CopyOnWriteArrayList<>();
private static final List HOT_TOPICS = new CopyOnWriteArrayList<>();
// 熱門話題排序:權重 4:6 (贊同數:評論數)
private static final Comparator HOT_COMPARATOR = (t1, t2) -> {
double s1 = t1.getAgreeCount() * 0.4 + t1.getCommentCount() * 0.6;
double s2 = t2.getAgreeCount() * 0.4 + t2.getCommentCount() * 0.6;
return Double.compare(s2, s1); // 降序
};
public static void addRecentRegistered(Login user) {
if (user == null) return;
RECENT_REGISTERED.remove(user); // 去重
RECENT_REGISTERED.add(0, user); // 新增到頭部
trim(RECENT_REGISTERED, 50); // 保留 Top 50
}
public static void addRecentLogined(Login user) {
if (user == null) return;
RECENT_LOGINED.remove(user);
RECENT_LOGINED.add(0, user);
trim(RECENT_LOGINED, 100);
}
public static void updateHotTopic(Topic topic) {
if (topic == null) return;
HOT_TOPICS.remove(topic);
HOT_TOPICS.add(topic);
HOT_TOPICS.sort(HOT_COMPARATOR);
trim(HOT_TOPICS, 20);
}
private static void trim(List list, int maxSize) {
while (list.size() > maxSize) list.remove(list.size() - 1);
}
// 只讀獲取方法...
}
@Aspect
@Component
@Slf4j
public class CacheUpdateAspect {
@AfterReturning(pointcut = "execution(* com.example.service.UserService.checkUserLogin(..)) && args(..) && returning(result)",
returning = "result")
public void onUserLogin(JoinPoint jp, Object result) {
if (result instanceof Login login) {
log.debug("更新最近登錄緩存: {}", login.getUsername());
WebCacheUtils.addRecentLogined(login);
}
}
@AfterReturning(pointcut = "execution(* com.example.service.UserService.addUser(..)) && args(user)",
argNames = "user")
public void onUserRegister(Login user) {
if (user != null) {
log.debug("更新最新註冊緩存: {}", user.getUsername());
WebCacheUtils.addRecentRegistered(user);
}
}
@AfterReturning(pointcut = "execution(* com.example.service.TopicService.publishTopic(..)) && args(topic)",
argNames = "topic")
public void onTopicPublish(Topic topic) {
if (topic != null) {
WebCacheUtils.updateHotTopic(topic);
}
}
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Override
public Login checkUserLogin(String username, String password) {
// 純業務邏輯:驗證密碼、更新最後登錄時間、返回 Login 對象
return loginDao.authenticate(username, password);
}
@Override
public void addUser(Login user) {
userDao.insert(user);
}
}
| 維度 | 現狀/風險 | 優化建議 |
|---|---|---|
| 數據結構 | CopyOnWriteArrayList 寫入複製整個數組,寫入頻繁時 GC 壓力大 | 熱點緩存改用 ConcurrentSkipListSet (自帶排序) 或 ConcurrentHashMap + 定時重排 |
| 一致性 | 最終一致性:DB 提交後異步更新緩存,極端情況下緩存比 DB 滯後幾毫秒 | 引入 @TransactionalEventListener(phase = AFTER_COMMIT) 保證事務提交後再更新緩存 |
| 分佈式擴展 | 單機內存緩存,集群間不一致 | 遷移至 Redis Sorted Set (ZSET) 存儲排行榜,本地緩存僅作 L2 |
| 失效策略 | 無主動失效,依賴容量裁剪 | 加入 TTL、定時全量重建、關鍵業務變更主動刷新 |
| 異常處理 | 切面異常不影響主流程,但緩存可能不更新 | 切面內 try-catch 記錄錯誤,引入補償定時任務糾錯 |
請求 → [Nginx 本地緩存] → [應用層 Caffeine/Guava L1] → [Redis Cluster L2] → [MySQL]
↑ ↑ ↑
靜態資源 熱點數據(毫秒級) 排行榜/會話/分佈式鎖
ZINCRBY score member 原子更新分數,ZREVRANGE 獲取 Top N,完美替代手工排序邏輯Spring AOP + 域模型封裝緩存邏輯,實現了業務代碼零侵入、緩存更新邏輯集中管理、實時性與一致性可控。單機階段 CopyOnWriteArrayList 足夠應付中低併發;隨著流量增長,應及時引入 Caffeine 本地緩存 + Redis 分佈式有序集合,並建立完善的緩存監控(命中率、延遲、一致性延遲)與降級預案。