最近研究了一下“redis定时触发”,网上查了查资料,这里记录一下。
从Redis 2.8.0开始,Redis加入了发布/订阅模式以及键空间消息提醒(keyspace notification)功能。键空间消息提醒提供了允许客户端通过订阅指定信道获取Redis数据变化的能力。
需要注意的是,键空间消息提醒并非可靠的,它不会对订阅端是否接收到消息进行确认。例如某个订阅的客户端暂时断开连接,在其直到恢复连接期间发生的事件将无法再次获得。
在默认情况下,Redis并未开启键空间消息提醒功能。为了打开该功能,需要通过notify-keyspace-events配置进行设置,例如:
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。
关于更多的notify-keyspace-events配置,可参考下面的描述:
在完成配置后,可通过SUBSCRIBE命令订阅指定信道实现对一个或多个指定事件的订阅。例如通过订阅__keyevent@0__:expired实现订阅数据库0中的键过期事件例如示例1:订阅键过期事件。
订阅的信道的格式为__
另外,也可以通过PSUBSCRIBE命令订阅一个或多个复合正则表达式匹配的信道。例如通过订阅__key*@*__:*订阅Redis中所有数据库中的所有事件。
Redis为许多命令提供了不同的事件,在本文中将选择其中部分命令及其对应的事件进行介绍:
关于更多的命令相关事件,请参考keyspace notification相关文档:
Redis keyspace notifications | Redis
springboot-maven配置
springboot提供了spring-boot-starter-data-redis,里面也已经封装了spring中redis的配置spring-data-redis。
http请求访问时,service层函数内部调用redisTemplate在Redis里设置一个注销key
代码例子如下:
个人平台账号可申请注销
@Autowired
private RedisTemplate redisTemplate;
public void deleteUser(String key, String value){
// 关键代码
redisTemplate.opsForValue().set(key, value);
// 设置七天注销时间
redisTemplate.expire(key, DEFAULT_DELETE_TIME, TimeUnit.SECONDS);
// 监听过期key,获取value使用
String tempKey = key + "_2";
redisTemplate.opsForValue().set(tempKey, value);
}
注意:
redis里面设置了两个key,原因在于:key过期之后,在ResdisExpirationListener 的 onMessage函数中 无法拿到key对应的value,所以设置两个,key不同但是value一样。这个value是为了key到期之后触发想要的任务函数。
申请注销后七天内可撤销注销
public void withdrawDeleteUser(String key) {
redisTemplate.delete(key);
// 删除临时key
redisTemplate.delete(key + "_2");
}
注意: redis里面设置的两个key都必须删除新建RedisListenerConfig.class
@Component
public class RedisListenerConfig {
@Bean
@Primary
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
新建ResdisExpirationListener.class
业务最主要在onMessage函数,key过期后会自动触发该函数,message信息是key,根据redisTemplate和另外设置的一个key拿到value,这个value在触发的定时任务函数里面用到了,所以必须拿到。
@Slf4j
@Component
@Transactional
public class ResdisExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
UserService userService;
@Autowired
private RedisTemplate redisTemplate;
public ResdisExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern){
String messageKey = message.toString();
// 业务实现
if(messageKey.contains(RedisKey.DELETE_USER)){
String userKey = redisTemplate.opsForValue().get(messageKey + "_2").toString();
Long userId = Long.parseLong(userKey);
// 触发定时任务
userService.deleteUserProcess(userId);
// 删除临时key
redisTemplate.delete(messageKey + "_2");
}
}
}
好了,可以跑跑看了。
添加redis监听
package com.cpl.tsl.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
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;
import java.util.Date;
/**
* RedisKey键监听以及业务逻辑处理
*
* @author: lll
* @date: 2022年03月07日 14:03:49
*/
@Component
public class RedisTaskListener extends KeyExpirationEventMessageListener {
private static final Logger logger = LoggerFactory.getLogger(RedisTaskListener.class);
@Value("${applicationName:tsl}")
private String applicationName;
/**
* @param listenerContainer 监听器
*/
public RedisTaskListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
// 将拿到的过期键使用之前拼接时的特殊符号分割成字符数组
String[] expiredKeyArr = expiredKey.split("\\|");
String businessSign = expiredKeyArr[0].toString();
String expiredTimeSign = expiredKeyArr[1].toString();
logger.info(businessSign +":"+ expiredTimeSign);
Date date = new Date();
// 只有本业务才执行以下操作
if (businessSign.equals(applicationName + ":ORDERINFO")) {
logger.info("订单超时,已取消");
} else {
logger.error("非订单业务不做处理");
}
}
}
添加controller调动接口
package com.cpl.tsl.controller;
import com.cpl.tsl.utils.RedisUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Date;
import java.util.UUID;
/**
* 测试类
*
*/
@RestController
@RequestMapping("/")
@Api(tags = "测试模块")
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@Resource
private RedisTemplate template;
@Resource
private RedisUtil redisUtil;
@Value("${applicationName:tsl}")
private String applicationName;
/**
* redis设置定时key,模拟订单下单,超时提醒或者去掉订单
*
*/
@RequestMapping(value = "/putkeys", method = RequestMethod.POST)
@ApiOperation(value = "测试redis存储参数", notes = "测试redis存储参数")
public String putRedisTaskKeys() {
/**
* 存入订单信息
*/
Date date = new Date();
//设置超时时间30秒
Long overTime = new Long(30);
//创建订单号
String orderNo = UUID.randomUUID().toString();
//订单信息
String orderInfo = "这是订单号为:" + orderNo + " 的订单,价格是:2000元,下单时间是:" + date;
//redis key
String redisKey = applicationName + ":ORDERINFO|" + orderNo;
redisUtil.set(redisKey, orderInfo, overTime);
logger.info("下单时间:" + date);
logger.info("订单的redisKey " + redisKey + " 订单信息 " + orderInfo);
return "下单成功";
}
/**
* 手动处理订单,从redis移除订单
*
*/
@RequestMapping(value = "/removeKeys", method = RequestMethod.POST)
@ApiOperation(value = "测试redis移除参数", notes = "测试redis移除参数")
public String removeRedisTaskKeys(@ApiParam(name = "orderNo", value = "订单号", required = true) @RequestParam("orderNo") String orderNo) {
/**
* 处理订单
*/
//拼接redis key
String redisKey = applicationName + ":ORDERINFO|" + orderNo;
//删除redis key
redisUtil.del(redisKey);
logger.info("订单redisKey " + redisKey + " 已处理");
return "处理完成";
}
}
application.yml
redis:
localhost: localhost
port: 6379
database: 7
password:
# 过期事件订阅,接收7号数据库中所有key的过期事件
listen-pattern: __keyevent@7__:expired
Redis 事件广播配置类
import com.coisini.springbootlearn.core.listener.RedisMessageListener;
import org.springframework.beans.factory.annotation.Value;
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;
import org.springframework.data.redis.listener.Topic;
@Configuration
public class RedisListenerConfiguration {
@Value("${spring.redis.listen-pattern}")
public String pattern;
@Bean
public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory redisConnection) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnection);
/**
* Topic是消息发布(Pub)者和订阅(Sub)者之间的传输中介
*/
Topic topic = new PatternTopic(this.pattern);
container.addMessageListener(new RedisMessageListener(), topic);
return container;
}
}
Redis 事件广播监听器
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
public class RedisMessageListener implements MessageListener {
/**
* Redis 事件监听回调
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] body = message.getBody();
String expiredKey = new String(body);
System.out.println("监听到已过期的key:" + expiredKey);
/**
* 监听到过期事件回调
* TODO:
*/
}
}
测试接口
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping(value = "/setExpiredVal")
public String setExpiredVal(@RequestParam String name) {
// 设置 20s 后过期
redisTemplate.opsForValue().set("name", name, 20, TimeUnit.SECONDS);
return "setVal is ok";
}
}
在 Spring Boot 中整合 Redis 监听订单超时主要的优缺点:
优点:
实时性:使用 Redis 来监听订单超时,可以实现实时性处理。当订单超时时,处理操作可以立即触发,而不需要定期轮询数据库或其他方式。
高性能:Redis 是一个内存数据库,因此具有高性能。它能够快速存储和检索数据,适合用于订单超时处理。
可扩展性:Redis 支持分布式部署,因此可以轻松扩展应用程序以处理更多订单。可以使用 Redis Sentinel 或 Redis Cluster 来实现高可用性和负载均衡。
减轻数据库压力:将订单超时的检查和处理从数据库转移到 Redis,可以减轻数据库服务器的负载,因为不再需要频繁地查询数据库。
简化代码:Redis 提供了内置的过期键和发布/订阅功能,这些功能使订单超时的处理逻辑更加简单和可维护。
缺点:
单一点故障:如果 Redis 实例发生故障,可能导致订单超时处理不可用。为了解决这个问题,可以使用 Redis Sentinel 或 Redis Cluster 来提高可用性。
不适合持久性数据:Redis 是一个内存数据库,不适合用于持久性数据存储。如果订单数据需要长期保留,仍然需要在数据库中保留订单信息。
配置和维护:Redis 需要一些配置和维护工作,包括备份、监控、调整内存限制等。这可能需要额外的管理工作。
消息队列的竞争条件:如果多个实例同时处理订单超时,可能会引发竞争条件,需要在代码中进行处理。
性能成本:虽然 Redis 具有高性能,但在大规模订单处理时,可能需要更多的 Redis 实例和更强大的硬件,这可能带来一些成本。
Redis键空间通知详解_notify-keyspace-events-CSDN博客
Spring boot整合 redis实现订单超时处理 - 掘金
https://www.cnblogs.com/vergilyn/p/7285457.html