前言

  • 当人们在过五一长假时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给主播提醒他开播,这些跟本文无关就不提了 )。

解决方案选择

  1. 使用MQ的延时消息来做,但是当时研究发现RocketMQ只支持指定的延时级别1s, 2s, … , 2h,没办法指定到具体的时间,其实这一点也能想到,RocketMQ作为一个高吞吐,低延时的消息队列,不设计这种具体到指定时间的延时消息也情有可原,因为这样就要在对消息排序上做大量处理,会极大地影响性能。于是 ,此方案不予采用。
  2. 采用定时任务来做,但是这样会很纠结这个定时任务的轮询时间,5分钟1次?2分钟1次?这样也没办法指定到具体时间,思来想去总觉得这个方案不太优雅,嗯,弃用。
  3. 自己实现一个时间轮?嗯,因为之前自己实现过一个时间轮,就拷贝到项目中使用,效果还不错,但是由于任务是放在内存中的,项目一重启,或者其他原因故障会导致任务丢失。这样一来那些空无一人的直播间挂在我们的直播首页也不太美观。于是考虑到要对任务做持久化,嗯,这样复杂度越来越高,而且还要新增业务表;于是乎,此方案也不采用。
  4. 最后是采用监听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分钟超时未支付取消订单也非常适合使用。在未来我也会对自己所知所学多做总结,希望能做到温故知新,在日常学习中提升技术广度,在工作中践行技术深度,自我勉励!


redis      redis

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Keepalived软件架构 上一篇
LeetCode题解-102.二叉树的层序遍历 下一篇