1. 键值过期监听
1.1. 问题分析及解决
- 问题描述:
线上项目需要监听设备的在线状态,通过设备心跳来实现;两分钟内没心跳 redis 会通知程序设备掉线,设备有心跳后回自动挂回 redis;无心跳时通过监听 redis key过期来处理。今天突然发现设备在线,但是 redis 中 key 没有了,导致设备异常,无法接任务。
- 问题分析:
排查项目设备掉线、上线日志,发现没有打印设备掉线日志,初步排查是 redis 库丢弃了 key,然后排查 redis 配置问题。
- 问题处理:
将 redis 数据库配置文件redis.conf 中的 notify-keyspace-events "" 改为 notify-keyspace-events "Ex"。
1.2. 问题拓展学习
1.2.1. Redis key过期策略
通过 EXPIRE key seconds 命令来设置数据的过期时间。返回1表明设置成功,返回0表明key不存在或者不能成功设置过期时间。在key上设置了过期时间后key将在指定的秒数后被自动删除。被指定了过期时间的key在Redis中被称为是不稳定的。
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。
1.2.2. Redis key过期的方式有三种
- 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key(无法保证冷数据被及时删掉);
- 定期删除:Redis会定期主动淘汰一批已过期的key(随机抽取一批key检查);
- 内存淘汰机制:当前已用内存超过maxmemory限定时,触发主动清理策略;
惰性删除
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
- 从过期字典中随机 20 个 key;
- 删除这 20 个 key 中已经过期的 key;
- 如果过期的 key 比率超过 1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
如果某一时刻,有大量key同时过期,Redis 会持续扫描过期字典,造成客户端响应卡顿,因此设置过期时间时,就尽量避免这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,避免同一时刻过期。
- 如何配置定期删除执行时间间隔
redis的定时任务默认是10s执行一次,如果要修改这个值,可以在redis.conf中修改hz的值。
redis.conf中,hz默认设为10,提高它的值将会占用更多的cpu,当然相应的redis将会更快的处理同时到期的许多key,以及更精确的去处理超时。 hz的取值范围是1~500,通常不建议超过100,只有在请求延时非常低的情况下可以将值提升到100。
- 单线程的redis,如何知道要运行定时任务?
redis是单线程的,线程不但要处理定时任务,还要处理客户端请求,线程不能阻塞在定时任务或处理客户端请求上,那么,redis是如何知道何时该运行定时任务的呢?
Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是接下来处理客户端请求的最大时长,若达到了该时长,则暂时不处理客户端请求而去运行定时任务。
懒惰删除
定时删除策略中,从删除方法来看,必然会导致有key过期了但未从redis中删除的情况。
面对这种情况,redis在操作一个key时,会先判断这个值是否过期,若已过期,则删除该key;若未过期,则进行后续操作。
Redis 内存淘汰机制
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(常用)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
1.2.3. Redis 中 key 过期的事件触发
Redis的key过期的事件触发
过期事件通过Redis的订阅与发布功能(pub/sub)来进行分发。
- redis 服务端配置
超时的监听,并不需要自己发布,只有修改配置文件redis.conf中的:notify-keyspace-events Ex,默认为notify-keyspace-events ""。
修改好配置文件后,redis会对设置了expire的数据进行监听,当数据过期时便会将其从redis中删除。
如果服务宕机,那么将接收不到 redis 推送过来的事件也就无法处理 redis 过期后的逻辑。我们可以采用 redis + 定时任务处理,这样可以避免服务宕机,Redis过期事件推送处理的问题,也能够提高系统整体性能(可以去了解一下 redis 的 key 过期策略)。
Java实现redis中key过期事件触发
创建一个实现类继承KeyExpirationEventMessageListener
package net.chenqiong.redis.utils;
import com.focussend.contacts.service.FissionMarketingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Created by chenqiong on 2021/3/19.
*/
@Component
class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Resource
private FissionMarketingService fissionMarketingService;
private Logger logger = LoggerFactory.getLogger(RedisKeyExpirationListener.class);
private final Object fanNumUpdate = new Object();
/**
* 针对redis数据失效事件,进行数据处理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 用户做自己的业务处理即可,注意message.toString()可以获取失效的key
String expiredKey = message.toString();
logger.info("此处根据对应的redis的key去处理业务逻辑");
}
}
- 注入RedisMessageListenerContainer
方法一:
<bean id="redisContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
方法二:
/**
* Redis缓存配置类
*/
@Configuration
public class RedisConfigurer extends CachingConfigurerSupport {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
- 测试
向redis中存入一个key和value并设置过期时间,key过期后会触发
onMessage(Message message, byte[] pattern)
方法
redisCacheManagerTool.set("test_renjiao","haha",20);