缓存设计的简单介绍

之前从一位大佬的文章中看到:缓存是提高性能最好的方式。从我个人的项目经历和所做的相关性能优化工作来看,对此深表认同。

对于业务系统而言最重要的就是数据,一般来说这种类似ERP的业务系统基本上都是使用关系型数据库,大部分的数据都是从数据库中取,属于磁盘IO,而缓存系统则都是放在内存中操作。一个是内存读写,一个是磁盘IO,效率自然是天壤之别。

本文从一下几个方面来介绍缓存系统的原理

  • 缓存的使用场景
  • 缓存设计的几种方式
  • 缓存设计的几个热点

缓存的使用场景

一般来说,绝大部分的业务系统的性能瓶颈都在于后端数据库的读写。

数据库的四种基本操作(select, udpate, insert, delete), 除了select外,其余三种操作都会带上主键或索引,且操作的数据集是相对较小,因此性能不是问题。而Select包含中各种left join, group, order及函数运算,且查询的目标数据集也很大,导致出现慢查询,拖垮数据库,致使整个系统性能下降。

另外对于一些需要复杂计算的场景,利用缓存存储中间结果可提高效率。如斐波那契数列:F(n)=F(n-1)+F(n-2)(n>=3,n∈N)。在实现方式上,使用递归来求f(n-1)和f(n-2)和使用中间值缓存的方式,有着本质意义上的差别。再如,网站统计在线用户数,从数据库中读取时免不了需要使用select count(),当用户数据量很大时,这个操作是很耗时的。而如果使用缓存,每次登陆成功将值增加1,就能很好的解决。

再者对于读多写少的场景,缓存就很合适。比如微博,明星大V上千万的粉丝,发微博只需要一次insert写操作,如果每一个粉丝登陆都需要读数据库,那么对数据库的压力将是难以想象的。利用缓存可以讲数据库的读微博操作分散到分布在各地的缓存服务器,如缓存100份。这样当用户登陆微博时,就会从他最近的缓存节点上读取数据。

缓存设计的几种方式

这里指缓存的更新方式,一般来说缓存的更新方式有三种:

Cache Aside更新模式

这是最常见的更新模式,其设计如下:

  • 读取:先从缓存中查询,如果存在直接返回;否则从数据库中读取结果放入缓存,然后返回;
    cache_aside-read.png
  • 写入:先写入数据库,然后将缓存中的数据设置为失效;
    cache_aside-write.png

那么对于写操作,在写入数据库之后为什么不直接讲数据写入缓存,而是将其设置为过期状态呢? 其实这么做的原因主要是怕引起脏读,因为如果两个并发的写操作,无法确定那个操作先完成,所以最后写入缓存的值也就无法确定。

而使其失效的方式就一定不会有并发问题了吗? 并不是的。想象一下来了一个读操作,缓存中不存在,所以要去数据库中读,而这时又来了一个写操作,写完数据库后会将数据放到缓存中。而前面的写操作马上又会将其从数据库中的旧数据覆盖到缓存中,导致错误产生。

但实际上,这种情况很少发生,因为它的概率确实很低,至于为什么大家可以google一下,网上有很多相关的信息。但也不是不可能,所以稳妥一点的方式是为缓存设置一个有效时间。

Write/Read Through更新模式

上面说的cache-aside模式,应用程序需要维护两份数据交互,一个是缓存,另一个是数据库,这样对应用程序来说比较繁琐。而Write/Read模式则将缓存和数据库看成是一个整体,我们把它叫做缓存系统,对于数据库的交互全由缓存系统自己控制。对应用程序来说只需跟缓存交互即可,至于对数据库的操作则不关心。

read through

这是对于读操作而言,当缓存失效时,缓存系统从数据库中读入新的数据放在缓存中,然后返回。

write through

对于写操作而言,如果没有命中缓存,则直接更新数据库;否则直接更新缓存,然后再由缓存系统写到数据库。

其过程可以参考wiki的示意图:
2ce5afe305e60d98bf3b647ef23f3edd.png

Write Behind Caching更新模式

与Write through类似,对于写操作,只更新缓存,而不更新数据库。更新数据库的操作通过后台异步方式实现,类似一些消息队列的异步持久化的方式,这样的好处是写操作只针对内存操作,后台异步写操作还可以批量处理,整体来说速度更快。

这种实现方式较为复杂,其过程可以参考wiki的示意图:
write-behind-cache.png

缓存设计的几个热点问题

缓存穿透

缓存穿透是指缓存没有起到作用,应用程序的请求大量到达了后端数据库的情况。因为查询时如果所需数据在缓存中不存在,便会到数据库中进行再次查询,当这样的数据量太大时,说明我们的缓存系统根本没有其他应有的作用。造成这样情况的有两个原因:

  • 数据本身就不存在

我们通常用命中率用来衡量缓存系统设计的好坏,一般来说命中率能够达到80%以上说明就不错了。当对一些系统中不存在的数据进行查询时,这部分请求就会直接转发到数据库中,如竞争对手可能使用爬虫进行恶意遍历查询,导致数据库的压力增大。

这种情况也是很好解决的,可以在缓存信息中存放一个null值即可。

  • 数据的生产需要经过大量的计算,耗时较长

这种情况常见于电商系统,如在商品列的分页时,商品数据很庞大,且筛选的规则很多,要根据不同条件生成结果需要一定的时间,如果在大并发的情况下,瞬间的流量可能回拖垮数据库。

缓存雪崩

缓存雪崩是指缓存失效后系统性能急剧下降的情况。缓存失效后,要重新生成缓存可能需要一定量的计算,这个过程无疑要耗费时间,对于高并发的系统来说,同一时间内大量的线程都查询到缓存失效了,因此都在重新计算生成缓存,这时大量的计算可能会给服务器带来很大的压力,导致系统性能下降。

要解决这种情况通常有两种方式:

  • 更新锁

更新缓存时使用锁机制,保证同一时间内只能有一个线程进行更新,其余线程要么等待要么返回空值或者默认值等;

  • 后台更新

对于缓存数据统一使用一个后台线程进行更新,这个线程对于一些设定过期时间的数据,定时查询,发现如果接近过期时,便将其更新;

缓存热点

虽然缓存系统本身性能较高,但是针对一些高热点的数据,同一时间内并发访问的请求太多时,也会出现瓶颈,此时可以将这些热点数据库保存多个副本,以减轻对同一服务器的读取压力。如热点微博,可能存多分副本,存放在不同的缓存服务器中,用户访问时根据不同的地理位置或用户特征访问不同的服务器,以减轻单点压力。

再就是对于缓存系统,一般都会有个预热环节,就是在正式上线前,使用预发布的模式,先让其生成一些常用的缓存数据,然后再切换到全站,这样可以避免刚上线时瞬间的高流量对缓存系统进行冲击。

总结

本文先是介绍了缓存的几种使用场景,表明在一些复杂计算和读多写少的系统中应用缓存有其独有的优势,接着介绍了缓存更新的三种模式,cache-aside, write/read through和write behind cache,最后对缓存设计中常见的缓存穿透、缓存击穿和缓存热点问题进行了讨论。

标签: 缓存

添加新评论