开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Camunda动态生成工作流流程定义并部署更新流程(新手上路

发表于 2021-11-14

环境: Spring-boot 嵌入式开发 、Canmunda版本7.16.0 、Canmunda依赖 camunda-bpm-spring-boot-starter

首先需要熟悉Camunda几个概念:

1. 部署(deploy/deployment):Camunda文档——processes.xml#流程应用程序部署

  • 部署意味着一个Camunda程序的启动,向数据库中注册自己,并上传自身的流程定义文件
  • 在processes.xml文件中,一个<process-archive> 定义意味着一次部署(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchivesStep),如果processes.xml文件全空,则会填充默认配置(源码位置org.camunda.bpm.container.impl.deployment.ParseProcessesXmlStep、org.camunda.bpm.application.impl.metadata.spi.ProcessesXml#EMPTY_PROCESSES_XML)。process-archive的name属性不为空时,作为部署的Name属性(数据库字段ACT_RE_DEPLOYMENT#NAME_)。Name留空(默认配置留空)时,部署名采用注解@EnableProcessApplication("appName")否则为spring程序名@Value("${spring.application.name:null}")(源码位置org.camunda.bpm.application.AbstractProcessApplication#getName)

2. 恢复/激活部署(resumed/activate)与注册(register):文档同上,建议看源码注释(org.camunda.bpm.engine.ManagementService)

  • 恢复部署意味着激活一个Camunda应用(ProcessApplication),流程引擎(根据架构选择,流程引擎可能是外部的、共享的,又或者和应用同时启动)将在此应用中执行工作,作业处理器(job executor)也开始获取属于这个部署的流程作业。(如果一个部署没有被激活,属于该部署id的流程定义的流程实例可能无法正常工作——比如具有调用额外java类和资源的流程将无法执行)
  • 当应用程序重启时,会自动发起部署(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#performOperationStep) 根据部署名(通常也就是process-archive name)查找最近版本部署,然后按byte对比两次部署的资源(.bpmn文件)差异(源码位置org.camunda.bpm.engine.impl.cmd.DeployCmd#resolveResourcesToDeploy、org.camunda.bpm.engine.repository.DeploymentHandler#shouldDeployResource),如果完全相同,视为同一个部署,则会恢复部署,若果有差异,则连同所有的流程定义(.bpmn)文件部署一个新版本的同名部署。
  • 根据<property name="isResumePreviousVersions">true</property>(默认)和<property name="resumePreviousBy">process-definition-key</property>(默认)(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#enableResumingOfPreviousVersions、org.camunda.bpm.engine.impl.cmd.DeployCmd#registerProcessApplication) 会自动恢复以前的部署,process-definition-key意味着根据这次部署中所有的流程定义,所有包含具有相同processKey的流程定义(只包含一部分也算)的部署都将恢复。deployment-name则意味着所有同名部署都会恢复。

3. isDeployChangedOnly:资源过滤

  • <property name="isDeployChangedOnly">false</property>(默认)
  • 此属性指示是否只应将与最新同名部署不同的资源作为部署的一部分,开启时,会拉取同名最新部署的同名资源进行比较,如果资源有一个byte不同,或者以前的部署没有同名资源才会放入部署列表
  • 根据官网文件档,指示此属性包含副作用,会影响自动恢复部署,但是在源码org.camunda.bpm.engine.impl.cmd.DeployCmd#getProcessDefinitionsFromResources中,将排除的资源也加入了恢复部署的列表(?)
  • 如果开启了此属性,则需要特别小心 CallActivity 的流程步骤,此功能会将新流程与旧流程分隔为两个版本的部署,流程调用中如果绑定方式设为部署,则被调用的流程定义与调用流程定义因为以部署id绑定,可能需要属于同一部署名且同一版本的部署(尚未验证),故绑定方式应设为latest或version(旧版本部署肯定会被恢复/激活/注册,尚未验证旧版本也被恢复/激活/注册的情况下这种调用能不能横跨部署版本)

Java代码动态生成工作流流程定义:

Camunda文档 BPMN API

Camunda文档 Fluent BPMN Builder API

1.原始Api :(搬运官网案例)

有两个并行任务的简单流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码// 创建一个空模型
BpmnModelInstance modelInstance = Bpmn.createEmptyModel();
Definitions definitions = modelInstance.newInstance(Definitions.class);
definitions.setTargetNamespace("http://camunda.org/examples");
modelInstance.setDefinitions(definitions);

// 创建元素
StartEvent startEvent = createElement(process, "start", StartEvent.class);
ParallelGateway fork = createElement(process, "fork", ParallelGateway.class);
ServiceTask task1 = createElement(process, "task1", ServiceTask.class);
task1.setName("Service Task");
UserTask task2 = createElement(process, "task2", UserTask.class);
task2.setName("User Task");
ParallelGateway join = createElement(process, "join", ParallelGateway.class);
EndEvent endEvent = createElement(process, "end", EndEvent.class);

// 创建流
createSequenceFlow(process, startEvent, fork);
createSequenceFlow(process, fork, task1);
createSequenceFlow(process, fork, task2);
createSequenceFlow(process, task1, join);
createSequenceFlow(process, task2, join);
createSequenceFlow(process, join, endEvent);

// 验证模型并将其写入文件
Bpmn.validateModel(modelInstance);
File file = File.createTempFile("bpmn-model-api-", ".bpmn");
Bpmn.writeModelToFile(file, modelInstance);

1.Fluent Api :(搬运官网案例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码BpmnModelInstance modelInstance = Bpmn.createProcess()
.startEvent()
.userTask()
.id("question")
.exclusiveGateway()
.name("Everything fine?")
.condition("yes", "#{fine}")
.serviceTask()
.userTask()
.endEvent()
.moveToLastGateway()
.condition("no", "#{!fine}")
.userTask()
.connectTo("question")
.done()

注意点:

节点id必须是合法的Xml NCName,且不能包含汉字、”-“
判断是否汉字用正则表达式,判断是否 Xml NCName 使用:com.sun.org.apache.xerces.internal.util.XMLChar#isValidNCName 注意此函数被idea提示为内部函数,jdk1.8以后不能保证存在

1
2
3
4
java复制代码    private static void validXmlChar(String str) {
//...汉字校验部分
Assert.isTrue(XMLChar.isValidNCName(str) && !str.contains("-"), "\"" + str + "\" 必须是有效的 Xml NcName 且不包含 '-' ");
}

部署生成的流程定义:

ProcessEngine 各种Service 的 Bean 声明在org.camunda.bpm.engine.spring.SpringProcessEngineServicesConfiguration

部署时候的配置参考org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#performOperationStep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码@Service
@RequiredArgsConstructor
public class ProcessReDeployService{
private final RepositoryService repositoryService;
private final SpringBootProcessApplication processApplication;
/**
* 以指定部署名部署,会生成最新版本的部署,并且激活旧版本的同名部署
*/
public void deployProcessByName(String processArchiveName, String resourceName, BpmnModelInstance bpmnModelInstance) {
getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(processArchiveName).deploy();
}
/**
* 在指定旧版部署id更新部署
*/
public void deployProcessUpdateIncrementallyById(String oldDeploymentId, String resourceName, BpmnModelInstance bpmnModelInstance) {
String processArchiveName = repositoryService.createDeploymentQuery().deploymentId(oldDeploymentId).singleResult().getName();
getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(processArchiveName).addDeploymentResources(oldDeploymentId).deploy();
}
/**
* 在指定部署名更新部署,并指定需要COPY的其他部署的流程定义
*/
public void deployProcessUpdateIncrementallyByName(String processArchiveName, String resourceName, BpmnModelInstance bpmnModelInstance) {
Deployment deployment = Optional.ofNullable(repositoryService.createDeploymentQuery()
.deploymentName(processArchiveName).orderByDeploymentTime().desc().listPage(0, 1))//查找同名最新部署
.map(e -> e.get(0)).orElseThrow(() -> new RuntimeException(processArchiveName + "not found"));
List<String> oldResourceNames = repositoryService.getDeploymentResourceNames(deployment.getId());
oldResourceNames.remove(resourceName);
getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(deployment.getName()).addDeploymentResourcesByName(deployment.getId(),oldResourceNames).deploy();
}
private ProcessApplicationDeploymentBuilder getDeploymentByDefaultConfig(String resourceName, BpmnModelInstance bpmnModelInstance) {
resourceName = validateResourceName(resourceName);
return repositoryService.createDeployment(processApplication.getReference())
.addModelInstance(resourceName, bpmnModelInstance)
//开启重复资源过滤 、关闭 isDeployChangedOnly
.enableDuplicateFiltering(false)
.resumePreviousVersions()
.resumePreviousVersionsBy(ResumePreviousBy.RESUME_BY_PROCESS_DEFINITION_KEY);
}
/**
* 如果不以.bpmn结尾则加上.bpmn后缀
*/
private String validateResourceName(String resourceName) {
if (!StringUtil.hasAnySuffix(resourceName, BpmnDeployer.BPMN_RESOURCE_SUFFIXES)) {
resourceName = resourceName.concat(BpmnDeployer.BPMN_RESOURCE_SUFFIXES[1]);
}
return resourceName;
}
}

需要注意的点是:

  • 如果部署不指定name,则会生成name为NULL的部署
  • 如果部署时不开启资源过滤(.enableDuplicateFiltering(true/false)),则每次部署都是新部署(虽然以更新流程为目标时,也必然是新部署就是了)
  • 没有特别需要不必开启 isDeployChangedOnly (.enableDuplicateFiltering(true))
  • 尚不确定是否需要在部署新流程时包含旧流程定义(更新式部署),但是按照一般性思路,更新流程文件通常是修改jar包 Resources文件夹内的.bpmn文件,然后打包重新发布,最好模拟这个流程,在更新流程文件的部署时,包含除了需要修改的流程文件以外旧部署的其他流程文件(但是如果存在运行时修改或者创建流程定义的情况,就需要下面的方案👇)

- 在运行过程中添加或修改的流程,如果程序重新启动,就不会被包含在启动时部署的流程中(如果启动时不包含任何流程byte甚至不会执行部署),需要在流程启动时的部署中添加原有部署的资源:

我这里直接顶替掉SpringBootProcessApplication,这样@EnableProcessApplication注解也用不加了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码@Configuration(proxyBeanMethods = false)
public class BpmProcessConfig extends SpringBootProcessApplication {

@Value("${spring.application.name}")
private String appName;

@Override
public void createDeployment(@Nullable String processArchiveName, DeploymentBuilder deploymentBuilder) {
if(processArchiveName==null){
processArchiveName =getName();
}
List<Deployment> deployments = processEngine.getRepositoryService().createDeploymentQuery().deploymentName(processArchiveName).orderByDeploymentTime().desc().listPage(0, 1);
if(deployments!=null&&!deployments.isEmpty()){
//额外部署配置 因为存在在运行期间的动态部署,所以初始部署时,从数据库拉去最新同名部署的资源,排除掉已有的部分,将运行deploymentBuilder = {ProcessApplicationDeploymentBuilderImpl@10932} 过程中部署的资源也包含在内
//防止动态部署的流程资源在应用重启后丢失
//但是如果部署时不添加本地资源,部署操作就会直接跳过——> DeployProcessArchiveStep#performOperationStep() 141行 —— /**if(!deploymentResourceNames.isEmpty()) {**/
//DeployCmd执行部署操作时,如果部署的资源与最新版资源完全一致,就会去掉部署资源,部署资源为空时,就会将其确定为重复部署 重复部署就直接查询数据库数据然后注册 DeployCmd 159行 DeploymentHandler#determineDuplicateDeployment()
//本来只要部署时啥也不添加资源就能重复部署,但是为了避免直接操作引擎侵入性太高,所以只能行此下策 ,先添加数据库的资源,再等部署时被筛选去掉,绕来绕去浪费性能(=_=)
Deployment latestDeployment = deployments.get(0);
if(latestDeployment!=null){
List<Resource> deploymentResources = processEngine.getRepositoryService().getDeploymentResources(latestDeployment.getId());
for (Resource deploymentResource : deploymentResources) {
deploymentBuilder.addInputStream(deploymentResource.getName(),new ByteArrayInputStream(deploymentResource.getBytes()));
}
}
}

}

@Override
public String getName() {
if (!StringUtils.hasText(appName)) {
appName = applicationContext.getBeanNamesForAnnotation(SpringBootApplication.class)[0];
}
return appName;
}
}

根据 DeployProcessArchiveStep、DeployCmd的源码,程序部署的流程是

  1. 程序启动,准备部署
  2. 查看部署是否含有资源(if(!deploymentResourceNames.isEmpty()))
    • 无资源直接打个日志,不部署了
  3. DeployCmd开始部署
  4. 下载部署资源的远程资源(DeployCmd#getResources())
  5. 从资源中解析需要部署的资源(DeployCmd#resolveResourcesToDeploy())
    • 如果全部资源都与数据库中最新同名部署资源全部一致,就判断为重复部署
    • 重复部署,根据部署名查找最新部署id(DefaultDeploymentHandler#determineDuplicateDeployment())
    • 根据id从数据库获取DeploymentEntity,将其注册
  6. 是新部署,往数据库插入数据,然后将其注册

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用21张图,把Git 工作原理彻底说清楚

发表于 2021-11-14

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

本文图解Git中的最常用命令。如果你稍微理解Git的工作原理,这篇文章能够让你理解的更透彻。

基本用法

e8e1b09695f575b00cf43b2fdd837a41.png

7a3d0980a0fb0a67f21b48cfab5bacfb.png

上面的四条命令在工作目录、暂存目录(也叫做索引)和仓库之间复制文件。

  • git add files把当前文件放入暂存区域。
  • git commit给暂存区域生成快照并提交。
  • git reset – files用来撤销最后一次git add files,你也可以用git reset撤销所有暂存区域文件。
  • git checkout – files把文件从暂存区域复制到工作目录,用来丢弃本地修改。

你可以用git reset -p,git checkout -p,or git add -p进入交互模式。

也可以跳过暂存区域直接从仓库取出文件或者直接提交代码。

c3e39b83a212f32325afd93dd44b0263.png

  • git commit -a相当于运行git add把所有当前目录下的文件加入暂存区域再运行。
  • git commit files进行一次包含最后一次提交加上工作目录中文件快照的提交。并且文件被添加到暂存区域。
  • git checkout HEAD – files回滚到复制最后一次提交。

约定

17c08a465b2c0b4febb84a72a87e7b2f.png

后文中以下面的形式使用图片。

1eb22138f579bb2cff347afd99b355f8.png

绿色的5位字符表示提交的ID,分别指向父节点。分支用橘色显示,分别指向特定的提交。当前分支由附在其上的HEAD标识。这张图片里显示最后5次提交,ed489是最新提交。master分支指向此次提交,另一个maint分支指向祖父提交节点。

命令详解

ebdd9db5c60688b9a31b846c9aeb2f80.png

Diff

有许多种方法查看两次提交之间的变动,下面是一些示例。

75a6c20d7cdb1c4490d80d531bb4ea54.png

Commit

提交时,Git用暂存区域的文件创建一个新的提交,并把此时的节点设为父节点。然后把当前分支指向新的提交节点。下图中,当前分支是master。在运行命令之前,master指向ed489,提交后,master指向新的节点f0cec并以ed489作为父节点。

e842dc7bce107f5a42c318ba2e2607ac.png

即便当前分支是某次提交的祖父节点,git会同样操作。下图中,在master分支的祖父节点maint分支进行一次提交,生成了1800b。这样,maint分支就不再是master分支的祖父节点。此时,合并[1](或者衍合[2])是必须的。

057754de8894e1dfb7b6e64a037c9e1e.png

如果想更改一次提交,使用git commit –amend。Git会使用与当前提交相同的父节点进行一次新提交,旧的提交会被取消。

0d2869a71d770943f89caba0c433997b.png

另一个例子是分离HEAD提交[3],后文讲。

Checkout

Checkout命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。

当给定某个文件名(或者打开-p选项,或者文件名和-p选项同时打开)时,Git会从指定的提交中拷贝文件到暂存区域和工作目录。比如,git checkout HEAD~ foo.c会将提交节点HEAD~(即当前提交节点的父节点)中的foo.c复制到工作目录并且加到暂存区域中。(如果命令中没有指定提交节点,则会从暂存区域中拷贝内容。)注意当前分支不会发生变化。

212a4803ed1a6a35900416c254db82b0.png

当不指定文件名,而是给出一个(本地)分支时,那么HEAD标识会移动到那个分支(也就是说,我们“切换”到那个分支了),然后暂存区域和工作目录中的内容会和HEAD对应的提交节点一致。新提交节点(下图中的a47c3)中的所有文件都会被复制(到暂存区域和工作目录中);只存在于老的提交节点(ed489)中的文件会被删除;不属于上述两者的文件会被忽略,不受影响。

86495af05caa02cc8b0b7d6b2f3e1710.png

如果既没有指定文件名,也没有指定分支名,而是一个标签、远程分支、SHA-1值或者是像master~3类似的东西,就得到一个匿名分支,称作detached HEAD(被分离的HEAD标识)。这样可以很方便地在历史版本之间互相切换。比如说你想要编译1.6.6.1版本的Git,你可以运行git checkout v1.6.6.1(这是一个标签,而非分支名),编译,安装,然后切换回另一个分支,比如说git checkout master。然而,当提交操作涉及到“分离的HEAD”时,其行为会略有不同,详情见在下面。

39147242b1b063f63fb597dd3bcbfcf3.png

HEAD标识处于分离状态时的提交操作

当HEAD处于分离状态(不依附于任一分支)时,提交操作可以正常进行,但是不会更新任何已命名的分支。(你可以认为这是在更新一个匿名分支。)

f9c66579085832811e91c7e3b0151502.png

一旦此后你切换到别的分支,比如说master,那么这个提交节点(可能)再也不会被引用到,然后就会被丢弃掉了。注意这个命令之后就不会有东西引用2eecb。

5ce0b2ef904bdd28587a60c97b102cd4.png

但是,如果你想保存这个状态,可以用命令git checkout -b name来创建一个新的分支。

c76979c3e2831c0ff82b95907a52cd27.png

Reset

Reset命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。

如果不给选项,那么当前分支指向到那个提交。如果用–hard选项,那么工作目录也更新,如果用–soft选项,那么都不变。

c3f8ebf03bded89ca33e763e97b5cfdf.png

如果没有给出提交点的版本号,那么默认用HEAD。这样,分支指向不变,但是索引会回滚到最后一次提交,如果用–hard选项,工作目录也同样。

1eb03d4690d65413d8945790bf94c296.png

如果给了文件名(或者-p选项),那么工作效果和带文件名的checkout差不多,除了索引被更新。

a568b45dda8eaa0ad07fd6e2f1c92203.png

Merge

Merge命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。另一种情况是如果当前提交是另一个分支的祖父节点,就导致fast-forward合并。指向只是简单的移动,并生成一个新的提交。

53e68402d5876c07423faa94378a5832.png

否则就是一次真正的合并。默认把当前提交(ed489 如下所示)和另一个提交(33104)以及他们的共同祖父节点(b325c)进行一次三方合并[4]。结果是先保存当前目录和索引,然后和父节点33104一起做一次新提交。

ff74fd4e876d672320b99e0928d05271.png

Cherry Pick

cherry-pick命令“复制”一个提交节点并在当前分支做一次完全一样的新提交。

5cbfdb5980082cc9cb6371c392534807.png

Rebase

衍合是合并命令的另一种选择。合并把两个父分支合并进行一次提交,提交历史不是线性的。衍合在当前分支上重演另一个分支的历史,提交历史是线性的。本质上,这是线性化的自动的 cherry-pick。

d835fc2f683fd80753b5185b163df032.png

上面的命令都在topic分支中进行,而不是master分支,在master分支上重演,并且把分支指向新的节点。注意旧提交没有被引用,将被回收。

要限制回滚范围,使用–onto选项。下面的命令在master分支上重演当前分支从169a6以来的最近几个提交,即2c33a。

422dcc6501fc758f00f4d157a9432214.png

同样有git rebase –interactive让你更方便的完成一些复杂操作,比如丢弃、重排、修改、合并提交。

文件内容并没有真正存储在索引(.git/index)或者提交对象中,而是以blob的形式分别存储在数据库中(.git/objects),并用SHA-1值来校验。索引文件用识别码列出相关的blob文件以及别的数据。对于提交来说,以树(tree)的形式存储,同样用对于的哈希值识别。树对应着工作目录中的文件夹,树中包含的 树或者blob对象对应着相应的子目录和文件。每次提交都存储下它的上一级树的识别码。

如果用detached HEAD提交,那么最后一次提交会被the reflog for HEAD引用。但是过一段时间就失效,最终被回收,与git commit –amend或者git rebase很像。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring容器的事件监听机制

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

前言

Spring的ApplicationContext容器内部中的所有事件类型均继承自 org.springframework.context.AppliationEvent,容器中的所有监听器都实现 org.springframework.context.ApplicationListener接口,并且以bean的形式注册在容器中。一旦在容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。

Spring容器内的事件监听机制.png

上面是Spring提供的标准的监听事件。

通过 ApplicationEvent 类和 ApplicationListener 接口来提供在 ApplicationContext 中处理事件。如果一个 bean 实现 ApplicationListener,那么每次 ApplicationEvent 被发布到 ApplicationContext 上,那个 bean 会被通知。

用Spring实现观察者模式

1. 创建监听事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class MyEvent extends ApplicationEvent {

private Long taskId;

// 创建事件,参数通过构造方法传入
public MyEvent(Object source, Long taskId) {
super(source);
this.taskId = taskId;
}

public Long getTaskId() {
return taskId;
}
}

2. 事件监听

1
2
3
4
5
6
7
8
9
10
java复制代码@Component
@Slf4j
public class MyListener implements ApplicationListener<MyEvent> {

// 创建一个事件监听
@Override
public void onApplicationEvent(MyEvent event) {
log.info("MyEvent event taskId:{}", event.getTaskId());
}
}

3. 事件发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Component
@Slf4j
public class MyEventPublisher implements ApplicationEventPublisherAware {

private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

this.applicationEventPublisher = applicationEventPublisher;
}

// 比如某个事件处理完成,发布一个任务处理事件做后续处理
public void publishEvent(Long taskId) {
log.info("发布事件, 任务ID:{} ", taskId);
applicationEventPublisher.publishEvent(new MyEvent(this, taskId));

}
}

4. 测试案例

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootTest
class SpringActuatorDemoApplicationTests {

@Resource
private MyEventPublisher myEventPublisher;

@Test
void contextLoads() {
myEventPublisher.publishEvent(10L);
}

}

image.png

最后

Spring 源码框架学习主要是学习他的一些设计模式还有架构的思想运用到自己的项目中, 比如你在做一些基础中间件项目的时候就需要考虑到Spring提供的一系列扩展点,Spring框架的生命周期,我们定义的Bean在什么时候加载最合适。如何保证加载配置的正确性等等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python matplotlib 绘制直方图 复习回顾 1

发表于 2021-11-14

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

复习回顾

经过前面对 matplotlib 模块从底层架构、基本绘制步骤等学习,我们已经学习了折线图、柱状图的绘制方法。

  • matplotlib 模块概述:对matplotlib 模块常用方法进行学习
  • matplotlib 模块底层原理:学习matplotlib 模块脚本层、美工层及后端层
  • matplotlib 绘制折线图:总结折线图相关属性和方法
  • matplotlib 绘制柱状图:总结柱状图相关属性和方法

在分析数据的时候,我们会根据数据的特点来选择对应图表来展示,需要表示质量这一概念,需要用直方图。

直方图.png

本期,我们将学习matplotlib 模块绘制直方图相关属性和方法,Let‘s go~

  1. 直方图概述

  • 什么是直方图?

+ 直方图是一种可视化表示数据在连续间隔或者特定时间段内容的分布情况
+ 直方图又称为质量分布图,属于条行图的一种
+ 直方图x轴表示数据类型,纵轴表示分布情况,每个数据宽度可以任意变化
  • 直方图使用场景

+ 直方图用于概率分布,展示一组数据在指定范围内的出现概率
+ 可以用于展示数据分布频率情况
+ 用于众数、中位数的位置
+ 关注数据存在缺口或者异常值
  • 直方图绘制步骤

1. 导入matplotlib.pyplot模块
2. 准备数据,可以使用numpy/pandas整理数据
3. 调用pyplot.hist()绘制直方图
  • 案例展示

本次案例我们来分析公司员工的身高分布情况

+ 案例数据准备,使用numpy随机生成200个升高数据



1
2
3
python复制代码import numpy as np

x_value = np.random.randint(140,180,200)
+ 绘制直方图
1
2
3
4
5
6
7
8
9
python复制代码import matplotlib.pyplot as plt

plt.hist(x_value,bins=10)

plt.title("data analyze")
plt.xlabel("height")
plt.ylabel("rate")

plt.show()
![image.png](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/3bb90ee721db17bb3ec60301f2cb203134421b45130be6a5b1845fb39156dbec)
  1. 直方图属性

  • 设置颜色

+ 设置长条形颜色关键字:facecolor
+ 设置边框的颜色关键字:edgecolor
+ 颜色选择值


    - 使用颜色的英语单词如红色"red",黄色"yellow"
    - 使用颜色简称:红色"r",蓝色"b"
    - 使用rgb:格式(r,g,b),取值范围:0~1
  • 设置长条形数目

+ 关键字:bins
+ 可选项,默认为10
  • 设置透明度

+ 关键字:alpha
+ 默认为0,取值范围为0~1
  • 设置样式

+ 关键字:histtype
+ 取值说明



| 属性值 | 说明 |
| --- | --- |
| 'bar' | 柱状形数据并排,默认值 |
| 'barstacked' | 柱状形数据重叠并排 |
| 'step' | 柱状形颜色不填充 |
| 'stepfilled' | 填充的线性 |
  • 我们对第一节直方图添加柱状形不填充,边框颜色为红色
1
python复制代码plt.hist(x_value,bins=10,edgecolor="r",histtype="step")

image.png

  • 边框设置为红色,透明度为0.5
1
python复制代码plt.hist(x_value,bins=10,edgecolor="r",histtype="bar",alpha=0.5)

image.png

  1. 添加折线直方图

在直方图中,我们也可以加一个折线图,辅助我们查看数据变化情况

  • 首先通过pyplot.subplot()创建Axes对象
  • 通过Axes对象调用hist()方法绘制直方图,返回折线图所需要的下x,y数据
  • 然后Axes对象调用plot()绘制折线图
  • 我们对第一节代码进行改造一下
1
2
3
4
5
python复制代码fig,ax = plt.subplots()

n,bins_num,pat = ax.hist(x_value,bins=10,alpha=0.75)

ax.plot(bins_num[:10],n,marker = 'o',color="yellowgreen",linestyle="--")

image.png

  1. 堆叠直方图

我们有时候会对吧同样数据范围情况下,对比两组不同对象群体收集的数据差异

  • 准备两组数据:
1
2
3
4
python复制代码import numpy as np

x_value = np.random.randint(140,180,200)
x2_value = np.random.randint(140,180,200)
  • 直方图属性data:以列表的形式传入两组数据
  • 设置直方图stacked:为True,允许数据覆盖
1
python复制代码plt.hist([x_value,x2_value],bins=10,stacked=True)

image.png

  1. 不等距直方图

我们上述绘制的直方图都是等距的,我们可以指定一组数据传入bins属性

  • bins 关键字:指定直方图的柱状形个数
  • 更改上述的代码后,看看效果吧
1
2
python复制代码bin_num = [140,155,160,170,175,180]
plt.hist([x_value,x2_value],bins=bin_num,alpha=0.75,stacked=True)

image.png

  1. 多类直方图

我们在使用直方图查查看数据的频率时,有时候会查看多种类型数据出现的频率。

  • 这时候我们可以以列表的形式传入多种数据给hist()方法的x数据
1
2
3
python复制代码x_value = [np.random.randint(140,180,i) for i in [100,200,300]]

plt.hist(x_value,bins=10,edgecolor="r",histtype="bar",alpha=0.5,label=["A公司","B公司","C公司"])

image.png

总结

本期,我们对matplotlib模块中详细学习绘制各种直方图标相关属性和方法。在需要查看数据分布频率时,我们可以使用hist()方法绘制直方图,同时也可以添加折线来辅助查看

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

G1垃圾收集器详解(二)

发表于 2021-11-14

分区模型

G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位。

image.png

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。

在物理上不需要连续,则带来了额外的好处——-有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,既首先收集垃圾最多的分区。

依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。

收集集合(CSet):一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、survivor空间、或者老年代。比如说这俩个区域要被回收则就可以称之为CSet:

image.png

巨形对象Humongous Region

一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。

巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

已记忆集合Remember Set (RSet)

在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

RSet记录了其它Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet既可。

事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

image.png

这就出现了RSet,专门用来记录谁引用了我。

下面来看图说明:Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中了这两个引用。

image.png
可以看到,对于每一个Region都会有一个RSet。

G1 GC是在points-out【言外之意就是指向别人的关系】的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内。

这个Rset其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。举例来说,如果region A的RSet里有一项的key是region B,value里有index为1的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。

Snapshot At The Beginning(SATB):SATB是G1 GC在并发标记阶段使用的增量式的标记算法。

Per Region Table

RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常”受欢迎”,那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

1.稀少:直接记录引用对象的卡片索引
2.细粒度:记录引用对象的分区索引
3.粗粒度:只记录引用情况,每个分区对应一个比特位

由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flask 入门系列之 response 对象!

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战。

在 Flask 中,响应使用 Response 对象表示,响应报文中的大部分内容由服务器处理,一般情况下,我们只负责返回主体内容即可。在之前的文章中,我们了解到 Flask 会先匹配请求 url 的路由,调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容。

如果视图函数只返回一个元素的话,Flask 会创建 Response 对象,Response将该返回值作为主体内容,状态码默认为200,MIME 的类型为text/html,然后返回该 Response 对象。

其实视图函数可以返回最多由个元素组成的元组:响应主体、状态码、首部字段。我们也可以指定这三个元素的值。

1
2
3
4
5
6
7
8
9
python复制代码@app.route('/hello1')
def hello1():
return 'Hello 1'
# 等价于返回 Response('Hello 1', status=200, mimetype='text/html')


@app.route('/hello2')
def hello2():
return 'Hello 2', 201, {'my-headers': 'tigeriaf'}

上面hello2()视图函数返回一个元组,那么状态码 status 值会覆盖默认的200状态码,headers 可以是一个列表或者字典,作为额外的消息头。

其实我们也可以先构建 Response 响应对象,设置一些参数(比如状态码,响应头等)后,然后直接返回Response 响应对象。

1
2
3
4
5
python复制代码@app.route('/hello3')
def hello3():
response = make_response('hello 3', 202)
response.headers["my-headers"] = "tigeriaf"
return response

make_response()方法就是用来构建response对象的,第二个参数表示响应状态码,默认是200。

之前的案例都是返回的纯文本格式,但是在实际开发中一般都是返回 json 格式,那该怎么做呢?

我们都知道 Python 标准库中的json模块为程序提供了 JSON 格式支持,Flask 也在json包的基础上做了一些功能的封装,我们可以直接导入 Flask 的 json 包,将数据序列化为 json 类型返回,再把Response 对象的 MIME 类型修改为applcation/json即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码@app.route('/hello4')
def hello4():
data = {
"status": 0,
"message": "success",
"data":
{"name": "tigeriaf",
"age": 24
}
}
json_data = json.dumps(data)
response = make_response(json_data)
response.mimetype = "applcation/json"
return response

image.png
除此之外,Flask 还提供了更方便的jsonify()方法,我们仅需要传入数据或者参数等,它会对我们传入的数据进行序列化,转换成 JSON 字符串作为响应的主体,然后生成一个响应对象,并且自动设置 MIME 类型,如下:

1
2
3
4
5
6
7
8
9
10
11
python复制代码@app.route('/hello4')
def hello4():
data = {
"status": 0,
"message": "success",
"data":
{"name": "tigeriaf",
"age": 24
}
}
return jsonify(data)

可见,jsonify()方法大大简化了我们的代码,而且jsonify()方法接收多种形式的参数,可以传入普通参数,也可以传入关键字参数,使用非常方便。

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【分布式事务系列】Spring Cloud集成Seata 实

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

Seata配置有几个配置项需要注意:

  • seata.support.spring.datasource-autoproxy:true属性表示数据源自动代理开关,在order-service、account-service、repo-service中设置为true,在rest-web中设置为false,因为该项目并没有访问数据源,不需要代理
  • 如果注册中心为file,seata.service.grouplist需要填写Seata服务端连接地址,在默认情况下,注册中心为file,如果需要从注册中心上进行服务发现,可增加配置如下:
1
2
3
4
5
6
yml复制代码seata:
registry:
type: nacos
nacos:
cluster: default
server-addr: 192.168.216.128:8848
  • tx-service-group表示指定服务所属的事务分组,如果没有执行,默认使用spring.application.name加上-seata-service-group,需要注意这两项配置必须配置一项,否则会报错。

添加回滚日志表

分别在3个数据库seata-account、seata-repo、seata-order中添加一张回滚日志表,用于记录每个数据库表操作的回滚日志,当某个服务的事务出现异常时会根据该日志进行回滚。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE undo_log(
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '' ,
branch_id bigint(20) COMMENT '' ,
xid VARCHAR(200) COMMENT '' ,
context VARCHAR(250) COMMENT '' ,
rollback_info longblob COMMENT '' ,
log_status int(11) COMMENT '' ,
log_created DATETIME COMMENT '' ,
log_modified DATETIME COMMENT '' ,
PRIMARY KEY (id)
UNIQUE KEY 'ux_undo_log'('xid,'branch_id')
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT = '回滚日志表';
rest-web增加全局事务控制

修改rest-web项目的RestOrderServiceImpl,需要做以下两个操作:

  • 增加@GlobalTransactional全局事务注解
  • 模拟一个异常处理,当商品编号等于某个指定的值时抛出异常,触发整个事务的回滚。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Slf4j
@Service
public class RestOrderServiceImpl implements IRestOrderService{
@Reference
private IRepoService repoService;
@Reference
private IOrderService orderService;

@Override
@GlobalTransactional(timeoutMills=300000,name="rest-web")
public AjaxResult handleBusiness(OrderReq orderReq) throw Exception{
log.info("开始全局事务:xid={}",RootContext.getIp());
log.info("开始订单:"+orderReq);
ProductDto productDto = new ProductDto();
productDto.setProductCode(orderReq.getProductCode());
productDto.setCount(orderReq.getCount());
AjaxResult ajaxResult = repoService.decreaseRepo(productDto);
//创建订单
OrderDto orderDto = new OrderDto();
orderDto.setUserId(orderReq.getUserId());
orderDto.setOrderAmount(orderReq.getAmount());
orderDto.setOrderCount(orderReq.getCount());
orderDto.setProductCode(orderReq.getProductCode());
AjaxResult orderResult = orderServce.createOrder(orderDto);
if(orderReq.getProductCode().equals("20211113")){
throw new Exception("触发异常");
}
AjaxResult result = new AjaxResult();
result.put("data",orderResult.getData());
return result;
}
}

在异常触发的位置来看,如果没有引入分布式事务,即使出现了异常,由于库存扣减、订单创建、账户资金扣减等操作已经生效,所以数据无法被回滚,而在引入seata后,在异常出现后会触发各个事务分支的数据回滚,保证数据的正确性,如果配置正常,能完成事务回滚操作。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security 会话管理

发表于 2021-11-14

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」。

前言

由于HTTP协议是无状态协议,对于服务器而言每个请求都一样,缺少一个状态去区分请求是否来自于不同的用户,以便服务器提供不同的服务。
所以我们需要利用某种机制来记录不同用户的标识信息。这个机制就是Session
这个时候cookie就体现了它的重要作用

  • 当客户端首次请求服务端时
  • 服务端为该用户生成一个sessionId,并保存在cookie中,带回客户端,客户端保存这个cookie。
  • 之后客户端每次请求都带上这个cookie
  • 服务端可以很容易区分是来自哪个用户的请求。

但出于安全考虑,有时用户在浏览器中禁用cookie,这个时候可以利用URL重新,将sessionId拼接在重新的URL后面返回给已经授权的用户。

一、 防御会话固定攻击

会话攻击

  • 攻击者自己正常访问系统,系统给攻击者分配了一个sessionId
  • 攻击者拿着自己手上的sessionId伪造一个系统登录链接
  • 受害者利用链接登陆了,那么sessionId绑定了用户的信息
  • 攻击者可以利用手里的sessionId冒充受害者

当我们登录成功之后重新生成新的 **session,即可避免**

Spring Security自带该功能,并且自带的HTTP防火墙会帮我们拦截掉哪些拼接的不合法的URL

sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种:

  • none:不做任何变动,登录之后沿用旧的session。
  • newSession:登录之后创建一个新的session。
  • migrateSession:登录之后创建一个新的session,并将旧的session中的数据复制过来。
  • changeSessionId:不创建新的会话,而是使用由Servlet容器提供的会话固定保护。

默认已经启用migrateSession策略,如有必要,可以做出修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**", "/captcha.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
//AuthenticationDetailsSource
// .authenticationDetailsSource(myWebAuthenticationDetailsSource)
.loginPage("/myLogin.html")
// 指定处理登录请求的路径,修改请求的路径,默认为/login
.loginProcessingUrl("/mylogin").permitAll()
.failureHandler(new MyAuthenticationFailureHandler())
.and()
//增加自动登录功能,默认为散列加密
.rememberMe()
.userDetailsService(myUserDetailsService)
.tokenRepository(jdbcTokenRepository)
//设置sessionManagement策略
.and()
.sessionManagement()
.sessionFixation()
.none()
.and()
.csrf().disable();
//将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}

二、会话过期

可以通过配置会话过期策略

  • 过期跳转
1
2
java复制代码.sessionManagement()
.invalidSessionUrl("/")
  • 过期时间
1
2
3
4
5
yml复制代码# 单位秒,最低限制60秒,小于60会被修正为60
server:
servlet:
session:
timeout: 90

三、会话并发控制

  • 异地登录踢掉当前登录用户
1
2
3
java复制代码.sessionManagement()
//设置最大会话数为1
.maximumSessions(1)

在其它客户端重新登录会挤掉之前登录的账号,并且之前页面会显示

1
java复制代码This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

具体实现可看ConcurrentSessionControlAuthenticationStrategy类源码。

  • 已经登录,禁止异地登录
1
2
3
4
5
JAVA复制代码.sessionManagement()
//设置最大会话数为1
.maximumSessions(1)
//阻止新会话登录,默认为false
.maxSessionsPreventsLogin(true)

异地登录报错

1
2
3
4
5
json复制代码{
"error_code": 401,
"error_name":"org.springframework.security.web.authentication.session.SessionAuthenticationException",
"message": "请求失败,Maximum sessions of 1 for this principal exceeded"
}

看似好像已经没有问题,但是我们将原来登录的用户注销(通过请求/logout),然后再去登录,发现任然登不上
这是因为通过监听session的销毁来触发会话信息的表相关清理工作,但我们还没有注册过相关的监听器,所以导致Spring Security无法正常清理过期或已注销的会话。

在Servlet中,监听session相关事件的方法是实现HttpSessionListener接口,并在系统中注册该监听 器。

Spring Security在HttpSessionEventPublisher类中实现HttpSessionEventPublisher接口,并转化成 Spring的事件机制。

在Spring事件机制中,事件的发布、订阅都交由Spring容器来托管,我们可以很方便地通过注册 bean的方式来订阅关心的事件。

1
2
3
4
java复制代码@Bean
public HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}

注意:principals采用了以用户信息为 **key 的设计,必须覆写 User 的 hashCode 和 equals 两个方法**

四、集群会话解决方案

当系统采用集群部署时,通常请求会先集中在一个中间件上(Nginx),再通过其转发到对应服务上,达到负载均衡的目的。
这样就会出现,我在A服务已经登录过,但是当请求被转发到B的时候,用户又要重新登录,这就是典型的会话状态集群不同步问题。
常见解决方案:

  • session保持
    • 通常采用IP哈希负载策略将来自相同客户端的请求转发至相同的服务器上进行处理。
    • 存在一定程度的负载失衡
  • session复制
    • session复制是指在集群服务器之间同步session数据,以达到各个实例之间会话状态一致的做法。
    • 消耗数据带宽,还会占用大量的资源。
  • session共享
    • session 共享是指将 session 从服务器内存抽离出来,集中存储到独立的数据容器,并由各个服务器共享。
    • 独立的数据容器增加了网络交互,数据容器的读/写性能、稳定性以及网络I/O速度都成为性能的瓶颈。

五、整合Spring Session解决集群会话问题

session共享,本质上就是存储容器的变动
**Spring Session 支持多种类型的存储容器**

基于Redis整合

  • 为工程引入依赖
1
2
3
4
5
6
7
8
9
10
xml复制代码<!--spring session对接Redis必要依赖-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--spring boot对接Redis必要依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 之后就可以配置Spring Session了,主要是为Spring Security提供集群支持的会话注册表。
  • 修改配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springSecurityDemo?useUnicode=true&&characterEncoding=utf8&&useSSL=false&&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6379
database: 2
timeout: 1000s
session:
store-type: redis
timeout: 1800000
  • 重启项目登录
  • 通过Redis客户端查看

image-20201023145928025.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ElasticSearch7X安装及集群搭建+Kibana

发表于 2021-11-14

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

下载ES安装包

ElasticSearch下载地址:点我

我这里是下载的linux已经编译好的包,直接配置启动就行了,因为它自带了JDK等环境。

如果你想要使用你自己配置好的 Java 版本,需要设置 JAVA_HOME 环境变量 —— 参考

下载好之后,我们进行tar包解压

1
2
3
4
5
6
bash复制代码# 创建es文件夹
mkdir /mydata/elasticsearch
# 进入创建的文件夹
cd /mydata/elasticsearch
# 解压
tar -zxvf elasticsearch-7.15.1-linux-x86_64.tar.gz

解压后的目录组成:

1
2
3
4
5
6
7
8
9
10
11
shell复制代码.
├── bin # 二进制脚本存放目录,包括 elasticsearch 来指定运行一个 node,包括 elasticsearch-plugin 来安装 plugins
├── config # 包含了 elasticsearch.yml 配置文件
├── data # 节点上分配的每个 index/分片 的数据文件
├── lib
├── LICENSE.txt
├── logs
├── modules
├── NOTICE.txt
├── plugins # 插键文件存放的位置
└── README.textile

准备工作

不能使用 root 用户启动 es,否则会报错:

Caused by: java.lang.RuntimeException: can not run elasticsearch as root

我们需要新建一个操作es的用户:

1
2
3
4
5
bash复制代码# 创建用户
# 给用户分配组:useradd –g 用户组 用户名
useradd -g es es
# 修改密码
passwd es

为了让我们的es用户能更好的操作es应用,我们修改es的文件所有者

1
2
bash复制代码# 递归修改es文件所有者
chown -R es:es elasticsearch-7.15.1

ElasticSearch相关配置

单节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yml复制代码vim config/elasticsearch.yml

# 集群名称
cluster.name: my-es
# 启动地址,如果不配置,只能本地访问
network.host: 0.0.0.0
# 节点名称
node.name: node-130
# 数据存储地址
path.data: /mydata/elasticsearch/elasticsearch-7.15.1/data
# 日志存储地址
path.logs: /mydata/elasticsearch/elasticsearch-7.15.1/logs
# 节点列表
discovery.seed_hosts: ["192.168.200.130"]
# 初始化时master节点的选举列表
cluster.initial_master_nodes: ["node-130"]
# 对外提供服务的端口
http.port: 9200
# 内部服务端口
transport.port: 9300
# 跨域支持
http.cors.enabled: true
# 跨域访问允许的域名地址(正则)
http.cors.allow-origin: /.*/

集群

192.168.200.130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yml复制代码vim config/elasticsearch.yml

# 集群名称
cluster.name: my-es
# 启动地址,如果不配置,只能本地访问
network.host: 0.0.0.0
# 节点名称
node.name: node-130
# 数据存储地址
path.data: /mydata/elasticsearch/elasticsearch-7.15.1/data
# 日志存储地址
path.logs: /mydata/elasticsearch/elasticsearch-7.15.1/logs
# 节点列表
discovery.seed_hosts: ["192.168.200.130","192.168.200.131","192.168.200.132"]
# 初始化时master节点的选举列表
cluster.initial_master_nodes: ["node-130"]
# 对外提供服务的端口
http.port: 9200
# 内部服务端口
transport.port: 9300
# 跨域支持
http.cors.enabled: true
# 跨域访问允许的域名地址(正则)
http.cors.allow-origin: /.*/

192.168.200.131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yml复制代码vim config/elasticsearch.yml

# 集群名称
cluster.name: my-es
# 启动地址,如果不配置,只能本地访问
network.host: 0.0.0.0
# 节点名称
node.name: node-131
# 数据存储地址
path.data: /mydata/elasticsearch/elasticsearch-7.15.1/data
# 日志存储地址
path.logs: /mydata/elasticsearch/elasticsearch-7.15.1/logs
# 节点列表
discovery.seed_hosts: ["192.168.200.130","192.168.200.131","192.168.200.132"]
# 初始化时master节点的选举列表
cluster.initial_master_nodes: ["node-130"]
# 对外提供服务的端口
http.port: 9200
# 内部服务端口
transport.port: 9300
# 跨域支持
http.cors.enabled: true
# 跨域访问允许的域名地址(正则)
http.cors.allow-origin: /.*/

192.168.200.132

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yml复制代码vim config/elasticsearch.yml

# 集群名称
cluster.name: my-es
# 启动地址,如果不配置,只能本地访问
network.host: 0.0.0.0
# 节点名称
node.name: node-132
# 数据存储地址
path.data: /mydata/elasticsearch/elasticsearch-7.15.1/data
# 日志存储地址
path.logs: /mydata/elasticsearch/elasticsearch-7.15.1/logs
# 节点列表
discovery.seed_hosts: ["192.168.200.130","192.168.200.131","192.168.200.132"]
# 初始化时master节点的选举列表
cluster.initial_master_nodes: ["node-130"]
# 对外提供服务的端口
http.port: 9200
# 内部服务端口
transport.port: 9300
# 跨域支持
http.cors.enabled: true
# 跨域访问允许的域名地址(正则)
http.cors.allow-origin: /.*/

这里的配置基本上和单机启动一样,只不过在discovery.seed_hosts中指定了节点列表。然后将每个节点启动即可,然后分别访问各服务的9200端口,查看服务是否正常启动。

同时,也可以访问任意节点的http://host:9200/_cluster/health查看集群状态:green表示正常,yellow表示警告,red表示异常

注:并不是每个节点都需要配置cluster.initial_master_nodes

JVM 配置

JVM 参数设置可以通过 jvm.options 文件(推荐方式)或者 ES_JAVA_OPTS 环境变量来修改。

jvm.options 位于

  • $ES_HOME/config/jvm.options 当通过 tar or zip 包安装
  • /etc/elasticsearch/jvm.options 当通过 Debian or RPM packages

官网也介绍了如何设置堆大小。

默认情况,ES 告诉 JVM 使用一个最小和最大都为 1GB 的堆。但是到了生产环境,这个配置就比较重要了,确保 ES 有足够堆空间可用。

ES 使用 Xms(minimum heap size) 和 Xmx(maxmimum heap size) 设置堆大小。你应该将这两个值设为同样的大小。

Xms 和 Xmx 不能大于你物理机内存的 50%。

设置的示例:

1
2
shell复制代码-Xms2g 
-Xmx2g

测试

1
2
3
4
bash复制代码#启动es
./bin/elasticsearch &
#测试es启动是否成功
curl -get localhost:9200

ES-FAQ

Q1:[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

1
2
shell复制代码echo "vm.max_map_count=262144" > /etc/sysctl.conf
sysctl -p

Q2:max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

1
2
3
4
5
6
7
shell复制代码sudo vim /etc/security/limits.conf
# 加入以下内容
* soft nofile 300000
* hard nofile 300000
* soft nproc 102400
* soft memlock unlimited
* hard memlock unlimited

修改了limits.conf,不需要重启,重新登录即生效。

查看当前用户的软限制

命令:ulimit -n 等价于 ulimit -S -n

查看当前用户的硬限制

命令:ulimit -H -n

Q3:master_not_discovered_exception

主节点指定的名字要保证存在,别指定了不存在的节点名

其他配置

我们使用浏览器连接es可能无法连接上,这时候看一看是不是防火墙端口是否放行

1
2
3
4
5
6
7
8
9
10
bash复制代码# 查看防火墙状态
systemctl status firewalld
#查询端口是否开放
firewall-cmd --query-port=9200/tcp
firewall-cmd --query-port=9300/tcp
#开放端口
firewall-cmd --permanent --add-port=9200/tcp
firewall-cmd --permanent --add-port=9300/tcp
#重新载入
firewall-cmd --reload

ik分词器

ElasticSearch应用时,我们常常会使用到中文,这样ElasticSearch原来的分词器就不够用了,需要安装一个中文分词器,用的多的就是IK分词器,下载地址(下载.zip包):github.com/medcl/elast…

下载好之后,在ElasticSearch主目录下的plugins目录新建一个目录,然后将下载好的ik分词器压缩包放进去,再使用unzip解压: 

1
2
3
4
5
6
7
8
bash复制代码# 在ElasticSearch主目录下的plugins目录新建一个目录:analysis-ik
mkdir plugins/analysis-ik
# 将下载好的ik分词器压缩包放进去
mv elasticsearch-analysis-ik-7.15.1.zip plugins/analysis-ik/
# 进入新建的analysis-ik目录进行解压
cd plugins/analysis-ik
# 解压
unzip elasticsearch-analysis-ik-7.15.1.zip

做完之后重启ElasticSearch就可以了

Kibana安装

Kibana下载地址:www.elastic.co/downloads/p…

同样的,下载编译好的包(7.15.1)

然后解压,然后修改config/kibana.yml,添加ElasticSearch的节点配置

1
2
3
4
5
6
7
8
9
yml复制代码# 服务端口,默认5601
server.port: 5601
# 启动地址,默认localhost,如果不修改,那么远程无法访问
server.host: 0.0.0.0
# elasticsearch集群地址,旧版本是elasticsearch.url
elasticsearch.hosts: ["http://192.168.200.130:9200","http://192.168.200.131:9200","http://192.168.200.132:9200"]
# 如果ES有设置账号密码,则添加下面的账号密码设置
#elasticsearch.username: username
#elasticsearch.password: passwor

然后就可以启动了:

1
2
3
4
bash复制代码#修改文件所有者
chown -R es:es kibana-7.15.1-linux-x86_64
# 启动
kibana-7.12.0-linux-x86_64/bin/kibana

防火墙放行端口

1
2
3
4
bash复制代码#开放端口
firewall-cmd --permanent --add-port=5601/tcp
#重新载入
firewall-cmd --reload

启动之后,访问http://ip:5601就能访问到kibina了,kibina内部功能功能很多,像绘制图表仪表盘等等,还有很多模拟数据,可自行了解,就是说kibina主要是通过连接ElasticSearch来查询数据,然后将数据统计汇总呈现出图形化的界面来方面我们进行数据分析的。

不过我们开发常用的就是它的Dev-Tools了,用来发送Restfull的请求来访问操作ElasticSearch:

进入dev-tools后就可以自行操作了:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

FMI20 ———— 联合仿真 一、开题 二、数学描述 三

发表于 2021-11-14

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战


本文是来自于FMI2.0规范的第四章联合仿真的译文,基于机翻与个人理解修订而成。

一、开题

本文定义了函数模拟接口(FMI)用于在联合仿真环境中联合两个或更多仿真模型(FMI进行共模)。联合仿真是一种相当常见的方法,用于仿真仿真的技术系统和相关的物理现象,着重于稳定性(time-dependent)问题。

联合模拟(coupling simulation ):应用共享网格技术,解决数字化环境下多学科CAE视图模型耦合仿真的难题。
时间依赖性:time-dependent

联合仿真的设计是为了与多个子系统模型耦合,它们已由其仿真器及其求解器导出为可运行代码;还可用于仿真工具的耦合(仿真器耦合,工具耦合)。

image.png

在工具耦合情况下,仿真工具提供的FMU实现类将FMI函数的调用包装为API调用(C->其它语言)。此外,还需要使用仿真工具来对FMU进行联合仿真。最常见的是,基于工具耦合的联合仿真在分布式硬件上实现了由不同计算机处理不同的子系统,其中包括不同的操作系统(集群计算机,计算机农场,不同位置的计算机)。

子系统之间的数据交换和通信通常使用一种网络通信技术(例如MPI,TCP / IP)完成。该通信层的定义不是FMI标准的一部分,但可以使用FMI来实现分布式联合仿真方案,如下图所示

image.png

主机必须实现通信层,用于建立网络通信的其他参数(例如,远程计算机的标识,端口号,用户帐户)将通过主机的GUI进行设置,而这些数据都不会通过FMI API传输。

二、数学描述

  1. 基础

联合仿真利用了仿真过程中所有的阶段都会遇到的耦合问题(模块化结构),从针对不同仿真工具(可以是功能强大的仿真器以及简单的C程序)中各个子系统单独的模型设置和预处理开始。

在进行时间积分时,将限制子系统间的数据交换只能在离散的通信点tci中进行, 对所有子系统独立执行仿真。对于模拟耦合,子系统中的仿真工具独自完成仿真数据的可视化和后处理。在不同的上下文中,通信点tci、通信步长tci->tci+1 以及通信步长总个数hci:=tci+1- tci 分别称为取样点(同步点)、宏步长和取样率。FMI中用于联合仿真的术语“通信点”是指联合仿真环境中子系统之间的通信,不应与用于将仿真结果保存到文件中的输出点混合使用,

取样:指的是取样的时间点
宏步长:指的是两次取样的时间间隔
取样率(Sampling Rate)是指在数码音频和视频技术应用中,当进行模拟/数码转换时,每秒钟对模拟信号进行取样时的快慢次数。例如,CD和MD的取样率为44.1kHz,表示每秒钟对模拟音频信号进行了44100次取样。取样率越高,转换的精度就越高,重放出的模拟信号波形就越接近原始的模拟信号波形。取样率的高低。决定了所能转换模拟信号的频率上限。

FMI为联合仿真提供了一个接口标准,提供了与时间依赖性相关的耦合系统的解决方案,该耦合系统由时间连续(由平稳差分方程描述的模型组件)或时间离散(由差分方程描述的模型组件,例如离散控制器)的子系统组成。在耦合系统的块表示中,子系统由具有(内部)状态变量x(t)*的块表示,这些块通过子系统输入 *u(t) 和子系统输出 y(t) 连接到其他耦合的子系统(模块)的模块。在这个框架中,子系统间的物理连接由所有子系统的输入 u(t) 和输出 y(t) 间的数学耦合条件表示。

image.png

差分方程:包含未知函数的差分及自变数的方程。在求微分方程的数值解时,常把其中的微分用相应的差分来近似,所导出的方程就是差分方程。通过解差分方程来求微分方程的近似解,是连续问题离散化的一个例子。

为了进行联合仿真,必须实现两个基本函数组:

  • 子系统之间的数据交换函数
  • 用于算法问题的函数,以同步所有子系统的仿真,并从初始时间tc0:= tstart到结束时间tcN:=tstop执行通信步长tci->tci+1。

在用于联合仿真的FMI中,这两个函数都在一个软件组件中实现,即联合仿真主机(Master)。子系统(Slave)之间的数据交换仅通过主站进行,从站之间没有直接通信;主函数可以通过特殊的软件工具(单独的仿真背板)或所涉及的仿真工具之一来实现。最常见的是,耦合系统可以在嵌套的协同仿真环境中进行仿真,并且用于协同仿真的 FMI 适用于层次结构的每个级别。

用于联合仿真的FMI定义了在联合仿真环境中主机与所有从机(子系统)之间进行通信的接口规则。最常见的主站算法是在每个通信点tci停止所有从站的仿真(时间积分)、收集所有子系统的输出y(tci)、调整子系统输入u(tci)将这些子系统输入分配给从站,然后继续与下一个交互步骤tc<sub>i</sub>->tc<sub>i+1</sub> = tc<sub>i</sub> + hc的(协同)仿真,交互步长为固定参数hc。在每个从站中,适合的求解器应当被用于给定交互步长 tci->tci+1 的子系统之一求积分。最简单的联合仿真算法通过冻结数据u(tci)来近似 tci≤t <tci + 1的(未知)子系统输入u(t),(t> tci)。用于联合仿真的FMI支持这种经典的暴力解法以及更复杂的主算法,旨在支持一类非常通用主算法,但它本身并未定义主算法。

从站支持更复杂的主站算法的能力,在从站的XML描述中包含一组功能标志(capability flags)。典型的例子是

  • 具有处理可变通信步长hci的 能力
  • 具有使用减小的通信步长重复执行被拒绝的通信步长tci≤t <tci + 1的 能力
  • 提供支持插值的输出时间的导数
  • 提供雅可比行列式的能力

用于联合仿真的 FMI 仅限于具有以下属性的从站:

  • 所有计算参数值 v(t) 是在预先定义的时间间隔(tstart <= t <= tstop)中的时间依赖函数(fmi2SetupExperiment,stopTimeDefined=fmi2True)
  • 通常,所有计算(模拟)都随着时间的增加而进行。当前时间t从tstart 到 tstop逐步允许。从站的算法可能拥有属性在[tstart, tstop]整个时间间隔或部分间隔中重复仿真。
  • 可以为从站提供一个时间值tci(tstart <= tci <= tstop)
  • 当时间到达tci,从站能够中断模拟
  • 在中断仿真期间,从站(独立求解器)可以从输入u(tci)获取值,将值发送到输出 y(tci)。
  • 每当从站中的模拟中断时,新的时间取值tci+1(tci <= tci+1 <= tstop)可以给出时间子区间(tci < t <= tci+1)
  • 子间隔长度hci是第i个通讯步的长度(通信步长必须大于零):hci=tci+1-tci

用于联合仿真的FMI相关流程如下

  • 该流程从实例化和初始化(准备好所有从设备进行计算,建立通信链接)开始
  • 然后进行仿真(强制从设备模拟通信step)
  • 最后在完成时关闭
  1. 数学模型

本节包含联合仿真 FMU 的正式数学模型,可以作出以下基本假设:

  • 从模拟器被主模拟器视为纯粹的采样数据系统,可以是
+ “真实的”采样数据系统(离散控制器;输入和输出可以是Real,Integer,Boolean,String或枚举类型。此类变量的定义为variability =“ discrete”;最小FMU外部可访问的采样周期由元素DefaultExperiment中的属性stepSize定义)
+ 混合ODE被集成在通信点间(对时间连续系统的采样存取),在其中内部事件可能发生并被处理,但在FMU外部看不到。在此假定此混合ODE的所有输入和所有输出均为实际信号(以variability =“ continuous”定义)
+ 上述系统的组合
  • 主站和从站之间的通信仅在一组离散的时刻进行,这些时间点称为通信点。

FMI联合仿真模型相关变量描述:

  • t:自变量time∈ℝ(causality =“independent”定义的变量);第i个通信点表示为t=tci,通信步长表示为hci = tci+1 - tci
  • v:所有暴露变量的向量
  • p:仿真过程中不变的参数
  • 不带下标符号引用p 表示独立参数(causality = “parameter”);
  • 因变参数 pcalculated(causality = “calculatedParameter”)
  • 可调参数 ptune(causality = “parameter”、variability= “tunable”)
  • u(tci):输入变量
  • y(tci):输出变量
  • w(tci):不能用于FMU连接的FMU的局部变量,通过causality = “local”定义
  • xc(t):用实际连续时间变量的向量表示连续时间状态
  • xd(t):离散时间变量
  • .xd(t):上一个离散时间变量

存在如下定义:

  • 在通信点上,主站向从站提供通用输入
  • 从机向主机提供通用输出
  • Initialization:从站是一个采样数据系统,其内部状态(连续时间或离散时间都没关系)需要初始化为t=tc0。这是通过辅助函数执行的[此关系在 下的xml文件中定义]。计算FMI联合仿真模型的解意味着将解过程分为两个阶段,并且在每个阶段中使用不同的方程式和解方法。可以根据以下模式对阶段进行分类
    • Initialization Mode:如果从站与其他模型循环连接,则可以对FMU方程进行迭代。在这种模式下,将求解代数方程
    • Step Mode:通过对常微分、代数和离散方程进行数值求解,此模式用于计算通信点上所有(实际)连续时间和离散时间变量的值。如果从站与其他模型循环连接,则不可能对FMU方程进行迭代

下表中使用的函数fmi2SetXXX是fmi2SetReal,fmi2SetBoolean,fmi2SetInteger和fmi2SetString的缩写。函数fmi2GetXXX是函数fmi2GetReal,fmi2GetBoolean,fmi2GetInteger和fmi2GetString的缩写

方程式 FMI函数
初始化模式之前的方程式(状态机中的“实例化”)
设置 i=0并设置自变量的tci fmi2SetupExperiment
设置变量vinitial=exact以及vinitial=approx fmi2SetXXX
初始化模式之后的方程式
在 t=t0时进入初始化模式(激活初始化,离散时间和连续时间方程式) fmi2EnterInitializationMode
设置变量vinitial=exact(包括初始值为xc,initial=exact独立参数p和连续时间状态) fmi2SetXXX
设置连续时间以及离散时间输入 uc+d(tc0)以及可以选择设置连续时间导数的输入ucj(tc0) fmi2SetXXX、fmi2SetRealInputDerivative
vInitialUnknows:=finit(uc,ud,t0,vinitial=exact) fmi2GetXXX、fmi2GetDirectionalDerivativ
退出初始化模式(停用初始化方程式) fmi2ExitInitializationMode
Step模式下的方程式(状态机中的“ stepComplete”,“ stepInProgress”
设置独立的可调参数ptune(不要设置其他参数pother) fmi2SetXXX
设置参数连续时间和离散时间输入 uc+d(tci)以及可选参数连续时间输入 uc(j)(tci)的导数 fmi2SetXXX、fmi2SetRealInputDerivative
tci+1:=tci+hci (yc+d,yc(j),wc+d)tci+1 := fdoStep(uc+d,uc(j),tci,hci,ptune,pother)tcitci:=tci+1其中fdoStep也是内部变量的函数 fmi2DoStepfmi2GetXXXfmi2GetRealOutputDerivativesfmi2GetDirectionalDerivative

三、FMI应用程序编程接口

本节包含用于从C程序访问联合仿真从站的输入/输出数据和状态信息的接口说明。

  1. 输入/输出值和参数的传输

输入变量、输出变量以及变量 通过本节中定义的fmi2GetXXX和fmi2SetXXX函数进行传输。

为了使从机能够在通信步长间使用连续实数作为输入的插值,可以提供输入对时间的导数;为了允许更高阶的插值,还可以设置更高阶的导数。从机是否能够进行插值由函数属性canInterpolateInputs提供(假如输入为速度,那么需要提供速度对时间的导数,即速率;那么在两个时间点间,可以使用速率*时间t求出时间t时的速度)。以下是相关函数:

fmi2SetRealInputDerivatives:

1
2
3
4
5
6
css复制代码-- 设置实数输入变量在第n个时间点的导数
fmi2Status fmi2SetRealInputDerivatives(fmi2Component c, constfmi2ValueReference vr[], size_t nvr, constfmi2Integer order[], constfmi2Real value[])
“vr”:包含变量值的向量,定义了需要设置导数的变量
“nvr”:向量维数
“order[]”:导数的阶(1表示一阶导数,不允许0)。
“value[]”:导数的向量

使用该函数的限制与fmi2SetReal函数相同。输入及其导数是在通信时间步开始时设置的。为了允许实际输出变量作为其他从站输入在通讯步间进行插值/逼近,可以读取输出相对于时间的导数。从站是否能够提供输出导数由函数标志MaxOutputDerivativeOrder(无符号整数类型)决定;它定义了输出导数的阶数。如果实际阶数较低(因为积分算法的阶数较低),则检索到的值为0。例如:内部多项式为1阶,并且主机请求输出变量的二阶导数,则从机将返回零。

fmi2GetRealOutputDerivatives:

1
2
3
4
5
6
css复制代码-- 检索输出值的第n个导数
fmi2Status fmi2GetRealOutputDerivatives (fmi2Component c, constfmi2ValueReference vr[], size_t nvr, constfmi2Integer order[], fmi2Real value[]);
“vr”:包含变量值的向量,定义了需要设置导数的变量
“nvr”:向量维数
“order[]”:导数的阶(1表示一阶导数,不允许0)。
“value[]”:导数的向量

返回的输出变量对应于当前从机时间,例如在成功执行fmi2DoStep后,返回值与通信时间步的结束有关。

  1. 计算

2.1 fmi2DoStep

时间步长的计算由以下函数控制

1
2
3
4
scss复制代码	fmi2Status fmi2DoStep(fmi2Component c, 
fmi2Real currentCommunicationPoint,
fmi2Real communicationStepSize,
fmi2Boolean noSetFMUStatePriorToCurrentPoint)

该函数用于计算时间步长,参数currentCommunicationPoint表示主机的当前通信点(tci)、参数communicationStepSize表示通信步长(hci > 0.0)。从站必须集成,直到tci+1 = tci +hci.【调用环境定义了通信点,fmi2DoStep必须求精确到tci +hci 的积分来与这些点进行同步。而如何实现此目标取决于fmi2DoStep】。

在调用fmi2ExitInitializationMode函数之后,第一次调用fmiDoStep时,参数currentCommunicationPoint必须等于由fmi2SetupExperiment设置的startTime参数。【参数currentCommunicationPoint不会被正式使用,定义它是为了解决主节点与从节点间FMU状态不匹配的问题:由前一个的fmi2DoStep或fmi2SetFMUStatecall函数定义的参数currentCommunicationPoint、从节点中的FMU状态必须彼此一致】

例如,如果从节点未按照上述要求对自变量使用更新公式(tci+1 = tci +hci),而是在内部使用自己的更新公式,例如tcs,i+1 = tcs,i +hcs,i (fmi2DoStep函数中,tci=currentCommunicationPoint);则从节点可以使用时间增量hcs,i :=(tci-tcs,i)+hci(替代hcs,i :=hci)去避免主节点时间tci+1和从节点内部时间tcs,i+1偏差过大。

在此次模拟运行中,如果在currentCommunicationPoint到预先设定的时刻之间不再调用fmi2SetFMUState函数,则设置参数noSetFMUStatePriorToCurrentPoint = “fmi2True” [从节点可以使用该标志刷新结果缓冲区]。

函数返回值:

  • fmi2OK —— 直到通信步长(主时间步)结束,每一子步长计算都成功。
  • fmi2Discard —— 从节点只成功计算出通讯步长的部分间隔,主节点可以调用相应的 fmi2GetXXXStatus 函数来获取更多信息。如果可能的话,主节点应使用更短的通信步长重复模拟。只有函数fmi2GetFMUState当前(失败)步骤开始时记录了 FMU 状态,才能重复步长;这是通过调用 fmi2SetFMUState 并随后使用新的communicationStepSize 调用fmi2DoStep 来实现的。
  • fmi2Error —— FMU 遇到错误。该 FMU 实例无法继续模拟。如果其中一个函数返回 fmi2Error,则可以尝试通过调用 fmi2SetFMUstate 从以前存储的 FMU 状态重新启动仿真。
  • fmi2Fatal —— 所有 FMU 实例的模型计算都无法修复(例如,由于在执行 fmi 函数期间出现访问冲突或整数除以零等运行时异常)
  • fmi2Pending —— 从节点异步执行,即开始执行时立即返回。如果从节点返回这个状态,主节点必须调用函数fmi2GetStatus(...,fmi2DoStep,...) 来确定从节点是否完成;另一种方法是等待slave调用回调函数fmi2StepFinished;而调用函数fmi2CancelStep可以取消当前的计算。如果返回这个值,在fmi2DoStep执行期间,不允许调用其它函数。

如果fmi2DoStep返回了fmi2Pending,则可以调用fmi2CancelStep函数停止当前的异步执行。

1
scss复制代码fmi2Status fmi2CancelStep(fmi2Component c);

如果用户或从节点之一停止运行联合仿真,则主节点调用次函数。之后,只允许调用fmi2Reset或fmi2FreeInstance。

  1. 获取从节点的状态信息

通过以下函数向主节点反馈从节点的当前状态信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码fmi2Status fmi2GetStatus(fmi2Component c,const fmi2StatusKind s, fmi2Status* value); 
fmi2Status fmi2GetRealStatus (fmi2Component c, const fmi2StatusKind s,fmi2Real* value);
fmi2Status fmi2GetIntegerStatus(fmi2Component c, const fmi2StatusKind s, fmi2Integer* value);
fmi2Status fmi2GetBooleanStatus(fmi2Component c, const fmi2StatusKind s, fmi2Boolean* value);
fmi2Status fmi2GetStringStatus (fmi2Component c, const fmi2StatusKind s, fmi2String* value);

-- 通知主节点,模拟运行的实际状态(响应正文)
typedefenum{
fmi2DoStepStatus,
fmi2PendingStatus,
fmi2LastSuccessfulTime,
fmi2Terminated
} fmi2StatusKind;

-- (响应)
typedef enum {
fmi2OK, // 通信步骤已成功计算到结束
fmi2Warning, // 警告
fmi2Discard, // 从节点成功计算出通讯步骤的一个子间隔
fmi2Error, // 错误
fmi2Fatal, // 发生错误,导致FMU无法修复损坏
fmi2Pending // 异步
} fmi2Status;

通知主节点有关模拟运行的实际状态,由参数fmi2StatusKind决定要返回的状态信息。从节点可以提供哪些状态信息取决于从节点的能力。如果需要从节点无法检索的状态,它将返回状态fmi2Discard。

结构体fmi2StatusKind包含以下状态:

状态 类型 描述
fmi2DoStepStatus fmi2Status 当fmi2DoStep函数返回fmi2Pending时可以调用。如果计算未完成,该函数将提供fmi2Pending。否则,该函数将返回异步执行的fmi2DoStep调用的结果。
fmi2PendingStatus fmi2String 当fmi2DoStep函数返回fmi2Pending时可以调用。该函数提供一个字符串,该字符串告知当前正在运行的异步fmi2DoStep计算的状态
fmi2LastSuccessfulTime fmi2Real 返回上一个成功完成的通信步骤的结束时间。可以在fmi2DoStep(…)返回fmi2Discard之后调用。
fmi2Terminate fmi2Boolean 如果从节点希望终止仿真,则返回true。可以在fmi2DoStep(…)返回fmi2Discard之后调用。使用fmi2LastSuccessfulTime确定从节点终止的时刻
  1. 主节点到从节点调用次序的状态机

以下状态机定义了fmi规范所支持的调用序列

image.png

状态机的每个状态对应于模拟的某个特定阶段,如下所示:

  • instantiated(实例化):在这种状态下,可以设置起始值和估计值(变量属性initial = “exact” or “approx.”)、设置导数、设置仿真条件
  • Initialization Mode(初始化模式):在这种状态下,方程式可用于确定所有输出(以及导出工具到处的其他可选变量)、可以通过fmi2GetXXX调用变量是在xml文件中的 下定义的(causality=”output”)变量、可以设置initial=”exact”的变量以及具有variability=”input”的变量
  • slaveInitialized(从节点初始化):在这种状态下,将对从节点进行初始化并执行联合仿真计算;使用函数“ fmi2DoStep”执行计算直到下一个通讯点。根据返回值,从节点处于不同的状态
+ step complete
+ step failed
+ step canceled
  • terminated(终止):在这种状态下,可以获取仿真最后时刻的解。

注:在初始化模式下,可以根据xml文件中的元素<ModelStructure>、 <InitialUnknowns>定义模型结构,使用fmi2SetXXX设置输入变量,并使用fmi2GetXXX获取输出变量。【例如,如果一个输出y1取决于两个输入u1,u2,则必须先设置这两个输入,然后才能获取y1。 如果另外输出y2取决于输入u3,则可以设置u3,然后再获取y2;可以通过使用适当的数值算法来处理初始化模式下连接的FMU上的人工或“真实”代数环。】

“slaveInitialized”状态还有一个额外的限制,在fmi2SetXXX函数之后如果没有fmi2DoStep调用,则不允许调用fmi2GetXXX函数。

与模型交换类型的FMI相反,为了避免缓存存在不同的解释,联合仿真的fmi2DoStep函数将执行实际的计算,而不是使用fmi2GetXXX函数。因此,模型交换时调用的fmi2GetXXX、fmi2SetXXX序列无法处理通讯点处的虚拟代数环。

四、FMU联合仿真(CoSimulation)

  1. 标签的定义

联合仿真功能在模型描述文件中的 标签定义,相关定义如下:

image.png

相关结构如下:

  • CoSimulation
    • modelIdentifier:类名缩写
    • needsExecution:决定是否需要外部工具执行模型
    • canHandleVariableCommunicationStepSize:从节点可以处理可变的通信步长。对于每个校准,通信步长(fmi2DoStep函数的参数communicationStepSize)在每次调用中不需要固定不变
    • canInterpolateInputs:从节点能够对连续输入插值
    • maxOutputDerivativeOrder:从节点能够提供最大阶数的输出导数
    • canRunAsynchronuously :异步
    • canBeInstantiatedOnlyOncePerProcess:单FMU单实例(如果需要多个实例,则必须在不同的进程中实例化 FMU)
    • canNotUseMemoryManagementFunctions:如果为true,则从节点使用自身的函数进行内存分配和释放;忽略fmi2Instantiate中给出的回调函数 allocateMemory、freeMemory
    • canGetAndSetFMUstate:如果为true,则 仿真环境可以查询和恢复内部FMU状态(即支持fmi2GetFMUstate,fmi2SetFMUstate,fmi2FreeFMUstate)
    • canSerializeFMUstate:如果为true,则仿真环境可以序列化内部FMU状态,即FMU支持fmi2SerializedFMUstateSize,fmi2SerializeFMUstate,fmi2DeSerializeFMUstate。如果是这种情况,则标记canGetAndSetFMUstate也必须为true。
    • providesDirectionalDerivative:如果为true,在通信点时可以使用fmi2GetDirectionalDerivative(..)计算方程的方向导数。
    • SourceFiles:资源文件
  1. 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
xml复制代码<?xml version="1.0" encoding="UTF8"?>
<fmiModelDescription fmiVersion="2.0" modelName="MyLibrary.SpringMassDamper" guid="{8c4e810f-3df3-4a00-8276-176fa3c9f9e0}" description="Rotational Spring Mass Damper System" version="1.0" generationDateAndTime="2011-09-23T16:57:33Z" variableNamingConvention="structured">

<CoSimulation modelIdentifier="MyLibrary_SpringMassDamper" canHandleVariableCommunicationStepSize="true" canInterpolateInputs="true"/>

<UnitDefinitions>
<Unit name="rad">
<BaseUnit rad="1"/>
<DisplayUnit name="deg" factor="57.2957795130823"/>
</Unit>

<Unit name="rad/s">
<BaseUnit s="-1" rad="1"/>
</Unit>

<Unit name="kg.m2">
<BaseUnit kg="1" m="2"/>
</Unit>
</UnitDefinitions>

<TypeDefinitions>
<SimpleType name="Modelica.SIunits.Inertia">
<Real quantity="MomentOfInertia" unit="kg.m2" min="0.0"/>
</SimpleType>

<SimpleType name="Modelica.SIunits.Torque">
<Real quantity="Torque" unit="N.m"/>
</SimpleType>

<SimpleType name="Modelica.SIunits.AngularVelocity">
<Real quantity="AngularVelocity" unit="rad/s"/>
</SimpleType>

<SimpleType name="Modelica.SIunits.Angle">
<Real quantity="Angle" unit="rad"/>
</SimpleType>
</TypeDefinitions>

<DefaultExperiment startTime="0.0" stopTime="3.0" tolerance="0.0001"/>

<ModelVariables>
<ScalarVariable name="inertia1.J" valueReference="1073741824" description="Moment of load inertia" causality="parameter" variability="fixed">
<Real declaredType="Modelica.SIunits.Inertia" start="1"/>
</ScalarVariable>

<ScalarVariable name="torque.tau" valueReference="536870912" description="Accelerating torque acting at flange (= -flange.tau)" causality="input">
<Real declaredType="Modelica.SIunits.Torque" start="0"/>
</ScalarVariable>

<ScalarVariable name="inertia1.phi" valueReference="805306368" description="Absolute rotation angle of component" causality="output">
<Real declaredType="Modelica.SIunits.Angle" />
</ScalarVariable>

<ScalarVariable name="inertia1.w" valueReference="805306369" description="Absolute angular velocity of component (= der(phi))" causality="output">
<Real declaredType="Modelica.SIunits.AngularVelocity" />
</ScalarVariable>
</ModelVariables>

<ModelStructure>
<Outputs>
<Unknown index="3"/>
<Unknown index="4"/>
</Outputs>
<InitialUnknowns>
<Unknown index="3"/>
<Unknown index="4"/>
</InitialUnknowns>
</ModelStructure>
</fmiModelDescription>

五、结语

模型交换是指同一仿真环境下某一个仿真实例生成(符合 FMI 接口定义)的可移植可调用的数学模型库;而联合仿真指的是将不同仿真环境(分布式或跨工具)的输入输出按照 FMI 标准定义好,通过Master Algorithm(ssp、dcp)传输数据,协同计算。

《Functional Mock-up Interface forModel Exchange and Co-Simulation v2.0》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…344345346…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%