延迟任务之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(使用时间轮实现的分布式延时队列)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容

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