SpringBoot实现监听redis key失效事件

SpringBoot实现监听redis key失效事件

当用户创建订单后需在30分钟内支付,否则订单失效。

image

一、基于定时任务

对于这样的需求我们首先想到的是指定时间的定时任务,或者长轮询的定时查询订单是否失效。这样的方式也能实现但这种高效率的延迟任务用任务调度(定时器)实现就得不偿失,而且对系统也是一种压力且数据库消耗极大。

二、基于MQ的定时消息或延迟消息实现

定时消息:Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。

延迟消息:Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

定时消息与延迟消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到 MQ 服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。

目前业界MQ对定时消息和延迟消息的支持情况:

image

可以看见不同的MQ对定时任务支持方式不同,而且技术复杂度和成本提高。

三、使用Redis过期Key监听实现

利用redis的key自动过期机制,再下单时将订单id写入redis,过期时间30分钟,监听key过期事件进行支付超时处理。

这种方式处理起来简单的很,对业务量不大的公司来说完全可以接受。

存在的弊端:

  • 1.后台服务不可用时,如果此时有key失效那么是监听不到的。(解决方法:项目启动时或定时任务补偿处理)
  • 2.redis重启或不可用时,导致业务无法处理。(解决方法:redis集群实现高可用)
1、Redis过期事件

通过订阅与发布功能(pub/sub)来进行分发。而对超时的监听呢,并不需要自己发布,只有修改配置文件redis.conf中的:notify-keyspace-events Ex,默认为notify-keyspace-events “”

1
2
3
4
5
6
7
8
9
10
11
K    键空间通知,以__keyspace@<db>__为前缀
E 键事件通知,以__keysevent@<db>__为前缀
g del , expipre , rename 等类型无关的通用命令的通知, ...
$ String命令
l List命令
s Set命令
h Hash命令
z 有序集合命令
x 过期事件(每次key过期时生成)
e 驱逐事件(当key在内存满了被清除时生成)
A g$lshzxe的别名,因此”AKE”意味着所有的事

打开一个redis-cli ,监控db0的key过期事件

1
2
3
4
5
127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1

打开另一个redis-cli ,发送定时过期key

1
127.0.0.1:6379> setex test_key 3 test_value

观察上一个redis-cli ,会发现收到了过期的key test_key,但是无法收到过期的value test_value

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "test_key"
2、集成SpringBoot中使用

首先要添加POM依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

实现方式一:使用监听器,该接口监听所有db的过期事件keyevent@*:expired

  • RedisListenerConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.pconline.config.redis;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
* @Description redis监听配置类
* 键空间通知(keyspace notification)http://redisdoc.com/topic/notification.html
* @Author jie.zhao
* @Date 2019/8/28 17:28
*/
@Configuration
public class RedisListenerConfig {

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
  • RedisKeyExpirationListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package cn.pconline.config.redis;

import cn.pconline.common.constant.CommonConstant;
import cn.pconline.redis.client.RedisClient;
import cn.pconline.service.ICouponOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
* @Description 监听所有db的过期事件__keyevent@*__:expired"
* @Author jie.zhao
* @Date 2019/8/28 17:32
*/
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

@Autowired
private ICouponOrderService couponOrderService;

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

/**
* 针对redis数据失效事件,进行数据处理
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 用户做自己的业务处理即可,注意message.toString()可以获取失效的key
String expiredKey = message.toString();

//监听优惠卷订单Key失效
if (expiredKey.startsWith(CommonConstant.COUPON_NO_PAY)) {
log.info("订单支付时间已过,key:{} ", expiredKey);
couponOrderService.payOverTime(expiredKey.substring(expiredKey.lastIndexOf(":")+1));
}
}
}

实现方式二:自定义监听器,这个地方定义的比较灵活,可以自己定义监控什么事件。

  • RedisListenerConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cn.pconline.config.redis;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
* @Description redis监听配置类
* 键空间通知(keyspace notification)http://redisdoc.com/topic/notification.html
* @Author jie.zhao
* @Date 2019/8/28 17:28
*/
@Configuration
public class RedisListenerConfig {

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(new RedisExpiredListener(), new PatternTopic("__keyevent@0__:expired"));
return container;
}
}
  • RedisExpiredListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.pconline.config.redis;

import cn.pconline.common.constant.CommonConstant;
import cn.pconline.common.utils.SpringContextUtil;
import cn.pconline.service.ICouponOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;

/**
* @Description redis过期监听
* @Author jie.zhao
* @Date 2019/8/29 9:07
*/
@Slf4j
public class RedisExpiredListener implements MessageListener {

//未解决,这里不能自动注入需要手动获取
ICouponOrderService couponOrderService = SpringContextUtil.getBean(ICouponOrderService.class);

/**
* 客户端监听订阅的topic,当有消息的时候,会触发该方法;
* 并不能得到value, 只能得到key。
* 姑且理解为: redis服务在key失效时(或失效后)通知到java服务某个key失效了, 那么在java中不可能得到这个redis-key对应的redis-value。
* * 解决方案:
* 创建copy/shadow key, 例如 set vkey "vergilyn"; 对应copykey: set copykey:vkey "" ex 10;
* 真正的key是"vkey"(业务中使用), 失效触发key是"copykey:vkey"(其value为空字符为了减少内存空间消耗)。
* 当"copykey:vkey"触发失效时, 从"vkey"得到失效时的值, 并在逻辑处理完后"del vkey"
* <p>
* 缺陷:
* 1: 存在多余的key; (copykey/shadowkey)
* 2: 不严谨, 假设copykey在 12:00:00失效, 通知在12:10:00收到, 这间隔的10min内程序修改了key, 得到的并不是 失效时的value.
* (第1点影响不大; 第2点貌似redis本身的Pub/Sub就不是严谨的, 失效后还存在value的修改, 应该在设计/逻辑上杜绝)
* 当"copykey:vkey"触发失效时, 从"vkey"得到失效时的值, 并在逻辑处理完后"del vkey"
*/
@Override
public void onMessage(Message message, byte[] bytes) {
String expiredKey = message.toString();
byte[] body = message.getBody();
byte[] channel = message.getChannel();
log.info("onMessage >> ");
log.info(String.format("key:%s,channel: %s, body: %s, bytes: %s",
expiredKey, new String(channel), new String(body), new String(bytes)));

//监听优惠卷订单Key失效
if (expiredKey.startsWith(CommonConstant.COUPON_NO_PAY)) {
log.info("订单支付时间已过,key:{} ", expiredKey);
couponOrderService.payOverTime(expiredKey.substring(expiredKey.lastIndexOf(":") + 1));
}
}

}
-------------已经触及底线 感谢您的阅读-------------

本文标题:SpringBoot实现监听redis key失效事件

文章作者:趙小傑~~

发布时间:2019年07月06日 - 12:30:03

最后更新:2019年09月01日 - 00:49:39

原始链接:https://cnsyear.com/posts/f565a40.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%