Contents
  1. 1. 为什么
  2. 2. 放什么
  3. 3. 怎么放
    1. 3.1. 缓存系统的特性
    2. 3.2. 缓存更新的频次
    3. 3.3. 缓存的结构设计
  4. 4. 失效和更新
  5. 5. 相关阅读

以前在做企业系统的时候,并没有用什么缓存策略。一来是内部系统,并发不高(通俗理解就是同时找系统要数据的人不多)。而且,企业系统的订单,发票等数据,要求数据实时性高,改动也频繁,所以缓存的意义并不太大。

在上一家公司的时候,曾经用过高大上的 Oracle Coherence,不过一堆坑。这头改了数据,那头打开的时候数据是旧的,然后保存的时候还没错,导致各种奇奇怪怪的 bug。

现在,我参与开发的是一个语音直播平台,使用缓存的场景稍微多一些。所以,在这里结合自己的实践,写一篇入门和自己的半总结给大家。

为什么

在没有缓存这个概念之前,获取数据都是直接从能永久保存数据的数据库里面读取的。而从数据库读取数据通常意味着要访问硬盘。在数据的访问量不太大的情况下,这种方式也没什么大问题。但是,如果几万人,甚至几十万人同时访问,比如打开某一个直播的详情页,全部请求都要访问数据库,读取硬盘的话,系统肯定受不了。

缓存的引入,是为了把数据放在访问速度更快的地方,提高用户打开网页,App 的速度,减少服务器和数据库的压力。牢记缓存的作用,才能在我们需要决定把什么东西放缓存的时候,有更明确的指导方针。不是为了缓存而缓存。

放什么

知道了为什么要使用缓存,那应该不难理解,访问次数非常频繁的数据,都应该考虑使用缓存。所以,放什么就取决于数据的读写比例,这是由业务特点决定的

还是拿直播详细资料来举例,主讲人信息,直播标题,时间和内容都是读取的次数远远大于修改的次数。这些信息在直播审核通过后,就基本不怎么更改。所以,它们是非常适合放在缓存里面的。

怎么放

把数据放在缓存,不是一股脑地把数据库的数据,直接往缓存里面搬就可以的了。缓存怎么放,取决于业务特点和缓存系统的特性,因为它们是做出合适的数据结构设计和选择的关键

缓存策略有很多种,可以应用在手机客户端,浏览器,服务器端的内存型数据库等。不同的策略,要考虑的着重点都不同。本文只重点拿服务端的场景来举例。

缓存系统的特性

Redis 是一个常用的服务端缓存系统。它其实是内存型 key-value 数据库。key-value 的意思就是,存数据的时候,只需要指定一个唯一的标识(key),和对应的数据(value)绑定。读取的时候只提供标识就可以了。那么,对于用户访问直播详情页的场景,我们可以用 JSON 格式表示所有的直播资料,把它转换成字符串后,和直播的唯一标识(ID)绑定,存放到 Redis。这种方式只访问一次 Redis,提供一个 key 就能拿到所有的数据,时间复杂度是 O(1)。

除了字符串类型的数据以外,Redis 还支持 Hash, Set, List 等结构。直播的信息其实还可以用 Hash 来存放直播数据,但是如果要一次取出所有的直播资料,时间复杂度就会是 O(N),N 取决于直播包含了多少属性存放在 Hash 里面。N 越大就越慢。所以,采用第一种方式更好一些。

缓存更新的频次

如果你们细心留意的话,会发现参与人数这个数据,是和主讲人,直播标题,时间和内容不一样的。它是经常改变的。尤其是大热门的直播,刚刚推广后的几小时内,参与人数的变化是非常频繁和巨大的。那这种数据能不能放缓存?如果可以,应该怎么放呢?

首先,这种瞬时改变量可能会很大的数据,如果要实时更新,无论是数据库还是缓存,都会造成非常严重的数据写锁。但是,仔细想想的话,直播参与人数,并不需要非常及时的更新,没必要说多了一个人购买,数值马上加一。

在数据实时性要求不是非常高,更新频次和量大的场景下,可以采取定期合并,批量更新的策略。也就是说,直播参与人数并不需要在每个用户购买的时候马上更新,而是由一个后台定时任务,统一通过数据库的购买记录,批量统计更新。这样的话,数据更新频次的量级,从可能是万的级别,直接下降到 1 而已。我们只需要为后台的定时任务设定一个合理的时间间隔就可以了。

缓存的结构设计

通过把直播详情的 JSON 数据转换成字符串来缓存,这种方式还算比较直观,容易理解。那像 iPhone App Store 这样的页面,按不同主题来显示直播资料的页面,是否应该做缓存,能否做缓存,应该怎么做缓存呢?

直播网站的首页,作为整个系统的入口,也是用户寻找信息的必经之道,访问量肯定是非常巨大的。如果在手机展示这个页面,一般首次展现(首屏)的信息就包含 3 个主题,每个主题包含最少 10 个直播,直播的个数相当多。

假如没有缓存,或者缓存还是直播的粒度,那么为了拿出首屏的数据,我就需要访问 3 次数据库或者 Redis,拿到所有主题,然后再访问 30 次 Redis,拿到所有的直播。假设还要逐个统计每个直播的回答问题个数,那每个用户访问首页对系统的冲击就很大了。

那我怎么处理这个首页的缓存结构设计呢?由于刚才说首屏大概有 3 个主题,每个主题下还有不少直播。所以,我在 Redis 缓存的就是分页了的,首页所有的主题和下面的直播信息。也就是说,客户端说要前三个主题,只通知服务器拿第一页数据就可以了。拿后面的数据就页数依次加一,直到没有数据返回,就表示没有更多主题了。通过这种方式,每个用户,每次访问首页只要访问 1 次 Redis 就可以了。

当然,为了提高访问速度,还需要减少每次传输的数据量。所以,首页缓存的直播信息必须精简,不能像直播详情的缓存那样,包含参与者头像等信息,因为它们量大,而且没必要。

失效和更新

有了缓存以后,用户获取数据就不直接从数据库读取了。那么,当数据需要更新的时候,我们需要同时更新数据库和缓存。缓存失效和更新的策略,就是为了解决这个问题。

这是一个非常难的问题,即便是有多年经验的程序员,也未必能很好处理。为什么说难啊?Martin Fowler 大叔在他的博客里面提到了一些有趣的计算机领域的难题,其中包括 Phil Karlton 说的:

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

所以,我这篇入门和半总结文就不细说了,有兴趣的同学可以参考「左耳朵耗子」陈皓的文章缓存更新的套路。在我们的直播系统,我们采用的算是第一种方式,只是实现的手段,由于我们是多地部署的系统,在失效和更新的操作上,做法有点特殊而已。

在我动手写本文的时候,恰好曹政 4 月 28 日也在他的公众号「caoz的梦呓」也推了一篇「谈谈编程 之 滥用内存的现象」。文章里讲述了他给小蜜圈做技术顾问时发现的一些缓存处理问题,里面的一些例子和原则其实和我这里说的有些重合,大家不妨一并查看。

相关阅读

数据的一致性只和数据库有关系吗?
一个简单的支付业务与模型演变
听听系统的多地部署改造

Contents
  1. 1. 为什么
  2. 2. 放什么
  3. 3. 怎么放
    1. 3.1. 缓存系统的特性
    2. 3.2. 缓存更新的频次
    3. 3.3. 缓存的结构设计
  4. 4. 失效和更新
  5. 5. 相关阅读