Skip to content

Redis面经

为什么Redis运行比较快?

Redis之所以运行比较快的原因有一下几个:

  1. 数据存储在内存中:Redis的数据存储在内存中,而内存的
  2. 读写速度远远快于硬盘。这使得 Redis 能够实现非常快速的读写操作。
  3. 单线程处理请求:Redis 是单线程的,因此可以避免线程切换和锁竞争等问题,提高了 CPU 的利用率和性能。
  4. 高效的数据结构:Redis 提供了多种高效的数据结构,如哈希表、有序集合等,这些数据结构能够快速地进行插入、删除、查找和排序等操作。
  5. 异步 I/O:Redis 使用异步 I/O 技术,可以在等待客户端输入或输出时继续处理其他请求,从而提高了系统的吞吐量。
  6. 高效的持久化机制:Redis 提供了多种持久化机制,如 RDB、AOF 和混合持久化机制,这些机制运行都非常高效,可以在不影响性能的情况下保证数据的安全。

综上所述,Redis 之所以快速,是因为它综合运用了多种优秀的技术和算法,并且针对内存数据库场景做了很多优化。

Redis可以实现什么功能?

Redis是一个开源的基于内存的数据结构存储系统,可以实现如下功能:

1.缓存:Redis 可以作为缓存系统,将热点数据存储在内存中,提高读写性能和响应速度,减少对后端数据存储的压力。

2.消息队列:Redis 的发布订阅功能和 List 数据结构可以实现消息队列的功能,实现异步处理任务、解耦系统组件之间的依赖关系等。

3.计数器和排行榜:Redis 的原子操作和 Sorted Set 数据结构可以实现计数器和排行榜的功能,支持快速地增加、减少和排序操作。

4.分布式锁:Redis的SETNX命令可以实现分布式锁,避免多个客户端同时修改同一个数据,保证数据的一致性和正确性。

5.分布式会话管理:Redis可以存储会话信息,实现分布式回话管理

Redis有哪些数据类型?

Redis常用的数据类型有5种:String字符串类型,List列表类型,Hash哈希表类型,Set集合类型,Sorted Set有序集合类型,如下图所示:

image-20240620183623595

这5种常用类型的用途如下:

  1. String(字符串类型):存储 Session 信息、存储缓存信息(如详情页的缓存)、存储整数信息,可使用 incr 实现整数+1,和使用 decr 实现整数 -1;
  2. List(列表类型):实现简单的消息队列、存储某项列表数据;
  3. Hash(哈希表类型):常见使用场景是:存储 Session 信息、存储商品的购物车,购物车非常适合用哈希字典表示,使用人员唯一编号作为字典的 key,value 值可以存储商品的 id 和数量等信息、存储详情页信息;
  4. Set(集合类型):是一个无序并唯一的键值集合,它的常见使用场景是:关注功能,比如关注我的人和我关注的人,使用集合存储,可以保证人员不会重复;
  5. Sorted Set(有序集合类型):相比于 Set 集合类型多了一个排序属性 score(分值),它的常见使用场景是:可以用来存储排名信息、关注列表功能,这样就可以根据关注实现排序展示了。

Redis如何实现消息队列?

Redis 实现消息队列的常见方法有两种:使用 Redis 中提供的发布订阅(Pub/Sub)功能或 List 数据结构。

具体实现方法如下:

  1. 发布订阅(Pub/Sub):Redis 的发布订阅功能可以实现消息队列的发布和订阅功能。生产者将消息发布到指定的通道(channel)中,消费者可以订阅该通道,接收并处理消息。该模式支持一对多的消息传递,一个消息可以被多个消费者接收,也可以实现分组订阅,将不同的消费者分为不同的组,实现广播或点对点的消息传递。
  2. List 数据结构:Redis 的 List 数据结构可以实现消息队列的入队和出队操作。生产者将消息插入到 List 的尾部,消费者从 List 的头部获取消息,实现先进先出(FIFO)的消息处理。可以使用阻塞式的 POP 操作来实现消费者等待新消息到达,也可以使用定时轮询的方式来获取新消息。

发布订阅 VS List

发布订阅和 List 的特点如下:

  • 发布订阅:发布订阅是一种一对多的消息传递方式,即一个消息可以被多个消费者同时接收。生产者发布消息到指定的通道,消费者订阅该通道,接收并处理消息。如果消费者在消息发布之前订阅了该通道,则可以接收到该消息;如果在消息发布后才订阅,则无法接收到之前的消息。发布订阅模式适用于广播或点对点的消息传递,但是无法保证消息的顺序。
  • List 数据结构:List 数据结构是一种先进先出(FIFO)的消息传递方式,即消息入队列时在队列尾部添加,消息出队列时从队列头部删除。生产者将消息插入到 List 的尾部,消费者从 List 的头部获取消息。可以使用阻塞式的 POP 操作来实现消费者等待新消息到达,也可以使用定时轮询的方式来获取新消息。List 模式适用于需要按顺序处理消息的场景,但是不适用于广播消息传递。

它们的区别如下:

  • 发布订阅:优点是可以实现一对多的消息传递,支持分组订阅和模式匹配订阅;缺点是无法保证消息的顺序和可靠性,消费者无法感知到生产者是否已经处理完该消息。
  • List 数据结构:优点是可以保证消息的顺序和可靠性,消费者可以在处理完一个消息后再获取下一个消息;缺点是不适用于一对多的消息传递。

Redis如何实现分布式锁?

Redis 作为一个独立的三方系统(通常被作为缓存中间件使用),其天生的优势就是可以作为一个分布式系统来使用,因此使用 Redis 实现的锁都是分布式锁,如下图所示:

image-20240620183639660

在 Redis 中实现分布式锁可以使用 SETNX 和 EXPIRE 命令来实现,SETNX 是 "SET if Not eXists" 的缩写,是一个原子性操作,用于在指定的 key 不存在时设置 key 的值。如果 key 已经存在,SETNX 操作将不做任何事情,返回失败;如果 key 不存在,SETNX 操作会设置 key 的值,并返回成功。而 EXPIRE 是设置锁的过期时间的,主要为了防止死锁的发生,SETNX + EXPIRE 的实现命令如下:

image-20240620183650886

其中“nx”表示 not exists 不存在则设置 key,“ex 10”表示过期时间为 10 秒,“mylock”值为 key,“lock”值为 value。

分布式锁问题

SETNX 和 EXPIRE 一起使用可以实现分布式锁的功能,但存在锁误删的问题,比如线程 1 设置的过期时间为 5s,而线程 1 执行了 7s,那么在第 5s 之后锁过期了,那么其他线程就可以拥有这把锁了,之后线程 1 执行完业务,又执行了锁删除操作,那么此时锁就被误删了。

解决方案

此时可以每个锁的 value 中添加拥有者的标识,删除之前先判断是否是自己的锁,如果是则删除,否则不删除。但是判断和删除之间不是原子性操作,所以依然有问题。此时可以使用 lua 脚本来判断并删除锁,lua 脚本可以保证 redis 中多条语句执行的原子性,所以就可以解决此问题了。

PS:如果觉得自己实现 lua 脚本比较麻烦,可以使用 Redisson 框架来实现分布式锁。它通过简单 API 可以实现分布式锁,并且没有上述问题,它的底层也是通过 lua 脚本来实现的,只不过框架已经帮开发者封装好了,这样开发者就可以把更多精力放在业务上,而无序担心分布式锁的操作问题了。

如何使用Redisson实现分布式锁?

在分布式系统中,当多个线程(或进程)同时操作同一个资源时,为了保证数据一致性问题,所以就需要一种机制来确保在同一时间只有一个线程(或进程)能够对资源进行修改,这就是分布式锁的作用。

分布式锁是一种在分布式环境下的锁实现,它允许在多个进程或服务器之间协调对共享资源的访问或操作。分布式锁的关键特性是它能够在集群内的不同节点间保持其锁定状态,使得某一时刻仅有一个客户端能够获取并持有该锁,从而确保对公共资源的原子性和一致性访问。

SETNX存在的问题

虽然可以使用 SETNX 命令方便的实现分布式锁,但是 SETNX 存在以下问题:

  1. 死锁问题:SETNX 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。
  2. 锁误删问题:SETNX 设置了超时时间,但因为执行时间太长,所以在超时时间之内锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。
  3. 不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
  4. 无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。

那么如何解决以上这些问题呢?这就是今天要讲的重点 Redisson,使用 Redisson 框架就可以解决以上这些问题了

什么是Redisson?

Redisson 是一个开源的用于操作 Redis 的 Java 框架。与 Jedis 和 Lettuce 等轻量级的 Redis 框架不同,它提供了更高级且功能丰富的 Redis 客户端。它提供了许多简化 Redis 操作的高级 API,并支持分布式对象、分布式锁、分布式集合等特性。

Redisson 官网:https://redisson.org/open in new window

源码:https://github.com/redisson/redisson

Redisson特性说明

Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。

Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。

Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。

Redisson使用分布式锁

① 添加Redisson框架支持

xml
<!-- Redisson -->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.25.2</version> <!-- 请根据实际情况使用最新版本 -->
</dependency>

② 配置RedissonClient对象

将RedissonClient重写,存放到IoC容器,并且配置连接的Redis服务器信息。

java
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 也可以将 redis 配置信息保存到配置文件
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

③ 创建分布式锁

Redisson 分布式锁的操作和 Java 中的 ReentrantLock(可重入锁)的操作很像,都是先使用 tryLock 尝试获取(非公平)锁,最后再通过 unlock 释放锁,具体实现如下:

java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class LockController {
    @Autowired
    private RedissonClient redissonClient;
    @GetMapping("/lock")
    public String lockResource() throws InterruptedException {
        String lockKey = "myLock";
        // 获取 RLock 对象
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁(尝试加锁)(锁超时时间是 30 秒)
            boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS);
            if (isLocked) {
                // 成功获取到锁
                try {
                    // 模拟业务处理
                    TimeUnit.SECONDS.sleep(5);
                    return "成功获取锁,并执行业务代码";
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                // 获取锁失败
                return "获取锁失败";
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "获取锁成功";
    }
}

实现公平锁

Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑),想要把它变成公平锁可使用以下代码实现:

RLock lock = redissonClient.getFairLock(lockKey);  // 获取公平锁

实现读写锁

Redisson还可以创建读写锁,如下代码所示:

RReadWriteLock lock = redissonClient.getReadWriteLock(lockKey); // 获取读写锁
lock.readLock();  // 读锁
lock.writeLock(); // 写锁

读写锁的特点就是并发性能高,它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。

实现联锁

Redisson 也支持联锁,也叫分布式多锁 MultiLock,它允许客户端一次性获取多个独立资源(RLock)上的锁,这些资源可能是不同的键或同一键的不同锁。当所有指定的锁都被成功获取后,才会认为整个操作成功锁定。这样能够确保在分布式环境下进行跨资源的并发控制。 联锁的实现示例如下:

java
// 获取需要加锁的资源
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
// 联锁
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);
try {
    // 一次性尝试获取所有锁
    if (multiLock.tryLock()) {
        // 获取锁成功...
    }
} finally {
    // 释放所有锁
    multiLock.unlock();
}

以上 Redisson 只是对 Redis 单节点加锁实现分布式锁的,但分布式环境中的 Redis 一定采用的是多机部署(主从、哨兵或集群),那么在多机环境下以上代码实现分布式锁还能用吗?如果不能用,那该如何处理呢?

Redis如何保证数据不丢失?

因为,Redis 保证数据不丢失的主要手段有两个:

  1. 持久化
  2. 集群运行

Redis持久化

持久化是指将数据从内存中存储到持久化存储介质中(如硬盘)的过程,以便在程序重启或者系统崩溃等情况下,能够从持久化存储介质中恢复数据。

Redis 4.0 之后支持以下 3 种持久化方案:

  1. RDB(Redis DataBase)持久化:快照方式持久化,将某一个时刻的内存数据,以二进制的方式写入磁盘;
  2. AOF(Append Only File)持久化:文件追加持久化,记录所有非查询操作命令,并以文本的形式追加到文件中;
  3. 混合持久化:RDB + AOF 混合方式的持久化,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

RDB持久化

RDB(Redis Database)是将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘的持久化机制。

RDB 持久化机制有以下优缺点:

优点:

  1. 速度快:相对于 AOF 持久化方式,RDB 持久化速度更快,因为它只需要在指定的时间间隔内将数据从内存中写入到磁盘上。
  2. 空间占用小:RDB 持久化会将数据保存在一个压缩的二进制文件中,因此相对于 AOF 持久化方式,它占用的磁盘空间更小。
  3. 恢复速度快:因为 RDB 文件是一个完整的数据库快照,所以在 Redis 重启后,可以非常快速地将数据恢复到内存中。
  4. 可靠性高:RDB 持久化方式可以保证数据的可靠性,因为数据会在指定时间间隔内自动写入磁盘,即使 Redis 进程崩溃或者服务器断电,也可以通过加载最近的一次快照文件恢复数据。

缺点:

  1. 数据可能会丢失:RDB 持久化方式只能保证数据在指定时间间隔内写入磁盘,因此如果 Redis 进程崩溃或者服务器断电,从最后一次快照保存到崩溃的时间点之间的数据可能会丢失。
  2. 实时性差:因为 RDB 持久化是定期执行的,因此从最后一次快照保存到当前时间点之间的数据可能会丢失。如果需要更高的实时性,可以使用 AOF 持久化方式。

所以,RDB 持久化方式适合用于对数据可靠性要求较高,但对实时性要求不高的场景,如 Redis 中的备份和数据恢复等。

AOF持久化

AOF(Append Only File)它是将 Redis 每个非查询操作命令都追加记录到文件(appendonly.aof)中的持久化机制。

AOF 持久化机制有以下优缺点:

优点:

  1. 数据不容易丢失:AOF 持久化方式会将 Redis 执行的每一个写命令记录到一个文件中,因此即使 Redis 进程崩溃或者服务器断电,也可以通过重放 AOF 文件中的命令来恢复数据。
  2. 实时性好:由于 AOF 持久化方式是将每一个写命令记录到文件中,因此它的实时性比 RDB 持久化方式更好。
  3. 数据可读性强:AOF 持久化文件是一个纯文本文件,可以被人类读取和理解,因此可以方便地进行数据备份和恢复操作。

缺点:

  1. 写入性能略低:由于 AOF 持久化方式需要将每一个写命令记录到文件中,因此相对于 RDB 持久化方式,它的写入性能略低。
  2. 占用磁盘空间大:由于 AOF 持久化方式需要记录每一个写命令,因此相对于 RDB 持久化方式,它占用的磁盘空间更大。
  3. AOF 文件可能会出现损坏:由于 AOF 文件是不断地追加写入的,因此如果文件损坏,可能会导致数据无法恢复。

所以,AOF 持久化方式适合用于对数据实时性要求较高,但对数据大小和写入性能要求相对较低的场景,如需要对数据进行实时备份的应用场景。

所以,AOF 持久化方式适合用于对数据实时性要求较高,但对数据大小和写入性能要求相对较低的场景,如需要对数据进行实时备份的应用场景。

混合持久化

Redis 混合持久化是指将 RDB 持久化方式和 AOF 持久化方式结合起来使用,以充分发挥它们的优势,同时避免它们的缺点。

它的优缺点如下:

优点:

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

缺点:

  1. 实现复杂度高:混合持久化需要同时维护 RDB 文件和 AOF 文件,因此实现复杂度相对于单独使用 RDB 或 AOF 持久化方式要高。
  2. 可读性差:AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
  3. 兼容性差:如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

所以,Redis 混合持久化方式适合用于,需要兼顾启动速度和减低数据丢失的场景。但需要注意的是,混合持久化的实现复杂度较高、可读性差,只能用于 Redis 4.0 以上版本,因此在选择时需要根据实际情况进行权衡。

Redis集群

Redis 集群是将原先的单服务器,变为了多服务器,这样 Redis 保存的数据也从一台服务器变成了多台服务器,这样即使有一台服务器出问题了,其他的服务器还有备份数据。所以使用 Redis 集群除了可以保证高可用,还保证了数据不丢失。

Redis 集群运行有以下 3 种方案:

  1. 主从同步
  2. 哨兵模式
  3. Redis Cluster

主从同步

主从同步 (主从复制) 是 Redis 高可用服务的基石,也是多机运行中最基础的一个。我们把主要存储数据的节点叫做主节点 (master),把其他通过复制主节点数据的副本节点叫做从节点 (slave),如下图所示:

image-20240620183733083

在 Redis 中一个主节点可以拥有多个从节点,一个从节点也可以是其他服务器的主节点,如下图所示:

image-20240620183744379

哨兵模式

主从同步存在一个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。 所以我们需要一个自动的工具——Redis Sentinel (哨兵模式) 来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复 (failover) 的能力。 哨兵模式如下所示:

image-20240620183755171

小贴士:Redis Sentinel的最小分配单位是一主一从。

Redis Cluster

Redis Cluster 是 Redis 3.0 版本推出的 Redis 集群方案,它将数据分布在不同的服务区上,以此来降低系统对单主节点的依赖,并且可以大大的提高 Redis 服务的读写性能。 Redis Cluster 架构图如下所示:

image-20240620183810098

从上图可以看出 Redis 的主从同步只能有一个主节点,而 Redis Cluster 可以拥有无数个主从节点,因此 Redis Cluster 拥有更强大的平行扩展能力,也就是说当 Redis Cluster 拥有两个主从节点时,从理论上来讲 Redis 的性能相比于主从来说性能提升了两倍,并且 Redis Cluster 也有自动容灾恢复的机制。

Redis 保证数据不丢失的主要手段有两个:持久化和集群运行。其中持久化有三种实现:RDB、AOF、混合持久化;而集群(运行)也包含了三种实现:主从复制、哨兵模式和 Redis Cluster。

Redis锁什么情况下会自旋?

Redis 锁的“自旋”是指当锁获取失败时,客户端并不立即放弃,而是在一个循环中不断重试,直到成功拿到锁或达到某个终止条件(如超时)。这是一种常见的锁竞争策略。

为了让你快速了解Redis锁在哪些核心场景下会进入自旋状态,我先用一个表格来总结。 20251017152009

💡 如何合理使用自旋锁 自旋锁虽然有用,但使用不当会带来性能问题。以下是几个关键实践建议:

设置合理的重试间隔:在每次重试之间让线程睡眠一小段时间(例如100毫秒),可以有效避免浪费CPU资源和给Redis带来过大压力。粗暴的、无间隔的频繁重试是不可取的。 限制最大重试次数或总超时时间:必须为自旋设置一个上限。例如,最大重试100次,或总尝试时间不超过10秒。这可以防止因网络故障或持有锁的客户端崩溃导致某些线程无限期自旋,从而增强系统的健壮性。 确保锁操作的原子性:加锁时应使用 SET key unique_value NX EX seconds 这样的单条原子命令,而非先SETNX再EXPIRE,以防止设置过期时间失败导致死锁。解锁时,应使用Lua脚本来保证判断锁归属和删除锁操作的原子性,避免误删其他客户端的锁。

💎 总结与备选方案 总而言之,Redis锁的自旋行为主要发生在锁被占用但请求方认为有必要等待并重试的场景下,其核心目的是在高并发环境中确保对共享资源的有序访问。 然而,自旋锁并非万能。如果锁的竞争非常激烈,或者临界区代码执行时间较长,自旋会导致大量的资源浪费。在这种情况下,可以考虑使用更高级的分布式锁解决方案,例如 Redisson 库,它提供了更完善的锁机制,包括自动续期和更高效的等待方式。或者,在业务允许的情况下,也可以尝试使用消息队列来串行化处理请求,避免直接的锁竞争。 希望这些解释能帮助你更好地理解和使用Redis分布式锁。如果你有特定的业务场景,可以分享出来,我们一起探讨更具体的实现方案。

Redis持久化能关吗?怎么关?

数据持久化是指将数据从内存中,保存到磁盘或其他持久存储介质的过程,这样做的目的是为了保证数据不丢失。

而 Redis 的持久化功能默认是开启的,这样做的目的也是为了保证程序的稳定性(防止缓存雪崩、缓存击穿等问题)和数据不丢失。

Redis持久化能关吗?怎么关?

Redis持久化默认是开启的,但可以手动关闭。

Redis4之后它的持久化总共有以下三种方式:

  1. RDB(Redis DataBase)持久化:快照方式持久化,将某一个时刻的内存数据,以二进制的方式写入磁盘。
  2. AOF(Append Only File)持久化:文件追加持久化,记录所有非查询操作命令,并以文本的形式追加到文件中。
  3. 混合持久化:RDB + AOF 混合方式的持久化,Redis 4 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

所以,我们要关闭 Redis 持久化,需要将以上三种持久化方式全部关闭,具体操作如下。

①关闭RDB持久化

在Redis的配置文件redis.conf中,你可以将以下配置项设置为禁用状态:

save ""  # 将save参数列表清空,表示不进行任何条件下的数据保存

或者直接注释掉所有save行以取消RDB持久化。

② 关闭AOF持久化

在redis.conf配置文件中,将以下配置项修改为禁用AOF:

appendonly no  # 设置为no,表示关闭AOF持久化

③ 关闭混合持久化

在redis.conf 配置文件中,将以下配置项修改为禁用混合持久化

rdb-aof-use-rdb-preamble no  # no 表示关闭混合持久化

或者是将混合持久化这行配置给注释掉

==注意:以上持久化关闭之后,都需要重启 Redis 服务才能生效。==

Redis 持久化功能默认是开启的,这样做的目的也是为了保证程序的稳定性(防止缓存雪崩、缓存击穿等问题)和保证数据不丢失。想要手动关闭 Redis 持久化,需要将 RDB、AOF 和混合持久化全部关闭才行,并且关闭之后需要重启 Redis 服务才能生效。

Redis过期删除策略有哪些?

Redis 中的过期删除策略是指在键(key)上设置了过期时间后,Redis 在某个条件触发时会自动删除过期的键。

Redis 中有两种过期删除策略:

  1. 定期删除策略(定时任务方式):Redis 会定期地(默认每秒钟检查 10 次)随机抽取一部分设置了过期时间的键,检查它们是否过期,如果过期则删除。该策略可以通过配置文件中的 hz 参数进行调整。
  2. 惰性删除策略(懒汉式方式):当访问一个键时,Redis 会先检查该键是否过期,如果过期则删除。这意味着过期键可能会在访问时被删除,而不是在过期时立即删除。

Redis 定期删除策略并不会遍历删除每个过期键,而是采用随机抽取的方式删除过期键,同时为了保证过期扫描不影响 Redis 主业务,Redis 的定期删除策略中还提供了最大执行时间,以保证 Redis 正常并高效的运行。

关于"hz"

在 Redis 中,"hz" 是 "hertz" 的缩写。Hertz 是国际单位制中表示频率的单位,表示每秒钟发生的周期数或事件发生的次数。在 Redis 中,"hz" 参数用于表示每秒钟执行定期删除策略的次数,即每秒钟检查过期键的频率。 默认 hz 配置如下图所示:

image-20240620183833677

可以编辑Redis的配置文件1redis.conf,找到并修改一下参数来调整定期删除策略:

hz <value>

默认情况下,hz 参数的值为 10,表示每秒钟进行 10 次检查。可以根据需要增加或减少该值来调整定期删除的频率。例如,将 hz 的值设置为 5,表示每秒钟进行 5 次检查。

修改完成后,保存配置文件并重启 Redis 服务器使配置生效。

什么是缓存雪崩?如何解决?

缓存雪崩是指在缓存中大量的键同时过期或失效,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。

缓存雪崩可能发生的原因包括:

  1. 大量缓存键同时过期:当缓存键设置了相同的过期时间,或者由于某种原因导致大量的键同时失效,会导致缓存雪崩。
  2. 缓存服务器故障:当缓存服务器发生故障,无法提供服务时,请求将直接访问后端服务,导致压力集中在后端服务上。

为了解决缓存雪崩问题,可以采取以下策略:

  1. 设置随机过期时间:为缓存键设置随机的过期时间,避免大量键同时过期的情况发生,减少缓存雪崩的概率。
  2. 实现缓存预热:在系统启动或缓存失效前,提前加载热门数据到缓存中,避免在关键时刻大量请求直接访问后端服务。
  3. 使用分布式缓存:将缓存数据分布在多个缓存节点上,通过分散请求负载来减少单个缓存节点的压力,提高系统的可用性和抗压能力。
  4. 设置熔断机制:在缓存失效的情况下,通过设置熔断机制,直接返回默认值或错误信息,避免请求直接访问后端服务,减轻后端服务的压力。
  5. 实时监控和报警:监控缓存系统的状态和性能指标,及时发现异常情况,并通过报警机制通知运维人员进行处理,减少缓存雪崩的影响。

什么是缓存穿透?如何解决?

缓存穿透是指在缓存系统中,大量的请求查询不存在于缓存和数据库中的数据,导致这些请求直接访问数据库,占用数据库资源,而缓存无法发挥作用的现象。

缓存穿透可能发生的原因包括:

  1. 恶意请求(异常情况):攻击者发送大量恶意请求,故意查询不存在的数据,以触发缓存穿透。
  2. 高并发请求(正常业务):当有大量的并发请求同时查询不存在的数据时,可能会导致缓存无法命中,从而触发缓存穿透。

为了解决缓存穿透问题,可以采取以下策略:

  1. 布隆过滤器(Bloom Filter):布隆过滤器是一种高效的数据结构,可以用于快速判断一个元素是否存在于集合中。在缓存层引入布隆过滤器,可以在查询请求到达时,首先通过布隆过滤器判断该请求对应的数据是否存在于缓存或数据库中,从而避免无效的查询操作。
  2. 缓存空值处理:对于查询数据库返回的空结果,也可以将空结果缓存起来,设置一个较短的过期时间,避免频繁查询数据库。这样在下次查询相同的数据时,可以直接从缓存中获取空结果,而不需要再次查询数据库。
  3. 异步加载缓存:当缓存未命中时,可以异步加载数据到缓存中,避免在高并发场景下直接访问数据库。在异步加载过程中,可以通过互斥锁或分布式锁来保证只有一个线程去加载数据,避免重复加载。
  4. 设置热点数据永不过期:对于一些热点数据,可以将其设置为永不过期,或者过期时间较长,以保证这部分数据始终在缓存中可用。
  5. 限制恶意请求:通过访问频率控制、验证码等手段,限制对缓存的恶意请求,防止攻击者通过查询不存在的数据来触发缓存穿透。

什么是缓存击穿?如何解决?

缓存击穿是指在缓存系统中,某个热点数据过期或失效时,同时有大量的请求访问该数据,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。

缓存击穿可能发生的原因包括:

  1. 热点数据失效:当某个热点数据过期时,此时大量请求访问该数据,导致缓存失效,请求直接访问数据库。
  2. 并发访问热点数据:在高并发环境下,大量的请求同时访问同一个热点数据,导致该热点数据在缓存失效期间被并发地访问,触发缓存击穿。

为了解决缓存击穿问题,可以采取以下策略:

  1. 设置热点数据永不过期或过期时间较长:对于一些热点数据,可以将其设置为永不过期,或者设置一个较长的过期时间,确保热点数据在缓存中可用,减少因为过期而触发的缓存击穿。
  2. 加互斥锁或分布式锁:在访问热点数据时,可以引入互斥锁或分布式锁,保证只有一个线程去访问后端服务或数据库,其他线程等待结果。当第一个线程获取到数据后,其他线程可以直接从缓存获取,避免多个线程同时访问后端服务,减轻压力。
  3. 限制并发访问:通过限制并发访问热点数据的请求量,可以控制请求的流量,避免过多请求同时访问热点数据。

什么是布隆过滤器?如何实现?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它基于位数组和多个哈希函数的原理,可以高效地进行元素的查询,而且占用的空间相对较小,如下图所示:

image-20240620183919383

根据 key 值计算出它的存储位置,然后将此位置标识全部标识为 1(未存放数据的位置全部为 0),查询时也是查询对应的位置是否全部为 1,如果全部为 1,则说明数据是可能存在的,否则一定不存在

也就是说,如果布隆过滤器说一个元素不在集合中,那么它一定不在这个集合中;但如果它说一个元素在集合中,则有可能是不存在的(存在误差)

布隆执行过程

布隆过滤器的具体执行步骤如下:

  1. 在 Redis 中创建一个位数组,用于存储布隆过滤器的位向量。
  2. 初始化多个哈希函数,并将每个哈希函数的计算结果对应的位数组位置设置为 1。
  3. 添加元素到布隆过滤器时,对元素进行多次哈希计算,并将对应的位数组位置设置为 1。
  4. 查询元素是否存在时,对元素进行多次哈希计算,并检查对应的位数组位置是否都为 1。

布隆使用场景

布隆过滤器的主要使用场景有以下几个:

  1. 大数据量去重:可以用布隆过滤器来进行数据去重,判断一个数据是否已经存在,避免重复插入。
  2. 缓存穿透:可以用布隆过滤器来过滤掉恶意请求或请求不存在的数据,避免对后端存储的频繁访问。
  3. 网络爬虫的 URL 去重:可以用布隆过滤器来判断 URL 是否已经被爬取,避免重复爬取。

如何实现布隆过滤器

在 Redis 中不能直接使用布隆过滤器,但我们可以通过 Redis 4.0 版本之后提供的 modules (扩展模块) 的方式引入,它的实现步骤如下。

① 打包RedisBloom插件

git clone https://github.com/RedisLabsModules/redisbloom.gitopen in new window

cd redisbloom

make # 编译redisbloom

编译正常执行完,会在根目录生成一个redisbloom.so文件

② 启用RedisBloom插件

重新启动redis服务,并指定启动RedisBloom插件,具体命令如下:

redis-server redis.conf --loadmoudlue ./src/modules/RedisBloom-master/redisbloom.so

③ 创建布隆过滤器

创建一个布隆过滤器,并设置期望插入的元素数量和误差率,在Redis客户端中输入一下命令:

BF.RESERVE my_bloom_filter 0.01 100000

④ 添加元素到布隆过滤器

在Redis客户端中输入一下命令:

BF.ADD my_bloom_filter leige

⑤ 检查元素是否存在

再Redis客户端中输入一下命令:

BF.EXISTS my_bloom_filter leige

Redis优化方案有哪些?

想要更好地发挥Redis性能,我们可以通过一下手段1来实现。

缩短键值对的存储长度

在 key 不变的情况下,value 值越大操作效率越慢,因为 Redis 对于同一种数据类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 的作者是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。


使用lazy free (延迟删除) 特性

lazy free 特性是 Redis 4.0 新增的一个非常实用的功能,它可以理解为惰性删除或延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O) 单独的子线程处理中,以减少删除删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。

在 Redis 配置文件 redis.conf 中开启 lazy free:

xml
# 开启lazy free机制
lazyfree-lazy-eviction yes

设置合理的过期时间

我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略

禁用长耗时的查询命令

生产环境禁止使用 keys 命令、避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历、通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小、将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力。


使用slowlog优化耗时命令

使用slowlog功能找出最耗时的Redis命令进行相关的优化,以提升Redis的运行速度

使用Pipeline批量操作数据

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

避免大量数据同时失效

如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。

优化客户端使用

在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。

限制Redis内存大小

在 64 位操作系统中 Redis 的内存大小是没有限制的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操作系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,来限制 Redis 的内存大小。

使用物理机部署Redis

使用物理机而非虚拟机安装 Redis,在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现。

合理使用功能持久化策略

对于不重要的数据,我们可以关闭持久化功能,以提升Redis的性能。

使用集群模式增加读写吞吐量

Redis 集群是通过将数据分散存储到多个节点,以提升 Redis 的整体吞吐量的。

如何保证本地缓存的一致性?

有人可能看到“本地缓存”这四个字就会觉得不屑,“哼,现在谁还用本地缓存?直接用分布式缓存不就完了嘛”。

然而,这就像你有一辆超级豪华的房车一样,虽然它空间很大,设备很全,但你去市中心的时候,依然会开小轿车一样,为啥?好停车啊!所以,不同的缓存类型是有不同得使用场景的。

并且,为了防止缓存雪崩问题、缓存击穿问题,我们通常会采用多级缓存的解决方案,所谓的多级缓存就是:分布式缓存(Redis 或 Memcached)+本地缓存(Guava Cache 或 Caffeine)。因为分布式缓存可能会失效、可能会挂掉,所以为了系统的稳定性,多级缓存策略使用的非常广泛。

那么,问题来了,怎么保证本地缓存的一致性?

所谓的一致性是指在同时使用缓存和数据库的场景下,要确保数据在缓存与数据库中的更新操作保持同步。也就是当对数据进行修改时,无论是先修改缓存还是先修改数据库,最终都要保证两者的数据是一样的,不会出现数据不一样的问题。

如何保证本地缓存的一致性?

在分布式系统中,使用本地缓存最大的问题就是一致性问题,所谓的一致性问题指的是当数据库发生数据变更时,缓存也要跟着一起变更。而分布式系统中每台机器都有自己的本地缓存,所以想要保证(本地缓存的)一致性是一个比较难的问题,但通过以下手段可以最大程度的保证本地缓存的一致性问题。

设置本地缓存短时间内失效

设置本地缓存短时间内失效,短的存活周期,保证了数据的时效性比较高,当数据失效之后,再次访问数据就会拉取新的数据了,这样能尽可能的保证数据的一致性。

它的特点是:代码实现简单,不需要写多余的代码;缺点是,效果不是很明显,不适合高并发的系统。

通过配置中心协调和同步

通过微服务中的配置中心(例如 Nacos)来协调,因为所有服务器都会连接到配置中心,所以当数据修改之后,可以修改配置中心的配置,然后配置中心再把配置变更的事件推送给各个服务,各个服务感知到配置中心的配置发生更改之后,再更新自己的本地缓存,这样就实现了本地缓存的数据一致性。

本地缓存自动更新功能

使用本地缓存框架的自动更新功能,例如 Caffeine 中的 refresh 功能来自动刷新缓存,这样就可以设置很短的时间来更新最新的数据,从而也能尽可能的保证数据的一致性,如下代码所示:

java
// 创建 Caffeine 缓存实例
Cache<String, String> caffeineCache = Caffeine.newBuilder()
// 设置缓存项在 5s 后开始自动更新
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 自定义缓存更新逻辑(即获取新值逻辑)
.build(new CacheLoader<String, String>() {
    @Override
    public void reload(String key, String oldValue) throws Exception {
        // 模拟更新缓存的操作
        updateCache(key, oldValue);
    }
});

实际工作中会使用哪种方案?

不同的业务系统,会采用不同的解决方案,例如以下这些场景和对应的解决方案:

  • 如果对数据一致性要求不是很高,并且程序的并发压力不大的情况下,可能使用方案 1,也就是设置本地缓存短时间内失效的解决方案,因为它的实现最简单。
  • 如果对数据的一致性要求极高,且有配置中心的情况下,可使用配置中心协调和同步本地缓存。
  • 相反,如果对一致性要求没有那么高,且为高并发的系统,那么可以采用本地缓存的自动更新功能来实现。

在多级缓存中,本地缓存是不可或缺的组成部分,而想要保证本地缓存的数据一致性,可能采用:设置较短的本地缓存过期时间、通过配置中心来协调和同步本地缓存,以及使用本地缓存框架的自动更新功能保证数据的一致性等解决方案,而不同的业务场景,选择的解决方案也是不同的。

为什么Redis为什么不用C语言的String而是自己造一个SDS

String的特性

String 的特性主要包含下面4点:

  • String 是最基本的 Redis 数据类型;
  • String 是二进制安全,存入和获取的数据相同;
  • Redis 字符串存储字节序列,包括文本、序列化对象和二进制数组;
  • String 存储的 value 值最大为 512MB;

String 高频指令如下表:

image.png

如下图,实例展示了 String 常用指令:

img

实现原理

Redis 底层是C语言实现的,但是 Redis的String数据对象并没有直接使用C语言传统的字符串,而是自创了一套SDS,接下来分析它的底层实现。

SDS

SDS,simple dynamic string,简单动态字符串。

SDS 的结构定义在 sds.h 文件中,每个 sds.h/sdshdr 结构表示一个 SDS 值,在 Redis 3.2 版本之后,SDS 由一种数据结构变成了 5 种数据结构,如下源码截图:

img

  • sdshdr5:存储大小为 32 byte = 2^ 5 ,被弃用;
  • sdshdr8:存储大小为 256 byte = 2^ 8;
  • sdshdr16:存储大小为 64KB = 2 ^16
  • sdshdr32:存储大小为 4GB = 2^ 32;
  • sdshdr64:存储大小为 2^ 64;

5 种数据结构存储不同长度的内容,Redis 会根据 SDS 存储的内容长度来选择不同的结构,源码实现对应 sds.c/sdsReqType,截图如下:

img

为了对 SDS 有一个更好的体感,这里以 sdshdr8 为例,执行指令:SET name Redis

img

执行上述 set 指令后,值对象对应的 SDS 结构如下图:

image.png

SDS 各个属性说明:

len:表示 buf 已用空间的长度,占 4 个字节,不包括 \0; alloc:表示 buf 的实际分配长度,占 4 个字节,不包括 \0; flags:标记当前字节数组是 sdshdr8/16/32/64 中的哪一种,占 1 个字节; buf:表示字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个\0,需要额外占用 1 个字节的开销;

从上面SDS的结构可以看出,SDS依然遵循了C语言中字符串以\0结尾的规则, 但是,\0占用的1 个字节空间并没有计算在SDS的len属性里面。

分析完 SDS 的结构,我们会问,SDS 在 Redis 中是如何存放的呢?

因为 Redis 的数据类型有很多(String、List、Set、Hash等等),不同数据类型会包含相同的元数据,所以值对象并不是直接存储,而是被包装成 redisObject 对象(源码位于 server.h中),其定义如下图:

image-20240624113048907

所以,SDS 在 Redis Server 端的存储如下图:

image-20240624113232354

另外,为了节省内存空间,Redis 还做了如下优化:

当保存 Long 类型整数,RedisObject 中的指针直接赋值为整数数据,这样就不用额外的指针指向整数。这种方式称为 int 编码方式。 当保存字符串数据,且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样可以避免内存碎片。这种方式称为 embstr 编码方式。 当保存字符串数据,且字符串大于 44 字节时,Redis 不再把 SDS 和 RedisObject 放在一起,而是给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种方式称为 raw 编码模式。 下图为 int、embstr 和 raw 这三种编码模式的对比: image-20240624113338867

如果想查看一个值对象是采用哪种编码模式,可以使用 OBJECT ENCODING((大小写不敏感)命令,下面给了几个示例截图:

image-20240624113411220

到此,SDS 的实现原理分析完成,需要补充的是:Redis 官方为了保证 String 的性能,在 SDS 设计上采用了两个非常优秀的设计:空间预分配 和 惰性空间释放。

空间预分配

在对 SDS 进行修改操作时(追加字符串,拷贝字符串等),通常会调用 sds.c/sdsMakeRoomFor 方法对 SDS 的剩余容量进行检查,如有必要会对 SDS 进行扩容,当计算修改之后字符串(用target_string表示)的目标长度之后分以下几种情况:

剩余的 freespace 足够容纳 target_string 和末尾\0字符,则不作任何操作 剩余的 freespace 不够容纳 target_string 和末尾的\0字符 当target_string_size < 1MB,则会直接分配2 * target_string_size 的空间用于存储字符串 当target_string_size >= 1MB,则会再额外多分配1MB的空间用于存储字符串(target_string_size + 1024*1024) img

惰性空间释放

当 SDS 字符串缩短时, 空余出来的空间并不会直接释放,而是会被保留,等待下次再次使用,字符串缩短操作需要更新 sdshdr 头中的 Len 字段以及alloced buffer中的\0字符的位置,如下源码截图,在更新字符串长度的过程中并没有涉及到内存的重分配策略,只是简单的修改sdshdr 头中的 Len 字段。 image-20240624113610394

image-20240624113623455

SDS的缺点

从上面 SDS 的结构可以看出,SDS 除了存储 String 的内容外,还需要额外的内存空间记录数据长度、空间使用等信息,这个就导致了 SDS 的一个比较大的缺点:占内存。那么有什么更好的数据结构呢?我们下篇文章会进行分析。

不过,计算机领域很多时候都在空间和时间上的一种权衡。而Redis String 这种浪费内存换取读写速度就是一个很好的体现。

SDS 与 C字符串比较

获取字符串长度复杂度 C字符串不记录长度,获取长度必须遍历整个字符串,复杂度为O(N),SDS 在 len 属性中记录了 SDS 本身的长度, 获取 SDS 长度的复杂度为 O(1) ;

缓冲区溢出 C字符串不记录自身的长度,每次增长或缩短一个字符串,都要对底层的字符数组进行一次内存重分配操作。如果在 append 操作之前没有通过内存重分配来扩展底层数据的空间大小,就会产生缓存区溢出;如果进行 trim 操作之后没有通过内存重分配来释放不再使用的空间,就会产生内存泄漏;

SDS 通过未使用空间解除了字符串长度和底层数据长度的关联,3.0版本用 free属性记录未使用空间,3.2版本用 alloc属性记录总的分配字节数量。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化的空间分配策略,解决了字符串拼接和截取的空间问题;

二进制安全 C 字符串以 \0结尾(即 以 \0判断字符串结束),所以在 C字符串的内容里面不能包含 \0,否则会被认为是字符串结尾,因此,C字符串只能保存文本数据,不能保存像图片这样的二进制数据;

而 SDS 的 API 会以处理二进制的方式来处理存放在 bu f数组里的数据,不会对里面的数据做任何的限制。SDS 使用 len 属性来判断字符串是否结束,而不是空字符。

两者比较归纳如下表: image-20240624115044383

本网站支持IPV6 | Powered by XiaoSheng