在工作中偶尔会遇到这样一个场景:用户下单之后,若30分钟内未完成支付,则取消订单。
做过电商业务的同学,尤其是做统一下单业务的同学一般都会接触过这个场景的需求,一般的处理方式是将订单数据存储到数据库中(MySQL之类的),然后由一个定时Job不断的去扫描符合条件的订单,修改订单状态为已取消。当然这只是其中一个处理办法,我们还可以使用到延时队列来处理。
那么如果我们使用Redis是否可以实现这一功能?我们知道在使用Redis的过程中,大多是由客户端主动的去操作服务端,比如set、del、get、expire等操作。而当一个key过期被删除的时候,由服务端主动的去通知客户端,这个要怎么做?
之前在一个项目中,是自己写了一个定时Job不断是去轮询要监听的某些key,然后如果发现Redis中不存在要get的key,则执行一段业务逻辑,我们的扫描频率取决于Job的执行频率,所以并不能保证key在过期时被立即监听到,如果n秒执行一次,则key最大可能会在2n-1秒之后被执行,会有一定的延迟,那么我们能否让Redis主动的在缓存失效的时候通知我们呢?
解析
由服务端主动通知客户端,那么就是需要通过一个事件来触发某项通知,事件通过Redis的订阅和发布功能来进行分发,我们查看Redis的配置文件中有一个EVENT NOTIFICATION配置,也名键空间通知
注释上说:Redis可以通知发布/订阅客户端关于键空间中发生的事件,如果Redis开启了键空间事件通知,且客户端订阅了某些键的事件,则在相应的键发生变动时,会通过发布/订阅向客户端发送两条消息:
PUBLISH __keyspace@0__:foo del
PUBLISH __keyevent@0__:del foo
客户端可以在一组类中选择Redis的通知事件,每个类都需要由唯一字符进行标识。
配置
当开启键空间通知功能时,需要额外的消耗一些CPU,所以此功能默认为关闭状态,可以通过修改redis.conf文件或者使用config set命令来开启或关闭键空间通知功能
- 当
notify-keyspace-events
的值为空字符串时,功能关闭 - 当参数的值不是空字符串时,功能开启,且参数的值的取值范围是固定的
参数的可选值
字符 | 通知事件 |
| 键空间通知,所有通知以 |
| 键事件通知,所有通知以 |
|
|
| 字符串命令的通知 |
| 列表命令的通知 |
| 集合命令的通知 |
| 哈希命令的通知 |
| 有序集合命令的通知 |
| 过期事件:每当有过期键被删除时发送 |
| 驱逐(evict)事件:每当有键因为 |
| 参数 |
输入的参数中至少要有一个K
或E
来指定通知类型,否则配置不会生效
过期通知事件
在Redis中有两种方式将key删除:
- 当一个键被访问时,Redis会对这个键进行检查,如果键已经过期,则将该键删除
- Redis后台会定期删除那些已经过期的键
当过期键被删除时,Redis会产生一个expired通知。在此要理解一点,就是并不是当key的TTL变为0时就会立即被删除,所以Redis产生expired通知的时间为键被删除的时候而不是键的TTL变为0的时候。
依据上述表格,我们可以将notify-keyspace-events
设置为Ex
,表示键过期事件通知。
Java应用中通知监控
Spring Data Redis 实现发布订阅功能非常简单,只有这样的几个类:Topic
、MessageListener
、RedisMessageListenerContainer
。下面对它们进行解释:
org.springframework.data.redis.listener.Topic
消息发送者与接收者之间的 channel 定义,有两个实现类:
-
org.springframework.data.redis.listener.ChannelTopic
:一个确定的字符串 -
org.springframework.data.redis.listener.PatternTopic
:基于模式匹配
org.springframework.data.redis.connection.MessageListener
一个回调接口,消息监听器,用于接收发送到 channel 的消息,接口定义如下:
package org.springframework.data.redis.connection;
import org.springframework.lang.Nullable;
/**
* 监听Redis的订阅通知
*
* @author Costin Leau
* @author Christoph Strobl
*/
public interface MessageListener {
/**
* 当从Redis接收到通知后的回调方法
*
* @param message message must not be {@literal null}.
* @param pattern pattern matching the channel (if specified) - can be {@literal null}.
*/
void onMessage(Message message, @Nullable byte[] pattern);
}
org.springframework.data.redis.listener.RedisMessageListenerContainer
用于消息监听,需要将 Topic
和MessageListener
注册到RedisMessageListenerContainer
中,当 Topic 上有消息时,由RedisMessageListenerContainer
通知MessageListener
,客户端通过onMessage()拿到消息后,自行处理。
- 引入redis的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
- 创建
RedisMessageListenerContainer
实例
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class RedisListenerConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
return container;
}
}
- 创建key过期事件监听器
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String key = message.toString();
System.out.println("监听到key: " + key + " 过期!");
}
}