Redis提供了丰富的过期机制,允许用户为键设置一个生存时间(TTL,Time To Live),当键的生存时间到期时,Redis会自动删除该键。为了高效地管理过期键,Redis采用了两种主要的过期策略:定时删除(Active Expiration)和惰性删除(Lazy Expiration)。这两种策略相辅相成,确保了在高并发环境下既能及时删除过期键,又能避免对性能造成过大影响。

1、定时删除(Active Expiration)

(1)、概述

定时删除(Active Expiration)是一种主动的过期策略,Redis会定期检查并删除那些已经过期的键。具体来说,Redis会在后台线程中运行一个定时任务,每隔一段时间扫描一定数量的过期键,并将它们从数据库中删除。

(2)、定时删除的工作原理

  • 过期字典:每个Redis数据库都有一个过期字典(expires字典),用于存储键过期的时间。每当用户为某个键设置了过期时间(例如通过EXPIRE、SETEX、PEXPIRE等命令),Redis会将该键及其过期时间记录到过期字典中。

  • 定时任务:Redis在后台运行一个定时任务,每隔一段时间(通常是100毫秒)检查过期字典中的键。每次检查时,Redis会随机选择一部分桶(bucket),并对这些桶中的键进行过期检查。如果某个键的过期时间已经到达,Redis会立即将其删除。

  • 分批处理:为了避免一次性扫描过多的键导致性能下降,Redis采用分批处理的方式。每次定时任务只会检查一定数量的桶,并且每个桶中只会检查固定数量的键。这样可以确保定时任务不会占用过多的CPU资源,影响其他操作的性能。

  • 自适应调整:Redis会根据系统的负载情况动态调整定时任务的频率和扫描的键数。当系统负载较高时,Redis会减少扫描的频率和键数;当系统负载较低时,Redis会增加扫描的频率和键数,以确保过期键能够及时被删除。

(3)、定时删除的优点

  • 及时性:定时删除策略能够主动检查并删除过期键,确保过期键不会长期存在于内存中,从而减少了内存占用。

  • 防止内存泄漏:通过定期清理过期键,Redis可以有效防止内存泄漏,确保系统的稳定性和可靠性。

(4)、定时删除的缺点

  • CPU开销:虽然Redis采用了分批处理的方式,但定时删除仍然会占用一定的CPU资源,特别是在过期键较多的情况下,可能会对性能产生一定的影响。

2、惰性删除(Lazy Expiration)

(1)、概述

惰性删除是一种被动的过期策略,Redis不会主动检查过期键,而是在访问某个键时才检查其是否已经过期。如果该键已经过期,Redis会立即删除它,并返回相应的结果(如 nil 或 null)。

(2)、惰性删除的工作原理

  • 按需检查:当用户尝试访问某个键时(例如通过GET、HGET、ZSCORE等命令),Redis会首先检查该键是否存在于过期字典中。如果该键存在且已经过期,Redis会立即将其删除,并返回nil或null,表示该键不存在或已过期。

  • 只检查访问的键:惰性删除只会检查那些被访问的键,而不会主动扫描所有键。因此,对于那些从未被访问的过期键,Redis不会立即删除它们,而是等到它们被访问时才会删除。

  • 无CPU开销:由于惰性删除只在访问键时进行检查,因此它不会占用额外的CPU资源,也不会影响其他操作的性能。

(3)、惰性删除的优点

  • 无CPU开销:惰性删除不会主动扫描过期键,因此不会占用额外的CPU资源,适用于高并发场景。

  • 简单高效:惰性删除的实现非常简单,只需在访问键时进行一次检查,减少了代码复杂度和维护成本。

(4)、惰性删除的缺点

  • 延迟删除:惰性删除只会删除那些被访问的过期键,因此对于那些从未被访问的过期键,Redis不会立即删除它们。这可能导致内存占用较高,特别是在大量键过期但未被访问的情况下。

3、Redis实际用的过期策略

为了兼顾及时性和性能,Redis采用了(定时删除 + 惰性删除)的混合策略。

具体来说:

  • 定时删除:Redis会定期扫描并删除过期键,确保过期键不会长期存在于内存中。
  • 惰性删除:当用户访问某个键时,Redis会检查该键是否已经过期,如果是,则立即删除它。

通过这种混合策略,Redis既能够在高并发环境下保持良好的性能,又能够及时删除过期键,防止内存泄漏。

4、过期键的删除流程

前面说的是Redis的键过期的策略,那么发现过期后,Redis是怎么删除的呢?

当Redis决定删除一个过期键时,它会执行以下步骤:
(1)、检查过期时间:Redis首先检查该键是否存在于过期字典中。如果该键存在且已经过期,则进入下一步。

(2)、删除键:Redis会从数据库中删除该键,并更新相关的数据结构(如哈希表、跳跃表等)。如果该键是Hash、List、Set或Sorted Set类型,Redis还会递归删除该键下的所有子元素。

(3)、通知AOF和RDB:如果启用了AOF(Append Only File)持久化,Redis会将删除操作写入AOF文件。如果启用了RDB(Redis Database Backup)持久化,Redis会在下次生成快照时将该键排除在外。

(4)、触发事件:Redis会触发expired事件,通知其他模块或插件该键已被删除。例如,Redis可能会触发Pub/Sub事件,通知订阅者该键已过期。

5、配置参数

Redis 提供了一些配置参数,允许用户调整过期策略的行为:

  • hz:控制Redis的事件循环频率,默认值为10。hz参数决定了Redis每秒钟执行定时任务的次数。较高的hz值可以提高定时删除的及时性,但也可能增加CPU开销。

  • active-expire-effort:控制定时删除任务的强度,默认值为10。active-expire-effort参数决定了每次定时任务扫描的桶数和键数。较高的值可以提高定时删除的效率,但也可能增加CPU 开销。

  • maxmemory-policy:控制Redis在内存不足时的淘汰策略。
    常用的淘汰策略包括:

  • volatile-lru:仅淘汰设置了过期时间的键,使用 LRU(最近最少使用)算法。

  • allkeys-lru:淘汰所有键,使用 LRU 算法。

  • volatile-ttl:仅淘汰设置了过期时间的键,优先淘汰 TTL 较短的键。

  • volatile-random:仅淘汰设置了过期时间的键,随机选择键进行淘汰。

  • allkeys-random:淘汰所有键,随机选择键进行淘汰。

6、过期策略总结

Redis的过期策略通过(定时删除+惰性删除)的混合方式,确保了在高并发环境下既能及时删除过期键,又能避免对性能造成过大影响。定期扫描并删除过期键,确保过期键不会长期存在于内存中,防止内存泄漏。同时在访问键时会检查其是否过期,只删除那些被访问的过期键,避免不必要的CPU开销。通过结合定时删除和惰性删除,Redis既能够在高并发环境下保持良好的性能,又能够及时删除过期键,确保系统的稳定性和可靠性。

7、Redis过期事件监听及实现

在java中,可以通过Redis的Redis Keyspace Notifications来监听Redis键的过期事件。
Redis提供了__keyevent@__:expired通道,当某个键过期时,Redis会发布一条消息到该通道。我们可以通过订阅这个通道来监听键的过期事件。

(1)、Keyspace Notifications(键空间通知)

1、概述

在Redis中,NOTIFY_KEYSPACE_EVENTS是一个配置参数,用于启用和控制Keyspace Notifications(键空间通知)。通过这个参数,Redis可以发布特定类型的事件到Pub/Sub通道,允许客户端监听这些事件。
NOTIFY_KEYSPACE_EVENTS的值是一个字符串,由多个字符组成,每个字符代表一种类型的事件。你可以根据需要组合这些字符来启用或禁用特定的事件类型。

2、常用组合
  • Ex:启用所有过期事件(E表示键事件,x表示过期事件)。这是最常见的组合之一,适用于监听键的过期事件。(比较常用)

  • AKE:启用所有事件(A表示所有事件,K表示键空间事件,E表示键事件)。这个组合会启用所有的键空间和键事件,适用于需要监听所有类型的键变化场景。

  • g:启用通用命令事件。适用于监听对键进行增删改的操作,如DEL、EXPIRE等。

  • Kg:启用键空间事件和通用命令事件。适用于需要监听键的变化以及通用命令操作的场景。

  • Kx:启用键空间事件和过期事件。适用于需要监听键的变化以及键过期事件的场景。

3、解释下E和K区别

E(Keyevent Events)表示键事件
发布到__keyevent@__:通道,包含具体的事件类型和受影响的键名,适用于需要监听具体的命令操作的场景。

如果你需要记录Redis中的所有操作日志,包括每个命令的具体类型,E是一个合适的选择。你可以通过E监听到所有的命令操作,并将这些操作记录到日志文件中,方便后续审计或分析。

K(Keyspace Events)表示键空间事件
发布到__keyspace@__:通道,只包含键的变化信息,不知道到底是什么命令操作的键,适用于只需要监听键的变化的场景。

如,当某个缓存键过期时,你可以通过K监听到expired事件,不用关心具体怎么造成的(人为删除或自动过期等),此时可以立即从数据库中重新加载数据到缓存中。

(2)、java实现步骤

1、启用Redis Keyspace Notifications

要使用Redis的Keyspace Notifications功能,首先需要在Redis配置中启用它。你可以通过修改Redis配置文件(redis.conf)或在启动Redis时传递参数来启用该功能。

方法1:修改redis.conf文件
找到notify-keyspace-events配置项,并将其设置为Ex,表示启用键过期事件的通知:
notify-keyspace-events Ex

方法2:启动指定参数
如果你使用的是Docker或其他方式启动Redis,可以在启动命令中添加参数:

redis-server --notify-keyspace-events Ex

完整示例如:

docker run -d \
  --name my-redis \
  -p 6379:6379 \
  -e REDIS_REPLICATION_MODE=master \
  -e REDIS_PASSWORD=123456 \
  -e NOTIFY_KEYSPACE_EVENTS=Ex \
  -v /path/to/redis-data:/data \
  redis:latest \
  redis-server --requirepass 123456 --appendonly no --save "" --notify-keyspace-events Ex

命令参数解释:

  • -d:以守护进程模式(后台运行)启动容器。
  • –name my-redis:为容器指定名称my-redis,方便后续管理和操作。
  • -p 6379:6379:将主机的6379端口映射到容器的637 端口,使得外部可以访 Redis服务。
  • -e REDIS_PASSWORD=123456:设置Redis的密码为123456。Redis客户端在连接时需要提供此密码。
  • -e NOTIFY_KEYSPACE_EVENTS=Ex:启用Keyspace Notifications,允许监听键过期事件(E表示所有事件,x表示过期事件)。
  • -v /path/to/redis-data:/data:将主机的/path/to/redis-data目录挂载到容器的/data目录,用于持久化Redis数据。你可以根据需要更改路径。
  • redis:latest:使用最新的Redis官方镜像。
  • redis-server --requirepass 123456 --appendonly no --save “” --notify-keyspace-events Ex:启动 Redis 服务器,并通过命令行参数进一步配置:
    • –requirepass 123456:设置Redis的密码为 123456。
    • –appendonly no:禁用AOF持久化。
    • –save “”:禁用RDB持久化。
    • –notify-keyspace-events Ex:启用Keyspace Notifications,允许监听键过期事件。
2、Spring Boot实现-1

在Spring Boot项目中实现Redis Key过期事件的监听。
我们先创建RedisMessageListenerContainer,这是Redis的消息事件容器,用于管理Redis的相关事件。
在定义一个监听实现类MessageListener来接收监听,并针对监听做出业务处理。
最后将具体监听实现类添加到Redis事件容器中即可。

(1)、添加依赖
<dependencies>
    <!-- Spring Boot Starter for Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter for Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Redis Java Client (Lettuce or Jedis) -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>
</dependencies>
(2)、配置文件

在 application.yml 或 application.properties 中配置 Redis 连接信息

spring:
  redis:
    database: 0
    host: localhost
    port: 6379
    password: your_password   如果有密码
    timeout: 10s
	lettuce:
	  max-active: 200
	  max-wait: -1ms
      max-idle: 10
	  min-idle: 0
(3)、创建Redis配置类

创建一个配置类RedisConfig,用于配置RedisMessageListenerContainer和 MessageListenerAdapter,并订阅 Redis 的过期事件通道。

代码示例:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisConfig {

    @Bean
    RedisMessageListenerContainer redisContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 订阅 __keyevent@0__:expired 通道,监听数据库 0 中的键过期事件
        container.addMessageListener(listenerAdapter, new PatternTopic("__keyevent@0__:expired"));
        return container;
    }

    @Bean
    MessageListenerAdapter messageListenerAdapter(KeyExpiredEventReceiver receiver) {
        // 指定处理方法
        return new MessageListenerAdapter(receiver, "handleKeyExpiredEvent");
    }

    @Bean
    KeyExpiredEventReceiver keyExpiredEventReceiver() {
        return new KeyExpiredEventReceiver();   // 具体监听的类
    }
}
(4)、监听实现类

创建一个KeyExpiredEventReceiver类,用于处理 Redis 发布的键过期事件。我们将在这个类中定义handleKeyExpiredEvent方法,该方法会在接收到过期事件时被调用。

代码示例:

import org.springframework.stereotype.Component;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class KeyExpiredEventReceiver implements MessageListener {

    private static final Logger logger = LoggerFactory.getLogger(KeyExpiredEventReceiver.class);

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取过期的键名
        String expiredKey = message.toString();
        handleKeyExpiredEvent(expiredKey);
    }

    public void handleKeyExpiredEvent(String expiredKey) {
        logger.info("Key '{}' has expired", expiredKey);
        // 在这里可以添加自定义逻辑,例如记录日志、触发其他业务操作等
    }
}
3、Spring Boot实现方式-2

除了上面的方式之外,还支持隐式的实现方式。第1,2步骤同上

(3)、配置类

配置类,只需要注入RedisMessageListenerContainer 管理即可,无需指定订阅的主题

/**
     * Redis 消息监听器容器.
     *
     * @param redisConnectionFactory the redis connection factory
     * @return the redis message listener container
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        return redisMessageListenerContainer;
    }


    /**
     * Redis Key失效监听器注册为Bean.
     *
     * @param redisMessageListenerContainer the redis message listener container
     * @return the redis event message listener
     */
    @Bean
    public RedisEventMessageListener redisEventMessageListener(RedisMessageListenerContainer redisMessageListenerContainer) {
        return new RedisEventMessageListener(redisMessageListenerContainer);    // 实现类加入到redis事件容器中
    }
(4)、实现类

通过实现KeyExpirationEventMessageListener ,直接实现了对过期key的监听,而无需在显示指定需要订阅的主题。

import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import javax.annotation.Resource;

/**
 *  当redis中的key过期时,触发一次事件。
 */
@Slf4j
public class RedisEventMessageListener extends KeyExpirationEventMessageListener {

    @Resource
    private RedisLock redisLock;

    /**
     * Instantiates a new Redis event message listener.
     * @param listenerContainer the listener container
     */
    public RedisEventMessageListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    protected void doHandleMessage(Message message) {
        String key = message.toString();      // 过期的key
        log.info("开始处理redis过期键-->"+key);
      	// 处理业务逻辑
      	
        log.info("redis过期键处理完毕!");
    }
}
4、其他注意事项

(1)、Redis 版本:确保你使用的Redis版本支持Keyspace Notifications。Redis 2.8及以上版本都支持该功能。

(2)、性能影响:启用Keyspace Notifications会对Redis性能产生一定的影响,特别是在大量键过期的情况下。因此,建议在生产环境中谨慎使用,并根据实际需求调整配置。

(3)、多数据库支持:如果你使用了多个Redis数据库(如 db0、db1 等),你需要为每个数据库单独订阅相应的过期事件通道。例如, keyevent@1:expired 表示监听数据库 1 中的键过期事件。

(4)、消息格式:Redis发布的过期事件消息格式为键名本身,因此在onMessage方法中可以直接获取过期的键名。

学海无涯苦作舟!!!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐