Redis基础,认识Redis
为什么要选择Redis,对比Memcache和Redis的优缺点?你们为啥用Redis?
Memcache
先来看看 Memcache 的特点:
Memcache 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
Memcache 功能简单,使用内存存储数据;
Memcache 的内存结构以及钙化问题我就不细说了,大家可以查看官网了解下;
Memcache 对缓存的数据可以设置失效期,过期后的数据会被清除;
失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。
使用 Memcache 有一些限制,这些限制在现在的互联网场景下很致命,成为大家选择Redis、MongoDB的重要原因:
key 不能超过 250 个字节;
value 不能超过 1M 字节;
key 的最大失效时间是 30 天;
只支持 K-V 结构,不提供持久化和主从同步功能。
Redis
与 Memcache 不同的是,Redis 采用单线程模式处理请求。
这样做的原因有 2 个:
一个是因为采用了非阻塞的异步事件处理机制;
另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
相比 Memcache,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。
Redis 与 Memcached共同点:
都是基于内存的数据库,一般都用来当做缓存使用。
都有过期策略。
两者的性能都非常高。
Redis 与 Memcached 区别:
Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;
项目里面用到了Redis,具体用来做了些什么?
内存缓存(基础数据类型)
消息队列(pub/sub,stream)
距离计算(GEOHAH)
分布式锁和红锁(set nx px)
大数据量精确统计、状态/标签记录、布隆过滤器等(bitmap)
超大数据量基数统计(hyperloglog)
库存秒杀保证精确性(incrby)
排行榜(zset)
Redis数据结构相关
Redis有哪些数据结构?
字符串(Strings)(SDS):字符串类型是Redis中最基本的数据类型,可以存储任何类型的数据,如文本、整数或二进制数据。
列表(Lists):列表类型是有序的字符串列表,可以按照插入顺序存储多个字符串值。可以在列表的两端执行插入和删除操作,还可以根据索引获取元素。
哈希(Hashes):哈希类型是一个键值对的集合,类似于关联数组或字典。在哈希中,每个键都与一个值相关联,可以通过键快速查找和访问值。
集合(Sets):集合类型是无序的字符串集合,每个集合中的元素都是唯一的。可以执行添加、删除和查找元素的操作,并支持集合之间的交集、并集和差集运算。
有序集合(Sorted Sets):有序集合类型是一个有序的字符串集合,每个元素都与一个分数相关联。可以按照分数对元素进行排序,并且支持按照分数范围或成员进行检索。
订阅(Pub/Sub):基于通道的一对多的发布订阅,可用于实时消息推送、事件驱动的系统、实时日志处理、消息队列等。
地理位置信息(Geo):Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
HyperLogLog:HyperLogLog是一种估算算法,具有可使用极小内存(最大12kb)进行大基数数据估算的特点,在对统计经度要求不高的场景下非常好用。
位图(Bitmap):Redis 的 Bitmap 数据类型是一种位图数据结构,可以表示大量的二进制位,并对这些位进行高效的位操作。可用于大数据量精确统计、状态/标签记录、布隆过滤器等场景。
4.0版本以后提供了(Bitfields)来进行更加方便的位操作。
消息队列(Stream,Redis 5.0以后):前面已经有了订阅这种模式来实现消息队列,但消息无法持久化,在5.0版本以后引入了stream数据类型,专门用作消息队列,可以方便的实现数据持久化,且速度由于kafka。
布隆过滤器(BloomFilter):如果一个元素在过滤器中存在,那它可能存在;如果不存在,那么它一定不存在。
Redis Module:上面提到的BloomFilter就是一个Redis提供的插件,实现了布隆过滤器的功能。
Redis底层有哪些数据结构?
String 底层数据结构
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:
SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
List 底层数据结构
List 数据类型底层数据结构由 quicklist 实现,简单来说,一个 quicklist 就是一个链表,而链表中的每个节点又是一个entry,每个entry中包含了对应对的listpack。
Hash 底层数据结构
Hash底层由 listpack 和 hashtable实现。
listpack,紧凑列表,用一块连续的内存空间来紧凑保存数据,同时使用多种编码方式,表示不同长度的数据(字符串、整数)。
Redis 整体就是一个 哈希表来保存所有的键值对,无论数据类型是 5 种的任意一种。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。
Set 底层数据结构
Set 类型的底层数据结构是由哈希表或整数集合实现的:
如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
ZSet 底层数据结构
Zset 类型的底层数据结构是由紧凑列表或跳表实现的:
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用紧凑列表作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
具体详见Redis面试八股文-Redis为什么快?中的数据结构部分
Redis数据持久化相关
Redis是怎么持久化的?服务主从数据怎么交互的?
采用的是RDB结合AOF的模式,开启混合持久化。
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。
在Redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
这里很好理解,把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,你再回放一下日志,数据不就完整了嘛。
不过Redis本身的机制是 AOF持久化开启且存在AOF文件时,优先加载AOF文件;AOF关闭或者AOF文件不存在时,加载RDB文件;加载AOF/RDB文件城后,Redis启动成功; AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
Redis如果突然机器掉电会怎样?Redis 如何实现数据不丢失?
取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。(always)
但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。(everysec)
如果AOF的配置为sync值为no,则意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。突然掉电会丢失大量数据。
跳出这个问题的答案是:你们家服务及机房不配个应急电源什么的?
RDB的原理是什么?
主要是fork和cow两种方式结合。
fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
Redis AOF和RDB的优缺点
Redis AOF(Append-Only File)和 RDB(Redis Database)是两种不同的持久化方式,它们各自具有一些优点和缺点。
AOF 的优点:
更好的数据安全性:AOF 以日志的形式记录每个写操作,可以提供更高的数据安全性,因为它可以保证每个写操作都被持久化到磁盘上。
可以进行部分恢复:AOF 文件包含了所有写操作的日志,可以通过回放 AOF 文件来完全恢复 Redis 的数据状态。此外,AOF 文件还可以使用 Redis 提供的 BGREWRITEAOF 命令进行重写,以优化文件的大小和性能。
更好的灾难恢复:在发生灾难性故障或停机时,AOF 可以提供更高的恢复能力,因为它包含了更详细的写操作日志。
AOF 的缺点:
AOF 文件的体积通常比 RDB 文件大:由于 AOF 记录了每个写操作的命令,AOF 文件通常比 RDB 文件更大,因此会占用更多的磁盘空间。
AOF 文件恢复速度较慢:AOF 文件需要回放所有的写操作来恢复数据,因此在大型数据集的情况下,恢复速度可能会比 RDB 快照慢。
AOF 文件的写入性能较 RDB 略低:由于每个写操作都需要写入到 AOF 文件中,AOF 持久化方式相对于 RDB 的写入性能略低一些。
RDB 的优点:
快速且紧凑:RDB 是通过生成 Redis 数据集的快照来实现持久化的,因此它的恢复速度比 AOF 快,并且生成的 RDB 文件通常比 AOF 文件更紧凑,占用的磁盘空间较小。
更适合备份和迁移:由于 RDB 文件是一个压缩的二进制文件,它更适合用于备份和迁移数据。可以将 RDB 文件直接复制到其他服务器上进行恢复或迁移。
RDB 的缺点:
数据丢失风险:RDB 是通过定期生成快照来持久化数据,如果 Redis 在最近一次快照之后发生故障,会丢失最后一次快照之后的数据。
不适合实时持久化:由于 RDB 是定期生成快照,所以在故障发生时,可能会丢失最近的数据修改,不适合对数据实时性要求较高的场景。
为什么会有混合持久化?
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
Redis 集群
是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?
哨兵模式: Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
集群模式:Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
Redis的同步机制了解么?
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。
集群脑裂导致数据丢失怎么办?
什么是脑裂?
先来理解集群的脑裂现象,这就好比一个人有两个大脑,那么到底受谁控制呢?
那么在 Redis 中,集群脑裂产生数据丢失的现象是怎样的呢?
总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。 如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。
这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。
然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
解决方案
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。
这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。
等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
再来举个例子。
假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线,开始进行主从切换。
同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。
这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。
缓存相关
缓存穿透、击穿、雪崩分别是什么?
缓存穿透指的是数据库本就没有这个数据,请求打到了数据库上,缓存系统形同虚设。
缓存击穿(失效)指的是数据库有数据,缓存本应该也有数据,但是缓存过期了,Redis 这层流量防护屏障被击穿了,请求打到了数据库上。
缓存雪崩指的是大量的热点数据无法在 Redis 缓存中处理(大面积热点数据缓存失效、Redis 宕机),流量全部打到数据库,导致数据库极大压力。
如何避免缓存穿透、击穿、雪崩?
避免缓存穿透:
缓存空值:
当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:0)。当后续再次进行查询则直接返回空值或者缺省值。
但这种方法不能防御来自恶意攻击的伪造数据ID,预先缓存的数据中如果没有这个ID,那么请求一样会打到数据库上,此时可以集合布隆过滤器进行过滤。
布隆过滤器:
在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不用去数据库查询了。
避免缓存击穿:
过期时间 + 随机值
对于热点数据,我们不设置过期时间,这样就可以把请求都放在缓存中处理,充分把 Redis 高吞吐量性能利用起来。
或者过期时间再加一个随机值。
设计缓存的过期时间时,使用公式:过期时间=baes 时间+随机时间。
即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对 DB 造成过大压力。
预热
预先把热门数据提前存入 Redis 中,并设热门数据的过期时间超大值或直接不过期。
使用锁
当发现缓存失效的时候,不是立即从数据库加载数据。
而是先获取分布式锁,获取锁成功才执行数据库查询和写数据到缓存的操作,获取锁失败,则说明当前有线程在执行数据库查询操作,当前线程睡眠一段时间在重试。
这样只让一个请求去数据库读取数据。
避免缓存雪崩:
缓存雪崩的原因:
大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
Redis 故障宕机,缓存系统异常。
过期时间添加随机值
要避免给大量的数据设置一样的过期时间,过期时间 = baes时间+ 随机时间(较小的随机数,比如随机增加 1~5 分钟)。
这样一来,就不会导致同一时刻热点数据全部失效,同时过期时间差别也不会太大,既保证了相近时间失效,又能满足业务需求。
同时,也可以给业务服务器留出足够的时间进行续期或者更新操作。
接口限流
限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。
例如:非核心业务接口限制1000请求/s,核心请求结合缓存和锁,允许少部分数据直接请求数据库。
服务熔断和限流
服务熔断就是当从缓存获取数据发现异常,则直接返回错误数据给前端,防止所有流量打到数据库导致宕机。
服务熔断和限流属于在发生了缓存雪崩,如何降低雪崩对数据库造成的影响的方案。
构建高可用的缓存集群
所以,缓存系统一定要构建一套 Redis 高可用集群,如果 Redis 的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。
如何设计一个缓存策略,可以动态缓存热点数据呢?
由于资源有限,我们只需要将部分热点数据进行缓存,具体什么是热点数据,可以访问量,最后访问时间,点击量,购买量,点赞量等指标来量化。
具体的实现策略是根据量化指标选出比如top500的数据,然后结合具体的量化指标进行数据重排,然后更新缓存。
以社交平台场景中的例子,现在要求只缓存点赞数top1000的内容。具体细节如下:
先通过缓存系统做一个排序队列(比如存放 1000 个内容),系统会根据内容的点赞数和时间,更新队列信息,越是最近发布和最高点赞的内容排名越靠前;
同时系统会定期过滤掉队列中排名最后的 500 个内容,然后再从数据库中读取出 500 个新发布并且点赞数最高的内容加入队列中;
这样当请求每次到达的时候,会先从队列中获取内容 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的内容信息,并返回。
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 500 个内容的操作。
说说常见的缓存更新策略?
常见的缓存更新策略共有3种:
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。
Cache Aside(旁路缓存)策略;
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
写策略的步骤:
先更新数据库中的数据,再删除缓存中的数据。
读策略的步骤:
如果读取的数据命中了缓存,则直接返回数据;
如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
注意,写策略的步骤的顺序不能倒过来,即不能先删除缓存再更新数据库,原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。
Read/Write Through(读穿 / 写穿)策略;
Read/Write Through(读穿 / 写穿)策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
1、Read Through 策略
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。
2、Write Through 策略
当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:
如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。
如果缓存中数据不存在,直接更新数据库,然后返回;
Write Back(写回)策略;
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
实际上,Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。
Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。
Write Back 策略特别适合写多的场景,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。
但是带来的问题是,数据不是强一致性的,而且会有数据丢失的风险,因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。
所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。
如何保证缓存和数据库数据的一致性?
先更新数据库再更新缓存;因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现某个请求已经更新了数据库并且删除了缓存,另外的请求才更新完缓存的情况。但是,极限情况下依然会出现,并不能完美。
使用事务(Transaction);对于涉及数据库和缓存的复杂操作,可以使用数据库事务来确保一致性。在事务中,先更新数据库,然后再更新缓存。如果任何一步操作失败,可以回滚整个事务,从而保持数据库和缓存的一致性。
延迟双删;大致流程:更新数据库之前删除缓存=>更新数据库=>睡眠一段时间=>再次删除缓存;这里的重点在于延迟的这段时间用于其它请求更新,确保读取请求在写入请求睡眠期间将数据读出来并更新缓存。
所以,写入请求的睡眠时间就需要大于读取请求从数据库读取数据并写入缓存的时间。这个时间很难估量,所以实际应用起来比较困难。
分布式锁;在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
短过期时间;在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
Redis 线程模型
Redis 线程模型简述
Redis 是一个单线程的服务器(网络io多线程),采用了异步的事件驱动模型。其线程模型可以概括如下:
单线程:Redis 服务器主要通过一个单独的线程来处理所有的客户端请求和其他操作。这个单线程负责接收和处理客户端请求、执行命令、读写数据以及触发事件等。
事件驱动:Redis 使用了事件驱动的机制来处理客户端请求和其他事件。它基于 I/O 多路复用技术,通过监听多个套接字(sockets)上的事件,当有事件发生时,触发相应的回调函数进行处理。
非阻塞 I/O:Redis 使用非阻塞的 I/O 操作来实现高性能的网络通信。它利用操作系统提供的异步 I/O 接口,通过非阻塞方式读取和写入网络数据,以避免阻塞等待。
事件循环:Redis 的主线程通过事件循环机制,不断地监听和处理事件。事件循环会检查所有注册的事件,并根据事件的类型调用相应的回调函数来处理事件。
单线程与多进程/多实例:虽然 Redis 是单线程的,但可以通过多进程或者多实例的方式来实现并发处理和负载均衡。可以启动多个 Redis 实例,每个实例都运行在不同的进程中,每个实例都有自己独立的线程和资源,从而实现并发处理和横向扩展。
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是传统意义上我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。
例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。
Redis 在6.0版本以后,引入了网络io多线程处理,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程):
Redis-server : Redis的主线程,主要负责执行命令;
bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
Redis 采用单线程为什么还这么快?
Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
Redis 采用了高效的数据结构。
Redis 过期删除与内存淘汰
Redis 使用的过期删除策略是什么?
惰性删除+定期删除
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
惰性删除策略的优点:
因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:
如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis 的定期删除的流程:
从过期字典中随机抽取 20 个 key;
检查这 20 个 key 是否过期,并删除已过期的 key;
如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
定期删除策略的缺点:
难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
可以看到,惰性删除策略和定期删除策略都有各自的优点,所以 Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
Redis 持久化时,对过期键会如何处理的?
Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File),下面我们分别来看过期键在这两种格式中的呈现状态。
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
Redis 持久化时,对过期键会如何处理的?
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis 内存满了,会发生什么?
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 内存淘汰策略有哪些?
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1、不进行数据淘汰的策略
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
2、进行数据淘汰的策略
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰:
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰:
allkeys-random:随机淘汰任意键值;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
#LRU 算法和 LFU 算法有什么区别?
什么是 LRU 和 LFU 算法?
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
需要用链表管理所有的缓存数据,这会带来额外的空间开销;
当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
Redis 是如何实现 LRU 算法的?
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
不用为所有的数据维护一个大链表,节省了空间占用;
不用在每次数据访问时都移动链表项,提升了缓存的性能;
但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。
什么是 LFU 算法?
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
Redis 是如何实现 LFU 算法的?
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。Redis 对象的结构如下:
typedef struct redisObject {
…
// 24 bits,用于记录对象的访问信息
unsigned lru:24;
…
} robj;
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。
在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),用来记录 key 的访问时间戳;低 8bit 存储 logc(Logistic Counter),用来记录 key 的访问频次。
Redis 实际应用相关
你使用过Redis分布式锁么,它是什么回事?
使用过:
1. 利用Redis和lua脚本进行原子操作,可实现在分布式系统中进行分布式锁操作
2. 需要根据失业的业务场景选择延长锁定时间或进行获取锁失败的容错操作
3. 红锁是在分布式锁的基础上,使用多个实例进行锁操作,如果(N/2+1)个客户端加锁成功,则认为获取到了锁
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
可以使用keys指令可以扫出指定模式的key列表。
但由于keys做大量扫描时会出现卡顿从而影响线上业务,此时可以使用scan命令进行扫描,但scan命令可能会出现重复数据,需要在业务代码中进行去重操作。
使用过Redis做异步队列么,你是怎么用的?
1. 使用List和ZSET实现的消息队列由于需要轮训,可能造成很多的资源浪费或消息的到达不及时,且数据保存在内存中,用后即销毁,无法回溯。
2. 使用pub/sub实现的消息队列支持多端接收,已经可以很好的满足业务解耦,到达速度也非常快;但同样存在读取后即销毁,无法回溯的问题。
3. Redis stream实现的消息队列基本比较好的解决的上面的问题,而且结合消费组以后对分布式系统非常友好;对比kafka等老牌重量级中间件速度还更快一些,唯一的缺点可能是内存会导致成本更高了。
详细参见:
Redis面试八股文-Stream数据结构和消息队列(List,Pub/Sub, Stream对比)
Redis如何实现延时队列?
1.可以使用有序集合来实现,拿时间戳做score,然后消费端使用zrangebyscore取出跟近期时间戳相近的元素进行消费。
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。
电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩
Pipeline有什么好处,为什么要用pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。
使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
程序猿老龚(龚杰洪)原创,版权所有,转载请注明出处.