你真的会用缓存吗

缓存的使用

当数据的读取量非常大的时候,为了缓解数据库的压力,会大量的使用到缓存。

那什么是缓存呢,我觉得是这样的

两种存储介质a,b,只要a比b快,就可以用a来做b的缓存

数据库一般都是存储在磁盘上面,但是磁盘的访问速度比较慢,当我们想快速获取到数据的时候,显然磁盘已经满足不了我们的需求了。

这时候我们一般会用到Redis, Memchached这种把数据存放在内存之中的NoSql数据库,用他们来缓存我们的数据,因为内存中的读取要比磁盘快的多。

那么缓存到底要怎么用呢?是只要把数据放到Redis里面,然后读取就可以了吗?

如果只是这么简单就好了,在缓存的时候我们要考虑几点东西。

缓存既然是在内存中,那么他们能存储的量就不大,那么哪些数据要放在缓存中呢?缓存的命中率应该达到多少呢?

在什么时候写入缓存呢?该怎么读取缓存呢?

怎么防止缓存穿透和缓存雪崩呢?

缓存策略

Cache Aside(旁路缓存)策略

当我们有缓存的时候,我们就要考虑怎么写入缓存,更新缓存,删除缓存,查询缓存。

写入缓存一般是在创建数据的时候,比如这么一个场景,我之前的一个项目跟房产有关,房产数据在创建的时候写入缓存。

这时候,缓存中存在这么一条数据

{id:1,name:1号房产,space:80,price:10000} //80平米,每平米单价1万

这时候这个房子的价格变动了,更新为每平米2万了,那么我们更新数据库,那这时候缓存要不要更新呢。

  • 更新缓存

当并发线程的时候更新,两个线程同时更新就会导致脏数据的产生。

a要修改成2万,b要修改成3万,a先修改,b在修改,但是b的程序执行的快,b先修改了缓存中的数据,a才修改了缓存中的数据,那么这时候数据库是3万,缓存是2万。

解决方案,直接删除缓存,将缓存删除,查询的时候再回种到缓存中,保证缓存和数据库信息的一致性。

这就是Cache Aside策略

当写入的时候:

  • 写入数据库信息
  • 写入缓存信息

当更新的时候:

  • 更新数据库信息
  • 删除缓存信息

当读取的时候:

  • 读取缓存
  • 缓存存在直接返回数据
  • 缓存不存在读取数据库
  • 创建缓存,返回数据

但是这个策略也存在问题,当更新很多数据的时候,会导致很多缓存失效,那么数据库的压力会增加。

解决方案:

  • 更新的时候更新缓存信息,但是增加锁机制,同一时间只有一个程序在更新缓存,但是对性能有影响。
  • 更新的时候更新缓存信息,给缓存的过期时间设置的短一些,就算有脏数据也会尽快过期,会产生脏数据。

Read/Write Through(读穿/写穿 策略)

这个策略是你的程序只和缓存打交道,你只读写缓存,然后由缓存把数据同步给数据库。

写入的时候有两种做法:

  • 写入缓存 让缓存同步给数据库
  • 写入数据库,当读取的时候回种缓存,这种速度快,因为没写入缓存

更新的时候:

  • 更新缓存数据

读取的时候:

  • 读取缓存
  • 缓存存在直接返回数据
  • 缓存不存在读取数据库
  • 创建缓存,返回数据

Write Back (回写策略)

这个策略是写入数据时候只写入缓存,然后将缓存标记为脏。当下次使用的时候把脏缓存的数据写入到数据库。

写入的时候:

  • 写入缓存
  • 标记为脏

更新的时候:

  • 判断缓存是否为脏
  • 如果是脏,将缓存数据写入数据库,然后更新缓存
  • 如果不是脏,直接更新缓存,并标记为脏

读取的时候:

  • 读取缓存
  • 缓存存在直接返回数据
  • 缓存不存在,寻找一个可用的缓存块,判断缓存是否为脏
  • 如果是脏,将缓存数据写入数据库,查询数据库信息,回写缓存
  • 如果不是脏,查询数据库信息,回写缓存
  • 返回数据

这个策略读取的时候有一些改变,当缓存命中,没有改变。

缓存没有命中会寻找一个缓存块,查看是否是脏缓存,脏缓存意味着缓存信息有变动,需要同步到数据库,所以先同步到数据库在查询。如果不是脏缓存,意味着缓存已经同步到数据库,可以直接查询回写缓存。

上面几种策略其实都是写入更新时候有变动,只有最后一个策略在读取的时候稍微有改变。

一般情况下,使用前两种已经足够。

缓存可用性

如果这时候,缓存挂掉了,就会有大量请求穿透到数据库层,严重情况下,会导致整个系统挂掉。那么这种情况怎么办呢。

我们可以横向扩展缓存,分布式布置多个缓存节点,那么其中一个节点挂掉,我们还有其他的缓存节点可以使用。

客户端方案

这里的客户端指的是使用缓存的客户端,也就是我们的服务器。

写入的时候

当写入分布式缓存的时候,我们要怎么写入呢。

这个和分库分表差不多,都是横向扩展嘛,那我们写入也可以差不多啊,使用hash取余算法,来计算数据写入哪个节点。

但是这样存在一个问题,当我们的节点需要增加或者减少的时候怎么办,我们要重新取余,数据重新存储,这就难搞了啊。

解决方案:

  • 一致性哈希算法

我们假设一个环,将所有节点hash取余之后放到环上,将要缓存的数据也hash取余之后放到环上,怎么确定数据存在哪个节点呢?将数据顺时针移动,遇到的第一个节点就是数据存储的节点。

这个算法增加或减少节点的时候会发生什么呢

  • 增加的时候,部分数据移动到新增节点
  • 减少的时候,减少的节点的数据移动到另一个节点

看上去好像没什么问题,但是如果环上的节点分布不均匀,某一个节点数据很多,当这个节点宕机后,他的数据转移到下一个节点,下一个节点压力骤增,如果节点扛不住,那么重复这些操作,会导致整个缓存系统的雪崩。

解决方案:

上面说了,是环上的节点分布不均匀导致的,那我们就让他分布均匀就好了。或者将一个节点放在环上多个位置,这样的话,这个节点挂掉,他的数据会移动到其他很多节点上,每个节点的压力就不会大增了。

读取的时候

读取缓存的时候也可以像mysql学习,配置主从读取,先从从库读取,读取不到再从主库读取,这样就算从库挂掉,那么还有主库可以读取缓存,读流量不会直接到数据库层。

除了配置主从,还可以配置多副本,副本也就是缓存的缓存,我们在缓存层前面再加一层缓存层,如果这个副本读取不到再去读取主从,回写副本,这样副本中都是最热的缓存,基于成本的考虑,副本容量一般很小。

中间层方案

客户端方案已经可以支撑分布式缓存,但是客户端方案存在一个问题,他受限于语言,如果将他单拿出来,那么所有的请求经过中间层,就不会受限于语言了

服务端方案

redis sentinel是redis 2.4版本中增加的,这个东西可以在redis master节点挂掉之后,在从节点中选取新的主节点,不至于主节点挂掉整个程序挂掉。

缓存穿透

当缓存发生缓存穿透怎么办,前面已经说了一致性hash算法中会引起的缓存雪崩及解决方案,这里就来看看缓存穿透。

如果查询一条数据库不存在的数据,就肯定会引起缓存穿透,因为数据库都没有,缓存更没有了。

这种情况可以回写空值,这样虽然数据库没有,但是缓存有,请求就会被拦截在缓存层中。

但是空值会占用空间而且无意义,如果缓存中存在大量空值,将会占用大量空间。

除了回写空值,还可以使用布隆过滤器。

下一篇文章讲布隆过滤器。

发布了30 篇原创文章 · 获赞 6 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/Thepatterraining/article/details/105308419