數據系統設計是關於數據存儲、共享、更新(以及傳播更新)、緩存(以及緩存失效)的技術。大部分軟件系統均可以從數據系統的角度去理解。html
數據系統是如此的廣泛,以致於開發者實際上天天都在設計數據系統,卻經常沒有意識到它們的普適性,將多個本質相同的問題看成了孤立的問題來理解。應用狀態管理、配置管理、用戶數據管理問題,本質上都屬於數據系統的問題。前端
本篇文章站在前端的視角上,經過對數據系統的討論,但願幫助開發者在開發的過程當中有意識地識別、設計數據系統:git
本文的大部分例子是前端應用,可是數據系統的規則適用於任何軟件系統。github
若是你但願從服務端、分佈式系統的視角來理解數據系統,我瞭解到 ddia是一本很優秀的書籍,提供了更完整、專業的討論。
任何數據系統都須要遵循一個原則:single source of truth,即單一數據本源。每一個數據應該只有一個【數據本源】,其餘的數據獲取方式都只是緩存。web
若是你是一名前端開發者,那麼你在學習前端狀態管理(好比redux)的時候,應該已經據說過這個原則,可是你可能會忽略這個原則的普適性:這個原則並不只僅適用於前端應用的狀態管理,它適用於任何軟件系統。狀態管理問題並非特定於前端領域的問題,而是任何軟件系統設計的廣泛問題。數據庫
數據系統的設計,很大程度上是【層級緩存系統】的設計。redux
從計算機底層的視角來看,緩存層級是這樣的:瀏覽器
完整版耗時表。經過這些時間,能夠大體估算出一個數據系統的性能。
緩存層級的特色:緩存
站在實際軟件系統的視角,道理也是同樣的,只不過應用在了更加宏觀的層面:服務器
任何涉及到緩存的地方,就免不了緩存落後的問題。當最底層的數據本源發生更新的時候,下游的數據緩存應該及時失效,而且針對舊數據的操做不該該直接應用於新數據上。一份數據源,可能被外部應用更新。若是緩存沒法在第一時間知道【數據本源】的更新,那麼它就會落後於實際數據,產生不一致。
不一樣的數據系統對於緩存不一致的容忍程度不一樣,緩存失效的策略也不一樣。
好比DNS系統,只須要保證用戶最終可以讀取到最新的IP地址(最終一致性)。修改DNS記錄後不會在全球全部DNS服務節點生效,須要等待DNS服務器緩存過時後向源服務器請求新記錄才能實現更新。
從web前端應用的視角來講,不少前端應用狀態能夠視爲服務端數據源的緩存。通常來講前端應用可以在」本身主動提交更新的時候「更新前端狀態。可是若是是一些外部事件形成服務端數據源的改變,大部分前端應用沒法馬上知曉更新。大部分前端應用選擇容忍這種緩存落後,僅在組件掛載時請求數據、更新狀態,由於跨客戶端/服務器作緩存失效的代價太大了。
緩存落後形成的典型問題有:」前端請求刪除某資源時,服務端發現資源已經不存在,所以請求失敗「。
【數據本源】、緩存都須要考慮做用域與生命週期。
做用域就是對數據共享範圍的考量;生命週期是對建立、銷燬時機的考量。二者每每有很大的相關性。
常見的【數據】做用域劃分方式:
應用局部級別:應用局部管理本身的【數據】,一個頁面中可能包含多個獨立的【數據】。好比:
這裏的【數據】能夠指代【數據本源】,也能夠指代【緩存】。
常見的生命週期劃分方式:
const sharedCache = new Map(); export const Component = class Component { // ... getData(key) { return sharedCache.get(key); } }
在識別、設計數據系統的時候,對於每個邏輯上的數據定義,應該先有一個明確的【數據本源】,而後衍生出多級緩存。下面列舉一些常見的數據系統類型。
常見的持久化存儲是文件系統、數據庫。
舉個例子,咱們能夠用數據庫來存儲用戶的帳號、姓名、郵箱等用戶數據,將它做爲【數據本源】。
這些地方可能包含用戶數據的【緩存副本】:
- 數據庫自己的緩存系統,由數據庫內部實現 - 服務端應用內通常會使用**請求級別**的緩存:每次請求讀取一次數據源,存到緩存(即變量)中,用來作計算。緩存的做用域和生命週期都是本次請求 - 客戶端應用向服務端請求某個用戶的數據之後,將結果保存在客戶端應用狀態中。**客戶端中的不少應用狀態,本質上都是服務端數據源的緩存**。當數據須要更新時,必須提交給服務端的【數據本源】。
持久化存儲在軟件系統在軟件關閉時也可以保持數據,通常只能由應用主動刪除。
有一類數據,是能夠基於其餘數據來計算出來的,它的本源並不存在於硬盤或內存中。這種數據又稱爲衍生數據。對於這種衍生數據來講,若是在每次須要使用的時候都計算一次,一來可能形成性能問題,二來可能致使先後不一致,所以每每須要將計算結果緩存起來,而且要明肯定義緩存的生命週期(好比軟件重啓、頁面刷新時從新計算)。
舉個例子,用戶年齡是一種數據,可是並無哪一個會數據庫會存儲「用戶如今多少歲」這個數據,它的【數據本源】是一個計算公式:當前時間-出生時間
。前端應用通常在須要展現年齡的時候就計算一次,存到應用狀態(本質上是內存中的緩存),而後在當前頁面一直使用這個結果。
這個緩存的生命週期與頁面生命週期一致,頁面關閉時緩存也隨之銷燬。做一個極端的假設,這個頁面打開使用超過了一年,那麼就會出現緩存過期的問題(歲數應該增加了一歲),所以須要引入緩存失效的手段。最原始的緩存失效手段是,重啓應用(即刷新頁面),下次啓動的時候從新計算最新的年齡。
這個緩存的做用域僅限於這個頁面,若是有多個標籤頁同時打開了這個前端應用,那麼每一個頁面都有一份本身的緩存,相互隔離,避免讀取到同一個數據的兩個緩存。
對於前端應用來講,瀏覽器url是一種前端應用狀態(只不過它由瀏覽器來管理,並提供操控API給前端應用代碼)。前端應用根據不一樣的url狀態來展現不一樣的功能,服務端不關心每一個客戶的url狀態,所以url是前端應用的一種【數據本源】。前端應用通常會訂閱url的更新,響應url的變化展現不一樣的頁面組件。
在這裏,前端url數據並無明顯的緩存的存在。理論上你能夠每次須要使用這個數據的時候都訪問數據本源
window.location.href
。有時候在路由框架中存在一份url緩存副本,只不過由於它訂閱了url的更新,因此通常不會出現緩存落後的問題。
好比,前端應用能夠識別這個模式的url來獲得region參數:www.my-app.com/${region}/items
。若是用戶訪問了url:www.my-app.com/cn-hangzhou/items
,那麼就至關於啓動應用,並把region數據初始化爲cn-hangzhou
。url就是region數據的【數據本源】。若是用戶在應用中經過操做按鈕切換了region,前端應用邏輯就使用瀏覽器API來更新url(數據本源),而後,前端應用感知到url的更新,進而更新本身的行爲。
這個例子也能夠看出,須要更新數據的時候,應該更新【數據本源】,而不該該直接更新緩存。
由前端管理的【數據本源】還包括:頁面的滾動狀態、輸入框的focus的狀態等UI狀態,無需提交給服務端。
因爲這種數據本源就在本進程中,訪問速度很快,所以通常不須要考慮緩存。主要須要考慮的是它的初始化方式和做用域。
常見的初始化方式:
做用域已在前面的段落討論。
前端React相關:
ddia相關章節: