springboot-redis设置定时触发任务详解

最近研究了一下“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@__作为事件的前缀
  • E:Keyevent事件,将会以__keyevent@__作为事件的前缀
  • 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:订阅键过期事件。

        订阅的信道的格式为__@__:,其包括了事件类型(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配置

            org.springframework.boot
            spring-boot-starter-data-redis


        springboot提供了spring-boot-starter-data-redis,里面也已经封装了spring中redis的配置spring-data-redis。


项目代码

http请求访问时,service层函数内部调用redisTemplate在Redis里设置一个注销key

例子1:多份数据,关键数据在value中

代码例子如下:

个人平台账号可申请注销
 

    @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");

        }
    }
}

好了,可以跑跑看了。

例子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);
    }

    @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 "处理完成";
    }

}

例子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 监听订单超时主要的优缺点:

优点

  1. 实时性:使用 Redis 来监听订单超时,可以实现实时性处理。当订单超时时,处理操作可以立即触发,而不需要定期轮询数据库或其他方式。

  2. 高性能:Redis 是一个内存数据库,因此具有高性能。它能够快速存储和检索数据,适合用于订单超时处理。

  3. 可扩展性:Redis 支持分布式部署,因此可以轻松扩展应用程序以处理更多订单。可以使用 Redis Sentinel 或 Redis Cluster 来实现高可用性和负载均衡。

  4. 减轻数据库压力:将订单超时的检查和处理从数据库转移到 Redis,可以减轻数据库服务器的负载,因为不再需要频繁地查询数据库。

  5. 简化代码:Redis 提供了内置的过期键和发布/订阅功能,这些功能使订单超时的处理逻辑更加简单和可维护。

缺点

  1. 单一点故障:如果 Redis 实例发生故障,可能导致订单超时处理不可用。为了解决这个问题,可以使用 Redis Sentinel 或 Redis Cluster 来提高可用性。

  2. 不适合持久性数据:Redis 是一个内存数据库,不适合用于持久性数据存储。如果订单数据需要长期保留,仍然需要在数据库中保留订单信息。

  3. 配置和维护:Redis 需要一些配置和维护工作,包括备份、监控、调整内存限制等。这可能需要额外的管理工作。

  4. 消息队列的竞争条件:如果多个实例同时处理订单超时,可能会引发竞争条件,需要在代码中进行处理。

  5. 性能成本:虽然 Redis 具有高性能,但在大规模订单处理时,可能需要更多的 Redis 实例和更强大的硬件,这可能带来一些成本。

参考

Redis键空间通知详解_notify-keyspace-events-CSDN博客

Spring boot整合 redis实现订单超时处理 - 掘金

https://www.cnblogs.com/vergilyn/p/7285457.html

你可能感兴趣的:(SpringBoot,Redis,spring,boot,redis,bootstrap)