• 集成 Redis & 异步任务 - SpringBoot 2.7 .2实战基础


    SpringBoot 2.7 .2实战基础 - 09 - 集成 Redis & 异步任务

    1 集成Redis

    《docker 安装 MySQL 和 Redis》一文已介绍如何在 Docker 中安装 Redis,本文就看看 SpringBoot 如何整合 Redis。SpringBoot 提供了整合 Redis 的 starter,使用非常简单。

    1.1 添加依赖

    在 pom.xml 中添加 redis 的 starter:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    

    1.2 配置 Redis

    修改 application.yml 文件,添加 Redis 的配置:

    spring:
      redis:
        host: 127.0.0.1
        port: 6379
        username:
        password:
        timeout: 5000
        jedis:
          pool:
            max-active: 3
            max-idle: 3
            min-idle: 1
            max-wait: -1
    

    1.3 添加配置

    com.yygnb.demo.config 中创建 RedisConfig,处理一些中文乱码问题。

    com.yygnb.demo.config.RedisConfig

    @Configuration
    public class RedisConfig {
    
        private final RedisTemplate redisTemplate;
    
        public RedisConfig(RedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        /**
         * 解决redis插入中文乱码
         * @return
         */
        @Bean
        public RedisTemplate redisTemplateInit() {
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringRedisSerializer);
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
    
            GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
            redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
            redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
            return redisTemplate;
        }
    
    }
    

    1.4 封装 Redis 操作

    可封装一些 Redis 的常见操作。

    com.yygnb.demo.utils.RedisUtils

    @RequiredArgsConstructor
    @Component
    public class RedisUtils {
    
        private final RedisTemplate redisTemplate;
    
        /**
         * 指定缓存失效时间
         * @param key
         * @param time 单位 秒
         */
        public void expire(Serializable key, long time) {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
        }
    
        /**
         * 根据key 获取过期时间
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断key是否存在
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除缓存
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    redisTemplate.delete(CollectionUtils.arrayToList(key));
                }
            }
        }
    
        /**
         * 普通缓存获取
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 普通缓存放入
         * @param key 键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 普通缓存放入并设置时间
         * @param key 键
         * @param value 值
         * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean set(String key, Serializable value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }
    

    操作太多,这里简单放了几个常见的方法。

    1. 5 测试

    新建 controller 测试 Redis 的操作。

    com.yygnb.demo.controller.RedisDemoController

    @Tag(name = "Redis测试接口")
    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/redis")
    public class RedisDemoController {
    
        private final RedisUtils redisUtils;
    
        @Operation(summary = "存值")
        @PostMapping()
        public void save(@RequestBody Map map) {
            Set keys = map.keySet();
            for (String key : keys) {
                redisUtils.set(key, map.get(key));
            }
        }
    
        @Operation(summary = "取值")
        @GetMapping()
        public Object get(String key) {
            return redisUtils.get(key);
        }
    }
    

    2 异步任务

    异步任务在后端开发中很常见,如生成报表,前端调用后端一个接口,如果数据量较大,后端生成报表会非常耗时,这时候如果是同步任务,等后端报表已经生成后,估计已经请求超时了。通常情况下,后端触发一个异步任务,成功触发任务后,后端就返回前端,无需等待报表生成成功。类似场景如同时给用户发送邮件和短信。

    2.1 准备工作

    (1) 两个 Service

    分别编写模拟发送邮件和短信的 Service,这里只是演示使用,故 Service 省略了接口定义,直接编写实现类:

    com.yygnb.demo.service.impl.EmailService

    @Slf4j
    @Service
    public class EmailService {
    
        public void sendEmail(String msg) {
            log.info("开始发送邮件: {}", msg);
            int i = new Random().nextInt(5);
            try {
                Thread.sleep(i * 1000);
                log.info("邮件发送成功");
            } catch (InterruptedException e) {
                log.error("邮件发送失败");
                e.printStackTrace();
            }
        }
    }
    

    发送短信的 Service 与邮件 service 类似。

    com.yygnb.demo.service.impl.SmsService

    @Slf4j
    @Service
    public class SmsService {
    
        public void sendSms(String msg) {
            log.info("开始发送短信: {}", msg);
            int i = new Random().nextInt(5);
            try {
                Thread.sleep(i * 1000);
                log.info("短信发送成功");
            } catch (InterruptedException e) {
                log.error("短信发送失败");
                e.printStackTrace();
            }
        }
    }
    

    (2) 接口开发

    创建 DemoService,在 DemoService 中调用上面两个 Service:

    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class DemoServiceImpl implements DemoService {
    
        private final EmailService emailService;
        private final SmsService smsService;
    
        @Override
        public void send(String msg) {
            log.info("发别发送短信和邮件");
            smsService.sendSms(msg);
            emailService.sendEmail(msg);
            log.info("Demo Service 结束");
        }
    }
    

    在 DemoController 中添加接口:

    @Slf4j
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("demo")
    public class DemoController {
    
        private final DemoService demoService;
    
        @GetMapping("async")
        public void asyncDemo(String msg) {
            demoService.send(msg);
        }
    }
    

    (3) 运行

    请求该接口:

    http://localhost:9099/demo/async?msg=hello
    

    由于现在是同步执行,需要短信和邮件两个service都执行完后才会返回结果。而且输出的日志顺序是固定的:

    image-20220905175810080

    2.2 异步任务

    接下来进行异步任务的改造。

    (1) @EnableAsync

    首先在启动类上添加注解 @EnableAsync 开启异步任务。

    @EnableAsync
    @MapperScan("com.yygnb.demo.mapper")
    @SpringBootApplication
    public class DemoApplication {
      ...
    }
    

    (2)@Async

    在需要异步执行的方法上添加注解 @Async。在 sendSmssendEmail 两个方法上添加该注解:

    ...
        @Async
        public void sendEmail(String msg) {
    ...
        }
    ...
    

    (3)注意事项

    调用异步任务的方法与异步任务的方法,不能在同一个 Service 中,即上面的 demo中,send 方法与 sendSms 不能在同一个 Service中。

    2.3 自定义线程池

    异步任务本质上是在子线程中运行的。可以自定义线程池。

    (1)定义配置的实体类

    创建线程池配置的实体类:

    com.yygnb.demo.config.ThreadPoolInfo

    @Data
    @Component
    @ConfigurationProperties(prefix = "thread-pool")
    public class ThreadPoolInfo {
    
        private int corePoolSize = 1;
    
        private int maxPoolSize = Integer.MAX_VALUE;
    
        private int keepAliveSeconds = 60;
    
        private int queueCapacity = Integer.MAX_VALUE;
    
        private String threadNamePrefix = "thread-";
    }
    

    (2)配置类

    com.yygnb.demo.config.AsyncConfig

    @RequiredArgsConstructor
    @Configuration
    public class AsyncConfig {
    
        private final ThreadPoolInfo info;
    
        @Bean("asyncExecutor")
        public Executor asyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(info.getCorePoolSize());
            executor.setMaxPoolSize(info.getMaxPoolSize());
            executor.setQueueCapacity(info.getQueueCapacity());
            executor.setThreadNamePrefix(info.getThreadNamePrefix());
            executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
            executor.initialize();
            return executor;
        }
    }
    

    (3)配置 YML

    在 application.yml 中配置线程池:

    thread-pool:
      core-pool-size: 3
      max-pool-size: 5
      thread-name-prefix: yyg-async-
    

    重启服务,访问上面的接口,日志如下:

    image-20220905182201881

    image

    感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货

  • 相关阅读:
    树莓派快速上手-远程调试图形界面
    Llama-7b-hf和vicuna-7b-delta-v0合并成vicuna-7b-v0
    js--解决工厂创建对象的缺点---使用构造函数
    函数的分文件编写
    C#基于inpoutx64读写ECRAM硬件信息
    自定义修改Discuz X3 今日发帖数量 昨日发帖数量 帖子总数 会员总数
    图像处理之图像傅里叶变换
    四种强大且隐秘的缓存
    【Linux】/proc/meminfo获取的参数信息分别是什么意思呐?
    基于docker搭建code-server(浏览器中的VScode)
  • 原文地址:https://www.cnblogs.com/youyacoder/p/16661965.html