閒談軟體設計:Cache, Repository style

更新於 2024/02/03閱讀時間約 12 分鐘

這一篇是重新整理閒談軟體設計:Repository時,說要獨立分出來的文章,但一直拖稿到現在,主要是最近都在玩 Final Fantasy 7 重製版,不太有時間寫文章,破關後,該開始來還債了 XD


抽象的威力

我想不少人在上作業系統課程時,或多或少都會看過類似的金字塔,這邊沒有要幫大家上課,只簡單說一下,愈接近金字塔上方,速度愈快,速度的差異通常都是一個數量級 (10x) 以上的差異,例如,Apple M1 Ultra 剛出來時,記憶體號稱 400 GB/s 的速度,但 M.2 PCI 4.0 x4 SSD 最多 7800 MB/s 的速度,這個速度可是 50x 的差距。但速度愈快價格愈貴,因此數量也比較少,我們不可能把所有的資料都塞進記憶體。

raw-image


為了減輕程式開發人員的負擔,MMU 則會管理資料 Memory 與 CPU Cache 的同步,作業系統會管理資料在 Disk 與 Memory 之間的同步,並提供一個抽象層給程式開發人員,除了非常少數需要確切知道資料的位置,一般程式開發人員並不需要管理資料在不同階層的同步,甚至也不需要知道資料在哪一層,反正程式就是會動,這就是抽象的威力 (參閱 閒談軟體設計:語意的抽象化)。

可能是我孤陋寡聞,這樣的抽象卻不常見於這幾年的後端開發中,Redis (或其他分散式記憶體快取技術) 充斥於程式的各個角落,好像 application/business domain 就應該要了解 Redis 該如何使用,快取的 expire time 應該要設成多少,甚至要知道如何更新快取,但真的是應該這樣嗎?

raw-image


閒談軟體設計:Repository中有提到,Repository 的目的就是提供一個抽象,讓您的 application/business domain 在讀取 domain objects 時,就像使用語言原生的 array 或 dictionary 一樣,因此 Repository 本身就是一個非常合適的抽象可以繼續沿用下去,但要怎麼加入 Redis 或 Local cache 等實作呢?

Decorator Pattern

開始之前,先仔細想一下我們要什麼?

想要不改變介面 (Repository 的抽象),但又想擴充功能 (支援快取)。

一般來說,很多人可能會想到繼承,但繼承在這個案例中其實很不好用,一個系統中通常會有多個 Repository,像 AccountRepositoryProductRepository 等,若每個都要繼承,是非常不方便的。有其他辦法嗎?有的,用 Decorator pattern 來幫忙。下面這張圖用一個很簡單的概念解釋 Decorator pattern:就是俄羅斯娃娃,一個套一個,每套一個就增加一點功能,但外表還是娃娃,而且可以任意套。


有什麼現成的例子可以參考嗎?有,Java 的 InputStream 就是一個不錯的設計 (當然也有人覺得很囉唆)。 Java 中,InputStream 是所有輸入串流的介面,提供 readclose 等函式,以多種的類別,為不同的裝置提供實作,例如 FileInputStream,提供從檔案 read 內容。

下面的範例中,建立一個 FileInputStream 讀取 large-image.png.gz,由於檔案很大,不太可能全讀進記憶體,於是用一個 BufferedInputStream 來增加緩衝的功能。這個 BufferedInputStream 就是一個 decorator,因為檔案是一個 GZip 檔案,於是又套了一個 GzipInputStream,增加解開 GZip 的功能,但即便套了這麼多層,對 ImageIO.read 來說,傳入的仍然是一個 InputStream 的物件,完全不需要知道內容是怎麼來的,只需要透過 read 函式讀取內容並轉換成 Image 即可,這就是一個精巧的 Decorator pattern 的設計。

建立抽象

所以,就依樣畫葫蘆吧!剛剛已經提過 Repository 是個合適的抽象,所以這裡就建立一個 GenericRepository 的介面,這個版本比閒談軟體設計:Repository 的範例​還要更精簡,原因待會再說。


此時,我們就可以提供一個 RedisCachedRepository 類別,有幾個重點:首先,一定要實作 GenericRepository 介面,我們希望可以像 InputStream 那樣,結果必須先是個 Repository,然後再看可以再套什麼別的功能;再來,建構子要能傳入一個 GenericRepository 的實作,作為資料來源。對了,下面的範例程式僅供說明概念,應該是無法成功編譯的。


事實上,這是簡化過的版本,這裡快取的同步採用最簡單的版本,只要有變更,一律清除 Redis 中的快取,等到下次要讀取時再去真正的 source 取得最新的版本,然後更新快取。主要是快取的同步演算法有超多種,但不是本篇的重點,有興趣的可以自行搜尋 write-through 和 write-back,可以找到很多文章說明,或是回去翻作業系統的課本。重點是,您可以提供多種不同版本的實作,例如 RedisWriteThroughRepository 或是 RedisWriteBackRepository,視需求搭配使用。


既然都已經有 RedisCachedRepository 了,有沒有機會再建立 local cache 呢?當然可以,但就像 CPU 從單核心到多核心會遇到的問題一樣,local cache 有效率地同步是另一個麻煩的事情,local cache 簡單說就是一個 local copy,若有兩個 instances 都有某個物件的 local copy,當一個 instance 修改了,怎麼通知另一個 instance?

Redis 有提供 keyspace notification,簡單來說,在特定設定下,Redis 的每個 key-value 有一個 PubSub 可以訂閱,例如:在 users:1234 放了一個 user 的資料,就可以訂閱 __keyspace@0__:users:1234,多數對 users:1234 的修改,Redis 都會發送通知,因此,可以提供一個類似的類別:


這邊,我偷懶一下,RedisLocalCachedRepository 不接受 GenericRepository 作為 source,而是指定 RedisCachedRepository 作為 source,這是因為若來源不是 Redis,那訂閱 keyspace notification 就不太有意義。這裡,在收到通知後,也採用最簡單的方式,直接清除 local cache。

總之,稍微修改一下,就可以像套俄羅斯娃娃那樣,替 RedisCachedRepository 再添加 local cache 的功能。當然,要讓 local cache 更加 generic 是可行的,把管理訂閱通知抽成一個類別,由外部傳入,就可以讓原先的實作不局限於 Redis 了,這就當成給大家的練習題了。


限制

用 Repository 建立抽象並搭配 Decorator pattern 完成 cache,看起來是個不錯的選擇,但還是有些限制。不過,有意思的是這邊所列的限制,即便不使用 Repository 也還是要小心注意。

額外介面

在建立抽象時有提到,Repository 的介面變得更精簡,除了方便說明外,有時因為特殊需求會繼承 GenericRepository,建立新的介面,例如下面 UserRepository 的例子:


這時,有點尷尬了,雖然 RedisCachedRepository<String, User> 可以 (應該吧) 接受 UserRepository 作為 source,但使用時,卻會少了 getByEmail 或是 activeUsers 等函式。這裡有幾種做法:

  • 建立一個 RedisCachedUserRepository 實作 UserRepository,建構子接受兩個 source,有 cached 的和沒有 cached 的,只要是 cached 的 source 能支援的函式,就 delegate 給 cahced 的 source,cached 的 source 不支援的就 delegate 給另一個 source,這時,會覺得 GenericRepository 精簡比較好。


  • 視 use case 用不同的 repository 實體,這也是 decorator 的彈性,並沒有說非得用全部套上去的版本,而且通常額外加上的介面,通常只會在特定情境下使用,且快取也幫不上忙,此時也不用堅持要用有快取的 repository。尷尬的是,例如,在 use case 中會同時用到 getactiveUsers,選擇沒有 cache 的 repository 在效率上就會比較差一點。

多筆查詢

由於快取中不會有全部的資料,因此,即便 Redis 等技術都有提供類似 wildcard 的方式查詢資料,卻無法保證結果是完整的,因此,像是 count 或是 contains 等函式,若要實作,其實也只能委任 (delegate) 給 source。

多筆更新

和多筆查詢相同,在多筆或是所謂的批次更新時,除了只能委任給 source 外,還有另一個麻煩的地方,那就是更新快取。由於快取都是 key-value 的集合,但在批次更新資料庫時,不見得會回傳所有有被更動到的 ID,因此也導致無法回頭更新快取。

若很在意資料的準確性,那就是清空該資源的所有快取,這聽起來也許不合理,但實際上對效能的影響不一定很大。拉長時間來看,只要整個系統運行的時間跨度裡,cache 的 hit rate 足夠高,系統就能受惠於快取。

甚至,快取的資料準確性是能有某種程度的容忍值,例如數秒、數分鐘的容忍值,那可以透過 expire time 讓資料自動從快取中移除,等待下次讀取時更新,此時,是可以不用清除該資源的所有快取。

多處快取

多處快取和剛剛提到的 local cache 很像,都會遇到難以同步的問題,理論上,就是希望透過 Repository 建立抽象後,避免到處都有建立快取的現象,但有時候為了加速 API 的 response time,可能會把查詢結果給快取起來,而且快取的內容可能還是多筆。此時 Repository 會無法知道該怎麼更新這些快取,並別說 key 的命名規則不同時,就更難處理了。不過,多處快取的問題在沒有 Repository 的抽象中應該會更嚴重。

直接存取

最後,即便用了 Repository 抽象,仍無法處理工程師直接修改資料庫,有些資料庫有提供通知的功能,例如 Firebase Realtime Database,但仍然需要寫程式去更新快取。不過,對於一個營運中的系統,直接修改資料庫應該是大忌啊!


小結

總於把拖很久的稿寫完了,我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。


延伸閱讀


avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
提到後端工程師,似乎就只是開發 API,但一個複雜的系統其實不太可能只透過 API 就能完成,例如一個簡單的功能,註冊會員,其實是由好幾個不同類型的工作互相配合,您才能收到開通信,才確保資料庫不會有一堆未開通帳號等。所以今天就來聊聊一個系統有幾種不同執行方式的工作。
最近隨著 FP 的流行,immutability 一直被提倡,物件有狀態,會被修改好像是一種惡,但真是如此?immutability 很好,但所謂的狀態就是會隨著操作變動,差別只在於變動發生在哪裡?
針對這議題,從 devOps 的角度看,團隊應抱有持續不斷地改進的精神,努力降低上版的風險,最後,哪一天上版就僅僅是風險控管的問題了。風險控管除了考量到損失,當然還要考慮到團隊要怎麼 on-call,on-call 的資源夠不夠應付上版後的突發狀況,才能做出適當的決策。
這文章來自網友在 在 Medium 上的留言 (有人幫忙想題目也挺不錯的),問到:Singleton 對於好的架構來說是否能避免就避免呢?我簡單地回了一下我的想法 ,但 Singleton 其實很有趣,所以就寫篇文章來聊聊吧!
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
提到後端工程師,似乎就只是開發 API,但一個複雜的系統其實不太可能只透過 API 就能完成,例如一個簡單的功能,註冊會員,其實是由好幾個不同類型的工作互相配合,您才能收到開通信,才確保資料庫不會有一堆未開通帳號等。所以今天就來聊聊一個系統有幾種不同執行方式的工作。
最近隨著 FP 的流行,immutability 一直被提倡,物件有狀態,會被修改好像是一種惡,但真是如此?immutability 很好,但所謂的狀態就是會隨著操作變動,差別只在於變動發生在哪裡?
針對這議題,從 devOps 的角度看,團隊應抱有持續不斷地改進的精神,努力降低上版的風險,最後,哪一天上版就僅僅是風險控管的問題了。風險控管除了考量到損失,當然還要考慮到團隊要怎麼 on-call,on-call 的資源夠不夠應付上版後的突發狀況,才能做出適當的決策。
這文章來自網友在 在 Medium 上的留言 (有人幫忙想題目也挺不錯的),問到:Singleton 對於好的架構來說是否能避免就避免呢?我簡單地回了一下我的想法 ,但 Singleton 其實很有趣,所以就寫篇文章來聊聊吧!
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
看到有人因為這次的事情在說這是因為沒有校園沒有髮禁、禁止體罰、延後到校時間導致學生逐漸沒有紀律,覺得自己什麼都能做......我第一個想到的是我爸說他高中時畢業典禮教官只要走得慢一點一定會被憤怒的學生蓋布袋拖去打。發生這樣的事情當然得檢討,但希望不是以一種意氣用事、情緒主導的心態......
Thumbnail
「一群人所決定的,就是對的嗎?」 這是民主的盲點也是缺點
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
看到有人因為這次的事情在說這是因為沒有校園沒有髮禁、禁止體罰、延後到校時間導致學生逐漸沒有紀律,覺得自己什麼都能做......我第一個想到的是我爸說他高中時畢業典禮教官只要走得慢一點一定會被憤怒的學生蓋布袋拖去打。發生這樣的事情當然得檢討,但希望不是以一種意氣用事、情緒主導的心態......
Thumbnail
「一群人所決定的,就是對的嗎?」 這是民主的盲點也是缺點
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。