【Flask/React】此博客服务端的缓存实现

少女dtysky

世界Skill

时刻2016.10.13

承接上篇【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,其中updateContentmodifyStatedelete是维护它们的核心方法,其他方法则负责在请求到来时判定状态和取值,由此,整个设计便完成如下:

后端cache

文本解释如下:

  1. 当前端请求到来时,如果cache中没有请求对应的内容,则从数据库查出内容并调用updateContent放入缓存,此时_cache将被更新,同时_state中的对应的值将被设为False,表明未被修改,并直接返回一个code为200的响应。
  2. 当前端请求到来时,如果cache中拥有对应的内容,并且_state中对应状态为False(表明未被修改),则直接返回一个code为304的响应。
  3. 当前端请求到来时,如果cache中拥有对应的内容,并且_state中对应状态为True(表明已被修改),则重复1。
  4. 当文件监控器发现文件被更改、并进行到写入数据库这一步时,直接根据当前写入的条目判断该条目是否在cache中,如果在,则调用modifyState方法将_state中对应的值设为True

如此,后端的缓存以及和前端的协议便已完成。

前端

前端服务器的Cache结构和后端基本一致,但也有一些不同,最明显在于前端的请求会有分页这个参数而后端没有,并且Redux中维护的状态和最终渲染的页面也并非对应,比如:

文章列表、也就是/category/Skill/0这种页面中,其对应Redux的store中的state是一个维护着listpayload的变量,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基本框架,便可以结合客户端请求和后端响应来进行最后的设计:

前端Cache

  1. 当一次客户端请求来到时,先解析出此次请求对应的后端接口url,并向后端发出请求。
  2. 得到后端响应,判断状态码code,如果是200,直接重新走一遍完整的服务端渲染流程,如果是304,则先去查cachePage中有没有对应的数据,如果有,则直接返回其中的内容。
  3. 如果code是304但cachePage中没有对应的内容,假如cacheStore中也没有对应的store,则走一遍完整的服务端渲染流程,如果有,则直接取出store执行相应的store.dispatch修改payload,接着直接进入二次渲染。
  4. 在二次渲染结束后,更新两个Cache。

改进

Cache带来的性能提升以内存消耗为代价,典型的空间换时间,所以能减少内存消耗的事情该做的还是要做。这一点也很简单,考虑到cachePage中存的是最终返回给客户端的页面,而这种页面终归是要被压缩的,所以在放入缓存之前就可以直接将其压缩,这样最后从里面取到的也是压缩后的数据,不但节省了空间还省去了每次的压缩带来的损耗:

cachePage = cachePage.set(
    frontUrl,
    zlib.gzipSync(page)
);

如果不是自己的创作,少女是会标识出来的,所以要告诉别人是少女写的哦。