魏晓东 发表于 2024-8-7 09:14:42

(四)activit5.23.0修复跟踪高亮表现BUG

一、先看bug

https://i-blog.csdnimg.cn/direct/bbcfba97cda14458acaba447b253efc3.png
在 (三)springboot2.7.6集成activit5.23.0之流程跟踪高亮表现 末端就发现高亮表现与预期不一样,比如上面的任务2前面的箭头没有高亮表现。
二、分析原因

详细分析步骤省略了,告急是ProcessInstanceHighlightsResource类中有段代码有bug。
https://i-blog.csdnimg.cn/direct/d3612416eb75450ab37c63b4e95a7b5d.png
https://i-blog.csdnimg.cn/direct/705461e672364959bd94d126c217f805.png可以看到源码本身有解释,说按开始时间排序不精确,用默认排序是精确的。
    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进行正序排序。
https://i-blog.csdnimg.cn/direct/938fa3cce3dd4453b644bc5257fb74ac.png 从上面截图,我们就可以看到,默认排序并没有按插入顺序排序。告急原因是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位的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
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天生器

这个替换研究了好一会,才找到替换的方法。
https://i-blog.csdnimg.cn/direct/a1d9e6b93ab3470a8f334b6fc94646dd.png
https://i-blog.csdnimg.cn/direct/c3724d25301a4d74b6ed6d2a125459e7.png
https://i-blog.csdnimg.cn/direct/737a25f574424930bcda3440f9a23339.png
所以需要扩展引擎配置,只需要实现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{

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

}
4.运行测试

https://i-blog.csdnimg.cn/direct/a8fc7b52560f4fa796516fd5455a55a0.png
https://i-blog.csdnimg.cn/direct/fd720b6b393f4fdcbf236c921e517cdb.png
(方案二)使用order by与实现按活动发生顺序进行排序。

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

https://i-blog.csdnimg.cn/direct/c0d843fe5b384c5d9a60c700007d4514.png
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字段参与排序。

https://i-blog.csdnimg.cn/direct/aae6806137394537ba61b0ffb047855e.png
6.运行测试

https://i-blog.csdnimg.cn/direct/e6fa6db983a8449b94f443e8e43b7e8f.png


方案二实在还可以更简单的实现。第一步与上面一样,但是可以省略上面2,3,4步,直接到第5步,使用createNativeHistoricActivityInstanceQuery查询列表,就可以直接跳过2,3,4步实现了。
https://i-blog.csdnimg.cn/direct/9d54005c19de4ed3b4794b6ab0894b21.png
四、总结

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: (四)activit5.23.0修复跟踪高亮表现BUG