数据系统设计是关于数据存储、共享、更新(以及传播更新)、缓存(以及缓存失效)的技术。大部分软件系统均可以从数据系统的角度去理解。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相关章节: