前言
- 当人们在过五一长假时
Redis 6.0
悄然发布了,这个版本提供了许多新特性和一些功能上的改进,其中最引入关注的当属“多线程”了。但是,本文先不谈新特性,先对本人学过的redis
相关知识及应用做一下回顾,以后抽空会再写一篇我对Redis 6.0
的所知所学
Redis的过期策略是什么
过期策略
- 解答是
定期删除
+惰性删除
定期删除
指的是redis
默认每隔100ms
就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除- 由于定期删除可能会导致key到过期时间了,没有被随机抽取删除掉,所以有了
惰性删除
,当使用获取key操作时,redis
会检查key是否过期,如果过期了就删除掉不会返回任何东西
但是如果某个key一直没有获取操作或者被定期删除随机抽取到,那么也会堆积大量key在内存里,所以有了内存淘汰机制
内存淘汰机制
redis 内存淘汰机制有以下几个:
noeviction
: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。allkeys-random
:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。volatile-lru
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。volatile-random
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。volatile-ttl
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
实战演练
我在公司负责过一个直播项目,主播会提前预约一个直播间,然后把直播间通过社交软件分享出去吸引流量;如果预约时间过了10分钟还没有来开播,那么会删除直播间 + 七牛直播云关闭
Stream
+imId
(当然,到了预约时间提前10分钟会push给主播提醒他开播,这些跟本文无关就不提了 )。
解决方案选择
- 使用
MQ
的延时消息来做,但是当时研究发现RocketMQ
只支持指定的延时级别1s, 2s, … , 2h,没办法指定到具体的时间,其实这一点也能想到,RocketMQ作为一个高吞吐,低延时的消息队列,不设计这种具体到指定时间的延时消息也情有可原,因为这样就要在对消息排序上做大量处理,会极大地影响性能。于是 ,此方案不予采用。 - 采用定时任务来做,但是这样会很纠结这个定时任务的轮询时间,5分钟1次?2分钟1次?这样也没办法指定到具体时间,思来想去总觉得这个方案不太优雅,嗯,弃用。
- 自己实现一个时间轮?嗯,因为之前自己实现过一个时间轮,就拷贝到项目中使用,效果还不错,但是由于任务是放在内存中的,项目一重启,或者其他原因故障会导致任务丢失。这样一来那些空无一人的直播间挂在我们的直播首页也不太美观。于是考虑到要对任务做持久化,嗯,这样复杂度越来越高,而且还要新增业务表;于是乎,此方案也不采用。
- 最后是采用监听
redis
的key过期事件来处理这个问题:将直播间roomId:
做为key前缀,expireTime
= 预约开播时间 + 10分钟 - 当前时间。当监听到指定key过期时,调用指定业务方法区处理。这样一来不需要引入额外的中间件,编码复杂度低,处理时间十分精确,完美地解决问题。
代码示例
- 注意:需要开启
Redis
key 过期提醒,在redis.conf
中加入notify-keyspace-events Ex
配置
配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
/**
* 将redis消息监听器注册为一个bean
* @param connectionFactory 链接工厂
* @return RedisMessageListenerContainer
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
key过期监听类
@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
//获取失效的key, 进行对应的业务处理
String expiredKey = message.toString();
//取redis前缀符合的进行乡音处理
if (expiredKey.startsWith(RedisKeyConstant.LIVE_ROOM_CLOSE)) {
log.info("【这里进行对应的业务处理】, 业务key = {}过期", expiredKey);
}
}
}
总结
其实,我这里的实战演练中的业务对时间点选择不是那么敏感,采用定时任务去处理,稍微晚那么几分钟关闭过期未及时开播的直播间也未尝不可,只是我个人是觉得监听redis key过期的方案更优雅,实现也简单罢了。其实这种方案适合很多种场景,比如订单30分钟超时未支付取消订单也非常适合使用。在未来我也会对自己所知所学多做总结,希望能做到温故知新,在日常学习中提升技术广度,在工作中践行技术深度,自我勉励!
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!