缓存可以用来做些什么

There are only two hard things in Computer Science: cache invalidation and naming things.
—— Phil Karlton

缓存是高性能 Web 服务的基本要求,然而对缓存的处理和使用不当将带来一系列的副作用,尤其是同时考虑到服务的高可用性和同步的问题时。

本文将简单探讨 Web 服务中哪些功能适合用缓存(此文均指 Web 后端自行实现的缓存,如使用 Redis)来完成。

提高性能

提高服务的性能是缓存最原始的目的,对于数据库中查询频繁而改动不频繁的数据都可以使用缓存以提高服务的性能,这往往是低成本大幅度提高服务性能的手段之一,收效也会非常明显。

对于一个 Web 服务而言,缓存可以从不同的层级高度来展开:

  • 路由层级的:如果是前后端不分离的 Web 服务,可以直接把某个路由下的静态页面缓存(比如未登录情况下具有高访问量的主页,并且不经常被更改时);如果是前后端分离的 API Server,则是从 API 层级进行的缓存,此时需要注意缓存的 key 一定需要涵盖所有会控制 API 输出的参数。
  • 单个数据库查询层级的:我们可以把缓存细化到某个数据库查询操作后的数据返回,这样缓存的数据将更细化和可控,缓存的 key 也可以直接对应 SQL 语句中某些关键的参数。此时需要注意的是,被缓存的数据在其他路由或 API 下被更改时,需要即刻删除(对于一般具有实时性要求的服务,否则可以把过期时间设短一些),这样设置缓存操作和删除缓存操作往往分离在不同的路由或 API 中,需要良好的机制来进行一致性的管理和维护。
  • 数据库单条记录层级的:层级再向下,我们甚至可以从数据库中的单条记录为单位进行缓存。此时缓存数据库实际已经变成了总的持久性数据库的一个子集。这种情况下其实可以把缓存操作从应用层开发隐藏,直接在 ORM 中进行完整闭合的操作,也就是说,应用层无需再关心数据是从缓存来的还是从持久性数据库来的,ORM 在持久性数据库的基础上自行维护好的缓存,这将极大的降低应用层开发的缓存维护工作,同时也可以在一定程度上提高性能。不过相应的,应用层也失去了对缓存的控制力。

在一个应用中,可以混合不同层级高度的缓存,以达到服务性能和开发维护成本之间的平衡。

优雅实现指定间隔时间后的内容更新

一个典型的场景是新浪微博的热门话题:每隔十分钟更新一次,而且访问量极高,此时我们可以把该数据完全用缓存来存储,并且把过期时间设置为十分钟,这样每十分钟缓存失效后,自动从数据库获取(组装)新的热门话题数据并存入缓存,在下一个十分钟内,所有的对热门话题的访问都是直接从缓存中拉取数据的。这样利用过期时间的合理设置来实现完全服务业务需求的功能,连手动删除缓存都免了。

当然,这样的业务需求可遇不可求,遇到一次就要好好把握充分利用。

作为定时器

相比上一点的业务需求,定时器的需求要常见得多,比如每天定时进行数据同步、日志导出等等,都需要用到定时任务。从 Node 生态来看,实现定时任务的方式很多,最常见的莫过于使用社区提供的cron库。我不知道 cron 库是如何来实现定时任务的,这里仅探讨定时任务的缓存实现。

用缓存实现定时任务其实很简单,本质上跟在程序中设置一个 flag 的思想是一致的,当 flag 的状态翻转时,相当于触发了一个事件,此时响应这个事件来执行一些任务。不同的是,程序中的 flag 是没有过期时间的,它只能在一些特定情形下或者另一些事件后改变状态。而利用缓存的过期时间,我们就可以完成一些定时任务了。

具体来说,我们在缓存中设置一个 flag,并且把过期事件设置为我们需要的定时时间,然后视具体业务对定时的容错情况不断来访问缓存中的这个 flag(为了不阻塞主线程,可能需要多线程支持),当该 flag 不存在后,便执行所需的任务。这样就用缓存实现了定时任务。

node-redis-cache

在用 Express 开发 API Server 时,没有在 Github 和 NPM 上找到满意的缓存库,于是想自己来重复造轮子了。

node-redis-cache是我建立并想持续维护的一个缓存库,目前基于ioredis并且被设计成单个数据库查询层级,已经实现了一些常用的兼容原生 Redis 接口的 API,具体使用时只需要在对应的数据库查询外面包裹(wrap)一层即可。

欢迎加入并共同完善!