一、事件通知介绍
1、前言
从Redis 2.8.0开始,Redis加入了发布/订阅模式以及键空间消息提醒(keyspace notification)功能。键空间消息提醒提供了允许客户端通过订阅指定信道获取Redis数据变化的能力。需要注意的是,键空间消息提醒并非可靠的,它不会对订阅端是否接收到消息进行确认。例如某个订阅的客户端暂时断开连接,在其直到恢复连接期间发生的事件将无法再次获得。
2、配置详解
可以通过对redis的redis.conf
文件中配置notify-keyspace-events
参数可以指定服务器发送哪种类型的通知。下面对于一些参数的描述。默认情况下此功能是关闭的。
字符 | 通知 |
---|---|
K | 键空间通知,所有通知以 keyspace@ 为前缀 |
E | 键事件通知,所有通知以 keyevent@ 为前缀 |
g | DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知 |
$ | 字符串命令的通知 |
l | 列表命令的通知 |
s | 集合命令的通知 |
h | 哈希命令的通知 |
z | 有序集合命令的通知 |
x | 过期事件:每当有过期键被删除时发送 |
e | 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 |
A | 参数 g$lshzxe 的别名 |
redis> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) ""
redis> CONFIG SET notify-keyspace-events KEA
OK
redis> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) "AKE"
在上述示例中将notify-keyspace-events
配置为KEA
,代表除未命中外的所有事件。其中,K
与E
代表事件的两种类型——Keyspace
与Keyevent
。
Keyspace
代表与事件名称相关的消息,例如订阅对指定键进行的操作事件;Keyevent
代表与键名称相关的消息,例如订阅发生键过期事件的相关键名称。
3、订阅指定事件
在完成配置后,可通过SUBSCRIBE
命令订阅指定信道实现对一个或多个指定事件的订阅。例如通过订阅__keyevent@0__:expired
实现订阅数据库0中的键过期事件
订阅的信道的格式为__<type>@<db>__:<event>
,其包括了事件类型(keyspace
或keyevent
)、数据库(例如数据库0
)以及事件(例如expired
)三部分组成。另外,也可以通过PSUBSCRIBE
命令订阅一个或多个复合正则表达式匹配的信道。例如通过订阅__key*@*__:*
订阅Redis中所有数据库中的所有事件。
4、命令事件
Redis为许多命令提供了不同的事件,在本文中将选择其中部分命令及其对应的事件进行介绍:
-
DEL
:在某个键被删除时产生del
事件 -
EXPIRE
、PEXPIRE
、EXPIREAT
以及PEXPIREAT
:当设置正数过期时间或未来时间的时间戳,则产生expire
事件,否则产生del
事件(将立即被删除) -
SET
以及同类的SETEX
、SETNX
、GETSET
:产生set
事件,若使用SETEX
则也会产生expire
事件 -
MSET
:将会为每个键都产生一个set
事件 -
LPUSH
、LPUSHX
与RPUSH
、RPUSHX
:根据插入的方向分别产生lpush
或rpush
事件 -
RPOP
、LPOP
:分别产生rpop
与lpop
事件,若移出的是列表中的最后一个元素,将会同时产生del
事件 -
LSET
:产生lset
事件 -
LREM
:产生lrem
事件,同样若移除的元素为列表中的最后一个元素时将同时产生del
事件 -
HSET
、HSETNX
以及HMSET
:产生一个hset
事件 -
HDEL
:产生一个hdel
事件,且在移除后哈希表为空的情况下产生del
事件 -
SADD
:产生一个sadd
事件 -
SREM
:产生一个srem
事件,且在移除后集合为空的情况下产生del
事件 -
SMOVE
:原键中产生srem
事件且在目标键中产生sadd
事件 -
SINTERSTORE
、SUNIONSTORE
、SDIFFSTORE
:分别产生sinterstore
、sunionstore
以及sdiffstore
事件,且在结果为空集且目标键存在的情况下,将会产生del
事件 -
ZADD
:无论添加几个元素都只产生一个zadd
事件 -
ZREM
:无论移除几个元素都只产生一个zrem
事件,当移除后有序集合为空时产生del
事件 -
XADD
:产生xadd
事件,若使用MAXLEN
子命令可能会同时产生xtrim
事件 -
XDEL
:产生xdel
事件 -
PERSIST
:如果对应的键所关联的过期事件成功被移除,则产生persist
事件 -
在键发生过期时产生
expired
事件 -
在达到
maxmemory
设定的内存值后发生键淘汰时产生evicted
事件 -
……
关于更多的命令相关事件,请参考keyspace notification相关文档
5、一些示例
订阅键过期事件
redis1> SUBSCRIBE __keyevent@0__:expired
1) "subscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
# redis2> SETEX greeting 1 "hello world"
# 等待1秒后:
1) "message"
2) "__keyevent@0__:expired"
3) "greeting"
订阅所有事件
redis1> PSUBSCRIBE __key*@*__:*
1) "psubscribe"
2) "__key*@*__:*"
3) (integer) 1
# redis2> SET greeting "hello world"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyspace@0__:greeting"
4) "set"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyevent@0__:set"
4) "greeting"
二、SpringBoot实现Redis失效监听事件
1、场景说明
基于Redis的主动事件的处理,比如:当用户购买了会员卡十分钟内没有付款,需要通过小程序或者APP向用户主动推送购买会员卡的优势,引导用户继续完成支付并购买等,类似的场景需要用户在指定的时间点后主动通知或者继续引导,使用 Redis过期键Event优雅、快捷的实现。
因为Redis的订阅/发布是没有ACK确认机制的,因此可能会丢失消息,如果要保证消息可靠,应该选择MQ消息队列实现,
2、代码实现(Redis单机版)
2.1 环境准备
引入对应redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
设置application.yml
连接信息
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
password: 123456
timeout: 20000
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 50
min-idle: 0
最后更改redis.conf
配置 notify-keyspace-events
# 默认
notify-keyspace-events ""
# 更改为
notify-keyspace-events Ex
2.2 配置监听Bean
@Configuration
public class RedisConfig {
/**
* key过期事件订阅需要
*/
@Bean
@ConditionalOnMissingBean(RedisMessageListenerContainer.class)
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
//连接池的设置
container.setConnectionFactory(connectionFactory);
return container;
}
}
2.3 配置监听key
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String SET_NX = "setnx:";
private final Integer database = 0;
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
protected void doRegister(RedisMessageListenerContainer listenerContainer) {
String topic = "__keyevent@"+database+"__:expired";
log.info("配置监听哪个频道:"+topic);
PatternTopic patternTopic = new PatternTopic(topic);
// 频道可以是多,多个传list
listenerContainer.addMessageListener(this,patternTopic);
}
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取过期的key,可以做自己的业务
String expiredKey = message.toString();
// 寻找需要的key前缀
if(!expiredKey.startsWith("test")){
return;
}
// 利用redis setIfAbsent命令,如果为空set返回true,如果不为空返回false,类似setnx加锁操作
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(SET_NX + expiredKey, String.valueOf(System.currentTimeMillis()),10, TimeUnit.SECONDS);
if (aBoolean){
// 避免多个服务监听情况下重复消费,也可以加锁与解锁
// 注意:只能获取失效的key值,不能获取key对应的value值
System.out.println(expiredKey);
// 这里可以进行相应的业务代码
}
}
}
3、Redis集群事件监听
这里就简单说一下思路,因为不能监听集群,那就建立多个redis连接,分别对每个redis的key过期进行监听,相当于注册多个监听器,最后进行过期key的筛选即可
三、一些问题
对于延时任务一般实现的方法有几种:
-
使用 rocketmq、rabbitmq、pulsar 等消息队列的延时投递功能
-
使用 redisson 提供的 DelayedQueue
有一些方案虽然广为流传但存在着致命缺陷,不要用来实现延时任务
-
使用 redis 的过期监听
-
使用 rabbitmq 的死信队列
-
使用非持久化的时间轮
Redis6.0学习笔记
Redis过期监听机制实现订单超时处理
Redis集群下过期key监听
SpringBoot监听redis过期key