延迟任务之redis key过期事件问题分析

前言

业务中有类似等待一定时间之后执行某种行为的需求 , 比如30分钟之后关闭订单, 其中一种方式是使用Redis key的过期事件来执行延时任务 , 但是其实这是个大坑。

问题

项目中使用Redis key过期事件发现的几个问题:
1、 Redis key过期事件通知的滞后性;
2、 RedisClusterClient偶发过期事件消息丢失
3、 RedisCommandTimeoutException

一、滞后性

一般可能认为redis key过期之后马上就会产生一条过期事件消息,但实际情况并非如此, 因为Redis不能确保key在指定时间被删除 , 也就造成了通知的延期。

如何处理过期key

Redis官方对key过期有如下说明(https://redis.io/commands/expire#how-redis-expires-keys):

如何处理过期

大意是: redis每秒执行10次检查,随机检查20个可能过期的key,然后把检查出来过期的key删除,如果超过25%的key是过期的,就重复执行上述过程。
换句话说,redis key过期时候并不一定会被马上删除,而是要等到检查到key过期了才会进行删除。

过期事件触发时机

key过期之后什么时候产生过期事件呢,官方说明如下(https://redis.io/topics/notifications#timing-of-expired-events):

过期事件触发时机

也就是说,只有删除key之后才会产生过期事件,而不是key过期时间到了就产生过期事件。

验证

理论上key越多,导致的滞后就会越大。通过代码测试一下这个延迟可能有多大:
服务器环境: 开发环境,3个节点redis集群:
初始化key:

    @Scheduled(cron = "0/5 * * * * ?")
    public void initKeys(){
        LocalDateTime now = LocalDateTime.now();
        Random random = new Random();
        LocalDateTime begin = now.withSecond(0).withNano(0);
        //每5秒跑一次,每次加999个缓存
        while(begin.getNano()<999){
            //过期时间,随机加(0-10)分钟
            LocalDateTime expireTime = begin.plusMinutes(random.nextInt(10)).plusSeconds(random.nextInt(60));
            String key = dateTimeFormatter.format(now)+count.getAndIncrement() + "@" + dateTimeFormatter.format(expireTime);
            redisTemplate.opsForValue().set(key, "a", Duration.between(expireTime, LocalDateTime.now()).abs());
        }
        log.info("initKeys job结束,-------count="+count.get());
    }

获取key过期事件:

/**
 * 监听redis key过期事件
 */
@Slf4j
@Component
public class RedisKeyExpireListener extends RedisClusterPubSubAdapter<String, String> {
    private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @Override
    public void message(RedisClusterNode node, String channel, String message) {
        if(message.contains("@")){
            LocalDateTime parse = LocalDateTime.parse(message.split("@")[1], dateTimeFormatter);
            long seconds = Duration.between(parse, LocalDateTime.now()).getSeconds();
            log.info("过期key:"+message+",滞后时间:"+seconds);
        }
    }

结果如下:


1w+key的滞后时间(大多在0-10s之间)

3W+key的滞后时间(大多在0-20s之间)

5W+key的滞后时间(大多在0-30s之间)

7W+key的滞后时间(大多在10-50s之间)

可以看到,key越大,滞后时间趋势是越大的。当key达到5w个时,滞后时间都可能超过100s,对业务影响会非常大。

二、 消息丢失

项目中使用的是RedisClusterPubSubAdapter,是lettuce框架组件。
直接看官方说(https://lettuce.io/core/release/reference/#pubsub.cluster):


发布场景:发布时候会进行广播
订阅场景:
Key事件通知只会在本节点,不会在集群广播
可以通过设置节点消息传播和NodeSelection API处理;

项目中配置已经开启了消息传播和监听所有的mater消息,但是之前在预发布环境出现过消息丢失现象,目前没找到其他原因

@Slf4j
@Component
public class RedisClusterSubscriber extends RedisPubSubAdapter {
    private static final String EXPIRED_CHANNEL = "__keyevent@0__:expired";
    @Autowired
    RedisClusterClient clusterClient;
    @Autowired
    private RedisKeyExpireListener redisKeyExpireListener;
    /**
     * 启动监听,异步订阅
     */
    @PostConstruct
    public void subscribe() {
        final StatefulRedisClusterPubSubConnection<String, String> pubSubConnection = clusterClient.connectPubSub();
        //消息传播
        pubSubConnection.setNodeMessagePropagation(true);
        pubSubConnection.addListener(redisKeyExpireListener);
        //选择所有master节点
        final PubSubAsyncNodeSelection<String, String> masters = pubSubConnection.async().masters();
        final NodeSelectionPubSubAsyncCommands<String, String> commands = masters.commands();
        commands.subscribe(EXPIRED_CHANNEL);
    }
}

三、RedisCommandTimeoutException

这个错误在服务中出现过几次,概率很小,网上很多博客说把过期时间设置大一点,并未解决;还是在Lettuce官方文档中找到了说明(https://lettuce.io/core/release/reference/#faq.timeout.blpop):


在回调方法中调用了同步阻塞的方法,比如通过RedisTemplate(底层也是委托的Lettuce)客户端查询缓存等操作,就会导致这个异常

查看代码,确实在RedisKeyExpireListener消息处理方法中执行了查询缓存的操作,查看错误日志确实是在查询redis缓存的地方产生的报错

解决方法也很简单,把任务处理方法改为异步即可

总结

用redis key过期事件做延迟任务还是很不靠谱的,可以用其他的替代方案:
1、 DelayQueue 延时队列
2、 定时任务
3、 Redis sorted set
5、 RabbitMQ 延时队列
6、 时间轮

下期讲讲Redisson的RedissonDelayedQueue(使用时间轮实现的分布式延时队列)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 一、需求 设置了生存时间的Key,在过期时能不能有所提示? 如果能对过期Key有个监听,如何对过期Key进行一个回...
    Gopher_39b2阅读 5,124评论 0 0
  • 1 简介 Redis,REmote DIctionary Server,是一个由 Salvatore Sanfil...
    叫我宫城大人阅读 233评论 0 0
  • 推荐指数: 6.0 书籍主旨关键词:特权、焦点、注意力、语言联想、情景联想 观点: 1.统计学现在叫数据分析,社会...
    Jenaral阅读 5,752评论 0 5
  • 昨天,在回家的路上,坐在车里悠哉悠哉地看着三毛的《撒哈拉沙漠的故事》,我被里面的内容深深吸引住了,尽管上学时...
    夜阑晓语阅读 3,831评论 2 9
  • 一。匹配。 判断一个字符串是否符合我们制定的规则? 二…捕获 字符串中符合我们正则表达式,规则的,内容捕获到。 三...
    时修七年阅读 1,005评论 2 0