文章目录
- 缓存过期时间
- 缓存淘汰策略
- 缓存模式
- 缓存一致性问题
- Redis单线程
- 分布式锁
- 用缓存来提升应用性能
-
- [一致性哈希 + 本地缓存 + Redis 缓存](#一致性哈希 + 本地缓存 + Redis 缓存)
- 请求级别缓存
参考资料
javaguide
《Redis设计与实现》
Redis中文文档:https://www.redisio.com/Getting-started.html
出处
以下问题出自牛客
作者:setsna
链接:https://www.nowcoder.com/discuss/603841169302245376?sourceSSR=users
来源:牛客网
1,介绍一下redis在工作中的用途
2,redis在工作中一般可以承受多少的qps?
3,redis有哪些数据结构,分别常用与哪些场合?
4,redis跳表实现细节,为什么选用跳表,而不选择红黑树和B+树?
5,redis中使用Lua脚本为什么能保证事务性?
6,在分布式锁实现时,Setnx一定是可靠的吗,为什么(主从不一致)?
7,如果需要保证可靠,需要怎样的机制保证(Raft算法)?
8,redis为什么要防止bigkey?
9,redis是内存相关的数据库,那么有1G的数据,想要以key-value的方式存储,那么是每个key的数据多一些更占内存,还是少一些更占内存,根据所学的redis底层数据结构实现进行分析和回答
10,redis的网络IO模型介绍
11,epoll和select模型的区别
12,redis中TTL到期后是如何回收的?
13,在使用分布式锁时,如何保证解锁时是之前上过的锁?
14,使用redis实现分布式缓存时,需要注意哪些问题?
15,说下缓存击穿,缓存穿透,缓存雪崩有什么区别?
缓存过期时间
缓存过期删除

实际使用惰性删除+定期删除的形式
懒惰删除是指 Redis 会在查询 key 的时候检测这个 key 是否已经过期,如果已经过期,那么 Redis 就会顺手删除这个 key。
单纯使用懒惰删除肯定是不行的,因为一个 key 过期之后,可能一直没有被使用过。所以 Redis 结合了定期删除策略。Redis 每运行一段时间,就会随机挑选出一部分 key,查看是否过期,如果已经过期了,就把 key 删除掉。
定期删除

在每一个定期删除循环中,Redis 会遍历 DB。如果这个 DB 完全没有设置了过期时间的 key,那就直接跳过。否则就针对这个 DB 抽一批 key,如果 key 已经过期,就直接删除。
如果在这一批 key 里面,过期的比例太低,那么就会中断循环,遍历下一个 DB。如果执行时间超过了阈值,也会中断。不过这个中断是整个中断,下一次定期删除的时候会从当前 DB 的下一个继续遍历。
随机只是为了保证每个 key 都有一定概率被抽查到。假设说我们在每个 DB 内部都是从头遍历的话,那么如果每次遍历到中间,就没时间了,那么 DB 后面的 key 你可能永远也遍历不到。

定期删除的频率
假如说 hz 的值是 N,那么就意味着每 1/N 秒就会执行一次后台任务。举例来说,如果 hz=10,那么就意味着每 100ms 执行一次后台任务。
在 Redis 里面有一个参数叫做 hz,它代表的是 Redis 后台任务运行的频率。正常来说,这个值不需要调,即便调整也不要超过 100。与之相关的是 dynamic-hz 参数。这个参数开启之后,Redis 就会在 hz 的基础上动态计算一个值,用来控制后台任务的执行频率。
从库处理过期 key
在 Redis 的 3.2 版本之前,如果读从库的话,是有可能读取到已经过期的 key。后来在 3.2 版本之后这个 Bug 就被修复了。不过从库上的懒惰删除特性和主库不一样。

持久化
RDB 简单来说就是快照文件,也就是当 Redis 执行 SAVE 或者 BGSAVE 命令的时候,就会把内存里的所有数据都写入 RDB 文件里
AOF 是之前我们就提到过的 Append Only File。Redis 用这个文件来逐条记录执行的修改数据的命令
过期时间如何确定
-
可以从业务场景出发。比如说,当某个数据被查询出来以后,用户大概率在接下来的三十分钟内再次使用这个对象,那么就可以把过期时间设置成 30 分钟。
-
是根据缓存容量和缓存命中率确定过期时间的。正常来说,越高缓存命中率,需要越多的缓存容量,越长的过期时间。所以最佳的做法还是通过模拟线上流量来做测试,不断延长过期时间,直到满足命中率的要求。
命中率的计算:
如果公司要求平均响应时间是 300ms,命中缓存响应时间是 100ms,没命中缓存的响应时间是 1000ms,假设命中率是 p,那么 p 要满足 100×p+1000×(1−p)=300。
缓存淘汰策略
万一我达到了内存使用上限,但是我又需要加入新的键值对,怎么办?
可以认为已经在缓存中的数据可能用不上了,虽然还没有过期,但是还是可以考虑淘汰掉,腾出空间来存放新的数据。这些新的数据比老的数据有更大的可能性被使用。
LRU
LRU(Least Recently Used)是指最近最少使用算法
缓存容量不足的时候,就从所有的 key 里面挑出一个最近一段时间最长时间未使用的 key。
这个算法从实现上来说很简单,只需要把 key 用额外的链表连起来,每次访问过的挪到队首,那么队尾就是最近最久未访问过的 key。
LFU
LFU(Least Frequently Used)是最不经常使用算法
按照访问频率来给所有的对象排序。每次要淘汰的时候,就从使用次数最少的对象里面找出一个淘汰掉。
缓存模式

我觉得上面的模式,只有Cache Aside和延迟双删比较常用。
Cache Aside
旁路缓存,写和读由业务方来控制。


数据都是以数据库为准的,也就是说如果写入数据库成功了,就可以认为这个操作成功了。即便写入缓存失败,但是缓存本身会有过期时间,那么它过期之后重新加载,数据就会恢复一致。
不管是先写数据库还是先写缓存,Cache Aside 都不能解决数据一致性问题,在两个线程同时更新数据时会有数据不一致问题

删除缓存
写数据时,写数据库,删除缓存,会有读写不一致问题,发生在一个线程更新数据,一个线程缓存未命中回查数据库时。

延迟双删
写数据的时候先写数据库,然后第一次删除缓存,定一个定时器,在一段时间之后再次执行删除。

第二次删除就是为了避开删除缓存中的读写导致数据不一致的场景。

有小概率出现出现以下不一致情况,但是因为两次删除的时间间隔很长,不至于出现图片里的这种情况。延迟双删的缺点是由于存在两次删除,所以实际上缓存命中率下降的问题更加严重。
总结:这么多模式里面,我比较喜欢延迟双删,因为它的一致性问题不是很严重。虽然会降低缓存的命中率,但是我们的业务并发也没有特别高,写请求是很少的。命中率降低一点点是完全可以接受的。

缓存一致性问题
解决缓存穿透、击穿和雪崩问题
缓存穿透
缓存穿透是指数据既不在缓存中,也不在数据库中,导致查询落到数据库上。最常见的场景就是有攻击者伪造了大量的请求,请求某个不存在的数据。如果你没有在服务层面上采用熔断、限流之类的措施,那么数据库就可能崩溃。
回写特殊值
第一种思路是在缓存未命中,而且数据库里也没有的情况下,往缓存里写入一个特殊的值。
这个值就是标记数据不存在,那么下一次查询请求过来的时候,看到这个特殊值,就知道没有必要再去数据库里查询了。
如果攻击者每次都用不同的且都不存在的 key 来请求数据,那么这种措施毫无效果。并且,因为要回写特殊值,那么这些不存在的 key 都会有特殊值,浪费了 Redis 的内存。这可能会进一步引起另外一个问题,就是 Redis 在内存不足,执行淘汰的时候,把其他有用的数据淘汰掉。
布隆过滤器
看一下key是否存在,原理后面补todo
缓存击穿
数据不在缓存中,导致请求落到了数据库上。比如说同一时刻有几百个人请求某个大博主的数据,这些请求都没有命中缓存,那么几百个查询请求都会落到数据库上。
因此,如果请求的数据并不是什么热点数据,那么击穿也没有什么问题,它就是普通的缓存未命中而已。
Singleflight
Singleflight 模式是指当缓存未命中的时候,访问同一个 key 的线程或者协程中只有一个会去真的加载数据,其他都在原地等待。
这个模式最大的优点就是可以减轻访问数据库的并发量。比如说如果同一时刻有 100 个线程要访问 key1,那么最终也只会有 1 个线程去数据库中加载数据。这个模式的缺点是如果并发量不高,那么基本没有效果。所以热点之类的数据就很适合用这个模式。

也就是说,就算是一个热点数据,当几百个请求缓存未命中的时候,在 singleflight 模式之下,也只有一个请求会真的去查询数据,剩下的都在等着这个请求查询回来的结果。
缓存雪崩
缓存雪崩是指缓存里大量数据在同一时刻过期,导致请求都落到了数据库上。
缓存雪崩基本上都是因为一次性加载了很多数据到缓存中,并且都设置为同一个过期时间。比如说在应用启动的时候,提前从数据库里查询数据,然后放到缓存里面,这样这一批数据就会在同一时刻过期。又比如榜单数据计算好了之后加载到缓存里,都是同一个过期时间,导致这一批榜单数据同一时间过期。
过期时间增加随机偏移量
最简单的思路,就是在过期时间的基础上加一个偏移量。
限流
缓存穿透和击穿只有在高并发下才会成为一个问题,所以一个很自然的想法就是使用限流。限流可以考虑在两个地方使用:服务层面和数据库层面。
异地多活
不管是缓存穿透、击穿还是雪崩,归根结底就是请求都落到了数据库上。除了这三个异常,Redis 本身也有可能崩溃,又或者因为网络问题连不上这个集群。

很多大厂会用一些异地多活的方案,就是使用两个 Redis 集群,然后两个集群之间要保持数据同步。那么其中一个 Redis 集群崩溃的时候,就可以用另外一个 Redis 集群。
Redis单线程
todo
分布式锁
用缓存来提升应用性能
一致性哈希 + 本地缓存 + Redis 缓存
我之前维护了一个强调高可用和高性能的服务。这个服务最开始使用的缓存方案是比较典型的,就是访问 Redis,然后访问数据库。后来有一次我们这边网络出了问题,连不上 Redis,导致压力瞬间都到了数据库上,数据库崩溃。
我后面做了两个事情,防止再一次出现类似的问题。第一个事情就是在数据库查询上加上了限流。另外一个事情就是引入了本地缓存。但是本地缓存一开始是没有启用的。我会实时监控 Redis 的状态,一旦发现 Redis 已经崩溃,就会启用本地缓存。启用本地缓存之后,好处是保住了数据库,并且响应时间还是很好。缺点就是本地缓存会面临更加严重的数据一致性问题。

在启用了本地缓存之后,还要监控 Redis 的状态。当 Redis 恢复过来,就可以逐步将本地缓存上的流量转发到 Redis 上。之所以不是立刻全部转发过去,是因为刚恢复的时候 Redis 上面可能什么数据都没有,导致缓存未命中,回查数据库。这可能会引起数据库的问题。
请求级别缓存
我来给你解释一下这种缓存的基本原理。使用这种缓存方案的前提是,你在一个请求里面会反复查询同一个请求数据多次。比如说,因为模块划分之后要恪守边界,所以每个模块都会自己去调用接口来获得同一份数据。
那么你就可以考虑将数据和请求关联在一起,做成一个在请求生命周期内有效的缓存。