Redis相关知识点与面试题
分布式缓存常见的技术选型方案
分布式缓存使用较多的主要是Redis和Memcached。不过目前最常见的是Redis。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题。因为本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共享的。
Redis和Memcached的区别
共同点:
- 都是基于内存的数据库,一般都当做缓存来使用;
- 都有过期策略;
- 两者性能都非常高;
不同点:
- Redis支持更丰富的数据类型(如:k/v、list、set、zset、hash等数据结构的存储,支持更复杂的应用场景),而Memcached仅支持k/v数据类型;
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载使用,而Memcached把全部数据存储在内存之中;
- Redis有灾难恢复机制,因为可以将缓存中的数据持久化到磁盘上;
- Redis在服务器使用完之后,可以将不用的数据放在磁盘上;而Memcached在内存使用完之后,就会直接报异常;
- Redis原生支持cluster集群模式;而Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
- Redis使用单线程的多路IO复用模型(Redis 6.0引入了多线程IO);而Memcached是多线程非阻塞IO复用的网络模型;
- Redis支持发布订阅模型、Lua脚本、事务等功能,并且Redis支持更多的编程语言;而Memcached不支持;
- Redis过期数据的删除策略同时使用了惰性删除与定期删除策略;而Memcached只用了惰性删除;
Redis除了做缓存还能做什么
- 分布式锁:通常情况下都是基于Redis的Redisson来实现分布式锁;
- 限流:一般是通过Redis+Lua脚本的方式来实现限流;
- 消息队列:Redis自带的list数据结构可以作为一个简单的队列来使用。Redis 5.0中增加的Stream类型的数据结构更加适合用来做消息队列;
- 复杂业务场景:通过Redis及其扩展(如Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过bitmap统计活跃用户、通过sorted set维护排行榜等;
过期数据的删除策略
-
惰性删除: 只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但可能会造成太多过期key没有被删除;
-
定期删除: 每隔一段时间抽取一批key执行删除过期key操作,并且Redis底层通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响;
定期删除堆内存友好,惰性删除堆CPU友好,因此,Redis采用的是 定期删除+惰性/懒汉式删除。
Redis内存淘汰机制
Redis提供6种内存淘汰策略:
- volatile-lru(least recently used): 从已设置过期时间的数据集种挑选最近最少使用的数据淘汰;
- volatile-ttl: 从已设置过期时间的数据集种挑选将要过期的数据淘汰;
- volatile-random: 从已设置过期时间的数据集种任意选择数据淘汰;
- allkeys-lru(least recently used): 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的);
- allkeys-random: 从数据集中任意选择数据淘汰;
- no-eviction: 禁止驱逐数据,也就是说当内存不足时,新写入操作会报错;
4.0版本之后增加以下两种:
- volatile-lfu(least frequently used): 从已设置过期时间的数据集中挑选最不经常使用的数据淘汰;
- allkeys-lfu(least frequently used): 当内存不足时,在键空间中移除最不经常使用的key;
Redis持久化机制
- 快照(snapshotting)持久化(RDB): Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用;(快照持久化是Redis默认采用的持久化方式)
- AOF(append-only file)持久化: 与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化;开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存
server.aof_buf
中,然后再根据appendfsync
配置来决定何时将其同步到硬盘中的 AOF 文件;
Redis 4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble
开启);
Redis事务
Redis可以通过 MULTI, EXEC, DISCARD和WATCH
等命令来实现事务(transaction)功能。
> MULTI
OK
> SET USER "Guide哥"
QUEUED
> GET USER
QUEUED
> EXEC
1) OK
2) "Guide哥"
使用 MULTI
命令后可以输入多个命令。Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC
命令将执行所有命令。
这个过程是这样的:
- 开始事务(
MULTI
)。 - 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。
- 执行事务(
EXEC
)。
你也可以通过 DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令。
> MULTI
OK
> SET USER "Guide哥"
QUEUED
> GET USER
QUEUED
> DISCARD
OK
WATCH
命令用于监听指定的键,当调用 EXEC
命令执行事务时,如果一个被 WATCH
命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
> WATCH USER
OK
> MULTI
> SET USER "Guide哥"
OK
> GET USER
Guide哥
> EXEC
ERR EXEC without MULTI
但是,Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: 1. 原子性,2. 隔离性,3. 持久性,4. 一致性。Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。
你可以将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
缓存穿透
什么是缓存穿透
缓存穿透简单来说就是大量请求的key根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
实际场景
黑客伪造请求进行恶意攻击;
解决办法
- **参数校验:**在查询缓存之前先对请求参数进行校验,一些不合法的参数请求直接抛出异常信息给客户端。比如查询的数据库id不能小于0、邮箱格式不对等;
- 缓存无效key: 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟;
- 布隆过滤器: 通过布隆过滤器判断key是否合法。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
缓存击穿
什么是缓存击穿
缓存击穿是指请求的某一个key不存在或者失效,需要去数据库查询的情况。在这种情况下,如果当前突然来了大量请求,那么由于当前缓存中不存在该数据,因此所有请求将全部落到数据库上,造成瞬时数据库的请求量增大,压力骤增,很有可能将数据库打垮。
解决办法
-
设置永不过期
-
通过定时任务定时更新缓存:在缓存失效前更新缓存
-
设置互斥锁:
1)请求key
2)查询缓存为空
3)查询数据库,同时设置排他锁
4)查询数据库成功,更新缓存,删除排他锁
5)返回请求
缓存雪崩
什么是缓存雪崩
缓存雪崩描述的是这样一个场景:缓存在同一时间大面积失效(或者说是过期),后面的请求都直接落到了数据库上,造成数据库在短时间内承受大量请求。
实际场景
- 系统的缓存模块宕机导致不可用,造成所有的请求都要走数据库;
- 有一些被大量访问的数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接打到了数据库上;
解决办法
针对Redis服务不可用的情况:
- 采用Redis集群,避免单机出现问题导致整个缓存服务都没办法使用;
- 限流,避免同时处理大量的请求;
针对热点缓存失效的情况:
- 设置不同的失效时间,比如随机设置缓存的失效时间;
- 缓存永不失效;
缓存穿透、击穿、雪崩的区别
- 缓存穿透——请求的数据不存在(即无效key),缓存和数据库中都不存在数据
- 缓存击穿——少量热点数据突然失效,缓存中不存在,但数据库中存在
- 缓存雪崩——大量热点数据同时失效,缓存中不存在,但数据库中存在
缓存常用的3种读写策略
Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern是我们平时使用比较多的一个缓存读写模式,比较时候读请求比较多的场景。
缓存读写过程:
**写:**1)先更新DB; 2)然后直接删除cache;
**读:**1)先从cache读取数据,读到就直接返回; 2)cache中读取不到的话,就从DB中读取然后返回; 3)再把数据放到cache中;
Cache Aside Pattern也存在一定的缺陷,具体可见:
Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
缓存读写过程:
写(Write Through):
- 先查cache,cache中不存在,则直接更新DB;
- cache中存在,则先更新cache,然后cache服务自己更新DB(同步更新cache和DB);
读(Read Through):
- 从cache中读取数据,读取到就直接返回;
- 读取不到的话,先从DB加载,写入到cache后返回响应;
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
评论区