章节介绍
我个人比较喜欢冲程序运行的顺序来写代码。所以本节主要介绍Example项目的启动及测试队列工具的编写。
Application
com.davin.DemoApplication
package com.davin;
import com.davin.service.RedisQueueService;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @author davin.bao
*/
@SpringBootApplication
public class DemoApplication {
private static final Logger log = LoggerFactory.getLogger(DemoApplication.class);
@Autowired
public RedisQueueService redisQueueService;
private final Environment env;
public DemoApplication(Environment env) {
this.env = env;
}
@PostConstruct
public void initApplication() {
redisQueueService.init();
redisQueueService.produce();
}
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(DemoApplication.class);
Environment env = app.run(args).getEnvironment();
}
}
代码解释:
@PostConstruct 注释的initApplication 期望运行服务时仅运行一次该方法。是为了启动我们的redis队列服务,redisQueueService.init() 是初始化我们的队列服务, redisQueueService.produce() 是作为生产者生产一些任务,用来测试队列是否可正常消费。
Redis 队列服务
如上所示,RedisQueueService(example\src\main\java\com\davin\service\RedisQueueService.java) 提供了两个接口,具体如下:
package com.davin.service;
/**
* @author davin.bao
* @date 2024/6/4
*/
public interface RedisQueueService {
/**
* 初始化
*/
void init();
/**
* 生产消息
*/
void produce();
}
服务实现RedisQueueServiceImpl (example\src\main\java\com\davin\service\impl\RedisQueueServiceImpl.java),需要声明可伸缩队列的实例,以便初始化, 我们实现的队列管理不依赖于具体队列存储方案,你可以用现在市面上任何实现了BlockingQueue的产品,本实例使用Redisson实现。
package com.davin.service.impl;
/**
* @author davin.bao
* @date 2024/6/4
*/
@Service
public class RedisQueueServiceImpl implements RedisQueueService {
private final static Logger log = LoggerFactory.getLogger(RedisQueueServiceImpl.class);
/** 注入RedissonClient示例 */
@Autowired
private RedissonClient redissonClient;
/** 可伸缩队列 */
private ElasticQueue<String> elasticQueue;
/**
* 初始化
*/
@Override
public void init() {
...
}
/**
* 生产消息
*/
@Override
public void produce() {
...
}
}
上面代码注入 RedissonClient 需要我们编写配置项,否则Spring会因无法导入配置而注入失败。具体配置RedisConfiguration(example\src\main\java\com\davin\config\RedisConfiguration.java)如下:
package com.davin.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.util.Strings;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URI;
/**
* @author davin.bao
* @date 2024/6/4
*/
@Configuration
@EnableCaching
public class RedisConfiguration {
@Bean
public RedissonClient getRedissonClient(Config redissConfig) {
return Redisson.create(redissConfig);
}
@Bean
public Config redissConfig(@Value("${spring.redis.host}") String host,
@Value("${spring.redis.port}") int port,
@Value("${spring.redis.database:0}") int database,
@Value("${spring.redis.password}") String password,
ObjectMapper objectMapper) {
String address = String.format("redis://%s:%s", host, port);
Config config = new Config();
SingleServerConfig singleServerConfig = config
.useSingleServer()
.setAddress(host + ":" + port)
.setDatabase(database)
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(24)
.setSubscriptionConnectionPoolSize(50)
.setAddress(address);
if (Strings.isNotEmpty(password)) {
singleServerConfig.setPassword(password);
}
config.setCodec(new JsonJacksonCodec(objectMapper));
return config;
}
}
上述配置需要在application.yml(example\src\main\resources\application.yml) 中配置如下参数:
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
到这一步我们的RedissonClient可以成功注入到 RedisQueueServiceImpl里来了。接下来看下ElasticQueue<String>
的声明, 其中String为队列任务的类型,我们为了简单使用了String类型,你可以声明你的业务需要的任务类型替代它。ElasticQueue并非注入进来,而是一个普通的类,这样做的好处就是可以根据业务需求灵活的声明多个队列,他们之间没有任何的关联。
接下来我们看下 produce()
方法的实现:
/**
* 生产消息
*/
@Override
public void produce() {
getExecutor().execute(() -> {
...
});
}
代码中getExecutor()
是获取线程池执行器,这样做的目的是模拟的生产者应该是独立的线程进行任务的添加,而不是征用主线程来做这个事情。具体该方法的实现如下:
private ThreadPoolExecutor getExecutor() {
return new ThreadPoolExecutor(
5, 10,
1L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
具体参数不是本教程重点,不清楚可以查阅相关文档。
关于 produce()
方法的完整实现如下:
/**
* 生产消息
*/
@Override
public void produce() {
getExecutor().execute(() -> {
// 消息ID,用来标识每个任务的ID
long msgId = 0L;
// 用来模拟生产任务的忙碌程度,值越小越忙
int sleep = 1000;
// 用来控制生产任务忙闲的过渡
boolean plus = false;
while (true) {
try {
msgId ++;
/** 以下6行含义:
* 如果sleep<=10, 需要从忙->闲过渡,sleep开始累加
* 如果sleep>1000, 需要从闲->忙过渡,sleep开始递减
* 这样就可以模拟生产过程的忙闲互相切换的情况
*/
sleep = plus ? sleep + 20 : sleep - 20;
if (sleep <= 10) {
plus = true;
} else if(sleep > 1000) {
plus = false;
}
// 组装任务内容
String msg = String.format("%s - msg", msgId);
// 将任务放入队列
boolean res = elasticQueue.produce(msg, 1, TimeUnit.SECONDS);
log.debug("Produce:{}, {}", msg, res);
// 阻塞线程,模拟忙闲
Thread.sleep(sleep);
} catch (Exception e) {
log.error("Produce exception", e);
break;
}
}
log.info("Exit Produce");
});
}
}
具体解释可以想看注释,这段代码埋了一个待实现的 elasticQueue.produce(msg, 1, TimeUnit.SECONDS)
方法,待后续实现队列时再讲解,接下来看下init()
的实现:
/**
* 初始化
*/
@Override
public void init() {
// 伸缩队列的配置
QueueConfig config = new QueueConfig.Builder()
.capacity(10)
.minConsumerCount(2)
.maxConsumerCount(150)
.pollTimeout(1)
.pollTimeoutUnit(TimeUnit.SECONDS)
.remainingUpperPercent(0.5f)
.remainingLowerPercent(0.3f)
.build();
// 声明一个有限的阻塞队列Redis实现
RBoundedBlockingQueue<String> queue = redissonClient.getBoundedBlockingQueue("test-queue");
// 申请队列的容量为10
queue.trySetCapacity(config.getCapacity());
// 初始化我们的伸缩队列
elasticQueue = new ElasticQueue<>(queue, new ConsumerImpl<>(), config);
}
综上,我们需要实现 ElasticQueue``类,初始化方法 ``ElasticQueue<>(RBoundedBlockingQueue queue, new ConsumerImpl<>(), QueueConfig config)
,生产方法 elasticQueue.produce(msg, 1, TimeUnit.SECONDS)
,以及 QueueConfig
配置类,这些再后续篇幅解释。
到此为止, 一个最简单的生产消费队列的redis实现已经完成,完整的服务实现代码如下:
package com.davin.service.impl;
import com.davin.framework.queue.ElasticQueue;
import com.davin.framework.queue.config.QueueConfig;
import com.davin.service.RedisQueueService;
import com.davin.vm.ConsumerImpl;
import org.redisson.api.RBoundedBlockingQueue;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.*;
/**
* @author davin.bao
* @date 2024/6/4
*/
@Service
public class RedisQueueServiceImpl implements RedisQueueService {
private final static Logger log = LoggerFactory.getLogger(RedisQueueServiceImpl.class);
@Autowired
private RedissonClient redissonClient;
private ElasticQueue<String> elasticQueue;
/**
* 初始化
*/
@Override
public void init() {
QueueConfig config = new QueueConfig.Builder()
.capacity(10)
.minConsumerCount(2)
.maxConsumerCount(150)
.pollTimeout(1)
.pollTimeoutUnit(TimeUnit.SECONDS)
.remainingUpperPercent(0.5f)
.remainingLowerPercent(0.3f)
.build();
RBoundedBlockingQueue<String> queue = redissonClient.getBoundedBlockingQueue("test-queue");
queue.trySetCapacity(config.getCapacity());
elasticQueue = new ElasticQueue<>(queue, new ConsumerImpl<>(), config);
}
private ThreadPoolExecutor getExecutor() {
return new ThreadPoolExecutor(
5, 10,
1L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
/**
* 生产消息
*/
@Override
public void produce() {
getExecutor().execute(() -> {
long msgId = 0L;
int sleep = 1000;
boolean plus = false;
while (true) {
try {
msgId ++;
sleep = plus ? sleep + 20 : sleep - 20;
if (sleep <= 10) {
plus = true;
} else if(sleep > 1000) {
plus = false;
}
String msg = String.format("%s - msg", msgId);
boolean res = elasticQueue.produce(msg, 1, TimeUnit.SECONDS);
log.debug("Produce:{}, {}", msg, res);
Thread.sleep(sleep);
} catch (Exception e) {
log.error("Produce exception", e);
break;
}
}
log.info("Exit Produce");
});
}
}
本节内容总结
本节介绍了启动类 DemoApplication
,Redis 实现的队列启动及生产模拟服务 RedisQueueService
,及服务实现RedisQueueServiceImpl
,Redisson注入配置及工程配置文件 application.yml
。自此我们实现了一个可以测试队列的骨架。
下节将介绍我们的主角可伸缩队列的实现。
继续阅读
上一篇: 自适应多线程可伸缩的生产者消费者队列服务(二)
下一篇: 自适应多线程可伸缩的生产者消费者队列服务(四)
目录: 详见 分布式可伸缩队列