flowable源码解读之LRU缓存设计

流程数据的定义在flowable中是比较复杂的, 涉及到多张数据库表关联关系,这些在一个流程引擎中也是最为核心的数据,并且需要进行频繁的读取,所以为了实现读取的高效性,flowable将流程定义的数据放在内存中,需要时直接从内存中获取。本文就简单看一看flowable是怎么实现缓存的。

1. 用户定义的入口

先回顾一下,如果我们要在Spring环境中使用flowable必须采用类似方式定义一个bean


SpringProcessEngineConfiguration继承于ProcessEngineConfigurationImpl,在ProcessEngineConfigurationImpl源码中可以看到:

    protected int processDefinitionCacheLimit = -1; // By default, no limit
    protected DeploymentCache processDefinitionCache;

    protected int processDefinitionInfoCacheLimit = -1; // By default, no limit
    protected ProcessDefinitionInfoCache processDefinitionInfoCache;

    protected int knowledgeBaseCacheLimit = -1;
    protected DeploymentCache knowledgeBaseCache;

    protected int appResourceCacheLimit = -1;
    protected DeploymentCache appResourceCache;
 
 

因此我们可以在bean定义中可以设置processDefinitionCacheLimit等属性的值即可控制缓存的容量,如果没有进行设置将不会对其限制,为以防止OOM异常,建议可以设置一个,当超出容量时flowable引擎将会通过LRU算法进行移除。

2. 缓存的初始化

在flowable流程引擎在Spring环境中的启动源码分析这篇文章我们已经知道了flowble流程的初始化过程,那么对缓存的初始化肯定也能在相关内里找到,在ProcessEngineConfigurationImpl的init方法中有如下几行代码:

        initProcessDefinitionCache();
        initProcessDefinitionInfoCache();
        initAppResourceCache();
        initKnowledgeBaseCache();

以initProcessDefinitionCache为例看一下方法实现,不用多说:

    public void initProcessDefinitionCache() {
        if (processDefinitionCache == null) {
            if (processDefinitionCacheLimit <= 0) {
                processDefinitionCache = new DefaultDeploymentCache();
            } else {
                processDefinitionCache = new DefaultDeploymentCache(processDefinitionCacheLimit);
            }
        }
    }

进入DefaultDeploymentCache的构造方法,当没有设置缓存大小时通过无参构造方法创建的是一个同步的Map, 重点可以看一下下面的有参构造函数实现:

/** Cache with no limit */
public DefaultDeploymentCache() {
    this.cache = Collections.synchronizedMap(new HashMap());
}

/**
 * Cache which has a hard limit: no more elements will be cached than the limit.
 */
public DefaultDeploymentCache(final int limit) {
    this.cache = Collections.synchronizedMap(new LinkedHashMap(limit + 1, 0.75f, true) { // +1 is needed, because the entry is inserted first, before it is removed
        // 0.75 is the default (see javadocs)
        // true will keep the 'access-order', which is needed to have a real LRU cache
        private static final long serialVersionUID = 1L;

        protected boolean removeEldestEntry(Map.Entry eldest) {
            boolean removeEldest = size() > limit;
            if (removeEldest && logger.isTraceEnabled()) {
                logger.trace("Cache limit is reached, {} will be evicted", eldest.getKey());
            }
            return removeEldest;
        }

    });
}

有参构造方法核心是基于LinkedHashMap并且重写了removeEldestEntry方法,当超出容量时会返回true, 查看LinkedHashMap可以知道当调用put或putAll返回前会根据该方法返回的值决定是否移除最老的一个元素,从而实现了LRU缓存算法。

3. 缓存的写入与更新

在流程部署时肯定会涉及到相关数据的更新,通过RepositoryServiceImpl.deploy->DeployCmd.executeDeploy查看有如下代码:

        // Actually deploy
        commandContext.getProcessEngineConfiguration().getDeploymentManager().deploy(deployment, deploymentSettings);

查看DeploymentManager.deploy->BpmnDeployer.deploy代码:

        cachingAndArtifactsManager.updateCachingAndArtifacts(parsedDeployment);

然后想看CachingAndArtifactsManager.updateCachingAndArtifacts方法源码即具体更新缓存的实现:

    /**
     * Ensures that the process definition is cached in the appropriate places, including the deployment's collection of deployed artifacts and the deployment manager's cache, as well as caching any
     * ProcessDefinitionInfos.
     */
    public void updateCachingAndArtifacts(ParsedDeployment parsedDeployment) {
        CommandContext commandContext = Context.getCommandContext();
        final ProcessEngineConfigurationImpl processEngineConfiguration = Context.getProcessEngineConfiguration();
        DeploymentCache processDefinitionCache = processEngineConfiguration.getDeploymentManager().getProcessDefinitionCache();
        DeploymentEntity deployment = parsedDeployment.getDeployment();

        for (ProcessDefinitionEntity processDefinition : parsedDeployment.getAllProcessDefinitions()) {
            BpmnModel bpmnModel = parsedDeployment.getBpmnModelForProcessDefinition(processDefinition);
            Process process = parsedDeployment.getProcessModelForProcessDefinition(processDefinition);
            ProcessDefinitionCacheEntry cacheEntry = new ProcessDefinitionCacheEntry(processDefinition, bpmnModel, process);
            processDefinitionCache.add(processDefinition.getId(), cacheEntry);
            addDefinitionInfoToCache(processDefinition, processEngineConfiguration, commandContext);

            // Add to deployment for further usage
            deployment.addDeployedArtifact(processDefinition);
        }
    }

4. 缓存的读取

一个典型的读取场景就是在启动流程的时候,所以查看RuntimeServiceImpl.startProcessInstanceByKey->StartProcessInstanceCmd.execute方法源码:

// Find the process definition
ProcessDefinition processDefinition = null;
if (processDefinitionId != null) {

    processDefinition = deploymentCache.findDeployedProcessDefinitionById(processDefinitionId);
    if (processDefinition == null) {
        throw new FlowableObjectNotFoundException("No process definition found for id = '" + processDefinitionId + "'", ProcessDefinition.class);
    }

}

接下来进入DeploymentManager.findDeployedProcessDefinitionById可以看到, 首先会从缓存中查找,如果没有则从数据库中加载:

public ProcessDefinition findDeployedProcessDefinitionById(String processDefinitionId) {
    if (processDefinitionId == null) {
        throw new FlowableIllegalArgumentException("Invalid process definition id : null");
    }

    // first try the cache
    ProcessDefinitionCacheEntry cacheEntry = processDefinitionCache.get(processDefinitionId);
    ProcessDefinition processDefinition = cacheEntry != null ? cacheEntry.getProcessDefinition() : null;

    if (processDefinition == null) {
        processDefinition = processDefinitionEntityManager.findById(processDefinitionId);
        if (processDefinition == null) {
            throw new FlowableObjectNotFoundException("no deployed process definition found with id '" + processDefinitionId + "'", ProcessDefinition.class);
        }
        processDefinition = resolveProcessDefinition(processDefinition).getProcessDefinition();
    }
    return processDefinition;
}

然后看一下resolveProcessDefinition方法, 当缓存中没有数据时会调用deploy方法来重新加载缓存。

/**
 * Resolving the process definition will fetch the BPMN 2.0, parse it and store the {@link BpmnModel} in memory.
 */
public ProcessDefinitionCacheEntry resolveProcessDefinition(ProcessDefinition processDefinition) {
    String processDefinitionId = processDefinition.getId();
    String deploymentId = processDefinition.getDeploymentId();

    ProcessDefinitionCacheEntry cachedProcessDefinition = processDefinitionCache.get(processDefinitionId);

    if (cachedProcessDefinition == null) {
        if (Flowable5Util.isFlowable5ProcessDefinition(processDefinition, processEngineConfiguration)) {
            return Flowable5Util.getFlowable5CompatibilityHandler().resolveProcessDefinition(processDefinition);
        }

        DeploymentEntity deployment = deploymentEntityManager.findById(deploymentId);
        deployment.setNew(false);
        deploy(deployment, null);
        cachedProcessDefinition = processDefinitionCache.get(processDefinitionId);

        if (cachedProcessDefinition == null) {
            throw new FlowableException("deployment '" + deploymentId + "' didn't put process definition '" + processDefinitionId + "' in the cache");
        }
    }
    return cachedProcessDefinition;
}

总结一下:flowable缓存的实现核心即基于LinkedHashMap并通过重写其removeEldestEntry方法实现LRU缓存移除算法。以流程定义缓存为例可以知道,每次部署时会将流程定义的数据加入缓存,每次流程启动时都会尝试去缓存中获取数据,如果缓存中有就直接返回,如果没有就从数据库中加载并放入缓存以供下次使用。

你可能感兴趣的:(flowable源码解读之LRU缓存设计)