Redis为什么这么快

  1. 基于内存存储

  2. redis使用了单线程架构,避免了上下文切换带来的资源消耗

  3. IO多路复用

什么是IO多路复用

指的是利用单个线程来同时监听多个socket,并在某个socket可读,可写时得到通知,从而避免无效的等待,可以重复利用cpu的资源。

IO多路复用有三种方式:select->poll->epoll。他们都可以同时监听多个socket,然后select和poll在得到通知的时候并不知道是哪个socket发出的,就会采用轮询的方式一个个去查找;select可以处理的最大连接数为1024,poll没有限制;而epoll是会把已就绪的socket写入用户空间,不需要挨个遍历socket

目前的IO多路复用都是采用epoll实现的

Redis的五种数据类型及使用场景

  • String:字符串是Redis中最简单的数据类型,也比较常用,可以用来存用户的token,最大512M

  • Hash:key/value结构;可以用于存储对象

  • List:列表是一个双向链表结构,可以快速地在头部和尾部进行插入和删除操作;可以用做消息队列,push放入消息,pop消费消息

  • Set:集合是无序的、不重复的元素集合,支持交、并、差集等操作;可以求共同好友,点赞、共同关注、抽奖活动(限制只能参加一次)

  • Zset:有序集合是集合的一种特殊形式,每个成员都与一个分数相关联,可以按分数从小到大排序;可以用于排行榜

Zset底层的实现

压缩列表和跳跃表

压缩列表

这种存储结构可以节省内存,它是相较于数组来说的。数组的每个元素的大小是相同的,如果我们要存储不同大小的字符串,就要选取最大的字符串作为空间的大小,这样就会浪费一部分内存。为了想要具有数组的优势(排列紧凑,内存连续),就有了压缩列表

压缩列表本质是一个数组,然后还多了四个字段,zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束

然后每个节点还存储了偏移量,使之可以跳到下一个节点

  • 优点:如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)

  • 缺点:而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N)

跳表

就是在链表的基础上增加了多级索引,实现了快速查找元素,时间复杂度是O(logn)

普通链表是O(n)

查找元素过程

先从最高级索引找到第一个小的元素,然后再下降一级索引跳到下一个小的元素,以此类推

我们可以把查找的过程总结为一条二元表达式(下一个是否大于结果?下一个:下一层)

缓存穿透,缓存雪崩、缓存击穿

缓存穿透

是指查询缓存和数据库中都不存在的数据,但是访问了该数据所在的数据库,导致大量请求直接打到数据库上,使得数据库负载过高,甚至宕机。

解决方法:

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

  2. 缓存空对象:当请求的数据在 Redis 缓存中不存在时,可以直接返回一个空对象,而不是一个空字符串或者 null。这样可以保证代码的健壮性,并且不会对后续的业务造成影响。

缓存雪崩

是指在某个时间点,缓存中大量数据同时失效,导致请求直接打到数据库上,使得数据库负载过高,甚至宕机。

解决方法:

  1. 设置热点数据永不过期:对于业务访问频率较高的数据,可以设置永不过期,这样即使缓存失效,也不会对业务造成太大的影响。

  2. 缓存预热:在系统启动时,可以将一些热点数据预先加载到缓存中,并设置较长的过期时间。这样可以避免在系统正式运行时出现缓存雪崩的情况。

  3. 给不同的key的TTL设置不同的值

缓存击穿

也称为热Key问题,是指在 Redis 中,Redis中一个热点key在失效的同时,大量的请求过来,从而会全部到达数据库。

解决方法:

  1. 设置热点数据永不过期:对于业务访问频率较高的数据,可以设置永不过期,这样即使缓存失效,也不会对业务造成太大的影响。

  2. 加分布式锁,获取锁的第一个线程把数据库拉出来加进缓存,这样后续的请求就不用访问数据库了

Redis过期策略

redis的过期策略是:定期删除 + 惰性删除

  • 定期删除:每隔一段时间,会去扫描一定数量的设置了过期时间的key,并清除过期的key

  • 惰性删除:只有当访问一个key时,才会去判断这个key是否过期,过期则清除

Redis内存溢出策略

1.当内存使用超过配置的时候会返回错误,不会删除任何键

2.从所有key中驱除使用频率最少的键

3.从所有key中随机删除

4.从所有key中删除最近最少使用的key

5.从设置了过期时间的key中随机淘汰

6.从设置了过期时间的键集合中驱除使用频率最少的键

7.从设置了过期时间的键中驱逐马上要过期的键

8.从设置了过期时间的键集合中删除最近最少使用的key

记忆:普通三种,随机删除,最近最少使用,使用频率最小;特殊:快要过期的键

Redis持久化机制

RDB

就是把内存数据以快照的方式存到磁盘中它是Redis默认的持久化方式。执行完操作后,在指定目录下会出现一个dump.rdb文件,Redis重启时,通过加载rdb文件来恢复数据

优缺点

优点:适合大规模的数据恢复场景,如备份

缺点:每隔一段时间持久化,没办法做到实时持久化

RDB 做快照时会阻塞线程吗?

redis提供两个指令生成RDB,分别是save和bgsave

  • 如果是save,会阻塞,因为是主线程执行的

  • 如果是bgsave,不会阻塞,而是另外开启一个子线程来生成RDB,生成快照由子线程来处理,父线程可以继续处理客户端的请求

AOF

(1)AOF 持久化采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行 AOF 文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的,可以通过修改 redis.conf 文件中 appendonly 参数开启:

appendonly yes

(2)回写策略:

  • everysec:每秒钟执行一次同步操作(默认值)。

  • always:执行一次写操作就执行一次同步操作,但是这样效率会有点低。

  • no:不主动进行同步,由操作系统来决定。

优缺点

优点:实时性更高

缺点:AOF 记录的内容越多,文件越大,数据恢复变慢。

AOF重写机制

为了避免AOF文件越写越大,redis提供了重写机制。当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

  • Redis会创建一个子进程来执行AOF重写操作,以避免干扰主进程处理客户端请求。

  • 子进程会将每个键的最新值以最简化的命令写入到新的AOF文件中。

  • 在重写过程中,主进程继续处理新的写命令,并将这些命令临时保存在缓冲区中。

  • 子进程完成重写后,会将缓冲区中的增量命令追加到新的AOF文件中,然后通知主进程。

  • 主进程收到信号后,会将缓冲区中的写操作再次追加到临时文件中,然后用临时文件替换旧的AOF文件,并关闭旧的AOF文件。

混合持久化

RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。AOF 优点是丢失数据少,但是数据恢复慢。

为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。

混合持久化实际上是对AOF持久化的一种改良。它工作在 AOF 日志重写过程。也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

  • 当AOF重写被触发时,混合持久化开始发挥作用:Redis会将当前的数据集以RDB格式写入新的AOF文件的顶部,然后再追加新的命令到文件的末尾。

  • 子进程负责AOF重写,首先创建新的AOF文件,将Redis当前的数据生成RDB快照写入新文件的开始部分,然后主进程将新的写操作命令写入AOF重写缓冲区,并追加到新文件的RDB数据末尾。

  • 最终,使用新的AOF文件替换旧的AOF文件。

优缺点

优点:混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

缺点:AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;兼容性差,只有Redis 4.0后才开启

Redis集群

主从模式(一主多从)

redis cluster(多主多从)

Redis主从架构(解决高并发)

跟Mysql的主从类似,也是主节点负责写,从节点负责读

原理:

1.从数据库向主数据库发生SYNC命令,表示要与主数据库同步(通过replication id和offset判断)

2.第一次同步时是采用的全量复制,主节点会生成一份RDB快照,然后发给从节点

3.从节点会把RDB文件写入磁盘,然后再从磁盘加载到内存实现同步

4.以后同步时采用的是增量复制,主结点只会将变化的数据同步给从节点(通过offset)

怎么保证redis的高可用(哨兵)

哨兵模式:实现主从集群的自动故障恢复(监控,自动故障恢复,通知)

哨兵的作用:监控master和slave的状态,当master出现故障的时候,会自动从slave节点中选取新的master

监控:哨兵就可以监控master和slave是否正常工作,采用心跳机制监控服务状态,每隔1s向实例发送ping命令,如果一个哨兵发现实例不能在规定时间内响应,就认为该实例主观下线,当有超过一半的哨兵发现该实例有问题,就认识该节点客观下线,这时候就可以做主从切换了

自动故障恢复(主从切换):

  • 如果slave-priority优先级一样的话,就选取slave结点里offset值最高的(常用)

  • 选取slave-priority优先级最高的

通知:基于redis的发布/订阅模式,客户端可以从哨兵订阅消息,故障转移后,客户端会收到消息

为什么需要客观下线机制?

因为当前哨兵节点探测对方没有得到响应,很有可能这两个机器之间的网络发生了故障,一个哨兵不能说明该master结点有问题,所以一般都是引入多个哨兵一起判断

redis集群脑裂,该怎么解决

什么是脑裂,在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。 如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。

这时,哨兵发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了

然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。

总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方案

当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。

在 Redis 的配置文件中有两个参数我们可以设置:

min-slaves-to-write 1   // 要求至少有1个slave
min-slaves-max-lag 10 //数据复制和同步的延迟不能超过10秒

如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。这样脑裂后的旧master就不会接受client的新数据,最多就丢失10秒的数据,也就避免了大量长时间的数据丢失

redis的分片集群(多主多从)

目的是处理海量数据存储问题和高并发写的问题

作用:

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave结点

  • master之间通过心跳来监测彼此的健康状态,当master出现故障时,会自动从slave节点选一个新的

  • 客户端可以访问任意主结点

集群中数据是怎么存储和读取的:

  • 引入了哈希槽的概念,redis集群里面有16384个哈希槽

  • 将16384个哈希槽分配到不同的实例

  • Redis使用CRC16算法对键进行哈希计算,然后将按照哈希值路由到对应的哈希槽上,然后就分配给不同的master处理

redis分布式锁

是如何实现的

  • 底层是setnx和lua脚本

怎么设置锁的过期时长

  • 里面有个看门狗机制,这个机制会自动续期锁的有效时间,默认是30s,如果进程在30s内没有完成,此时锁并不会释放,而是自动续期,超过看门狗时间的1/3就会重新给锁设置过期时间,也就是10s续期一次,直到任务完成

这个锁可以重入吗

可以重入,获取到锁后他会先判断是否是当前线程,在redis存储的时候是用hash结构来存储线程信息以及可重入次数

如何设计一个缓存策略,可以动态缓存热点数据呢?

由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。

热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据

以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:

  • 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;

  • 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;

  • 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。