自适应多线程可伸缩的生产者消费者队列服务(三)

章节介绍

我个人比较喜欢冲程序运行的顺序来写代码。所以本节主要介绍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。自此我们实现了一个可以测试队列的骨架。
下节将介绍我们的主角可伸缩队列的实现。

继续阅读

上一篇: 自适应多线程可伸缩的生产者消费者队列服务(二)
下一篇: 自适应多线程可伸缩的生产者消费者队列服务(四)
目录: 详见 分布式可伸缩队列

猜你喜欢

转载自blog.csdn.net/s011803/article/details/139576464