过期监听实现关闭订单

一.前言

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?

一般实现的方法有几种:

  1. 使用 rocketmq、rabbitmq、pulsar 等消息队列的延时投递功能

  2. 使用 redisson 提供的 DelayedQueue

有一些方案虽然广为流传但存在着致命缺陷,不要用来实现延时任务

  1. 使用 redis 的过期监听

  2. 使用 rabbitmq 的死信队列

  3. 使用非持久化的时间轮

二.缺陷分析

2.1 使用 redis 的过期监听

2.1.1 实现

通过开启key过期的事件通知,当key过期时,会发布过期事件;我们定义key过期事件的监听器,当key过期时,就能收到回调通知。

注意:

  1)由于Redis key过期删除是定时(设置定时器)+惰性(查询删除),当key过多时,删除会有延迟,回调通知同样会有延迟,因此性能较低。

  2)且通知是一次性的,没有ack机制,若收到通知后处理失败,将不再收到通知。需自行保证收到通知后处理成功。

  3)通知只能拿到key,拿不到value。

  4)Redis将数据存储在内存中,如果遇到恶意下单或者刷单的将会给内存带来巨大压力。

使用场景:实现延时队列,消息作为key,将需要延迟的时间设置为key的TTL,当key过期时,在监听器收到通知,达到延迟的效果。

步骤:

1.修改Redis配置

首先将redis的配置文件中的 notify-keyspace-events改成 notify-keyspace-events Ex
这里的 EX代表 expire 和 evicted 过期和驱逐 的时间监听 ,注意:改了配置要重启Redis

2.开发配置

pom.xml


<!-- redis -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<!-- 使用jedis -->
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>

application.yml

spring:
  redis:
    host: 127.0.0.1
    jedis:
      pool:
        max-active: 8   #最大连接数据库连接数,设 -1 为没有限制
        max-idle: 8     #最大等待连接中的数量,设 0 为没有限制
        max-wait: -1ms  #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
        min-idle: 0     #最小等待连接中的数量,设 0 为没有限制
      shutdown-timeout: 100ms
    password: ''
    port: 6379

3.配置类 RedisConfig.java 

RedisConfig 中配置 Message Listener Containers (消息订阅者容器)

类似于Redis pub/sub 中 Message Listener Containers 的配置,区别少了监听器的指定。

@Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
        // redis 消息订阅(监听)者容器
        RedisMessageListenerContainer messageListenerContainer = new RedisMessageListenerContainer();
        messageListenerContainer.setConnectionFactory(redisConnectionFactory);
        messageListenerContainer.addMessageListener(new ProductUpdateListener(), new PatternTopic("*.product.update"));
        return messageListenerContainer;
    }

4.定义 key过期监听器,继承 KeyExpirationEventMessageListener

定义的继承了KeyExpirationEventMessageListener的redis key 过期监听器本质上就是一个消息监听器,监听来自channel为__keyevent@*__:expired 上发布的消息

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    /**
     * 创建RedisKeyExpirationListener bean时注入 redisMessageListenerContainer
     *
     * @param redisMessageListenerContainer RedisConfig中配置的消息监听者容器bean
     */
    public RedisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {
        super(redisMessageListenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel()); // __keyevent@*__:expired
        String pa = new String(pattern); // __keyevent@*__:expired
        String expiredKey = message.toString();
        System.out.println("监听到过期key:" + expiredKey);
    }
}

可以看到,其本质是Redis的发布订阅,当key过期,发布过期消息(key)到Channel :__keyevent@*__:expired中,再看KeyExpirationEventMessageListener 源码:

public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements ApplicationEventPublisherAware {

    // 发布key过期频道的topic
    private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");

    private @Nullable
    ApplicationEventPublisher publisher;

    /**
     * 子类在由Spring容器创建bean的时候调用父类的此构造器,并传入容器中的RedisMessageListenerContainer bean作为参数
     */
    public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * doRegister 在 KeyspaceEventMessageListener(实现了InitializingBean和 MessageListener) 中的init方中被调用
     * 将我们自定义的key过期监听器添加到 消息监听容器中
     */
    @Override
    protected void doRegister(RedisMessageListenerContainer listenerContainer) {
        listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
    }

    /**
     * 发布key过期事件
     *
     * @param message 过期key
     */
    @Override
    protected void doHandleMessage(Message message) {
        publishEvent(new RedisKeyExpiredEvent(message.getBody()));
    }

    /**
     * 由Spring RedisKeyExpiredEvent事件监听器执行实际上的redis key过期消息的发布
     */
    protected void publishEvent(RedisKeyExpiredEvent event) {

        if (publisher != null) {
            this.publisher.publishEvent(event);
        }
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}

redis过期监听_Cool_Pepsi的博客-优快云博客_redis 过期监听

redis key过期监听 - 简书

Redis key过期监听 - 杨七 - 博客园

2.1.2 缺点

2.2 使用 rabbitmq 的死信队列

死信(Dead Letter) 是 rabbitmq 提供的一种机制。当一条消息满足下列条件之一那么它会成为死信:

  • 消息被否定确认(如channel.basicNack) 并且此时requeue 属性被设置为false(消费端执行nack或者reject时,设置requeue=false)

  • 消息在队列的存活时间超过设置的TTL时间

  • 消息队列的消息数量已经超过最大队列长度

若配置了死信队列,死信会被 rabbitmq 投到死信队列中,如果没有配置死信队列,这些死信消息会被丢弃。

理解死信队列

死信队列并不仅仅只是一个queue,还包含死信交换机(Dead Letter Exchange),关于死信队列和死信交换机要说明几点:

死信交换机可以是fanout、direct、topic等类型,和普通交换机并无不同;

死信交换机要绑定要业务队列上才会生效;

给死信交换机绑定的队列称之为死信队列,其实就是普通的队列,没有任何特殊之处;

并不是整个项目只能设置一个死信交换机和死信队列,可以根据业务需要设置多个或者单个死信交换机使用不同的routing-key; 

 

2.2.1 实现

在 rabbitmq 中创建死信队列的操作流程大概是:

  • 创建一个交换机作为死信交换机

  • 在业务队列中配置 x-dead-letter-exchange 和 x-dead-letter-routing-key,将第一步的交换机设为业务队列的死信交换机

  • 在死信交换机上创建队列,并监听此队列

 

pom.xml添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
       </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

application.properties配置RabbitMQ连接信息

spring:
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: lzm
    password: lzm
    virtual-host: test
    listener:
      simple:
        acknowledge-mode: manual  # 手动ack
        default-requeue-rejected: false # 设置为false,requeue或reject

创建交换机和队列以及绑定

 /**
 * 死信交换机
 */
@Bean
public DirectExchange dlxExchange(){
	return new DirectExchange(dlxExchangeName);
}

/**
 * 死信队列
 */
@Bean
public Queue dlxQueue(){
	return new Queue(dlxQueueName);
}

/**
 * 死信队列绑定死信交换机
 */
@Bean
public Binding dlcBinding(Queue dlxQueue, DirectExchange dlxExchange){
	return BindingBuilder.bind(dlxQueue).to(dlxExchange).with(dlxRoutingKey);
}

/**
 * 业务队列
 * 注意创建业务队列的部分,设置业务队列的超时时间是10s,队列中消息最大数量为10。
 * 业务交换机为fanout类型的交换机,死信交换机为Direct类型的交换机。
 */
@Bean
public Queue queue(){
	Map<String,Object> params = new HashMap<>();
	params.put("x-dead-letter-exchange",dlxExchangeName);//声明当前队列绑定的死信交换机
	params.put("x-dead-letter-routing-key",dlxRoutingKey);//声明当前队列的死信路由键
	params.put("x-message-ttl",10000);//设置队列消息的超时时间,单位毫秒,超过时间进入死信队列
	params.put("x-max-length", 10);//生命队列的最大长度,超过长度的消息进入死信队列
	return QueueBuilder.durable(queueName).withArguments(params).build();
}

/**
 * 业务交换机
 */
@Bean
public FanoutExchange fanoutExchange(){
	return new FanoutExchange(exchangeName,true,false);
}

/**
 * 业务队列和业务交换机的绑定
 */
@Bean
public Binding binding(Queue queue, FanoutExchange fanoutExchange){
	return  BindingBuilder.bind(queue).to(fanoutExchange);
}

生产者

/**
    注意:
    队列中消息的超时时间可以是在创建队列时设置,表示对队列中所有的消息生效,
    也可以在发送消息时设置,两者相比取最小值作为TTL的值。
*/
public void send(){
	for (int i = 0; i < 5; i++) {
		CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
		rabbitTemplate.convertAndSend(exchangeName,"","消息==>"+i,message -> {
			message.getMessageProperties().setExpiration(3000+"");//发送消息时设置消息的超时时间
			return message;
		},correlationData);
	}
}

 

//启动消费者,并且模拟在某些情况下执行nack操作,先看消费者代码
@RabbitHandler
//platform.queue-name为dlxQueueName即dlx-queue,concurrency =1,即每个Listener容器将开启一个线程去处理消息
@RabbitListener(queues = {"${platform.queue-name}"},concurrency = "1") 
public void msgConsumer(String msg, Channel channel, Message message) throws IOException {
	try {
		if(msg.indexOf("5")>-1){
			throw new RuntimeException("抛出异常");
		}
		log.info("消息{}消费成功",msg);
		channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
	} catch (Exception e) {
		log.error("接收消息过程中出现异常,执行nack");
		//第三个参数为true表示异常消息重新返回队列,会导致一直在刷新消息,且返回的消息处于队列头部,影响后续消息的处理
		channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
		log.error("消息{}异常",message.getMessageProperties().getHeaders());
	}
}

 

 

RabbitMQ死信队列 - 一步一年 - 博客园 

2.2.2 缺点

当往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费。

依次发送两个请求http://localhost:4399/send?msg=消息A&time=30和http://localhost:4399/send?msg=消息B&time=10,消息A先发送,过期时间30S,消息B后发送,过期时间10S,我们想要的结果应该是10S收到消息B,30S后收到消息A,但结果并不是,控制台输出如下:

2020-12-16 22:54:47.339  INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:消息A发送成功,过期时间:30秒。
2020-12-16 22:54:54.278  INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:消息B发送成功,过期时间:10秒。
2020-12-16 22:55:17.356  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:消息A
2020-12-16 22:55:17.357  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:消息B


消息A30S后被成功消费,紧接着消息B被消费。因此当我们使用死信队列时应该注意是否消息的过期时间都是一样的,比如订单超过10分钟未支付修改其状态。如果当一个队列各个消息的过期时间不一致时,使用死信队列就可能达不到延时的作用。这时候我们可以使用延时插件来实现这需求。
 

死信队列的设计目的是为了存储没有被正常消费的消息,便于排查和重新投递。死信队列同样也没有对投递时间做出保证,在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。

为了解决这个问题,rabbit 官方推出了延迟投递插件 rabbitmq-delayed-message-exchange ,推荐使用官方插件来做延时消息。使用 redis 过期监听或者 rabbitmq 死信队列做延时任务都是以设计者预想之外的方式使用中间件,这种出其不意必自毙的行为通常会存在某些隐患,比如缺乏一致性和可靠性保证,吞吐量较低、资源泄漏等。比较出名的一个事例是很多人使用 redis 的 list 作为消息队列,以致于最后作者看不下去写了 disque 并最后演变为 redis stream。工作中还是尽量不要滥用中间件,用专业的组件做专业的事

2.3 使用非持久化的时间轮

时间轮是一种很优秀的定时任务的数据结构,然而绝大多数时间轮实现是纯内存没有持久化的。运行时间轮的进程崩溃之后其中所有的任务都会灰飞烟灭,所以奉劝各位勇士谨慎使用。

三.实现方法

3.1 使用消息队列的延时投递功能

推荐使用 rocketmq、pulsar 等拥有定时投递功能的消息队列

详解RabbitMQ实现延时消息的2种方法:死信队列+延时插件_java 分享官的博客-优快云博客_rabbitmq 延迟消息插件

RabbitMQ 延迟队列实现订单自动关闭-蒲公英云

延时插件实现步骤:

1.安装好插件后只需要声明一个类型type为"x-delayed-message"的exchange,并且在其可选参数下配置一个key为"x-delayed-typ",值为交换机类型(topic/direct/fanout)的属性。

     //延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
    public static final String DMP_EXCHANGE = "dmp.exchange";
    public static final String DMP_ROUTEKEY = "dmp.routeKey";
    public static final String DMP_QUEUE = "dmp.queue";



    //延迟插件使用
    //1、声明一个类型为x-delayed-message的交换机
    //2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
    @Bean
    public CustomExchange dmpExchange(){
        Map<String, Object> arguments = new HashMap<>(1);
        arguments.put("x-delayed-type", "direct");
        return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
    }
 
    @Bean
    public Queue dmpQueue(){
        return new Queue(DMP_QUEUE,true,false,false);
    }
 
    @Bean
    public Binding dmpBind(){
        return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
    }


2.声明一个队列绑定到该交换机
3.在发送消息的时候消息的header里添加一个key为"x-delay",值为过期时间的属性,单位毫秒。


    //使用延迟插件发送消息方法封装
    public void send2(String message,Integer time){
        rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
            //使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
                message.getMessageProperties().setHeader("x-delay",time*1000);
                return message;
            }
        });
        log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);


4.代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。


    @RabbitHandler
    @RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
    public void onMessage2(Message message){
        log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
    }


5.启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机。

启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。
 
在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
2020-12-16 22:47:28.071  INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:hello发送成功,过期时间:5秒。
2020-12-16 22:47:33.145  INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver           : 使用死信队列,收到消息:hello

3.2 使用 redisson 提供的 DelayedQueue

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.8.0</version>
</dependency>  

生成订单并放进延时队列的类

package com.jeiao.redis;
 
import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
import java.util.concurrent.TimeUnit;
 
/**
 * Author:ZhuShangJin
 * Date:2018/8/31
 */
public class RedisPutInQueue {
     public static void main(String args[]) {
         Config config = new Config();
         config.useSingleServer().setAddress("redis://192.168.19.173:6379").setPassword("ps666").setDatabase(2);
         RedissonClient redissonClient = Redisson.create(config);
         RBlockingQueue<CallCdr> blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
 
         RDelayedQueue<CallCdr> delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
         for (int i = 0; i <10 ; i++) {
             try {
                 Thread.sleep(1*1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             // 一分钟以后将消息发送到指定队列
 //相当于1分钟后取消订单
//             延迟队列包含callCdr 1分钟,然后将其传输到blockingFairQueue中
             //在1分钟后就可以在blockingFairQueue 中获取callCdr了
             CallCdr callCdr = new CallCdr(30000.00);
             callCdr.setPutTime();
             delayedQueue.offer(callCdr, 1, TimeUnit.MINUTES);
 
         }
//         在该对象不再需要的情况下,应该主动销毁。
// 仅在相关的Redisson对象也需要关闭的时候可以不用主动销毁。
         delayedQueue.destroy();
 
//redissonClient.shutdown();
         }
}

取消订单的操作类

package com.jeiao.redis;
 
import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.logging.SimpleFormatter;
 
/**
 * Author:ZhuShangJin
 * Date:2018/8/31
 */
public class RedisOutFromQueue {
     public static void main(String args[]) {
         Config config = new Config();
         config.useSingleServer().setAddress("redis://192.168.19.173:6379").setPassword("ps666").setDatabase(2);
         RedissonClient redissonClient = Redisson.create(config);
         RBlockingQueue<CallCdr> blockingFairQueue = redissonClient.getBlockingQueue("delay_queue");
         RDelayedQueue<CallCdr> delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
         while (true){
             CallCdr callCdr = null;
             try {
                 callCdr = blockingFairQueue.take();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
                 System.out.println("订单取消时间:"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"==订单生成时间"+callCdr.getPutTime());
 
         }
 
         }
}

基于redisson实现延迟队列 - ※星光※ - 博客园 

聊聊redisson的DelayedQueue_Redis - UCloud云社区

Redisson 延时队列 原理 详解 - 知乎

Redisson延迟队列RDelayedQueue的使用 - 灰信网(软件开发博客聚合)

 

 

四.结论

<think> 首先,用户的问题是:“请问Java实现Redis过期监听的方法”,基于提供的引用内容。 我需要从系统级指令开始: - 所有行内数学表达式用$...$,但这个问题似乎不涉及数学,所以可能不需要。 - 独立公式用$$...$$,单独成段,同样不适用。 - LaTeX语法正确:确保如果使用,语法正确。 - 使用中文回答:整个回答必须用中文。 - 生成相关问题:在回答末尾添加相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层指令: - 回答结构清晰,逐步解决问题。 - 保证回答真实可靠。 现在,分析引用内容: - [^1]: 提到Redis开启过期监听,用于订单取消场景,需要Redis 2.8.0及以上版本。 - [^2]: 讨论Redis过期消息订阅实现,提到同一个键过期可能会有多次通知,解决方案是加锁。 - [^3]: 描述如何从Kafka数据保存到Redis,key自定义失效时间,失效后通过监听器处理。 - [^4]: 解释使用Jedis的psubscribe方法进行监听,注意阻塞操作,需要在Spring Boot中保持运行状态。 - [^5]: 提到基于Redis的延迟处理,使用getDelay()方法判断元素是否弹出。 用户的需求是:查找Java中实现Redis过期监听的方法和相关知识。 结构化回答: 1. **介绍Redis过期监听的概念**:简要解释什么是Redis过期监听及其用途。 2. **实现步骤**:基于引用,列出在Java中实现Redis过期监听的关键步骤。 3. **代码示例**:提供一个简单的Java代码示例,展示如何实现。 4. **注意事项**:讨论常见问题,如多次通知、阻塞操作等。 5. **生成相关问题**:在末尾添加相关问题。 确保回答真实可靠:基于提供的引用,不要添加虚构内容;引用内容来自用户提供的文本。 回答结构: - 开头:直接回答用户问题。 - 主体:分步骤解释。 - 结尾:总结并添加相关问题。 语言:全部用中文。 引用标识:在引用具体内容时,在末尾添加[^n],例如[^1]。 现在,规划内容: 1. **概念介绍**: - Redis过期监听允许在键过期时触发事件,常用于定时任务如订单取消[^1]。 - 需要Redis 2.8.0及以上版本支持[^1]。 2. **实现方法**: - 配置Redis:启用键空间通知(keyspace notifications),在redis.conf中设置`notify-keyspace-events Ex`。 - 在Java项目中,使用Jedis或Spring Data Redis订阅过期事件。 - 订阅模式:使用`psubscribe`监听`__keyevent@*__:expired`模式。 - 示例代码:参考[^4],在Spring Boot中实现`CommandLineRunner`或使用`JedisPubSub`。 3. **代码示例**: - 提供一个简单的Java代码片段,基于引用。 - 例如,使用Jedis: ```java Jedis jedis = new Jedis("localhost"); jedis.psubscribe(new JedisPubSub() { @Override public void onPMessage(String pattern, String channel, String message) { if (channel.startsWith("__keyevent@*__:expired")) { // 处理过期键 System.out.println("Key expired: " + message); } } }, "__keyevent@*__:expired"); ``` 注意:这是一个阻塞操作,需要后台运行[^4]。 4. **注意事项**: - **多次通知问题**:同一个键过期可能触发多次通知,由于主从复制或集群机制[^2]。解决方案:在回调函数中加锁,确保只执行一次[^2]。 - **性能考虑**:Redis过期监听可能影响性能;在高负载下,使用延迟队列替代,如基于Redis的Sorted Set实现延迟处理[^5]。 - **Spring Boot集成**:在Spring中,可以使用`@EventListener`或自定义`JedisPubSub` bean[^4]。 5. **总结**:Redis过期监听是一种简单的方法,但需注意可靠性和性能。 生成相关问题: - 基于主题,类似示例中的快速排序相关问题。 - 例如: 1. 如何避免Redis过期监听中的重复通知? 2. Redis过期监听的性能优化方法有哪些? 3. 在Spring Boot中如何优雅地实现Redis过期监听? 最后,确保回答清晰、逐步。 </think> ### Java实现Redis过期监听的方法 在Java中实现Redis过期监听,允许你在键(key)过期时触发自定义操作(如取消订单、发送通知)。以下是基于引用内容的逐步实现方法,确保可靠性和实用性。Redis过期监听依赖于Redis的**键空间通知(Keyspace Notifications)** 功能,需Redis 2.8.0及以上版本支持[^1]。 --- ### 一、Redis配置:启用键空间通知 在Redis服务器端,需修改`redis.conf`配置文件以启用过期事件通知: 1. 打开`redis.conf`文件,添加或修改配置: ```conf notify-keyspace-events Ex # 启用键过期事件通知 ``` - `Ex`表示监听过期事件(expired events)。 2. 重启Redis服务使配置生效: ```bash redis-server redis.conf ``` 此步骤确保Redis能发布过期事件消息[^1]。 --- ### 二、Java项目实现步骤(使用Jedis或Spring Boot) #### 步骤1:添加依赖 在Java项目的`pom.xml`中添加Jedis(Redis Java客户端)依赖: ```xml <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.0</version> <!-- 推荐4.x版本 --> </dependency> ``` #### 步骤2:订阅过期事件 订阅Redis的`__keyevent@*__:expired`频道监听过期事件。以下是两种实现方式: **方式A:基本Jedis实现(适合简单应用)** ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; public class RedisExpirationListener { public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); // 连接Redis JedisPubSub subscriber = new JedisPubSub() { @Override public void onPMessage(String pattern, String channel, String message) { // 处理过期键:message即过期的key if (channel.startsWith("__keyevent@*__:expired")) { System.out.println("Key expired: " + message); // 执行业务逻辑,例如取消订单 } } }; // 订阅过期事件(阻塞操作,需后台线程运行) jedis.psubscribe(subscriber, "__keyevent@*__:expired"); } } ``` - **注意**:`psubscribe`是阻塞方法,需在独立线程中运行[^4]。 **方式B:Spring Boot集成(推荐生产环境)** 在Spring Boot中,结合`CommandLineRunner`实现后台监听: ```java import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Service; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; @Service public class RedisListenerService implements CommandLineRunner { @Override public void run(String... args) { new Thread(() -> { Jedis jedis = new Jedis("localhost", 6379); jedis.psubscribe(new JedisPubSub() { @Override public void onPMessage(String pattern, String channel, String message) { if (message.contains("order:")) { // 示例:订单key过期 System.out.println("订单过期: " + message); // 调用Service取消订单 } } }, "__keyevent@*__:expired"); }).start(); } } ``` - **优势**:Spring Boot启动时自动运行,管理线程生命周期[^4]。 --- ### 三、关键注意事项与优化 1. **避免重复通知问题**: - Redis集群或主从复制可能导致同一键过期触发多次通知[^2]。 - **解决方案**:在回调函数中加锁(如`synchronized`或分布式锁),确保逻辑只执行一次: ```java public synchronized void onPMessage(...) { // 加锁后仅一个线程执行 } ``` 2. **性能优化**: - 高并发场景下,过期监听可能影响Redis性能。参考[^5],可改用**延迟队列**(如Redis Sorted Set): ```java // 示例:使用ZSET实现延迟处理 jedis.zadd("delayed:queue", System.currentTimeMillis() + 15000, "order:123"); // 15秒后过期 ``` - 轮询ZSET检查过期元素,减少事件监听开销。 3. **键命名与业务结合**: - 设计键名包含业务信息(如`order:123`),便于在监听器中解析[^3]。 - 示例场景:订单15分钟未支付则取消[^3]: ```java jedis.setex("order:123", 900, "unpaid"); // 设置15分钟过期 ``` --- ### 四、总结 - **核心机制**:通过Redis的键空间通知订阅`__keyevent@*__:expired`事件。 - **适用场景**:订单超时取消、缓存失效处理、定时提醒等[^1][^3]。 - **最佳实践**: - 生产环境使用Spring Boot管理监听线程。 - 处理重复通知问题,确保幂等性[^2]。 - 监控性能,必要时切换为延迟队列方案[^5]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值