最近研究了一下“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。
- Keyspace代表与事件名称相关的消息,例如订阅对指定键进行的操作事件;
- Keyevent代表与键名称相关的消息,例如订阅发生键过期事件的相关键名称。
关于更多的notify-keyspace-events配置,可参考下面的描述:
- K:Keyspace事件,将会以__keyspace@<db>__作为事件的前缀
- E:Keyevent事件,将会以__keyevent@<db>__作为事件的前缀
- g:非特定类型的通用命令,例如DEL、EXPIRE、RENAME等
- $:字符串命令,例如SET、INCR等
- l:列表命令,例如LPUSH、LPOP等
- s:集合命令,例如SADD、SREM等
- h:哈希表命令,例如HSET、HINCRBY等
- z:有序集合命令,例如ZSET、ZREM等
- t:流命令,例如XADD、XDEL等
- x:过期事件(在每个发生键过期的时侯产生)
- e:淘汰事件(在每个发生键被淘汰的时候产生)
- m:未命中事件(在访问某个不存在的键使产生)
- A:配置g$lshztxe的别名,但不包括未命中事件m
订阅指定事件
在完成配置后,可通过SUBSCRIBE命令订阅指定信道实现对一个或多个指定事件的订阅。例如通过订阅__keyevent@0__:expired实现订阅数据库0中的键过期事件例如示例1:订阅键过期事件。
订阅的信道的格式为__<type>@<db>__:<event>,其包括了事件类型(keyspace或keyevent)、数据库(例如数据库0)以及事件(例如expired)三部分组成。对应事件的名称,可参考下文命令事件章节。
另外,也可以通过PSUBSCRIBE命令订阅一个或多个复合正则表达式匹配的信道。例如通过订阅__key*@*__:*订阅Redis中所有数据库中的所有事件。
命令事件
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相关文档:
Redis keyspace notifications | Redis
实例
配置
springboot-maven配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
springboot提供了spring-boot-starter-data-redis,里面也已经封装了spring中redis的配置spring-data-redis。
项目代码
http请求访问时,service层函数内部调用redisTemplate在Redis里设置一个注销key
例子1:多份数据,关键数据在value中
代码例子如下:
个人平台账号可申请注销
@Autowiredprivate 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);// 删除临时keyredisTemplate.delete(key + "_2");
}
注意: redis里面设置的两个key都必须删除新建RedisListenerConfig.class
@Component
public class RedisListenerConfig {@Bean@Primarypublic 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
@Transactionalpublic class ResdisExpirationListener extends KeyExpirationEventMessageListener {@AutowiredUserService userService;@Autowiredprivate RedisTemplate redisTemplate;public ResdisExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}@Overridepublic 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);// 删除临时keyredisTemplate.delete(messageKey + "_2");}}
}
好了,可以跑跑看了。
例子2:关键数据在key中
添加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);}@Overridepublic 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);@Resourceprivate RedisTemplate<String, String> template;@Resourceprivate 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 keyString 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 keyString redisKey = applicationName + ":ORDERINFO|" + orderNo;//删除redis keyredisUtil.del(redisKey);logger.info("订单redisKey " + redisKey + " 已处理");return "处理完成";}}
例子3
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