从前端的视角理解数据和缓存

对数据系统的理解

数据系统设计是关于数据存储、共享、更新(以及传播更新)、缓存(以及缓存失效)的技术。大部分软件系统均可以从数据系统的角度去理解。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状态,无需提交给服务端。

因为这种数据本源就在本进程中,访问速度很快,所以通常不须要考虑缓存。主要须要考虑的是它的初始化方式做用域

常见的初始化方式

  • 能够直接初始化为一个默认值。
  • 能够读取应用启动参数来初始化数据本源。好比上面的例子,用户点击怎样的url来打开页面,决定了应用的初始region。对于命令行应用则能够读取命令行参数。
  • 能够在应用启动时读取外部状态来初始化数据本源。初始化之后就无需再考虑外部状态。当数据须要更新时,直接更新应用中的【数据本源】,这是它与”外部持久化存储做为数据本源“的根本区别

做用域已在前面的段落讨论。

相关阅读

前端React相关:

ddia相关章节:

相关文章
相关标签/搜索