Redis八股文

基础篇

参考:Redis 数据库详解:特性和实战应用-CSDN博客

1.关系/非关系型数据库

关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织

非关系型数据库不是数据库,而是数据结构化存储方法的集合,可以是文档或键值对等

2.什么是Redis

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此**读写速度非常快**, 常用于**缓存,消息队列、分布式锁等场景**。并且对数据类型的操作都是**原子性**的,因为执行命令由单线程负责的, 不存在并发竞争的问题。Redis 还支持**事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制**等等。

3.redis数据类型

类型

用途

底层实现

String(字符串)

可存储字符串、整数或浮点数

计数器

分布式锁

存储序列化对象

int(如果存的全数字)

SDS 简单动态字符串

Hash(哈希)

可存储多个键值对

缓存对象

购物车

ZipList 压缩列表 (连续占用内存,因此存储的元素不能太多)

一旦元素存储太多,会转为 Dict 哈希表

Redis 7.0 引入了 listpack 取代 ZipList 压缩列表

List(列表)

可重复、(插入和取出)有序

朋友圈点赞/评论列表

QuickList 快速列表:一种双向链表,每个节点是一个压缩列表

结合了压缩列表和双向链表的优点

Set(集合)

不可重复,无序,求交集、差集等

求共同关注

去重

若集合中的元素都是整数且元素个数小于 512 : IntSet 整数集合

否则:Dict 哈希表(Set是单列集合,Dict的每个节点是存键值对的,那么其实是只用了键来存元素,值都设为null)

ZSet(有序集合)

不可重复,每个元素都关联一个分数

自动按照分数排序元素

排行榜

当元素数量小于128,每个元素小于64字节:ZipList 压缩列表

否则:Dict + SkipList,存储两份数据,Dict 和 SkipList 各一份

为什么要存储两份数据?因为这是它的特性需求

  • 利用Dict去键值查询:根据元素查分数
  • 利用SkipList范围查询 (快速跳过大量无关节点,定位范围的起始点):
    • 获取指定分数区间的元素
    • 获取排名在指定区间的元素

Redis 7.0 引入了 listpack 取代 ZipList 压缩列表

Redis 的 String 类型底层实现是怎样的?为什么会有多种编码?

String 类型底层由 redisObject 表示,其实际编码可能是 intembstrraw,目的是根据数据大小和特性优化内存和性能:

  • 小整数 → int 编码,节省内存;
  • 小字符串(≤44字节) → embstr,一次性分配,创建销毁更快;
  • 大字符串 → raw,更灵活但开销大一些。

4.Redis单线程

Redis是单线程的;网络I/O和数据读写操作都是由单个线程来完成的。

但Redis的其他功能,如持久化,异步删除,集群数据同步等,是由额外线程来执行的。

因此,严格来说Redis并不是全面单线程

5.Redis数据不丢失(持久化机制)

AOF 日志(完整):每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;

RDB 快照(恢复快):将某一时刻的内存数据,以二进制的形式写入磁盘;

RDB 快照就是记录某一个瞬间的内存数据,记录的是**实际数据**,而 AOF 文件记录的是**命令操作的日志,**而不是实际的数据。

因此在 Redis 恢复数据时, **RDB 恢复数据的效率会比 AOF 高些**,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据

RDB快照

一篇文章彻底理解Redis持久化:RDB和AOF_rdb和aof存得是什么-CSDN博客

自动触发(主流)

save 命令来设置 自动快照的触发条件,当满足特定的“时间 + 修改次数”条件

save 300 10     # 如果300秒内至少有10个key发生变化,则触发保存
  1.  在配置文件中设置了save的相关配置,如sava m n,它表示在m秒内数据被修改过n次时,自动触发bgsave操作。
  2. 当从节点做全量复制时,主节点会自动执行bgsave操作,并且把生成的RDB文件发送给从节点。
  3. 执行debug reload命令时,也会自动触发bgsave操作。
  4. 执行shutdown命令时,如果没有开启AOF持久化也会自动触发bgsave操作。

手动触发==>阻塞与非阻塞保存

方式命令阻塞主线程描述
阻塞保存SAVE阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例造成长时间阻塞,基本不采用。
非阻塞保存BGSAVERedis 进程执行fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。

Redis进程会执行fork操作创建子进程,RDB持久化由子进程负责,不会阻塞Redis服务进程。Redis服务的阻塞只发生在fork阶段,一般情况时间很短。

  1. 执行bgsave命令,Redis进程先判断当前是否存在正在执行的RDB或AOF子线程,如果存在就是直接结束。
  2. Redis进程执行fork操作创建子线程,在fork操作的过程中Redis进程会被阻塞。
  3. Redis进程fork完成后,bgsave命令就结束了,自此Redis进程不会被阻塞,可以响应其他命令。
  4. 子进程根据Redis进程的内存生成快照文件,并替换原有的RDB文件。
  5. 子进程通过信号量通知Redis进程已完成。

思考:如果当前Redis服务器中存储的数据特别多,内存消耗特别大(比如100GB),此时,进行上述的fork操作,是否会有很大的性能开销?

答:此时的性能开销,其实是挺小的。fork在进行内存拷贝的时候,不是简单无脑的直接把所有的数据都拷贝一遍,而是“写时拷贝”的机制来完成的!

具体来说,fork只会在内存被修改时才实际复制数据,而在未修改时,父子进程共享同一份物理内存。因此,即使有大量数据,内存的实际拷贝成本也能得到控制,从而在处理高内存使用情况下的性能影响降到最低。这使得Redis在高负载时仍然能够有效运行。


AOF日志

【Redis】持久化机制--RDB和AOF_redis rdb和aof-CSDN博客

AOF(Append Only File)持久化: 以独立日志的方式记录每次写命令,重启时再重新执行 AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是
Redis 持久化的主流方式。(当开启AOF后,RDB就不再生效)

AOF的工作流程

1. 所有的写⼊命令会追加到 aof_buf(缓冲区)中。
2. AOF 缓冲区根据对应的策略向硬盘做 同步操作 。 这样可以减少写磁盘的次数。【缓冲区还是为了追求高的效率,如果 突然断电 ,缓冲区中的数据还没有来得及写磁盘,这时候还是会出现 数据丢失 的情况的】
3. 随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写 ,达到压缩的⽬的。
4. 当 Redis 服务器启动时,可以加载 AOF ⽂件进⾏数据恢复。

文件同步(缓冲区刷新策略)==3种策略

AOF 同步(写磁盘)发生在 Redis 执行完写操作之后,它有 3 种策略

策略含义特点
always每次写命令都同步到磁盘最安全但最慢
everysec每秒同步一次折中方案,默认选项
no由操作系统控制(缓冲区写满再刷盘)性能高但最不安全

重写机制

AOF文件重写是把 Redis 进程内的数据转化为写命令同步到新的 AOF文件。重写后的 AOF为什么可以变小?有如下原因:

  • 进程内已超时的数据不再写入文件。
  • 旧的 AOF 中的无效命令,例如 del、hdel、srem 等重写后将会删除
  • 多条写操作合并为一条


 Redis启动流程

 

 优缺点对比

1. RDB快照
优点:1、性能影响较小(子进程完成)  2、数据恢复速度快  3、适用于定期备份

缺点:1、数据丢失风险较高:RDB是定期执行的,因此在两次快照之间的数据将会丢失。如果Redis在快照完成之前崩溃,会导致最新的数据丢失。2、不灵活:RDB持久化只能定期执行,无法根据实际业务需求灵活地设置持久化频率。

2. AOF
优点: 1、数据丢失风险低(同步策略)2、更可控的持久化机制 3、文件内容可读

缺点:1、文件体积大:由于AOF会记录每一条写操作指令,随着时间的推移,文件会比RDB大得多,导致持久化文件占用大量磁盘空间。2、数据恢复速度较慢:AOF文件在恢复时需要逐条执行日志中的命令,因此恢复速度通常比RDB慢。3、对性能影响更大:频繁的写操作会导致性能开销,尤其是在设置为“每次写操作”同步时,对Redis的性能影响较大。

总结

  • RDB适用于:希望最小化对Redis性能影响、对数据持久化频率要求不高,或者需要定期备份的场景。
  • AOF适用于:对数据持久性要求高,不能接受较多数据丢失的场景,但需要注意性能和磁盘空间的占用。

6.什么要用Redis/为什么要用缓存

主要从高性能高并发这两个角度来看待问题

高性能:假如用户第一次访问数据库中的某些数据,因为从磁盘上读取的,过程比较慢。将该用户访问的数据存储在缓存中,这样下次再次访问这些数据的时候,就可以从缓存中拿了。操作缓存就是直接操作内存,所以速度特别快。

高并发:直接操作缓存所能承受的请求是远远大于直接访问数据库

7.为什么Redis这么快

(1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);

(2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的

(3)采用单线程,避免了不必要的上下文切换、多进程或者多线程切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

(4)使用多路 I/O 复用模型

8.Redis的过期键和删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。

Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

Redis 使用的过期删除策略是「**惰性删除+定期删除**」。

过期策略通常有以下三种

 (1)定时删除:每个Key都需要一个定时器,到过期期间就会立即删除。

该策略可以立即清楚过期的key,对内存很友好;但是会占用大量CPU去处理过期的数据,从而影响缓存的响应时间和吞吐量。

(2)惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

该策略可以最大化的节省CPU资源,但对内存十分不友好,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存

(3)定期删除:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key

通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

9.Redis 持久化时,对过期键会如何处理的?

Redis 持久化文件有两种格式: AOF和RDB

1.AOF 文件分为两个阶段:AOF 文件写入阶段(不检查)和 AOF 重写阶段(检查)

**AOF 文件写入阶段**:如果数据库过期键还没被删除,  AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来 显式地删除该键值**。

- **AOF 重写阶段**:执行 AOF 重写时,会对 Redis 中的键值对进行检查,**已过期的键不会被保 存到重写后的 AOF 文件中**,因此不会对 AOF 重写造成任何影响

 2.RDB 文件分为两个阶段:RDB 文件生成阶段(检查)和加载阶段(主库检查、从库不检查)

- **RDB 文件生成阶段**:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,** 过期的键「不会」被保存到新的 RDB 文件中**,因此 Redis 中的过期键不会对生成新 RDB 文件产 生任何影响。

- RDB 加载阶段 :RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:- **如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行 检查,过期键「不会」被载入到数据库中**。所以过期键不会对载入 RDB 文件的主服务器造成影 响;- **如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到 数据库中**。

但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

10.Redis 主从模式中,对过期键会如何处理?

当 Redis 运行在主从模式下时,**从库不会进行过期扫描,从库对过期的处理是被动的**。也就是即 使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值 对一样返回。 从库的过期键处理依靠主服务器控制,**主库在 key 到期时,会在 AOF 文件里增加一条 del 指令, 同步到所有的从库**,从库通过执行这条 del 指令来删除过期的 key。

11.Redis 内存淘汰策略/内存中的cache淘汰

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策 略。

1、不进行数据淘汰的策略  :当运行内存超过最大设置内存时, 不淘汰任何数据,但不再提供服务,直接返回错误。

2、进行数据淘汰的策略,细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。

2-1、在设置了过期时间的键值中进行淘汰:

- **volatile-random**:随机淘汰任意过期键值;

- **volatile-ttl**:优先淘汰更早过期的键值。

- **volatile-lru**(Redis3.0 之前,默认的内存淘汰策略):淘汰最久未使用的过期键值;

- **volatile-lfu**(Redis 4.0 后新增的内存淘汰策略):淘汰最少使用的过期键值;

2-2、在所有数据范围内进行淘汰:

- **allkeys-random**:随机淘汰任意键值;

- **allkeys-lru**:淘汰最久未使用的键值;

- **allkeys-lfu**(Redis 4.0 后新增的内存淘汰策略):淘汰最少使用的键值

12.LRU 算法和 LFU 算法有什么区别?

什么是 LRU 算法?

**LRU** 全称是 Least Recently Used 翻译为**最近最少使用**,会选择淘汰最近最少使用的数据。

传统 LRU 算法的实现是基于**「链表」结构**,链表中的元素按照操作顺序从前往后排列,最新操 作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素 就代表最久未被使用的元素。

Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:

  1. 需要用链表管理所有的缓存数据,这会带来额外的空间开销;-
  2. 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链 表移动操作,会很耗时,进而会降低 Redis 缓存性能。

Redis 是如何实现 LRU 算法的?

`Redis 实现的是一种近似 LRU 算法`,目的是为了更好的节约内存,实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。---时间戳

当 Redis 进行内存淘汰时,会使用**随机采样的方式来淘汰数据**,它是随机取值,然后**淘汰最久没有使用的那个**。

Redis 实现的 LRU 算法的优点

  1. 不用为所有的数据维护一个大链表,节省了空间占用;
  2. 不用在每次数据访问时都移动链表项,提升了缓存的性能;

但是 LRU 算法有一个问题,**无法解决缓存污染问题**,比如应用一次读取了大量的数据,而这些 数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。

> 什么是 LFU 算法?

LFU 全称是 Least Frequently Used 翻译为**最近最不常用的**,LFU 算法是根据数据访问次数来 淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会**增加该数据的访问 次数**。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。

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

一、如何避免缓存雪崩?(大量数据无缓存,在数据库)

**大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处 理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,造成整个系统崩溃。

对于缓存雪崩问题,我们可以采用两种方案解决。

  1. - 将缓存失效时间随机打散: 在原有的失效时间基础上增加一个随机值(0-1)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
  2. - 设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存 雪崩,也可以在一定程度上避免缓存并发问题。

二、 如何避免缓存击穿?(热点数据无缓存,在数据库)

如果缓存中的**某个热点数据过期**了,此时大量的请求访问了该热点数据,就无法从缓存中读取, 直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是**缓存击穿**的问题。

可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。

应对缓存击穿 可以采取前面说到两种方案:

  1. - 互斥锁方案:保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  2. - 不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

三、如何避免缓存穿透?(缓存和数据库都没有)

当用户访问的数据,**既不在缓存中,也不在数据库中**, 那么当有大量这样的请求到来时,数据库的压力骤增,这就是**缓存穿透**的问题。

缓存穿透的发生一般有这两种情况:

  1. - 业务误操作:缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数 据;
  2. - 黑客恶意攻击:故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案,常见的方案有三种。

  1. - 限制非法字段的请求:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判 断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  2. - 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存 中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而 不会继续查询数据库。
  3. - 使用布隆过滤器:用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

14.Redis主从复制/同步

主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。

1、从节点发送 SYNC 命令给主节点。
2、主节点生成 RDB 快照文件,并将后续的写命令缓存到缓冲区。
3、主节点发送 RDB 文件给从节点,从节点加载 RDB 文件。
4、主节点将缓冲区中的写命令发送给从节点,从节点执行这些命令以保持数据同步。

15.Redis哨兵模式(监控、选主、通知)

 Redis 主从服务的时候,存在问题:当 Redis 的主从服务器出现故障宕机时,需要 动进行恢复。 为了解决这个问题,Redis 增加了哨兵模式,因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能

哨兵模式选取主节点

1、故障节点主观下线

2、故障节点客观下线

3、哨兵集群选取leader

4、哨兵leader选取主节点

16.Redis切片集群

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 **Redis 切片集群**方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服 务的读写性能。

17.Redis与MySQL数据一致性?

1.先写缓存,再写数据库
先写缓存,当我们写入缓存成功的时候,突然网络出现异常,导致写数据库失败,这时候数据库没有新的数据,缓存有新的数据,此时缓存中的数据就变成了脏数据

2.先写数据库,再写缓存
如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致

3.先删缓存,再更新数据库---**Cache Aside 策略**

在删除缓存与更新数据库期间,请求刚好查询,缓存中没有,从数据库读取。造成数据不一致

延时双删

删除缓存-->更新数据库--->一定时间间隔---->再删除缓存

防止旧缓存还存在,这样就不会从mysql更新

4.先更新数据库 ,再删除缓存---订阅MySQL binlog,后操作缓存

第一步是更新数据库,那么更新数据库成功,就会产生一 条变更日志,记录在 binlog 里。 于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,

18.大 key 如何处理?

什么是 Redis 大 key?

大 key 并不是指 key 的值很大,而是 key 对应的 **value 很大**。

一般而言,下面这两种情况被称为大 key:

  1. - String 类型的值大于 10 KB;
  2. - Hash、List、Set、ZSet 类型的元素的个数超过 5000个;

 大 key 会造成什么问题?

大 key 会带来以下四种影响:

- **客户端超时响应**:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久都没有响应。

- **引发网络阻塞**:每次获取大 key 产生的网络流量较大

- **阻塞工作线程**:如果使用 del 删除大 key 时,会阻塞工作线程

- **内存分布不均**:集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

> 如何处理?

-**识别大key**:使用`redis-cli`工具的`SCAN`命令结合`KEYS`或`HGETALL`、`LRANGE`等命令来 定位哪些key占用过多的内存或哪些操作可能引起性能问题。-

**优化数据结构**:使用更高效的数据结构,例如用`Set`、`Sorted Set`或`Hash`替换`String`类型,当数据适合这些结构时。对于大型列表,考虑使用`List`的`LPUSH`和`RPUSH`来限制列表的长度,或者使用`ZADD`和`ZREM`在有序集合中维护固定大小的滑动窗口。使用`Bitmaps`来存储大量二进制数据,尤其是当数据是稀疏的或需要进行位操作时

 **数据拆分**:最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否 存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主 线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞 主线程。

**避免全量读取**:对于大key,尽量避免一次性读取全部数据,而是使用范围查询如`HGET`、 `LPOP`、`RPOP`等命令来分批次读取数据。

19.热key如何处理

热key:访问率高、读写频繁的键

  1. 本地缓存:例如Java的Caffeine地本地缓存,减少Redis的压力,注意设置过期时间
  2. Key分片,将热Key拆分成多个子Key,分散到不同的Redis实例上
  3. 读写分离(含主从架构),读请求分流到多个从节点

20.事务与Redis

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

Redis事务保证原子性吗,支持回滚吗

Redis中单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务 中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时 先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

21.跳表和字典

ZSet 使用了跳表和字典的组合

  1. 跳表:在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25,那么层数就增加1层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数(最高为64层)。
  2. 字典(哈希表):用来存储键值对,并且提供了高效的查找、插入和删除操作。在内部,字典通常由两个哈希表组成,使得在哈希冲突较多或者哈希表负载因子较高时,能够平滑地进行重新哈希,从而保持操作的高效性。

**为什么需要两个哈希表**-->针对重新哈希期间

1. **平滑重新哈希**

  • 一个哈希表会保留原有的键值对,而另一个哈希表则用于新插入的键值对。
  • 避免了在单次操作中完成整个哈希表 的重新计算,从而减少了单次操作的延迟和对性能的影响。

2. **避免停顿时间**

  • 旧的哈希表仍然可以被读取,新的写操作会被导向到新的哈希表。
  • 避免了在重新哈希过程中可能出现的长时间停顿,提高了系统的响应时间和并发能力。

3. **渐进式重新哈希**

  • 重新哈希操作可以被分割成多个步骤,每个 步骤只移动一部分键值对到新的哈希表,直到所有的键值对都完成了迁移。
  • 这种策略可以确保在高负载下系统的稳定性和性能。

4. **节省内存**

  • 旧的哈希表不会立即被释放,而是等到所有的键值对都迁移到的哈希表后才被回收。
  • 这种机制可以避免在短时间内大量的内存分配和释放,有助于减少内存碎片和提高内存管理的效率。

22.跳表、红黑树、B+树

23.Redis I/O多路复用

首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。

24.Redis 管道有什么用?

管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整 个交互的性能。

使用**管道技术可以解决多个命令执行时的网络等待**,它是把多个命令整合到一起发送给服务器端 处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序 的执行效率。 但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。

要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能

进阶篇  场景题

redis如何实现分布锁

1.通过 SET 命令的 NX 和 PX 选项可以实现分布式锁

SET lock_key unique_value NX PX 30000(30s)

释放锁时,使用 Lua 脚本确保只有持有锁的客户端才能释放

  • 脚本先检查锁的值是否匹配,匹配则删除锁。

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

 锁自动过期导致误释放

如果业务执行时间超过 EX 10 设置的时间,锁会自动释放,可能导致其他进程获取锁,产生数据竞争问题。
解决方案:可以使用 看门狗机制 续约锁(Redisson 解决方案)

lua脚本:确保获取锁和设置过期时间的原子性,适用于需要更高可靠性的场景

  • 原子性:Lua 脚本在 Redis 中原子执行,无需担心并发问题。

  • 减少网络开销:将多个操作合并为一个脚本,减少客户端与服务器之间的通信次数。

  • 复杂逻辑支持:支持条件判断、循环等复杂逻辑。

2.使用 Redlock 算法,适用于多节点 Redis 集群

  • 获取当前时间(T1)。
  • 依次向 N 个 Redis 节点请求锁,使用相同的 lock_key 和 unique_value,并设置相同的过期时间。
  • 计算获取锁的时间(T2 - T1),如果大多数节点(N/2 + 1)成功获取锁,且总时间小于锁的过期时间,则认为锁获取成功。
  • 如果获取失败,向所有节点发送释放锁的请求。

3.使用Redisson库

Redisson可重入锁原理:即一个线程可以多次获取锁。利用Hash结构,记录获取锁的线程获取锁的次数

获取锁的逻辑:判断 key(unique_value一般是业务唯一标识) 是否存在,即判断是否有线程持有锁

  1. 如果key不存在,代表没有线程持有锁,则获取锁,即设置一个Hash结构的 (field,value),field是机器码+线程ID,value是 次数 1。
  2. 如果key存在,代表有线程持有锁,此时判断持有锁的线程是否是当前线程,如果是当前线程,则重入次数+1。

释放锁的逻辑:重入次数-1,如果重入次数为0,删除key,释放锁

超时续约机制(看门狗机制)

为了解决TTL不好设置,如果分布式锁的key设置了TTL,又因为业务阻塞原因,导致当前线程的业务未完成,TTL到期了,导致被迫释放锁,此时就会有其他线程抢占锁,导致有线程并行执行,违背了加分布式锁的初心。

当 Redisson 获取锁没有指定锁的TTL时,默认会使用超时续约机制(看门狗WatchDog)机制。

续期规则

  • 默认 TTL 为 30 秒,后台线程(定时任务)每隔 10 秒 检查业务是否完成。
  • 若未完成 → 通过 Lua 脚本重置锁 TTL(续期至 30 秒),保证锁不被释放,等到业务执行完。

Redisson分布式锁的数据未同步导致线程安全

主从问题

  • 主节点宕机时,若从节点未同步 锁 数据 → 新主节点可能允许其他线程重复加锁 → 锁失效

解决方案:

  • 此时需要部署多个 Redis 主节点,多主多从架构,向多个主节点都去获取锁
  • MultiLock 联锁方案:要求 所有主节点加锁成功,否则立即失败并释放已获取的锁
  • RedLock 红锁方案:要求 半数主节点以上(如 3/5)加锁成功,否则立即失败并释放已获取的锁。

布隆过滤器

布隆过滤器主要用于判断一个元素是否可能属于某个集合,而不支持直接获取集合中的所有元素。其基本结构是一个固定长度的位数组和一组(多个)哈希函数。

  • 位数组:长度为 m,初始时所有位都设为 0。注意:不是二进制数组,是排序数组
  • k 个哈希函数:每个哈希函数 hi(x) 计算元素 x 的哈希值,并将结果映射到 [0, m-1] 之间的某个位置。

原理实现

插入示例(m=10,k=3,插入 A):

  • 初始状态:  [0 0 0 0 0 0 0 0 0 0]===>对应0,1,2,3,4--..--9
  • 哈希计算:  h1(A) = 2, h2(A) = 5, h3(A) = 7
  • 插入后:      [0 0 1 0 0 1 0 1 0 0]

查询示例(查询B)

  • 当前位数组:  [0 0 1 0 0 1 0 1 0 0]
  • 哈希计算:    h1(B) = 3, h2(B) = 6, h3(B) = 9
  • 索引位置:    3, 6, 9  (0, 0, 0) -> B一定不存在

如果所有位置都是 1,说明 B 可能存在(但不一定)。

如果有任意一个位置是 0,说明 B 一定不存在

删除元素

标准布隆过滤器不支持删除,因为多个元素可能共享相同的位,无法安全地清除某个元素的标记。

改进方法

  • 计数布隆过滤器(Counting Bloom Filter):用整数数组代替位数组,每个位置的值是一个计数器,插入时 +1,删除时 -1

删除A元素

  • 当前计数器: [0 0 1 1 0 1 0 1 0 0]
  • 哈希计算:   h1(A) = 2, h2(A) = 5, h3(A) = 7
  • 哈希计算:   h1(B) = 3, h2(B) = 5, h3(B) = 7
  • 删除后:      [0 0 0 1 0 1 0 1 0 0 0]  (如果 A 是唯一的)

会误判不漏判

为什么会误判,主要是哈希碰撞和无法删除元素。

  • 误判:可能会错误地认为一个不存在的元素存在(假阳性)。
  • 不漏判:如果布隆过滤器认为元素不存在,则一定不存在

Redis扩容

1.哈希表扩容问题

1. 阻塞 / 延迟增加

  • Redis 在进行 rehash(扩容或缩容)时会将键从旧表搬运到新表。
  • 虽然 Redis 使用 渐进式 rehash,但如果数据量大、QPS 高,仍可能出现瞬时延迟。

2. 内存瞬时飙升

  • 扩容时 Redis 会 同时维护两个哈希表(ht[0], ht[1]),在未完全 rehash 前占用双倍内存。
  • 如果内存紧张,容易触发 OOM。

3. 并发操作复杂度上升

  • 在 rehash 过程中,所有读写操作必须同时查询 ht[0]ht[1]
  • 会增加 CPU 消耗和代码复杂度

2.Redis集群扩容问题

1. 数据迁移导致阻塞

  • 使用 reshardcluster addslots 时,需要从旧节点迁移部分数据到新节点。
  • 如果量大,会阻塞或降低性能。

2. 槽(slot)不均衡

  • Redis Cluster 采用 16384 个哈希槽分片,扩容后槽分配不合理会导致负载不均衡。

3. 迁移失败/数据丢失

  • 网络异常、节点故障可能导致 migrate 命令失败,甚至数据不一致。

Rehash

Rehash 是 Redis 在 哈希结构(Hash)扩容或缩容 时的一种机制,本质上是为了保持高性能的数据访问。它适用于 Redis 的底层数据结构,比如 dict(哈希表),尤其是在存储大量键值对时。

哈希表的 负载因子(元素个数 / 哈希桶个数)超过一定阈值时,会触发扩容:

  • 默认情况:负载因子 > 1,且不在执行 bgsave/bgrewriteaof
  • 强制情况:负载因子 > 5 时立即扩容(即使正在 bgsave)

1. 有一个初始容量为 4 的哈希表
2. 插入 30 个 key,Redis 开始渐进 rehash
3. 但是请求量暴增,根本来不及迁移
4. 这时 ht[0] 仍保有 30 个元素,数组长度为 4
5. 负载因子 = 30 / 4 = 7.5

  1. 创建一个 更大(或更小)容量的哈希表 ht[1]
  2. 开始将 ht[0] 中的键值对迁移到 ht[1]
  3. Redis 并不一次性迁移全部键值对,而是采用 渐进式 Rehash
  4. rehashidx 表示已经迁移到的索引位置:每次操作,搬运 ht[0][rehashidx] 位置上的所有键值对到 ht[1],然后 rehashidx++
  5. 所有 key 搬完后,ht[1] 替代 ht[0],rehash 完成。

一致性哈希

是一种分布式系统中的哈希算法,用于解决数据在多台机器间分布存储时的负载均衡问题,尤其在节点频繁增删的场景下也能保持数据尽可能稳定

为什么需要一致性哈希

传统哈希方式的问题是:

  • 节点数量 N 变化时,几乎所有 key 的映射都会改变
  • 比如从 3 个节点增加到 4 个,会造成约 75% 的 key 映射位置变化 → 数据迁移代价大!

一致性哈希解决的就是这个问题:

✅ 当节点数发生变化时,只有少量 key 的映射会受到影响,从而减少数据迁移。

核心思想

1. 将 key 和 节点都映射到一个哈希环(0 ~ 2³²)上

2. key 沿顺时针方向找最近的节点处理

  • 比如 hash(key1) 落在节点 A 和 B 之间,则交给 B 处理
  • 如果 B 挂了,key1 会被 A 处理,迁移量小

虚拟节点

一致性哈希可能会因节点分布不均导致某些节点负载过高。为了解决这个问题,引入了虚拟节点

  • 每个物理节点映射多个“虚拟节点”在环上
  • key 还是通过 hash 找到“最近的虚拟节点”
  • 虚拟节点再映射回原始物理节点

✅ 平衡负载
✅ 减少某个节点突然成为热点的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值