Java 高并发编程之最实用的任务执行架构设计建议收藏

目录
  • 前言
    • 1、业务架构
    • 2、技术架构
    • 3、物理架构
  • 高并发任务执行架构
    • 需求场景
    • 业务架构设计
    • 技术架构设计
      • 初始设计
      • 演化阶段一
      • 演化阶段二
      • 演化阶段三
  • 代码设计
  • 总结

前言

随着互联网与软件的发展,除了程序员,架构师也是越来越火的职业。他们伴随着项目的整个生命过程,他们更像是传统工业的设计师,将项目当做生命一般细心雕琢。

目前对于项目架构而言,基本都会需要设计的几个架构。

1、业务架构

项目或者产品的市场定位、需求范围、作用场景都是需要在项目启动初期进行系统性分析的。在设计业务架构中,架构师还需要明确角色。我看过很多关于架构的文章,谈到角色的很少。

什么是角色?

例如:商场作为一个整体系统,角色就有消费者、店员、收费员、保安等等。各个角色完成好自己角色所需要承担的任务,整体系统就能完美的运行。

对应到软件系统中,根据产品的定位和需求,也会有着对照的角色,比如:用户、数据审核者、产品制作者、运维人员等。在项目启动初期,架构师需要对项目中的每个角色做好职责定位,我相信在这点上,大部分开发同学在工作中,或多或少都有过职责不明确带来的困扰。

2、技术架构

在软件项目研发过程中,我们会用到许多外部组件。在使用组件中,架构师必须结合业务需求合理的选择各个组件。项目是个生命,她会成长,架构师需要明白如果一开始就选择重量级组件会让还是个孩童的项目不受重负,架构师也需要明白如果技术架构的设计不具备拓展性,那么这个孩子无法茁壮成长。所以技术架构尤为重要。

3、物理架构

物理架构又叫做部署架构,项目产品如果要在生产环境稳定运行,一个稳定又高效的物理架构是必不可少的。而且往往物理架构和技术架构是相辅相成的,性能监控、异常告警、业务日志等等设计,都是为了让项目做更好的自己。

高并发任务执行架构

在我十年的工作中,业务相关、中间件、大数据都有做过。本文主要分享一下高并发任务执行框架设计,会由浅入深的讲述一下设计演化过程。如果你不只是想做业务后端开发,那么本文会给你一个全新的视野。

需求场景

我们列一下该项目的需求场景,看看工作中是否遇到过。

1、有个复杂的数据需要制作,而且制作的时间很长,无法让请求方持续等待。所以请求方只能给你个回调地址,需要你完成这个制作后将产物通知他。

2、复杂的制作过程需要消耗资源,而且资源有限,无法无限量提供。如果你有接触过AI,就会比较了解资源有限的感受。除了ASR、TTS这类识别类型的AI功能能做到近实时的反馈,大部分的算法在运行的时候都会消耗整张显卡,而且耗时很长。

初看场景,很多后端可能会第一时间想到elastic-job(一个分布式任务调度框架)。即便你熟悉使用elastic-job,一开始就选择重框架是不是有种杀鸡用牛刀的感觉。不着急,我们一步步分析,一步步设计。

业务架构设计

高度抽象一下我们的业务,对产品设计者而言,貌似是个简单的不能再简单的东西。等到了技术架构,我们深入分析其中演化的功能点,就会发现这是个庞大的机器。我们先给他起个简单的名字:Task Execution Engine(缩写:TEE)

技术架构设计

下面我们开始进行核心模块的技术架构设计,按照我们的初始需求开始我们的设计旅程。

初始设计

设计说明:

1、业务后端发出q1请求,我们首先需要对该请求的参数做矫正,为了可用性考虑。

2、参数校验过后,给到执行引擎模块。执行引擎主要的职责有从资源表获取资源数据、将任务参数与资源参数封装到任务对象内、将任务提交线程池。有一点要说明执行引擎最好使用队列模式,任务先进队列,可以通过while循环方式或者定时线程池都可以,后面会推荐更好的。

3、任务执行的状态与结果需要同步到数据库中,建议使用mysql。

小结:

按照初始需求,该设计相对比较简单,完全够用了。但是按照产品的迭代,业务方的需求不会仅限于此。继续演化。

演化阶段一

随着业务的上线,业务端会马上迎来新的问题。

1、由于提交的任务太多了,排在后面的任务迟迟无法等到自己获取到资源执行任务。当然我们可以完全靠增加资源来解决,但是资源的数量在产品前期是不可知的。所以需要有一些策略,比如让用户可以取消自己任务,而不是一直等待。

2、任务的种类开始增加,业务端不满足于单一制作,开始要求多样化。

3、任务的执行过程开始需要用到其他资源,不再是一个资源对一个任务的模式了。

4、任务的整体执行情况不可知,需要一定的量化分析,至少让业务组知道每天的任务成功率。

按照需求进行第二版的设计,在尽量不改变原来整体设计的情况下,补充功能。

设计说明:

1、为了解决排队问题,增加了双队列算法来解决。用图解的方式解释一下双队列。

逻辑简单说明一下,任务优先提交至执行队列,引擎的定时读取队列的顺序优先为等待队列。如果等待队列中的任务可以获取所需资源,则立即启动线程执行,否则原封不动回到等待队列。引擎其次读取执行队列,如果无法获取资源则进入等待队列,如获取资源,则立即启动线程执行。

那么取消队列,则只需要将队列中的任务踢出队列即可。在送回队里的过程中,一定要保证队列的有序性。

2、创建了任务池,增加了任务封装层,在任务池中挑选需要执行的任务类。

3、增加了策略机模块,添加资源调度策略,由资源调度策略堆任务所需资源合理分配。可以由业务方提供分配方案,尽可能保证任务的公平性。

4、数据库增加统计表,可以考虑使用定时任务,将任务表的数据统计存入统计表。

小结:

现在看上去已经比较完善了,合理了任务调度、增加了任务种类、合理的资源调度,好像还不错。但是产品总会有新要求的,那么继续演化。

演化阶段二

渐渐的,你设计的引擎还不错。那么新的挑战来了。

1、更多的业务方找到你,希望也使用你的项目进行任务制作,但是他们并不想共享资源,而是希望有自己的独立资源,和独立的队列。但并不是所有的资源都需要独立,一些可以支持高并发的资源,是可以共享的。简而言之,更多的业务方,由业务方为维度的独立队列,独立和共享的资源分配。

2、业务方找到你,说如果把任务1的结果给到任务2,其实就能拿到我要的结果。问题来了,原子任务要具备可以编排成复杂任务的能力。

3、任务的状态过程无法监控。OK,任务状态机。

4、既然大家都需要对接你的项目,能不能提供标准的sdk,我只需要引入就可以完美的对接你的系统。

5、相同的任务参数,是不是制作出来的结果一致呢?那么是否需要增加结果缓存,降低对资源的消耗呢?

6、完正的生产项目必然需要将日志、告警等关键信息传递出来,一旦发生问题可以马上定位到问题的起因。

这些问题对于新人来说还是很有挑战的,需要对系统深层的含义有充足的理解。没事,我来好好来说下设计所需要掌握的知识点。

设计说明:

1、需要在资源表中区别资源类型,共享资源组所有业务组都可以使用,独立资源则资源具备业务标识。在执行引擎的队列管理中,也需要区分业务组,避免共用排队。这里给一个建议,共享的资源一定要是可以支持并发或者可以部署多个实例的,避免所有的业务组产品制作瘫痪。

2、增加了高级任务概念,高级任务可以将任意的原子任务进行组合编排,形成全新的任务。需要定义专属于TEE的语法规则。对语法规则引擎的开发,有一些建议。你可能会选择规则引擎,建议其实可以自己开发,毕竟语法不会太过复杂,没必要引入三方的引擎。

3、增加任务状态机,执行引擎在提交线程的同时,也想任务状态机提交任务线程信息。任务的进度状态可以同步给任务状态机中,同时一旦任务执行过长的时间,除了任务自己的超时机制外,也可由状态机的看门狗程序将卡死线程释放、资源回收。

4、研发属于TEE的SDK,作为内部系统不建议SDK增加鉴权模块。毕竟你对接的往往都是业务后端,鉴权不通过的话根本渗透不到TEE层面。给开发SDK一些建议,尽量引用较少的包,避免业务端引入带来的包冲突。SDK也需要添加一些回调Consumer或者Function,尽可能让业务端对接起来代码简单。

5、增加了缓存策略,可以设想一下,大部分情况下,相同的参数制作出来的结果也必然相同。使用redis,将任务参数与任务结果进行缓存,主键可以采用任务参数的MD5值。任务在提交给任务执行引擎前,检查缓存中是否已经存在结果。缓存的过期时间按照具体情况而定。

6、增加日志系统和监控系统的对接,状态机与任务执行中的信息接入到日志系统中。对于日志系统的建议是,最好采用成熟的ELK架构。可以考虑两种方式

a、将日志异步推送到消息队列(例如:kafka),使用flink将kafka存入es。

b、使用logstash将日志内容清洗处理,推送到es。

两种方式都可以,但是一定要异步推送日志,避免任务阻塞。

告警系统的接入可以使用Prometheus,将TEE的指标信息开放出来,特别是告警信息。在Prometheus的告警监控规则中,可以将告警信息按照某些策略发送邮件或者短信,通知运维人员。

小结:

做到这里,我们再看看我们的技术架构图,是不是感到很满足。但是这真的够了吗?

演化阶段三

随着业务愈发繁重,一个新的问题出现。

1、TEE在本机运行很顺利,但是每个任务都是需要消耗线程的,单台模式线程必然不是无限的,总有吃满的时候。问题来了,TEE得支持分布式部署结构。但是有资源管理的存在,你无法通过加实例的方式来实现,因为资源调度必然混乱。

2、假设TEE挂掉,则等于业务组此刻提交的任务均失败,容灾机制需要建立。

到了这一步,很多小伙伴可能觉着一阵头大,分布式不是大数据的东西吗?不是的,不是大数据就不能分布式部署吗?就不能有主从节点吗?就不能有注册中心吗?要跨过内心的固有思想,我们往下看。

设计说明:

1、需要先将TEE项目做一下代码分解,将管理调度模块与任务执行模块拆解开了。消耗性能和线程较高的是任务执行模块,定义为TEE执行节点。消耗性能和线程较低,却需要参数校验、任务封装、资源调度、队列管理的是管理调度模块,定义为TEE管理节点。

2、TEE执行节点可以多实例,形成节点池。管理节点可以考虑做成共享任务队列的管理池。这里有个难点,如何共享任务队列。建议使用zookeeper或者缓存方式,但是不管哪种方式都要注意使用集群,避免单点故障导致队列数据丢失。

3、关于注册中心,可以使用开源组件,nacos、zookeeper都可以。在该架构设计中,其实注册中心并没有太多功能,如果你对自己有信心,可以尝试自己写一个注册中心,核心功能就是服务注册与心跳检测。可以用netty架构做一做,提高一下自己的代码能力。

4、在管理池的调用方面,增加网关代理,可以使用nginx、konga等。主要功能是业务端调用的时候,随机打到管理节点上,就算一个管理节点挂了,也不会影响使用,保证了生产线的稳定。

小结:

分布式架构,主从节点模式也好、哨兵模式也好、选举模式也好,按照自己的业务需求选择最适合自己的。不是大数据才有分布式,它是一种设计思想,要知道开发大数据组件的大佬们,也是一行行java写出来的。大佬们可以,为什么你不可以呢?

代码设计

在着手开发该系统的时候,我给大家一些代码开发的建议:

1、定时任务的实现,从最简单的while死循环加sleep,到定时线程池,或者springboot的@Scheduled注解,都可以实现。我在这里推荐一下时间轮算法TimeWheel,有兴趣的可以去了解一下。

2、异步消息处理,如果你只是想在项目内部使用消息总线,推荐使用guava包内的EventBus。按照消息的数量级,考虑使用RabbitMQ或者Kafka作为消息中间件。

3、任务执行,推荐使用CompletableFuture进行异步非阻塞任务编程。

4、在资源的使用中,可能存在多种协议类型http、ws、tcp等。所以代码设计中,尽可能提供完整全面的协议工具类。

总结

最近和一个同事闲聊的时候,他和我说了说最近面试高级研发的情况。总结一下,现在想招一个做过高并发场景的太难了,大部分人选只做业务相关。这让我想到了十年前,我刚工作的时候。那时候没有那么多框架,大部分功能需要看很多资料研究底层的原理与算法。随着软件行业的日益成熟,从以前拿着谷歌翻译看国外的组建说明,到从阿里云直接对接功能,软件研发的成本和时间也在大大缩短。渐渐失去了研究分析的动力,框架什么都有为什么要抓破脑袋自己写。

怎么成长?怎么进步?

上面给出的架构图,看上去都挺好理解。但真要自己去代码实现,中间各个环节出现的问题真的好解决吗?冰冻三尺,非一日之寒。没有日积月累的学习,是很难成功的。不要惧怕那些你看上去遥远的东西,获取你的几个晚上的学习,它就成了你最趁手的武器。对学习而言,难的永远不是过程,而是踏出第一步。

高并发任务执行架构中,有一个模块看上去很不起眼,但是在你研发的过程就会发现他会给你制造大麻烦-资源调度。以后有时间我会单独做一篇关于资源调度的架构设计,与大家说道说道里面的坑。

最后说一句:为什么不ban猛犸?

到此这篇关于Java 高并发编程之最实用的任务执行架构设计建议收藏的文章就介绍到这了,更多相关Java 高并发编程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java高并发之线程组详解

    目录 线程组 创建线程关联线程组 为线程组指定父线程组 根线程组 批量停止线程 总结 线程组 我们可以把线程归属到某个线程组中,线程组可以包含多个线程以及线程组,线程和线程组组成了父子关系,是个树形结构,如下图: 使用线程组可以方便管理线程,线程组提供了一些方法方便方便我们管理线程. 创建线程关联线程组 创建线程的时候,可以给线程指定一个线程组,代码如下: package com.itsoku.chat02; import java.util.concurrent.TimeUnit; /** *

  • java高并发之线程的基本操作详解

    目录 新建线程 终止线程 线程中断 等待(wait)和通知(notify) 挂起(suspend)和继续执行(resume)线程 等待线程结束(join)和谦让(yeild) 总结 新建线程 新建线程很简单.只需要使用new关键字创建一个线程对象,然后调用它的start()启动线程即可. Thread thread1 = new Thread1(); t1.start(); 那么线程start()之后,会干什么呢?线程有个run()方法,start()会创建一个新的线程并让这个线程执行run()

  • Java 浅谈 高并发 处理方案详解

    目录 高性能开发十大必须掌握的核心技术 I/O优化:零拷贝技术 I/O优化:多路复用技术 线程池技术 无锁编程技术 进程间通信技术 Scale-out(横向拓展) 缓存 异步 高性能.高可用.高拓展 解决方案 高性能的实践方案 高可用的实践方案 高扩展的实践方案 总结 高性能开发十大必须掌握的核心技术 我们循序渐进,从内存.磁盘I/O.网络I/O.CPU.缓存.架构.算法等多层次递进,串联起高性能开发十大必须掌握的核心技术. - I/O优化:零拷贝技术 - I/O优化:多路复用技术 - 线程池技

  • java实用型-高并发下RestTemplate的正确使用说明

    目录 前言 一.RestTemplate是什么? 二.如何使用 1.创建一个bean 2.使用步骤 三.高并发下的RestTemplate使用 1.设置预热功能 2.合理设置maxtotal数量 总结 前言 如果java项目里有调用第三方的http接口,我们可以使用RestTemplate去远程访问.也支持配置连接超时和响应超时,还可以配置各种长连接策略,也可以支持长连接预热,在高并发下,合理的配置使用能够有效提高第三方接口响应时间. 一.RestTemplate是什么? RestTemplat

  • java高并发之理解进程和线程

    目录 进程 线程 进程与线程的一个简单解释 总结 进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.程序是指令.数据及其组织形式的描述,进程是程序的实体. 进程具有的特征: 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的 并发性:任何进程都可以同其他进行一起并发执行 独立性:进程是系统进行资源分配和调度的一个独立单位 结构性:进程由程序,数据和进程控制块三部分组成 我们经常使用wi

  • java高并发的并发级别详解

    目录 阻塞 无饥饿(Starvation-Free) 无障碍(Obstruction-Free) 无锁(Lock-Free) 等待 总结 阻塞.无饥饿.无障碍.无锁.无等待几种. 阻塞 一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行.当我们使用synchronized关键字或者重入锁时,我们得到的就是阻塞的线程. synchronize关键字和重入锁都试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止. 无饥饿(Starvation-

  • java高并发的线程中断的几种方式详解

    目录 通过一个变量控制线程中断 通过线程自带的中断标志控制 线程阻塞状态中如何中断? 总结 通过一个变量控制线程中断 代码: package com.itsoku.chat05; import java.util.concurrent.TimeUnit; /** * 微信公众号:路人甲Java,专注于java技术分享(带你玩转 爬虫.分布式事务.异步消息服务.任务调度.分库分表.大数据等),喜欢请关注! */ public class Demo1 { public volatile static

  • java高并发的用户线程和守护线程详解

    目录 程序只有守护线程时,系统会自动退出 设置守护线程,需要在start()方法之前进行 线程daemon的默认值 总结 守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程.JIT线程都是守护线程.与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作.如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了.所以当系统只剩下守护进程的时候,java虚拟机会自动退出. java线程分为用户线程和守护线程,线程的

  • Java 高并发编程之最实用的任务执行架构设计建议收藏

    目录 前言 1.业务架构 2.技术架构 3.物理架构 高并发任务执行架构 需求场景 业务架构设计 技术架构设计 初始设计 演化阶段一 演化阶段二 演化阶段三 代码设计 总结 前言 随着互联网与软件的发展,除了程序员,架构师也是越来越火的职业.他们伴随着项目的整个生命过程,他们更像是传统工业的设计师,将项目当做生命一般细心雕琢. 目前对于项目架构而言,基本都会需要设计的几个架构. 1.业务架构 项目或者产品的市场定位.需求范围.作用场景都是需要在项目启动初期进行系统性分析的.在设计业务架构中,架构

  • Java面试必备之JMM高并发编程详解

    目录 一.什么是JMM 二.JMM定义了什么 原子性 可见性 有序性 三.八种内存交互操作 四.volatile关键字 可见性 volatile一定能保证线程安全吗 禁止指令重排序 volatile禁止指令重排序的原理 五.总结 一.什么是JMM JMM就是Java内存模型(java memory model).因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题.所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差

  • 详解Java高并发编程之AtomicReference

    目录 一.AtomicReference 基本使用 1.1.使用 synchronized 保证线程安全性 二.了解 AtomicReference 2.1.使用 AtomicReference 保证线程安全性 2.2.AtomicReference 源码解析 2.2.1.get and set 2.2.2.lazySet 方法 2.2.3.getAndSet 方法 2.2.4.compareAndSet 方法 2.2.5.weakCompareAndSet 方法 一.AtomicReferen

  • Java 多线程并发编程_动力节点Java学院整理

    一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关的API,进程主要包括表示进程的jav

  • java 高并发中volatile的实现原理

    java 高并发中volatile的实现原理 摘要: 在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性".可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值.它在某些情况下比synchronized的开销更小 1. 定义: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量.

  • Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等方案后,问题得到解决. 加锁方案见下文. 二.乐观锁 & 悲观锁 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • java高并发情况下高效的随机数生成器

    前言 在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的. 下面是Random.next()生成一个随机整数的实现: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend)

  • Java 高并发的三种实现案例详解

    提到锁,大家肯定想到的是sychronized关键字.是用它可以解决一切并发问题,但是,对于系统吞吐量要求更高的话,我们这提供几个小技巧.帮助大家减小锁颗粒度,提高并发能力. 初级技巧-乐观锁 乐观锁使用的场景是,读不会冲突,写会冲突.同时读的频率远大于写.  悲观锁的实现: 悲观的认为所有代码执行都会有并发问题,所以将所有代码块都用sychronized锁住 乐观锁的实现: 乐观的认为在读的时候不会产生冲突为题,在写时添加锁.所以解决的应用场景是读远大于写时的场景. 中级技巧-String.i

  • java高并发InterruptedException异常引发思考

    目录 前言 程序案例 问题分析 问题解决 总结 前言 InterruptedException异常可能没你想的那么简单! 当我们在调用Java对象的wait()方法或者线程的sleep()方法时,需要捕获并处理InterruptedException异常.如果我们对InterruptedException异常处理不当,则会发生我们意想不到的后果! 程序案例 例如,下面的程序代码,InterruptedTask类实现了Runnable接口,在run()方法中,获取当前线程的句柄,并在while(t

  • Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解这个关键字.只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁.(并不是) volatile 的特性: 具备可见性 保证不同线程对被 volatile 修饰的变量的可见性. 有一被 volatile 修饰的变量 i,在一个线程中修改了此变量 i,对于其他线程来说 i 的修改是立即可见的. 如: vola

随机推荐