【Flask/React】此博客服务端的缓存实现
世界Skill
承接上篇【React/Redux】深入理解React服务端渲染,由于React服务端渲染带来的开销,在内存尚可的状况下使用缓存是个不错的选择,而对于这个项目,由于前后端是分离的,所以需要在Node这边和Flask那边来协作完成最完备的缓存,本片文章就将记录一下我的思路。
前端架构在前面两篇文章均已说清,而后端架构则和这篇差不多(【Flask/React/MongoDB】BlogReworkIII-如何搭建一个动态Blog),没怎么改过。
问题
由于前端服务器实现了服务端渲染,考虑这个博客的文章修改不会很频繁,其修改的频率远远小于访问频率,所以用Cache
来减少这个渲染的开销是合适的。为了设计这个Cache,需要明确现在前后端的设计下一次客户端请求的完整响应流程:
客户端发起请求,服务端开始渲染,进入相应组件的生命周期并向后端发起ajax请求,后端响应后进行下一步操作。由于前后端是分离的,Blog内容管理的核心在后端,所以核心就在于判定后端返回什么状态时从Cache中拿东西,在什么状态时修改Cache,这就要求后端提供给前端一个约定的协议。
后端
在本Blog项目中,Cache这个方面,后端需要提供给前端的实际上是一个状态,这个状态表明前端向后端请求的数据是否发生了改动。一个显而易见的方法是由后端先缓存所哟已经被请求过的数据,在下次请求时再拿从数据库中得到的新数据来对比,根据比对的结果返回一个状态码(比如304),这样后端响应前端的结果可以表述为:
{
content: {......},
code: 200(304)
}
注意这个200和304虽然和HTTP标准中的状态码值一致,但却是两个东西,在两种状态下它们返回的状态都是200,只不过数据中有个code
字段来标示状态。
这种做法很直观,但每次响应除了原先的数据库查询操作外,还要多一步对象对比的过程,这显然是不合适的,而且在这种设计下后端的这个Cache也失去了本来的意义,必须有其他的方案来解决这个问题。这里可以回顾一下后端的实现:
可以发现,数据库只会在程序监听pages
文件夹内文件的改动后才会发生,并且这个改动事件是可以被程序捕获到的,这就提供了另一个解决方案——同时维护一个cache
和与其对应的state
,cache中存储响应内容,state中存储改内容对应的修改状态,而这个state中的内容除了初始化之外,只能伴随文章改动的事件而修改。
在这个思想的基础上,一个抽象的Cache和其成员便可以被构造出来:
class WebCache():
def __init__(self):
self._cache = {}
self._state = {}
@property
def flag(self):
......
def updateContent(self, parameters, content):
......
def modifyState(self, parameters):
.....
def get(self, parameters):
.....
def delete(self, parameters):
.....
def has(self, parameters):
......
def is_modified(self, parameters):
......
这是一个Cache的基类,它的作用和后端架构中其他基类基本一致(详见后端设计的那篇文章),它提供了一系列方法来维护内部的_cache
和_state
,其中updateContent
、modifyState
、delete
是维护它们的核心方法,其他方法则负责在请求到来时判定状态和取值,由此,整个设计便完成如下:
文本解释如下:
- 当前端请求到来时,如果cache中没有请求对应的内容,则从数据库查出内容并调用
updateContent
放入缓存,此时_cache
将被更新,同时_state
中的对应的值将被设为False
,表明未被修改,并直接返回一个code
为200的响应。 - 当前端请求到来时,如果cache中拥有对应的内容,并且
_state
中对应状态为False
(表明未被修改),则直接返回一个code
为304的响应。 - 当前端请求到来时,如果cache中拥有对应的内容,并且
_state
中对应状态为True
(表明已被修改),则重复1。 - 当文件监控器发现文件被更改、并进行到写入数据库这一步时,直接根据当前写入的条目判断该条目是否在cache中,如果在,则调用
modifyState
方法将_state
中对应的值设为True
。
如此,后端的缓存以及和前端的协议便已完成。
前端
前端服务器的Cache结构和后端基本一致,但也有一些不同,最明显在于前端的请求会有分页这个参数而后端没有,并且Redux中维护的状态和最终渲染的页面也并非对应,比如:
在文章列表、也就是/category/Skill/0
这种页面中,其对应Redux的store中的state是一个维护着list
和payload
的变量,list
存储着所有的文章,payload
负责分页,由于Blog自身的性质,我设计的是在一次用于访问中只会获取一次list
,分页通过设置payload
中的page
加之组件渲染逻辑来实现(本质上就是数组切片)。
在这种情况下,如果两次请求的路由中分类都是一致的,只是分页不同,那么将其作为两种请求来多做一次服务端渲染显然是不合适的,因为我们完全可以利用第一次请求的list
、加之对payload
的修改来直接进行二次渲染,而不必再走一次状态的初始化,这也就要求将Redux中的store和最终页面的内容分别进行缓存:
let cacheStore = Immutable.fromJS({});
let cachePage = Immutable.fromJS({});
cacheStore
用于缓存不同请求下的store,以后端接口url为key;cachePage
则用于缓存最终的结果,以客户端请求url为key。有了Cache基本框架,便可以结合客户端请求和后端响应来进行最后的设计:
- 当一次客户端请求来到时,先解析出此次请求对应的后端接口url,并向后端发出请求。
- 得到后端响应,判断状态码
code
,如果是200,直接重新走一遍完整的服务端渲染流程,如果是304,则先去查cachePage
中有没有对应的数据,如果有,则直接返回其中的内容。 - 如果
code
是304但cachePage
中没有对应的内容,假如cacheStore
中也没有对应的store
,则走一遍完整的服务端渲染流程,如果有,则直接取出store
执行相应的store.dispatch
修改payload
,接着直接进入二次渲染。 - 在二次渲染结束后,更新两个Cache。
改进
Cache带来的性能提升以内存消耗为代价,典型的空间换时间,所以能减少内存消耗的事情该做的还是要做。这一点也很简单,考虑到cachePage
中存的是最终返回给客户端的页面,而这种页面终归是要被压缩的,所以在放入缓存之前就可以直接将其压缩,这样最后从里面取到的也是压缩后的数据,不但节省了空间还省去了每次的压缩带来的损耗:
cachePage = cachePage.set(
frontUrl,
zlib.gzipSync(page)
);