缓存使用需要考虑的一些细节
一、数据库与缓存一致性
使用缓存,可以降低耗时,提供系统吞吐性能。但是,使用缓存,会存在数据一致性的问题。
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类型