新建springboot项目redis-queue。
引入相关依赖,其中用到了lombok,需要安装lombok插件。
<?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>
编写application.properties配置文件,配置redis连接信息。
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
消息实体Message.java
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
package com.lpl.common;
/**
* 系统中一些变量定义
*/
public class ConstantResult {
public static final String SUCCESS_CODE = "200"; //成功状态码
public static final String FAIL_CODE = "500"; //失败状态码
}
公共返回结果类CommonResult.java
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;
}
}
我们操作redis需要用到RedisTemplate,编写redis配置类RedisConfig.java。这里配置了多个消息监听适配器以通过不同的方法去监听、订阅不同的redis channel消息。
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
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时就会使用此工厂进行创建,此时使用的就是集群版。
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;
}
}
消息发布业务层接口PublisherService.java
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
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;
}
}
消息发布者controller接口,PublisherController.java
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;
}
}
启动项目,我们可以使用postman工具调用发送消息到redis。如下图调用:
使用redic-cli客户端登录redis并使用–raw强制原始输出(否则订阅查看到的中文内容是以16进制展示的)。
./redis-cli -h localhost -p 6379 --raw
然后使用命令订阅相应频道消息。
subscribe sms
消息监听类Receiver.java,我们在RedisConfig.java类中已经指定了该类中各方法对应监听的redis channel,当有消息发布到channel时,该类中对应的方法就会监听到这些消息。
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);
}
}
在主类中开启异步支持(注意:需要开启异步的方法不能是private修饰),在Receiver.java类中的监听方法已经开启了异步。
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);
}
}
线程池配置类TaskExecutorConfig.java
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;
}
}
消息处理类(多线程处理)SendAndStorageProcess.java,类中发消息的方法使线程休眠了两秒,模拟发消息的相对耗时操作,用于验证多线程。
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();
}
}
}