
實作 LRU Cache - Clean Architecture in Go
Clean Architecture 提到許多軟體開發的設計模式和原則,當我們將系統一定程度的套用這些設計後,就能得到很不錯的擴充能力。假設現在需要對資料庫的讀取功能加入快取機制,來應付逐漸變大的流量,能如何實作呢?
TokenRepository 介面
我們已經利用 Golang 語言特性以 usecase.TokenRepository
介面來避免資料讀取的細節混入商業邏輯,以此為基礎,我們可以像這樣改寫依賴的對象。
1// ...
2tokenRepo := repository.NewCacheTokenRepository(...)
3lookupOrder := usecase.NewLookupOrder(...)
然而,我們使用 wire 來管理依賴注入,受到 wire 的限制我們無法綁定兩個 usecase.TokenRepository
來使用,但我們可以利用 Golang 的語言特性,定義一個 CacheableTokenRepository
來解決這個問題。
1type CacheableTokenRepository usecase.TokenRepository
2
3// ...
4var SQLiteSet = wire.NewSet(
5 sqlite.New,
6 NewSQLiteOrderRepository,
7 wire.Bind(new(usecase.OrderRepository), new(*SQLiteOrderRepository)),
8 NewSQLiteTokenRepository,
9 wire.Bind(new(CachableTokenRepository), new(*SQLiteTokenRepository)),
10 NewLruTokenRepository,
11 wire.Bind(new(usecase.TokenRepository), new(*LruTokenRepository)),
12)
上述的例子可以看到,我們將原本的 SQLiteTokenRepository
綁定給 CacheableTokenRepository
並且將 LruTokenRepository
綁定給 usecase.TokenRepository
來形成一個一個三層的依賴關係。
usecase.LookupUseCase
repository.LruTokenRepository
repository.SQLiteTokenRepository
實際上就是利用了 Proxy 這個設計模式,讓我們的 Cache Repository 代理原有的操作,那麼就可以得到一些額外的操作空間。
即使在沒有介面概念的語言中,有意識的去區分出邊界就能很好的設計出恰當的介面,也能做到類似的效果
LruTokenRepository
接下來只需要將 LruTokenRepository
實現即可,因為 Golang 已經有 hashicorp/go-lru 這個套件可以使用,我們可以很簡單的製作有特定時效的 LRU Cache 設計。
打開 internal/repository/lru_tokens.go
加入以下實作。
1// ...
2
3const (
4 DefaultCacheSize = 1000
5 DefaultCacheTTL = 60
6)
7
8type CachableTokenRepository usecase.TokenRepository
9
10type LruTokenRepository struct {
11 cache *expirable.LRU[string, InMemoryTokenSchema]
12 tokens CachableTokenRepository
13}
14
15func NewLruTokenRepository(tokens CachableTokenRepository) *LruTokenRepository {
16 cache := expirable.NewLRU[string, InMemoryTokenSchema](DefaultCacheSize, nil, DefaultCacheTTL*time.Second)
17
18 return &LruTokenRepository{
19 cache: cache,
20 tokens: tokens,
21 }
22}
我們先不考慮調整快取設定的問題,先假設最大會保存 1000
筆資料,以及最長 60
秒這樣的設計,建立一個 expirable.LRU
的結構來保存資訊,因為結構跟 InMemoryRepository
的樣式相同,我們可以直接借用。
1func (r *LruTokenRepository) Find(ctx context.Context, tokenStr string) (*tokens.Token, error) {
2 token, ok := r.cache.Get(tokenStr)
3 if ok {
4 log.Printf("Cache hit: %s", tokenStr)
5
6 entity := tokens.New(
7 token.Id,
8 tokens.WithVersion(token.Version),
9 tokens.WithData(token.Data),
10 )
11
12 return entity, nil
13 }
14
15 log.Printf("Cache miss: %s", tokenStr)
16
17 entity, err := r.tokens.Find(ctx, tokenStr)
18 if err != nil {
19 return nil, err
20 }
21
22 r.cache.Add(tokenStr, InMemoryTokenSchema{
23 Id: entity.Id(),
24 Data: entity.Data(),
25 Version: entity.Version(),
26 })
27
28 return entity, nil
29}
30
31func (r *LruTokenRepository) Save(ctx context.Context, token *tokens.Token) error {
32 return r.tokens.Save(ctx, token)
33}
接下來 Save()
方法我們並沒有想要有額外處理,直接呼叫原本的方法即可。然而在 Find()
的處理我們會希望使用快取。
所以在拿到一個 tokenStr
時,我們就會直接去詢問是否有快取在記憶體中,如果存在的話則利用記憶體中的資料建立一個新的 Token 實體並且回傳。
假設找不到這筆資料,則先嘗試用原本的查詢方式處理,一但找到資料後先把資料保存到快取中,再回傳給使用者。
如此一來就完成最簡單的快取機制,假設要處理寫入後的資料不同步問題,則可以繼續在 Save()
上做額外的處理,基本上跟常見的快取設計方式沒有太大的差異。
考慮到 Entity(實體)的定義,我們會選擇每次都建立新的 Token 實體,而不是在快取中保存一個 Token 實體指標,來避免不同請求呼叫時,資料被修改的狀況。
分層結構
我認為 Clean Architecture 有時候跟 Layered Architecture 並不完全衝突,至少在區分物件類型上是蠻類似的。
我們從 Clean Architecture 上可以觀察到,透過區分出基礎的 Driver、Adapter、Use Case、Entity 四個大層級,就能讓我們在系統維護上獲得許多好處。
最底層的 Driver、Framework 提供的是各種支持,我們可以在不需要耗費太多力氣的狀況下搭建 Web 伺服器、串連資料庫等等。
接下來是將這些底層資訊轉換成商業邏輯的部分,因此 Adapter 是非常貼切的,以 Controller 來說我們會有 HTTP、gRPC 等不同協定的 Adapter 幫我們轉換成可以被 Use Case 理解的資訊(結構)
而 Use Case 負責整個流程的進行,該從哪些地方獲取資料、統整資訊都在這個階段發生,至於最後的 Entity 則是負則維護資訊(狀態)本身。
大多數情況下四個層級已經非常足夠使用,在 Clean Architecture 中並沒有明確的限定只能有四個層級,像是這次我們設計的 LruTokenRepository
也許我們可以獨立出一個 Proxy 層級,用 proxy.TokenCacheProxy
的方式來設計,同時也允許 Controller 等等去使用,也可能是一種架構的選擇。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in Go
- 目標設定 - Clean Architecture in Go
- wire 的依賴注入 - Clean Architecture in Go
- 案例說明 - Clean Architecture in Go
- 操作介面設計 - Clean Architecture in Go
- Place Order 實作 Controller 部分 - Clean Architecture in Go
- Place Order 實作 Entity 部分 - Clean Architecture in Go
- Place Order 實作 Repository 部分 - Clean Architecture in Go
- Lookup Order 功能 - Clean Architecture in Go
- Tokenization 機制設計 - Clean Architecture in Go
- 在 Place Order 實作 Token 機制 - Clean Architecture in Go
- 在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
- Token 內容加密 - Clean Architecture in Go
- gRPC Server 準備 - Clean Architecture in Go
- gRPC Server 實作 - Clean Architecture in Go
- 輸入檢查 - Clean Architecture in Go
- 資料庫抽換 - BoltDB - Clean Architecture in Go
- 資料庫抽換 - SQLite(一) - Clean Architecture in Go
- 資料庫抽換 - SQLite(二) - Clean Architecture in Go
- 實作 LRU Cache - Clean Architecture in Go