缓存

对于数据库的CRUD操做而言,当并发量较大时会出现读或者写的瓶颈。对于大多数场景而言,都是读多写少,所以读更容易成为数据库的瓶颈。而缓存就是为了解决读的问题而出现的。缓存的数据存储在内存中,所以性能很高。前端

缓存更新方案

缓存的更新方式从大的方向分能够分为同步更新缓存和异步更新缓存redis

同步更新缓存

同步更新缓存就是写数据或者读数据的时候同步更新缓存。spring

读的时候更新缓存

读的时候更新缓存策略很简单,如上图所示,主要有如下几个步骤:数据库

  1. 读请求时,若是缓存数据存在则直接返回该数据;
  2. 读请求时,若是缓存数据不存在则从数据库中读入数据并写入缓存,而后返回数据;
  3. 写请求时,写入数据库成功后,删除缓存

写的时候更新缓存

写的时候更新缓存与读的时候更新缓存原理相似,只是在写数据时候会先写数据库,而后写缓存,而不是删除缓存。缓存

接下来咱们对比一下这两种方式的优缺点。bash

读的时候更新缓存在数据写入数据库后只须要删除缓存便可,操做比较简单,所以逻辑上会简单一些,这种方式是最多见的缓存更新方式。可是读请求的时候要先读数据库而后写入缓存,若是是一个影响很大的更新,那么缓存失效后的第一次读请求可能会比较慢。好比常见的好友列表,若是缓存失效,须要从数据库先从关系链表查好友的关系链,而后去用户表查每一个好友的头像和昵称,最后将数据还要写入缓存,这个过程可能会比较耗时。架构

而写的时候更新缓存,只须要将一样的更新数据先写入数据库,而后写一遍缓存,不用从数据库中取出来而后写入缓存。不过使用这种方式的时候,读请求的时查询缓存没有命中,而后查数据库的逻辑不能省,由于缓存还会由于过时而失效。并发

这两种方式都有一个问题,写请求时写入数据库成功,而后同步写入缓存或者删除缓存这两个动做均可能失败,若是失败就会致使数据库中的数据与缓存中的数据不一致。首先,能够采起重试的策略来尽量减少出现的几率,并且尽可能要给缓存设置一个过时时间,这样可使缓存中的数据与数据库中的数据达到最终一致性。异步

异步更新缓存

同步更新缓存须要在业务逻辑里单独处理这一段逻辑,而其自己与业务逻辑是不相关的,咱们只能为了提高性能而引入了缓存系统。所以能够考虑经过异步的方式更新缓存,将缓存更新的服务与业务服务进行解耦。并且异步更新的方式,将缓存更新的操做单独用一个服务来实现,所以读写请求减小了缓存更新的逻辑,性能会获得提高。分布式

先写DB,异步MQ更新缓存

一个简单的异步缓存更新方案入上图所示,写请求写完数据库后会抛一个MQ消息,而后有一个独立的缓存更新服务区接受这个消息,而后从数据库读数据并写入缓存。采用异步的方案之后,数据无需同步写入,减轻了业务服务的逻辑任务,在业务场景下可能不少个地方都须要更新缓存,采用异步更新发消息很方便。不过这里须要依赖中间件消息队列,须要消息队列能保证不丢消息。缓存更新服务中也会存在缓存更新失败的状况,不过咱们能够采用不断重试的方案来避免这样的问题。

可是上面这个设计会有一些问题,主要是在并发状况下。

问题1:若是先有一个写请求更新了数据库的数据,而后抛出一条MQ消息。可是在这个MQ消息被处理前,这时候一条读请求被发起了,那么这个时候读请求会读到缓存中的旧数据。

问题2:若是先有一个写请求更新了数据库的数据,并抛消息MQ1。而后接着有另外一个写请求紧跟着也更新了数据,并抛消息MQ2。若是MQ1和MQ2串行执行,那么就没有问题。可是分布式环境下,服务是多机多进程部署,所以MQ2可能比MQ1先被处理。考虑这种极端条件下,若是第二次写请求前,MQ1的消息已经到达缓存更新服务并从数据库中取出消息。就在这时,MQ2消息到达被另外一个进程处理,从数据库中取出数据并先于MQ1消息更新了缓存,而后这时MQ1消息取出的数据写入缓存就覆盖了MQ2消息的更新的数据。这时候缓存中的数据也与数据库中的数据不一致了。

若是对缓存中的数据与数据库中的数据的一致性要求很是高,能够引入脏标和版本号的机制来实现。若是彻底不能接受缓存中数据与数据库数据不一致,就不要使用缓存。

  1. 在更新数据到数据库以前先写一个脏标来标识缓存中的数据是脏的。脏标是用来解决问题1的。若是写脏标失败,则本次请求失败。若是写脏标成功,可是写数据库失败,本次请求也失败,这会致使缓存失效,下次读请求时发现缓存中的数据是脏数据,而后去读数据库。脏标写成功,数据也写成功,可是发消息失败,则由下一次读请求来更新缓存。为了不脏标删除失败而致使缓存雪崩,最好给脏标设置一个过时时间。
  2. 给每一条数据都维护一个版本号,每次更新数据库都将版本号加1。版本号是用来解决问题2的。更新缓存以前先判断缓存中数据的版本号与数据库中数据的版本号,若是将要写入缓存中的数据的版本号大于缓存中数据的版本号则说明要更新的数据更新,此时更新缓存。若是数据库中数据的版本号小于缓存中数据的版本号则说明要更新的数据比缓存中的数据更旧或者数据相同,此时不更新缓存。
  3. 基于版本号的更新能够用redis的lua脚原本实现原子性
local cache_info = redis.call('GET', KEYS[1])
local cache_version = redis.call('GET', KEYS[2])
if(type(cache_version) ~= 'string' or 
   type(cache_info) ~= 'string' or 
   tonumber(cache_version) < tonumber(ARGV[1])) 
then 
   redis.call('SET', KEYS[2], ARGV[1], 'EX', ARGV[3])
   return redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
else 
   return 0 
end
复制代码

ps:KEYS[1]是缓存数据的key,KEYS[2]是版本号的key,ARGV[1]是更新后的缓存数据,ARGV[2]是更新后的版本号,ARGV[3]是key的过时时间。

先写缓存,异步将脏数据刷到数据库

先写缓存而后异步将数据刷到数据库的方法与操做系统的文件系统的读写核心流程是相同的。对于操做系统的文件系统,因为内存操做与磁盘操做存在百万数量级的差异,所以操做系统的文件系统维护了一个高速缓存区来减少这种巨大差距带来的影响。文件系统读操做时,先查询高速缓存区是否存在数据,若是没有则从磁盘读入高速缓存区。写数据时,将数据写入高速缓存区,系统调用write就返回成功了。而后经过一个名为update的后台进程,不断的调用sync将高速缓存区的内容写入磁盘。

先写缓存,而后异步将数据刷到数据库的方案流程图以下:

该方案的好处是,读写都是走缓存,所以数据极快,能够应对极高的并发请求。不过这种方案会致使缓存中数据与数据库中数据存在不一致的时间段,更为严重的是若是机器宕机,还没写入数据库的脏数据会丢失。若是要避免数据丢失,还可使用双缓存的方案,不过这有会是系统更加复杂,维护一致性更加困难。

缓存中存在常见问题

缓存穿透

通常状况下查询数据,数据都是存在的。大部分业务系统都须要给用户建立一个帐户,若是一个新用户去查询用户信息,数据库中不存在这个用户的信息,系统会返回前端说明是一个新用户。正常状况下,这样没有问题。若是有人利用这个漏洞,用不少个这种新用户的帐号,不断请求用户系统的接口,全部的请求都会打到DB上,会DB带来很大的压力,甚至宕机。

像这种查询系统中压根不存在的数据,使请求落到DB上的状况,被称为缓存穿透。

对于缓存穿透经常使用解决方案有两个:缓存和空值和布隆过滤器

缓存空值

缓存空值的方法,正如其名,当查询到数据不存在时,向缓存的key中写入null。当查询到该key存在,且值为null时,按数据存在处理。

布隆过滤器

第二种方案是在前一种方案以前再加一层布隆过滤器,若是布隆过滤器能命中,则查缓存,若是布隆过滤器没有命中,则直接返回。布隆过滤器的特色是若是数据存在则布隆过滤器必定会命中,若是数据不存在则布隆过滤器绝大多数状况下不会被命中。所以,即便有部分不存在的数据经过了布隆过滤器的过滤,仍是会被空值缓存拦截住。

第二种方案是在第一种方案的基础上造成了,所以第二种方案复杂一些,可是若是有大量不存在的数据被缓存会浪费缓存的空间,而布隆过滤器能过滤掉绝大多数这样的状况。所以,若是为null的key的数量不是不少,直接用第一种方法便可,反之,若是为null的key的数量不少,则建议加一层布隆过滤器。

缓存洞穿

在高并发下,当缓存数据失效的一瞬间,这时全部的请求都会打到DB上,形成DB瞬时压力陡增,这就是缓存洞穿。

防止缓存洞穿的方法是当发现缓存失效时,在查询DB以前先加锁,这样第一个取到锁的线程更新缓存,其余线程由于取不到锁会等待。等到一个线程更新缓存成功后,其余线程就能够从缓存中查询信息了。

缓存雪崩

缓存雪崩是指同一时间缓存大规模失效,致使请求都直接打到DB上,瞬间的流量将DB打挂,致使整个系统崩溃,这种状况就是缓存雪崩。好比缓存机器宕机或者重启时均可能致使缓存雪崩。

对于缓存雪崩首先采用缓存集群的方案来增长容错性,若是使用redis作缓存,可使用主从+哨兵的部署来方案来提升可用性,避免缓存大量失效的问题发生。

对于微服务架构,雪崩已经发生的状况,可使用开源的Hystrix实现降级和限流,避免DB宕机。可是Hystrix不具有很好的通用性,对于spring cloud能够比较方便的使用,对于其余语言下该怎么作呢?微服务治理的新趋势是使用server mesh,经过server mesh来避免服务雪崩。server mesh具备更好的通用性,并且对语言彻底兼容。

热点数据失效

大量热点缓存数据同时失效,致使大量请求直接打到DB上。对于热点数据同时失效的问题,能够在过时时间上,加上一个随机值,避免缓存同时失效。

总结

本篇文章,总结了本身对缓存知识的认识,介绍了四种常见的缓存方案,每种方案各有优劣,须要根据业务需求来选择合理的方案。而后介绍了使用缓存时可能遇到的几个问题,并总结了常见的解决方案。

相关文章
相关标签/搜索