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

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


  • 首页

  • 归档

  • 搜索

Python中sorted方法和列表的sort方法使用详解

发表于 2021-11-09

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

自我学习:python中sorted方法和列表的sort方法使用详解:

列表有自己的sort方法,其对列表进行原址排序,既然是原址排序,那显然元组不可能拥有这种方法,因为元组是不可修改的。

1.排序,数字、字符串按照ASCII,中文按照unicode从小到大排序

1
2
3
4
python复制代码x = [3,6,2,1,7,9]
x.sort()
print(x)
#输出为:[1,2,4,6,7,9]

创造一个排序好的副本,同时保持原列表不变:

1
2
3
4
5
python复制代码x = [4, 6, 2, 1, 7, 9]
y = x[:]
y.sort()
print(y) # [1, 2, 4, 6, 7, 9]
print(x) # [4, 6, 2, 1, 7, 9]

注意:y = x[:] 通过分片操作将列表x的元素全部拷贝给y,如果简单的把x赋值给y:y = x,y和x还是指向同一个列表,并没有产生新的副本。

另一种获取已排序的列表副本的方法是使用sorted函数:

1
2
3
4
5
6
python复制代码x =[4, 6, 2, 1, 7, 9]
y = sorted(x)
print (y) #[1, 2, 4, 6, 7, 9]
print (x) #[4, 6, 2, 1, 7, 9]
#sorted返回一个有序的副本,并且类型总是列表,如下:
print (sorted('Python')) #['P', 'h', 'n', 'o', 't', 'y']

2.可选参数
sort方法还有两个可选参数:key和reverse
1、key在使用时必须提供一个排序过程总调用的函数:

1
2
3
python复制代码x = ['mmm', 'mm', 'mm', 'm' ]
x.sort(key = len)
print (x) # ['m', 'mm', 'mm', 'mmm']

2、reverse实现降序排序,需要提供一个布尔值:

1
2
3
4
python复制代码y = [3, 2, 8 ,0 , 1]
y.sort(reverse = True)
print (y) #[8, 3, 2, 1, 0]
#True为倒序排列,False为正序排列

多种排序方法:

(1)列表按照其中每一个值的绝对值排序:

1
2
3
4
python复制代码l1 = [1,3,5,-2,-4,-6]
l2 = sorted(l1,key=abs)
print(l1)
print(l2)

(2)列表按照每一个元素的len排序

1
2
python复制代码l = [[1,2],[3,4,5,6],(7,),'123']
print(sorted(l,key=len))

举例客观观察:

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
python复制代码a = [5,2,1,9,6]        

>>> sorted(a) #将a从小到大排序,不影响a本身结构
[1, 2, 5, 6, 9]

>>> sorted(a,reverse = True) #将a从大到小排序,不影响a本身结构
[9, 6, 5, 2, 1]

>>> a.sort() #将a从小到大排序,影响a本身结构
>>> a
[1, 2, 5, 6, 9]

>>> a.sort(reverse = True) #将a从大到小排序,影响a本身结构
>>> a
[9, 6, 5, 2, 1]

注意,a.sort() 已改变其结构,b = a.sort() 是错误的写法!

>>> b = ['aa','BB','bb','zz','CC']
>>> sorted(b)
['BB', 'CC', 'aa', 'bb', 'zz'] #按列表中元素每个字母的ascii码从小到大排序,如果要从大到小,请用sorted(b,reverse=True)下同

>>> c =['CCC', 'bb', 'ffff', 'z']
>>> sorted(c,key=len) #按列表的元素的长度排序
['z', 'bb', 'CCC', 'ffff']

>>> d =['CCC', 'bb', 'ffff', 'z']
>>> sorted(d,key = str.lower ) #将列表中的每个元素变为小写,再按每个元素中的每个字母的ascii码从小到大排序
['bb', 'CCC', 'ffff', 'z']

>>> def lastchar(s):
return s[-1]
>>> e = ['abc','b','AAz','ef']
>>> sorted(e,key = lastchar) #自定义函数排序,lastchar为函数名,这个函数返回列表e中每个元素的最后一个字母
['b', 'abc', 'ef', 'AAz'] #sorted(e,key=lastchar)作用就是 按列表e中每个元素的最后一个字母的ascii码从小到大排序

>>> f = [{'name':'abc','age':20},{'name':'def','age':30},{'name':'ghi','age':25}] #列表中的元素为字典
>>> def age(s):
return s['age']
>>> ff = sorted(f,key = age) #自定义函数按列表f中字典的age从小到大排序

[{'age': 20, 'name': 'abc'}, {'age': 25, 'name': 'ghi'}, {'age': 30, 'name': 'def'}]

>>> f2 = sorted(f,key = lambda x:x['age']) #如果觉得上面定义一个函数代码不美观,可以用lambda的形式来定义函数,效果同上

本文转载自: 掘金

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

SpringBoot系列之Prometheus自定义埋点上报

发表于 2021-11-09

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

logo.jpg

之前介绍了一篇SpringBoot集成Prometheus实现数据上报的博文,在前面一篇博文中,更多的是一个SpringBoot应用如何最小成本的接入Prometheus,并结合Grafana配置一个完整的应用监控大盘

有看过前文的小伙伴可能知晓,SpringBoot接入Prometheus之后,基本上不用做额外的开发,就已经实现了我们关心的JVM情况、GC情况、HTTP调用请求等信息,然而在实际的业务开发过程中,我们总会遇到一些需要手动上报的场景,那么我们可以怎么处理呢?

本文的核心知识点:

  • 通过一个实例演示SpringBoot应用,如何实现自定义的数据上报

上篇博文: SpringBoot整合Prometheus实现应用监控

I. 项目环境搭建

本文演示的项目主要为SpringBoot2.2.1版本,更高的版本使用姿势没有太大的区别,至于1.x版本的不确保可行(因为我并没有测试)

1.依赖

pom依赖,主要是下面几个包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>

2. 配置信息

其次是配置文件,注册下Prometheus的相关信息

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码spring:
application:
name: prometheus-example
management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
application: ${spring.application.name}

上面配置中,有两个关键信息,前面博文也有介绍,这里简单说明

  • management.endpoints.web.exposure.include 这里指定所有的web接口都会上报
  • metrics.tags.application 这个应用所有上报的metrics 都会带上application这个标签

配置完毕之后,会提供一个 /actuator/prometheus的端点,供prometheus来拉取Metrics信息

II. 自定义上报

假设我们现在想自己上报http请求的相关信息,当前计划采集下面几个信息

  • 总的请求数:采用Counter
  • 当前正在处理的请求数:采用Gauge
  • 请求耗时直方图: Histogram

1. Prometheus Metric封装

基于上面的分析,我们这里实现了三种常见的Metric信息上报,这里提供一个统一的封装类,用于获取对应的Metric类型

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
java复制代码package com.git.hui.boot.prometheus.interceptor;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter;
import io.prometheus.client.Gauge;
import io.prometheus.client.Histogram;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
* @author yihui
* @date 2021/11/09
*/
@Component
public class PrometheusComponent implements ApplicationContextAware {
private static PrometheusComponent instance;


/**
* 请求总数
*/
private Counter reqCounter;

/**
* 正在请求的http数量
*/
private Gauge duringReqGauge;

/**
* 直方图,请求分布情况
*/
private Histogram reqLatencyHistogram;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
instance = this;
CollectorRegistry collectorRegistry = applicationContext.getBean(CollectorRegistry.class);
// 这里指定SpringBoot容器的CollectorRegistry,如果使用默认的会导致无法收集
reqCounter = Counter.build().name("demo_rest_req_total").labelNames("path", "method", "code")
.help("总的请求计数").register(collectorRegistry);
duringReqGauge = Gauge.build()
.name("demo_rest_inprogress_req").labelNames("path", "method")
.help("正在处理的请求数").register(collectorRegistry);
reqLatencyHistogram = Histogram.build().labelNames("path", "method", "code")
.name("demo_rest_requests_latency_seconds_histogram").help("请求耗时分布")
.register(collectorRegistry);
}

public static PrometheusComponent getInstance() {
return instance;
}

public Counter counter() {
return reqCounter;
}

public Gauge gauge() {
return duringReqGauge;
}

public Histogram histogram() {
return reqLatencyHistogram;
}
}

注意上面的setApplicationContext()的方法实现逻辑,其中在创建Counter/Gauge/Histogram时,使用的是simpleclient包中提供的最基础的用法,并不是micrometer的封装方式,后面一篇博文会介绍到两种的差异性

上面实现的特点在于,创建Metric时,就已经定义好了label标签,这里定义了

  • path: 请求url路径
  • method: http方法, get/post
  • code: 状态码,表示请求成功还是异常

2. 拦截器实现自定义信息采集上报

接下来我们实现一个自定义的拦截器,拦截所有的http请求,然后上报关键信息

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
java复制代码public class PrometheusInterceptor extends HandlerInterceptorAdapter {

private ThreadLocal<Histogram.Timer> timerThreadLocal = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 正在处理的请求量
PrometheusComponent.getInstance().gauge().labels(request.getRequestURI(), request.getMethod()).inc();

timerThreadLocal.set(PrometheusComponent.getInstance().histogram()
.labels(request.getRequestURI(), request.getMethod(), String.valueOf(response.getStatus()))
.startTimer());
return super.preHandle(request, response, handler);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String uri = request.getRequestURI();
String method = request.getMethod();
int status = response.getStatus();
// count 请求计数,标签分别为 请求路径,请求方法,response http code
// 请求应用总量: sum(demo_rest_req_total)
// 每秒http请求量: sum(rate(demo_rest_req_total[1m])
// 请求topk的url: topk(10, sum(demo_rest_req_total) by (path))
PrometheusComponent.getInstance().counter().labels(uri, method, String.valueOf(status)).inc();

// 请求完毕,计数器-1
PrometheusComponent.getInstance().gauge().labels(uri, method).dec();

// 直方图统计
Histogram.Timer timer = timerThreadLocal.get();
if (timer != null) {
timer.observeDuration();
timerThreadLocal.remove();
}
super.afterCompletion(request, response, handler, ex);
}
}

对于拦截器的知识点这里不进行展开,有兴趣的小伙伴可以查看 SpringBoot系列Web篇之拦截器Interceptor使用姿势介绍

这里我们主要关心的就两点

  • 执行之前(preHandle): gauge计数+1,开始计时
  • 执行之后 (afterCompletion): guage计数-1,counter计数+1,计时收集

3. 测试

最后我们需要注册上面的拦截器,并写个demo进行测试一下

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
java复制代码@RestController
@SpringBootApplication
public class Application implements WebMvcConfigurer {
private Random random = new Random();

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PrometheusInterceptor()).addPathPatterns("/**");
}

@GetMapping(path = "hello")
public String hello(String name) {
int sleep = random.nextInt(200);
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello sleep: " + sleep + " for " + name;
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return (registry) -> registry.config().commonTags("application", applicationName);
}

}

应用启动之后,访问几次hello的http接口,然后在查看一下metric信息,看是否有我们刚才上报的数据

image.png

4. 小结

这一篇博文算是上一篇的补全,若我们希望自定义上报一些信息,可以使用上面这种方式来支持

当然,上报并不代表结束,接下来配置大盘等信息也非常的关键,特别是直方图如何配置Grafana?怎么查看请求的耗时分布情况,就由下文来介绍了

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

本文转载自: 掘金

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

开源项目|Go 开发的一款分布式唯一 ID 生成系统

发表于 2021-11-09

原文连接: 开源项目|Go 开发的一款分布式唯一 ID 生成系统

今天跟大家介绍一个开源项目:id-maker,主要功能是用来在分布式环境下生成唯一 ID。上周停更了一周,也是用来开发和测试这个项目的相关代码。

美团有一个开源项目叫 Leaf,使用 Java 开发。本项目就是在此思路的基础上,使用 Go 开发实现的。

项目整体代码量并不多,不管是想要在实际生产环境中使用,还是想找个项目练手,我觉得都是一个不错的选择。

项目背景

在大部分系统中,全局唯一 ID 都是一个强需求。比如快递,外卖,电影等,都需要生成唯一 ID 来保证单号唯一。

那业务系统对 ID 号的要求有哪些呢?

  1. 全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求。
  2. 趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  3. 单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求。
  4. 信息安全:如果 ID 是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。

在此背景下,有一个高可用的唯一 ID 生成系统就很重要了。

项目使用

生成 ID 分两种方式:

  1. 根据数据库生成 ID。
  2. 根据雪花算法生成 ID。

使用上提供两种方式来调用接口:

  1. HTTP 方式
  2. gRPC 方式

HTTP 方式

1、健康检查:

1
arduino复制代码curl http://127.0.0.1:8080/ping

2、获取 ID:

获取 tag 是 test 的 ID:

1
bash复制代码curl http://127.0.0.1:8080/v1/id/test

3、获取雪花 ID:

1
bash复制代码curl http://127.0.0.1:8080/v1/snowid

gRPC 方式

1、获取 ID:

1
bash复制代码grpcurl -plaintext -d '{"tag":"test"}' -import-path $HOME/src/id-maker/internal/controller/rpc/proto -proto segment.proto localhost:50051 proto.Gid/GetId

2、获取雪花 ID:

1
bash复制代码grpcurl -plaintext -import-path $HOME/src/id-maker/internal/controller/rpc/proto -proto segment.proto localhost:50051 proto.Gid/GetSnowId

本地开发

1
2
3
4
5
ruby复制代码# Run MySQL
$ make compose-up

# Run app with migrations
$ make run

项目架构

项目使用 go-clean-template 架构模板开发,目录结构如下:

id-maker.png

下面对各目录做一个简要说明:

  • cmd:程序入口
  • config:配置文件
  • docs:生成的项目文档
  • integration-test:整合测试
  • internal:业务代码
  • pkg:一些调用的包

借用官方的两张图:

go-clean-template-1.png

整体的层次关系是这样的,最里面是 models,定义我们的表结构,然后中间是业务逻辑层,业务逻辑层会提供接口,给最外层的 API 来调用,最外层就是一些工具和调用入口。

这样做的最大好处就是解耦,不管最外层如何变化,只要在业务逻辑层实现对应接口即可,核心代码可能根本不需要改变。

所以,它们之间的调用关系看起来是这样的:

go-clean-template-2.png

1
2
3
4
scss复制代码HTTP > usecase
usecase > repository (Postgres)
usecase < repository (Postgres)
HTTP < usecase

以上就是本项目的全部内容,如果大家感兴趣的话,欢迎给我留言交流,要是能给个 star 那就太好了。


项目地址: :id-maker

往期文章:

  • 听说,99% 的 Go 程序员都被 defer 坑过
  • 测试小姐姐问我 gRPC 怎么用,我直接把这篇文章甩给了她
  • gRPC,爆赞
  • 使用 grpcurl 通过命令行访问 gRPC 服务
  • 推荐三个实用的 Go 开发工具

推荐阅读:

  • go-clean-template
  • hwholiday/gid
  • Leaf——美团点评分布式ID生成系统

本文转载自: 掘金

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

一个由INSERT INTO ON DUPLICATE KE

发表于 2021-11-09

一、东窗事发

某日早,收到了许多接口报错的告警,虽涉及多个接口,但报错信息出奇一致。相关背景(模拟)交代如下:

1.错误信息:

1
2
sql复制代码Error attempting to get column 'id' from result set. Cause:    
com.mysql.jdbc.exceptions.jdbc4.MySQLDataException: '2.156220742E9' in column '1' is outside valid range for the datatype INTEGER.

2.代码信息:

a.建表sql

1
2
3
4
5
6
7
8
9
10
mysql复制代码CREATE TABLE `t_test`
(
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`biz_field` varchar(255) NOT NULL DEFAULT '' COMMENT '业务字段',
`biz_field_unique` varchar(255) NOT NULL DEFAULT '' COMMENT '业务字段(有唯一索引)',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_t_test_biz_field_unique` (`biz_field_unique`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='测试表'
  • 主键 id 是 int(11) unsigned 类型
  • 字段 biz_field_unique 建了唯一索引

b.MyBatis的Mapper【报错的Mapper】

1
2
3
4
mysql复制代码<select id="getAllTest" resultType="TestPo">
SELECT * FROM t_test
WHERE biz_field = #{bizValue}
</select>
  • 查询字段使用了 select *
1
2
3
4
5
6
7
8
mysql复制代码<insert id="insertTest" parameterType="TestPo">
-- 此处虽然有写id,但代码中 #{id} 未被赋值,所以一直都是用自增id
INSERT INTO t_test(id, biz_field, biz_field_unique)
VALUES (#{id}, #{bizField}, #{bizFieldUnique})
ON DUPLICATE KEY
UPDATE
biz_field = #{bizField}
</insert>
  • 插入数据时使用了 INSERT INTO ON DUPLICATE KEY UPDATE

c.映射的Po

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码//kotlin
class TestPo(
/**
* 主键id
*/
var id: Int = 0,

/**
* 业务字段
*/
var bizField: String = "",

/**
* 业务字段(有唯一索引)
*/
var bizFieldUnique: String = ""
)
  • 映射字段id使用了Int类型(对应java中的Integer)

二、简要分析

从错误信息的字面看,翻译过来大概就是:

1
2
sql复制代码原文:'2.156220742E9' in column '1' is outside valid range for the datatype INTEGER.
译文:第一列的值 '2.156220742E9' 超出 INTEGER 类型的取值范围。

从 Mapper 的 sql 可以看到报错的是一条查询的语句,通过查询表数据可知,’2.156220742E9’= 2156220742 数据就是表中id的值。因此不难理解,这是在MyBatis请求select语句时,拿到id字段的值超过了java中的Integer类型允许的最大值,无法被映射而报错。

  • MySql 中 id 值是 unsigned int,取值范围 [0,4294967295]
  • java 中 id 是 Integer,取值范围:[-2147483648,2147483648]
  • 2147483648(max java Integer) < 2156220742(db行记录值) < 4294967295(max mysql int unsigned)

虽然java与MySql中都称为int类型,但由于Mysql中是 unsigned 的,所以其上限值比java中的int高,因此自动生成超过java中int值上限的id值时,并不会报错

三、临时处理

在第二步我们明确了错误的直接原因,临时处理起来也比较简单

1.业务没有使用该字段

a.在Po中移除该字段

如果该字段没有在业务中使用,可以直接在Po中去掉该字段,这样即使Mapper的sql中有select出该字段,但是由于不会尝试将该字段映射字到Po,所以也不存在类型转换问题,当然也不会引起报错。

1
2
3
4
5
6
7
8
9
kotlin复制代码//kotlin
class TestPo(
/**
* 主键id,移除字段后,即使 select 出该字段,也不会报错
*/
//var id: Int = 0,

...
)

b.在sql中移除该字段

同样的,该字段没有在业务中使用的话,也可以在直接在Mapper的sql中去掉该字段,这样mybatis处理的时候,会使用Po默认值填充,同样也不会报错。

1
2
3
4
5
6
mysql复制代码<select id="getAllTest" resultType="TestPo">
--将select * 改成具体的字段,不包括有问题的字段id
SELECT biz_field,biz_field_unique
FROM t_test
WHERE biz_field = #{bizValue}
</select>

如果在sql中使用了select * ,那就比较麻烦了,还要逐个字段名写上去

2.业务有使用该字段

a.修改po中该字段的类型

如果业务有使用该字段,那就需要修改Po中该字段的类型了,改为long类型即可。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码//kotlin
class TestPo(
/**
* 主键id
*/
var id: Long = 0

...

)

如果其它表有保存该字段,那也需要做相应的修改,并且如果该字段的存储类型值范围小于 int unsigned 的话,需要修改该字段的类型

四、寻找真凶

临时处理完成后暂时恢复了正常,但是真正原因仍有待进一步寻找

1.真有22亿的数据?当然不是

报错时我们看到该表的自增id高达21亿,但不代该表数据真有21亿(如果真的有,那你大概率会被运维请去喝茶),所以我们用select count 确认了一下,只有5万的数据。那么问题来了5万的数据,那自增id为何能增长到21亿呢?

1
2
mysql复制代码sql-> select count(1) from t_test;
res-> 50000

2.有人手动插了id很大的数据?也不是

我们都知道,自增id值是不会自动回填的,也就是我手动插了一个id=1亿的数据,那下一个自增id就是1亿01。所以我们手动插入一个id值为21亿的数据,那就有可能出现仅有5万数据的情况下自增id达到21亿的场景。(当然一般直接修改线上数据是不被允许的)

确认的方法:导出所有的id,然后看下区间分布即可

image.png
结果:id值分布均匀、没有高度集中且跨度较大,不符合推测,那跨度中间的id是怎样消失的呢?

思考:由于id是明显不连续的,所以推测肯定是有什么地方申请了id没有使用,并且mysql不会回收这部分id

问:不连续和跨度大是怎么看出来的?从图上看不是挺连续的吗?

答:图的横坐标是id的序号,而纵坐标是id值,在21亿里边均匀分布了5万的数据,其id肯定不是连续滴。(这点直接看真实id值可能会更直观)

3.那么,什么情况下会导致mysql自增id不连续呢?

a.有人删除了中间的id值

该表没有物理删除的操作,排除

b.代码指定了不连续的id进行插入

没有此逻辑,排除

c.带插入语句的事务回滚

没有此逻辑,排除

d.唯一建冲突

由表结构可知,存在 biz_field_unique 字段,是有唯一索引的,所以可能会产生冲突而消耗自增id值

回看代码,插入该表数据的就只有一个sql,并且明确了代码中不会指定id的值,也就保证了所有id均由mysql生成,所以必定是有某些操作影响了id值的生成!

4.INSERT INTO ON DUPLICATE KEY UPDATE ? yes

经查阅 资料 可知,INSERT INTO ON DUPLICATE KEY UPDATE 在唯一索引冲突时,虽然执行的是更新操作,但仍然会使该表的自增id+1。通过逻辑排查发现该sql所在接口的调用频率很高,而且绝大部分都是更新数据,这样会导致频繁发生唯一索引冲突,消耗自增id,这也符合id相对连续且间隔较大的特点。

五、后续处理

1.为什么要用 INSERT INTO ON DUPLICATE KEY UPDATE ?

a.INSERT INTO ON DUPLICATE KEY UPDATE 的作用

顾名思义,使用该句式可以在发生唯一键冲突的时候,去更新某些字段,不冲突的时候,则插入一条记录。

需要注意的是,该语句包含了 判断是否存在唯一键冲突 及 插入或更新数据 两步操作的。

b.为什么不用 select ,然后再判断用 update 或 insert 呢?

既然 INSERT INTO ON DUPLICATE KEY UPDATE 在频繁冲突更新的情况下会浪费id,那我换种方式,先根据主键select一次,如果记录存在,我就执行update操作,如果记录不存在,我再执行insert操作?

I.朴素版

1
2
3
4
5
6
7
mysql复制代码//伪代码
var data = "select * from t_test where biz_field_unique = uniqueValue"
if (data == null){
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue')"
}else{
"update t_test set biz_field = value where biz_field_unique = uniqueValue"
}

有经验的小伙伴一眼就能看出来,这是会存在并发问题的,如下:
image.png

request-2 的select语句在 request-1 的insert语句之前执行,导致重复插入报错

II.事务版

这时有些小伙伴就要问了,既然sql执行有并发问题,那我加个事务是否可以呢?

1
2
3
4
5
6
7
8
9
mysql复制代码//伪代码
transaction.open //打开事务
var data = "select * from t_test where biz_field_unique = uniqueValue;"
if (data == null){
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue');"
}else{
"update t_test set biz_field = value where biz_field_unique = uniqueValue;"
}
transaction.commit //提交事务

在 mysql RR级别下是不行滴,朴素版的问题依旧会存在,因为select的时候是没有互斥锁的,所以 request-2 的select语句仍有可能在 request-1 的insert语句之前执行。

III.互斥锁版

既然如此,那我手动给个互斥锁(for update)如何呢?

1
2
3
4
5
6
7
8
9
10
mysql复制代码//伪代码
transaction.open //打开事务
//select 语句加入 for update
var data = "select * from t_test where biz_field_unique = uniqueValue for update;"
if (data == null){
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue');"
}else{
"update t_test set biz_field = value where biz_field_unique = uniqueValue;"
}
transaction.commit //提交事务

select然后insert或者update操作的并发问题_加入事务.jpg
从示意图可以看到,这样是可以的,但其执行过程相当于串行了,性能堪忧,并且其锁竞争发生在mysql中,给db造成了压力。

IV.分布式锁版

既然不想将压力放到db,其实也可以从服务侧解决这个问题:

1
2
3
4
5
6
7
8
9
mysql复制代码//伪代码
distributeLock.lock //获取分布式锁
var data = "select * from t_test where biz_field_unique = uniqueValue;"
if (data == null){
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue');"
}else{
"update t_test set biz_field = value where biz_field_unique = uniqueValue;"
}
distributeLock.unlock //释放分布式锁

通过分布式锁,可以将这串代码串行执行,但性能依旧堪忧

V.结合业务版

前面几个都是版本都是希望能得到通用解决方案的,那我们结合业务实际使用情况会不会好点呢?

首先明确一点,业务对该表时没有delete操作的,也就是记录一旦插入,往后所有操作都会是更新操作,根据表中数据,我们可以得到估算分支的执行频率,如下

1
2
3
4
5
6
7
8
9
10
mysql复制代码//伪代码
//select 执行次数 21亿 100%
var data = "select * from t_test where biz_field_unique = uniqueValue;"
if (data == null){
//insert 执行次数 5w 0.0024%
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue');"
}else{
//update 执行次数 21亿-5w 99.9976%
"update t_test set biz_field = value where biz_field_unique = uniqueValue;"
}

如上计算可见,实际进入到 insert 的占比是非常少的,为了这部分的请求量而让整个流程串行执行是不妥的,因此,我们可以针对 insert 这里做一下优化,使用 INSERT INTO ON DUPLICATE KEY UPDATE 来代替insert,可以在id消耗和性能之间找到一个平衡。

1
2
3
4
5
6
7
8
9
10
mysql复制代码//伪代码
//select
var data = "select * from t_test where biz_field_unique = uniqueValue;"
if (data == null){
//insert into on duplicate key update
"insert into t_test(biz_field, biz_field_unique) values ('value','uniqueValue') on duplicate key update biz_field = value;"
}else{
//update
"update t_test set biz_field = value where biz_field_unique = uniqueValue;"
}
  • 使用了 INSERT INTO ON DUPLICATE KEY UPDATE ,所以出现冲突的时候还是会有id值的浪费,但这个场景是少数的,其增长范围是可接受的

六、写在最后

遇到问题的时候,一般都需要了解好业务的使用场景,虽然很多问题都有通用的解决办法,但不一定是最优解,还是需要综合考虑业务场景。例如本文的问题就可以使用分布式锁,但是由于我们业务中不会物理删数据,因此我们可以舍弃一部分id,换来的是保证数据正确的情况下,仍保持较好的性能。

本文转载自: 掘金

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

RedissonLock如何实现等待锁的 前言 tryLoc

发表于 2021-11-09

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

前言

经常会有到这样的需求,就是在一个查询接口,第一次查询的时候,如果没有查询到就要执行初始化方法,初始化数据出来,之后的查询就可以直接查询库里的数据了。这样设计的目的是,如果需要初始化的数据特别大,无法再一次调用方法里处理完,或者说数据并不是每条都需要初始化,这种情况下,优先查询的数据优先初始化。

问题

这种方案随之而来就会引发一个问题。查询接口众所周知是个自然幂等的,不需要我们额外去做幂等处理。但是在方案中,这个查询就不单单是个查询了。没有查询到就要执行初始化方法,本质上是个插入逻辑。这就需要我们自己去做幂等了。

方案

单台服务,我们可以用Java的锁来实现幂等,每条数据的主键id来当锁。但在现在基本上都是分布式服务,如同上篇文章说的,我们可以用分布式锁RedissonLock来实现。

并发第一次请求时,竞争RedissonLock,谁获得了锁,谁就执行初始化方法,没有竞争到锁的请求,可以设置一个等待时间,等待锁释放。锁释放了,就可以先查询数据有没有初始化好,完成了就直接查库。这里,就要提一下RedissonLock是如何实现等待的?

tryLock

RedissonLock在加锁方法提供了一个api,提供了一个参数waitTime即等待时间。

1
java复制代码public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)

在waitTime时间内会订阅消息,这里用的是redis本身的发布订阅功能。

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(threadId);
return false;
}

这样,在释放锁的时候,同时发布消息出来。所有监听该锁的线程都将收到通知,那么,这些线程将再次去竞争加锁,从而达到我们需要的幂等功能。我们在看看释放锁的逻辑,是不是发布消息了?

unlockInnerAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

}

可以看出在unlockInnerAsync方法里,执行了lua脚本,在脚本里,我们很容易能看到执行了publish命令。

思考

Redisson巧妙的用了redis的发布订阅功能,实现了分布式锁的等待功能。那么我们在实际业务应用中,redis的发布订阅功能还有哪些使用场景呢?欢迎留言讨论!

本文转载自: 掘金

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

若依中的代码生成器-数据库篇

发表于 2021-11-09

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

继上一篇《若依中的代码自动生成器研究-表查询篇》,我们继续来学习若依系统中的代码生成逻辑。

导入表之Sql查询

在菜单栏点击“代码生成”,在右侧栏中点击“导入”按钮,在文章若依中的代码自动生成器研究-表查询篇中,我们已经一直到若依是通过查询数据库的information_schema.tables从而查询到数据库的所有表。

我们下一步的操作是,“勾选my_user表,点击确定”。操作路径示意图以及通过F12调试,查看接口请求,如下图所示。

image.png

所请求的接口是/tool/gen/importTable,我们通过idea检索后台接口,发现其位于ruoyi-generator/com.ruoyi.generator/controller/GenController下,如下图:

image.png

Controller中的代码源码如下:(我们通过调试,补充上各个值的获取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 导入表结构(保存)
*/
@PreAuthorize("@ss.hasPermi('tool:gen:import')")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@PostMapping("/importTable")
public AjaxResult importTableSave(String tables)
{
// 通过调试,字符串tables格式为:"my_user,table_name_1,table_name_2...."
String[] tableNames = Convert.toStrArray(tables);
// tableNames: ["my_user", "table_name_1", "table_name_2"]
// 查询表信息
List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
// 见下图
genTableService.importGenTable(tableList);
return AjaxResult.success();
}

image.png

我们来研究一下其中查询表的那句sql,通过在xml中搜索,其执行sql为:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码SELECT
table_name,
table_comment,
create_time,
update_time
FROM
information_schema. TABLES
WHERE
table_name NOT LIKE 'qrtz_%'
AND table_name NOT LIKE 'gen_%'
AND table_schema = (SELECT DATABASE())
AND table_name IN ('my_user')

通过这个查询将my_user表的基本信息查询出来。

那么genTableService.importGenTable(tableList);这一句又执行了哪些操作呢?我们继续来看。

导入表之导入

同过定位importGenTable,我们查询到他的执行方法,我们补充上调试过程中的参数值

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
java复制代码/**
* 导入表结构
*
* @param tableList 导入表列表
*/
@Override
@Transactional
public void importGenTable(List<GenTable> tableList)
{
// tableList: [{my_user表的信息}]
// admin
String operName = SecurityUtils.getUsername();
try
{
for (GenTable table : tableList)
{
String tableName = table.getTableName(); // tableName: my_user
GenUtils.initTable(table, operName);
int row = genTableMapper.insertGenTable(table);
if (row > 0)
{
// 保存列信息
List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
for (GenTableColumn column : genTableColumns)
{
GenUtils.initColumnField(column, table);
genTableColumnMapper.insertGenTableColumn(column);
}
}
}
}
catch (Exception e)
{
throw new ServiceException("导入失败:" + e.getMessage());
}
}

其中有一个关键的数据结构:GenTable,它位于Domain中,对应数据库中的表gen_table,字段截图如下所示:

image.png

类似的,另外一个表gen_table_column以及其对应的domain域类为GenTableColumn,保存生成数据库表的列信息,其字段结构如下截图:该表通过字段table_id与表gen_table中的某一行关联上。

image.png

那么查询数据库表的列用的什么sql呢?

通过检查,在xml中检索到sql为:

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
sql复制代码SELECT
column_name,
(
CASE
WHEN (
is_nullable = 'no' && column_key != 'PRI'
) THEN
'1'
ELSE
NULL
END
) AS is_required,
(
CASE
WHEN column_key = 'PRI' THEN
'1'
ELSE
'0'
END
) AS is_pk,
ordinal_position AS sort,
column_comment,
(
CASE
WHEN extra = 'auto_increment' THEN
'1'
ELSE
'0'
END
) AS is_increment,
column_type
FROM
information_schema. COLUMNS
WHERE
table_schema = (SELECT DATABASE())
AND table_name = 'my_user'
ORDER BY
ordinal_position

是通过查询information_schema. COLUMNS并约束table_name以及数据库来查询指定表my_user的所有列。

总结

通过导入操作,将my_user转换为了gen_table,gen_table_column中的几行数据,以便后续代码的生成。

本文转载自: 掘金

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

有关 lnmp 下安装php链接mongodb扩展的安装

发表于 2021-11-09

在运行一个项目时,很多人在设置一些配置文件和相关的应用扩展上十分的头疼,比如我本人,今天本人通过百度成功安装了 php 链接 mongodb 的扩展文件,为此来做一下分享。

首先需要确定你的 php 版本,使用命令 php -v 进行查看

1
复制代码 我本人项目需要的是 php5.6 的版本 并且是laravel的项目 运行在homestead的集成环境中

根据自己的php版本下载相对应的 mongo 的扩展文件 使用命令

1
bash复制代码 wget  http://pecl.php.net/get/mongo-1.6.16.tgz  (此扩展适用于 php5.6) 下载路径为 /var/bin 下载(下载路径可以自己定义)

将下载完成的压缩包进行解压

1
复制代码 sudo tar -zxvf mongo-1.6.16.tgz (加不加sudo需要看自己用户的权限 有权限就不需要加)

进入到解压完成的目录中

1
bash复制代码 cd mongo-1.6.16

运行自己系统中的 phpize 文件

1
2
arduino复制代码 sudo /usr/bin/phpize5.6     (这里写的路径是自己环境中的 phpize 文件所在的路径,我的环境是通过homestead进行搭建的所以 phpize 文件在这个目录下并且有对应的版本号),
如果不知道自己的 phpize 文件所在的位置 可以通过命令进行查找 find / -name phpize

继续在 mongo-1.6.16 目录中使用命令来写配置文件

1
javascript复制代码 sudo ./configure --with-php-config=/usr/bin/php-config5.6  (这里的路径也是自己环境下文件所有位置的路径,也可以通过上面的方法进行查找)

在写完配置文件后开始执行安装

1
go复制代码 sudo make  或者 sudo make install

在执行到这一步时并且上面没有出现错误时就表示扩展文件已经安装成功,只需在修改一下 php 的配置文件 php.ini 即可

切换到php的配置文件下
1
bash复制代码 cd /etc/php/5.6/fpm/  (本人环境下的php安装路径,根据自己的安装位置进行切换)
进行编辑 php.ini文件
1
复制代码 sudo vim php.ini
进入到文件后在文件最后的位置添加
1
ini复制代码 extension = "mongo.so" (如果你是高版本的php,这里应该是 mongodb.so,我的版本是php5.6 )

注意因为我的是 homestead 的环境需要更改两个php的配置文件 另一个配置文件在 /etc/php/5.6/cli 然后进行相应的编辑操作。

添加完成之后,需要重新启动 php与服务器的通信文件 php-fpm 使用命令

1
复制代码 sudo service php5.6-fpm restart   (运行自己相应版本的文件)

最后使用代码 phpinfo() 在代码中执行打印 如果有 mongo 的扩展则表示安装成功

本文转载自: 掘金

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

解放人与设备距离,5G时代的远程操控该如何完成

发表于 2021-11-09

点击一键订阅《云荐大咖》专栏,获取官方推荐精品内容,学技术不迷路!

8毛峻岭.jpg

*导语 | 5G的落地发展带来了高带宽、低时延、本地分流等新的特性。而远程控制作为5G技术的先导,其对于智能化时代具备重要价值,5G可以满足远程控制应用中更多信息的同步需求。文章将会系统介绍5G时代,远程操控的理论与使用技巧。

213.png

物联网这个概念早在十多年前便已提出,其主要依托于移动通讯网络来实现其功能的传输。在过去物联网领域的一些设备控制场景中,我们或多或少都见到过远程控制技术的身影,但受限于当时的网络条件和技术场景,大部分应用都属于对设备的简单操作,并不会同步太多的现场实时信息。随着通讯技术的不断发展,以及5G技术的出现,智能化的生活也离大家越来越近。

5G的出现给移动网络带来了高带宽、低时延、本地分流等新的特性。同时,远程控制作为5G技术的先导,其对于智能化时代具备重要价值,5G可以满足远程控制应用中更多信息的同步需求。可以说,5G技术的成熟促进了远程操控的加速与落地。

目前,5G远程实时操控的典型应用场景主要是:港口、露天矿等封闭区域和开放道路下自动驾驶车辆意外情况下的远程接管,以及天车、吊机、化工、地下矿等高危环境或恶劣环境下的远程作业。前者是作为必要的应急介入手段,更好协助自动驾驶等设备本地智能工作;后者是作为常态化的作业方式,提升一线人员作业体验。

随着行业数字化发展,未来像矿山、港口、物流等场景,无人化、远程化作业将逐渐成为行业趋势,云出租车、云代驾等C端应用也会逐渐兴起。预计5G远程实时操控将打开百亿级规模以上的市场空间,渗透到各领域助力社会发展。

5G 远程实时操控的主要痛点和相关技术

5G远程实时操控,主要面向解决车辆等复杂设备的远程操控,需要支持基于实时场景的人机交互方式。

为了更好地在远端还原真实的操作场景,方便人员进行更为细致的实时控制,除传统的状态数据外,在5G远程实时操控中会引入现场侧视频、音频等媒体数据的实时同步。为保证远程控制的安全以及流畅,这些丰富的现场数据和细致的远端操作的同步,对感知的实时性以及操作的可靠性和及时性有非常高的要求。

以5G远程控制领域非常有代表性的车辆远程控制场景为例,其对于车端视频画面等信息及时回传有着严格的时延要求。下表是基于移动场景下的车辆远程控制对实时性要求的一个简单分析,可以看出在低速下进行车辆远程驾驶,建议需要达到200ms的时延,而较为理想的指标是要达到150ms的时延。而目前基于传统视频监控的远控时延往往在300-400ms左右。这对网络时延、音视频通信的时延以及控制信令的时延和可靠性都提出了很高的要求。

image.png

为降低5G远程控制中音视频端到端时延,并保障操控的可靠性和及时性,需要引入实时音视频通信、控制信令同步和5G网络优化等技术来联合提升操控体验。

  • 实时音视频通信:主要解决音视频通信的实时性;在远控端到端时延中,音视频通信时延占比往往会达到80%左右;因此面向远控的音视\频通信时延的优化是非常重要的;另外在远控场景中,往往会使用多路视频流来还原现场,单个设备可能会涉及4-8路高清视频流的同时传输,会占用较高的网络带宽,视频码率和卡顿率的优化也是远控非常关注的因素。
  • 控制信令同步:主要解决控制信令的传输可靠性和时延;控制信令最终是会影响现场设备的动作,因此对可靠性要求非常高,在尽可能保证时延的基础上,需要达到极致的可靠性,并考虑应对各种意外情况的检测和处理。
  • 5G网络优化: 主要解决上行音视频数据的低时延传输,保障控制信令的下行传输。音视频通信和控制信令同步的基础均是网络,在苛刻的时延和可靠性要求下,需要应用和网络进行协同的优化,来提升端到端的性能。

可以看出,这三大技术都是围绕5G远程控制的时延和可靠性等痛点来进行优化和提升,其中5G网络优化是底座,实时音视频通信是时延优化的核心,控制信令同步是保障控制可靠和安全的关键。除了这些技术优化外,在5G远程操控的规模化应用中,系统架构也是非常重要的,这会直接影响5G远程控制的灵活度和扩展性。

5G远程操控系统的四大主流架构

5G远程操控系统中,主要包含受控端、控制端、5G网络等必要元素,以及如远控服务器等可选元素。 下面是目前5G远程操控应用中的一些常见系统架构:

1)架构A:单车直连+视频与控制分离

image.png

这种架构是基于简单拓展传统视频监控+传统CAN总线控制,来实现简单1对1场景下的远程操控。

  • 视频链路:多路摄像头连接到一个类似NVR这样的视频网关,接入到5G专网,控制端会使用根据预先配置的视频网关的IP进行拉流,获取远端的音视频流;
  • 控制链路:基于CAN总线,通过CAN转以太网再转CAN的方式,将CAN总线数据over在5G专网提供的IP网络上传输, 完成了受控端的控制器CAN接口与控制端的操控器CAN接口的对接;

这种架构虽然能够简单达到远程操控的基本功能,但是受控端与控制端的连接,依赖于两端IP的提前配置和网络通道的规划,灵活性不足,很难应用于规模部署的多车场景;另外受限于传统视频监控的时延,其端到端时延也较大。

2)架构B:单车直连+视频与控制融合

image.png

这种架构与架构A的区别,在于受控端网关中融入了CAN接口的控制能力,升级成为远控网关,而非常规的NVR这样的纯视频网关。 这样可以在网关中,融合视频、音频和其他如振动、姿态、车辆工况这样类传感数据的采集和控制,使得远控的扩展性和现场内容的丰富度更强,而且相比传统视频监控的方案,其视频时延也可得到进一步优化;另外网关侧还可以定义控制指令的保护策略来应对网络波动和意外情况,具有更好的可靠性和安全性。

同样由于单车直连,这样的架构在规模部署的多车场景,仍有很大的灵活性问题。

3)架构C: 统一转发

image.png

由于单车直连架构在部署上的局限性,出现了统一转发的架构;多个受控端和操控端都连接到一个统一的远控服务器上。通过远控服务器扮演连接转发的角色,来保证受控端和控制端的连通性。

基于这种架构,控制端和受控端可以通过远控服务器中转,按照各自ID直接建立连接,而不需要预先知道对方的IP,并且也不需要依赖两端网络的IP可达性。

这种架构虽然大大简化了规模场景部署的复杂性,但是由于中间服务器的引入,对服务器的转发能力和可靠性提出了较高要求,并且也为远控服务引入了中间的转发时延。

4)架构D:融合架构

image.png

融合架构是由腾讯云5G团队提出,并用于其5G远程操控产品,目前已在矿区、港口、末端物流等多个场景落地应用。

该架构中,远控服务器主要负责控制面,统一管理受控端远控网关和控制端操控PC,因此在操控PC仍可以基于受控端的ID来向远控服务器申请进行连接的建立,而不需要预先配置受控端的IP。

音视频、控制命令和传感器等数据的传输过程中,数据面仍尽量采用传统直连的网络通信方式,在直连网络不可达的情况下,通过媒体中转服务器进行中转。

这种架构融合了单车直连架构和统一转发架构各自的优点,既能大大简化规模部署场景的复杂性,又能保持单车直连架构中低时延的优点,对远控服务器的要求也大大降低。

从长远看,融合架构是5G远程控制未来的发展趋势。因为5G远程操控应用场景较多,网络场景也较为复杂,有专网场景(如矿山、港口的远程控制),也有公网场景(如末端物流、干线物流、云出租车),另外还会与5G MEC结合进行边缘分流和计算,来进一步降低网络时延。因此在系统架构上,进行控制面和数据面的分离是一个非常好的选择,可更灵活的部署媒体数据面,适应多类网络环境,并发挥出5G MEC的优势。

未来,随着5G远程操控应用的不断发展,除了在技术和架构上不断演进升级外,相信受控端和控制端的音视频及控制接口协议标准化上也会不断完善,可以实现不同车辆、驾驶舱间的互操作性。

8毛峻岭.jpg

《云荐大咖》是腾讯云加社区精品内容专栏。云荐官特邀行业佼者,聚焦于前沿技术的落地及理论实践之上,持续为您解读云时代热点技术、探索行业发展新机。点击一键订阅,我们将为你定期推送精品内容。

本文转载自: 掘金

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

Java基础学习—day08 Obeject父类 包装类 访

发表于 2021-11-09

Obeject父类

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。

匿名内部类

内部类:在一个类的内部定义了另外的类,称为内部类,匿名内部类指的是没有名字的内部类。为了清楚内部类的主要作用,下面首先观察一个代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码interface IMessage{
public void print();
}
class MessageImpl implements IMessage{//定义接口实现类
public void print(){
System.out.println("Hello World");
}
}
class Demo{
public static void get(IMessage msg){//接受接口对象
msg.print();
}
}
public class TestDemo1{
public static void main(String args[]){
IMessage msg = new MessageImpl();//子类为接口实例化
Demo.get(msg);//传递msg对象
}
}

如果说现在MessageImpl这个子类只使用一次,有必要按照以上的方式进行定义吗?

这个时候MessageImpl就没有什么意义了,但是可以利用匿名内部类的概念来解决此问题。匿名内部类是在抽象累和接口的基础之上发展起来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码interface IMessage{
public void print();
}
class Demo{
public static void get(IMessage msg){//接受接口对象
msg.print();
}
}
public class TestDemo1{
public static void main(String args[]){
IMessage msg = new IMessage(){//匿名内部类
public void print(){
System.out.println("hello,world!");
}
};
Demo.get(msg);//传递msg对象
}
}

结论:基本上搞匿名内部类都应该在接口或抽象类形式上完成。

在抽象类中使用匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码abstract class Message{
public void print(){
System.out.print(this.getInfo());
}
public abstract String getInfo();
}
class Demo{
public static void get(Message msg){//接受接口对象
msg.print();
}
}
public class TestDemo1{
public static void main(String args[]){
Demo.get(new Message(){
public String getInfo(){
return "www.baidu.com";
}
});//传递msg对象
}
}

强调:一个普通类进行不要再去有子类进行继承,能够继承的只是抽象类和接口,所以在普通类上继续使用

匿名内部类的形式来定义子类,但是在正常的开发逻辑上是错误的。

Object类简介

在Java的定义之中,除了Object类之外,所有的类实际上都存在继承关系,即:如果现在定义了一个类,没有默认继承任何一个父类的话,则默认讲继承Object类,以下两种类最终定义效果是完全一样的。

Object类的无参构造是专门子类提供服务的。

方法名称 类型 描述
public String toString() 普通 取得对象信息
public boolean equals(Object obj) 普通 对象的比较
public int hashCode() 普通 返回对象的哈希码值

取得对象信息toString()

toString()的核心目的在于取得对象信息。相当于替换了getInfo()方法的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name = " + this.name + ",age = " + this.age ;
}
}

public class TestDemo2{
public static void main(String args[]){
Person p = new Person("zsr",18);
System.out.print(p.toString());
}
}

对象的比较equals()

实际上对于equals()方法应该并不陌生,这个方法在String类中见过,String是Object类的子类,所以String类的equals()方法就是覆写了Object类中的equals()方法,在Object类之中,默认的equals()方法实现比较的是两个对象的内存地址数值,但是并不符合与真正的对象比较需要。对象比较之前也写过,但是之前那是自己定义的一个新的方法名称,今天可以给出标准的方法名称:equals()。

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复制代码class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public boolean equals(Object anObject){
if(anObject == null){
return false;
}
if(this == anObject){
return true;
}
//判断anObject的实例是不是Person
if( !(anObject instanceof Person)){
return false;
}
//必须将Object类型变为Person类型后才可以调用name和age属性
Person per = (Person) anObject;
return this.name.equals(per.name) && this.age == per.age;
}
public String toString(){//覆写Object类方法
return "name = " + this.name + ",age = " + this.age ;
}
}

public class TestDemo3{
public static void main(String args[]){
Person per1 = new Person("zsr",18);
Person per2 = new Person("zsr",18);
//true
System.out.println(per1.equals(per2));
//false
System.out.println(per1.equals("Hello,world!"));
}
}

但是需要有一个注意,很多人在写对象的比较会使用如下的形式:

  • public boolean equals(Person anObject)

因为父类中的equals()方法用的是Object,所以以上的方法严格来讲已经不叫覆写,叫重载。

Object接口引用数据类型

在之前的分析来讲Object可以接收任意的对象,从结构上来讲Object是所有类的父类,但是Object概念并不仅仅局限于此,他已接收所有的引用数据类型,包括:接口、数组。

使用Object类接收数组,数组和Object没有任何明确的关系。

1
2
3
4
5
6
7
8
9
java复制代码public class TestDemo4{
public static void main(String args[]){
Object obj = new int []{1,3,4};
int data [] = (int [])obj;//向下转型
for(int i = 0 ;i < data.length ; i++){
System.out.println(data[i]);
}
}
}

接收接口对象,从接口的定义而言,它是不能去继承一个父类的,但是由于接口依然属于引用类型,所以即使没有继承类,也可以使用Object接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码interface Message{}
class MessageImpl implements Message{//定义接口子类
public String toString(){
return "Hello World";
}
}
public class TestDemo5{
public static void main(String args[]){
Message msg = new MessageImpl();//向上转型
Object obj = msg;//向上转型
Message temp = (Message) obj;//向下转型
System.out.println(temp);//toString()
}
}

从代码上讲,以上只能算是一个固定的操作概念,不过从实际来讲,因为有了Obejct类的出现,所有的操作就可以达到统一,那么之前的链表程序,就应该变得很方便了。所有的数据都使用Object接收,所有的对象比较(删除、查找)都可以使用equals()。

包装类

在Java的设计之中,一直倡导一个原则:一切皆对象,这个原则本省有一个漏洞,基本数据类型不是对象,所以这个原则就出现了问题,那么如果说现在这个问题由我们来解决,该如何解决呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class MyInt{
private int num;//基本类
public MyInt(int num){
this.num=num;
}
public int intValue(){
return this.num;
}
}
public class TestDemo6{
public static void main(String args[]){
Object obj = new MyInt(10);//子类自动变为Object父类对象
MyInt temp = (MyInt) obj;//向下转型
int result = temp.intValue();
System.out.println(result*result);
}
}

以上的操作是将基本类型变为了一个对象的形式进行操作了,但是这里面有一个问题:基本数值型数据是可以进行数学运算的,可是以上变为了类的形式,那么肯定无法直接计算了。以上的问题既然我们都想到方法解决,那么Java也一定早已解决,为此它专门提供了八种包装类:

byte(Byte),short(Short),int(Integer),long(Long),float(Float),double(Double),boolean(Boolean),char(Character);

而这八种包装类有分为两大阵营:

​ 数值型(Number子类):Byte,Short,Integer(int),Float,Double,Long;

​ 对象型(Object子类):Boolean,Character(char)。

可是对于Number的子类,就必须观察出Number类之中定义的方法:byteVlue()、intVlue()、doubleVlue()、shortVlue()、longVlue()、floatVlue(),就是从包装的类之中取得所包装的数值。

装箱与拆箱

在基本数据类型和包装类之间的转化之中分为两个重要概念:

​ 装箱操作:将基本数据类型变为包装类,称为装箱,包装类的构造方法。

​ 拆箱操作:将包装类变为基本数据类型,称为拆箱,Number类中的xxValue()方法。

以int和Integer为例

1
2
3
4
5
6
7
java复制代码public class TestDemo{
public static void main(String args[]){
Integer var = new Integer(10);//装箱
int result = var.intValue();//拆箱
System.out.println(result*result);
}
}

以double和Double为例

1
2
3
4
5
6
7
java复制代码public class TestDemo{
public static void main(String args[]){
Double var = new Double(10.0);//装箱
double result = var.doubleValue();//拆箱
System.out.println(result*result);
}
}

以上的操作实在JDK1.5之前所进行的必须的操作,但是到了JDK1.5之后,Java提供了自动装箱和自动拆箱的机制,并且包装类的对象可以自动的进行数学计算了。

自动装箱与拆箱

1
2
3
4
5
6
7
8
java复制代码public class TestDemo{
public static void main(String args[]){
Integer var = 10;//自动装箱
int result = var;//自动拆箱
//可以直接利用包装类进行对象操作
System.out.println(++var*result);//自动进行数学运算
}
}

但是到此为止还有一个小问题,实际上这一问题之前已经见过。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class TestDemo{
public static void main(String args[]){
Integer x = new Integer(10);//新空间
Integer y = 10;//入池
Integer z = 10;
System.out.println(x==y);//false
System.out.println(x==z);//false
System.out.println(y==z);//ture
System.out.println(x.equals(y));//ture
}
}

使用包装类的时候还需要考虑equals()和==的区别。

使用int还是Integer?

  • 在接收数据的时候,使用的一定都是int,而保存数据的时候一般使用Integer
  • 以后编写的简单java类统一不要再去使用基本数据类型,全部换位包装类

字符串与基本数据类型的转换

包装类之中所提供的最大优点在于可以讲字符串变为制定的基本数据类型,下面列出几个操作:

​ Integer类:public static int parseInt(String s);

​ Double类:public static double parseDouble(String s);

​ Boolean类:public static boolean parseboolean(String s;

但是character这个包装类之中,并没有提供一个类似的parseCharacter(),因为字符串String类之中提供了一个charAt()方法,可以取得制定索引的字符,而且一个字符的长度就是一位。

将字符串变为int

1
2
3
4
5
6
7
java复制代码public class TestDemo{
public static void main(String args[]){
String str = "16";
int result = Integer.parseInt(str);//String ——>int
System.out.println(result*result);
}
}

但是需要提醒的是,在执行这种转化的操作过程之中,字符串字符串中的全部内容必须由数字所组成,如果有一位内容不是数字,则在转化的过程之中讲出现如下的错误提示:NumbnerFormatException。

将字符串变为double

1
2
3
4
5
6
7
java复制代码public class TestDemo{
public static void main(String args[]){
String str = "16.";
double result = Double.parsedouble(str);//String ——>int
System.out.println(result*result);
}
}

将字符串变为boolean型数据

1
2
3
4
5
6
7
java复制代码public class TestDemo{
public static void main(String args[]){
String str = "true";
boolean result = Boolean.parseboolean(str);//String ——>int
System.out.println(result);
}
}

提示:在使用Boolean型包装类的时候,如果字符串之中的内容不是true或者是false,统一都按照false处理。

以上的操作是通过字符串变为一些基本类型的数据,但是反过来讲,基本数据类型如何变为字符串呢?

方式一:任何基本数据类型遇到了String之后都会变为String型数据;

1
2
3
4
5
6
7
8
java复制代码public class TestDemo{
public static void main(String args[]){
int num = 100;
String str = num+"";//int——>String //会产生垃圾
System.out.println(str.length());
}
}
//会有垃圾产生

方式二:利用String方法,public static String valueOf(数据类型 b)

1
2
3
4
5
6
7
java复制代码public class BaoZhuangLei{
public static void main(String args[]){
int num = 100;
String str =String.valueOf(num);//int——>String
System.out.println(str.length());
}
}

包的定义

在Java程序之中的包,主要的目的是可以将不同功能的文件进行分割,在之前的代码开发之中,所有的程序都保存在了同一个目录之中,这样一来所带来的问题:如果出现了同名的文件,那么会发生覆盖问题,因为在同一个目录之中不允许有重名的文件,而在不同的目录下可以有重名文件,所谓的包实际上指的就是文件夹。

1
2
3
4
5
6
java复制代码package cn.mldn.demo;//定义包
public class Hello{
public static void main(String args[]){
System.out.println("Hello World");
}
}

一旦定义完成之后,那么这个类的名字就成了“cn.mldn.demo.Hello”,即这既是完整的类名称,而在进行程序编译的时候也需要将*.class文件保存在包之中,于是为了方便开发,那么也就提供了一个打包的编译操作。

打包编译:javac -d . 类.java

-d:表示生成目录,根据package定义生成

-“.”:再当前目录下生成*.class

类.java:编译源程序代码

这个时候类的名字必须带上包的名称,所以执行类的时候:java cn.mldn.demo.Hello,也就是说完整类的名称就是“包.类”,而在所有的开发之中,没有包的类是绝对不存在的,只要是程序一定要有包。

包的导入

既然使用包可以将一个大型的程序拆分成不同的功能目录保存,那么这些不同的包之间也一定会存在包的导入问题,而导入包在程序之中使用import完成,下面通过一个程序进行演示。

1
2
3
4
5
6
7
java复制代码//定义一个Message
package cn.mldn.util;//打包
class Massage{
public String print(){
return "Hello World";
}
}
1
2
3
4
5
6
7
8
9
java复制代码//定义另外一个类使用Message类
package cn.mldn.text;//打包
import cn.mldn.util.Message;//导入包
public class Text{
public static void main(String args[]){
Massage msg = new cn.mldn.util.Massage();
System.out.println(msg.print());
}
}

这个时候上面的两个类应该是按照顺序编译:

​ 应该首先编译Message.java程序:javac –d . Message.java;

​ 再次编译Test.java程序:javac –d . Test.java,但是这个时候出现了一下的错误提示:

1
2
3
4
java复制代码Text.java:5: 错误: Massage在cn.mldn.util中不是公共的; 无法从外部程序包中对其进行
访问
Massage msg = new cn.mldn.util.Massage();
^

提示:关于public class 和class定义类的区别

​ Public class:文件名和类名称保持一致,在一个*.java文件之中只能存在一个public class定义,如果一个类要想被外部包所访问必须定义为public;

​ Class:文件名称可以和类名称不一致,在一个*.java之中可以同事存在多个class定义,并且编译完成之后会形成多个*.class文件,使用class定义的类只能够在一个包中访问,不同包之间无法访问。

1
2
3
4
5
6
java复制代码package cn.mldn.util;//打包
public class Massage{
public String print(){
return "Hello World";
}
}

但是同时也发现了一个问题,现在这些类在编译的时候要有顺序,实在很麻烦,为此在java之中专门提供了一个可以进行自动连编的操作,编译的时候使用*.java:javac –d . .java,将一个目录之中所有的.java文件进行编译。

​ 但是以上的代码还有一个小问题:程序在进行导入的时候使用了“包.类”的完整名称完成的,但是如果在一个程序之中要同时导入一个包的多个类的时候,那么分开去编写实在很麻烦,为此可以使用通配符“*”完成导入。

1
2
3
4
5
6
7
8
java复制代码package cn.mldn.text;//打包
import cn.mldn.util.*;//导入包
public class Text{
public static void main(String args[]){
Massage msg = new cn.mldn.util.Massage();
System.out.println(msg.print());
}
}

但是需要注意的是,在java之中使用“”或者是的单独导入,其从实际的操作性能上是没有任何区别的,因为即使使用了也表示导入所需要的类,不需要的不导入。

​ 可是在导入包的时候也会遇到一种比较麻烦的问题:会导入不同包的同名类,例如:对于Message类,现在在两个包中都有:cn.mldn.util cn.mldn.info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package cn.mldn.text;//打包
import cn.mldn.util.*;//导入包
import cn.mldn.info.*;//导入包
public class Text{
public static void main(String args[]){
Message msg = new cn.mldn.util.Message();
System.out.println(msg.print());
}
}
/*
Text.java:6: 错误: 对Message的引用不明确, cn.mldn.info中的类 cn.mldn.info.Messag
e和cn.mldn.util中的类 cn.mldn.util.Message都匹配
Message msg = new cn.mldn.util.Message();
^
*/

由于某种需要,同时导入两个包,这个时候要使用Message类的时候必须加上类的全名。

1
2
3
4
5
6
7
8
9
java复制代码package cn.mldn.text;//打包
import cn.mldn.util.*;//导入包
import cn.mldn.info.*;//导入包
public class Text{
public static void main(String args[]){
cn.mldn.util.Message msg = new cn.mldn.util.Message();
System.out.println(msg.print());
}
}

访问控制权限

之前学习到的private就属于一种访问控制权限,而这种访问控制权限只是封装的一部分,再java里面提供有四种访问控制权限:private、default、protected、public,而这四种访问控制权限定义如下:

范围 private default protected public
同一包中的同一类 √ √ √ √
同一包中不同类 √ √ √
不同包中的子类 √ √
不同包中的非子类 √

实际上public永远都可以访问,但是对于封装而言主要使用三个权限:private、default、protected。

观察protected访问权限

Info.java

1
2
3
4
5
6
java复制代码package cn.sxau.demo.a;
public class Info {
//protected权限
protected String str = "www.baidu.com";

}

SubInfo.java

1
2
3
4
5
6
7
java复制代码package cn.sxau.demo.a;
import cn.sxau.demo.a.Info;
public class SubInfo extends Info{
public void print(){
System.out.println(super.str);
}
}

TestInfo.java

1
2
3
4
5
6
7
java复制代码package cn.sxau.testab;
import cn.sxau.demo.a.SubInfo;
public class TestInfo{
public static void main(String args[]){
new SubInfo().print();
}
}

image-20210807211643215

可以发现SubInfo继承的子类Info不在同一个包内,但是在同一个子类中,并且可以调用其子类。

错误代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package cn.sxau.testab;
import cn.sxau.demo.a.Info;
public class TestInfo{
public static void main(String args[]){
System.out.println(new Info().str);
}
}
/*
F:\java\javabase\day09>javac -d . TestInfo.java
TestInfo.java:5: 错误: str 在 Info 中是 protected 访问控制
System.out.println(new Info().str);
^
1 个错误
*/

原因是str是protected权限,所以在不同类非子类的类中是无法访问。

对于权限的选择

  • 对于封装的描述大部分情况下都使用的是private,很少的情况下使用protected,这两个都叫封装
  • 属性都是以private,方法都使用public。

封装性就是指private、protected、default三个权限的使用。

jar命令

Jar是一种java给出的压缩格式文件,即:可以将*.class文件以*.jar压缩包的方式给用户,这样方便程序的维护,如果要使用jar的话,可以直接利用JDK给出的jar命令完成。

image-20210807214226657

c:创建一个新的归档文件

f:指定jar的文件名称,由用户制定一个*.jar的文件名。

v:生成标准的压缩信息

Message.java

1
2
3
4
5
6
java复制代码package cn.sxau.util;//打包
public class Message{
public String print(){
return "hello world";
}
}
  1. 将Message.java程序进行编译:javac –d . Message.java,生成包.类;
  2. 将“包.类”压缩成my.jar文件:jar –cvf my.jar cn,出现了一个my.jar包;

此时my.jar就包含了所需要的程序使用类

现在my.jar和MyTest.java处于同一目录之中。但是发现找不到my.jar之中定义的内容,这是因为在java之中每一个*.jar文件都属于一个独立的CLASSPATH路径,如果要想使用,必须配置CLASSPATH。

在测试之前需要在cmd配置SET CLASSPATH=.;F:\java\javabase\day09\my.jar

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//定义一个测试类 调用my.jar
package cn.sxau.test;
public class MyTest{
public static void main(String args[]){
cn.sxau.util.Message msg = new cn.sxau.util.Message();
System.out.println(msg.print());
}
}
/*
执行
F:\java\javabase\day09>java cn.sxau.test.MyTest
hello world
*/

本文转载自: 掘金

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

Flowable最新版670入门篇之基于JavaAPI的

发表于 2021-11-09

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

作者:汤圆

个人博客:javalover.cc

前言

前面我们接触了很多Flowable以及Flowable UI相关方面的例子;

这篇我们再通过一个例子,来系统地了解一下BPMN以及Flowable的相关概念;

为了熟悉Flowable的相关API,这里我们用的例子是基于JavaAPI的。

同时也有用到flowable-ui相关知识

目录

  1. 背景介绍
  2. 流程定义
  3. 部署流程
  4. 配置用户、组、权限
  5. UI界面启动流程
  6. 代码层面启动流程

正文

1. 背景介绍

这里的案例是基于一个公司来讲的,比如一个公司有股东,有经理,有财务;

每个月财务都需要把报表提交给上级经理,经理审批通过后,会发送给股东。

2. 流程定义

这里我们先看一个图,如下所示:这里我们直接引用官方的图

image-20211109143551966

其中包括的元素有:

  • 空的启动事件
  • 用户任务-写每个月的财务报表
  • 用户任务-审核每个月的财务报表
  • 空的结束事件

下面我们就来编写这个bpmn.xml文件:FinancialReportProcess.bpmn20.xml

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
xml复制代码<definitions id="definitions"
targetNamespace="http://flowable.org/bpmn20"
xmlns:flowable="http://flowable.org/bpmn"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">

<process id="financialReport" name="Monthly financial report reminder process">

<startEvent id="theStart" />

<sequenceFlow id="flow1" sourceRef="theStart" targetRef="writeReportTask" />

<userTask id="writeReportTask" name="Write monthly financial report" >
<documentation>
Write monthly financial report for publication to shareholders.
</documentation>
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>accountancy</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>

<sequenceFlow id="flow2" sourceRef="writeReportTask" targetRef="verifyReportTask" />

<userTask id="verifyReportTask" name="Verify monthly financial report" >
<documentation>
Verify monthly financial report composed by the accountancy department.
This financial report is going to be sent to all the company shareholders.
</documentation>
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>management</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>

<sequenceFlow id="flow3" sourceRef="verifyReportTask" targetRef="theEnd" />

<endEvent id="theEnd" />

</process>

</definitions>

3. 部署流程

这里我们创建一个maven程序,将刚才创建的流程定义部署到资源目录下,完整路径为:src/main/resources/FinancialReportProcess.bpmn20.xml

主程序如下所示:这里我们直接运行主程序,就可以把流程定义部署到flowable中,这里的实际存储地址就是程序中的那个h2数据库

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
java复制代码package org.flowable;

import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FinancialReport {
public static void main(String[] args) {
// 1. 流程引擎
// 1.1 流程引擎 配置
ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:h2:~/flowable-db/engine-db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9093;DB_CLOSE_DELAY=-1")
.setJdbcUsername("flowable")
.setJdbcPassword("flowable")
.setJdbcDriver("org.h2.Driver")
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
// 1.2 构建流程引擎
ProcessEngine processEngine = cfg.buildProcessEngine();

// 2. 流程定义
// 2.1 将 流程定义文件 部署到 流程引擎 中
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();

}
}

PS:程序中的h2数据库配置 跟 官方的 flowable-ui.war 例子中的 h2 数据库是保持一致的(通过解压flowable-ui.war即可看到相关参数配置),方便后续的界面化操作

这里我们需要明白一点:流程启动后,每执行一步,都会将这一步的状态和数据保存到数据库中,此时就算流程重启,数据也不会丢失;

比如流程启动后,执行了空的启动事件,此时来到第一个用户任务:任务内容是填写财务报表,任务的分配人/组是财务组,任务的状态为等待,那么这几个数据就会保存到数据库中;

当这个任务完成时,流程才会继续往下走,走到下一个任务,继续等待(同时存储任务的相关数据)。

这里的部署其实有很多种方式,详细参考:wwv.flowable.com/open-source…

4. 配置用户、组、权限

下面我们需要涉及到之前flowable-ui介绍的东西;

先运行flowable-ui.war程序:下载地址

1
bash复制代码java -jar flowable-ui.war

启动成功之后,访问: http://localhost:8080/flowable-ui/

用admin/test登录之后,分别执行如下操作:

  • 进入IDM管理界面,创建两个用户:jalon 和 tangyuan(这个名字随便起)
  • 给上面这两个用户分配工作流权限
  • 创建两个组:accountancy 和 management(一个是财务组,一个是经理组,在xml中有定义)
  • 将jalon添加到accountancy 组,将 tangyuan 添加到 management 这个组

5. UI界面启动流程

  • 然后用jalon账号登录,进入任务应用程序之后,点击启动流程,就能看到刚才部署的流程定义

image-20211109153652930

  • 启动之后,点击活动任务进去,然后认领任务,因为这里没有表单需要填写,所以直接点击完成即可

image-20211109153847038

image-20211109153924333

image-20211109154012249

  • 然后用tangyuan账号登录,就可以看到jalon启动的流程,操作步骤类似,先认领,再完成

6. 代码层面启动流程

上面我们用UI实现了一个流程的完整过程,下面我们用代码来实现下:

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
70
71
72
73
74
75
76
77
78
java复制代码package org.flowable;

import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FinancialReport {
public static void main(String[] args) {
// 1. 流程引擎
// 1.1 流程引擎 配置
ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:h2:~/flowable-db/engine-db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9093;DB_CLOSE_DELAY=-1")
.setJdbcUsername("flowable")
.setJdbcPassword("flowable")
.setJdbcDriver("org.h2.Driver")
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
// 1.2 构建流程引擎
ProcessEngine processEngine = cfg.buildProcessEngine();

// 2. 流程定义
// 2.1 将 流程定义文件 部署到 流程引擎 中
// RepositoryService repositoryService = processEngine.getRepositoryService();
// Deployment deployment = repositoryService.createDeployment()
// .addClasspathResource("FinancialReportProcess.bpmn20.xml")
// .deploy();
// 3. 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
String procId = runtimeService.startProcessInstanceByKey("financialReport").getId();

// 4. 领取任务
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
for (Task task : tasks) {
System.out.println("Following task is available for accountancy group: " + task.getName());

// claim it
taskService.claim(task.getId(), "jalon");
}

// 查看领取的任务
tasks = taskService.createTaskQuery().taskAssignee("jalon").list();
for (Task task : tasks) {
System.out.println("Task for jalon: " + task.getName());

// Complete the task
taskService.complete(task.getId());
}

System.out.println("Number of tasks for jalon: "
+ taskService.createTaskQuery().taskAssignee("jalon").count());

// 5. 经理领取任务
tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
System.out.println("Following task is available for management group: " + task.getName());
taskService.claim(task.getId(), "tangyuan");
}

// 6. 完成任务
for (Task task : tasks) {
taskService.complete(task.getId());
}

// 7. 查看任务是否完成
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
}
}

源码

上传在github

总结

这一篇主要是将代码和UI结合起来做了演示,虽然例子简单,但是概念都大同小异;

本文转载自: 掘金

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

1…386387388…956

开发者博客

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