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

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


  • 首页

  • 归档

  • 搜索

中介者模式

发表于 2021-11-19

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

概述

一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。

如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。

定义:

又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。

结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

案例实现

【例】租房

现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。

类图如下:

代码如下:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
java复制代码//抽象中介者
public abstract class Mediator {
//申明一个联络方法
public abstract void constact(String message,Person person);
}

//抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;

public Person(String name,Mediator mediator){
this.name = name;
this.mediator = mediator;
}
}

//具体同事类 房屋拥有者
public class HouseOwner extends Person {

public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("房主" + name +"获取到的信息:" + message);
}
}

//具体同事类 承租人
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("租房者" + name +"获取到的信息:" + message);
}
}

//中介机构
public class MediatorStructure extends Mediator {
//首先中介结构必须知道所有房主和租房者的信息
private HouseOwner houseOwner;
private Tenant tenant;

public HouseOwner getHouseOwner() {
return houseOwner;
}

public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}

public Tenant getTenant() {
return tenant;
}

public void setTenant(Tenant tenant) {
this.tenant = tenant;
}

public void constact(String message, Person person) {
if (person == houseOwner) { //如果是房主,则租房者获得信息
tenant.getMessage(message);
} else { //反正则是房主获得信息
houseOwner.getMessage(message);
}
}
}

//测试类
public class Client {
public static void main(String[] args) {
//一个房主、一个租房者、一个中介机构
MediatorStructure mediator = new MediatorStructure();

//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("张三", mediator);
Tenant tenant = new Tenant("李四", mediator);

//中介结构要知道房主和租房者
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);

tenant.constact("需要租三室的房子");
houseOwner.constact("我这有三室的房子,你需要租吗?");
}
}

优缺点

1,优点:

  • 松散耦合

中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。

  • 集中控制交互

多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。

  • 一对多关联转变为一对一的关联

没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。

2,缺点:

当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

本文转载自: 掘金

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

【笔记】前缀函数与KMP算法

发表于 2021-11-19

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


本期音乐:打上花火 DAOKO/米津玄师

前缀函数

一个字符串s的border是一个最长的字符串,且既是s的后缀,又是s的真前缀。

给定长为n的字符串s,其前缀函数定义为一个长为n的数组π\piπ。其中π[i]\pi[i]π[i]为s的第i个前缀的border长度。

【例子】字符串“abcabcd”的前缀函数为[0,0,0,1,2,3,0],字符串”aabaaab”的前缀函数为[0,1,0,1,2,2,3].

【练习】写出前缀函数的暴力求法。

复杂度O(n^3)

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
cpp复制代码char save[M];
int prefix_function[M]; //前缀函数,第i个前缀的border长度
int main(void)
{
scanf("%s",save);
for(int i=0; save[i]; i++) //第i个前缀
{
int &j = prefix_function[i];
for(j=i; j>=0; --j) //尝试答案为j
{
//如果pi[i]=j,意味着s[0,j-1]与s[i-j+1,i]相等
int suc = 1;
for(int k=0; k<j; ++k)
{
if(save[k]!=save[i-j+1+k])
{
suc = 0;
break;
}
}
if(suc) break;
}
printf("%d ",j );
}

return 0;
}

【笔记】如果π[i]=j\pi[i]=jπ[i]=j,意味着s[0,j-1]与s[i-j+1,i]相等


高效算法

优化1

结论:π[i]−π[i−1]<=1\pi[i]-\pi[i-1]<=1π[i]−π[i−1]<=1

反证法:如果π[i]=j,π[i+1]=j+2\pi[i]=j,\pi[i+1]=j+2π[i]=j,π[i+1]=j+2:那么s[0,j−1]s[0,j-1]s[0,j−1]与s[i−j+1,i]s[i-j+1,i]s[i−j+1,i]相等,s[0,j+2]s[0,j+2]s[0,j+2]与s[i−j,i+1]s[i-j,i+1]s[i−j,i+1]相等。
此时显然s[0,j+1]s[0,j+1]s[0,j+1]与s[i−j,i]s[i-j,i]s[i−j,i]相等,那么π[i]\pi[i]π[i]应该是j+1而不是j,出现矛盾。
两者差值大于2时同理。所以前缀函数后项减前项一定小于等于1.

由此结论,在求π[i+1]\pi[i+1]π[i+1]时,可以从π[i]+1\pi[i]+1π[i]+1开始向前循环。
【复杂度分析】显然,pi的值最多增加n,也就最多减少n,意味着仅需要n次字符串比较就可以得到所有pi的值,所以此时求前缀函数的复杂度为O(n2)O(n^2)O(n2).

优化2

如果s[i+1]=s[π[i]]s[i+1]=s[\pi[i]]s[i+1]=s[π[i]],显然π[i+1]=π[i]+1\pi[i+1]=\pi[i]+1π[i+1]=π[i]+1。

如果两者不相等,我们还需要尝试更小的字符串,为了加速,希望直接移动到一个长度j<π[i]j<\pi[i]j<π[i],且位置i的前缀性质仍然保持,即s[0,j-1] = s[i-j+1…i]。

【笔记】求第i+1个前缀的border时,总是要从第i个前缀的候选border去转移。侯选border为一个子串,且既是真前缀又是后缀,但不一定最长,仍然满足s[0,j-1] = s[i-j+1…i]。

一直重复这个过程,直到j=0为止,此时如果s[0]=s[i+1],那么pi[i+1]=1,否则为0.

现在只剩下一个问题,如何找到第i个前缀的下一个候选border,即在s[0,j-1]=s[i-j+1]的情况下找到最大的k<j,使得s[0,k-1]=s[i-k+1]。

注意到,s[0,k-1]是s[0,j-1]的真前缀,s[i-k+1]是s[i-j+1]的后缀,也是s[0,j-1]的后缀,所以问题就变成了找到s[0,j-1]的border,即π[j−1]\pi[j-1]π[j−1]。

最终算法

  1. π[0]=0\pi[0]=0π[0]=0,从i=1i=1i=1到 n−1n-1n−1按如下方式计算π[i]\pi[i]π[i]:
  2. 为了计算π[i]\pi[i]π[i],定义变量jjj表示第i-1个前缀的当前最好的候选border的长度。首先j=π[i−1]j=\pi[i-1]j=π[i−1]。
  3. 比较s[j]和s[i]s[j]和s[i]s[j]和s[i],如果两者相等,那么π[i]=j+1\pi[i]=j+1π[i]=j+1,否则j=π[j−1]j=\pi[j-1]j=π[j−1]并重复该过程。
  4. 当j=0j=0j=0时仍失配,令π[i]=0\pi[i]=0π[i]=0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码char save[M];
int prefix_function[M]; //前缀函数,第i个前缀的border长度
int main(void)
{
scanf("%s",save);
printf("0\n");
for(int i=1; save[i]; ++i)
{
int j = prefix_function[i-1];
while(j && save[i]!=save[j]) j=prefix_function[j-1];
if(save[i]==save[j]) ++j;
prefix_function[i] = j;

printf("%d\n",prefix_function[i] );
}

return 0;
}

此算法不需要进行字符串比较,由优化1可知总操作次数O(n),而且是在线算法。

应用

单模式匹配

给定文本串t和模式串s,求s在t中的所有出现位置。

构造一个字符串 s + # + t,对其求前缀函数,会发现在t部分的前缀函数的值如果等于|s|,就表示s在其中出现了一次。

这就是KMP算法,很自然吧。
(我终于脱离了会AC自动机但是不会KMP的状态)

找字符串最小周期

【题解】UVA455 找字符串周期 KMP
【题解】UVA11022 String Factoring 字符串周期,区间DP


本文也发表于我的 csdn 博客中。

本文转载自: 掘金

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

Go 常见错误集锦 字符串底层原理及常见错误

发表于 2021-11-19

大家好,我是Go学堂的渔夫子。

string是Go语言的基础类型,在实际项目中针对字符串的各种操作使用频率也较高。本文就介绍一下在使用string时容易犯的一些错误以及如何避免。

01 字符串的一些基本概念

首先我们看下字符串的基本的数据结构:

1
2
3
4
golang复制代码type stringStruct struct {
str unsafe.Pointer
len int
}

由字符串的数据结构可知,字符串只包含两个成员:

  • stringStruct.str:一个指向底层数据的指针
  • stringStruct.len:字符串的字节长度,非字符个数。

假设,我们定义了一个字符串 “中国”, 如下:

1
golang复制代码a := "中国"

因为Go语言对源代码默认使用utf-8编码方式,utf-8对”中“使用3个字节,对应的编码是(我们这里每个字节编码用10进制表示):228 184 173。同样“国”的utf-8编码是:229 155 189。如下存储示意图:

02 rune是什么

要想理解rune,就会涉及到unicode字符集和字符编码的概念以及二者之间的关系。

unicode字符集是对世界上多种语言字符的通用编码,也叫万国码。在unicode字符集中,每一个字符都有一个对应的编号,我们称这个编号为code point,而Go中的rune类型就代表一个字符的code point。

字符集只是将每个字符给了一个唯一的编码而已。而要想在计算机中进行存储,则必须要通过特定的编码转换成对应的二进制才行。所以就有了像ASCII、UTF-8、UTF-16等这样的编码方式。而在Go中默认是使用UTF-8字符编码进行编码的。所有unicode字符集合和字符编码之间的关系如下图所示:

我们知道,UTF-8字符编码是一种变长字节的编码方式,用1到4个字节对字符进行编码,即最多4个字节,按位表示就是32位。所以,在Go的源码中,我们会看到对rune的定义是int32的别名:

1
2
3
golang复制代码// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

好,有了以上基础知识,我们来看看在使用string过程中有哪些需要注意的地方。

03 strings.TrimRight和strings.TrimSuffix的区别

strings.TrimRight函数

该函数的定义如下:

1
golang复制代码func TrimRight(s, cutset string) string

该函数的功能是:从s字符串的末尾依次查找每一个字符,如果该字符包含在cutset中,则被移除,直到遇到第一个不在cutset中的字符。例如:

1
golang复制代码fmt.Println(strings.TrimRight("123abbc", "bac"))

执行示例代码,会将字符串末尾的abbc都去除掉,打印出”123”。执行逻辑如下:

strings.TrimSuffix函数

该函数是将字符串指定的后缀字符串移除。定义如下:

1
golang复制代码func TrimSuffix(s, suffix string) string

此函数的实现原理是,从字符串s中截取末尾的长度和suffix字符串长度相等的子字符串,然后和suffix字符串进行比较,如果相等,则将s字符串末尾的子字符串移除,如果不等,则返回原来的s字符串,该函数只截取一次。

我们通过如下示例来了解下其执行逻辑:

1
golang复制代码fmt.Println(strings.TrimSuffix("123abab", "ab"))

我们注意到,该字符串末尾有两个ab,但最终只有末尾的一个ab被去除掉,保留”123ab”。执行逻辑如下图所示:

以上的原理同样适用于strings.TrimLeft和strings.Prefix的字符串操作函数。 而strings.Trim函数则同时包含了strings.TrimLeft和strings.TrimRight的功能。

04 字符串拼接性能问题

拼接字符串是在项目中经常使用的一个场景。然而,拼接字符串时的性能问题会常常被忽略。性能问题其本质上就是要注意在拼接字符串时是否会频繁的产生内存分配以及数据拷贝的操作。

我们来看一个性能较低的拼接字符串的例子:

1
2
3
4
5
6
7
golang复制代码func concat(ids []string) string {
s := ""
for _, id := range ids {
s += id
}
return s
}

这段代码执行逻辑上不会有任何问题,但是在进行 s += id进行拼接时,由于字符串是不可变的,所以每次都会分配新的内存空间,并将两个字符串的内容拷贝到新的空间去,然后再让s指向新的空间字符串。由于分配的内存次数多,当然就会对性能造成影响。如下图所示:

那该如何提高拼接的性能呢?可以通过strings.Builder进行改进。strings.Builder本质上是分配了一个字节切片,然后通过append的操作,将字符串的字节依次加入到该字节切片中。因为切片预分配空间的特性,可参考切片扩容,以有效的减少内存分配的次数,以提高性能。

1
2
3
4
5
6
7
golang复制代码func concat(ids []string) string {
sb := strings.Builder{}
for _, id := range ids {
_, _ = sb.WriteString(id)
}
return sb.String()
}

我们看下strings.Builder的数据结构:

1
2
3
4
golang复制代码type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}

由此可见,Builder的结构体中有一个buf []byte,当执行sb.WriteString(id)方法时,实际上是调用了append的方法,将字符串的每个字节都存储到了字节切片buf中。如下图所示:

上图中,第一次分配的内存空间是8个字节,这跟Go的内存管理有关系,网上有很多相关文章,这里不再详细讨论。

如果我们能提前知道要拼接的字符串的长度,我们还可以提前使用Builder的Grow方法来预分配内存,这样在整个字符串拼接过程中只需要分配一次内存就好了,极大的提高了字符串拼接的性能。如下图所示及代码:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
golang复制代码func concat(ids []string) string {
total := 0
for i := 0; i < len(ids); i++ {
total += len(ids[i])
}

sb := strings.Builder{}
sb.Grow(total)
for _, id := range ids {
_, _ = sb.WriteString(id)
}
return sb.String()
}

strings.Builder的使用场景一般是在循环中对字符串进行拼接,如果只是拼接两个或少数几个字符串的话,推荐使用 “+”操作符,例如: s := s1 + s2 + s3,该操作并非每个 + 操作符都计算一次长度,而是会首先计算三个字符串的总长度,然后分配对应的内存,再将三个字符串都拷贝到新申请的内存中去。

05 无用字符串的转换

我们在实际项目中往往会遇到这种场景:是选择字节切片还是字符串的场景。而大多数程序员会倾向于选择字符串。但是,很多IO的操作实际上是使用字节切片的。其实,bytes包中也有很多和strings包中相同操作的函数。

我们看这样一个例子:实现一个getBytes函数,该函数接收一个io.Reader参数作为读取的数据源,然后调用sanitize函数,该函数的作用是去除字符串内容两端的空白字符。我们看下第一个实现:

1
2
3
4
5
6
7
8
golang复制代码func getBytes(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
// Call sanitize
return []byte(sanitize(string(b))), nil
}

函数sanitize接收一个字符串类型的参数的实现:

1
2
3
golang复制代码func sanitize(s string) string {
return strings.TrimSpace(s)
}

这其实是将字节切片先转换成了字符串,然后又将字符串转换成字节切片返回了。其实,在bytes包中有同样的去除空格的函数bytes.TrimSpace,使用该函数就避免了对字节切片到字符串多余的转换。

1
2
3
golang复制代码func sanitize(s []byte) []byte {
return bytes.TrimSpace(s)
}

06 子字符串操作及内存泄露

字符串的切分也会跟切片的切分一样,可能会造成内存泄露。下面我们看一个例子:有一个handleLog的函数,接收一个string类型的参数log,假设log的前4个字节存储的是log的message类型值,我们需要从log中提取出message类型,并存储到内存中。下面是相关代码:

1
2
3
4
5
6
7
8
golang复制代码func (s store) handleLog(log string) error {
if len(log) < 4 {
return errors.New("log is not correctly formatted")
}
message := log[:4]
s.store(message)
// Do something
}

我们使用log[:4]的方式提取出了message,那么该实现有什么问题吗?我们假设参数log是一个包含成千上万个字符的字符串。当我们使用log[:4]操作时,实际上是返回了一个字节切片,该切片的长度是4,而容量则是log字符串的整体长度。那么实际上我们存储的message不是包含4个字节的空间,而是整个log字符串长度的空间。所以就有可能会造成内存泄露。 如下图所示:

那怎么避免呢?使用拷贝。将uuid提取后拷贝到一个字节切片中,这时该字节切片的长度和容量都是36。如下:

1
2
3
4
5
6
7
8
golang复制代码func (s store) handleLog(log string) error {
if len(log) < 36 {
return errors.New("log is not correctly formatted")
}
uuid := string([]byte(log[:36]))
s.store(uuid)
// Do something
}

07 小结

字符串是Go语言的一种基本类型,在Go语言中有自己的特性。字符串本质上是一个具有长度和指向底层数组的指针的结构体。在Go中,字符串是以utf-8编码的字节序列将每个字符的unicode编码存储在指针指向的数组中的,因此字符串是不可被修改的。在实际项目中,我们尤其要注意字符串和字节切片之间的转换以及在字符串拼接时的性能问题。

本文转载自: 掘金

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

LeetCode383 赎金信

发表于 2021-11-19

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

题目描述:

383. 赎金信 - 力扣(LeetCode) (leetcode-cn.com)

为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思。

给你一个赎金信 (ransomNote) 字符串和一个杂志(magazine)字符串,判断 ransomNote 能不能由 magazines 里面的字符构成。

如果可以构成,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例一

1
2
ini复制代码输入: ransomNote = "a", magazine = "b"
输出: false

示例二

1
2
ini复制代码输入: ransomNote = "aa", magazine = "ab"
输出: false

示例三

1
2
ini复制代码输入: ransomNote = "aa", magazine = "aab"
输出: true

提示:

  • 1 <= ransomNote.length, magazine.length <= 10^5
  • ransomNote 和 magazine 由小写英文字母组成

思路分析

哈希表

别看题目说的花里胡哨的,其实就是要看 赎金信 中的字母在杂志中能不能都找出来。数学上来说就是子集的概念而已。

明白了题意,我们就很容易想到哈希表的解法了,我们可以利用hash表记录 杂志 中每个字母的个数,也就是我们有的字母,然后遍历 赎金信 的时候,每找到一个就减掉个数,如果出现 负数, 说明我们的 杂志 已经不能满足我们的需求了。

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kotlin复制代码class Solution {
fun canConstruct(ransomNote: String, magazine: String): Boolean {
val bucket = IntArray(26)

for (c in magazine) {
bucket[c - 'a']++
}

for (c in ransomNote) {
if (bucket[c - 'a'] <= 0) {
return false
}
bucket[c - 'a']--
}

return true
}

}

总结

每日简单一题。

参考

小白简单思路 - 赎金信 - 力扣(LeetCode) (leetcode-cn.com)

python3 Counter永远的神,使用“与”操作,极少代码解决问题 - 赎金信 - 力扣(LeetCode) (leetcode-cn.com)

哈希 - 赎金信 - 力扣(LeetCode) (leetcode-cn.com)

本文转载自: 掘金

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

(六)Nacos开发教程之后台系统使用

发表于 2021-11-19

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

前情回顾

第六篇关于Nacos的文章了,今天的文章既可以算作是Nacos的开发教程,同样也可以被称为Nacos后台系统的操作手册,简单的普及一下其后台系统相关功能。

Nacos后台系统

Nacos后台系统中一共有着以下菜单:

image.png

接下来就让我们看一下这些菜单下功能的基本使用吧。

配置管理

配置管理是Nacos重点功能之一,其中一共有三个小菜单,分别是配置列表、历史版本和监听查询。

分别提供了相应模块配置的增删改查、配置文件的历史记录及可回滚操作、监听消费配置IP信息等功能。

让我们一个一个的去看一下如何使用。

添加配置

image.png

Data ID: 此参数标明配置文件的名称,一般设置为与模块名称相同,如果需要增加环境的不同,那么在名称之后增加-dev、-test后缀即可。

Group: 分组,Nacos提供了默认分组DEFAULT_GROUP。

描述: 备注描述,可填可不填,不是很重要。

配置格式: Nacos提供了多种配置格式,分别是text、json、xml、yaml、html、properties格式,我们在使用SpringCloud时,一般都是推荐使用yaml格式文件。

配置内容: 配置内容就是相应的配置了,比如指定数据库等等。

添加完成后,我们就会在列表中查询到一条数据了,如下图:

image.png

在列表中存在操作列,其中可以查看、编辑、删除等,最重要的是提供了一个示例代码,可以通过此示例代码来进行动态调用Nacos配置。

image.png

历史版本

历史版本,在每次修改配置的时候,Nacos会进行存储历史版本的配置文件,由此可以避免错误的修改。

监听查询

监听查询,Nacos提供了监听订阅者IP相关信息的功能,由此可以确认是否可以正常的推送最新配置。

image.png

服务管理

服务管理菜单,分别有着服务列表、订阅者列表两类功能。

服务列表

服务列表,自然是Nacos服务治理功能中的一个查询功能了,通过这个列表可以查询到目前注册到Nacos的相关服务。

订阅者列表

订阅者列表,此功能也很好理解,意思就是可以查询到有哪些IP客户端将服务注册至Nacos中了,此列表可以查询到相关信息。

权限控制

image.png

权限控制中共存在用户管理、角色管理、权限管理三大类功能,虽然此功能不是Nacos针对服务提供的主要功能,但是同样重要。

此权限功能是针对当前Nacos后台设计的,包含着用户的增删改查、角色的增删改查,同时提供了角色的授权功能。

大家可以自行去操作一下,操作非常简单。

命名空间

命名空间功能,是为了隔离多种用户逻辑,或者是多种环境,比如声明开发dev,测试test、生产pro等三种命名空间,以此来有效的隔离不同操作。

image.png

新增的命名空间ID,可以通过项目中namespace属性来指定即可。

集群管理

如果你需要部署Nacos集群,或者是已经部署了Nacos集群,就可以在当前页面中查询出相应Nacos集群中各个节点的Nacos服务器。

image.png

总结

Nacos后台系统的可操作性很好,所以学习如何使用的话,不会花费很长的时间,只需要一点点的精力就可以了,大家学到了吗?

本文转载自: 掘金

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

JVM里的GC背景与原理你知道吗? 一、GC背景 二、根据什

发表于 2021-11-19

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

一、GC背景

为什么会有 GC?

本质上是内存资源的有限性,因此需要大家共享使用,手工申请,手动释放。只不过Java这门优秀的语言替我们做了。

二、根据什么判断对象是垃圾对象呢

1、引用计数法

引用计数算法就是在对象中添加了一个引用计数器,当有地方引用这个对象时,引用计数器的值就加1,当引用失效的时候,引用计数器的值就减1。当引用计数器的值为0时,jvm就开始回收这个对象。

  简单的来说,在JVM中的栈中,如果栈帧中指向了一个对象,那么堆中的引用计数器的值就会加1,当栈帧这个指向null时,对象的引用计数器就减1。

  这种方法虽然很简单、高效,但是JVM一般不会选择这个方法,因为这个方法会出现一个问题:当对象之间相互指向时,两个对象的引用计数器的值都会加1,而由于两个对象时相互指向,所以引用不会失效,这样JVM就无法回收。

2、可达性分析算法

针对引用计数算法存在的问题,JVM采用了另一种方法:定义一个名为”GC Roots”的对象作为起始点,这个”GC Roots”可以有多个,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。

GC Roots对象一般包括有:

1.虚拟机栈(栈帧中本地变量表)中引用的对象;

2.方法区中类静态属性引用的对象;

3.方法区中常量引用的对象;

4.本地方法栈中JNI(Native方法)引用的对象。

三、清除垃圾对象算法

1、标识清除算法

标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间。

  缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。

2、标识-复制算法

把内存空间分成两个维度,每次只使用其中一个空间。当开始gc的时,垃圾回收器会把相应的垃圾清除,剩下的存活对象按照顺序排列完成清理。

  缺点:复制算法把有用的空间压缩了一半,因为每次只能使用一半的空间用作分配,剩下的用作gc后的分配。

3、标记—压缩算法

定义:标记压缩算法是标记清除的改进版本,当垃圾收集器把垃圾清除后,下一步还会对内存碎片进行整理,把存活对象统一的整理到一边。

  缺点:虽然算法免去了内存碎片的出现和节省了空间,但这种算法需要频繁的移动对象,所以会造成gc效率的降低。

标记—清除算法 标识-复制算法 标记—压缩算法
速度 中等 最快 最慢
空间开销 少(堆积碎片) 通常需要两倍的空间 少
移动对象 否 是 是

由表格可知:

  1. 标记——清除算法由于速度效率不高且会产生内存碎片,在实际中也很少被垃圾收集器使用。
  2. 而标记-复制算法由于它的效率较高,在经常发生GC的新生代区应用是个不错的选择,因为在新生代的存活对象一般不多,对于空间的需求不高,而且还可以通过对象提升把对象放入老年代。
  3. 而标记压缩算法则可以应用在GC不那么频繁的老年代,虽然效率较低,但由于GC的次数没有那么频繁、同时由于可以进行内存碎片的整理,也有利于老年代的大对象的存放。

本文转载自: 掘金

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

力扣第一百题-相同的树 前言 一、思路 二、实现 三、总结

发表于 2021-11-19

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

前言

力扣第一百题 相同的树 如下所示:

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

1
2
css复制代码输入: p = [1,2,3], q = [1,2,3]
输出: true

一、思路

这一题其实很简单,两个树相同是指:

  • 两个树在结构上相同
  • 两个数上个对应节点具有相同的值

很显然这一题我们使用递归来比对相同方向各节点的值即可,只要出现了不相同的情况,则代表这两个树不相同。反之,则这两个树为相同的。

这里我使用的是 前序遍历(根左右) 来遍历这两个树,我们分别从 p 和 q 的根节点出发,再比对它两的左孩子,最后比较它两的右孩子。

需要注意的是,节点会有哪些情况是表示这两个节点不同呢?

  1. 有一个节点为空,另一个节点不为空
  2. 两个不为空的节点值不相等

除了上面的这两种情况,其他情况均认为节点相等,包括两个都为 空节点。

综上所述,大致的步骤如下所示:

  1. 比对当前节点是否相同,如不同则返回 false
  2. 比对当前节点的左孩子是否相同,如不同则返回 false
  3. 比对当前节点的右孩子是否相同,如不同则返回 false

举个例子

此处以示例中的 p = [1,2,3], q = [1,2,3] 作为例子

  1. 从根节点出发,先比对 p 和 q 的根节点,显然 1 == 1,则继续向下
  2. 比对他们的左孩子,显然 2 == 2,则继续向下
  3. 再比左孩子 2 的左孩子和孩子,发现都为 null,则向上回溯
  4. 回到节点 1,比对它两的右孩子 3,发现相同,继续向下
  5. 再比对右孩子 3 的左孩子和孩子,发现都为 null
  6. 所有节点均相等,返回结果 true 即可

二、实现

实现代码

实现代码与思路中保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public boolean isSameTree(TreeNode p, TreeNode q) {
return dfs(p, q);
}

private boolean dfs(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
} else if (p == null || q == null) {
return false;
} else if (p.val != q.val) {
return false;
}
return dfs(p.left, q.left) && dfs(p.right, q.right);
}

测试代码

1
2
3
4
5
java复制代码    public static void main(String[] args) {
TreeNode p = new TreeNode(1, new TreeNode(2), new TreeNode(3));
TreeNode q = new TreeNode(1, new TreeNode(2), new TreeNode(3));
new Number100().isSameTree(p, q);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

每天一个 Linux 命令(14)—— iostat 命令简

发表于 2021-11-19

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

命令简介

iostat 是 I/O statistics(输入/输出统计)的缩写。iostat 命令主要用于对系统的磁盘操作活动进行监视。,包括 读写次数与读写的数据量、I/O 请求的服务时间与等待时间 等。同时,iostat 命令还可用于 考察 CPU 的利用率,考察 NFS 网络文件系统的处理活动 等。

iostat 命令生成的第一个报告是从系统启动迄今的平均统计数据。随后的每个报告是从前一个报告开始至指定时间间隔期间收集的统计数据。统计报告通常包括一个 CPU 标题行,以及 CPU 利用率的统计数据。在一个多 CPU 的系统中,CPU 的统计数据是所有 CPU 处理活动的平均值。此外,统计报告也会包括一个设备标题行,以及每个 I/O 设备的统计数据。当指定了 -n 选项时,iostat 仅显示 NFS 标题行及每个已安装的 NFS 文件系统的统计数据。

iostat 也有一个弱点,就是它不能对某个进程进行深入分析,仅对系统的整体情况进行分析。

命令格式

1
css复制代码iostat[参数][时间][次数]

命令参数

参数 解释
-c 显示 CPU 使用情况的分类统计数据。
-d 显示设备使用情况的分类统计数据。
-h 按照易读的适当单位显示 -n 选项提供的 NFS 统计数据。
-k 以 KB/s(而非默认的每秒数据块)为单位显示统计数据。
-m 以 MB/s 为单位显示统计数据。
-n 显示 NFS 统计数据。
-N 用于显示 LVM2 逻辑卷的统计数据。
`-p[devices ALL]`
-t 在显示的统计数据中增加时间信息(时间的显示格式依赖于 S_TIME_FORMAT 环境变量的设置)。
-V 显示命令的版本与版权信息,然后退出。
-x 显示附加的统计信息。
-z 禁止输出数据采样期间没有 I/O 活动的任何设备。

输出数据

iostat 命令能够生成 3 类统计数据:CPU 使用情况、I/O 设备使用情况以及 NFS 网络文件系统。

CPU使用情况(在多 CPU 的系统中,统计数据是所有 CPU 的平均值)

字段 解释
%user 用户模式运行时间(应用)占用整个 CPU 时间的百分比。
%nice 用户模式运行时间(处理具有优先级调整值的进程)占用整个 CPU 时间的百分比。
%system 内核模式运行时间(系统)占用整个 CPU 时间的百分比。
%iowait CPU 因等待 I/O 完成而处于空闲状态期间占用整个 CPU 时间的百分比。
%steal 虚拟 CPU 因等待虚拟机管理器提供服务花费的时间占用整个 CPU 时间的百分比。
%idle CPU 完全空闲时(期间没有等待处理的 I/O 请求)占用整个 CPU 时间的百分比。

存储设备使用情况(针对每个存储设备或分区的分类统计)

当需要考察一个具体的块设备,显示相关的统计数据时,可在命令行中指定块设备或分区的设备文件名。如果既未指定块设备文件名,也未指定分区设备文件名,则分别显示系统使用的每个存储设备的统计数据。如果指定了关键字 ALL,则显示系统维护的每个设备(包括未用的设备)的统计数据。根据命令行选项,输出的报告通常会包含下列字段。

字段 解释
Device 设备或分区的名字(标准设备文件名 /dev/name 省略 /dev 部分)
tps 设备每秒执行数据 I/O 的实际次数(多个逻辑 I/O 请求可以合并为一个实际 I/O 请求)。
Blk_read/s 每秒从设备读取的数据块数量。
Blk_wrtn/s 每秒写入设备的数据块数量。
Blk_read 从设备读取的数据块总量。
Blk_wrtn 写入设备的数据块总量。
kB_read/s 每秒从设备读取的数据量(KB)。
kB_wrtn/s 每秒写入设备的数据量(KB)。
kB_read 从设备读取的数据总和(KB)。
kB_wrtn 写入设备的数据总和(KB)。
MB_read/s 每秒从设备读取的数据量(MB)。
MB_wrtn/s 每秒写入设备的数据量(MB)。
MB_read 从设备读取的数据总和(MB)。
MB_wrtn 写入设备的数据总和(MB)。
rrqm/s 每秒合并请求读设备的次数。
wrqm/s 每秒合并请求写设备的次数。
r/s 设备每秒受理读请求的次数。
w/s 设备每秒受理写请求的次数。
rsec/s 每秒从设备读取的扇区数量。
wsec/s 每秒写入设备的扇区数量。
rkB/s 每秒从设备读取的数据量(KB)。
wkB/s 每秒写入设备的数据量(KB)。
rMB/s 每秒从设备读取的数据量(MB)。
wMB/s 每秒写入设备的数据量(MB)。
avgrq-sz 设备受理读写请求的平均扇区数量。
avgqu-sz 设备受理读写请求的平均队列长度。
await 设备处理 I/O 请求的平均等待时间(毫秒),包括队列等待时间与实际服务时间。
svctm 设备处理 I/O 请求的平均服务时间(毫秒)。
%util 设备处理 I/O 请求的时间占用整个 CPU 时间的百分比。如果这个数值接近 100%,表示设备即将达到饱和状态。

NFS 网络文件系统(针对已安装的每个网络文件系统的分类统计)

字段 解释
Filesystem NFS 服务器的主机名与共享目录资源名(形如 host:/directory)。
rBlk_nor/s 应用通过 read(2) 系统调用读取的数据块数量。
wBlk_nor/s 应用通过 write(2) 系统调用写出的数据块数量。
rBlk_dir/s 从打开标志为 O_DIRECT 的文件中读取的数据块数量。
wBlk_dir/s 写到打开标志为 O_DIRECT 的文件中的数据块数量。
rBlk_svr/s NFS 客户机通过 NFS READ 请求从 NFS 服务器读取的数据块数量。
wBlk_svr/s NFS 客户机通过 NFS WRITE 请求写到 NFS 服务器的数据块数量。
rkB_nor/s 应用通过 read(2) 系统调用读取的数据量(KB)。
wkB_nor/s 应用通过 write(2) 系统调用写出的数据量(KB)。
rkB_dir/s 从打开标志为 O_DIRECT 的文件中读取的数据量(KB)。
wkB_dir/s 写到打开标志为O_DIRECT的文件中的数据量(KB)。
rkB_svr/s NFS 客户机通过 NFS READ 请求从 NFS 服务器读取的数据量(KB)。
wkB_svr/s NFS 客户机通过 NFS WRITE 请求写到 NFS 服务器的数据量(KB)。
rMB_nor/s 应用通过 read(2) 系统调用读取的数据量(MB)。
wMB_nor/s 应用通过 write(2) 系统调用读出的数据量(MB)。
rMB_dir/s 从打开标志为 O_DIRECT 的文件中读取的数据量(MB)。
wMB_dir/s 写到打开标志为 O_DIRECT 的文件中的数据量(MB)。
rMB_svr/s NFS 客户机通过 NFS READ 请求从 NFS 服务器读取的数据量(MB)。
wMB_svr/s NFS 客户机通过 NFS WRITE 请求写到 NFS 服务器的数据量(KB)。
ops/s NFS 服务器文件系统每秒受理的读写操作的数量。
rops/s NFS 服务器文件系统每秒受理的读操作的数量。
wops/s NFS 服务器文件系统每秒受理的写操作的数量。

应用实例

  1. 显示所有设备负载情况
1
复制代码iostat
  1. 定时显示所有信息
1
复制代码iostat 2 3

参考文档

  • iostat命令
  • 《Linux 常用命令简明手册》—— 邢国庆编著

本文转载自: 掘金

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

Python 中的面向对象和异常处理

发表于 2021-11-19

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

前言

在之前我们已经说过了 Python 中内置的主要的几种对象类型,(数,字符串,列表,元组和字典)。而面向对象的核心人物还没出场呢 。那么我们常说的对象是什么类型的呢,其实他的类型就是“类”。继承封装和多态,这是通用的面向对象编程的思想 。

Python 中的继承

继承是为了提高代码的复用性,子类可以通过继承父类来实现父类中的方法,这就是光明正大的偷懒 。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码class Person():
def eat(self):
print("person can eat ...")
def slepp(self):
print("person can slepp ...")

calss Man(Person):
def hardWork(self):
print("man should be work hard ...")

# 测试
m = Man()
m.eat()
# person can eat ...

以上一个例子,说明了很多问题,首先,定义类是使用class关键字,定义方法使用def,默认传入一个参数,其实这个参数不一定非要叫self但是为了辨识性,我们这样定义,因为它代表的就是当前对象,类似 Java 中的 this 。当然还有我们的继承是通过一个括号+父类来定义的,那为什么 Person 没有写呢,其实这里省略了一个object 不写就表示默认继承 object 超类 。另外,Python 支持多继承,像这样即可,calss Man(Animal,Person) 一个问题需要注意,当多个父类中含有同一个方法时,以后面父类中的为准 。但是,不推荐大家使用多重继承 。

Python 中的封装

封装,理解起来很简单,就是将类中的属性信息隐藏起来,提供公共的方法以备调用,我们将属性进行 ” 私有化 “,在属性的前面加上两个下划线 __name 定义一个假的私有的属性 。看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby复制代码class Man():

def __init(self): # 这是对象的初始化方法,创建对象是默认执行
self.__name = ''

def set_name(self,name):
self.__name = name
def get_name(self):
return self.__name

m = Man() # 创建对象
m.set_name('YJK923') # 设置 name 值 ( 其实是 _Man__name )
m.get_name() # 获取 name 值 ( 其实是 _Man__name )
'YJK923'
m.name = 'YJK' #注意这里是另外添加了一个属性 name
m.get_name() # 获取 name 值 ( 其实是 _Man__name )
'YJK923'
m.name # 获取的是刚刚为 m 创建的 name 的值
'YJK'
m._Man__name # 获取属性 _Man__name ,这就是 Python 猫腻的地方,其实并没有私有化,只是转化格式了 。
'YJK923'

Python 中的多态

还有就是多态了,简单理解,就是有多种状态,常见的就是同一个方法但是执行的效果却不一样,就像是同一个名字人有太多了,而每个人却又不一样,看吧,编程思想也都是来自于日常的生活 。举例 ,都是睡觉 ,但是有的人喜欢躺在床上,有的人喜欢睡在椅子上 。用代码怎么实现呢?看下面

1
2
3
4
5
6
7
ruby复制代码class People():
def sleep(self):
print("人睡觉 。。。")

class Roommate(People):
def sleep(self):
print('睡在椅子上 。。。')

看吧,同样是睡觉,Roommate 却是睡在椅子上,通过继承的方式实现多态只是实现多态的一种方式而已 。还可以通过其它的方式,比方说这样,方法的参数是超类。

1
2
3
kotlin复制代码# 不同的对象调用同样的方法,结果却一样 。
fun(obj):
print( obj.__len__() )

Python 中常用的方法

附加说几个比方常用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码# 标准模块 random 中包含一个名为 choice 的函数,用于从序列中水机选择一个元素。
from random import choice
x = choice(['Hello,world !',[1,2,'e','e',4]])
x.count('e')
2 # 随机生成的,也可能不是 2

# 判断类 A 是不是 B 的子类
issubclass(A,B) # 儿子在前,老子在后

# 查找类 A 的所有父类
A.__bases__

# 查找一个对象 A 中的所有属性
A.__dict__

# 查找对象 A 属于哪一个类
A.__class__

# 检查方法或属性是否存在与对象中
hasattr(instance,'methedName | attrName')

# 设置对象的属性
setattr(instance,'attrName',value')

Python 中的抽象类

关于抽象类:定义了一种规则(抽象方法),继承这个类的子类必须实现抽象类中的抽象方法 。而且,抽象类是不能被实例化的 。

Python 中引入了 abc 模块来实现抽象类的定义,示例:

1
2
3
4
5
6
7
8
python复制代码# 下面表示定义了一个 抽象类 Talker , 包含一个抽象方法 talk .

from abc import ABC,abstractmethod

class Talker(ABC):
@abstractmethod
def talk(self):
pass

插播一曲关于面向对象设计的一些思考 。

  1. 将相关的东西放在一起,如果一个方法要使用全局变量,那就将他作为类的属性和方法
  2. 不要让对象之间过于亲密 。这就是所谓的解耦和吧 。
  3. 慎用继承,尤其是多重继承 。
  4. 保持简单,让方法尽可能的短小精悍 。
1
2
3
4
5
6
7
8
复制代码如何将需求转化为具体的实体类呢 ? 我们可以尝试这样做 。
将需求描述出来记录其中的名词,动词和形容词。
在名词中找出可能的类,
在动词中找出可能的方法,
在形容词中找出可能的属性,
最后将找出的方法和属性分配给各个类。
这样类的模型就出来了,然后我们可以思考对象之间的关系,继承或是组合。
后面再思考一下对应业务有哪些可以使用的模式,待各个业务模块都思考清楚后就可以着手编程了 。

Python 中的异常处理机制

下面简单的说一下 Python 中的异常处理机制 。

抛出异常使用关键字 raise 例如,raise Exception('exception msg !') 但是需要注意的是异常的抛出只能是 Exception 或 Exception 的子类 。

捕获异常:我们可以使用try ... except : ... finally: ... 语句块来处理可能出现异常的代码 。

1
2
3
4
5
6
7
8
python复制代码try 
1 / 0
except ZeroDivisionError as e:
print(e)
else :
print('No exception will run ...')
finally :
print('must be run ... ')

自定义异常类,定义一个类继承 Exception 类即可 。

1
2
kotlin复制代码class MyException(Exception):
pass

本文转载自: 掘金

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

🍃【Spring专题】「实战系列」重新回顾一下异常重试框架S

发表于 2021-11-19

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

重试机制的业务背景

外部服务对于调用者来说一般都是不可靠的,尤其是在网络环境比较差的情况下,网络抖动很容易导致请求超时等异常情况,这时候就需要用失败重试策略重新调用 API 接口来获取。

在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。

重试策略的介绍和限制

重试策略在服务治理方面也有很广泛的使用,通过定时检测,来查看服务是否存活。

重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

  • 远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

重试的场景有哪些?

外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。

常用的重试框架

  • Spring异常重试框架Spring Retry:Spring Retry支持集成到Spring或者Spring Boot项目中,而它支持AOP的切面注入写法,所以在引入时必须引入aspectjweaver.jar包。
  • sisyphus 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。
  • github.com/houbb/sisyp…
  • guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

spring-retry的重试机制

Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。

This is a small extension to Google’s Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.

Maven配置

启用重试功能

启动类上面添加@EnableRetry注解,启用重试功能,或者在使用retry的service上面添加也可以,或者Configuration配置类上面。建议所有的Enable配置加在启动类上,可以清晰地统一管理使用的功能。

添加@Retryable和@Recover注解

@Retryable注解,被注解的方法发生异常时会重试
  • value:指定发生的异常进行重试
  • include:和value一样,默认空,当exclude也为空时,所有异常都重试
  • exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试
  • maxAttemps:重试次数,默认3
  • backoff:重试补偿机制,默认没有
@Backoff注解
  • delay:指定延迟后重试
  • multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二 次为10秒,第三次为20秒
@Recover注解

当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。

guava-retry

guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

guava-retry的Git地址

github.com/rholder/gua…

优势

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callable 的 call() 方法,遇到异常之后,重试 3 次停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}

其主要接口及策略介绍

  • Attempt:一次执行任务;
  • AttemptTimeLimiter:单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
  • BlockStrategies:任务阻塞策略(通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么……- - BlockStrategies.THREAD_SLEEP_STRATEGY 也就是调用 Thread.sleep(sleepTime);
  • RetryException:重试异常;
  • RetryListener:自定义重试监听器,可以用于异步记录错误日志;
  • StopStrategy:停止重试策略,提供三种:
  • StopAfterDelayStrategy:设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
  • NeverStopStrategy:不停止,用于需要一直轮训直到返回期望结果的情况;
  • StopAfterAttemptStrategy:设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
  • WaitStrategy:等待时长策略(控制时间间隔),返回结果为下次执行时长:
  • FixedWaitStrategy:固定等待时长策略;
  • RandomWaitStrategy:随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)
  • IncrementingWaitStrategy:递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
  • ExponentialWaitStrategy:指数等待时长策略;
  • FibonacciWaitStrategy :Fibonacci 等待时长策略;
  • ExceptionWaitStrategy :异常时长等待策略;
  • CompositeWaitStrategy :复合时长等待策略;

根据结果判断是否重试

使用场景:如果返回值决定是否要重试。

重试接口

重试策略设定无限重试

使用场景:在有异常情况下,无限重试(默认执行策略),直到返回正常有效结果;

根据异常判断是否重试

使用场景:根据抛出异常类型判断是否执行重试。

等待策略——设定重试等待固定时长策略

使用场景:设定每次重试等待间隔固定为10s;

等待策略——设定重试等待时长固定增长策略

场景:设定初始等待时长值,并设定固定增长步长,但不设定最大等待时长;

例如:调用间隔时间递增1秒:

重试框架的总结

优雅重试共性和原理

  • 正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介,还有一种方式,是开发者自己编写重试机制,但是大多不够优雅
  • 约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。
  • 都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。
  • spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。两种方式都是比较优雅的重试策略,Spring-retry配置更简单,实现的功能也相对简单,Guava本身就是谷歌推出的精品java类库,guava-retry也是功能非常强大,相比较于Spring-Retry在是否重试的判断条件上有更多的选择性,可以作为Spring-retry的补充。

优雅重试适用场景

功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。

对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。

对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。

本文转载自: 掘金

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

1…273274275…956

开发者博客

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