(四)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
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位的ID
        return ((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

java 复制代码
package org.activiti.engine.impl.ext;

import org.activiti.engine.impl.cfg.IdGenerator;

import xpl.util.id.SnowflakeIdWorker;

public class SnowflakeIdWorkerGenerator implements IdGenerator {

	@Override
	public String getNextId() {
		return ""+SnowflakeIdWorker.nextId();
	}

}
3.替换activiti默认的ID生成器

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

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

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

java 复制代码
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{

	@Override
	public void configure(SpringProcessEngineConfiguration processEngineConfiguration) {
		processEngineConfiguration.setIdGenerator(new SnowflakeIdWorkerGenerator());
	}

}
4.运行测试

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

1.对act_hi_actinst表新增一个排序字段,并且自增。
2.修改源码HistoricActivityInstanceQuery,新增一个接口orderBySeq。
java 复制代码
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实现
java 复制代码
public HistoricActivityInstanceQuery orderBySeq() {
	    orderBy(HistoricActivityInstanceQueryProperty.SEQ);
	    return this;
  }
4.修改源码HistoricActivityInstanceQueryProperty ,新增一个静态变量。
java 复制代码
public static final HistoricActivityInstanceQueryProperty SEQ = new HistoricActivityInstanceQueryProperty("SEQ_");
5.修改源码ProcessInstanceHighlightsResource ,查询时拼接seq字段参与排序。
6.运行测试

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

四、总结

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

相关推荐
139的世界真奇妙1 天前
生产问题排查记录
golang·bug·学习方法
oioihoii1 天前
我的第一次移动端 AI 办公:在地铁上把 Bug 修了
人工智能·bug
Coder_Shenshen1 天前
【基于LibUA库的OPC UA服务器与客户端Demo——协议解析与Bug修复实践】
网络·c#·bug
YDS8292 天前
DeepSeek RAG&MCP + Agent智能体项目 —— 环境搭建和项目初始化
java·springboot·agent·rag·deepseek
jasnet_u3 天前
SpringMVC 请求处理深度解析:从 DispatcherServlet 到视图渲染
spring·springmvc·springboot
格鸰爱童话3 天前
springboot3.2使用neo4j
springboot·neo4j
Pan Zonghui3 天前
GitHub Bug反馈与修复全流程指南
github·bug
不是光头 强4 天前
Spring Boot 多线程场景下 i18n 国际化失效问题排查与解决
java·开发语言·springboot
初圣魔门首席弟子4 天前
bug 2026.05.15(以前能运行的java springboot项目突然间不能运行后台数据了)
java·开发语言·bug
Desenberg4 天前
【Claude Code】因为中途修改配置路径导致Claude Code 插件安装失败
windows·bug