Redis篇-7--原理篇6--过期机制(定时删除,惰性删除,Redis过期事件监听和Java实现)

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,可以在启动命令中添加参数:

java 复制代码
redis-server --notify-keyspace-events Ex

完整示例如:

java 复制代码
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)、添加依赖
java 复制代码
<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 连接信息

java 复制代码
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 的过期事件通道。

代码示例:

java 复制代码
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方法,该方法会在接收到过期事件时被调用。

代码示例:

java 复制代码
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 管理即可,无需指定订阅的主题

java 复制代码
/**
     * 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的监听,而无需在显示指定需要订阅的主题。

java 复制代码
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方法中可以直接获取过期的键名。

学海无涯苦作舟!!!

相关推荐
thekenofdis24 分钟前
Lua脚本执行多个redis命令提示“CROSSSLOT Keys in request don‘t hash to the same slot“问题
redis·lua·哈希算法
rayylee30 分钟前
生活抱怨与解决方案app
数据库·生活
bagadesu1 小时前
使用Docker构建Node.js应用的详细指南
java·后端
没有bug.的程序员1 小时前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
Lucifer三思而后行2 小时前
使用 BR 备份 TiDB 到 AWS S3 存储
数据库·tidb·aws
洛_尘3 小时前
JAVA EE初阶 2: 多线程-初阶
java·开发语言
Slow菜鸟3 小时前
Java 开发环境安装指南(五) | Git 安装
java·git
百***17073 小时前
Oracle分页sql
数据库·sql·oracle
qq_436962183 小时前
数据中台:打破企业数据孤岛,实现全域资产化的关键一步
数据库·人工智能·信息可视化·数据挖掘·数据分析
lkbhua莱克瓦244 小时前
Java基础——方法
java·开发语言·笔记·github·学习方法