redis面试总结

什么是redis? #

redis是一个高性能的key-value数据库,它是完全开源免费的,而且redis是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库

Reids的特点 #

Redis本质上是一个Key-Value类型的内存数据库,很像memcached(一个高性能的分布式内存缓存系统),整个数据库统统加载在内存当中进行操作定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。

Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能,比方说用他的List来做FIFO双向链表,实现一个轻量级的高性 能消息队列服务,用他的Set可以做高性能的tag系统等等。另外Redis也可以对存入的Key-Value设置expire时间,因此也可以被当作一 个功能加强版的memcached来用。

Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

Redis是AP的还是CP的? #

CAP理论:分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)三项中的两项。

AP

Redis的设计目标是高性能、高可扩展性和高可用性,Redis的一致性模型是最终一致性,即在某个时间节点的数据可能并不是最新的,但最终会达到一致性的状态。

Reids没办法保证强一致性的主要原因是,因为它的分布式设计中采用的是异步复制,这导致在节点之间的数据同步延迟和不一致的可能性。

也就是说,当某个节点上的数据发生改变时,Redis会将这个修改操作发送给其他节点进行同步,但由于网络传输的延迟等原因,这些操作不一定会立即被其他节点接收到和执行,这就可能导致节点之间存在数据不一致的情况。

Reids的集群模式 #

主从模式 #

这个模式主要为了解决单节点故障问题

主从模式包括一个主节点和多个从节点。主节点负责处理写操作和读操作,从节点则复制主节点的数据,并且只能处理读操作。当主节点发生故障时,可以将一个从节点升级为主节点,实现故障转移(需要手动实现)。

优势:简单易用,适合读多写少的场景

缺点:不具备故障自动转移能力,没办法做容错和恢复。主节点宕机若数据没有及时复制到从节点,会导致数据丢失。

哨兵模式 #

为了解决主从模式无法自动容错及恢复问题,引入哨兵模式。

哨兵模式是在主从模式的基础上加入哨兵节点。哨兵节点是一种特殊的Redis节点,用于监控主节点和从节点的状态。当主节点发生故障时,哨兵节点可以自动进行故障转移,选择一个合适的从节点升级为主节点,并通知其他从节点和应用程序进行更新。

通常需要不俗多个哨兵节点,以确保故障转移的可靠性。

哨兵节点定期发送ping命令到主节点和从节点,当指定时间内为收到相应,则哨兵节点会将该节点标记为主管下线。如果一个主节点被多数哨兵标记为主观下线,则认为它客观下线。挂了

此时,哨兵节点触发故障转移,从所有健康从节点中选择一个新的主节点,并将所有从节点切换到新的主节点,实现故障自动转移。同时,哨兵节点会更新所有客户端的配置,指向新的主节点。

Cluster模式 #

它将数据自动分片到多个节点上,每个节点只负责一部分数据。

Redis Cluster采用主从复制模式来提高可用性。每个分片都有一个主节点和多个从节点。主节点负责处理写操作,而从节点负责复制主节点的数据并处理读请求。

Redis Cluster能够自动检测节点的故障。当一个主节点失去连接或不可达时,Redis Cluster会尝试将该节点标记为不可用,并从可用的从节点中提升一个新的主节点。

Redis Cluster是适用于大规模应用的解决方案,它提供了更好的横向扩展和容错能力。它自动管理数据分片和故障转移,减少了运维的负担。

Cluster模式的特点是数据分片存储在不同的节点上,每个节点都可以单独对外提供读写服务。不存在单点故障的问题。

Redis Cluster中使用事务和lua有什么限制? #

**在 Redis Cluster 中,事务不能跨多个节点执行。**事务中涉及的所有键必须位于同一节点上。如果尝试在一个事务中包含多个分片的键,事务将失败。另外,对 WATCH 命令也用同样的限制,要求他只能监视位于同一分片上的键。

**执行 Lua 脚本时,脚本中访问的所有键也必须位于同一节点。**Redis 不会在节点之间迁移数据来支持跨节点的脚本执行。

RDB和AOD的写回策略分别是什么? #

写回策略是指将数据从内存写入到持久化存储(如磁盘)的方式和时机。在Redis中,不同的持久化机制有着不同的写回策略。

RDB #

  • 定时出发

    Redis通过配置文件中 save 参数定义了 RDB 的自动保存条件。

    Redis会定期检查这些条件,如果满足,触发 RDB 的保存操作。

  • 手动触发

    我们可以通过以下命令手动生成 RDB 文件:

    • SAVE:会阻塞 Redis 服务器,直到快照完成。
    • BGSAVE:在后台异步生成 RDB 文件,不会阻塞 Redis。

AOF #

AOF有三种数据写回策略,分别是Always,Everysec和No。

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

Redis能完全保证数据不丢失吗? #

不能

  • 磁盘和系统故障
  • 操作系统缓冲区:即使Redis请求立即将数据同步到磁盘,操作系统的I/O缓冲区可能会导致实际写入磁盘的操作延迟发生。如果在写入缓冲区之后,没写磁盘前,机器挂了,那么数据就丢了。
  • 磁盘写入延迟

Redis的事务和MySQL的事务区别? #

Redis中提供了一种简单的事务机制,通过 MULTI、EXEC、DISCARD 和 WATCH 命令来实现的。他可以保证一组命令按顺序执行,中间不会被其他客户端的命令打断。

但是,Redis 的事务不支持回滚,**一旦事务中的某条命令出错,其他命令仍会继续执行。**而且Redis的事务也没有隔离级别的定义,本身是单线程的,也没啥隔离的必要了。

Redis的事务的好处是,简单,轻量,不需要复杂的事务管理,之所以这么设计,是因为Redis天生就是一个缓存框架,他的目的就是为了性能。所以他放弃了数据的强一致性,而选择了优先保性能!

Redis中的setnx和setex有什么区别? #

SETNX ,SET if Not eXists , 只有键不存在时才设置值

  • 不能设置过期时间

SETEX , SET with EXpiration, 设置值并指定过期时间

  • 无条件进行设置,并带有过期时间

是否使用过Redis集群,集群的原理是什么? #

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

是Redis的数据分片? #

是一种将一个Redis数据集分割成多个部分,分别存储在不同的Redis节点上的技术。从而提高Redis集群的性能和可扩展性。

通常是按照Key的hash值分配到不同的节点上。先计算key应该存储在那个节点上,然后到该节点进行操作。

**在Cluster集群模式中,使用哈希槽(hash slot)的方式进行数据分片。**将整个数据集划分为多个槽,每个槽分配给一个节点。

Redis Cluster将整个数据集划分为16384个槽,每个槽都有一个编号(0~16383),集群的每个节点可以负责多个hash槽,客户端访问数据时,先根据key计算出对应的槽编号,然后根据槽编号找到负责该槽的节点,向该节点发送请求。

2^14=16384 权衡利弊后的结果,2kb

在 Redis 的每一个节点上,都有这么两个东西,一个是槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们在存取的 Key 的时候,Redis 会根据 CRC16 算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

特点: #

  • 提高性能和吞吐量
  • 提高可扩展性
  • 更好的资源利用
  • 避免单节点故障
  • 数据冗余和高可用性

Redis使用什么协议进行通信? #

RESP协议,自己设计的,基于TCP协议,简单高效、易于解析。

Redis为什么这么快? #

  • 基于内存,数据存储在内存中,数据的读写速度非常快。

  • 单线程模式:它所有的操作都是在一个线程内完成,不需要进行线程切换和上下文切换。

  • 多路复用I/O模型:在单线程基础上使用了I/O多路复用技术,实现了单个线程同时处理多个客户端连接的能力,从而提高了Redis的并发性能。

    多路复用 I/O(I/O Multiplexing)是一种让单个线程可以同时监听多个文件描述符(FD) 的 I/O 模型,核心是 “一个线程管多个连接”,而非 “一个连接一个线程”。

  • 高效的数据结构:哈希表、有序集合、无序集合、列表、string。

  • 多线程的引入:6.0引入多线程进一步提升I/O性能。

使用redis有哪些好处? #

3.1 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

3.2 支持丰富数据类型,支持string,list,set,sorted set,hash

String #

常用命令 :set/get/decr/incr/mget等;

应用场景 :String是最常用的一种数据类型,普通的key/value存储都可以归为此类;

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

Hash #

常用命令 :hget/hset/hgetall等

应用场景 :我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;

实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field),也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时redisObject的encoding字段为int。

List #

常用命令 :lpush/rpush/lpop/rpop/lrange等;

应用场景 :Redis list的应用场景 非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;

实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

Set #

常用命令 :sadd/spop/smembers/sunion等;

应用场景 :Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;

实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

Sorted Set 有序集合 #

常用命令 :zadd/zrange/zrem/zcard等;

应用场景 :Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

如何实现的? #

Redis中的ZSet在实现中,有多种结构,大类分两种,分别是ziplist(压缩列表)和skiplist(跳跃表),5.0版本新增了一个listpack(紧凑列表)的数据结构,这种数据结构就是为了代替ziplist的,而在7.0之后就彻底不使用ziplist了。

跳表(Skip List)的核心底层结构确实是链表,但它是一种经过优化和增强的多层有序链表,不能简单等同于普通的单链表 / 双向链表。

当ZSet的元素数量比较少时,Redis会采用ZipList(ListPack)来存储ZSet的数据。ZipList(ListPack)是一种紧凑的列表结构,它通过连续存储元素来节约内存空间。当ZSet的元素数量增多时,Redis会自动将ZipList(ListPack)转换为SkipList,以保持元素的有序性和支持范围查询操作。

而ZipList是一个压缩的数据结构,它的每个元素都是连续存储的,因此内存的使用非常紧凑。与其他数据结构相比,ZipList在小规模数据存储时显著减少了内存占用。

但是ZipList也有缺点,正因为他是紧凑的线性结构,所以如果在 ZipList 中查找一个元素时,可能需要遍历整个列表,同理插入和删除操作也是线性的。所以ZipList的插入、删除和查找操作的时间复杂度通常是 O(N)。相对来说是比较慢的。

当元素数量少于128,每个元素的长度都小于64字节的时候,使用ZipList(ListPack),否则,使用SkipList!

在这个过程中,Redis会遍历ZipList(ListPack)中的所有元素,按照元素的分数值依次将它们插入到SkipList中,这样就可以保持元素的有序性。

其中,SkipList用来实现有序集合,其中每个元素按照其分值大小在跳表中进行排序。跳表的插入、删除和查找操作的时间复杂度都是O(log n),可以保证较好的性能。

dict用来实现元素到分值的映射关系,其中元素作为键,分值作为值。哈希表的插入、删除和查找操作的时间复杂度都是O(1),具有非常高的性能。

redis相比memcached有哪些优势? #

memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

redis的速度比memcached快很多 (3) redis可以持久化其数据

Memcache与Redis的区别都有哪些? #

存储方式

Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis有部份存在硬盘上,这样能保证数据的持久性。

数据支持类型

Memcache对数据类型支持相对简单。Redis有复杂的数据类型。

使用底层模型不同

它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

Redis为什么要自己定义SDS? #

SDS即简单动态字符串

Redis本身用C语言实现,却没有使用C语言点字符串。

  • 字符串在Redis中使用广泛,要求支持任意字符的存储,要求各种操作都高效。C的不适合。

除了做缓存,Redis还能用来干什么? #

  • 消息队列(不建议):Redis 支持发布/订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务或处理高并发请求。

  • 延迟消息(不建议):Redis的ZSET可以用来实现延迟消息,也可以基于Key的过期消息实现延迟消息,还可以借助Redisson的RDelayQueue来实现延迟消息,都是可以的。

  • 排行榜(建议):利用Redis 的有序集合和列表结构,可以成为设计实时排行榜的绝佳选择,例如各类热门排行榜、热门商品列表等。

  • 计数器(建议):基于Redis可以实现一些计数器的功能,比如网站的访问量、朋友圈点赞等。通过 incr 命令就能实现原子性的自增操作,从而实现一个全局计数器。·

  • 分布式ID(可以):因为他有全局自增计数的功能,所以在分布式场景,我们也可以利用Redis来实现一个分布式ID来保障全局的唯一且自增。

  • 分布式锁(建议):Redis 的单线程特性可以保证多个客户端之间对同一把锁的操作是原子性的,可以轻松实现分布式锁,用于控制多个进程对共享资源的访问。

  • 地理位置应用(建议):Redis 支持GEO,支持地理位置定位和查询,可以存储地理位置信息并通过 Redis 的查询功能获取附近的位置信息。比如"附近的人"用它来实现就非常方便。

  • 分布式限流(可以):Redis提供了令牌桶和漏桶算法的实现,可以用于实现分布式限流。

    Redis 实现限流主要方式:

    • 固定窗口限流:统计单位时间内(比如 1 秒)的请求数,超过阈值则拒绝;
    • 滑动窗口限流:更精准,把单位时间拆分成多个小窗口,计算滑动窗口内的总请求数;
    • 令牌桶算法:Redis 存储令牌数,请求时先取令牌,无令牌则拒绝(适合突发流量)。
  • 分布式Session(建议):可以使用Redis实现分布式Session管理,保证多台服务器之间用户的会话状态同步。

    传统 Session 存在单节点内存中,多节点部署时会话不共享。Redis 实现分布式 Session 的核心:

    • 生成唯一 SessionID(比如 UUID),作为 Cookie 返回给客户端;
    • 把 Session 数据(用户 ID、登录状态、过期时间)存入 Redis,Key 为session:{sessionID};
    • 客户端每次请求携带 SessionID,服务端从 Redis 读取 Session 数据。
  • 布隆过滤器(建议):Redis提供了布隆过滤器(Bloom Filter)数据结构的实现,可以高效地检测一个元素是否存在于一个集合中

  • 状态统计(数据量大建议用):Redis中支持BitMap这种数据结构,它不仅查询和存储高效,更能节省很多空间,所以我们可以借助他做状态统计,比如记录亿级用户的登录状态,或者是拿他来做签到统计也比较常见。

  • 共同关注(建议):Redis中支持Set集合类型,这个类型非常适合我们做一些取并集、交集、差集等,基于这个特性,我们就能取交集的方式非常方便的实现共同好友、或者共同关注的功能。

  • 推荐关注(可以):和上面的共同关注类似,交集实现共同好友,那么并集或者差集就能实现推荐关注的功能。

7、redis的缓存失效策略和主键失效机制 #

作为缓存系统都要定期清理无效数据,就需要一个主键失效和淘汰策略.

在Redis当中,有生存期的key被称为volatile。在创建缓存时,要为给定的key设置生存期,当key过期的时候(生存期为0),它可能会被删除。

1、影响生存时间的一些操作

生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆盖原来的数据,也就是说,修改key对应的value和使用另外相同的key和value来覆盖以后,当前数据的生存时间不同。

比如说,对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间。另一方面,如果使用RENAME对一个 key 进行改名,那么改名后的 key的生存时间和改名前一样。

RENAME命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此,新的 another_key 的生存时间也和原本的 key 一样。使用PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key 。

2、如何更新生存时间

可以对一个已经带有生存时间的 key 执行EXPIRE命令,新指定的生存时间会取代旧的生存时间。过期时间的精度已经被控制在1ms之内,主键失效的时间复杂度是O(1),

EXPIRE和TTL命令搭配使用,TTL可以查看key的当前生存时间。设置成功返回 1;当 key 不存在或者不能为 key 设置生存时间时,返回 0 。

最大缓存配置 在 redis 中,允许用户设置最大使用内存大小 server.maxmemory 默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。redis 提供 6种数据淘汰策略:

volatile-lru: 从已设置过期时间的数据集( server.db\[i\].expires)中挑选最近最少使用的数据淘汰

volatile-ttl: 从已设置过期时间的数据集( server.db\[i\].expires)中挑选将要过期的数据淘汰

volatile-random: 从已设置过期时间的数据集( server.db\[i\].expires)中任意选择数据淘汰

allkeys-lru: 从数据集( server.db\[i\].dict)中挑选最近最少使用的数据淘汰

allkeys-random: 从数据集( server.db\[i\].dict)中任意选择数据淘汰

no-enviction(驱逐): 禁止驱逐数据

注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。

使用策略规则:

1、 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru2、 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random

三种数据淘汰策略:

ttl和random比较容易理解,实现也会比较简单。主要是Lru最近最少使用淘汰策略,设计上会对key 按失效时间排序,然后取最先失效的key进行淘汰

8、为什么redis需要把所有数据放到内存中? #

Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。

如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

Redis是单进程单线程的 #

redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销

Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。

为什么被设计成单线程? #

**主要是因为,Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。**所以没有必要

**多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。**Redis不需要提升CPU利用率

为什么Redis的单线程这么快? #

多路复用 #

这里先讲讲Linux多路复用技术,就是多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。

为什么Redis6.0引入了多线程? #

只是针对网络请求过程引入了多线程,数据操作还是单线程

随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的 QPS。为了提升QPS,很多公司的做法是部署Redis集群,并且尽可能提升Redis机器数。但是这种做法的资源消耗是巨大的。

如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。

为什么Lua脚本可以保证原子性? #

原子性在并发编程中,和在数据库中是两种不同的概念。

在数据库中,原子性代表要么执行,要么会滚

在编程中,原子性指操作不可拆分,不被中断

Lua脚本可以保证原子性,因为Redis是单线程执行命令的,当客户端提交一个Lua脚本的时候,Redis会把他当做一个命令来执行,在这个脚本执行期间,不会有其他的命令插入进来执行。

因此,Redis保证了编程的原子性,没有保证数据库的原子性

Lua语言:轻量小巧的脚本语言

  • 高效性
  • 简单性
  • 可移植性
  • 灵活性
  • 安全性

Redis中的setnx命令为什么是原子性的? #

SETNXSET if Not Exists 的缩写,直译就是 “当键不存在时才设置值”。它是 Redis 中一个原子性命令,核心逻辑是:

  • 如果指定的 key 不存在,则为该 key 设置指定的 value,返回 1
  • 如果指定的 key 已经存在,则不做任何操作,返回 0

它利用了Redis单线程的特点,所有的命令都是在主线程中顺序执行的,这意味着每个命令在执行时不会被其他命令打断。

Redis 5.0中的Stream是什么? #

Redis Stream 是 Redis 5.0 版本引入的一种持久化的、有序的、可追加的消息流数据结构,专门用于处理 “消息队列” 类场景。你可以把它理解成一个:

  • 按时间戳(精确到毫秒)有序排列的 “消息日志”;
  • 支持多消费者组、消息确认、消息回溯的高性能消息队列;
  • 数据持久化(重启 Redis 不丢失),且支持按 ID 精准定位消息。

Stream 的核心特性 #

相比 Redis 其他 “伪消息队列” 方案(如 List + BRPOP),Stream 具备更完善的消息队列能力:

特性 说明
有序性 每个消息有唯一的 ID(格式:时间戳-序列号,如 1719820000000-0),按 ID 递增排列
持久化 消息写入后持久化到 RDB/AOF,重启不丢失
消费者组(Consumer Group) 支持多组消费者独立消费,每组消费进度互不干扰(核心特性)
消息确认(ACK) 消费者获取消息后需确认,未确认的消息会被标记为 “待处理”,支持重试
消息回溯 可通过消息 ID 读取历史消息,支持 “从头读”“从指定位置读”“读新消息”
阻塞读取 支持阻塞等待新消息(类似 Kafka 的 consume)

Redis如何实现发布/订阅? #

即在Redis中定义频道,客户端可以订阅一个或多个频道并接收它们所发布的消息。发布者向一个或多个频道发布消息,所有订阅该频道的客户端都会收到该消息。

Redis的发布/订阅模式一般用于实时消息传递和事件驱动的应用程序中,例如:

  • 即时通讯:发布/订阅模式可以用于实现即时消息传递应用程序,例如聊天室或社交媒体应用程序。订阅者可以订阅特定频道以接收他们感兴趣的消息,并能够实时更新。
  • 日志处理:发布/订阅模式可以用于日志处理应用程序,例如日志聚合或日志监控系统。订阅者可以订阅特定频道以接收他们感兴趣的日志消息,例如错误或异常消息,并能够实时更新。
  • 实时数据更新:发布/订阅模式可以用于实时数据更新应用程序,例如股票市场或在线游戏。订阅者可以订阅特定频道以接收他们感兴趣的实时数据更新,并能够实时更新。
  • 缓存刷新:发布/订阅模式可以用于缓存刷新应用程序,例如缓存的数据过期时自动更新。当数据被更新时,发布者将消息发布到特定频道,订阅者将接收到消息并更新其本地缓存。

Redis的发布/订阅模式有以下优点和缺点:

优点:

  • 实时性高:发布/订阅模式可以实现实时消息传递,能够提高应用程序的实时性和响应速度。
  • 灵活性高:发布/订阅模式可以根据需要订阅特定频道,订阅者只会接收他们感兴趣的消息,从而提高了灵活性。
  • 可扩展性高:发布/订阅模式能够支持多个订阅者同时订阅特定频道,从而提高了可扩展性。

缺点:

  • 可靠性低:发布/订阅模式是一种异步通信方式,发布者不会等待订阅者接收到消息,因此消息的可靠性可能会受到影响。
  • 可靠性难以保证:发布/订阅模式在传输过程中可能会出现消息丢失的情况,尤其是在高负载情况下。
  • 不适合高频次的请求:在高频次的请求场景下,发布/订阅模式可能会对性能造成影响,因为每个订阅者都需要对每个发布的消息进行处理。

Redis的持久化机制是怎么样的? #

RDB #

RDB是将Redis的内存中的数据定期存储到磁盘上,以防止数据在Redis进程异常退出或服务器断电等情况下丢失。

优点:快照文件小,恢复速度快,适合做备份和灾难恢复

缺点:定期更新可能会丢失数据

AOF #

AOF是将Redis的所有写操作追加到AOF文件(Append Only File)的末尾,从而记录了Redis服务器运行期间所有修改操作的详细记录。当Redis重新启动时,可以通过执行AOF文件中保存的写操作来恢复数据。

AOF的优点是:可以实现更高的数据可靠性、支持更细粒度的数据恢复,适合做数据存档和数据备份。

AOF的缺点是:文件大占用空间更多,每次写操作都需要写磁盘导致负载较高

混合持久化 #

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

11、redis常见性能问题和解决方案 #

11.1 Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。

11.2 Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久

化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

11.3 Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

11.4 Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。

redis事物的了解CAS(check-and-set 操作实现乐观锁 )? #

和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI/EXEC/DISCARD/WATCH这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出

  • MULTI:标记一个事务块的开始。
  • DISCARD:取消事务,放弃执行事务块内的所有命令。
  • EXEC:执行所有事务块内的命令。
  • UNWATCH:取消 WATCH 命令对所有 key 的监视。
  • WATCH key [key …]:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

Reids中事务不支持回滚

Redis中事务的实现特征:

在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。

和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。

在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。

使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。

Redis的过期策略是怎么样的? #

通过设置过期时间来控制键值对的生命周期。过期时间可以通过EXPIRE、EXPIREAT、PERSIST等命令设置。

Redis的过期策略采用的是定期删除和惰性删除相结合的方式。

惰性删除 #

当key过期后,不会立即删除,而是在访问这个key的时候会触发删除操作。先检查是否设置了过期时间,再检查是否过期。

可能存在内存泄露,如果永远不被访问。

定期删除 #

每过100ms随机抽取一些设置了过期时间的key,定期删除。

默认开启定期删除和惰性删除两种策略

内存释放? #

Redis有内存回收操作,不代表删除的内存立马会被回收。

  • 缓存与池化:当Reids释放(删除)一个键值对时,它只是把这部分内存归还给了它内部的内存分配器。分配器会保留这块内存,并将其放入空闲的内存池中,目的是为了在未来Redis需要分配新的内存时,可以快速地从池中取出复用,而无需每次都向操作系统申请(系统调用是昂贵的)。
  • 碎片化:频繁的分配和释放不同大小的内存块,会导致内存碎片。即使有足够的总空闲内存,也可能因为找不到一块连续的、足够大的内存来满足新的大对象分配请求,从而导致实质上的内存浪费。
  • 归还OS的延迟与不彻底:内存分配器通常不会每次释放内存都立即munmap(解除内存映射)还给操作系统。它只会在满足特定条件时,比如空闲内存块非常大,或者总空闲内存超过某个阈值时,才会将部分内存归还给OS。这是一个权衡,目的是为了性能。

Redis的内存淘汰策略是怎么样的? #

Redis 的内存淘汰策略用于在内存满了之后,决定哪些 key 要被删除。Redis 支持多种内存淘汰策略,可以通过配置文件中的 maxmemory-policy 参数来指定。

  • noeviction:不会淘汰任何键值对,而是直接返回错误信息。
  • allkeys-lru:从所有 key 中选择最近最少使用的那个 key 并删除。
  • volatile-lru:从设置了过期时间的 key 中选择最近最少使用的那个 key 并删除。
  • allkeys-random:从所有 key 中随机选择一个 key 并删除。
  • volatile-random:从设置了过期时间的 key 中随机选择一个 key 并删除。
  • volatile-ttl:从设置了过期时间的 key 中选择剩余时间最短的 key 并删除。
  • volatile-lfu:淘汰的对象是带有过期时间的键值对中,访问频率最低的那个。
  • allkeys-lfu:淘汰的对象则是所有键值对中,访问频率最低的那个。

如果有大量的key需要设置同一时间过期,一般需要注意什么? #

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

什么是热Key问题,如何解决热Key问题? #

如果在同一时间点上,Redis中的同一个key被大量访问,就会导致流量过于集中,使得很多物理资源无法支撑,如网络带宽、物理存储空间、数据库连接等。

  • 事前预测

    比如秒杀

  • 事中解决

    多级缓存、热key备份、限流等

    • 热点key拆分

      热点key后面加后缀名,A_01,A_02….,根据相应规则算出一个下标,再访问

什么是大key问题,如何解决? #

不仅仅代表key的值很大, 还代表value占用大空间很大

  • 影响性能
  • 占用内存
  • 内存空间不均匀
  • 影响redis备份恢复
  • 搜索困难
  • 迁移困难
  • 过期执行耗时

处理 #

  • 有选择的删除
  • 设置缓存删除
  • 拆分
  • 部分迁移

什么是缓存击穿、缓存穿透、缓存雪崩? #

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

什么情况下会出现数据库和缓存不一致的问题? #

不并发情况下,缓存与数据库存在时间差很正常

并发情况下: #

如果在并发场景中,如果两个线程,同时进行先写数据库,后更新缓存的操作,就可能会出现不一致:

W W
写数据库,更新成20
写数据库,更新成10
写缓存,更新成10
写缓存,更新成20(数据不一致)

如果在并发场景中,如果两个线程,同时进行先更新缓存,后写数据库的操作,同理,也可能会出现不一致:

W W
写缓存,更新成20
写缓存,更新成10
写数据库,更新成10
写数据库,更新成20(数据不一致)

在并发场景中,还有一种容易忽略的并发场景,那就是读写并发。

我们知道,当我们使用了缓存之后,一个读的线程在查询数据的过程是这样的:

1、查询缓存,如果缓存中有值,则直接返回

2、查询数据库

3、把数据库的查询结果更新到缓存中

所以,对于一个读线程来说,虽然不会写数据库,但是是会更新缓存的,所以,在一些特殊的并发场景中,就会导致数据不一致的情况。

读写并发的时序如下:

W R
读缓存,缓存中没有值
读数据库,数据库中得到结果为10
写数据库和缓存,更新成20
写缓存,更新成10(数据不一致)

也就是说,假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询,但是如果自查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就导致最终缓存会被重新用一个”旧值”覆盖掉。

这也就导致了缓存和数据库的不一致的现象。

但是这种现象其实发生的概率比较低,因为一般一个读操作是很快的,数据库+缓存的读操作基本在十几毫秒左右就可以完成了。

而在这期间,刚好另一个线程执行了一个比较耗时的写操作的概率确实比较低。

当然,根据墨菲定律,只要有可能发生的事情,就一定会发生。

如何解决Redis和数据库一致性问题? #

  • 先更新数据库,再删除缓存
  • 延迟双删:先删除缓存,再更新数据库,再删一次缓存
  • cache-aside:更新数据库,基于binlog监听进行缓存删除
方案 优点 缺点 适合场景
先更新数据库,后删除缓存 简单 删缓存失败,会导致数据不一致 95%一般场景都适合,尤其是并发量不大,或者对一致性要求不太高的。
延迟双删 数据一致性保证更好 延迟时间不好控制,太短了没用,太长了也会导致不一致时间长 对一致性要求高,并发量大的场景。
cache-aside 解耦、一致性有保障 复杂,需要引入新的组件 适合大厂,有完善的中间件支持,并发高,一致性要求高的场景。

为什么是删除缓存,而不是更新 #

  • 简单
  • 更新缓存,比删除操作过程更复杂,也容易出错
  • 如果是写写并发, 更新缓存很容易造成数据不一致

先写数据库还是先删缓存 #

  • 先删缓存

    先删除缓存后写数据库的这种方式,会无形中放大"读写并发"导致的数据不一致的问题。我们知道,当我们使用了缓存之后,一个读的线程在查询数据的过程是这样的:

    1、查询缓存,如果缓存中有值,则直接返回

    2、查询数据库

    3、把数据库的查询结果更新到缓存中

    所以,对于一个读线程来说,虽然不会写数据库,但是是会更新缓存的,所以,在一些特殊的并发场景中,就会导致数据不一致的情况。

    读写并发的时序如下:

    W R
    删除缓存
    读缓存,缓存中没有值
    读数据库,数据库中得到结果为10
    更新数据库,更新成20
    写缓存,更新成10(数据不一致)

    也就是说,假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询,但是如果自查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就导致最终缓存会被重新用一个"旧值"覆盖掉。

    这也就导致了缓存和数据库的不一致的现象。

    因为这种"读写并发"问题发生的前提是读线程读缓存没读到值,而先删缓存的动作一旦发生,刚好可以让读线程就从缓存中读不到值。

    所以,本来一个小概率会发生的"读写并发"问题,在先删缓存的过程中,问题发生的概率会被放大。

    而且这种问题的后果也比较严重,那就是缓存中的值一直是错的,就会导致后续的所有命中缓存的查询结果都是错的!

  • 先写数据库

    如果我们先更新数据库,再删除缓存,有一个好处,那就是缓存删除失败的概率还是比较低的,除非是网络问题或者缓存服务器宕机的问题,否则大部分情况都是可以成功的。

    并且这个方案还有一个好处,那就是数据库是作为持久层存储的,先更新数据库就能确保数据先写入持久层可以保证数据的可靠性和一致性,即使在删除缓存失败的情况下,数据库中已有最新数据。

    但是这个方案也存在一个问题,那就是先写数据库,后删除缓存,如果第二步失败了,会导致数据库中的数据已经更新,但是缓存还是旧数据,导致数据不一致。

    那么怎么解决呢?

为什么需要延迟双删,删两次的原因是什么? #

  • 先删缓存

    因为先写数据库存在一个关键的问题是,那就是缓存的更新和数据库的更新不是一个原子性操作,存在失败可能。

    如果写数据库成功了,但缓存删失败了,数据会不一致。

    若先删,后面更新失败了,无所谓,删数据库的失败概率更大。

  • 再更新数据库

  • 再删缓存

    为了防止并发读写时,另一个线程把脏数据又写入缓存,一般设置1-2秒

有了第二次删除,第一次还有意义吗? #

如果不要第一次删除,只保留第二次删除那么就这个流程就变成了:

1、更新数据库2、删除缓存

那么这个方案的缺点前面讲过了,一旦删除缓存失败,就会导致数据不一致的问题。

那么有人又问了:延迟双删的第二次删除不也一样可能失败么?

没错,确实第二次删除也还是有概率失败,但是因为我们在延迟双删的方案中先做了一次删除,而延迟双删的第二次删除只为了尝试解决 因为读写并发导致的不一致问题,或者说尽可能降低这种情况发生的概率。

而如果没有第一次删除,只靠第二次删除,那么第二次删除要解决的可就不只是读写并发情况下的不一致问题了,即使没有并发,第二次只要删除失败,就会存在缓存的不一致问题。所以,第一次删除的目的就是降低不一致的发生的概率。

Redis如何实现延迟消息? #

zset(有序集合)实现延迟消息 #

zset是一个有序集合,每一个元素(member)都关联了一个 score,可以通过 score 排序来取集合中的值。

我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为 score 和 member。这样redis会对zset按照score延时时间进行排序。然后我们再开启redis扫描任务,获取”当前时间 > score”的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。

使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是在高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用加分布式锁解决,但是这样做也会降低吞吐型。

但是,在大多数业务场景下,如果幂等性做得好的,多个消费者取到同一个订单号也无妨。

Redission实现延迟消息 #

在 Go 语言中用 Redisson(准确说是 Redisson 的 Go 版本 redisson-go)实现延迟消息,核心是利用 Redis 的 Sorted Set(有序集合)+ 定时轮询 / 阻塞查询 机制,Redisson 已经封装好了现成的 RDelayedQueue(延迟队列),不用自己造轮子,开箱即用。

为什么ZSet既能支持高效的范围查询,还能以O(1)复杂度获取元素权重 #

是因为它的核心数据结构设计采用了跳表,而它又能O(1)的复杂度获取元素权重,这是因为它同时采用了哈希表进行索引。

typedef struct zset 
{ 
   dict *dict; 
   zskiplist *zsl;

} zset;

以上是zset的数据结构,其中包含了两个成员,分别是哈希表dict和跳表zsl。

dict存储 member->score 之间的映射关系,所以 ZSCORE 的时间复杂度为 O(1)。skiplist 是一个「有序链表 + 多层索引」的结构,查询元素的复杂度是 O(logN),所以他的查询效率很高。

Redis操作最佳实践 #

  • 避免使用 KEYS 命令获取所有 key,因为该命令会遍历所有 key,可能会阻塞 Redis 的主线程。
  • 避免使用 FLUSHALL 或 FLUSHDB 命令清空 Redis 数据库,因为这会清空所有数据库中的数据,而不仅仅是当前数据库。
  • 避免在 Redis 中存储大的数据块,因为这会导致 Redis 实例内存占用过高,影响 Redis 的性能。
  • 合理设置过期时间,避免过期时间设置过短或过长,导致 Redis 实例内存占用过高或数据过期失效时间不准确。
  • 对于写入操作频繁的数据,考虑使用 Redis 的持久化机制进行数据持久化,以保证数据的可靠性。
  • 避免使用 Lua 脚本中的无限循环,因为这会导致 Redis 的主线程被阻塞。
  • 对于需要频繁更新的数据,可以使用 Redis 的 Hash 数据结构,以减少 Redis 实例的内存占用和网络传输数据量。因为Hash可以做部分更新。
  • 避免在 Redis 实例上运行复杂的计算逻辑,因为这会导致 Redis 的主线程被阻塞,影响 Redis 的性能。
  • 对于需要高可用的 Redis 实例,可以使用 Redis Sentinel 或 Redis Cluster 进行搭建,以实现 Redis 的高可用性。
  • 对于需要高并发的场景,可以使用 Redis 的分布式锁机制,以避免并发访问数据的冲突。

如何用SETNX实现分布式锁? #

SETNX = SET if Not Exists,翻译过来就是:只有当指定的 key 不存在时,才给这个 key 设置值;如果 key 已经存在,就什么都不做

核心特性(最关键) #

  • 原子性:整个判断 + 设置的过程是 Redis 单线程一次性完成的,不会被其他命令打断(这也是它能做分布式锁的核心原因);
  • 无副作用:如果 key 已存在,命令直接返回「失败」,不会修改原有值;
  • 返回值
    • 1:表示设置成功(key 原本不存在);
    • 0:表示设置失败(key 已经存在)。
命令 核心逻辑 原子性 适用场景
SETNX key value 仅 key 不存在时设置 分布式锁、防重复提交
SET key value 不管 key 是否存在都覆盖 普通赋值、更新数据
SET key value NX 等价于 SETNX(新版推荐) 替代 SETNX(更易加过期时间)

优点

(1)实现简单:SETNX命令实现简单,易于理解和使用。

(2)性能较高:由于SETNX命令的执行原子性,保证了分布式锁的正确性,而且在Redis中,SETNX命令是单线程执行的,所以性能较高。

缺点

(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。

(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。

(3)存在竞争:由于SETNX命令是对Key的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。

(4)setnx不支持可重入,可以借助redission封装的能力实现可重入锁。

如何利用Redisson实现分布式锁? #

Redisson 是一款基于 Redis 实现的分布式框架,可以把它理解成「Redis 的高级功能封装库」—— 它在原生 Redis 命令的基础上,封装了分布式锁、分布式集合、延迟队列、分布式限流器等一系列开箱即用的分布式功能,让开发者不用手动写复杂的 Lua 脚本、处理原子性 / 并发问题,就能轻松实现分布式系统的核心能力。

  • 可重入锁

    就是可以加两次

  • RedLock

  • 读写锁

什么是RedLock,他解决了什么问题? #

Redlock(红锁)是 Redis 官方提出的分布式锁高可用方案,专门解决「单 Redis 节点故障导致锁失效」的问题。它的核心思路是:在多个独立的 Redis 节点(通常 3 + 个)上同时加锁,只有超过半数节点加锁成功,才算整体加锁成功;即使个别节点故障,只要多数节点正常,锁依然有效。

为什么需要 Redlock? #

之前讲的单节点 Redis 分布式锁(比如 SETNX/Redisson 单机锁)有个致命问题:

  • 如果 Redis 节点宕机(比如主节点挂了,从节点还没同步数据),锁会丢失,其他客户端可能重复加锁,导致并发安全问题;
  • Redlock 用「多节点投票机制」解决单点故障,是生产环境高可用分布式锁的标准方案。

Redlock 的核心原理(5 个步骤) #

假设部署 5 个独立的 Redis 节点(无主从、无集群,完全独立),加锁流程如下:

graph TD
    A[客户端获取当前时间戳] --> B[依次向5个Redis节点发起加锁请求(SET NX EX)]
    B --> C{统计加锁成功的节点数}
    C -->|≥3个节点成功 + 总耗时 < 锁过期时间| D[加锁成功]
    C -->|否则| E[加锁失败,向所有节点释放锁]
    D --> F[执行业务逻辑]
    F --> G[向所有节点释放锁]

关键细节: #

  1. 节点独立性:5 个节点必须完全独立(不同服务器 / 容器),避免单点故障影响多个节点;
  2. 超时控制:向单个节点加锁的超时时间要远小于锁的过期时间(比如锁过期 10 秒,单节点超时 1 秒),防止网络阻塞导致整体耗时过长;
  3. 半数以上成功:5 个节点中至少 3 个加锁成功,且总耗时 < 锁过期时间,才算加锁成功(满足「拜占庭容错」);
  4. 解锁逻辑:无论加锁成功 / 失败,都要向所有节点释放锁(包括加锁失败的节点),避免残留锁。

Redlock 的优缺点 #

优点 缺点
解决单节点 Redis 故障导致的锁失效 实现复杂,需要维护多个独立 Redis 节点
高可用(只要半数节点正常,锁就有效) 性能略低(需向多个节点发请求)
锁丢失概率极低 对时钟同步敏感(节点时钟偏差不能太大)

Redisson里面的锁是如何实现可重入的? #

Redisson 实现可重入锁的核心,是基于 Redis 的 Hash 数据结构 记录「锁持有者(线程 ID)+ 重入次数」,而非简单的 SETNX 仅判断 key 是否存在 —— 同一个线程再次加锁时,只递增重入次数,而非重新竞争锁;解锁时递减次数,只有次数归 0 才真正删除锁 key。

核心实现逻辑(3 步极简版) #

1. 第一次加锁(初始加锁) #
  • Redisson 会在 Redis 中创建一个 Hash 类型的 key(比如 lock:order:123);
  • Hash 的 field 是「当前线程的唯一标识」(比如 uuid:threadId),value 是重入次数,初始值为 1
  • 同时给这个 Hash key 设置过期时间(避免死锁)。
2. 重入加锁(同一线程再次加锁) #
  • Redisson 先检查 Redis 中该 Hash key 是否存在,且 Hash 的 field 匹配当前线程 ID;
  • 若匹配,直接将 Hash 的 value(重入次数)+1,无需重新竞争锁;
  • 过期时间会重置(续期),保证锁不会提前释放。
3. 解锁(重入次数递减) #
  • 解锁时先检查 Hash 的 field 是否匹配当前线程 ID;
  • 若匹配,将重入次数 - 1;
  • 只有当重入次数减到 0 时,才真正删除 Redis 中的 Hash key(释放锁);若次数 > 0,仅递减次数、重置过期时间。

Redisson里面的锁是如何防止误删的? #

Redisson 防止锁被误删的核心,是给每个锁绑定 “唯一标识(线程 ID / 随机值)”,解锁时先校验标识是否匹配,只有匹配才删除锁—— 彻底避免线程 A 删除线程 B 持有的锁,或线程重复解锁导致的误删问题。

核心实现逻辑(极简版) #

1. 加锁时:绑定唯一标识 #

Redisson 加锁时,会为当前线程生成一个唯一标识(比如 UUID:线程ID),并把这个标识和锁绑定:

  • 普通重入锁:存入 Redis Hash 的 field(如 lock:test 的 Hash 中,field = 唯一标识,value = 重入次数);
  • 非重入锁:直接把唯一标识作为锁 key 的 value(如 SET lock:test "UUID:1001" NX EX 10)。
2. 解锁时:先校验再删除(原子操作) #

解锁时不会直接执行 DEL 命令,而是通过 Lua 脚本 实现「校验 + 删除」的原子操作:

  • 第一步:检查锁的唯一标识是否属于当前线程;
  • 第二步:仅当标识匹配时,才执行删除(重入锁则先递减次数,次数归 0 再删除);
  • 第三步:若标识不匹配,直接返回失败,不做任何操作。

Redisson的watchdog机制是怎么样的? #

watchdog的作用是防止Redisson实现的分布式锁超时,他可以帮助我们在Redisson实例被关闭前,不断的延长锁的有效期。

  • 自动续期:当一个Redisson客户端实例获取到一个分布式锁时,如果没有制定锁的超时时间,WatchDog会基于Netty的时间轮启动一个后台任务,定期向Redis发送命令,重新设置锁的过期时间,通常是锁的租约时间的1/3.
  • 续期时长:默认时间每10秒做一次续期,续期时长30s
  • 停止续期:当锁被释放或客户端实例被关闭时,会自动停止续期。

什么是Redis的渐进式rehash? #

在 Redis 中,他的hash表结构随着数据量的增大可能会导致扩容,通常是将数组大小扩大为原来的两倍,而在扩容过程中,因为容量变化了,所以元素在新的hash表中所处的位置也会随之变化,这个变化过程就是通过rehash实现的。

而随着Redis的hash表越来越大,rehash的成本也会越来越高。Redis中实现了一种渐进式rehash的方案,他可以在哈希表rehash操作时,分多个步骤逐渐完成的方式,这样不会因为要一次性把所有元素都完成迁移而导致IO升高,线程阻塞。这个特性使得Redis可以在继续提供读写服务的同时,逐步迁移数据到新的哈希表,而不会对性能造成明显的影响。

在 Redis 中,他的hash结构其实底层是使用了两个全局哈希表的。我们把他们称之为哈希表 1 和哈希表 2。并且会维护一个rehashindex ,初始值为-1,来记录当前rehash的下标位置。

当我们开始向hash表中插入数据时,只使用哈希表 1,不断向其中添加数据。

而随着数据逐渐增多,当元素个数和hash表中的数组长度一致时,就会触发rehash动作,这时候,会把哈希表2的容量扩大一倍。然后就开始进入rehash流程。

在进入rehash过程中,不会立刻把哈希表1中的数据全部rehash到哈希表2中,而是在后续有新的增删改查操作时,会从头开始进行rehash动作。

假如,我们现在要新增一个元素:

那么就会从当前的hashindex开始,把这个哈希表1的hashindex这个位置的桶中的数据全部rehash到哈希表2中,然后rehashindex +1 。

然后再在哈希表2中进行添加操作:

在后续的其他操作中也一样,会沿着hashindex一直往后开始进行逐个桶的rehash,一直到哈希表1中的元素全部完成rehash。

然后再把哈希表1和哈希表2的指针互换一下(后续会再把哈希表2给直接置为NULL),后续的增删改查继续在新的哈希表1中操作,直到下一次rehash开始。

查询怎么办? #

现在hash1中查找,没有去hash2

介绍下Redis集群的脑裂问题? #

Redis的脑裂问题可能发生在网络分区或者主节点出现问题的时候:

  • 网络分区:网络故障或分区导致了不同子集之间的通信中断。

    Master节点,哨兵和Slave节点被分割为了两个网络,Master处在一个网络中,Slave库和哨兵在另外一个网络中,此时哨兵发现和Master连不上了,就会发起主从切换,选一个新的Master,这时候就会出现两个主节点的情况。

  • 主节点问题:集群中的主节点之间出现问题,导致不同的子集认为它们是正常的主节点。

    Master节点有问题,哨兵就会开始选举新的主节点,但是在这个过程中,原来的那个Master节点又恢复了,这时候就可能会导致一部分Slave节点认为他是Master节点,而另一部分Slave新选出了一个Master

脑裂的危害 #

  • 数据不一致:不同子集之间可能对同一数据进行不同的写入,导致数据不一致。
  • 重复写入:在脑裂解决后,不同子集可能尝试将相同的写入操作应用到主节点上,导致数据重复。
  • 数据丢失:新选出来的Master会向所有的实例发送slave of命令,让所有实例重新进行全量同步,而全量同步首先就会将实例上的数据先清空,所以在主从同步期间在原来那个Master上执行的命令将会被清空。

如何避免 #

Redis 已经提供了两个配置项可以帮我们做这个事儿,分别是 min-slaves-to-write 和 min-slaves-max-lag。

min-slaves-to-write:主库能进行数据同步的最少从库数量;

min-slaves-max-lag:主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟秒数。

这两个配置项必须同时满足,不然主节点拒绝写入。在期间满足min-slaves-to-write和min-slaves-max-lag的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。

Redis中的key过期了一定立即删除吗? #

  • 主动删除:定期删除
  • 被动删除:再次访问到就删除

Redis会定期随机检查一些带有过期时间的键

Redis每秒会执行以下操作10次:

  • 从带有过期时间的键集合中随机选择20个键。
  • 删除所有已经过期的键。
  • 如果已经过期的键占比超过25%,则重新从步骤1开始。

直到过期Key的比例下降到 25% 或者这次任务的执行耗时超过了25毫秒,才会退出循环

Redis中有一批key瞬间过期,为什么其它key的读写效率会降低? #

因为主动删除策略占用了主线程。Redis的命令执行又是单线程的,所以这时候后面来的业务操作请求,就需要等这个删除命令执行完才可以处理业务请求。

那么这时候就会出现,业务访问延时增大的问题。

如何基于Redis实现滑动窗口限流? #

滑动窗口限流的主要优点是可以在时间内平滑地控制流量,而不是简单地设置固定的请求数或速率。这使得系统可以更灵活地应对突发流量或峰值流量,而不会因为固定速率的限制而浪费资源或降低系统性能。

利用Redis,我们就可以实现一个简单的滑动窗口限流的功能。因为滑动窗口和时间有关,所以很容易能想到要基于时间进行统计。

那么我们只需要在每一次有请求进来的时候,记录下请求的时间戳和请求的数据,然后在统计窗口内请求的数量时,只需要统计窗口内的被记录的数据量有多少条就行了。

在Redis中,我们可以基于ZSET来实现这个功能。假如我们限定login接口一分钟只能调用100次:

那么,我们就可以把login接口这个需要做限流的资源名作为key在redis中进行存储,然后value我们现在ZSET这种数据结构,把他的score设置为当前请求的时间戳,member的话建议用请求的详情的hash进行存储(或者UUID、MD5什么的),避免在并发时,时间戳一致出现score和memberv一样导致被zadd幂等的问题。

所以,我们实现滑动窗口限流的主要思想是:只保留在特定时间窗口内的请求记录,而丢弃窗口之外的记录。

主要步骤:

  • 定义滑动窗口的时间范围,例如,窗口大小为60秒。
  • 每次收到一个请求时,我们就定义出一个zset然后存储到redis中。
  • 然后再通过ZREMRANGEBYSCORE命令来删除分值小于窗口起始时间戳(当前时间戳-60s)的数据。
  • 最后,再使用ZCARD命令来获取有序集合中的成员数量,即在窗口内的请求量。

Redis的Key和Value的设计原则? #

Key #

  • 可读性
  • 简洁性
  • 避免特殊字符串
  • 命名空间
  • 长度限制

Value #

  • 数据类型选择
  • 避免大key
  • 过期时间
  • 压缩
  • 合理控制和使用数据结构内存编码优化配置

什么是Redis和Pipeline,和事务什么区别? #

Redis 的 Pipeline 机制是一种用于优化网络延迟的技术,主要用于在单个请求/响应周期内执行多个命令。在没有 Pipeline 的情况下,每执行一个 Redis 命令,客户端都需要等待服务器响应之后才能发送下一个命令。这种往返通信尤其在网络延迟较高的环境中会显著影响性能。

在 Pipeline 模式下,客户端可以一次性发送多个命令到 Redis 服务器,而无需等待每个命令的响应。Redis 服务器接收到这批命令后,会依次执行它们并返回响应。

需要注意的是,Pipeline是不保证原子性的,他的多个命令都是独立执行的,Redis并不保证这些命令可以以不可分割的原子操作进行执行。这是Pipeline和Redis的事务的最大的区别。

虽然都是执行一些相关命令,但是Redis的事务提供了原子性保障,保证命令执行以不可分割、不可中断的原子性操作进行,而Pipeline则没有原子性保证。

但是他们在命令执行上有一个相同点,那就是如果执行多个命令过程中,有一个命令失败了,其他命令还是会被执行,而不会回滚的。

Redis事务和Lua之间有哪些区别? #

原子性保证 #

事务和Lua都可以保证原子性操作,但是,这里说的原子性,指定的不可再分,不可中断的操作。不管是Redis的事务和Lua,都没办法回滚,一旦命令失败了,都不支持回滚。

但是,Redis的事务在执行过程中,如果有某一个命令失败了,是不影响后续命令的执行的,而Lua脚本中,如果执行过程中某个命令执行失败了,是会影响后续命令执行的。

交互次数 #

在Redis的事务执行时,每一条命令都需要和Redis服务器进行一次交互,我们可以在Redis事务过程中,MULTI 和 EXEC 之间发送多个 Redis 命令给到Redis服务器,这些命令会被服务器缓存起来,但并不会立即执行。但是每一条命令的提交都需要进行一次网络交互。

而Lua脚本则不需要,只需要一次性的把整个脚本提交给Redis即可。网络交互比事务要少。

前后依赖 #

在 Redis 的事务中,事务内的命令都是独立执行的,并且在没有执行EXEC命令之前,命令是没有被真正执行的,所以后续命令是不会也不能依赖于前一个命令的结果的。

而在Lua 脚本中是可以依赖前一个命令的结果的,Lua 脚本中的多个命令是依次执行的,我们可以利用前一个命令的结果进行后续的处理。

流程编排 #

借助Lua脚本,我们可以实现非常丰富的各种分支流程控制,以及各种运算相关操作。而Redis的事务本身是不支持这些操作的。

Redis为什么不支持回滚? #

因为Redis的设计就是简单、高效等,所以引入事务的回滚机制会让系统更加的复杂,并且影响性能。从使用场景上来说,Redis一般都是被用作缓存的,不太需要很复杂的事务支持,当人们需要复杂的事务时会考虑持久化的关系型数据库。相比于关系型数据库,Redis是通过单线程执行的,在执行过程中,出现错误的概率比较低,并且这些问题一般来编译阶段都应该被发现,所以就不太需要引入回滚机制。

Redisson的lock和tryLock有什么区别? #

tryLock是尝试获取锁,如果能获取到直接返回true,如果无法获取到锁,他会按照我们指定的waitTime进行阻塞,在这个时间段内他还会再尝试获取锁。如果超过这个时间还没获取到则返回false。如果我们没有指定waitTime,那么他就在未获取到锁的时候,就直接返回false了。

lock的原理是以阻塞的方式去获取锁,如果获取锁失败会一直等待,直到获取成功。

如何使用setnx实现一个可重入锁? #

可重入锁是一种多线程同步机制,允许同一线程多次获取同一个锁而不会导致死锁。

加锁的逻辑:

  • 当线程尝试获取锁时,它首先检查锁是否已经存在。
  • 如果锁不存在(即 SETNX 返回成功),线程设置锁,存储自己的标识符和计数器(初始化为1)。
  • 如果锁已存在,线程检查锁中的标识符是否与自己的相同。
    • 如果是,线程已经持有锁,只需增加计数器的值。
    • 如果不是,获取锁失败,因为锁已被其他线程持有。

解锁逻辑:

  • 当线程释放锁时,它会减少计数器的值。
  • 如果计数器降至0,这意味着线程已完成对锁的所有获取请求,可以完全释放锁。
  • 如果计数器大于0,锁仍被视为被该线程持有。

Redis实现分布式锁等时候,哪些问题需要考虑? #

锁的基本要求 #

  • 互斥性:setnx
  • 可重入性、锁的性能等

误解锁问题 #

确保只有锁的持有者能释放锁,避免其他客户端误解锁。

setnx加锁时把持有锁owner放进去

锁的有效时间 #

单点故障问题 #

首先,在使用单节点Redis实现分布式锁时,如果这个Redis实例挂掉,那么所有使用这个实例的客户端都会出现无法获取锁的情况。

这个问题是有解的,就是引入集群模式,通过哨兵检测redis实例挂掉的情况,提升整个集群的可用性。

当使用集群模式部署的时候,如果master一个客户端在master节点加锁成功了,然后没来得及同步数据到其他节点上,他就挂了, 那么这时候如果选出一个新的节点,再有客户端来加锁的时候,就也能加锁成功,因为数据没来得及同步,新的master会认为这个key是不存在的。

为了解决这个问题,redis的作者提出了一个算法——RedLock,他通过这种算法来保证在半数以上加锁成功才认为成功,这样就可以确保即使master挂了,新选出来的master也会有之前的加锁数据。

Redis如何高效安全的遍历所有key? #

分别使用KEYS命令和SCAN命令

KEYS命令用于查找所有符合给定模式的键,例如KEYS *会返回所有键。它在小数据库中使用时非常快,但在包含大量键的数据库中使用可能会阻塞服务器,因为它一次性检索并返回所有匹配的键。

SCAN命令提供了一种更安全的遍历键的方式,它以游标为基础分批次迭代键集合,每次调用返回一部分匹配的键。SCAN命令不会一次性加载所有匹配的键,因此不会像KEYS命令那样阻塞服务器,更适合用于生产环境中遍历键集合。

zset为什么在数据量少的时候用ziplist,而在数据量大的时候用skiplist? #

默认情况下,当元素数量少于128,每个元素的长度都小于64字节的时候,使用ZipList(ListPack),否则,使用SkipList!

是出于 内存优化性能考量 的双重目的。

内存优化 #

首先ZipList是一个压缩的数据结构,它的每个元素都是连续存储的(和数组有点像),因此内存的使用非常紧凑。与其他数据结构相比,ZipList在小规模数据存储时显著减少了内存占用。

而在内存方面,SkipList 相比于 ZipList 内存开销要大得多,因为它是通过多个链表层级来实现的,每个元素需要额外的指针来维护层级结构。

性能对比 #

ZipList 是一个紧凑的线性结构,所以如果在 ZipList 中查找一个元素时,可能需要遍历整个列表,同理插入和删除操作也是线性的。所以它的插入、删除和查找操作的时间复杂度通常是 O(N)。

而SkipList 采用多级索引结构,它的查找、插入、删除操作的时间复杂度是 O(log N),远远优于 ZipList 的 O(N)。

总结 #

所以,ZipList的存储更节省空间,而SkipList的操作性能会更好。

ListPack #

ListPack是 Redis 在 5.0 版本引入的一种新的内存高效数据结构(在Redis 7.0正式替代ZipList用在ZSet中),它是为了解决 ziplist 和 skiplist 在一些场景下的不足而提出的。

listpack 是一种 适用于小型有序集合、列表和哈希的压缩数据结构,它比 ziplist 更加灵活、适应性更强,并且提供了对大数据量的更好支持。

更重要的事,ListPack通过创新的数据结构方案,避免了级联更新的问题。

13、WATCH命令和基于CAS的乐观锁? #

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务

执行失败。例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:

val = GET mykey val = val + 1 SET mykey $val

以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景–竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:

WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC

和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。

15、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来? #

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

16、使用过Redis做异步队列么,你是怎么用的? #

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

如果对方追问redis如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。

18、Redis如何做持久化的? #

bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。

对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

19、Pipeline有什么好处,为什么要用pipeline? #

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

20、Redis的同步机制了解么? #

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

Redis中的hash和go语言中map什么区别? #

Redis 的 Hash(哈希)和 Go 语言的 map 虽然都是「键值对(Key-Value)」形式的哈希结构,但一个是分布式内存数据库中的数据类型,一个是编程语言内置的内存数据结构,核心差异体现在「存储位置、使用场景、特性支持」等维度

维度 Redis Hash Go map
存储位置 独立的 Redis 服务器(内存 / 持久化到磁盘),属于「分布式存储」 Go 程序进程的内存中,属于「本地存储」
访问方式 需通过网络 RPC 调用(如 HGET/HSET 直接内存访问,无网络开销
线程 / 并发安全 天然线程安全(Redis 单线程处理命令) 非线程安全,并发读写会 panic
持久化 支持(RDB/AOF),重启后数据不丢 进程退出后数据丢失,仅内存有效
数据共享 多进程 / 多机器可共享(分布式场景) 仅当前 Go 进程内可见,跨进程需序列化传输
键值类型 仅支持 Redis 基础类型(string/int 等),且 key/field 均为 string 键:可比较类型(string/int/ 指针等);值:任意类型(struct/slice 等)
过期策略 支持给整个 Hash key 设置过期时间(无法给单个 field 设过期) 无内置过期,需手动实现
容量限制 受 Redis 服务器内存限制(TB 级) 受当前机器内存 / Go 进程内存限制
操作原子性 单个命令原子性(如 HINCRBY),多命令需 Lua 脚本 无原子性,需手动加锁(sync.Mutex

了解Redis的内存碎片吗? #

所谓内存碎片,就是分配给Redis的内存空间,实际上没有被用到的部分。

原因:

  • 分配时无法精准分配
  • 一些数据结构自动扩容也会导致碎片

如何清理:

  • 使用Redis提供的碎片整理功能