一、发布消息到redis
1.新建springboot项目
新建springboot项目redis-queue。
2.引入依赖
引入相关依赖,其中用到了lombok,需要安装lombok插件。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> </parent> <groupId>com.lpl</groupId> <artifactId>redis-queue</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis-queue</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--springboot web场景依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--属性配置支持,使用传统的xml或properties配置时需要此注解的支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!--springboot redis场景依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!--默认继承lettuce,切换成jedis需要排除依赖--> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!--加入jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--alibaba json--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <!--springboot maven插件,以maven方式提供对springboot的支持,将springboot项目打包为传统的jar活war运行--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
3.编写配置文件
编写application.properties配置文件,配置redis连接信息。
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 | server.port=8000 #redis单机版配置 spring.redis.database=0 spring.redis.host=192.168.2.75 spring.redis.port=6379 #spring.redis.password=连接密码(默认为空) #连接池中最小空闲连接 spring.redis.jedis.pool.min-idle=0 #连接池中最大空闲连接 spring.redis.jedis.pool.max-idle=8 #连接池中最大连接数(负数表示没有限制) spring.redis.jedis.pool.max-active=8 #连接池中最大阻塞等待时间(单位毫秒,负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 #redis监听的server名称 #spring.redis.sentinel.master=myMaster #哨兵的配置列表 #spring.redis.sentinel.nodes=192.168.2.76:26379,192.168.2.77:26379 #连接超时时间(单位毫秒,为0表示无限制) spring.redis.timeout=0 #不使用ssl加密 spring.redis.ssl=false #redis集群版配置 #集群中各主从节点 #spring.redis.cluster.nodes=192.168.2.75:7001,192.168.2.75:7002,192.168.2.75:7003,192.168.2.75:7004,192.168.2.75:7005,192.168.2.75:7006 #最大重定向次数(由于集群中数据存储在多个节点,所以在访问数据时需要通过转发进行数据定位) #spring.redis.cluster.max-redirects=2 |
4.编写实体类
消息实体Message.java
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 | package com.lpl.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; /** * 发送的消息实体 */ @Data @NoArgsConstructor @AllArgsConstructor public class Message implements Serializable { private String id; //消息id private String personNo; //发送人的学工号(可指定多个,以逗号隔开,不超过1000个) private String title; //消息标题 private String content; //消息内容 private String type; //消息类型,system(系统消息)、sms(短信消息) private Date createTime; //创建时间 private Date updateTime; //更新时间 private String statusCode; //消息发送结果状态码(4000表示成功,4001表示失败) } |
结果常量类ConstantResult.java
1 2 3 4 5 6 7 8 9 10 | package com.lpl.common; /** * 系统中一些变量定义 */ public class ConstantResult { public static final String SUCCESS_CODE = "200"; //成功状态码 public static final String FAIL_CODE = "500"; //失败状态码 } |
公共返回结果类CommonResult.java
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 com.lpl.common; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 返回的指定公共结果 */ @Data @NoArgsConstructor @AllArgsConstructor public class CommonResult<T> { private String code; //返回状态码 private String msg; //返回提示信息 private T data; //返回数据 public CommonResult(String code, String msg){ this.code = code; this.msg = msg; } } |
5.编写redis配置类
我们操作redis需要用到RedisTemplate,编写redis配置类RedisConfig.java。这里配置了多个消息监听适配器以通过不同的方法去监听、订阅不同的redis channel消息。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | package com.lpl.config; import com.lpl.listener.Receiver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * redis自定义配置类 */ @Configuration public class RedisConfig { /** * 返回一个RedisTemplate Bean * @param redisConnectionFactory 如果配置了集群版则使用集群版,否则使用单机版 * @return */ @Bean(name = "redisTemplate") public RedisTemplate<?, ?> getRedisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate<?, ?> template = new RedisTemplate<>(); //设置key和value序列化机制 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)); template.setConnectionFactory(redisConnectionFactory); //设置单机或集群版连接工厂 return template; } /** * 系统消息适配器 * @param receiver * @return */ @Bean(name = "systemAdapter") public MessageListenerAdapter systemAdapter(Receiver receiver){ //指定类中回调接收消息的方法 MessageListenerAdapter adapter = new MessageListenerAdapter(receiver, "systemMessage"); //adapter.afterPropertiesSet(); return adapter; } /** * 短信消息适配器 * @param receiver * @return */ @Bean(name = "smsAdapter") public MessageListenerAdapter smsAdapter(Receiver receiver){ //指定类中回调接收消息的方法 MessageListenerAdapter adapter = new MessageListenerAdapter(receiver, "smsMessage"); //adapter.afterPropertiesSet(); return adapter; } /** * 构建redis消息监听器容器 * @param connectionFactory * @param systemAdapter * @param smsAdapter * @return */ @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter systemAdapter, MessageListenerAdapter smsAdapter){ RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); //指定不同的方法监听不同的频道 container.addMessageListener(systemAdapter, new PatternTopic("system")); container.addMessageListener(smsAdapter, new PatternTopic("sms")); return container; } } |
若要使用redis集群版,需要增加如下配置类:
读取集群配置属性类RedisClusterProperty.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.lpl.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import java.util.List; /** * redis集群配置属性,在redis实例工厂提供参数配置 */ @Component @Validated @Data @ConfigurationProperties(value = "spring.redis.cluster") public class RedisClusterProperty { private List<String> nodes; //集群各节点ip和port } |
redis集群配置类RedisClusterConfig.java,当我们配置类集群版连接工厂时,创建RedisTemplate时就会使用此工厂进行创建,此时使用的就是集群版。
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 | package com.lpl.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.stereotype.Component; /** * redis集群版配置 */ @Configuration @Component public class RedisClusterConfig { @Autowired private RedisClusterProperty redisClusterProperty; /** * 配置返回RedisConnectionFactory连接工厂 * @return */ @Bean @Primary //若有相同类型的Bean时,优先使用此注解标注的Bean public RedisConnectionFactory connectionFactory(){ RedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory( new RedisClusterConfiguration(redisClusterProperty.getNodes())); return redisConnectionFactory; } } |
6.编写Service层
消息发布业务层接口PublisherService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.lpl.service; import com.lpl.bean.Message; import com.lpl.common.CommonResult; /** * 消息发布者接口 */ public interface PublisherService { /** * 发布消息到redis * @param message 消息对象 * @return */ CommonResult pubMsg(Message message); } |
消息发布业务层接口实现类PublisherServiceImpl.java
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 53 54 55 | package com.lpl.service.impl; import com.lpl.bean.Message; import com.lpl.common.CommonResult; import com.lpl.common.ConstantResult; import com.lpl.service.PublisherService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.Date; import java.util.UUID; /** * 消息发布者service实现类 */ @Service public class PublisherServiceImpl implements PublisherService { @Autowired private RedisTemplate<String, Message> redisTemplate; /** * 发送消息到redis频道供订阅。注意:使用redis客户端redis-cli登录订阅查看的中文内容是以16进制的形式 * 表示的,若要查看中文字符,需要在连接时强制原始输出 redis-cli -h localhost -p 6379 --raw * 然后再使用命令订阅频道 subscribe system * @param message 消息对象 * @return */ @Override public CommonResult pubMsg(Message message) { //返回结果 CommonResult result = null; if (null != message){ //补全消息实体 if (StringUtils.isEmpty(message.getId())){ //如果为传id则生成并返回 message.setId(UUID.randomUUID().toString()); } message.setCreateTime(new Date()); message.setUpdateTime(new Date()); try{ redisTemplate.convertAndSend(message.getType(), message); //往指定频道发布消息 //redisTemplate.opsForList().leftPush(message.getType(), message); //采用队列的形式发布到redis System.out.println("消息发布到redis队列频道:" + message.getType() + "成功!"); result = new CommonResult<Message>(ConstantResult.SUCCESS_CODE, "消息发布到" + message.getType() + "频道成功!", message); }catch (Exception e){ e.printStackTrace(); result = new CommonResult<Message>(ConstantResult.FAIL_CODE, "消息发布到" + message.getType() + "频道失败!", message); } } return result; } } |
7.编写Controller层
消息发布者controller接口,PublisherController.java
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 | package com.lpl.controller; import com.lpl.bean.Message; import com.lpl.common.CommonResult; import com.lpl.service.PublisherService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * 消息发布者controller */ @RestController public class PublisherController { private static final Logger logger = LoggerFactory.getLogger(PublisherController.class); @Autowired private PublisherService publisherService; /** * 发布消息到redis指定频道 * @param message * @return */ @PostMapping("/pubMsg") public CommonResult pubMsg(@RequestBody Message message){ CommonResult commonResult = publisherService.pubMsg(message); return commonResult; } } |
8.消息发布测试
启动项目,我们可以使用postman工具调用发送消息到redis。如下图调用:
使用redic-cli客户端登录redis并使用–raw强制原始输出(否则订阅查看到的中文内容是以16进制展示的)。
1 | ./redis-cli -h localhost -p 6379 --raw |
然后使用命令订阅相应频道消息。
1 | subscribe sms |
这里,我们看到消息已经发布到指定的频道
二、异步接收、多线程消费消息
1.编写消息监听类
消息监听类Receiver.java,我们在RedisConfig.java类中已经指定了该类中各方法对应监听的redis channel,当有消息发布到channel时,该类中对应的方法就会监听到这些消息。
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 53 54 | package com.lpl.listener; import com.alibaba.fastjson.JSONObject; import com.lpl.bean.Message; import com.lpl.service.SendAndStorageProcess; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; /** * 消息接收侦听器 */ @Component public class Receiver{ @Autowired private SendAndStorageProcess sendAndStorageProcess; private AtomicInteger counter = new AtomicInteger(); //消息计数器 /** * 接收系统消息,开启异步监听 * @param message */ @Async public void systemMessage(String message){ int counter = this.counter.incrementAndGet(); System.out.println("接收到第" + counter + "条消息!!频道为:system,消息内容为======:"); //将消息内容字符串转化为对象 Message messageObject = JSONObject.parseObject(message, Message.class); System.out.println(messageObject.getContent()); //TODO 开启多线程调用发送并处理消息 JSONObject result = sendAndStorageProcess.sendAndStorageMsg(messageObject); } /** * 接收短信消息,开启异步监听 * @param message */ @Async public void smsMessage(String message){ int counter = this.counter.incrementAndGet(); System.out.println("接收到第" + counter + "条消息!!频道为:sms,消息内容为======:"); //将消息内容字符串转化为对象 Message messageObject = JSONObject.parseObject(message, Message.class); System.out.println(messageObject.getContent()); //TODO 开启多线程调用发送 JSONObject result = sendAndStorageProcess.sendAndStorageMsg(messageObject); } } |
2.开启异步接收
在主类中开启异步支持(注意:需要开启异步的方法不能是private修饰),在Receiver.java类中的监听方法已经开启了异步。
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 com.lpl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import java.util.concurrent.Executor; /** * 关于redis单机切换到集群,打开application.properties文件的redis集群配置和RedisClusterConfig文 * 件的RedisConnectionFactory Bean注册配置。 */ @SpringBootApplication @EnableAsync public class RedisQueueCacheApplication { public static void main(String[] args) { SpringApplication.run(RedisQueueCacheApplication.class, args); } } |
3.编写线程池配置类
线程池配置类TaskExecutorConfig.java
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 | package com.lpl.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /** * 线程池配置类 */ @Configuration public class TaskExecutorConfig { /** * 创建一个线程池 * @return */ @Bean(name = "threadTaskExecutor") public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); //核心线程池大小 executor.setMaxPoolSize(50); //最大线程池大小 executor.setQueueCapacity(1000); //任务队列大小 executor.setKeepAliveSeconds(300); //线程池中空闲线程等待工作的超时时间(单位秒) executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //线程拒绝策略,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度 return executor; } /** * 创建一个固定大小的线程池 * @return */ @Bean(name = "fixedThreadPool") public ExecutorService executorService(){ ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); return fixedThreadPool; } } |
4.编写消息处理类
消息处理类(多线程处理)SendAndStorageProcess.java,类中发消息的方法使线程休眠了两秒,模拟发消息的相对耗时操作,用于验证多线程。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | package com.lpl.service; import com.alibaba.fastjson.JSONObject; import com.lpl.bean.Message; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import java.util.Date; import java.util.concurrent.Callable; import java.util.concurrent.Future; /** * 发送和存储消息任务 */ @Component public class SendAndStorageProcess{ @Autowired private ThreadPoolTaskExecutor threadTaskExecutor; //注入线程池 /** * 多线程调用发送消息 * @param message * @return */ public JSONObject sendAndStorageMsg(Message message) { Future<JSONObject> future = threadTaskExecutor.submit(new Callable<JSONObject>() { //采用带返回值的方式 @Override public JSONObject call() throws Exception { //1.调用相对比较耗时的发消息方法 String code = sendMessage(message); message.setUpdateTime(new Date()); if ("200".equals(code)){ //发送成功 message.setStatusCode("4000"); }else{ //发送失败 message.setStatusCode("4001"); } //2.存储消息 storageMessage(message); JSONObject result = new JSONObject(); result.put("code", "200"); result.put("msg", "发送消息成功!"); return result; } }); JSONObject jsonResult = new JSONObject(); //返回结果 try{ if (future.isDone()){ //线程调度结束时,才获取结果 jsonResult = future.get(); } }catch (Exception e){ e.printStackTrace(); } return jsonResult; //消息发送与存储结果 } /** * 调用接口发送消息 * @param message * @return */ private String sendMessage(Message message) { try{ //TODO 这里写一些发消息的业务逻辑 Thread.sleep(2000); //增加耗时操作,查看多线程效果 System.out.println(Thread.currentThread().getName() + "线程发送消息成功,消息内容:" + message.getContent()); return "200"; //发送消息结果状态码 }catch (Exception e){ System.out.println(Thread.currentThread().getName() + "线程发送消息失败,消息内容:" + message.getContent()); e.printStackTrace(); } return "500"; //发送消息结果状态码 } /** * 存消息到数据库 * @param message * @return */ private void storageMessage(Message message) { try{ //TODO 这里执行插入消息到数据操作 System.out.println(Thread.currentThread().getName() + "线程插入消息到数据库成功,消息内容:" + message.getContent()); }catch (Exception e){ System.out.println(Thread.currentThread().getName() + "线程插入消息到数据库失败,消息内容:" + message.getContent()); e.printStackTrace(); } } } |
5.消息消费测试
快速调用接口,看到如下打印,已经实现了异步、多线程发送效果。
OVER,如有错误!感谢指正!