数据库Redis

1.⭐️说说什么是Redis

Redis是一种基于键值对的 NoSQL 数据库

  • 它主要的特点是把数据放在内存当中,相比直接访问磁盘的关系型数据库,读写速度会快很多,基本上能达到微秒级的响应。
  • 所以在一些对性能要求很高的场景,比如缓存热点数据、防止接口爆刷,都会用到 Redis。

2.⭐️⭐️⭐️Redis有哪些数据类型?底层的数据结构?

Redis 本质上是一个 key-value 内存数据库,key 通常是字符串,Value 有多种数据类型。最常见的有五种基础类型,还有一些扩展类型。

String(字符串)

  • 最基本的类型,可以存储字符串、整数或浮点数。
  • 常用于:缓存数据、分布式锁、计数器(点赞数)。
  • 底层用 SDS简单动态字符串(Simple Dynamic String) 实现,支持快速读写和动态扩展。

List(列表)

  • 有序、可重复,支持从两端插入/弹出。
  • 常用于:消息队列、时间线相关的:朋友圈点赞/评论列表。
  • 小数据量:ZipList(压缩列表,节省空间)。
  • 大数据量:QuickList(双向链表 + 压缩列表结合)。

Hash(哈希表)

  • 类似于键值对集合Map,存储字段和值的映射。
  • 常用于:存储对象,比如用户信息、商品详情。
  • 小哈希用 ziplist 压缩列表 压缩存储;
  • 大哈希用 Dict 哈希表

Set(集合)

  • 无序,去重,可以做交集、并集、差集。
  • 常用于:去重、好友关系、共同关注。
  • 元素全是整数且数量小:IntSet(整数集合)。
  • 大集合用 **Dict **哈**希表**。

ZSet(有序集合)

  • 在 Set 的基础上,每个元素带一个 score 排序。
  • 常用于排行榜。
  • 小数据:ZipList。
  • 大数据:SkipList(跳表)+ 哈希表 (Dict)。
  • 在集合的基础上,每个元素带一个分数 score,用于排序,常用于排行榜。
  • 为什么要存储两份数据?
    • Dict:O(1) 根据元素查分数。
    • SkipList:O(logN) 做范围查询(按分数、按排名)。

扩展类型

  • Bitmap:用二进制位表示状态,比如用户签到、用户活跃标记。
  • HyperLogLog:做基数统计,节省内存,比如 网页UV计数统计。
  • Geo:存储和查询地理位置,比如附近的人/地点查询,地理位置范围查询。

img

img

3.⭐️redis中Set和Sorted Set的区别?

Set 和 Sorted Set 都是集合类型,但区别主要有三个方面:

有序性

  • Set 是无序的,里面的元素没有固定顺序。
  • Sorted Set 每个元素都有一个分数 score,Redis 会按照 score 自动排序,所以可以做排名。

底层实现

  • Set 底层是 哈希表(小数据量时用 intset),只保证去重,不保证顺序。
  • Sorted Set 底层是 跳表 + 哈希表,跳表保证有序性,哈希表保证快速查找。

使用场景

  • Set 常用在去重、集合运算,比如用户标签、共同好友。
  • Sorted Set 常用在排行榜、优先队列,需要根据分数排序的场景。

Set 关注“有没有”,Sorted Set 关注“排序”。

4.⭐️⭐️你了解跳表吗?

了解过跳表,Redis 的 有序集合(Sorted Set) 就是用 跳表(SkipList) 来实现的。

简单来说,跳表是一个 基于链表的多层级索引结构

  • 普通链表查找某个元素只能从头到尾遍历,效率是 O(n)。
  • 跳表在链表的基础上,加了多层“索引”。
    • 最底层是完整的链表;上面一层会抽取一部分节点作为索引;再往上一层继续抽取
  • 查找的时候,就可以先在上层快速跳跃,接近目标范围后,再到下一层精细查找。

这样,跳表查找的平均时间复杂度能降到 **O(log n)**,插入和删除也一样是 O(log n),和平衡树一个级别。实现起来比红黑树这类平衡树要更简单。

img

img

5.**⭐️redis为什么用跳表而不用平衡树(B+)?**跳表节点查询过程?

实现简单

  • 跳表本质上是链表加多层索引,相对于红黑树复杂的旋转和平衡操作,跳表的插入和删除逻辑更清晰,代码更容易实现和维护。

并发性能好。

  • 跳表在修改时,锁定的粒度非常小,通常只需要锁住待修改节点周围的几个节点。
  • 而平衡树的旋转可能会导致大范围的节点受影响,甚至需要锁住整棵树,这在高并发下性能会差很多。

范围查询更方便

  • 排行榜这种场景,经常要查一个区间,比如前 100 名。
  • 跳表可以顺着索引层快速定位,再在链表里顺序遍历,非常适合范围查询。
  • 而平衡树虽然查单个元素快,但做范围遍历没链表顺滑。

性能差不多

  • 跳表的查询、插入、删除都是 **O(log n)**,和红黑树、B+树一个量级。
  • 对 Redis 这种内存型数据库来说,常数时间的差距并不大。

所以 Redis 选择跳表,是在 实现复杂度、性能和功能 之间的权衡。

下跳表的查询过程:

  • 跳表有多层索引,最上层节点少,越往下越多,最底层是完整链表。
  • 查找一个元素时,从最高层开始往右走,直到下一个节点比目标大,就往下层走。
  • 这样层层“下降 + 右移”,最后到达底层链表找到目标节点。

6. ⭐️什么是缓存穿透?解决?

  • 缓存穿透是指查询一个不存在的数,由于存储层查不到数据因此不写入缓存,导致不存在的数据每次请求都要到数据库去查询,导致数据库压力很大。
  • 比如有人恶意传不存在的 ID,就会形成缓存穿透。

常见的解决方法有两类:

  1. 缓存空值

    如果数据库里查不到结果,可以把这个空结果也放到缓存里,下次再查直接返回空,避免一直打数据库。

  2. 布隆过滤器

    在缓存前加一层布隆过滤器,先判断 key 是否可能存在,如果一定不存在就直接拦截,连数据库都不查。

缓存空值导致的不一致问题

img

缓存空值导致的不一致问题:

  • 如果后续数据库中该数据被正确写入,但缓存中的空值没有及时更新,这样会导致缓存与数据库的数据不一致。即使数据库中有数据,缓存中仍然返回空值,造成不一致。

解决办法:

  • 设置合理的过期时间:缓存空值时,应该为缓存中的空值设置一个短期的过期时间,这样可以确保缓存不会长时间存储错误的数据。
  • 使用布隆过滤器:为了防止缓存穿透,除了缓存空值外,还可以引入布隆过滤器来判断数据是否存在,避免不必要的数据库查询,同时避免不一致问题。

你能介绍一下布隆过滤器吗?

img

布隆过滤器其实是一种高效判断元素是否存在的数据结构,很适合拦截缓存穿透。

  • 它的原理是这样的:底层是一个很长的位数组,配合多个哈希函数。
    • 插入元素时,用哈希函数算出几个下标,把对应的位置标记为 1。
    • 查询时,再用相同的哈希函数算下标,如果这些位置有一个是 0,就说明一定不存在;如果全是 1,就可能存在。
  • 这里要注意,它可能会有误判(把不存在的元素当成存在),但不会漏判。一般设置合理,误判率在 5% 以内,业务是能接受的。

我们项目里用过布隆过滤器,比如优惠券秒杀场景。提前把所有合法的优惠券 ID 存到布隆过滤器里,请求进来先判断,不存在就直接拦截,避免无效请求打到数据库,保护后端系统。

img

img

  • 我们项目中使用的是 Redisson 提供的布隆过滤器实现,非常方便,Redisson 内置了布隆过滤器的数据结构支持。
  • Redisson 是一个 Java 客户端框架,用来操作 Redis,它为 Redis 提供了一个高层次的 API,简化了许多操作。类似于我们熟悉的 Spring、MyBatis。它实现了 Redis 中的布隆过滤器功能,允许你在 Redis 中创建、管理和使用布隆过滤器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
// 需要一个配置对象连接reids
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.203.128:6379");

// 配置 Redisson 客户端
RedissonClient redisson = Redisson.create(config);

// 获取布隆过滤器对象(布隆过滤器名称为 myBloomFilter)
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter");

// 初始化布隆过滤器(只在第一次创建时有效),设置预期存储1000个元素,误判率为0.01
if (!bloomFilter.isExists()) {
bloomFilter.tryInit(1000, 0.01);
}

// 向布隆过滤器添加元素
bloomFilter.add("apple");
bloomFilter.add("banana");
// 查询元素是否存在
System.out.println(bloomFilter.contains("apple")); // 输出 true
System.out.println(bloomFilter.contains("pear")); // 输出 false

// 关闭 Redisson 客户端
redisson.shutdown();
}

5. ⭐️什么是缓存击穿?解决?

缓存击穿就是:一个热点数据刚好过期了,结果这个时候有一大堆请求冲上来查这个数据,发现缓存没了,只能都去查数据库,可能把数据库压垮。

针对这个问题,业界主要有两种解决思路:

方案一:互斥锁

  • 给这个热点 key 加个分布式锁(比如 Redis 的 setnx)。第一个拿到锁的线程去查数据库并回填缓存,其他线程等一等或者重试。
  • 这样能保证数据是最新的,但并发量大时可能会有一些请求被阻塞。

方案二:逻辑过期(推荐)

  • 在缓存里存一份逻辑过期时间。即使缓存过期了,还是先把旧数据返回给用户,同时后台异步刷新缓存。
  • 这样用户几乎不会感受到延迟,适合大流量场景,只是数据可能会有一点点延迟。

如果业务需要强一致性,我会用加锁的方案;如果业务更看重高性能和高可用,我会选逻辑过期。

6. ⭐️什么是缓存雪崩?解决?

缓存雪崩是指大量缓存的 key 在同一时间过期,这时候如果有大量用户访问这些数据,请求就全部打到数据库上,数据库压力骤增,可能直接被压垮,造成整个系统崩溃。

解决:避免同时过期

  • 设置随机过期时间,比如TTL在基础值上加一个随机数,避免同一时刻大量key一起失效
  • 或者对热点数据,提前缓存预热,在大促/高峰前就把数据提前放进缓存。

7.⭐️⭐️⭐️redis 和 mysql 的双写一致性?

  • Redis 和 MySQL 双写一致性,主要是指保证缓存和数据库中的数据不要出现不一致。
  • 在实际项目里,我们经常会遇到缓存和数据库的数据不一致问题,比如先更新了数据库但缓存还没更新,用户就可能读到旧数据。

常见的思路有这几种:

先更新数据库,再删除缓存

  • 因为如果先删缓存,再更新数据库,在高并发下可能会出现脏数据(线程 A 删除缓存,线程 B 查询数据库回填旧值)。所以一般推荐“更新 DB → 删除缓存”。
  • 这种方式适合对实时性要求不高的场景,只要最终一致就行。

延迟双删策略

  • 在更新数据库后,先删一次缓存,然后延迟一小段时间再删一次。
  • 这样可以降低并发下回填旧值的问题,但延迟时间需要根据业务调优。

异步更新

  • 更新数据库后,通过消息队列或 Canal 订阅 binlog,异步去更新或删除缓存,保证最终一致性,不阻塞业务。

前面的都是只能保证最终一致性。

需要强一致性(如秒杀库存)的可以:

  • Redisson 提供的分布式读写锁(排他锁 + 共享锁)
  • 共享锁(ReadLock):读的时候可以多个线程共享,不互斥。
  • 排他锁(WriteLock):写的时候会阻塞其他读写操作,保证写操作执行时没有其他线程读或写,避免脏数据。
  • 底层一般用 SETNX 原子操作实现,保证同一时刻只有一个线程持有锁。读方法和写方法要使用同一把锁才能保证一致性。

对实时性不高的业务,可以用先更新数据库 → 删除缓存 / 延迟双删 / 异步刷新就够了;

对强一致性业务,用分布式读写锁保证读写互斥

img

img

img

img

8.⭐️⭐️⭐️redis的持久化?(如何保证数据不丢失)

Redis 本身是内存数据库,所以数据存在内存里,掉电或者重启就可能丢失。为了保证数据不丢失,Redis 提供了两种主要持久化机制:

  1. RDB(快照方式)
    • Redis 会在指定时间间隔生成数据的快照,保存到磁盘。
    • 优点:恢复快,占用资源少;缺点:如果 Redis 宕机,最后一次快照后的数据会丢失。
    • 场景:适合对数据丢失能容忍一点的业务,比如缓存、统计数据。
  2. AOF(Append Only File)日志方式
    • Redis 会把每一次写操作以命令追加到日志文件里。
    • 优点:数据恢复更完整,可以根据策略做到每秒同步一次或者每次写操作同步;缺点:日志文件大,恢复速度比 RDB 慢。
    • 场景:对数据安全要求高的业务,比如消息队列、交易系统。

保证数据不丢失的方法

  • 单纯用 RDB:可能会丢最后几秒的数据。
  • 单纯用 AOF:可以通过配置策略控制同步频率,数据几乎不会丢失。
  • 混合持久化:Redis 也可以同时开启 RDB + AOF,兼顾恢复速度和数据安全。

9.⭐️⭐️⭐️redis的数据过期策略?

Redis 支持给 key 设置过期时间,也就是 TTL(Time To Live)。当 key 超过 TTL 就会被删除。Redis 的过期策略主要有两种:

  1. 定期删除(定时扫描)
    • Redis 会定期随机扫描一部分设置了过期时间的 key,把过期的 key 删除。
    • 优点:不会每次访问都检查,性能开销小;
    • 缺点:过期 key 不会立即删除,有可能稍微滞后。
  2. 惰性删除(访问时检查)
    • 当客户端访问 key 的时候,Redis 会检查它是否过期,如果过期就直接删除,并返回 key 不存在。
    • 优点:不会浪费资源去扫描不活跃的 key;
    • 缺点:如果某个 key 很少被访问,过期后会暂时还占用内存。

Redis 结合这两种策略保证过期 key 最终会被清理,同时避免对性能的过大影响

10.⭐️⭐️⭐️redis的数据淘汰策略?

(假如缓存过多,内存是有限的,内存被占满了怎么办?)

Redis 是内存数据库,如果内存满了,又有新的 key 要加入,就会触发内存淘汰机制。我们可以在redis的配置文件中设置具体的淘汰策略。

Redis 提供的具体策略包括:

  • noeviction(默认)
    • 不淘汰任何 key,当内存不足时,写操作会报错。
    • 适合对数据丢失不能接受的业务。
  • allkeys-lru / volatile-lru(LRU 最近最少使用)
    • allkeys-lru:从所有 key 中删除最近最少使用的 key。
    • volatile-lru:只删除设置了过期时间的 key 中最近最少使用的。
    • 适合热点数据缓存场景。
  • allkeys-lfu / volatile-lfu(LFU 最不常用)
    • 类似 LRU,但根据使用频率淘汰,删除使用次数最少的 key。
    • 适合访问频率差异大的业务。
  • allkeys-random / volatile-random
    • 随机删除 key,volatile-random 只在有过期时间的 key 中随机删除。
    • 简单但不精准,一般不推荐高价值数据场景。
  • volatile-ttl
    • 优先删除 TTL 最短的 key,也就是马上就要过期的 key。

其中有两个重要的概念:
LRU(Least Recently Used):最近最少使用。Redis 会记录 key 的最近访问时间,淘汰时优先删除很久没访问过的数据。
LFU(Least Frequently Used):最少频率使用。Redis 会统计 key 的访问频率,访问次数少的 key 优先被淘汰。

数据库有1000万数据,Redis只能缓存20w数据。如何保证Redis中的数据都是热点数据?

嗯,我想一下()。可以使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略。那留下来的都是经常访问的热点数据。

Redis的内存用完了会发生什么?

嗯~,这个要看redis的数据淘汰策略是什么。如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的是allkeys-lru策略,把最近最常访问的数据留在缓存中。

11. 分布式锁使用场景

分布式锁主要用在 分布式系统中多个进程/节点需要互斥访问共享资源的场景。它的作用就是保证 同一时刻只有一个客户端能操作临界资源,避免并发冲突。

  • 库存扣减/秒杀场景
    • 多个节点同时处理下单请求时,都去操作库存。
    • 如果没有分布式锁,可能导致超卖。
    • 用锁保证同一时刻只有一个节点能修改库存数据。
  • 订单生成
    • 分布式系统下,多个服务可能同时给同一个用户生成订单。
    • 分布式锁可以保证每个用户在同一时间只有一个订单被创建。

在分布式锁场景里,安全就是指:
不会出现同一时刻多个客户端同时持有同一把锁
即使有部分 Redis 节点挂了,剩下的节点也能保证锁的唯一性。

12.⭐️⭐️⭐️Redis 分布式锁如何实现?

  • Redis 分布式锁的核心思想是 利用 Redis 的原子操作,保证同一时间只有一个客户端持有锁,从而控制分布式系统中多个节点对共享资源的访问。

常见实现方式是:使用 setnx 命令 (SET key value NX EX seconds)

  • Redis 的 SETNX 命令保证键不存在时才能设置成功,这样同时只有一个线程能拿到锁。
  • 给锁设置 过期时间,防止持锁线程宕机导致锁无法释放。
  • 释放锁时,需要检查是否是自己加的锁再删除,避免误删其他线程的锁。

还有一个比较完整的方案是:Redisson 提供的分布式锁实现

  • Redisson 封装了很多细节,比如可重入锁、读写锁、看门狗机制。
  • 还有自动续期的看门狗机制,能保证业务执行时间超过锁的 TTL 时锁不会被释放。
  • 读写锁的话,写操作会阻塞读和写,读操作可以允许多线程并发读。

img

13. 那你如何控制Redis实现分布式锁的有效时长呢?

  • 最常见的是当线程拿到锁的时候,给这个锁的 key 设置一个过期时间,这样即使线程挂掉,锁也会自动释放,不会造成死锁。
  • 如果业务执行时间不确定:
    • 可以手动设置一个比较大的 TTL,这样大部分情况锁就能自动释放。
    • 或者使用 Redisson 的看门狗机制,它会自动续期,如果业务还在执行,锁不会过期;一旦业务结束或者线程挂掉,看门狗就会让锁释放。

14. Redisson实现的分布式锁是可重入的吗?

是的,Redisson 的分布式锁是可重入的。

  • 同一个线程可以多次加同一把锁而不会被阻塞,每次加锁会在内部记录加锁次数,释放的时候也需要释放相同次数才能真正释放锁。

15. Redisson实现的分布式锁能解决主从一致性的问题吗?

Redisson 自带的分布式锁,其实不能完全解决主从一致性问题。

  • 因为 Redis 的主从复制是异步的,主节点加锁成功但还没同步到从节点就宕机时,从节点被提升成新主节点,就可能出现锁丢失,导致多个客户端同时拿到锁。
  • 针对这个问题,Redisson 提供了 RedLock,它会在多个完全独立的 Redis 实例上同时加锁,只要超过半数成功,就算加锁成功。这样即使某个节点宕机,锁依然是安全的。
  • 当然,RedLock 的缺点是性能开销比较大,运维成本也高。
    • 所以实际工程里,如果一致性要求极高,可以用 RedLock
    • 如果更看重性能,大多时候会选择单节点 Redis 加上看门狗机制,基本能满足大多数场景。

16. ⭐️如果业务非要保证数据的强一致性,这个该怎么解决呢?

  • Redis 本身更偏向于高可用和高性能,它没法保证严格的强一致性,如果要强一致,性能会大打折扣。
  • 所以在这种场景下一般不会选 Redis,而是用 ZooKeeper 来做分布式锁,因为它们底层有一致性协议,可以保证强一致性。

img

17.⭐️⭐️怎么判断 Redis 某个节点是否正常工作?

  • 最简单的方式就是用 PING 命令,如果节点正常会返回 PONG,如果超时或者没响应,就说明可能有问题。
  • 第二,可以用 INFO 命令,它会返回节点的详细运行状态,比如内存、客户端连接数,还有 role 字段可以区分是主节点还是从节点。通过这些信息,我们能判断节点是不是在正常工作。
  • 如果是集群模式,还可以用 CLUSTER INFO,里面有个 cluster_state 字段,如果是 ok 就表示集群健康,如果是 fail 就说明有节点挂了。
  • 也可以用监控系统(像 Prometheus、Grafana)去看延迟、内存和 QPS,提前发现节点异常。

18. Redis集群有哪些方案,知道吗?

主从复制 + 哨兵模式(Sentinel)

  • 一个主节点,多个从节点。主节点负责写,从节点做备份。
  • 哨兵负责监控主节点,宕机时能自动切换,从节点变成新主节点。
  • 这种方案的优点是实现简单、适合高可用场景,但缺点是写能力受限,扩展性不强,节点增加不太方便。

Redis Cluster(官方分片集群)

  • 数据按 哈希槽 分片存储在多个主节点上,每个主节点还可以挂从节点做备份。
  • 集群支持水平扩展,节点可以动态增加或删除,数据自动迁移。
  • 优点是扩展性好、适合大数据量,缺点是管理和运维相对复杂。

如果只想要高可用,可以用主从+哨兵;如果要大规模扩展和分布式存储,用 Redis Cluster

增加节点通常指:
增加从节点:只能分担读请求,写还是单点瓶颈。
增加主节点:架构本身不支持多个主节点

19.⭐️⭐️⭐️介绍一下Redis主从同步,主从同步数据的流程?

Redis 主从同步是为了实现读写分离和高可用,一般是一主多从,主节点负责写,从节点负责读。Redis 的主从同步主要分成两个阶段:全量同步增量同步

全量同步(Full Sync)

这通常发生在从节点第一次连接主节点时。整个过程可以分为这几步:

  1. 建立连接:从节点会向主节点发送一个PSYNC命令,因为是第一次连接,所以它没有主节点的ID和偏移量信息。
  2. 主节点准备数据:主节点收到命令后,就知道这是一个全新的从节点。它会执行BGSAVE命令,在后台生成一个完整的RDB内存快照文件。
  3. 发送快照:主节点将这个RDB文件发送给从节点。从节点接收到后,会先清空自己的所有旧数据,然后加载这个RDB文件,这样它的状态就和主节点在某个时间点上完全一致了。
  4. 同步增量:在主节点生成和发送RDB文件的这段时间里,它并没有闲着,而是把所有新的写命令都缓存到了一个叫做“复制积压缓冲区”的地方。当RDB发送完毕后,主节点会把这个缓冲区里的命令再发送给从节点,从节点执行这些命令,就追上了主节点的最新状态。

增量同步(Incremental Sync)

如果主从之间的网络连接只是短暂中断,恢复之后并不需要再进行一次完整的全量同步。

  • 从节点会带着自己之前记录的偏移量(offset)去请求主节点。
  • 主节点在“复制积压缓冲区”里找到这个偏移量之后的数据,然后把这部分增量数据发送给从节点就行了。这个过程非常快。

最后,我想补充两点:

  1. Redis的复制默认是异步的,主节点执行写命令后会立即返回,不会等待从节点确认。所以主从之间存在短暂的数据延迟。
  2. 在实际生产环境中,为了实现自动故障转移,我们通常不会只用主从复制,而是会配合哨兵(Sentinel)模式或者集群(Cluster)模式来保证整个系统的高可用。

我的回答完毕,谢谢面试官。

img

img

20.⭐️⭐️⭐️怎么保证Redis的高并发高可用?

Redis 的高并发和高可用主要靠 以下几个方面

高并发

  • Redis 是 内存存储 + 单线程 + I/O 多路复用,所以单机吞吐量很高。
  • 读的扩展:用主从复制实现读写分离,主节点负责写,从节点并行处理大量读请求。
  • 写的扩展:用 Redis Cluster 做分片,把数据分布到多个主节点,分散写压力。

高可用

  • 数据冗余:主从复制保证每份数据至少有一份备份。
  • 自动故障转移
    • 哨兵模式:哨兵监控主节点,主节点宕机时自动提升从节点为新主。
    • Cluster 模式:集群本身支持节点故障检测和自动切换,不依赖额外哨兵。

一句话总结:Redis 高并发靠内存 + 单线程 + 分片,高可用靠主从复制 + 哨兵/Cluster,实现数据冗余和自动切换。

img

img

img

img

21. 你们使用Redis是单点还是集群,哪种集群?

我们在实际工程中一般会根据业务需求选择。

  • 如果是小型业务或者对写性能要求不高,可以用 单点 Redis + 主从复制,实现读写分离,高可用靠哨兵(Sentinel)监控。
  • 如果是大流量或者大数据量业务,会用 Redis Cluster。Cluster 是官方提供的分布式方案,通过 哈希槽分片把数据分散到多个主节点,每个主节点又可以挂从节点做备份,实现水平扩展和高可用。

一句话总结:小流量可以单点+哨兵,高流量用 Cluster,兼顾性能和可用性。

22. Redis集群脑裂,该怎么解决呢?

  • 脑裂其实就是网络分区导致的,一个主节点跟大多数节点断开了,它自己还以为是健康的,还在对外接收写请求;同时另外一边的哨兵可能已经把新的主节点推选出来,这样两个 Master 并存,数据就会冲突。

Redis在不同的模式下有不同的解决方法:

  1. 在哨兵(Sentinel)模式下,主要靠两个配置参数来预防:
    1. 可以设置 min-replicas-to-write <数量> min-replicas-max-lag <秒数>
      通过这两个参数要求主节点必须连上足够数量、并且延迟在一定范围内的从节点才能写。否则主节点就会自动降级为只读。
    2. 举个例子,如果我们设置 min-replicas-to-write 1,那么当主节点因为网络分区,发现自己身边一个从节点都没有了,它就会立刻停止接受写数据。这样,即使另一边选举出了新的主节点,也不会发生数据冲突。
  2. 在集群(Cluster)模式下:它依赖的是少数服从多数(Quorum)机制
    • 一个节点必须获得超过半数主节点的投票,才能成为或继续担任主节点。
    • 在网络分区中,被隔离的少数派主节点拿不到足够的票数,就会自动降级,这就避免了脑裂。

23**.** Redis的分片集群有什么作用?

  • Redis 分片集群的主要作用就是解决单机Redis遇到的两个主要瓶颈
  • 存储容量的瓶颈:单台Redis服务器的内存是有限的,最多只能用几十个 G 内存,通过集群,可以把海量数据分散存储到多台服务器上,从而构建一个大规模的内存数据库。
  • 并发性能的瓶颈:单机Redis的写入性能会受限于单个CPU的核心。通过集群,写请求被分散到多个主节点上执行,使得整个集群的并发写入能力得到极大的提升。

同时,Redis集群也内置了主从复制和故障转移机制,所以在实现水平扩展的同时,也保证了高可用

24. Redis分片集群中数据是怎么存储和读取的?

Redis 分片集群的数据存储和读取,核心是通过 哈希槽(Hash Slot) 实现的

  1. 哈希槽的设计
    • Redis集群预设了 16384 个哈希槽。
    • 集群初始化时,这些槽会平均分配给各个主节点。比如有 3 个主节点,节点 A 负责 0–5500 号槽,节点 B 负责 5501–11000,节点 C 负责剩下的槽。
  2. 数据的存储过程
    • 写入 key 时,Redis 先对 key 做 CRC16 校验,再对 16384 取模,得到对应的哈希槽号。
    • 根据槽号槽位与节点的查映射表,找到负责该槽的主节点,将 key 存到该节点上
  3. 数据的读取过程
    • 读取时流程相同:先算槽号,再去对应节点读取数据。
    • 如果客户端访问了错误节点,该节点会返回 MOVED 重定向,客户端会按照提示访问正确的节点。

当你写入一个 key,例如 "user:1001" 时,Redis 会做以下几步:
**计算 CRC16:
** Redis 对 key 的字符串内容进行 CRC16 运算,得到一个 16 位整数。
例:CRC16("user:1001") → 0x5A3B(十六进制表示)
**取模得到哈希槽
** Redis 有 16384 个槽,所以用 CRC16 对 16384 取模:
slot = CRC16(key) % 16384 得到的 slot 就决定了 key 应该存到哪个主节点。

img

25.⭐️⭐️⭐️Redis是单线程的,但是为什么还那么快?

Redis 虽然是单线程,但性能非常高,主要有几个原因:

  1. 内存操作:数据全部存在内存里,读写比硬盘快几个数量级,没有磁盘 I/O 的阻塞,而且使用了I/O 多路复用机制来高效处理网络请求。
  2. 采用单线程,所有命令在同一个线程中按顺序执行,避免不必要的上下文切换和线程间的锁竞争。
  3. Redis 使用压缩列表、跳表、哈希表等高效内存数据结构,操作复杂度低,保证性能。

26.⭐️⭐️⭐️解释下I/O多路复用模型?

  • Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度。 I/O多路复用模型主要就是实现了高效的网络请求
  • 传统阻塞 I/O 的做法是每个请求分配一个线程,如果线程被阻塞等待 I/O,CPU 就闲置了,而且线程多了还会有上下文切换开销。
  • I/O 多路复用不同,它通过非阻塞的方式和事件通知机制,让一个线程可以同时管理多个 I/O 连接。操作系统会告诉程序哪个 I/O 已经准备好,程序就去处理它,而不是一直阻塞等待。
  • 常见实现有 selectpollepoll,其中 epoll 在 Linux 上效率最高,特别适合大量并发连接。
  • I/O 多路复用让 Redis 在单线程里同时处理成千上万客户端请求,减少了线程开销,提高了吞吐。

基本概念

I/O 操作,在计算机和网络编程中,指的是**输入(Input)和输出(Output)**的操作。I/O 操作通常涉及到**与外部世界的数据交换**,例如从磁盘读取数据、从网络接收数据或将数据写入到终端等。
I/O 操作的常见类型:
磁盘 I/O
:从硬盘或其他存储设备中读取或写入数据。例如,读取文件、写入文件。
网络 I/O:与远程服务器或其他计算机交换数据。例如,通过 HTTP 协议发送请求并接收响应
键盘/屏幕 I/O:与用户的交互有关。例如,从键盘读取输入,或者将信息输出到屏幕上

1. I/O 阻塞与非阻塞
阻塞 I/O
:每当一个线程执行 I/O 操作时,线程会被阻塞直到操作完成。例如,读取文件或网络数据时,如果没有数据返回,线程会等待直到数据准备好。
非阻塞 I/O:线程发起 I/O 操作后,不会被阻塞,线程可以继续做其他任务。如果没有数据,线程可以继续执行,不会停下来等待。

2. I/O 多路复用:能够让一个线程同时处理多个 I/O 操作。当多个 I/O 操作(如读写文件、网络请求等)都处于阻塞状态时,多路复用允许一个线程在这些操作上轮询,知道某个 I/O 操作可以继续

27. Redis 为什么使用单线程执行命令?

  • 抛开持久化不谈,Redis是纯内存操作,执行命令速度非常快,它的性能瓶颈是网络延迟,而不是执行速度,因此多线程并不会带来巨大的性能提升
  • 多线程会导致过多的上下文切换整体来说会带来不必要的开销,反而导致性能下降
  • 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度就会大大提高,而且性能也会大打折扣。

28. Redis 6.0 为什么引入多线程?

  • Redis 在 6.0 之前一直是单线程的,其实执行命令本身速度非常快,真正的瓶颈主要出在 网络 I/O,也就是读写客户端数据这一步。
  • Redis 虽然用了 I/O 多路复用模型,但如果有成千上万个连接同时读写,单线程去收发数据还是会卡住。
  • 所以从 6.0 开始,Redis 引入了多线程,不过这个多线程只用在 处理网络请求的读写,比如:
    • 从 socket 里读客户端请求数据。
    • 把响应结果写回客户端。

但是命令的真正执行,比如 set、get,这些还是在主线程里完成的,保证了线程安全,不需要加锁。

这样做的好处是:虽然单次请求的延迟没有缩短,但整体吞吐量提升了不少,高并发场景下能更好地利用多核 CPU,提高 Redis 的处理能力。

29.Redis 是 AP 还是 CP?

Redis 是 AP 的。Redis 的设计目标是高性能、高可扩展性、高可用性

  • 当发生网络分区时或者部分节点故障不可用时,Redis 仍然允许接受读写请求,保证可用性(A)
  • Redis 是保证最终一致性,即在某个时间点读取的数据可能并不是最新的,但最终会达到一致的状态。
  • Redis 主从数据同步采用的是异步复制,这导致在节点之间存在数据同步延迟和不一致的可能性。

30.Redis 支持事务/事务回滚 吗?

  • Redis 支持事务,但它的事务只保证多个命令执行的原子性,且事务不支持回滚。
  • Redis 不支持事务回滚:
    • Redis 的设计思路是简单高效:如果引入事务回滚会让系统的复杂性升高,并且影响性能。
    • Redis 的定位:主要用于缓存、高速计数、分布式锁等场景,对严格事务需求较低
    • 替代方案:如需强一致性事务,应选择关系型数据库(如MySQL)或支持ACID的NoSQL数据库(如MongoDB 4.0+)。

31. 你知道 热key问题 和 大key问题 吗?

  • 热 Key 指的是某个 Key 被大量并发访问,比如一个热点商品或者热点新闻,如果都打到同一台 Redis,会造成这台机器压力过大。
  • 解决办法:给热点 Key 做缓存预热,然后在应用层做本地缓存。
  • 大 Key 指的是某个 Key 对应的数据特别大,比如一个集合里有上百万个元素,或者 Value 是一个超大的字符串。大 Key 的问题是操作它会占用很长时间,甚至阻塞线程,还会导致网络传输压力大。
  • 解决办法 是:
    • 在设计上避免,把大 Key 拆分成多个小 Key。
    • 如果必须要存,可以考虑分片存储。
    • 删除大 Key 时,使用异步删除,避免阻塞。

数据库Redis
https://blog.xirui.work/posts/367dea3.html
作者
xirui
发布于
2025年5月10日
许可协议