Redis系统篇

从系统维度介绍Redis的常见面试题。参考:https://zhuanlan.zhihu.com/p/427496556

基本数据类型与应用场景

  • 缓存
  • 排行榜
  • 计数器应用
  • 共享Session
  • 分布式锁
  • 消息队列
  • 位操作

string类

应用场景:缓存(共享session)、分布式锁,计数器、限流

哈希类

应用场景:缓存用户信息等。

列表

1
2
3
4
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)

应用场景:消息队列

集合

应用场景:用户标签、共同好友

有序集合

应用场景:排行榜

Redis为什么快?

基于内存

  • redis是纯内存操作:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础。

  • 非阻塞I/O:Redis采用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了时间,不在I/O上浪费过多的时间。

单线程机制

Redis用单线程处理客户端请求,避免了线程切换和竞争态消耗。

Redis基于Reactor模式开发了自己的网络事件处理器,由以下这四部分组成。

  • 套接字;
  • I/O多路复用程序;
  • 文件事件分派器(dispatcher);
  • 事件处理器

IO多路复用思想是:让内核遍历socker是否就绪,而不是线程sleep+遍历+非阻塞套接字。

底层数据结构

Redis支持多种复杂数据结构,如字符串、列表、集合、有序集合和哈希表等。这些数据结构在某些场景下能够显著提高性能。

过期删除策略

过期校验

首先,Redis维护了一个过期字典,expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。

任何对key的查询都要先查过期字典,如果不在过期字典中,说明是持久数据(没有设置过期时间的数据)

如果在字典中,就校验是否过期,没过期才继续查询。

过期删除

Redis维护了两种过期删除策略,实际上这两种过期策略都有使用。

  • 惰性删除(被动删除)
  • 定期删除(主动删除)

(1)惰性删除

一个key只有被访问时才去判断是否已经过期,如果过期了,就删除它。这是一种被动删除的策略。

优点:在执行查询时才检查key是否过期,CPU友好

缺点:存在未访问的过期key堆积现象,内存得不到有效清理(内存不友好)

(2)定期删除

Redis每隔100ms就随机抽取一定数量的key进行过期检查和删除。如果过期的key占比全部抽样key超过25%,那么Redis就会继续进行抽样检测。同时为了避免抽样检测的无限进行,设置了一个最长任务时间。

优点:较为平衡的策略,cpu压力低同时也能执行删除策略。

缺点:定期任务的频率和时长难以控制。频率高则CPU压力大,时长低则删除不彻底。

内存淘汰策略

当Redis的运行内存达到max_memory,就会触发内存淘汰机制。此时内存不足以容纳新数据的写入。

内存淘汰主要可分为三大类,共8种

  • 不进行内存淘汰,直接报错服务不可用;
  • 针对设置过期时间的key的内存淘汰策略;
  • 针对没有设置过期时间的key的内存淘汰策略。

后两者差不多,只不过针对的key不同。$4+3+1 = 8$

1
2
3
4
5
6
7
ttl:淘汰最早过期时间的key

random:随机淘汰

lru:最少最近使用

lfu:最少不常用

LRU

least Recently Used ,Recently Used 即最近使用的,least最少的。从时间的角度上淘汰key,即淘汰距离上一次使用时间最长的key。

LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)

实现:我们可以用双向链表(LinkedList)+哈希表(HashMap)实现(链表用来表示位置,哈希表用来存储和查找),在Java里有对应的数据结构LinkedHashMap

1
2
3
4
LRU算法的描述: 设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:

set(key,value):将记录(key,value)插入该结构。当缓存满时,将最久未使用的数据置换掉。
get(key):返回key对应的value值。

LFU

LFU(Least Frequently Used ,最近最少使用算法),从使用次数的角度上淘汰key,即淘汰最少使用次数的数据。

LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。

实现:考虑到 LFU 会淘汰访问使用次数最小的数据,即选出使用次数最小的元素,最终实现策略为小顶堆+哈希表。

1
2
3
4
5
6
LFU 算法的描述:

设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:

set(key,value):将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉。
get(key):返回key对应的value值。

持久化机制

redis是一个内存数据库,当Redis服务重启时,在内存中的数据就会丢失。所以需要定期将内存中的数据同步到硬盘文件来保证数据的持久化。当Redis重启时,通过加载硬盘文件到内存,就能达到恢复数据的目的。

redis主要有两种持久化的手段:

  • AOF (append only file, 只追加文件)
  • RDB (Redis DataBase, 基于Redis数据库)

AOF

(1)概念

AOF就是append only file, 只追加文件。通过日志的方式记录Redis的每个“写”命令。

在Redis服务重启时,重新加载执行日志文件中的命令,从而达到恢复数据的效果。

(2)应用场景

优点:稳定、数据完整性好;

适合场景:数据安全性要求高(订单、购物车)

缺点:AOF文件会越来越大,比较冗余

RDB

(1)概念

RDB就是Redis Databse,基于Redis的数据库文件。这是指在一定的时间间隔内,将内存中的键值对数据以数据集快照的形式写入磁盘。也就是保存某个时间点的快照,这个快照文件就是RDB。

Redis定时执行一个子进程,这个子进程将数据写到RDB临时文件,等待临时文件写完,就将临时文件替换RDB文件。

(2)应用场景

优点:恢复大数据集速度比AOF快

适合场景:数据备份

缺点:基于数据备份的方式会丢失上次备份与服务宕机时间点之间的数据

实际 使用通常都是结合两者来。

Redis集群/高可用

为什么要集群?因为单台服务器部署的Redis服务不能满足高并发压力,且服务稳定性也无法得到保障(单服务器挂之后,整个系统可能就会崩溃)。

主从模式

主从模式,就是一台Redis服务器作为主服务器,同时有若干台服务器作为从服务器。主从服务器通常采取读写分离的策略,主服务器可以进行读写操作,从服务器只读。

也就是说:数据修改只在主服务器上,然后主服务器将最新数据同步给从服务器。从节点的数据来自主节点,实现原理就是主从复制机制。主从复制包括全量复制,增量复制两种。

(1)全量复制

一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制

1
2
3
4
5
6
7
8
1.slave发送sync命令到master。
2.master接收到SYNC命令后,执行bgsave命令,生成RDB全量文件。
3.master使用缓冲区,记录RDB快照生成期间的所有写命令。
4.master执行完bgsave后,向所有slave发送RDB快照文件。
5.slave收到RDB快照文件后,载入、解析收到的快照。
6.master使用缓冲区,记录RDB同步期间生成的所有写的命令。
7.master快照发送完毕后,开始向slave发送缓冲区中的写命令;
8.salve接受命令请求,并执行来自master缓冲区的写命令

(2)增量复制

slave与master同步之后,master上的数据,如果再次发生更新,就会触发增量复制

master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。

(3)主从模式存在的问题

  • 数据不一致

客户端可能读到落后的副本,进而读到不一致的数据。

  • 容错

主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。

哨兵模式

哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点

但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。

  • 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;

  • 哨兵之间还会相互监控。

Cluster集群

Cluster集群实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。

Hash Slot插槽算法

使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384($2^{14}$)取模。

集群中的每个节点负责一部分的hash槽,比如当前集群有A、B、C个节点,每个节点上的哈希槽数 =16384/3,那么就有:

  • 节点A负责0~5460号哈希槽
  • 节点B负责5461~10922号哈希槽
  • 节点C负责10923~16383号哈希槽

Redis 大Key

通常以Key的大小和Key中成员的数量来综合判定大Key:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。

大Key带来的常见问题

  • 客户端执行命令的时长变慢。
  • Redis内存达到maxmemory参数定义的上限引发操作阻塞或重要的Key被逐出,甚至引发内存溢出(Out Of Memory)。
  • 集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。
  • 对大Key执行读请求,会使Redis实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务。
  • 对大Key执行删除操作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换。

大key的常见处理办法:

  • string类型 压缩处理 进行序列化压缩,代价是反序列化读取;

  • list/set类型 分片处理 进行分片、拆分;

  • 对大Key进行清理(删除)

    将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。

说明

  • Redis 4.0及之后版本:您可以通过UNLINK命令安全地删除大Key甚至特大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
  • Redis 4.0之前的版本:建议先通过SCAN命令读取部分数据,然后进行删除,避免一次性删除大量key导致Redis阻塞。

Redis 热点Key

阿里云手册关于热点key问题的链接

什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。

而热点Key是怎么产生的呢?主要原因有两个:

  • 用户消费的数据远大于生产的数据:如秒杀、热点新闻等读多写少的场景。

  • 请求分片集中,超过单Redi服务器的性能:比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。

怎么解决热点key问题?

  • 在Redis集群架构中对热Key进行复制,将热点数据分散存储在多个Redis节点上

    例如将热Key foo复制出3个内容完全一样的Key并名为foo2、foo3、foo4,将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。

    该方案的缺点在于需要联动修改代码,同时带来了数据一致性的挑战(由原来更新一个Key演变为需要更新多个Key),仅建议该方案用来解决临时棘手的问题。

  • 使用二级缓存(即JVM本地缓存+Redis缓存,减少Redis读请求)

  • 限制流量

限流是通过控制请求的速率来防止系统过载。在应用层实现限流,可以有效减轻热点Key对Redis的压力。常见的限流算法有漏桶算法和令牌桶算法。

作者

Desirer

发布于

2023-12-13

更新于

2024-11-15

许可协议