微创新与稳定性的权衡
之前做过一个项目,业务最高峰CPU使用率也才50%,是一个IO密集型的应用。里面涉及一些业务编排,所以为了提高CPU使用率,我有两个方案:一个是简单的梳理将任务可并行的采用并行流、额外线程池等方式做并行;另外一个方案是采用基于DAG有向无环图的任务调度。采用并行的方式,改造代码在几十行;采用DAG方案改造代码在几百行,自己觉得也不复杂,但跟别人讲时,感觉理解成本还是有点高,加上并行方案已经可以将最高峰CPU使用率提高到80%多,最终权衡必要性不高,还引入了额外的复杂性,增加维护成本,所以我自己否定了这个方案。但这并不妨碍我对DAG有了一定的理解:
DAG任务调度
DAG任务调度系统的核心概念是将任务表示为一个有向无环图(DAG),其中每个节点表示一个任务,每条边表示一个任务之间的依赖关系。DAG任务调度系统的主要优势在于它可以有效地管理和执行依赖关系复杂的多任务系统,并且可以在大规模分布式环境中运行。所以我们的简单业务编排确实并不需要这样复杂的设计。

设计DAG,主要要设计三块。一块是节点,代表的是任务。任务对象主要关注任务的执行;
//定义一个Executor接口
//代表一个可执行的任务,execute代表任务的执行
public interface Executor {boolean execute();
}
/*
* 定义一个Executor接口的实现Task
*id:任务id
*name:任务名
*state:任务状态,简化为0:未执行,1:已执行
*hasExecuted返回任务是否已执行
*/
public class Task implements Executor{private Long id;private String name;private int state;public Task(Long id, String name, int state) {this.id = id;this.name = name;this.state = state;}public boolean execute() {System.out.println("Task id: [" + id + "], " + "task name: [" + name +"] is running");state = 1;return true;}public boolean hasExecuted() {return state == 1;}
} 第二块是图,里面要做两件事,一件是管理任务,一件事管理任务之间的依赖,反应到图上就是要有节点和节点之间的连线;
//任务图,这个类使用了邻接表来表示有向无环图。tasks是顶点集合,也就是任务集合。
//map是任务依赖关系集合。key是一个任务,value是它的前置任务集合。
//一个任务执行的前提是它在map中没有以它作为key的entry,或者是它的前置任务集合中的任务都是已执行的状态。public class Digraph {private Set<Task> tasks;private Map<Task, Set<Task>> map;public Digraph() {this.tasks = new HashSet<Task>();this.map = new HashMap<Task, Set<Task>>();}public void addEdge(Task task, Task prev) {if (!tasks.contains(task) || !tasks.contains(prev)) {throw new IllegalArgumentException();}Set<Task> prevs = map.get(task);if (prevs == null) {prevs = new HashSet<Task>();map.put(task, prevs);}if (prevs.contains(prev)) {throw new IllegalArgumentException();}prevs.add(prev);}public void addTask(Task task) {if (tasks.contains(task)) {throw new IllegalArgumentException();}tasks.add(task);}public void remove(Task task) {if (!tasks.contains(task)) {return;}if (map.containsKey(task)) {map.remove(task);}for (Set<Task> set : map.values()) {if (set.contains(task)) {set.remove(task);}}}public Set<Task> getTasks() {return tasks;}public void setTasks(Set<Task> tasks) {this.tasks = tasks;}public Map<Task, Set<Task>> getMap() {return map;}public void setMap(Map<Task, Set<Task>> map) {this.map = map;}
} 第三块是调度,就是获取任务列表,并按照它们之间的依赖关系来执行。
//调度器,就是遍历任务集合,找出待执行的任务集合,
//放到一个List中,再串行执行(若考虑性能,可优化为并行执行)。
//若List为空,说明所有任务都已执行,则这一次任务调度结束。
public class Scheduler {public void schedule(Digraph digraph) {while (true) {List<Task> todo = new ArrayList<Task>();for (Task task : digraph.getTasks()) {if (!task.hasExecuted()) {Set<Task> prevs = digraph.getMap().get(task);if (prevs != null && !prevs.isEmpty()) {boolean toAdd = true;for (Task task1 : prevs) {if (!task1.hasExecuted()) {toAdd = false;break;}}if (toAdd) {todo.add(task);}} else {todo.add(task);}}}if (!todo.isEmpty()) {for (Task task : todo) {if (!task.execute()) {throw new RuntimeException();}}} else {break;}}}public static void main(String[] args) {Digraph digraph = new Digraph();Task task1 = new Task(1L, "task1", 0);Task task2 = new Task(2L, "task2", 0);Task task3 = new Task(3L, "task3", 0);Task task4 = new Task(4L, "task4", 0);Task task5 = new Task(5L, "task5", 0);Task task6 = new Task(6L, "task6", 0);digraph.addTask(task1);digraph.addTask(task2);digraph.addTask(task3);digraph.addTask(task4);digraph.addTask(task5);digraph.addTask(task6);digraph.addEdge(task1, task2);digraph.addEdge(task1, task5);digraph.addEdge(task6, task2);digraph.addEdge(task2, task3);digraph.addEdge(task2, task4);Scheduler scheduler = new Scheduler();scheduler.schedule(digraph);}
} 是不是也不是很复杂,但是添加删除任务和添加删除依赖需要页面可视化管理,添加多了就容易乱。特别是作为一个平台:用户没有问题,所有用户的误操作问题都可以通过减少操作的复杂性来规避。如果将来真有必要使用DAG任务调度,界面设计至少要将节点和依赖以图形化的方式展示出来,让用户一目了然。
工作流引擎
我们的项目涉及的业务编排,有的同事叫这个是工作流。我就仔细的思考了一下,这个到底是不是工作流。我的理解,就是一个责任链搞定的事情。而工作流是复杂版本的状态机。这个工作流的“流”字更多不是流程,而是流转。如果没有复杂的状态流转就不应该当成工作流来看,增加问题的复杂性。说我们的项目是工作流从道理上讲也不是不对,但就好像说:橘子是一个对象。 对,但没有什么指导意义。
现在基于BPMN2.0协议的工作流引擎很受推崇。Activiti,Flowable都是它的实现。BPMN2.0协议中元素的主要分类为,事件-任务-连线-网关。
一个流程必须包含一个事件(如:开始事件)和至少一个结束(事件)。其中网关的作用是流程流转逻辑的控制。任务则分很多类型,他们各司其职,所有节点均由连线联系起来。
网关分为三类:互斥网关(Exclusive Gateway),又称排他网关,他有且仅有一个有效出口。并行网关(Parallel Gateway),他的所有出口都会被执行。包容性网关(Inclusive Gateway),只要满足条件的出口都会执行。是不是很像DAG有向无环图的一个节点到其他节点的路径?排他网关就是只有一条路径到下一个节点;并行网关就是到下一排节点都有路径;包容性网关就是到下一排节点部分有路径。
这就对了,工作流必须是DAG的。它和任务调度不同在于工作流没有强调调度。但工作流终究要被执行的,实时被执行就是实时被调度;在大数据工作流里也经常见到被周期性调度的情况。
有限状态机
刚才提到工作流是复杂版本的状态机,有没有简单的状态机呢?很多。比如咱们经常见到的用枚举来实现的。定义一个枚举,里面有成功和失败两个状态,他们之间的转换也是状态机。在金融支付领域,支付状态有 支付中,支付成功,支付失败,冲正中,冲正完成…… 状态流转就会比较复杂。可以用状态模式来管理。
状态模式
状态模式是我之前非常喜欢用的来避免大量if else的方法。
根据 GoF 的定义,状态模式的三个核心角色分别是:
环境(Context):它定义了客户端所感兴趣的接口,并维护一个当前状态,在具体状态类中实现该接口的各个具体操作。
抽象状态(State):它定义了一个接口,用于封装环境对象中不同状态对应的行为。
具体状态(Concrete State):它实现了抽象状态接口,封装了不同状态下对环境对象的响应行为。
下面是一个简单实现:
// 定义抽象状态接口
interface State {void handle();
}// 定义具体状态类
class ConcreteState1 implements State {@Overridepublic void handle() {System.out.println("当前状态为 State1.");}
}class ConcreteState2 implements State {@Overridepublic void handle() {System.out.println("当前状态为 State2.");}
}// 定义环境类
class Context {private State state;public void setState(State state) {this.state = state;}public void request() {state.handle();}
}public class StatePatternDemo {public static void main(String[] args) {// 创建状态对象State state1 = new ConcreteState1();State state2 = new ConcreteState2();// 创建环境对象Context context = new Context();context.setState(state1);context.request();context.setState(state2);context.request();}
} 总结
在工作中,有两顶思考帽:一顶是项目可持续性的帽子,要对项目负责,要使用合适的技术;一顶是让项目与时俱进的帽子,过时老套的技术降低了项目的吸引力,可能面临吸引不到更优秀的人才。其实项目中可以不使用某技术本身,却可以使用其思想,比如DAG任务调度的思想梳理清楚依赖,让能执行的尽早执行,提高运行效率。并行化也一定程度的增加了并发度,达到了效果。再举个例子,咱们平时提到的分布式事务,分布式事务的框架工作中很少用,但是分布式事务的思想却随处可见。比如支付时,如果失败超时,则返回失败并发起冲正,确保支付款退回给消费者,这就是一种补偿性事务的思想,就是这么简单。思考有了,工具是次要的。
声明:本文中使用的代码均为网上拷贝,不是本文重点,只做解释说明用。
相关文章:
微创新与稳定性的权衡
之前做过一个项目,业务最高峰CPU使用率也才50%,是一个IO密集型的应用。里面涉及一些业务编排,所以为了提高CPU使用率,我有两个方案:一个是简单的梳理将任务可并行的采用并行流、额外线程池等方式做并行;另外…...
对回调函数的各种讲解说明
有没有跟我师弟一样的童靴~,学习和使用ROS节点时,对其中的callback函数一直摸不着头脑的,以下这么多回调函数的讲解,挨个看,你总会懂的O.o 回调函数怎么调用,如何定义回调函数: 回调函数怎么调用,如何定义…...
Java多线程:创建多线程的三种方式
在Java中,有三种方式创建多线程,继承类Thread,继承接口Runnable,继承接口Callable。其中Thread和Runnable需要重写方法run,方法run没有返回值;Callable需要重写方法call,方法call可以返回值。 …...
Unity中打印信息的两种方式
不继承MonoBehaviour的普通C#类中打印信息: 使用Debug类的方法: Unity提供了Debug类,其中包含了一些用于打印信息的静态方法。以下是常用的几种方法: Debug.Log(message):打印普通信息。Debug.LogWarning(message)&a…...
给定n个字符串s[1...n], 求有多少个数对(i, j), 满足i < j 且 s[i] + s[j] == s[j] + s[i]?
题目 思路: 对于字符串a,b, (a.size() < b.size()), 考虑对字符串b满足什么条件: 由1、3可知a是b的前后缀,由2知b有一个周期是3,即a.size(),所以b是用多个a拼接而成的,有因为a是b的前后缀&…...
Linux磁盘空间与文件大小查看命令详解
1. 查看磁盘空间大小 在Linux系统中,有多个命令可以用来查看磁盘空间的使用情况。最常用的命令是df(disk free)。 df -hdf命令的 -h 选项以人类可读的方式显示磁盘空间,该命令将显示文件系统的使用情况、剩余空间等信息。 2. 查看…...
网络通信过程的一些基础问题
客户端A在和服务器进行TCP/IP通信时,发送和接收数据使用的是同一个端口吗? 这个问题可以这样来思考:在客户端A与服务器B建立连接时,A需要指定一个端口a向服务器发送数据。当服务器接收到A的报文时,从报文头部解析出A的…...
STL——stack容器和queue容器详解
目录 💡stack 💡基本概念 常用接口 💡queue 💡基本概念 💡常用接口 💡stack 💡基本概念 栈(stack):一种特殊的线性表,其只允许在固定的一端…...
django websocket实现聊天室功能
注意事项channel版本 django2.x 需要匹配安装 channels 2 django3.x 需要匹配安装 channels 3 Django3.2.4 channels3.0.3 Django3.2.* channels3.0.2 Django4.2 channles3.0.5 是因为最新版channels默认不带daphne服务器 直接用命令 python manage.py runsever 默认运行的是w…...
软件测评中心▏性能测试之压力测试、负载测试的区别和联系简析
在如今的信息时代,软件已经成为人们日常工作和生活不可或缺的一部分。然而,随着软件的发展和应用范围的不断扩大,软件性能的优劣也成为了影响用户使用体验的重要因素。 软件性能测试即对软件在不同条件下的性能进行评估和验证的过程。通过模…...
Go 语言 panic 和 recover 详解
panic() 和 recover() 是 Go 语言中用于处理错误的两个重要函数。panic() 函数用于中止程序并引发panic,而 recover() 函数用于捕获panic并恢复程序的执行。 什么是panic和recover? panic panic() 函数用于中止程序并引发panic。panic() 函数可以接收…...
NAND Separate Command Address (SCA) 接口数据传输解读
在采用Separate Command Address (SCA) 接口的存储产品中,DQ input burst和DQ output burst又是什么样的策略呢? DQ Input Burst: 在读取操作期间,数据以一种快速并行的方式通过DQ总线传送到控制器。在SCA接口下,虽然命令和地址信…...
彻底认识Unity ui设计中Space - Overlay、Screen Space - Camera和World Space三种模式
文章目录 简述Screen Space - Overlay优点缺点 Screen Space - Camera优点缺点 World Space优点缺点 简述 用Unity中开发了很久,但是对unity UI管理中Canvas组件的Render Mode有三种主要类型:Screen Space - Overlay、Screen Space - Camera和World Spa…...
档案数字化怎样快速整理资料
对于机构和组织来说,档案数字化是一个重要的信息管理和保护措施。要快速整理资料进行档案数字化,可以遵循以下步骤: 1. 准备工具和设备:确保有一台计算机、扫描仪和相关软件。 2. 分类和组织资料:先将资料分类…...
面试算法100:三角形中最小路径之和
题目 在一个由数字组成的三角形中,第1行有1个数字,第2行有2个数字,以此类推,第n行有n个数字。例如,下图是一个包含4行数字的三角形。如果每步只能前往下一行中相邻的数字,请计算从三角形顶部到底部的路径经…...
androj studio安装及运行源码
抖音教学视频 目录 1、 jdk安装 2、下载安装androj studio 3 、打开源码安装运行相关组件 4、 安装模拟器 1、 jdk安装 安卓项目也是java开发的,运行在虚拟机上,安装jdk及运行的时候,就会自动生成虚拟机, jdk前面已经讲过&…...
【Web】token机制
🍎个人博客:个人主页 🏆个人专栏:Web ⛳️ 功不唐捐,玉汝于成 目录 前言 正文 机制基本: 优势: 结语 我的其他博客 前言 在当今互联网时代,安全、高效的用户身份验证和资源授…...
JVM 11 调优指南:如何进行JVM调优,JVM调优参数
JVM 11的优化指南:如何进行JVM调优,以及JVM调优参数有哪些”这篇文章将包含JVM 11调优的核心概念、重要性、调优参数,并提供12个实用的代码示例,每个示例都会结合JVM调优参数和Java代码 本文已收录于,我的技术网站 dd…...
横版动作闯关游戏:幽灵之歌 GHOST SONG 中文版
在洛里安荒凉的卫星上,一件长期休眠的死亡服从沉睡中醒来。踏上发现自我、古老谜团和宇宙骇物的氛围2D冒险之旅。探索蜿蜒的洞穴,获得新的能力来揭开这个外星世界埋藏已久的秘密。 游戏特点 发现地下之物 探索这个广阔而美丽如画,充满密室和诡…...
【C++】:C++中的STL序列式容器vector源码剖析
⛅️一 vector概述 vector的使用语法可以参考文章: 总的来说:vector是可变大小数组 特点: 支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 元素保存在连续的内存空间中,因此通过下标取值非常快 在容器中间位置添加…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...
