实现一个简单实用的的并发同步模型

写在文章开头

日常开发后端接口时,总是会遇到一些和业务关联性不是很大却又很耗时的操作,由于功能的重要性和体量远达不到要上消息中间件的情况,这时候我们就可以实现一个简单的生产者消费者模型来实现异步消费。就以笔者这篇文章为例,通过JUC包下的阻塞队列实现了一个简单的并发同步模型。

本文整体结构如下,通过笔者的代码示例,你会对生产者消费者这种并发同步的设计模式的开发模型和使用场景有着更进一步的理解。

设计思路

简单来说生产者消费者模型就是让多线程去异步消费生产者的任务,对于web开发而言 ,我们的生产者可以是任意的HTTP 请求,这些HTTP请求会将一些耗时操作提交到队列中让消费者进行消费(这里消费者可以是一个异步的线程或者线程池,具体看读者的业务场景) 所以我们的实现思路如下:

  1. 封装一个任务,将用户的耗时操作封装到该任务中。
  2. 声明一个队列,存储Web请求中的耗时操作。
  3. 将web接口中的耗时操作提交到队列中。
  4. 创建一个线程池,异步消费队列中的任务。

实践

任务封装

这里笔者假设耗时的操作是对一个第三方接口的请求,所以笔者在封装任务时,只需在任务中声明调第三方接口的参数即可:

arduino 复制代码
/**
 * 要被执行的任务
 */
@Data
public class Task {

    /**
     * 任务id
     */
    private Long id;
    /**
     * 任务名称
     */
    private String taskName;
    /**
     * 请求参数
     */
    private JSONObject params;
    /**
     * 创建时间
     */
    private DateTime createTime;
    /**
     * 结束时间
     */
    private DateTime finishTime;
}

队列声明

为了方便管理,我们将阻塞队列以聚合的方式封装一个QueueBean 交给Spring 进行管理,注意笔者这里声明的阻塞队列的容量为2000 仅仅是示例,具体数值读者需要结合压测进行调整,参考StackOverflow的回答一般建议设置为可分配的堆内存大小除以对象平均字节数:

Make it "as large as is reasonable". For example, if you are OK with it consuming up to 1Gb of memory, then allocate its size to be 1Gb divided by the average number of bytes of the objects in the queue.

"

笔者建议阻塞队列的大小可设置为每秒处理的任务数:

typescript 复制代码
@Component
@Slf4j
public class QueueBean {

    private BlockingQueue<Task> blockingQueue = new ArrayBlockingQueue<>(2000);


    @SneakyThrows
    public void put(Task task) {
        blockingQueue.put(task);
    }

    @SneakyThrows
    public Task take() {
        return blockingQueue.take();
    }


}

实现生产者

我们的HTTP请求就是一个生产者,所以在业务执行过程中,笔者将耗时的三方请求封装为Task提交到阻塞队列中交由消费者异步消费:

typescript 复制代码
@Autowired
    private QueueBean queueBean;


@PostMapping("/submitTask")
    public String submitTask() {
        Task task = new Task();
        long id = snowflake.nextId();
        task.setId(id);
        task.setTaskName("任务-" + id);
        task.setParams(new JSONObject().putOnce("userName", RandomUtil.randomString(5)));
        task.setCreateTime(new DateTime());
        task.setFinishTime(new DateTime());

        log.info("提交任务:{}", JSONUtil.toJsonStr(task));
        queueBean.put(task);
        return "success";
    }

实现消费者

因为消费者的执行逻辑需要提交到线程池中让池中的线程进行处理,所以我们这里封装了一个消费的Runnable ,因为这个Runnable 不受Spring 容器管理,所以获取Spring 容器中的队列可以采用hutool 封装的SpringUtil 上下文,当然如果了解Spring 扩展点的读者也可以采用ApplicationContext获取阻塞队列,而笔者对于任务消费逻辑比较简单,仅仅打印一下任务信息:

java 复制代码
@Slf4j
public class ConsumerTask implements Runnable {


    @Override
    public void run() {
        QueueBean queueBean = SpringUtil.getBean(QueueBean.class);
        while (true) {
            //从阻塞队列中获取任务
            Task task = queueBean.take();
            log.info("消费者消费任务,任务详情:{}", JSONUtil.toJsonStr(task));

        }
    }
}

基于线程管理消费者

完成消费者的封装之后,我们采用线程池的方式创建线程来执行消费者的逻辑,可以看到笔者采用Spring 后置的扩展点,确保在线程池的Bean完成加载之后对线程池进行初始化,并提交5个消费者。

java 复制代码
 private static ThreadPoolExecutor threadPoolExecutor = ExecutorBuilder.create()
            .setCorePoolSize(Runtime.getRuntime().availableProcessors())
            .setMaxPoolSize(Runtime.getRuntime().availableProcessors() << 2)
            .setThreadFactory(new NamedThreadFactory("consumerTask-", false))
            .build();


    @PostConstruct
    private void init() {
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(new ConsumerTask());
        }
    }

测试

我们将应用启动后可以看到下面这段输出,不难看出每当我们的HTTP请求提交一个任务到队列中后,总有一个线程池中的线程出来消费者任务,两者高效并发同步的同时又能保证线程安全:

css 复制代码
2024-01-02 15:20:31.328  INFO 12084 --- [nio-8080-exec-8] c.s.q.controller.BasicController         : 提交任务:{"id":1742083432578711552,"taskName":"任务-1742083432578711552","params":{"userName":"9b53q"},"createTime":1704180031328,"finishTime":1704180031328}
2024-01-02 15:20:31.329  INFO 12084 --- [ consumerTask-1] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083432578711552,"taskName":"任务-1742083432578711552","params":{"userName":"9b53q"},"createTime":1704180031328,"finishTime":1704180031328}
2024-01-02 15:20:31.934  INFO 12084 --- [io-8080-exec-10] c.s.q.controller.BasicController         : 提交任务:{"id":1742083435116265472,"taskName":"任务-1742083435116265472","params":{"userName":"xwoth"},"createTime":1704180031933,"finishTime":1704180031933}
2024-01-02 15:20:31.934  INFO 12084 --- [ consumerTask-3] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083435116265472,"taskName":"任务-1742083435116265472","params":{"userName":"xwoth"},"createTime":1704180031933,"finishTime":1704180031933}
2024-01-02 15:20:32.623  INFO 12084 --- [nio-8080-exec-1] c.s.q.controller.BasicController         : 提交任务:{"id":1742083438010335232,"taskName":"任务-1742083438010335232","params":{"userName":"2udas"},"createTime":1704180032623,"finishTime":1704180032623}
2024-01-02 15:20:32.624  INFO 12084 --- [ consumerTask-2] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083438010335232,"taskName":"任务-1742083438010335232","params":{"userName":"2udas"},"createTime":1704180032623,"finishTime":1704180032623}

小结

笔者在这里仅仅是实现了一个比较简单的并发同步模型,该模型只能算是一个比较实用的示例版本,后续笔者会考虑补充异步消费失败等兜底策略,感兴趣的读者可以点点关注。

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考资料

SpringBoot ------ 基于BlockingQueue实现的生产者消费者用例:zealon.cn/article/23

How to estimate or calculate the size of the ArrayBlockingQueue:stackoverflow.com/questions/7...

本文使用 markdown.com.cn 排版

相关推荐
Apifox.1 小时前
测试用例越堆越多?用 Apifox 测试套件让自动化回归更易维护
运维·前端·后端·测试工具·单元测试·自动化·测试用例
sunnyday04262 小时前
Nginx与Spring Cloud Gateway QPS统计全攻略
java·spring boot·后端·nginx
康王有点困2 小时前
Link入门
后端·flink
海南java第二人2 小时前
Spring Boot全局异常处理终极指南:打造优雅的API错误响应体系
java·spring boot·后端
小楼v3 小时前
消息队列的核心概念与应用(RabbitMQ快速入门)
java·后端·消息队列·rabbitmq·死信队列·交换机·安装步骤
小北方城市网3 小时前
接口性能优化实战:从秒级到毫秒级
java·spring boot·redis·后端·python·性能优化
鸡蛋豆腐仙子3 小时前
Spring的AOP失效场景
java·后端·spring
小北方城市网3 小时前
SpringBoot 全局异常处理最佳实践:从混乱到规范
java·spring boot·后端·spring·rabbitmq·mybatis·java-rabbitmq
qq_256247053 小时前
如何系统性打造高浏览量视频号内容
后端
码界奇点3 小时前
基于Spring Boot与Vue.js的连锁餐饮点餐系统设计与实现
vue.js·spring boot·后端·毕业设计·源代码管理