(AT、TCC、Saga、XA)模式分析

四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景

  • AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎 0 学习成本。
  • TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
  • Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
  • XA 模式是分布式强一致性的解决方案,但性能低而使用较少。

由于 Saga 不保证隔离性,所以我们在业务设计的时候需要做到“宁可长款,不可短款”的原则,长款是指在出现差错的时候站在我方的角度钱多了的情况,钱少了则是短款,因为如果长款可以给客户退款,而短款则可能钱追不回来了,也就是说在业务设计的时候,一定是先扣客户帐再入帐,如果因为隔离性问题造成覆盖更新,也不会出现钱少了的情况。

软件环境

virtualbox: 6.1
centos: 7.0
jdk: 1.8
rocketmq: https://archive.apache.org/dist/rocketmq/4.5.1/rocketmq-all-4.5.1-bin-release.zip
rocketmq-console: https://github.com/apache/rocketmq-externals/tree/release-rocketmq-console-1.0.0
redis: Redis-x64-5.0.10
mysql: 5.6

配置

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
# 修改主机名
hostnamectl set-hostname node1.com
hostnamectl set-hostname node2.com
hostnamectl set-hostname node3.com
hostnamectl set-hostname node4.com

# 刷新终端
bash
# 查看是否生效
hostname

# 修改网络为具体某个ip
vi /etc/sysconfig/network-scripts/ifcfg-ens0s8
TYPE=Ethernet
BOOTPROTO=none
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=enp0s8
UUID=ed6c068c-149c-4678-8a13-d6e34a5d50c9
DEVICE=enp0s8
ONBOOT=yes
IPADDR=192.168.56.11
GATEWAY=192.168.56.1
DNS1=10.0.2.2
DNS2=8.8.8.8

# 重启网络
systemctl restart network

修改和同步 hosts

  1. 修改 node1.com
1
2
3
4
192.168.56.11 node1.com
192.168.56.12 node2.com
192.168.56.13 node3.com
192.168.56.14 node4.com
1
2
3
4
scp /etc/hosts root@node1.com:/etc/hosts
scp /etc/hosts root@node2.com:/etc/hosts
scp /etc/hosts root@node3.com:/etc/hosts
scp /etc/hosts root@node4.com:/etc/hosts

启动 RocketMQ

启动 nameserver
node1 操作:

1
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqnamesrv

node2 操作:

1
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqnamesrv

启动 broker

1
2
3
4
5
6
7
8
[root@node1 ~]#
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqbroker -n "node1.com:9876;node2.com:9876" -c /root/c/rocketmq-all-4.5.1-bin-release/conf/2m-2s-sync/broker-a.properties
[root@node2 ~]#
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqbroker -n "node1.com:9876;node2.com:9876" -c /root/c/rocketmq-all-4.5.1-bin-release/conf/2m-2s-sync/broker-b.properties
[root@node3 ~]#
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqbroker -n "node1.com:9876;node2.com:9876" -c /root/c/rocketmq-all-4.5.1-bin-release/conf/2m-2s-sync/broker-a-s.properties
[root@node4 ~]#
/root/c/rocketmq-all-4.5.1-bin-release/bin/mqbroker -n "node1.com:9876;node2.com:9876" -c /root/c/rocketmq-all-4.5.1-bin-release/conf/2m-2s-sync/broker-b-s.properties

启动控制台

1
2
3
cd /root/c/rocketmq-externals-rocketmq-console-1.0.0
mvn clean package -Dmaven.test.skip
java -jar /root/c/rocketmq-externals-rocketmq-console-1.0.0/rocketmq-console/target/rocketmq-console-ng-1.0.0.jar

分布式事务

通过分布式事务来保证一致性(数据库与 MQ 的一致性)

RocketMQ-2021-06-15-11-33-52

RocketMQ 事务消息的实现原理就是基于两阶段提交和事务状态回查,来决定消息最终是提交还是回滚。

演示

下单页
http://localhost:8081/order
RocketMQ实现秒杀-2021-06-16-11-09-12

支付页
http://localhost:8081/pay
RocketMQ实现秒杀-2021-06-16-11-09-30

批量购买
RocketMQ实现秒杀-2021-06-16-11-14-49
不会导致库存超卖
RocketMQ实现秒杀-2021-06-16-11-10-18

源码

https://gitee.com/lyloou/practice-rocketmq-seckill/tree/feature_transaction/

判断数组中的所有的数字是否只出现一次。

给定一个数组 array,判断数组 array 中是否所有的数字只出现一次。
例如,arr = {1, 2, 3},输出 YES
又如,arr = {1, 2, 1},输出 NO
约束时间复杂度为 O(n)

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
public class Main {
public static void main(String[] args) {
final int[] data1 = {1, 2, 3};
final int[] data2 = {1, 2, 1};

System.out.println(getOnlyOnceStatus1(data1));
System.out.println(getOnlyOnceStatus1(data2));
}

/**
* 通过set来存储,比较数量
*
* @param data 数组
* @return 结果
*/
private static String getOnlyOnceStatus1(int[] data) {

Set<Integer> result = new HashSet<>(data.length);
for (int datum : data) {
if (!result.add(datum)) {
return "NO";
}
}

if (result.size() == data.length) {
return "YES";
}
return "NO";
}
}
/*
YES
NO
*/

找出数组中只出现一次的数字,其它数字都出现了两次

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
public class Main {
public static void main(String[] args) {
final int[] data1 = {1, 2, 3, 3, 2, 1, 4, 6, 4};
final int[] data2 = {1, 2, 1};

System.out.println(xorIntArray(data1));
System.out.println(xorIntArray(data2));
}

/**
* 异或操作
*
* @param data 数组
* @return 结果
*/
private static Integer xorIntArray(int[] data) {

if (data == null || data.length == 0) {
return null;
}
if (data.length == 1) {
return data[0];
}
int result = data[0];
for (int i = 1; i < data.length; i++) {
result ^= data[i];
}

return result;
}
}
/*
6
2
*/

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
102
103
/**
* 判断链表是否有环
*
* @author lilou
* @since 2021/6/2
*/
public class Ring {
public static class Node {
private final Integer id;
private final String name;
private Node next;

public Node(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

public Node getNext() {
return next;
}

public void setNext(Node next) {
this.next = next;
}
}

public static void main(String[] args) {
final Node n1 = new Node(1, "赵云");
final Node n2 = new Node(2, "关羽");
final Node n3 = new Node(3, "张飞");
final Node n4 = new Node(4, "刘备");
final Node n5 = new Node(5, "曹操");
n1.setNext(n2);
n2.setNext(n3);
n3.setNext(n4);
n4.setNext(n5);
n5.setNext(n1);

boolean isRing = isRing2(n1);
System.out.println(isRing);
}

/**
* 方法1:快慢指针
*
* @param n1 节点
* @return 是否有环
*/
private static boolean isRing(Node n1) {
if (n1 == null) {
return false;
}

// 慢指针
Node slow = n1;
// 快指针,从下一个开始
Node fast = n1.getNext();
while (fast != null && fast.getNext() != null) {
if (fast == slow) {
return true;
}
slow = slow.getNext();
fast = fast.getNext().getNext();
}

return false;
}

/**
* 方法2:
* 1个指针原地不动,
* 1个指针一步一步向前搜索
*
* @param n1 节点
* @return 是否有环
*/
private static boolean isRing2(Node n1) {
if (n1 == null) {
return false;
}
// 原地不动
Node notMovePointer = n1;
// 步长为1,向前搜索
Node forwardPointer = n1.getNext();
while (forwardPointer != null) {
if (notMovePointer == forwardPointer) {
return true;
}
forwardPointer = forwardPointer.getNext();
}

return false;
}
}

软件环境

virtualbox: 6.1
centos: 7.0
jdk: 1.8
rocketmq: https://archive.apache.org/dist/rocketmq/4.5.1/rocketmq-all-4.5.1-bin-release.zip
rocketmq-console: https://github.com/apache/rocketmq-externals/tree/release-rocketmq-console-1.0.0

启动 RocketMQ

启动 mq server

1
./mqnamesrv

启动 broker

1
./mqbroker -n localhost:9876

示例

1
2
3
4
5
6
# 配置 namesrv 地址环境
export NAMESRV_ADDR=localhost:9876
# 示例:生产
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
# 示例:消费
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

启动控制台

1
2
3
cd /root/c/rocketmq-externals-rocketmq-console-1.0.0
mvn clean package -Dmaven.test.skip
java -jar /root/c/rocketmq-externals-rocketmq-console-1.0.0/rocketmq-console/target/rocketmq-console-ng-1.0.0.jar

基于 RocketMQ 分布式事务

RocketMQ-2021-06-15-11-33-52

RocketMQ 事务消息的实现原理就是基于两阶段提交和事务状态回查,来决定消息最终是提交还是回滚。

遍历根到所有叶子节点的路径

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.lyloou.tool.tree;

import java.util.ArrayList;
import java.util.List;


/*
*
* **** 示例1 ****
1
2 3
4 5 6 7

输出如下:
1 2 4
1 2 5
1 3 6
1 3 7


* **** 示例2 ****
1
2 3
5
a = TreeNode(1)
b = TreeNode(2)
c = TreeNode(3)
d = TreeNode(5)
a.left = b
a.right = c
b.right = d

输出如下:
['1->2->5', '1->3']
*/
public class Solution2 {
public static void main(String[] args) {
test1();
test2();
}

private static void test1() {
TreeNode a = new TreeNode(1);
TreeNode b = new TreeNode(2);
TreeNode c = new TreeNode(3);
TreeNode d = new TreeNode(5);
a.left = b;
a.right = c;
b.right = d;
System.out.println(binaryTreePaths(a));
}

private static void test2() {
TreeNode a1 = new TreeNode(1);
TreeNode a2 = new TreeNode(2);
TreeNode a3 = new TreeNode(3);
TreeNode a4 = new TreeNode(4);
TreeNode a5 = new TreeNode(5);
TreeNode a6 = new TreeNode(6);
TreeNode a7 = new TreeNode(7);
a1.left = a2;
a1.right = a3;
a2.left = a4;
a2.right = a5;
a3.left = a6;
a3.right = a7;
System.out.println(binaryTreePaths(a1));
}

/**
* @param root the root of the binary tree
* @return all root-to-leaf paths
*/
private static List<String> binaryTreePaths(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
// 有几个叶子节点,就有几条路径
if (isLeaf(root)) {
List<String> list = new ArrayList<>();
list.add(root.val + "");
return list;
}

// 获取左子树的列表,循环向元素前面追加当前值
final List<String> leftList = binaryTreePaths(root.left);
for (int i = 0; i < leftList.size(); i++) {
leftList.set(i, root.val + "->" + leftList.get(i));
}

// 获取右子树的列表,循环向元素前面追加当前值
final List<String> rightList = binaryTreePaths(root.right);
for (int i = 0; i < rightList.size(); i++) {
rightList.set(i, root.val + "->" + rightList.get(i));
}

// 拼接成一个列表返回
List<String> result = new ArrayList<>(leftList.size() + rightList.size());
result.addAll(leftList);
result.addAll(rightList);
return result;
}


/**
* 判断node是否为叶子节点
*
* @param node 节点
* @return true 为叶子节点,
*/
private static boolean isLeaf(TreeNode node) {
return node.left == null && node.right == null;
}

static class TreeNode {
public int val;
public TreeNode left, right;

public TreeNode(int val) {
this.val = val;
this.left = this.right = null;
}
}
}

运行结果

all-to-leaf-path-2021-05-25-00-35-39

maven 修改版本号命令

1
2
3
4
5
# 更新根模块及所有子模块的版本号,同时会生成 pom.xml.versionsBackup 文件
mvn versions:set -DnewVersion=x.x.x-SNAPSHOT

# 提交版本修改 同时会删除 pom.xml.versionsBackup 文件。
mvn versions:commit

ref:

多个 maven 模块的项目,只打包某个模块和其关联的模块

参考:Maven 多个 mudule 只编译、打包指定 module_fqwgc8 的博客-CSDN 博客_maven 编译指定 module

例如 A,B,P 的继承关系为
P
|
—– A
|
—– B

1
2
3
4
-pl, --projects
Build specified reactor projects instead of all projects
-am, --also-make
If project list is specified, also build projects required by the list

打包 A

1
2
3
4
5
# 进入目录 P
mvn install -pl A -am
# 添加prod参数
mvn install -pl A -am -Pprod
mvn package -pl A -am -Pprod

maven 多模块

模块太多,编译指定模块

1
2
3
4
5
6
7
8
#!/bin/bash
# 编译指定module
# [continuous integration - Skip a submodule during a Maven build - Stack Overflow](https://stackoverflow.com/questions/8304110/skip-a-submodule-during-a-maven-build)

mvn -pl \
:marketing-api-tv-topic-pk,\
:marketing-api-phone-topic-pk\
clean install -Dmaven.skip.test=true

查看包依赖情况,从什么时候引入的

通过 IntelliJ IDEA 和 Maven 命令查看某个 jar 包是怎么引入的 - 小墨的童鞋 - 博客园

1
mvn dependency:tree -Dverbose -Dincludes=org.yaml:snakeyaml

打包时跳过测试

注意用的是 package 命令 而不是 war 命令

1
mvn clean package -Dmaven.test.skip=true

java8 中,stream 流多次使用会抛出异常:java.lang.IllegalStateException: stream has already been operated upon or closed

所以如何解决呢——如何以并发方式在同一个流上执行多种操作

可以 借助 Spliterator,尤其是它的延迟绑定能力,结合 BlockingQueues 和 Futures 来实现这一大有裨益的特性

代码实现

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* 如何以并发方式在同一个流上执行多种操作
* 参考:java8_in_action 附录C
*
* @author lilou
* @since 2021/5/7
*/
public class StreamForker<T> {
private final Stream<T> stream;
private final Map<Object, Function<Stream<T>, ?>> forks = new HashMap<>();

public StreamForker(Stream<T> stream) {
this.stream = stream;
}

public StreamForker<T> fork(Object key, Function<Stream<T>, ?> f) {
forks.put(key, f);
return this;
}

public Results getResults() {
ForkingStreamConsumer<T> consumer = build();
try {
stream.sequential().forEach(consumer);
} finally {
consumer.finish();
}
return consumer;
}

private ForkingStreamConsumer<T> build() {
List<BlockingQueue<T>> queues = new ArrayList<>();
Map<Object, Future<?>> actions = forks.entrySet()
.stream()
.reduce(
new HashMap<Object, Future<?>>(),
(map, e) -> {
map.put(e.getKey(), getOperationResult(queues, e.getValue()));
return map;
},
(m1, m2) -> {
m1.putAll(m2);
return m1;
}
);
return new ForkingStreamConsumer<>(queues, actions);
}

private Future<?> getOperationResult(List<BlockingQueue<T>> queues, Function<Stream<T>, ?> f) {
BlockingQueue<T> queue = new LinkedBlockingQueue<>();
queues.add(queue);
final Spliterator<T> spliterator = new BlockingQueueSpliterator<>(queue);
final Stream<T> source = StreamSupport.stream(spliterator, false);
return CompletableFuture.supplyAsync(() -> f.apply(source));
}

public static interface Results {

public <R> R get(Object key);
}

static class ForkingStreamConsumer<T> implements Consumer<T>, Results {
static final Object END_OF_STREAM = new Object();
private final List<BlockingQueue<T>> queues;
private final Map<Object, Future<?>> actions;

public ForkingStreamConsumer(List<BlockingQueue<T>> queues, Map<Object, Future<?>> actions) {
this.queues = queues;
this.actions = actions;
}

@SuppressWarnings({"unchecked", "SSBasedInspection"})
@Override
public <R> R get(Object key) {
try {
return ((Future<R>) actions.get(key)).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public void accept(T t) {
queues.forEach(q -> q.add(t));
}

public void finish() {
//noinspection unchecked
accept((T) END_OF_STREAM);
}
}

static class BlockingQueueSpliterator<T> implements Spliterator<T> {
private final BlockingQueue<T> q;

public BlockingQueueSpliterator(BlockingQueue<T> q) {
this.q = q;
}

@Override
public boolean tryAdvance(Consumer<? super T> action) {
T t;
while (true) {
//noinspection SSBasedInspection
try {
t = q.take();
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (t != ForkingStreamConsumer.END_OF_STREAM) {
action.accept(t);
return true;
}
return false;
}

@Override
public Spliterator<T> trySplit() {
return null;
}

@Override
public long estimateSize() {
return 0;
}

@Override
public int characteristics() {
return 0;
}
}
}

测试

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


import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @author lilou
* @since 2021/5/7
*/
public class StreamForkerTest {
public static void main(String[] args) {
int[] a = new int[100];
Arrays.fill(a, 1);
Arrays.parallelPrefix(a, Integer::sum);

final Stream<Integer> integerStream = Arrays.stream(a).boxed();
final StreamForker.Results results = new StreamForker<>(integerStream)
.fork("平均值", StreamForkerTest::average)
.fork("求和", StreamForkerTest::sum)
.fork("拼接", StreamForkerTest::concat)
.getResults();

String average = results.get("平均值");
String sum = results.get("求和");
String concat = results.get("拼接");

System.out.println("平均值:" + average);
System.out.println("求和:" + sum);
System.out.println("拼接:" + concat);

}

private static String concat(Stream<Integer> stream) {
return stream.map(String::valueOf).collect(Collectors.joining(","));
}

private static String sum(Stream<Integer> stream) {
return stream.reduce(Integer::sum).map(String::valueOf).orElse("");
}

private static String average(Stream<Integer> stream) {
return stream.mapToInt(Integer::intValue).summaryStatistics().getAverage() + "";
}
}

结果

1
2
3
平均值:50.5
求和:5050
拼接: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

设置主机名称

1
2
3
4
# 设置主机名
hostnamectl set-hostname yourhostname
# 刷新生效
bash

查看版本

1
2
3
cat /etc/redhat-release
uname -a
uname -r

配置固定 ip

编辑:vi /etc/sysconfig/network-scripts/ifcfg-ens0s8
(注意:ens0s8 这个名字要和 ip addr中的名字保持一致,如果不一样修改文件名即可,否则会重启失败)

默认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TYPE=Ethernet
BOOTPROTO=dhcp
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=enp0s8
UUID=ed6c068c-149c-4678-8a13-d6e34a5d50c9
DEVICE=enp0s8
ONBOOT=yes
DNS1=10.0.2.2
DNS2=8.8.8.8

自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
TYPE=Ethernet
BOOTPROTO=none
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_PEERDNS=yes
IPV6_PEERROUTES=yes
IPV6_FAILURE_FATAL=no
NAME=enp0s8
UUID=ed6c068c-149c-4678-8a13-d6e34a5d50c9
DEVICE=enp0s8
ONBOOT=yes
IPADDR=172.20.130.84
GATEWAY=172.20.130.1
DNS1=10.0.2.2
DNS2=8.8.8.8

重启: systemctl restart network

关闭防火墙

1
2
3
systemctl status firewalld
systemctl stop firewalld
systemctl disable firewalld

免密登录

1
ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.20.130.84
0%