Redis篇-7--原理篇6--过期机制(定时删除,惰性删除,Redis过期事件监听和Java实现)
定时删除(Active Expiration)是一种主动的过期策略,Redis会定期检查并删除那些已经过期的键。具体来说,Redis会在后台线程中运行一个定时任务,每隔一段时间扫描一定数量的过期键,并将它们从数据库中删除。惰性删除是一种被动的过期策略,Redis不会主动检查过期键,而是在访问某个键时才检查其是否已经过期。如果该键已经过期,Redis会立即删除它,并返回相应的结果(如 nil 或 n
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方法中可以直接获取过期的键名。
学海无涯苦作舟!!!
更多推荐
所有评论(0)