我寫 Solon 已經有一段時間了,早期讓我驚訝的一點是它的快取層有多乾淨。不是因為它做了什麼革命性的事——它沒有——而是因為它 在處理生產環境中真正遇到的邊緣案例時,仍然能「不礙手礙腳」。
讓我來分享一下我學到的東西。
三個註解
Solon 只提供了三個快取註解。不多也不少:
-
@Cache— 讀取快取。如果金鑰存在就直接回傳;否則執行方法並儲存結果。 -
@CachePut— 總是執行方法,然後用結果更新快取。 -
@CacheRemove— 執行方法,然後清除一個或多個快取項目。
以下是一個展示全部三個註解的最小範例:
@Controller
public class CacheController {
@Cache(key = "user:${userId}", seconds = 30)
@Mapping("/user/{userId}")
public User getUser(int userId) {
return userService.findById(userId);
}
@CachePut(key = "user:${userId}")
@Mapping("/user/{userId}/refresh")
public User refreshUser(int userId) {
return userService.findById(userId);
}
@CacheRemove(tags = "users")
@Mapping("/user/{userId}/delete")
public String deleteUser(int userId) {
userService.delete(userId);
return "deleted";
}
}
Enter fullscreen mode Exit fullscreen mode
@CacheRemove(tags = "users") 這一行值得進一步探討——它完成了一項大多數基於註解的快取系統都難以做到的事。
標籤技巧
大多數快取框架只提供基於金鑰的清除。你知道金鑰,就能移除它。對簡單情境來說沒問題,但如果是「讓所有與使用者相關的快取失效」呢?
這就是標籤的用武之地。當你用 @Cache(tags = "users") 註解方法時,Solon 不只會將值儲存在計算後的金鑰之下——它還會把該金鑰記錄到標籤索引中。之後當你呼叫 @CacheRemove(tags = "users") 時,它會找出所有與該標籤關聯的金鑰並一次清除它們。
@Cache(key = "user:${userId}", tags = "users")
@Mapping("/user/{userId}")
public User getUser(int userId) { ... }
@Cache(key = "user:${userId}:profile", tags = "users")
@Mapping("/user/{userId}/profile")
public Profile getProfile(int userId) { ... }
// Both caches get cleared in one shot
@CacheRemove(tags = "users")
@Mapping("/admin/users/flush")
public String flushUsers() { ... }
Enter fullscreen mode Exit fullscreen mode
我經常使用這個模式來處理分頁列表。你可以快取搜尋結果的第 1、2、3 頁——全部都標記為 "search"。當有新資料進來時,一個 @CacheRemove(tags = "search") 就能全部清除。
文件提醒了一件事:標籤是透過將收集到的金鑰儲存為列表來運作,因此不要將數千個金鑰標記在同一個標籤下。對於大多數真實世界的場景——每個標籤有幾十到幾百個金鑰——都沒問題。
金鑰範本:超越簡單字串
key 參數支援 ${} 表達式,可解析方法參數:
// Simple parameter
@Cache(key = "user:${userId}")
public User getUser(int userId) { ... }
// Nested property
@Cache(key = "user:${user.id}")
public User getUser(UserDto user) { ... }
// Map key
@Cache(key = "result:${params.type}")
public List<Result> search(Map<String, String> params) { ... }
Enter fullscreen mode Exit fullscreen mode
如果不指定 key,Solon 會根據所有參數名稱和值的 MD5 自動產生一個。它能用,但我建議永遠提供明確的 key——這會讓除錯和手動檢查更容易。
還有一個特殊的 ${.property} 語法用來引用回傳值,主要用於 @CachePut:
@CachePut(key = "order:${.orderId}")
public Order createOrder(CreateRequest req) {
// ... create order, return it
}
Enter fullscreen mode Exit fullscreen mode
讓快取可替換的介面
在底層,所有快取都透過 CacheService 運作:
public interface CacheService {
void store(String key, Object obj, int seconds);
Object get(String key);
void remove(String key);
}
Enter fullscreen mode Exit fullscreen mode
只有三個方法。預設實作是 LocalCacheService——一個適合單一實例應用程式的記憶體快取。
但真正的威力在於你可以用單一行 bean 定義來替換它:
@Configuration
public class Config {
@Bean
public CacheService cache(@Inject("${cache}") Properties props) {
return new MemCacheService(props);
}
}
Enter fullscreen mode Exit fullscreen mode
一旦你定義了 CacheService bean,它就會全域替換預設實作。所有 @Cache、@CachePut 和 @CacheRemove 註解都會自動使用新的後端。
透過設定切換後端
Solon 也提供 CacheServiceSupplier,根據設定選擇後端:
demo.cache1:
driverType: "local"
# driverType: "redis"
# driverType: "memcached"
Enter fullscreen mode Exit fullscreen mode
@Bean
public CacheService cache1(@Inject("${demo.cache1}") CacheServiceSupplier supplier) {
return supplier.get();
}
Enter fullscreen mode Exit fullscreen mode
內建支援的後端:
| 後端 | 外掛 | 說明 |
|---|---|---|
| Local |
solon-data (built-in) |
預設,無需額外依賴 |
| Redis |
solon-cache-jedis 或 solon-cache-redisson
|
選擇你的用戶端 |
| Memcached | solon-cache-spymemcached |
老但仍在使用 |
二級快取:本地 + 遠端
我在讀取密集型服務中發現的一個有用模式是二級快取。它在遠端快取(Redis、Memcached)前面疊加一層本地記憶體快取:
@Bean
public CacheService cache1(@Inject("${demo.cache1}") CacheServiceSupplier supplier) {
CacheService remote = supplier.get();
if (remote instanceof LocalCacheService) {
return remote;
}
// Local L1 + remote L2, with a 5-second buffer
LocalCacheService local = new LocalCacheService();
return new SecondCacheService(local, remote, 5);
}
Enter fullscreen mode Exit fullscreen mode
「緩衝」的意思是:當本地快取未命中但遠端快取命中時,結果會在本地儲存 5 秒。在這 5 秒內的後續讀取會直接命中本地快取——無需網路往返。對於熱門資料,這可以大幅降低 Redis 負載。
分區設計
一旦有多個服務共用同一個 Redis 實例,你會想要對快取進行分區。Solon 透過命名快取服務支援這一點:
demo.cache.user:
keyHeader: "user"
server: "localhost:6379"
db: 0
demo.cache.order:
keyHeader: "order"
server: "localhost:6379"
db: 1
Enter fullscreen mode Exit fullscreen mode
@Configuration
public class Config {
@Bean("cache_user")
public CacheService cacheUser(@Inject("${demo.cache.user}") RedisCacheService cache) {
return cache.enableMd5key(false);
}
@Bean("cache_order")
public CacheService cacheOrder(@Inject("${demo.cache.order}") RedisCacheService cache) {
return cache.enableMd5key(false);
}
}
Enter fullscreen mode Exit fullscreen mode
然後在你的服務中使用它們:
@Component
public class OrderService {
@Cache(service = "cache_order")
public String hello(String name) {
return String.format("Hello %s!", name);
}
}
Enter fullscreen mode Exit fullscreen mode
不同的資料庫、不同的金鑰前綴,而註解只需要一個 service 屬性就能選擇正確的快取服務。
我想改變的地方
如果我有個小地方要挑剔,那就是當你不指定 key 時,自動產生的金鑰是所有參數的 MD5。實際上,我總是指定明確的 key,所以這對我來說不是問題。但對框架新手來說,當他們在快取存放區中看到不透明的雜湊字串時,可能會造成混淆。
結論
Solon 的快取層簡單得令人耳目一新。三個註解、一個介面,以及一個比我用過的大多數框架都更能處理批次失效的標籤系統。CacheService 介面實作非常簡單,這意味著你幾乎可以插入任何你想要的後端——或者為你的特定使用情境撰寫自訂實作。
如果你已經在使用 Solon,快取註解位於 solon-data 模組中,你可能已經將其作為依賴項目了。只要加上 @Cache 就完成了。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.