当前位置: 首页 > news >正文

(四)activit5.23.0修复跟踪高亮显示BUG

一、先看bug

在 (三)springboot2.7.6集成activit5.23.0之流程跟踪高亮显示 末尾就发现高亮显示与预期不一样,比如上面的任务2前面的箭头没有高亮显示。

二、分析原因

具体分析步骤省略了,主要是ProcessInstanceHighlightsResource类中有段代码有bug。

可以看到源码本身有注释,说按开始时间排序不正确,用默认排序是正确的。

 select * from `act_hi_actinst` 

我们可以去数据库中查询活动列表,希望按活动发生的先后顺序排序。可以发现,大部分时候,按活动的开始时间排序是正确的,但是在活动系统自动完成或者说几个活动在毫秒级时间内同时完成,这个时候无法通过开始时间判断活动发生的先后顺序。所以官方源码中注释了按开始时间排序。

后面还有一个按活动id排序,这个也是不对的,活动id在流程部署时就确定了,与流程实例中的活动顺序无关。

那注释上为什么说按默认排序是正确的呢?

我的理解是当默认排序是以活动记录的插入顺序排序时,就是正确的。

但实际情况默认排序并不一定总是活动记录的插入顺序排序的。不一定总是正确。

MySQL的默认排序规则:
1.如果查询条件无索引列,默认按主键正序排序。
2.查询条件中有索引列,默认顺序为:主键 > 唯一索引 > 普通索引,如果在SQL中查询条件同时存在有多个,那么按照索引最先创建的顺序进行正序排序。
例:SELECT * FROM a WHERE a.id = ‘a’ and a.user_id = ‘a’;
如果id和user_id都是索引,id先创建,则按照id进行正序排序。

 从上面截图,我们就可以看到,默认排序并没有按插入顺序排序。主要原因是ID_是字符串类型,而不是整数类型,所以升序就是上图的结果。

综上,高亮显示BUG的原因是查询活动列表时没有按活动的先后顺序排序。

三、修复方案

找出原因后,就可以针对性的进行修复。具体上面的问题,可以有2种方案。

(方案一)利用mysql的默认排序规则。

这种方案,这则是利用activiti的ID生成器实现,默认的ID生成器实现就不具体分析了,看上面的截图大概也差不多可以推测出来。只要将ID生成器替换为严格按递增顺序生成的就可以了。

下面介绍几种的ID生成方法:

  • UUID:生成的UUID是由 8-4-4-4-12格式的数据组成,其中32个字符和4个连字符' - ',一般我们使用的时候会将连字符删除 uuid.toString().replaceAll("-","")。该算法不是递增的,不能满足要求。
  • StrongUuidGenerator:activiti自带的ID生成器。但是生成的ID不是递增的。
  • 数据库生成:ID_字段类型是字符串,所以无法使用自增字段。如果要改为整数自增字段,引擎改动太复杂,后过不可控,排除。
  • 雪花算法-Snowflake:该方法比较适配。但他也有相应的缺点:依赖系统时钟,64位字符串占空间,不适用短时间生成大量的ID。
  • 百度-UidGenerator:不是很熟悉,没用过。
  • 美团Leaf:不是很熟悉,没用过。

所以该方案,只要把ID生成器换成雪花算法就可以了。但要注意避免雪花算法生成重复ID。

1.雪花算法实现SnowflakeIdWorker.java
package xpl.util.id;/*** Twitter_Snowflake<br>* SnowFlake的结构如下(每部分用-分开):<br>* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>* 加起来刚好64位,为一个Long型。<br>* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。*/
public class SnowflakeIdWorker {private final static SnowflakeIdWorker sfiw = new SnowflakeIdWorker(0,0);// ==============================Fields===========================================/** 开始时间截 (2015-01-01) */private final long twepoch = 1420041600000L;/** 机器id所占的位数 */private final long workerIdBits = 5L;/** 数据标识id所占的位数 */private final long datacenterIdBits = 5L;/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */private final long maxWorkerId = -1L ^ (-1L << workerIdBits);/** 支持的最大数据标识id,结果是31 */private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);/** 序列在id中占的位数 */private final long sequenceBits = 12L;/** 机器ID向左移12位 */private final long workerIdShift = sequenceBits;/** 数据标识id向左移17位(12+5) */private final long datacenterIdShift = sequenceBits + workerIdBits;/** 时间截向左移22位(5+5+12) */private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */private final long sequenceMask = -1L ^ (-1L << sequenceBits);/** 工作机器ID(0~31) */private long workerId;/** 数据中心ID(0~31) */private long datacenterId;/** 毫秒内序列(0~4095) */private long sequence = 0L;/** 上次生成ID的时间截 */private long lastTimestamp = -1L;//==============================Constructors=====================================/*** 构造函数* @param workerId 工作ID (0~31)* @param datacenterId 数据中心ID (0~31)*/public SnowflakeIdWorker(long workerId, long datacenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}// ==============================Methods==========================================/*** 获得下一个ID (该方法是线程安全的)* @return SnowflakeId*/public synchronized long getNextId() {long timestamp = timeGen();//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}//如果是同一时间生成的,则进行毫秒内序列if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;//毫秒内序列溢出if (sequence == 0) {//阻塞到下一个毫秒,获得新的时间戳timestamp = tilNextMillis(lastTimestamp);}}//时间戳改变,毫秒内序列重置else {sequence = 0L;}//上次生成ID的时间截lastTimestamp = timestamp;//移位并通过或运算拼到一起组成64位的IDreturn ((timestamp - twepoch) << timestampLeftShift) //| (datacenterId << datacenterIdShift) //| (workerId << workerIdShift) //| sequence;}/*** 获得下一个ID (该方法是线程安全的)* @return SnowflakeId*/public static long nextId() {return sfiw.getNextId();}/*** 阻塞到下一个毫秒,直到获得新的时间戳* @param lastTimestamp 上次生成ID的时间截* @return 当前时间戳*/protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 返回以毫秒为单位的当前时间* @return 当前时间(毫秒)*/protected long timeGen() {return System.currentTimeMillis();}//==============================Test=============================================/** 测试 */public static void main(String[] args) {SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);for (int i = 0; i < 1000; i++) {long id = idWorker.getNextId();System.out.println(Long.toBinaryString(id));System.out.println(id);}}
}
2.自定义ID生成器

SnowflakeIdWorkerGenerator.java

package org.activiti.engine.impl.ext;import org.activiti.engine.impl.cfg.IdGenerator;import xpl.util.id.SnowflakeIdWorker;public class SnowflakeIdWorkerGenerator implements IdGenerator {@Overridepublic String getNextId() {return ""+SnowflakeIdWorker.nextId();}}
3.替换activiti默认的ID生成器

这个替换研究了好一会,才找到替换的方法。

所以需要扩展引擎配置,只需要实现ProcessEngineConfigurationConfigurer接口就可以了。

于是,我们创建MyProcessEngineConfigurationConfigurer类。代码如下:

package xpl.study.activiti;import org.activiti.engine.impl.ext.SnowflakeIdWorkerGenerator;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyProcessEngineConfigurationConfigurer implements ProcessEngineConfigurationConfigurer{@Overridepublic void configure(SpringProcessEngineConfiguration processEngineConfiguration) {processEngineConfiguration.setIdGenerator(new SnowflakeIdWorkerGenerator());}}
4.运行测试

(方案二)使用order by与实现按活动发生顺序进行排序。

1.对act_hi_actinst表新增一个排序字段,并且自增。

2.修改源码HistoricActivityInstanceQuery,新增一个接口orderBySeq。
package org.activiti.engine.history;
import org.activiti.engine.query.Query;/*** Programmatic querying for {@link HistoricActivityInstance}s.* * @author Tom Baeyens* @author Joram Barrez*/
public interface HistoricActivityInstanceQuery extends Query<HistoricActivityInstanceQuery, HistoricActivityInstance>{//...........HistoricActivityInstanceQuery orderBySeq();}
3.修改源码HistoricActivityInstanceQueryImpl,新增orderBySeq实现
public HistoricActivityInstanceQuery orderBySeq() {orderBy(HistoricActivityInstanceQueryProperty.SEQ);return this;}
4.修改源码HistoricActivityInstanceQueryProperty ,新增一个静态变量。
public static final HistoricActivityInstanceQueryProperty SEQ = new HistoricActivityInstanceQueryProperty("SEQ_");
5.修改源码ProcessInstanceHighlightsResource ,查询时拼接seq字段参与排序。

6.运行测试

方案二其实还可以更简单的实现。第一步与上面一样,但是可以省略上面2,3,4步,直接到第5步,利用createNativeHistoricActivityInstanceQuery查询列表,就可以直接跳过2,3,4步实现了。

四、总结

经过测试二种方案都是可行。个人比较倾向于第二种。第二种比较有安全感,第一种如果服务器时间回拨,可能导致ID重复,系统故障。虽然发生几率不是很大,但如果对系统稳定性要求较高的话还是存在一些风险。

相关文章:

  • 【Linux】:环境变量
  • 收银机打印机相关知识 windows7 查看打印机名称--未来之窗智慧经营收银系统百科
  • Linux OS:线程封装 | RAII封装锁 | 随机数运算任务封装
  • 华为校招机试 - 电影知识图谱和查询系统(20240605)
  • @Value获取值和@ConfigurationProperties获取值用法及比较(springboot)
  • 开发框架DevExpress XAF v24.2产品路线图预览——增强跨平台性
  • 医院不良事件监测预警上报系统,PHP不良事件管理系统源码
  • 认识MySQL
  • C++的GUI(图形用户界面)设计工具
  • LeetCode Hot100 二叉搜索树中第K小的元素
  • 探秘企业孵化基地,聚焦国际数字影像产业园
  • Spring有5种自动装配方式,其中autodetect默认使用?
  • 考研数学|《660》《880》怎么搭配使用
  • 【计算机网络】什么是socket编程?以及相关接口详解
  • Java线程池的这几个大坑,你踩过几个?
  • 网络传输文件的问题
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • 2017届校招提前批面试回顾
  • Java 实战开发之spring、logback配置及chrome开发神器(六)
  • Java精华积累:初学者都应该搞懂的问题
  • Mysql数据库的条件查询语句
  • nfs客户端进程变D,延伸linux的lock
  • redis学习笔记(三):列表、集合、有序集合
  • Redux系列x:源码分析
  • 初探 Vue 生命周期和钩子函数
  • 创建一个Struts2项目maven 方式
  • 电商搜索引擎的架构设计和性能优化
  • 给自己的博客网站加上酷炫的初音未来音乐游戏?
  • 聊聊springcloud的EurekaClientAutoConfiguration
  • 码农张的Bug人生 - 见面之礼
  • 面试遇到的一些题
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • 正则表达式小结
  • PostgreSQL之连接数修改
  • ​Java基础复习笔记 第16章:网络编程
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • (¥1011)-(一千零一拾一元整)输出
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (13)DroneCAN 适配器节点(一)
  • (145)光线追踪距离场柔和阴影
  • (2024,Vision-LSTM,ViL,xLSTM,ViT,ViM,双向扫描)xLSTM 作为通用视觉骨干
  • (SpringBoot)第二章:Spring创建和使用
  • (第二周)效能测试
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (转载)Linux 多线程条件变量同步
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .NET单元测试使用AutoFixture按需填充的方法总结
  • .net访问oracle数据库性能问题
  • .NET实现之(自动更新)
  • .net中生成excel后调整宽度
  • @Bean注解详解
  • @EnableConfigurationProperties注解使用
  • @RequestBody与@ResponseBody的使用
  • [3D游戏开发实践] Cocos Cyberpunk 源码解读-高中低端机性能适配策略
  • [4]CUDA中的向量计算与并行通信模式