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

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


  • 首页

  • 归档

  • 搜索

一种服务端分层架构设计方法

发表于 2021-11-07

如果在服务器端的技术领域选择一个架构设计方法,那一定是“分层架构设计”方法。

前言

“分层”是一种思维方式,一种解决复杂性问题的方式,也是一种体系结构。

“分层体系结构”指的是将系统的组件分隔到不同的层中,每一层中的组件应保持内聚性,并且应大致在同一抽象级别;每一层都应与它下面的各层保持松散耦合。“分层架构设计”是一种采用分层思维方式在软件架构设计领域进行设计的体系化方法。

分层设计为什么适用范围非常广,因为这个设计方法包含了两个优点,而这两个优点正好对应了架构设计的六字真言。

架构设计六字真言:高内聚低耦合

分层设计优点:

  • 用Layer分层定义了高内聚的聚类方式
  • 用上层只能调用下层定义了低耦合通讯方式

正文

软件研发人员都是专业人士,专业人士要能用专业词汇进行定义描述。架构风格和模式的一种专业描述架构设计的方法。“分层架构设计”的风格和模式描述如下。

  • 架构风格 - 分层架构,约束(分层,上层依赖下层,下层不依赖上层)
  • 架构模式 - 三层系统架构、N-Layer层系统架构

三层系统架构

三层系统架构是服务器端研发人员张口就来的设计方法。分层架构有一个通用的切分规则是“技术横切业务竖切”,三层系统架构的三层主要指的是三个横切的技术层,主要用于技术组件的高内聚低耦合。三层的主要特点如下。

层次 特性
表现层 又称UI层,表现层支撑用户访问,需要提供API供外部用户访问,主要定义用户行为的输入输出。
业务层 又称业务逻辑层,主要进行用户场景的业务逻辑处理,包括输入数据的业务合法性检查和业务逻辑处理等。
持久化层 又称数据库层,支撑业务数据的持久化存储,一般Web系统指的是关系型数据库。

三层系统架构和MVC是什么关系?

有同学把三层系统架构和MVC弄混,其实这两个是不同维度上的设计方法,一般认为MVC(Model-View-Controller)是“表现层”的一种设计方法。

N-Layer层系统架构

既然有三层系统架构,那么肯定也有更多层系统架构设计,比如在设计层面也可以把数据库当做单独的一层,形成四层架构设计。

四层架构设计:

2.png

同理除了四层还可以有五层/六层等设计方法。

六边形系统架构

同理继续扩展分层架构的设计方法,除了上下的分层也可以是内外的分层,六边形架构设计方法就是一种内外分层的设计方法。

分层架构设计的目的是降低耦合提高功能复用,但是不可避免的将业务逻辑拆散在不同层次中了,反而导致了核心业务逻辑的离散化,离散化导致持续迭代的难度增高和不能独立测试验证业务逻辑,并且随着微服务架构的流行,软件架构设计也从一维的设计发展为多维的设计,原有的上下分层架构设计方法无法很好地支撑这种变化。

六边形架构设计:
3.webp

六边形架构设计从内到外分为业务核心层、支撑层、适配器层,这种设计方法将业务、内部技术和外部技术进行区分,有效的防止了业务逻辑泄露到多层的问题,并且通过适配器设计方法可以支撑替换三方框架服务,进行独立业务逻辑验证的目的。

案例

最知名的一种分层设计成果是TCP/IP的七层网络协议和五层模型。

5.jpeg

总结

有一种采用“南北东西”进行设计描述的方法,这也是一种分层架构设计的方法,相比于东南西北我更喜欢分层架构设计,一个“分层”概括了这个架构的所有特点,也体现了解决复杂性问题的本质:分解切分。

如果用一个类比思维,设计更像是切蛋糕,除了竖着切之外,还可以横着切、斜着切、和盘子一起切、连着桌子一起切、组合切分等方法,每种切分方法都可以像是“六边形架构”一样可以命名一种新的架构设计方法。

9.jpeg

架构设计不是一蹴而就的,一般情况更多是确定架构风格达成团队共识后,逐步进行演进设计。

1.png

参考

  • 架构风格和模式
  • 一文读懂架构设计
  • The C4 model

本文转载自: 掘金

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

Spring 事务的传播行为

发表于 2021-11-07

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

传播行为

事务的第一个方面是传播行为(propagation behavior)。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:

传播行为 含义
PROPAGATION_REQUIRED 当前方法必须运行在事务中,如果当前存在事务,则加入该事务;否则创建一个新的事务。
PROPAGATION_SUPPORTS 不需要事务上下文,如果当前存在事务,则加入该事务;否则以非事务的方式继续运行。
PROPAGATION_MANDATORY 当前方法必须运行在事务中, 如果当前存在事务,则加入该事务;否则抛出异常。(mandatory:强制性)
PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NOT_SUPPORTED 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。

详解

PROPAGATION_REQUIRED

(1)PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//事务属性 PROPAGATION_REQUIRED
methodA{
……
methodB();
……
}

//事务属性 PROPAGATION_REQUIRED
methodB{
……
}

使用spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。
单独调用methodB方法:

1
2
3
java复制代码main{
methodB();
}

相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码Main{
Connection con=null;
try{
con = getConnection();
con.setAutoCommit(false);
//方法调用
methodB();
//提交事务
con.commit();
} catch(RuntimeException ex) {
//回滚事务
con.rollback();
} finally {
//释放资源
closeCon();
}
}

Spring保证在methodB方法中所有的调用都获得到一个相同的连接。在调用methodB时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。
单独调用MethodA时,在MethodA内又会调用MethodB。
执行效果相当于:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码main{
Connection con = null;
try{
con = getConnection();
methodA();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
closeCon();
}
}

调用MethodA时,环境中没有事务,所以开启一个新的事务。当在MethodA中调用MethodB时,环境中已经有了一个事务,所以methodB就加入当前事务。
​

PROPAGATION_SUPPORTS

(2)PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。

1
2
3
4
5
6
7
8
9
java复制代码//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_SUPPORTS
methodB(){
……
}

单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
​

PROPAGATION_MANDATORY

(3)PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。

1
2
3
4
5
6
7
8
9
java复制代码//事务属性 PROPAGATION_REQUIRED
methodA(){
methodB();
}

//事务属性 PROPAGATION_MANDATORY
methodB(){
……
}

当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常

throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);

当调用methodA时,methodB则加入到methodA的事务中,事务地执行。
​

PROPAGATION_REQUIRES_NEW

(4)PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_REQUIRES_NEW
methodB(){
……
}

调用A方法:

1
2
3
java复制代码main(){
methodA();
}

相当于

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
java复制代码main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}

//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}

在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了。使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
​

PROPAGATION_NOT_SUPPORTED

(5)PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。(代码示例同上,可同理推出)
​

PROPAGATION_NEVER

(6)PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。
​

PROPAGATION_NESTED

(7)PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务,则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。需要JDBC 驱动的java.sql.Savepoint类。有一些JTA的事务管理器实现可能也提供了同样的功能。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true;而 nestedTransactionAllowed属性值默认为false。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//事务属性 PROPAGATION_REQUIRED
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

//事务属性 PROPAGATION_NESTED
methodB(){
……
}

如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}

当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。

嵌套事务

嵌套事务一个非常重要的概念:就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别

它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。

使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。

PROPAGATION_REQUIRES_NEW 启动一个新的,不依赖于环境的 “内部” 事务。这个事务将被完全commited 或 rolled back 而不依赖于外部事务,它拥有自己的隔离范围,自己的锁,等等。当内部事务开始执行时,外部事务将被挂起, 内务事务结束时,外部事务将继续执行。

另一方面,PROPAGATION_NESTED 开始一个 “嵌套的” 事务,它是已经存在事务的一个真正的子事务。潜套事务开始执行时,它将取得一个 savepoint。如果这个嵌套事务失败,我们将回滚到此 savepoint。潜套事务是外部事务的一部分,只有外部事务结束后它才会被提交。

由此可见,PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于,PROPAGATION_REQUIRES_NEW 完全是一个新的事务,而 PROPAGATION_NESTED 则是外部事务的子事务,如果外部事务 commit,嵌套事务也会被 commit,这个规则同样适用于 roll back。

PROPAGATION_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。

参考—Spring 事务机制详解

本文转载自: 掘金

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

架构整洁之道-03 编程范式-函数式编程

发表于 2021-11-07

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

函数式编程

架构设计另一个编程范式—函数式编程,其主要关心数据到数据之间的映射关系,即将计算过程抽象描述成一种表达式求值。先看下以下实现数组转换成数组对象的函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码// 实现数组转换数组对象
function AryToObjectAry() {
 const ary = ["jasen-yang", "tom-han", "marry-han", "lucy-any"];
 const newObj = [];
​
 ary.forEach((value) => {
   let names = value.split("-");
   let newNames = [];
   names.forEach((name) => {
     const temp = name[0].toUpperCase() + name.slice(1);
     newNames.push(temp);
  });
   newObj.push({ name: newNames.join(" ") });
});
 console.log(newObj);
 return newObj;
}
​
(()=>{
   AryToObjectAry()
})();
​
// 输出结果
[ { name: 'Jasen Yang' }, { name: 'Tom Han' }, { name: 'Marry Han' }, { name: 'Lucy Any' }]

定义变量,循环数组,把值开头大写,每个函数各司其职,按一定的步骤,从函数输入到输出结果。从代码上阅读可以很清楚知道在做什么,但是一但出问题就很难定位,其中掺杂了大些逻辑和变量。

如果每个过程都是一个函数,会怎么样?看下以下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码​
function convertAry() {
 const ary = ["jasen-yang", "tom-han", "marry-han", "lucy-any"];
​
 const getNames = (value) => {
   return value.split("-");
};
 const toUpper = (value) => {
   return value[0].toUpperCase() + value.slice(1);
};
 const convertName = (newObj,value) => {
   return newObj.push({ name: value });
};
​
 const newObj = [];
 ary.forEach((value)=>{
   const newName = compose(getNames(value),toUpper);
   const result = compose(newObj,convertName(newName));
});
 return newObj;
}

看整个编程思路,可以清晰看出,函数式编程,重点是函数而不是过程,通过函数的组合变换去解决问题。每个函数可以看出在做什么,一目了然,函数让代码更加语义化,可读性更高。

函数式编程,重点是在构建关系,通过构建一条高效的流水线,一次解决所有的问题,而不是关注各个函数之间的数据。

函数式编程特点

函数式一等公民

在函数式编程中,变量是不能被修改的,所有的变量只能被赋值一次,所有的值全都靠传参来解决的。函数作为一等公民,可以在任何地方定义,可以作为参数和返回值,可以对函数进行组合。

无状态和数据不可变

函数式编程的核心

  • 数据不可变,若修改一个对象,应建一个新对象而不是修改已有的对象
  • 无状态,对于一个函数,无论何时运行,在给定的相同输入都能输出相同结果,完全不依赖外部状态的变化

惰性执行

函数只在需要的适合执行,不产生无意义的中间变量。比如,在处理过程中都在写函数,而只有在最后才去产生实际的结果。

参考资料

  • mp.weixin.qq.com/s/ezHCBGr6S…
  • mp.weixin.qq.com/s/BzMHd4KNb…

\

本文转载自: 掘金

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

从k8s集群主节点数量为什么是奇数来聊聊分布式系统 作者:肥

发表于 2021-11-07

作者:肥嘟嘟左卫门熊

xiongji

前言

今天简单聊一聊一个小问题,即为什么k8s的集群主节点数量通常是奇数,且3或5个居多?

我们先抛出答案

  • 2467等数量的主节点也是可以的,但是不推荐的的原因如下

1. 奇数的原因是防止资源的浪费

k8s的一致性算法RAFT,要求集群需要数量大于(n/2)的正常主节点才能提供服务(n为主节点数)

因此3个主节点有1个节点的容错率,而4个主节点也只有1个节点的容错率

2. 三个或五个也是平衡的考虑

3或5则是因为1个没有容错率,7个主节或更多将导致确定集群成员和仲裁的开销加大,不建议这样做

脑裂现象?

这是在Elasticsearch、ZooKeeper、k8s集群都会出现的现象

集群中的Master或Leader节点往往是通过选举产生的。

在网络正常的情况下,可以顺利的选举出Leader。但当两个机房之间的网络通信出现故障时,选举机制就有可能在不同的网络分区中选出两个Leader。当网络恢复时,这两个Leader该如何处理数据同步?又该听谁的?这也就出现了“脑裂”现象。

举例

某公司有机房如下

pic1

如果当有一天电缆被挖怀了,上海的机房和昆明的机房没法连通了,会存在什么情况呢?

pic2

情况一:原来的leader是在上海的服务器

我们假设原来的leader是在上海的服务器,那么昆明的两台服务器由于电缆被挖断,无法与leader建立连接,那么昆明的两台服务器就会认为主节点挂了,开始选举leader,2台服务器会把票投给其中的一台,2<5/2,不满足集群中存活的节点数必须要超过总节点数的半数才能继续提供服务的规定,所以昆明的机房是不能继续提供服务的。上海机房的存活数量为3台,组成了一个只有3个节点的小集群,3>5/2,所以上海的机房能继续提供服务。

情况二:原来的leader是在昆明的服务器

我们假设原来的leader是在昆明的服务器,那么上海的机房由于和leader断开,上海的机房会开始重新选举leader,上海机房的存活数量为3台,组成了一个只有3个节点的小集群,3>5/2,所以上海的机房能继续提供服务。昆明只剩下两台服务器,不满足集群中存活的节点数必须要超过总节点数的半数才能继续提供服务的规定,昆明机房是不能继续提供服务。

也就是说,即使出现两个机房网络通信断开的情况,ZK由于集群中存活的节点数必须要超过总节点数的半数才能继续提供服务这个规定,也只会有一个leader对外服务,不会出现各个机房分选出自己的leader的情况,这样就避免了脑裂(多个主节点)的问题。

我们设想一下如果没有集群中存活的节点数必须要超过总节点数的半数才能继续提供服务这个规定,当出现故障的时候就可能会存在多个leader,就会造成数据不一致,各个机房自己玩自己的,而这是很致命的。

那么为什么一定要是超过半数,不能是等于半数呢?

我们还是以上面的图来说,如果上海和昆明各有3台服务器,总数是6台服务器。两个机房网络断开的时候,如果是等于半数,就会分选出各自的leader,出现脑裂的问题。

脑裂现象总结

为了避免脑裂的问题,给出了一个规定:集群中存活的节点数必须要超过总节点数的半数才能继续提供服务,而正是由于这个规定,导致集群中n台和n+1台你的容灾能力是一样的(n为奇数),都只能坏一台。

k8s的一致性算法(consensus algorithm)

k8s使用RAFT作为它的一致性算法,使用该算法的还有etcd等。目的是实现分布式系统所需要的特性。(见下面条目分布式系统的挑战)

RAFT 算法只能容错故障节点,并且最大容错节点数为(n-1)/2。

分布式系统的挑战

  • 一致性
  • 时序性
  • 并发性
  • 健壮性

有哪些一致性

  • 逻辑时间的一致性:决定事件发生的顺序
  • 互斥性的一致性:访问资源的所有权
  • 协调者的共识:谁是当前的leader

为什么分布式系统需要leader?

分布式系统中很多类任务都需要单个进程(或实例或节点)扮演居中协调的协调者角色(coordinator),比如数据备份管理、组成员通信或原子性提交协议制定等,这个协调者角色就是leader或master。

选举

在RAFT算法里,有如下的概念

名词解释

  • term 任期:数字越大表示是越新的任期
  • requestVote:当node被索要投票时,如果没有投给别人就会投给索要的人

比如NodeA的term是1,向term为0的NodeB和NodeC索要投票,此时NodeB和NodeC的term会变成1,一个term内只能投票一次

  • log entry: 操作条目日志
  • appending entry:追加条目,即集群进行的操作
  • 平票情况:平票时,节点会震荡直到有一个leader产生,由于timeout是一个随机数,所以很少会出现平票

两个timeout

  • election timeout: 竞选倒计时,没有在倒计时前收到leader消息就会转为candidate
  • heartbeat timeout:leader间断性的发送心跳,表明自己正常

日志复制(log replication)

之所以需要log replication,为了即使leader挂了,剩下的node也可以恢复日志,是一个容错机制

日志复制过程

  1. 系统的所有更改现在都通过领导者,每次更改都在节点日志中的添加记录
  2. 客户端发送一个变更请求
  3. 该日志记录当前没有提交,所以不会更新节点的值。
  4. 要提交记录,节点首先将其复制到follower节点…
  5. 然后leader等待,直到大多数节点都记完了这条记录
  6. leader得到反馈后,记录已commit到领导者节点上,并且节点状态为“ 5 ”
  7. 然后领导者通知跟随者该记录已commit
  8. 现在,集群已就系统状态达成共识

要求节点数大于n/2的逻辑

当一个请求进入时,leader会同步到所有node,当大多数node认可时,才会执行commit

所以当5个节点因网络问题被为2和3时,由于2个节点的集群无法得到大多数节点的认可,因此无法commit

而如果3集群选举出了新节点,那么可以继续提供服务,网络问题修复后,由于3是新选举出的节点,因此3的term更高,2节点会被同步3节点的信息

参考资料

  1. 可视化raft
  2. 易于理解的分布式共识算法,Raft! | 神奇代码在哪里第10期
  3. Zookeeper脑裂问题以及为什么推荐奇数节点讲解

本文使用 文章同步助手 同步

本文转载自: 掘金

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

【转载】泛化之美 —— C++11 可变模版参数的妙用(下)

发表于 2021-11-07

参考原文地址: 泛化之美 —— C++11 可变模版参数的妙用

我对文章的格式和错别字进行了调整,并补充并标注出了重要的部分。以下是正文。

正文

可变参数模板类

可变参数模板类是一个带可变参数的模板类,比如 C++11 中的元组 std::tuple 就是一个可变模板类,它的定义如下:

1
2
cpp复制代码template <class... Types>
class tuple;

这个可变参数模板类可以携带任意类型任意个数的模板参数:

1
2
3
cpp复制代码std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, "");

可变参数模板类的参数个数可以为 0 个,所以下面的定义也是也是合法的:

1
cpp复制代码std::tuple<> tp;

可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。下面我们来看一下展开可变参数模板类中的参数包的方法。

模版偏特化和递归方式来展开参数包

可变参数模板类的展开一般需要定义两到三个类,包括类声明和偏特化的模板类。如下方式定义了一个基本的可变参数模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpp复制代码// 前向声明
template <typename... Args>
struct Sum;

// 基本定义
template <typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

// 递归终止
template <typename Last>
struct Sum<Last>
{
enum
{
value = sizeof(Last)
};
};

这个 Sum 类的作用是在编译期计算出参数包中参数类型的 size 之和,通过 Sum<int, double, short>::value 就可以获取这 3 个类型的 size 之和为 14。这是一个简单的通过可变参数模板类计算的例子,可以看到一个基本的可变参数模板应用类由三部分组成:

第一部分是:

1
2
cpp复制代码template <typename... Args>
struct sum

它是前向声明,声明这个 Sum 类是一个可变参数模板类;

第二部分是类的定义:

1
2
3
4
5
6
7
8
cpp复制代码template <typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

它定义了一个部分展开的可变参数模板类,告诉编译器如何递归展开参数包。

第三部分是特化的递归终止类:

1
2
3
4
5
6
7
8
cpp复制代码template <typename Last>
struct sum<last>
{
enum
{
value = sizeof(First)
};
}

通过这个特化的类来终止递归:

1
2
cpp复制代码template <typename First, typename... Args>
struct sum;

这个前向声明要求 Sum 的模板参数至少有一个,因为可变参数模板中的模板参数可以有 0 个,有时候 0 个模板参数没有意义,就可以通过上面的声明方式来限定模板参数不能为 0 个。上面的这种三段式的定义也可以改为两段式的,可以将前向声明去掉,这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cpp复制代码template <typename First, typename... Rest>
struct Sum
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

template <typename Last>
struct Sum<Last>
{
enum
{
value = sizeof(Last)
};
};

上面的方式只要一个基本的模板类定义和一个特化的终止函数就行了,而且限定了模板参数至少有一个。

递归终止模板类可以有多种写法,比如上例的递归终止模板类还可以这样写

1
2
3
4
5
6
7
8
9
10
cpp复制代码template <typename... Args>
struct sum;
template <typename First, typenameLast>
struct sum<First, Last>
{
enum
{
value = sizeof(First) + sizeof(Last)
};
};

在展开到最后两个参数时终止。

还可以在展开到 0 个参数时终止:

1
2
3
4
5
6
7
8
cpp复制代码template <>
struct sum<>
{
enum
{
value = 0
};
};

还可以使用 std::integral_constant 来消除枚举定义 value 。利用 std::integral_constant 可以获得编译期常量的特性,可以将前面的 sum 例子改为这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码//前向声明
template <typename First, typename... Args>
struct Sum;

//基本定义
template <typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};

//递归终止
template <typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};
sum<int, double, short>::value; //值为14

继承方式展开参数包

还可以通过继承方式来展开参数包,比如下面的例子就是通过继承的方式去展开参数包:

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
cpp复制代码//整型序列的定义
template <int...>
struct IndexSeq
{
};

//继承方式,开始展开参数包
template <int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...>
{
};

// 模板特化,终止展开参数包的条件
template <int... Indexes>
struct MakeIndexes<0, Indexes...>
{
typedef IndexSeq<Indexes...> type;
};

int main()
{
using T = MakeIndexes<3>::type;
cout << typeid(T).name() << endl;
return 0;
}

其中 MakeIndexes 的作用是为了生成一个可变参数模板类的整数序列,最终输出的类型是:struct IndexSeq<0,1,2>。

MakeIndexes 继承于自身的一个特化的模板类,这个特化的模板类同时也在展开参数包,这个展开过程是通过继承发起的,直到遇到特化的终止条件展开过程才结束。 MakeIndexes<1,2,3>::type 的展开过程是这样的:

1
2
3
4
5
6
cpp复制代码MakeIndexes<3> : MakeIndexes<2, 2> {}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2> {}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2>
{
typedef IndexSeq<0, 1, 2> type;
}

通过不断的继承递归调用,最终得到整型序列 IndexSeq<0, 1, 2> 。

如果不希望通过继承方式去生成整型序列,则可以通过下面的方式生成。

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码template <int N, int... Indexes>
struct MakeIndexes3
{
using type = typename MakeIndexes3<N - 1, N - 1, Indexes...>::type;
};

template <int... Indexes>
struct MakeIndexes3<0, Indexes...>
{
typedef IndexSeq<Indexes...> type;
};

我们看到了如何利用递归以及偏特化等方法来展开可变模板参数,那么实际当中我们会怎么去使用它呢?我们可以用可变模版参数来消除一些重复的代码以及实现一些高级功能,下面我们来看看可变模版参数的一些应用。

可变参数模板消除重复代码

C++11 之前如果要写一个泛化的工厂函数,这个工厂函数能接受任意类型的入参,并且参数个数要能满足大部分的应用需求的话,我们不得不定义很多重复的模版定义,比如下面的代码:

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
cpp复制代码template <typename T>
T *Instance()
{
return new T();
}

template <typename T, typename T0>
T *Instance(T0 arg0)
{
return new T(arg0);
}

template <typename T, typename T0, typename T1>
T *Instance(T0 arg0, T1 arg1)
{
return new T(arg0, arg1);
}

template <typename T, typename T0, typename T1, typename T2>
T *Instance(T0 arg0, T1 arg1, T2 arg2)
{
return new T(arg0, arg1, arg2);
}

template <typename T, typename T0, typename T1, typename T2, typename T3>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3)
{
return new T(arg0, arg1, arg2, arg3);
}

template <typename T, typename T0, typename T1, typename T2, typename T3, typename T4>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return new T(arg0, arg1, arg2, arg3, arg4);
}
struct A
{
A(int) {}
};

struct B
{
B(int, double) {}
};
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);

可以看到这个泛型工厂函数存在大量的重复的模板定义,并且限定了模板参数。用可变模板参数可以消除重复,同时去掉参数个数的限制,代码很简洁, 通过可变参数模版优化后的工厂函数如下:

1
2
3
4
5
6
7
cpp复制代码template <typename… Args>
T *Instance(Args &&… args)
{
return new T(std::forward<Args>(args)…);
}
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);

可变参数模板实现泛化的 delegate

C++ 中没有类似 C# 的委托,我们可以借助可变模版参数来实现一个。C# 中的委托的基本用法是这样的:

1
2
3
4
5
6
7
8
9
cpp复制代码delegate int AggregateDelegate(int x, int y); //声明委托类型

int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }

AggregateDelegate add = Add;
add(1, 2); //调用委托对象求和
AggregateDelegate sub = Sub;
sub(2, 1); // 调用委托对象相减

C# 中的委托的使用需要先定义一个委托类型,这个委托类型不能泛化,即委托类型一旦声明之后就不能再用来接受其它类型的函数了,比如这样用:

1
2
3
4
cpp复制代码int Fun(int x, int y, int z) { return x + y + z; }
int Fun1(string s, string r) { return s.Length + r.Length; }
AggregateDelegate fun = Fun; //编译报错,只能赋值相同类型的函数
AggregateDelegate fun1 = Fun1; //编译报错,参数类型不匹配

这里不能泛化的原因是声明委托类型的时候就限定了参数类型和个数,在 C++11 里不存在这个问题了,因为有了可变模版参数,它就代表了任意类型和个数的参数了,下面让我们来看一下如何实现一个功能更加泛化的 C++ 版本的委托(这里为了简单起见只处理成员函数的情况,并且忽略 const、volatile 和 const volatile 成员函数的处理)。

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
cpp复制代码template <class T, class R, typename... Args>
class MyDelegate
{
public:
MyDelegate(T *t, R (T::*f)(Args...)) : m_t(t), m_f(f) {}

R operator()(Args &&...args)
{
return (m_t->*m_f)(std::forward<Args>(args)...);
}

private:
T *m_t;
R (T::*m_f)
(Args...);
};

template <class T, class R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T *t, R (T::*f)(Args...))
{
return MyDelegate<T, R, Args...>(t, f);
}

struct A
{
void Fun(int i) { cout << i << endl; }
void Fun1(int i, double j) { cout << i + j << endl; }
};

int main()
{
A a;
auto d = CreateDelegate(&a, &A::Fun); //创建委托
d(1); //调用委托,将输出1
auto d1 = CreateDelegate(&a, &A::Fun1); //创建委托
d1(1, 2.5); //调用委托,将输出3.5
}

MyDelegate 实现的关键是内部定义了一个能接受任意类型和参数个数的 “万能函数”:R (T::*m_f)(Args...),正是由于可变模版参数的特性,所以我们才能够让这个 m_f 接受任意参数。

总结

使用可变模版参数的这些技巧相信读者看了会有耳目一新之感,使用可变模版参数的关键是如何展开参数包,展开参数包的过程是很精妙的,体现了泛化之美、递归之美,正是因为它具有神奇的“魔力”,所以我们可以更泛化的去处理问题,比如用它来消除重复的模版定义,用它来定义一个能接受任意参数的 “万能函数” 等。其实,可变模版参数的作用远不止文中列举的那些作用,它还可以和其它 C++11 特性结合起来,比如 type_traits、std::tuple 等特性,发挥更加强大的威力,将在后面模板元编程的应用中介绍。

本文转载自: 掘金

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

基于Springboot框架开发实现物流统计分析系统

发表于 2021-11-07

项目编号:BS-XX-019

今天给在家介绍一个我们为客户开发的物流统计分析系统,本系统主要实现针对物流公司车辆运营情况的数据分析、数据管理、以及统计功能:

涉及框架与技术\

echarts、管理台ui:X-admin

SpringBoot、SSM、swagger2、shiro、thymeleaf、poi、pagehelper

数据库:mysql

开发工具:IDEA ECLIPSE

本系统功能完整,运行无误,界面简洁大方,用户体验较好,比较适合做毕业设计使用,是一个难得的优秀项目。

具体功能展示如下:

  1. 登陆页面:

)​

2.后台管理首页:

)​

3,用户管理模块

)​

用户修改

)​

  1. 车辆管理模块:可以进行批量导入 可以下载导入EXCEL模板

)​

车辆添加

)​

车辆修改

)​

  1. 统计分析

车辆运营情况统计:可以在右方切换图形,可以将生成的图表保存到本地

)​

应收账款统计

)​

应付账款统计:

)​

应付账款统计:可以将生成的图表保存到本地

)​

)​

司机产出统计

)​

  1. 系统管理模块:

角色管理

)​

基于Swagger产生的接口文档

)​

系统菜单管理

)​

字体图标维护

)​

以上就是基于Springboot实现的物流统计分析系统的部分功能展示。此系统功能完整,运行流畅,

​

本文转载自: 掘金

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

【Maven专栏系列】重新认识一下Maven这款工具

发表于 2021-11-07

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

前言

在我最开始学习Java时,经常会遇到需要一些第三方Jar包,比如学习JDBC时,需要MySQL的驱动包,还有一些其他的工具包,例如Guava,Apache Common等等,最开始我都是去官网或者网上搜索下载,比如CS某N,还经常因为没有积分下不了,体验特别差,有时候还会因为各种版本问题,遗漏某个Jar,导致代码执行时报错,然后丈二的和尚摸不着脑袋,走了很多的弯路。

直到后来学习了Maven这款构建工具,再也不用到处下载了,需要的Jar包在pom.xml文件中添加依赖,然后mvn install一下就好了,是真香呀,后悔没有早点遇见,如果刚开始有人能告诉我有Maven这款工具,那我肯定非常感谢他/她/它。当然Maven不仅仅可以管理Jar包依赖,还能做很多项目构建的事情。

所以这就是我写这个专栏的原因。我希望能够让看到这篇专栏的人能少走一点我走过的弯路,学习路上更容易一点点。

扯了这么多,回归今天的正题。

Maven是一款Java软件项目的强大构建工具。本期内容的目的是让你了解Maven是如何工作的。因此,会重点介绍Maven的核心概念。一旦理解了核心概念,在使用时遇到问题再去网上查找文档就容易得多了。一旦理解Maven并开始使用它,你就会发现它到底是什么。

什么是构建工具?

首先说一下什么是构建工具,构建工具是一种自动化构建软件项目的工具。构建一个软件项目通常包括以下一个或多个步骤:

  • 生成源代码(如在项目中使用自动生成的代码);
  • 将源代码生成文档;
  • 编译源代码;
  • 将编译后的代码打包成JAR文件或ZIP文件;
  • 将打包的代码安装到服务器、存储库或其他地方;

自动化构建过程的好处是,可以在手工构建软件时将人为出错的风险降到最低。此外,自动化构建工具通常比人工手动执行相同步骤要快。

Maven安装

要在自己的系统(计算机)上安装Maven,首先需要去官网下载安装包然后进行安装,安装步骤如下:

Maven官网下载地址

  1. 设置好JAVA_HOME环境变量;
  2. 下载并解压Maven;
  3. 设置MAVEN_HOME环境变量为解压Maven的目录;
  4. 将MAVEN_HOME添加到PATH环境变量(Windows上为%MAVEN_HOME%\bin,Linux上为$MAVEN_HOME/bin);
  5. 打开命令提示符并输入mvn -version验证是否安装成功。

如果命令行显示结果如下则表示安装成功。

image-20211104233852537

注意:Maven在执行时需要使用Java,因此在安装Maven之前需要安装Java环境(以及如上所述设置JAVA_HOME环境变量)。Maven 3.0.5需要Java版本1.5或更高版本。

Maven核心概念

在Maven中以POM文件(项目对象模型)为中心。POM文件是管理项目资源的XML,如源代码、测试代码、依赖项(外部jar)等。

POM包含对所有这些资源的引用。POM文件应该位于它所属项目的根目录中。

下面的图表说明了Maven如何使用POM文件,以及POM文件主要包含什么内容:

image-20211104235128310

pom文件

当执行Maven命令时,需要向Maven提供一个POM文件,以便在其上执行命令。然后,Maven将按照对POM中配置的资源执行命令。

Build Life Cycles, Phases和Goals

Maven的构建过程被划分为Build Life Cycles, Phases和Goals。Build Life Cycles由一系列Phases组成,每个Phases由一系列Goals组成。

当运行Maven时,将一个命令传递给Maven。这个命令是Build Life Cycles, Phases或Goals的名称。

如果请求执行一个Life Cycles,则执行该Life Cycles中的所有Phases。

如果一个Phases被请求执行,那么在它之前的预定义Phases中的所有Phases也将被执行。

依赖和仓库

Maven执行的第一个目标是检查项目所需的依赖项。

依赖项是项目使用的外部JAR包。如果在本地Maven仓库中没有找到依赖项,Maven将从中央仓库下载,并将它们放到本地存储库中。

本地仓库只是计算机硬盘上的一个目录,可以指定本地仓库位置。还可以指定使用哪个远程仓库来下载依赖项。

构建插件Plugins

构建插件用于在构建Phases时插入额外的Goals。如果需要为项目执行一组标准Maven构建Phases和Goals没有涵盖的操作,可以向POM文件添加一个插件。Maven有一些已有的标准插件,如果需要,还可以用Java实现自定义插件。

Profiles文件

如果需要以不同的方式构建项目,则需要使用Profiles文件。例如,可能需要在本地构建项目进行开发和测试。还需要用不同的方式构建项目用于生产环境部署。这两个版本可能是有差异的,可以向POM文件添加不同的Profiles文件,在执行Maven时,选择使用哪一个。

Maven的POM文件

Maven的POM文件(项目对象模型)是配置项目资源的XML文件,包括源代码、测试源代码等所在的目录,项目的外部依赖等等。

POM文件配置要构建什么,但不需要配置如何构建。如何构建它取决于Maven构建Phases和Goals。

但是,如果需要,也可以将自定义操作(Goals)插入到Maven构建Phases。

每个项目都有一个POM文件。POM文件名为pom.xml,应该位于项目的根目录中。

一个被划分为子项目的项目通常有一个归属于父项目的POM文件,这种结构既允许在一个步骤中构建整个项目,也允许单独构建任何子项目。

下面是一个最简单的pom.xml示例。

1
2
3
4
5
6
7
8
9
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.heiz</groupId>
<artifactId>java-web-demo</artifactId>
<version>1.0.0</version>
</project>

modelVersion元素设置正在使用的POM模型的版本。要使用与正在使用的Maven版本匹配的版本。版本4.0.0匹配Maven版本2和3。

groupId元素是一个组织或项目(例如,一个开源项目)的唯一ID。最常见的情况是,将使用与项目的根Java包名称类似的groupId。例如,对于我的java-web-demo项目,我可以选择组ID com.heiz。如果这个项目是一个有许多独立贡献者的开源项目,那么使用与这个项目相关的groupId可能比使用与我的公司相关的groupId更有意义。

groupId不一定要是Java包名。符号(点符号)用于分隔ID中的单词。groupId中的每一个.会在项目构建后替换为存放在本地仓库的目录分割符,每个单词对应一个文件夹,所以groupId com.heiz将位于名为MAVEN_REPO/com/heiz的目录中。目录的MAVEN_REPO部分将被Maven存储库的目录路径替换。

artifactId元素代表要构建的项目的名称。在我的java-web-demo项目中,artifactId将是java-web-demo。artifactId用作Maven存储库中groupId目录下的子目录的名称。artifactId也用作在构建项目时生成的JAR文件的名称的一部分,当然结果也可以是WAR文件或者其他格式。

version元素表示项目的版本号。如果项目已经以不同的版本发布,例如一个开源API,对构建版本进行版本控制是很常见的。这样项目的用户就可以引用项目的指定版本。版本号用作artifactId目录下的子目录的名称。版本号也用作所构建JAR的名称的一部分。

上面的groupId、artifactId和version将生成一个JAR文件,并将其放入本地Maven存储库中,路径如下(目录和文件名):

1
shell复制代码MAVEN_REPO/com/heiz/java-web-demo/1.0.0/java-web-deom-1.0.0.jar

父级POM

所有的Maven POM文件都继承自一个父级POM。如果没有指定父级POM,则POM文件继承自基本POM。如下图所示:

可以在POM文件显式地继承另一个POM文件。通过这种方式,可以通过所有继承POM的通用POM来更改设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.heiz.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<relativePath>../my-parent</relativePath>
</parent>

<artifactId>my-project</artifactId>
...
</project>

继承的POM文件在自己的文件中也可以用新的配置覆盖父级POM的配置。

有效的POM

如果需要确认POM文件是否在一个项目中是有效的POM,可以使用下面的命令进行验证:

1
shell复制代码mvn help:effective-pom

会在命令行中将有效的POM打印出来。

运行Maven

当安装了Maven并创建了一个Maven项目,增加了POM文件配置后,可以在项目上运行Maven。

运行Maven可以通过在命令行执行mvn命令来完成的。在执行mvn命令时,将Build Life Cycles, Phases和Goals的名称作为参数。

如:

1
shell复制代码mvn install

该命令执行称为install的构建阶段,该阶段构建项目并将打包的JAR文件复制到本地Maven存储库中。

也可以指定多个参数,比如:

1
shell复制代码 mvn clean install

该命令首先执行clean构建生命周期,从Maven输出目录中删除已编译的类,然后执行install构建阶段。

还可以通过将构建阶段和目标作为参数来执行Maven目标。下面是一个例子:

1
shell复制代码mvn dependency:copy-dependencies

此命令执行dependency构建阶段的copy-dependencies目标。

Maven目录结构

Maven有一个标准的目录结构。如果项目遵循这个目录结构,那么就不需要在POM文件中指定源代码、测试代码的目录。

以下是最重要的目录:

1
2
3
4
5
6
7
8
9
10
markdown复制代码- src
- main
- java
- resources
- webapp
- test
- java
- resources

- target

src目录是源代码和测试代码的根目录。

main目录是与应用程序本身相关的源代码的根目录(不是测试代码)。

test目录包含测试源代码。

main和test下的java目录包含应用程序本身的java代码(在main下)和测试的java代码(在test下)。

resources目录包含项目所需的其他资源,比如一些配置文件。

如果是一个web应用程序,webapp目录包含Java web应用程序。webapp目录将成为web应用程序的根目录。因此,webapp目录包含WEB-INF目录等。

target目录是由Maven创建的。它包含Maven生成的所有已编译类、JAR文件等。在执行清理构建阶段时,要清理的是target目录。

项目依赖

除非你的项目很小,否则可能会需要引入外部框架,这些框架打包在它们自己的JAR文件中。在编译项目代码时,classpath上需要这些JAR文件。

引入外部JAR文件的正确版本是一项重要的任务。每个外部JAR可能还需要其他外部JAR文件。尤其是当项目越来越大的时候确保下载正确版本的JAR是很麻烦的。

而Maven内置的依赖项管理就是为了解决这个事情。可以在POM文件中指定项目依赖的外部依赖以及版本,然后Maven会下载它们并将它们放入本地Maven存储库中。如果这些外部依赖中的任何一个需要其他依赖,那么这些其他依赖也会下载到本地Maven存储库中。

以在POM文件中的dependencies元素中指定项目依赖项。下面是一个例子:

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
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.heiz.demo</groupId>
<artifactId>java-web-demo</artifactId>
<version>1.0.0</version>

<dependencies>

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.7.1</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
</build>

</project>

请注意dependencies元素,它里面有两个dependency元素。每个dependency项描述一个外部依赖项。

每个依赖项由其groupId、artifactId和version描述。

当这个POM文件被Maven执行时,这两个依赖项将从中央仓库下载并放到本地仓库中。如果已经在本地仓库中找到了依赖项,将不会下载。

有时候中央仓库中的依赖不可用,你可以从官网或其他地方下载需要的Jar放入到本地Maven仓库中使用。

外部依赖

Maven中的外部依赖项是不在Maven仓库(既不是本地、中央或远程仓库)中的依赖项(JAR文件)。它可能位于本地的某个地方,例如在webapp的lib目录中,或者其他地方。因此,“外部”一词意味着Maven存储库系统的外部——而不仅仅是项目的外部。大多数依赖项都在项目的外部,但很不在仓库中。如果有这种情况,你可以按下面的方式配置外部依赖:

1
2
3
4
5
6
7
xml复制代码<dependency>
<groupId>mydependency</groupId>
<artifactId>mydependency</artifactId>
<scope>system</scope>
<version>1.0</version>
<systemPath>${basedir}\war\WEB-INF\lib\mydependency.jar</systemPath>
</dependency>

scope元素值被设置为system。systemePath元素被设置为指向包含依赖项的JAR文件的位置。${basedir}指向POM所在的目录。

快照依赖(Snapshot)

快照依赖关系是正在开发的依赖关系(JAR文件)。您可以依赖项目的快照版本,而不是不断地更新版本号以获得最新版本。

每次构建,即使匹配的快照版本已经位于本地存储库中,也会重新下载到本地存储库中。这可以确保本地存储库中始终拥有针对每个构建的最新版本。

只需在POM开头的版本号后面加上-SNAPSHOT即可表示你的项目是快照版本。

1
xml复制代码<version>1.0-SNAPSHOT</version>

在配置依赖项时,指定版本号为快照版本,便可以实现对快照版本的依赖。

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.jenkov</groupId>
<artifactId>java-web-crawler</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

可以在Maven的settings.xml文件中配置下载快照依赖项的频率。

传递依赖

如果你的项目依赖于一个依赖项,比如依赖项ABC,而依赖项ABC本身又依赖于另一个依赖项,比如依赖项XYZ,那么你的项目就依赖于XYZ。

排除依赖

有时候,项目的直接依赖可能会与直接依赖的传递依赖发生冲突。

例如,你可能正在使用JAX-RS实现,该实现在内部使用较旧版本的Jackson包。但是,你的应用程序可能正在使用更新版本的Jackson包。您如何知道将使用这两个版本中的哪一个?

一个解决方案是为JAX-RS依赖项指定应该排除它对Jackson包的旧版本的依赖项。这也被称为依赖排除。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>example.jaxrs</groupId>
<artifactId>JAX-RS-TOOLKIT</artifactId>
<version>1.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
</exclusions>
</dependency>

有了这个依赖项排除声明,在Maven编译项目时,被排除依赖项将被忽略。

Maven仓库

Maven仓库是一个包含特殊元数据的打包JAR文件的目录。

元数据是指每个打包JAR文件所属的项目的POM文件,包括每个JAR具有的外部依赖项。正是这个元数据使Maven能够递归地下载依赖项的依赖项,直到整个依赖项树被下载并放到本地仓库中。

Maven有三种类型的仓库:

  • 本地仓库
  • 中央仓库
  • 远程仓库

Maven会按照上述顺序在这些仓库中搜索依赖项。首先在本地仓库中,然后在中央仓库中,如果在POM中指定,则在远程仓库中搜索。

本地仓库

默认情况下,Maven将本地仓库放在本地计算机上的用户主目录中。但是,可以通过在Maven的settings.xml文件中来更改本地仓库的位置。

Maven的settings.xml文件位于Maven安装目录的conf/目录下。以下是为本地仓库指定位置的方法:

1
2
3
4
5
xml复制代码<settings>
<localRepository>
d:\data\java\products\maven\repository
</localRepository>
</settings>

中央仓库

中央仓库库是Maven社区提供的。默认情况下,Maven会在这个中央仓库库中查找任何需要但在本地仓库库中没有找到的依赖项。然后将这些依赖项下载到本地仓库库中。

远程仓库

远程仓库通常用于承载组织内部的项目,这些项目由多个项目共享。例如,一个公共安全项目可以跨多个内部项目使用。外部世界不应访问此安全项目,因此中央Maven仓库不应暴露在公网中。可以在POM文件中配置远程存储库:

1
2
3
4
5
6
xml复制代码<repositories>
<repository>
<id>heiz.code</id>
<url>http://maven.heiz.com/maven2/lib</url>
</repository>
</repositories>

Build Life Cycles、Phases和goals

Build Life Cycles

Maven有3个内置的构建生命周期:

  1. default
  2. clean
  3. site

这些构建生命周期中的每一个都负责构建软件项目的不同情况。因此,每一个构建生命周期都是独立地执行的。可以让Maven执行多个构建生命周期,但它们将依次执行,彼此分开,就像执行了两个独立的Maven命令一样。

default处理编译和打包项目相关的所有事情。

clean处理输出目录删除临时文件相关的所有事情,包括生成的源文件、编译的类、以前的JAR文件等。

site处理与为项目生成文档相关的所有事情。事实上,site可以为项目生成一个包含文档的完整网站。

Build Phases

你可以执行像clean或site这样的整个构建生命周期,像install这样的构建阶段(这是default构建生命周期的一部分),或者像dependency:copy-dependencies这样的构建目标。

注意:不能直接执行default的生命周期。您必须在default的生命周期内指定构建阶段或目标。

当执行一个构建阶段时,将执行该标准阶段序列中该构建阶段之前的所有构建阶段。因此,执行install构建阶段实际上意味着在install阶段之前执行所有构建阶段,然后执行install阶段。

default的生命周期是最重要的,因为它是构建代码的部分。因为不能直接执行default的生命周期,所以需要从default的生命周期执行构建阶段或目标。默认的生命周期有一个构建阶段和目标的顺序,最常用的构建阶段是:

Build Phase 描述
validate 校验项目是正确的,所有必要的信息都是可用的,确保依赖项成功下载。
compile 编译项目的源代码。
test 使用合适的单元测试框架对编译后的源代码运行测试。这些测试不应该被package或deploy。
package 将编译后的代码打包成可发布的格式,例如JAR。
install 将包安装到本地仓库中,作为本地其他项目的依赖项使用。
deploy 将最终包复制到远程存储库,以便与其他开发人员和项目共享。

可以通过mvn命令来执行其中一个构建阶段。下面是一个例子:

1
shell复制代码mvn package

此示例执行package构建阶段,也会执行Maven预定义的构建阶段序列中package之前的所有构建阶段。

Build Goals

构建目标是Maven构建过程中最好的步骤。一个目标可以绑定到一个或多个构建阶段,也可以不绑定。如果目标没有绑定到任何构建阶段,则只能通过将目标名称传递给mvn命令来执行它。如果一个目标被绑定到多个构建阶段,那么该目标将在它所绑定的每个构建阶段中执行。

Maven构建Profiles

profiles使能够使用不同的配置来构建项目。不需要创建两个单独的POM文件,只需使用不同的构建配置指定一个profiles,然后在需要时使用这个profiles构建项目。

profiles在POM文件的profiles元素中指定。每个profile都嵌套在profiles元素中。下面是一个例子:

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
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.jenkov.crawler</groupId>
<artifactId>java-web-crawler</artifactId>
<version>1.0.0</version>

<profiles>
<profile>
<id>test</id>
<activation>...</activation>
<build>...</build>
<modules>...</modules>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<dependencies>...</dependencies>
<reporting>...</reporting>
<dependencyManagement>...</dependencyManagement>
<distributionManagement>...</distributionManagement>
</profile>
</profiles>

</project>

profile中配置在构建时应该对POM文件进行哪些更改。profile元素中的元素将覆盖POM中同名元素的值。

在profile元素中,可以看到一个activation元素。此元素描述触发要使用的构建概要文件的条件。选择profiles的一种方法是在settings.xml文件中设置使用的配置文件。另一种方法是在Maven命令行中添加-P profile-name。

插件Plugins

Maven插件允许将自己的操作添加到构建过程中。可以创建一个简单的Java类,它扩展了一个特殊的Maven类,然后为项目创建一个POM。关于插件的开发将会在本专栏后面几期的内容中涉及。

小结

今天的内容主要和大家重点介绍Maven的核心概念,有POM文件,构建过程、阶段和目标,依赖,仓库、Profiles和插件等。

希望通过本期内容你能够对Maven有一个更清晰的认识,下期内容将会用一个项目来对今天的内容进行展开阐述。

推荐与下面的文章连续阅读,效果更佳。

【Maven专栏系列】Maven项目从0到1

点个关注可以及时收到后续文章的更新,如果觉得对你有所帮助,点个赞是对我最大的鼓励!

本文转载自: 掘金

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

这一篇 K8S(Kubernetes)集群部署 我觉得还可以

发表于 2021-11-07

点赞再看,养成习惯,微信搜索【牧小农】关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友。

国内安装K8S的四种途径

Kubernetes 的安装其实并不复杂,因为Kubernetes 属于Google 的产品,都是从Google的官方上进行下载,但是因为网络问题,在国内是没办法连接它的中央仓库进行下载安装包的,只能通过其他的途径进行安装,在国内有四种安装方式

  1. 使用 Kubeadmin 通过离线镜像安装: Kubeadmin 是K8S提供的管理控制台,通过这里的命令可以非常方便的对我们集群进行快速发布和部署
  2. 使用阿里云公有云平台安装K8S: 这是也是非常好用的,不用做任何设置,拿来就用,但是有一个缺点——要钱
  3. 通过yum官方仓库安装: 这个是最简单的,但是这个安装K8S的包是非常古老的版本,听说是和谷歌最新的差了十个版本
  4. 通过二进制包的形式进行安装: 采用第三方的提供的二进制包的形式来安装K8S,比如 Kubeasz,它是github的一个开源项目,因为它是由三方来提供的,如果在没有仔细验证的情况下,是非常容易出错的,后台存在什么样的缺陷,你是不知道的。

今天我们这里采用的是第一种 使用 Kubeadmin 通过离线镜像安装K8S,本文也会使用 Kubeadmin 来为大家展示K8S的集群部署和安装,环境和安装包我都为大家准备好了,大家感兴趣的可以进行下载安装。

关注公众号:牧小农,回复 k8s ,就可以获取下载地址了

环境准备

结构图:

在这里插入图片描述

1.1 物理机系统

在这里需要说明一下,小农是本地虚拟机安装测试的,如果不知道怎么安装,看下面的我安装虚拟机的教程:
虚拟机安装教程:安装linux虚拟机(CentOS) 详细教程

安装K9S需要处理器的数量为2,否则后面初始化的时候会失败

在这里插入图片描述

1,1 物理机操作系统采用Centos7.8 64位

1
2
3
4
java复制代码[root@localhost ~]# uname -a
Linux localhost.localdomain 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
[root@localhost ~]# cat /etc/redhat-release
CentOS Linux release 7.8.2003 (Core)

1.2 集群信息

节点及功能 主机名 IP
Master、etcd、registry Master 192.168.137.129
Node1 Node1 192.168.137.130
Node2 Node2 192.168.137.131

环境准备命令

2.1 设置时区

三台机器都需要执行:timedatectl set-timezone Asia/Shanghai

2.2 设置主机名

129执行: hostnamectl set-hostname master

130执行: hostnamectl set-hostname node1

131执行: hostnamectl set-hostname node2

2.3 添加hosts网络主机配置

三台主机都需要添加这个配置

1
2
3
4
java复制代码vi /etc/hosts
192.168.137.129 master
192.168.137.130 node1
192.168.137.131 node2

添加完成之后我们在master上验证一下 ping node1

1
2
3
4
5
java复制代码[root@localhost ~]# ping node1
PING node1 (192.168.137.130) 56(84) bytes of data.
64 bytes from node1 (192.168.137.130): icmp_seq=1 ttl=64 time=0.605 ms
64 bytes from node1 (192.168.137.130): icmp_seq=2 ttl=64 time=0.382 ms
64 bytes from node1 (192.168.137.130): icmp_seq=3 ttl=64 time=0.321 ms

2.4 关闭防火墙

生产环境可以跳过这一步,在生产环境不要执行这个,这里只是为了方便我们学习的时候使用

SELINUX是安全增强型的LINUX,是LINUX内置安全增强模块,可以增强LINUX的安全性,但是这个设置起来太麻烦了,所以我们在学习过程中一般都会关闭掉这个

关闭命令:sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

设置为临时生效:setenforce 0

禁用防火墙:systemctl disable firewalld

停止防火墙:systemctl stop firewalld

安装Kubeadm部署工具

首先我们要弄清楚一个概念,Kubeadm 并不是 K8S 本身,Kubeadm只是一个快速部署工具,通过安装这个工具可以帮助我们简化K8S部署的过程。

创建文件目录: mkdir /usr/local/k8s

切换目录地址: cd /usr/local/k8s

然后我们将安装包(kubernetes-1.14 安装包在开头有下载连接)放到 k8s 这个目录下

1
2
java复制代码[root@master k8s]# ll
drwxr-xr-x 2 root root 335 Nov 6 11:17 kubernetes-1.14

切换到kubernetes的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码[root@master k8s]# cd kubernetes-1.14/
[root@master kubernetes-1.14]# ll
total 986908
-rw-r--r-- 1 root root 357 Jul 3 14:15 admin-role.yaml
-rw-r--r-- 1 root root 67 Jul 3 14:15 daemon.json
-rw-r--r-- 1 root root 67850818 Jul 3 14:15 docker-ce-18.09.tar.gz
-rw-r--r-- 1 root root 177698304 Jul 3 14:15 flannel-dashboard.tar.gz
-rw-r--r-- 1 root root 927 Jul 3 14:15 init.sh
-rw-r--r-- 1 root root 706070528 Jul 3 14:15 k8s-114-images.tar.gz
-rw-r--r-- 1 root root 79 Jul 3 14:15 k8s.conf
-rw-r--r-- 1 root root 58913350 Jul 3 14:15 kube114-rpm.tar.gz
-rw-r--r-- 1 root root 12306 Jul 3 14:15 kube-flannel.yml
-rw-r--r-- 1 root root 281 Jul 3 14:15 kubernetes-dashboard-admin.rbac.yaml
-rw-r--r-- 1 root root 4809 Jul 3 14:15 kubernetes-dashboard.yaml
-rw-r--r-- 1 root root 953 Jul 3 14:15 worker-node.sh
[root@master kubernetes-1.14]#

这里面就包含了我们安装K8S的所有内容,其中

kube114-rpm.tar.gz: 是我们Kubeadm 集群管理工具的安装压缩包

docker-ce-18.09.tar.gz: 是我们docker的安装压缩包,可以进行本地化安装

k8s-114-images.tar.gz: 是K8S镜像本身,我们K8S的安装是 通过 Kubeadm 集群管理工具对K8S的镜像进行自动化部署

flannel-dashboard.tar.gz: 用来监控集群状态

安装docker

我们需要在三台机器上都安装docker,文中用 master 节点来做演示

首先对 docker-ce-18.09.tar.gz进行解压缩

1
2
java复制代码[root@master kubernetes-1.14]# tar -zxvf docker-ce-18.09.tar.gz
[root@master kubernetes-1.14]# cd docker

在安装之前我们首先需要保证本来的yum源和docker依赖是最新的,所以我们需要先执行以下命令,这一步命令执行都是在 docker这个目录下执行的

1、安装GCC

1
2
java复制代码yum -y install gcc
yum -y install gcc-c++

2、卸载老版本的docker和依赖

1
java复制代码yum  remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine

3、安装 yum-utils组件:yum install -y yum-utils device-mapper-persistent-data lvm2

4、添加yum 源 PS

docker官网地址: sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
阿里云地址: sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

5、更新yum索引:sudo yum makecache fast

6、安装docker-ce:sudo yum install docker-ce

7、卸载老版本的docker和依赖
这一步不是重复,是为了卸载上一步的 docker-ce来安装我们k8s包里面的 docker-ce

1
java复制代码yum  remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine

8、执行我们安装包里面的文件:yum localinstall -y *.rpm

在这里插入图片描述
到这里就代表我们的docker安装完成了

启动docker: systemctl start docker

设置docker为自动启动: systemctl enable docker

保证 cgroups 在同一个 groupfs

执行命令:

1
2
java复制代码[root@master docker]# docker info | grep cgroup 
Cgroup Driver: cgroupfs
  • cgroups是 control groups 的简称,它为Linux内核提供了一种任务聚集和划分的机制,通过一组参数集合将一些任务组织成一个或多个子系统。
  • cgroups是实现IaaS虚拟化(kvm、lxc等),PaaS容器沙箱(Docker等)的资源管理控制部分的底层基础。
  • 子系统是根据cgroup对任务的划分功能将任务按照一种指定的属性划分成的一个组,主要用来实现资源的控制。
  • 在cgroup中,划分成的任务组以层次结构的形式组织,多个子系统形成一个数据结构中类似多根树的结构。cgroup包含了多个孤立的子系统,每一个子系统代表单一的资源

我们只需要确认输入上面的命令后出现的是: Cgroup Driver: cgroupfs就可以了

当我们输入命令发现不是 Cgroup Driver: cgroupfs这个结果,那我们需要执行下面的命令:

对 daemon.json 进行修改:

1
2
3
4
5
6
java复制代码cat << EOF > /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=cgroupfs"]
}
EOF
systemctl daemon-reload && systemctl restart docker

安装kubeadm

kubeadm是K8S官方提供的集群部署工具,通过这个工具可以快速帮助我们简化的完成K8S的管理,以及各集群节点下的容器创建

切换目录: cd /usr/local/k8s/kubernetes-1.14

解压 kube114安装包: tar -zxvf kube114-rpm.tar.gz

切换目录: cd kube114-rpm

在这里插入图片描述

安装: yum localinstall -y *.rpm

在这里插入图片描述

关闭交换区

  • 在linux系统中,交换区类似于我们的 windows的虚拟内存,作为windows的虚拟内存,其实就是用物理磁盘模拟内存.
  • 比如说我们系统内存比较小,那么在进行数据处理的时候,内存不够了,我们就会先把这些数据寄存在硬盘上,用硬盘空间模拟内存来使用,虽然硬盘提取数据的速度比较慢,但是总是比内存不够要好,在linux系统中,交换区就是我们所说的虚拟内存了.
  • 在这里,虚拟内存可能会对系统部署产生不必要的影响,在K8S的环境下,使用的服务器一般都是内存比较充足的,所以我们一般不推荐使用系统交换区,这样会让我们系统的性能降低,在这里选择关闭交换区

关闭交换区:swapoff -a

修改配置文件,永久关闭交换区:vi /etc/fstab
swap一行注释

在这里插入图片描述

配置网桥

所谓配置网桥其实就是我们对k8s.conf这个文件进行修改
iptables 是在linux中的一个网络工具,用来对我们包按照规则进行过滤,在k8s.conf中增加下面两行配置当在K8S容器间进行网络通信的时候,当网桥进行数据传输的时候,也要遵循iptables的规则,进行相应的处理,这样可以提高我们系统在网络传输间的安全性

开启方式:

1
2
3
4
5
java复制代码cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system

在这里插入图片描述

当我们执行完成以后,要确保红框中的值为1

通过镜像安装K8S

切换目录: cd /usr/local/k8s/kubernetes-1.14

加载本地镜像—k8s: docker load -i k8s-114-images.tar.gz

加载完成后查看:docker images

1
2
3
4
5
6
7
8
9
java复制代码[root@master kubernetes-1.14]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
k8s.gcr.io/kube-proxy v1.14.1 20a2d7035165 2 years ago 82.1MB
k8s.gcr.io/kube-apiserver v1.14.1 cfaa4ad74c37 2 years ago 210MB
k8s.gcr.io/kube-controller-manager v1.14.1 efb3887b411d 2 years ago 158MB
k8s.gcr.io/kube-scheduler v1.14.1 8931473d5bdb 2 years ago 81.6MB
k8s.gcr.io/coredns 1.3.1 eb516548c180 2 years ago 40.3MB
k8s.gcr.io/etcd 3.3.10 2c4adeb21b4f 2 years ago 258MB
k8s.gcr.io/pause 3.1 da86e6ba6ca1 3 years ago 742kB

如果看过我上一篇K8S入门的同学,应该很熟悉这些东西,这里就不做详细介绍了,感兴趣的可以去看一看
这一篇 K8S(Kubernetes)我觉得可以了解一下!!!

加载本地镜像—对集群可视化: docker load -i flannel-dashboard.tar.gz

同样我们也可以使用docker images进行查看

在这里插入图片描述

到这里我们K8S的前置工作已经完成了,但是我们今天安装的集群环境,所以上面的步骤,在其他两台机器中也要进行安装,当我们安装好了之后,其他的两台机器使用docker images,也能 出现上面的信息表示我们安装完成,这里我们就不做重复的工作了,大家可以自行安装好

使用Kubeadm部署K8S集群

master 主服务配置

下面的步骤我们都是在 129(master) 这台服务器上执行的,请大家注意!!!

  1. master主服务器配置:kubeadm init --kubernetes-version=v1.14.1 --pod-network-cidr=10.244.0.0/16

versioin:版本

cidr:Ip范围必须在10.244之内

安装成功之后我们可以看到下面的信息:

在这里插入图片描述

第一步:

这三行命令需要我们复制以后手动去运行

1
2
3
4
java复制代码
mkdir -p $HOME/.kube ## 表示需要我们创建一个.kube目录
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config ##将admin.conf 复制到config中
sudo chown $(id -u):$(id -g) $HOME/.kube/config ## 进行授权操作

admin.conf 是kubeadm有关于当前集群的核心配置文件,包含了kubeadm集群信息,也包含节点的信息,大家可以看一下
在这里插入图片描述
第二步:
这条命令需要我们在节点中(130、131)去运行,让我们的节点信息加入到master(129)中,我们可以先复制保存一下

1
2
java复制代码kubeadm join 192.168.137.129:6443 --token lg870y.lxik26ib84938ton \
--discovery-token-ca-cert-hash sha256:6d8331fe88ae99e89608d6dc414a9fe0a378b84ffa4044d7cacfdbbf5de41871

通过kubectl获取节点信息(包含master):kubectl get nodes
在这里插入图片描述

name:主机名
STATUS:状态
ROLES:角色
AGE:创建时间26分钟
VERSION:版本

从上图中我们可以看到,节点只有master,没有Node,是因为当前没有执行第二步的操作,所以当前没有节点加入到我们的master中,当时我们看到 status 是 NotReady,是没有准备好,这是为什么呢,在底层肯定是有什么组件没有正常的执行,我们可以通过下面这个命令来查看

查看存在问题的pod:kubectl get pod --all-namespaces
在这里插入图片描述

如果我们在状态(STATUS)中看到 CrashLoopBackOff一般重新执行几次kubectl get pod --all-namespaces命令就会没有,但是如果一直存在,表示我们硬件不够,需要增加CPU和内存资源就可以了。

我们可以前两行的是状态一直是 Pending,其他的都是Running,这是不正常的,那为什么只有前两个是这样的,后面的是好的呢,这个问题是必然会出现的。

我们可以看到 name下 有一个 coredns,它代表的是我们基础中的网络应用,这个基础的网络应用因为缺少一个额外的组件,所以无法进行安装,在这里是因为缺少 flannel网络组件,它是pod的网络组件,我们只需要通过 kubectl 进行安装就可以了

安装flannel网络组件:kubectl create -f kube-flannel.yml
在这里插入图片描述
安装成功以后我们再使用命令:kubectl get pod --all-namespaces,就不会出现Pending的状态了
在这里插入图片描述
并且master也是准备就绪了
在这里插入图片描述

node 从服务配置

还记得我们在主服务(master)初始化的时候,复制下来的第二步的命令吗,我们在node(130、131)节点中只需要执行那条命令就可以了,

PS:这条命令是由我自己的master生成的,大家需要替换成你们自己的那条命令才可以。

如果我们忘记了这条命令,我们可以通过 kubeadm token list命令去查看,然后将下方的IP地址和token替换一下就可以了,其他不用替换
在这里插入图片描述

1
2
java复制代码kubeadm join 192.168.137.129:6443 --token lg870y.lxik26ib84938ton \
--discovery-token-ca-cert-hash sha256:6d8331fe88ae99e89608d6dc414a9fe0a378b84ffa4044d7cacfdbbf5de41871

通过kubectl获取节点信息(包含master):kubectl get nodes

在这里插入图片描述
然后我们就可以看到,两个节点已经加入进来了

重新启动服务

重启docker:systemctl restart docker
重启kubelet:systemctl restart kubelet
设置开机启动:systemctl enablekubelet

kubeadm/kubelet/kubectl区别

  • kubeadm:是kubernetes集群快速构建工具
  • kubelet 运行在所有节点上,负责启动Pod和容器,以系统服务形式出现
  • kubectl:kubectl是kubernetes命令行工具,提供指令

小结

到这里K8S的集群服务就讲完了,其实本身安装K8S并不复杂,但是你架不住里面有许多许多的坑,大家可能看到我安装的比较顺利,但是小农也是躺了很多坑,这篇文章才呈现给大家的,如果觉得文章对你有帮助的,记得点赞关注,你的支持我创作的最大动力。

如果有疑问或者不懂的地方,欢迎在下面留言,小农看到了,会第一时间回复大家。

怕什么真理无穷,进一步 有进一步的欢喜,大家加油~

本文转载自: 掘金

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

Redis持久化之RDB

发表于 2021-11-07

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

RDB

image.png

  • 上文我们提到订阅者与发布者,如果机器宕机挂掉是无法恢复数据的
    • 换个角度去想,如果Redis突然宕机呢们是否有一种或多种机制保证数据的不丢失呢
    • 而此文介绍的RDB就是Redis做数据持久化的一种
    • RDB是作为Redis上某一个时间点的快照,存储的是二进制形式,存储效率高

快照原理

  • 谈到快照我们就要联想到我们操作系统中的写时复制原则(COW)
    • 在我们的内存数据不断变化的同时
    • 父子进程是同时面对内存数据的,所以父进程在对一页数据修改的时候
    • 子进程对应页的数据是没有变化的,而父进程是修改复制出来的那一份数据
    • 所以父进程是不断接收客户端的请求,进行数据的修改
    • 而子进程看到数据的那一个时间点数据是不会改变的,所以才叫快照!

image.png

  • 我们也手动执行快照
    • 当我们使用save命令的时候,redis会阻塞一段时间
    • 或者使用非阻塞的bgsave

image.png

RDB缺点

  • 虽然RDB每次保存的是二进制文件
    • 但是其每次保存的也只是一个时间点,如果在10:00子进程进行了快照
    • 但是此刻父进程还在持续不断的修改数据,所以我们在10:00:50这一刻的时候
    • 后面数据依然是丢失的,当数据量很大的时候,数据丢失的量会很大
    • 所以我们继续看下一篇的AOF

本文转载自: 掘金

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

剑指 Offer II 083 没有重复元素集合的全排列|

发表于 2021-11-07

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

非常感谢你阅读本文~
欢迎【👍点赞】【⭐收藏】【📝评论】~
放弃不难,但坚持一定很酷~
希望我们大家都能每天进步一点点~
本文由 二当家的白帽子:https://juejin.cn/user/2771185768884824/posts 博客原创~


剑指 Offer II 083. 没有重复元素集合的全排列|46. 全排列:

给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。

样例 1

1
2
3
4
5
ini复制代码输入:
nums = [1,2,3]

输出:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

样例 2

1
2
3
4
5
lua复制代码输入:
nums = [0,1]

输出:
[[0,1],[1,0]]

样例 3

1
2
3
4
5
lua复制代码输入:
nums = [1]

输出:
[[1]]

提示

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

分析

  • 这道算法题采用递归,回溯法比较简单,谁要是非要用循环非递归,二当家的佩服。
  • 提示中说每个数字各不相同,那我们全排列就可以考虑成数字所在位置或者说是数组的下标的不同排列,因为数字都不同,所以我们就不必关心每个数字是几了。
  • 可以单开辟空间存储中间排列,这样我们需要能判断某个数字是否被选择过,可以用hash表存储当前排列结果,然后去看是否含有当前数字,但是这样似乎比较低效。
  • 每个位置的数字都不一样,所以我们直接存储一下某个位置的数字是否被使用即可。
  • 可以直接使用一个布尔数组存储访问过的位置,但是提示中说数字个数最多6个,那我们最多用6个二进制位就可以表示所有数字的已使用和未使用,一个 int 型变量足以,我们用这个 int 型变量的二进制位变化,去对应数字的已使用和未使用。
  • 也可以直接在原数组用交换的方式模拟排列,每个数字在所有位置上都排一次不就是全排列吗?先轮着放第一个位置,然后轮着放第二个位置,以此类推。

题解

java

不使用交换的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
dfs(nums, new ArrayList<>(nums.length), 0, ans);
return ans;
}

private void dfs(int[] nums, List<Integer> row, int flag, List<List<Integer>> ans) {
if (row.size() == nums.length) {
ans.add(new ArrayList<>(row));
return;
}
for (int i = 0; i < nums.length; ++i) {
if (((flag >> i) & 1) == 0) {
row.add(nums[i]);
dfs(nums, row, flag | (1 << i), ans);
row.remove(row.size() - 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
java复制代码class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
backtrack(nums, 0, ans);
return ans;
}

private void backtrack(int[] nums, int cur, List<List<Integer>> ans) {
if (cur == nums.length) {
ans.add(Arrays.stream(nums).boxed().collect(Collectors.toList()));
return;
}
// 当前位置保持不变,接着排下一个
backtrack(nums, cur + 1, ans);
// 换后面的某一个到当前位置
for (int i = cur + 1; i < nums.length; ++i) {
swap(nums, cur, i);
backtrack(nums, cur + 1, ans);
swap(nums, cur, i);
}
}

private void swap(int[] nums, int a, int b) {
nums[a] ^= nums[b];
nums[b] ^= nums[a];
nums[a] ^= nums[b];
}
}

c

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
c复制代码/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
*returnSize = numsSize;
for (int i = 2; i < numsSize; ++i) {
*returnSize *= i;
}

int **ans = (int **) malloc(sizeof(int *) * (*returnSize));
*returnColumnSizes = (int *) malloc(sizeof(int) * (*returnSize));
for (int i = 0; i < *returnSize; ++i) {
ans[i] = (int *) malloc(sizeof(int) * numsSize);
(*returnColumnSizes)[i] = numsSize;
}

int ansSize = 0;

backtrack(nums, numsSize, 0, ans, &ansSize);

return ans;
}

void backtrack(int* nums, int numsSize, int cur, int **ans, int *ansSize) {
if (cur == numsSize) {
for (int i = 0; i < numsSize; ++i) {
ans[*ansSize][i] = nums[i];
}
*ansSize += 1;
return;
}
// 当前位置保持不变,接着排下一个
backtrack(nums, numsSize, cur + 1, ans, ansSize);
// 换后面的某一个到当前位置
for (int i = cur + 1; i < numsSize; ++i) {
swap(nums, cur, i);
backtrack(nums, numsSize, cur + 1, ans, ansSize);
swap(nums, cur, i);
}
}

void swap(int* nums, int a, int b) {
nums[a] ^= nums[b];
nums[b] ^= nums[a];
nums[a] ^= nums[b];
}

c++

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
cpp复制代码class Solution {
private:
void backtrack(vector<int> &nums, int cur, vector<vector<int>> &ans) {
if (cur == nums.size()) {
ans.push_back(nums);
return;
}
// 当前位置保持不变,接着排下一个
backtrack(nums, cur + 1, ans);
// 换后面的某一个到当前位置
for (int i = cur + 1; i < nums.size(); ++i) {
swap(nums[cur], nums[i]);
backtrack(nums, cur + 1, ans);
swap(nums[cur], nums[i]);
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;

backtrack(nums, 0, ans);

return ans;
}
};

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
ans = []

def backtrack(cur: int) -> None:
if cur == n:
ans.append(nums[:])
return
# 当前位置保持不变,接着排下一个
backtrack(cur + 1)
# 换后面的某一个到当前位置
for i in range(cur + 1, n):
nums[cur], nums[i] = nums[i], nums[cur]
backtrack(cur + 1)
nums[cur], nums[i] = nums[i], nums[cur]

backtrack(0)
return ans

go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码func permute(nums []int) [][]int {
n := len(nums)
var ans [][]int

var backtrack func(cur int)
backtrack = func(cur int) {
if cur == n {
ans = append(ans, append([]int{}, nums...))
return
}
// 当前位置保持不变,接着排下一个
backtrack(cur + 1)
// 换后面的某一个到当前位置
for i := cur + 1; i < n; i++ {
nums[cur], nums[i] = nums[i], nums[cur]
backtrack(cur + 1)
nums[cur], nums[i] = nums[i], nums[cur]
}
}

backtrack(0)

return ans
}

rust

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rust复制代码impl Solution {
pub fn permute(mut nums: Vec<i32>) -> Vec<Vec<i32>> {
let mut ans = Vec::new();

Solution::backtrack(&mut nums, 0, &mut ans);

ans
}

fn backtrack(nums: &mut Vec<i32>, cur: usize, ans: &mut Vec<Vec<i32>>) {
if cur == nums.len() {
ans.push(nums.clone());
return;
}
// 当前位置保持不变,接着排下一个
Solution::backtrack(nums, cur + 1, ans);
// 换后面的某一个到当前位置
(cur + 1..nums.len()).for_each(|i| {
nums.swap(cur, i);
Solution::backtrack(nums, cur + 1, ans);
nums.swap(cur, i);
});
}
}

在这里插入图片描述


原题传送门:https://leetcode-cn.com/problems/VvJkup/

原题传送门:https://leetcode-cn.com/problems/permutations/


本文转载自: 掘金

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

1…405406407…956

开发者博客

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