缓存使用需要考虑的一些细节

一、数据库与缓存一致性

使用缓存,可以降低耗时,提供系统吞吐性能。但是,使用缓存,会存在数据一致性的问题。

1、几种缓存使用模式

  • Cache-Aside Pattern,旁路缓存模式
  • Read-Through/Write-Through(读写穿透)
  • Write- behind (异步缓存写入)

一般我们使用缓存,都是旁路缓存模式,读请求流程如下:

  • 读的时候,先读缓存,缓存命中的话,直接返回数据;
  • 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。

旁路缓存模式的写流程:

2、删除缓存呢,还是更新缓存?

我们在操作缓存的时候,到底应该删除缓存还是更新缓存呢?我们先来看个例子:

  • 线程A先发起一个写操作,第一步先更新数据库;
  • 线程B再发起一个写操作,第二步更新了数据库;
  • 由于网络等原因,线程B先更新了缓存;
  • 线程A更新缓存。

这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

3、先操作数据库还是先操作缓存

双写的情况下,先操作数据库还是先操作缓存?我们再来看一个例子:假设有A、B两个请求,请求A做更新操作,请求B做查询读取操作。

  • 线程A发起一个写操作,第一步del cache;
  • 此时线程B发起一个读操作,cache miss;
  • 线程B继续读DB,读出来一个老数据;
  • 然后线程B把老数据设置入cache;
  • 线程A写入DB最新的数据;

酱紫就有问题啦,缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。

4、如何保证最终一致性

  • 缓存延时双删
  • 删除缓存重试机制
  • 读取biglog异步删除缓存

二、缓存穿透

1、原理

缓存穿透`:指查询一个一定不存在的数据,由于缓存不命中时,需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。”

缓存穿透一般都是这几种情况产生的:业务不合理的设计、业务/运维/开发失误的操作、黑客非法请求攻击。如何避免缓存穿透呢?

2、解决办法

一般有三种方法。

  • 如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。

  • 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)

  • 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。


三、缓存雪崩

1、原理

缓存雪崩:指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。”

2、解决办法

缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒酱紫。

Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。


四、缓存机击穿

1、原理

缓存击穿:指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。”

缓存击穿看着有点像缓存雪崩,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是在于击穿针对某一热点key缓存,雪奔则是很多key。

2、解决方法

解决方案就有两种:

  • 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
  • “永不过期”。是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。

五、缓存热Key

1、原理

在Redis中,我们把访问频率高的key,称为热点key。如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

2、解决方法

如何解决热key问题?

  • Redis集群扩容:增加分片副本,均衡读流量;
  • 对热key进行hash散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量;
  • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

六、缓存容量内存考虑

1、评估容量,合理利用

如果我们使用的是Redis,而Redis的内存是比较昂贵的,我们不要什么数据都往Redis里面塞,一般Redis只缓存查询比较频繁的数据。同时,我们要合理评估Redis的容量,也避免频繁set覆盖,导致设置了过期时间的key失效。

如果我们使用的是本地缓存,如guava的本地缓存,也要评估下容量。避免容量不够。

2、Redis的八种内存淘汰机制

为了避免Redis内存不够用,Redis用8种内存淘汰策略保护自己~

  • volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
  • allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
  • volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
  • allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
  • volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据。
  • allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
  • noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

3、不同的业务场景,Redis选择适合的数据结构**

  • 排行榜适合用zset
  • 缓存用户信息一般用hash
  • 消息队列,文章列表适用用list
  • 用户标签、社交需求一般用set
  • 计数器、分布式锁等一般用String类型


转载

化解日常Bug的50个大法(覆盖数据库、代码层面、缓存)


posted on 2021-10-26 21:51  雨点的名字  阅读(531)  评论(0编辑  收藏  举报