0%

前言

近段时间在了解分布式时,经常绕不开一个算法: 一致性哈希算法。于是在了解并实践这个算法后,就有了此文章。

算法间的对比

在分布式分片中,存在着几种算法: 取模,分段,一致性 hash。

取模 分段 一致性哈希
上层是否感知
迁移成本 低,只涉及相邻节点
单点故障影响 低,只影响相邻节点
算法复杂度
热点数据 存在 存在 存在

一致性哈希主要解决问题

从上述对比可知,一致性哈希主要降低节点上下线中带来的数据迁移成本,同时节点数量的变化与分片原则于上层无感,使上层更专注于领域内逻辑的编写,使整体架构更加灵活。

一致性 hash 原理

  1. 基本数据结构

​ 基本数据类型因人而已,常规的哈希取模采用大多采用将数据 hash 到 2^32 - 1的空间中,一致性哈希通常在此基础上将空间变成一个环。如下图所示。

​ 本次实现采用的是 key 按大小排列的哈希表。原打算使用数组承接数据,但排序成本随 key 的增多而加大,遂放弃。

  1. 数据存储

    数据存储与哈希取模算法大致相同,都是通过计算存入数据的哈希值放入对应的哈希槽上。但一致性哈希差异之处在于当计算 hash 不在环上,数据存入首个 hash 槽中。

    场景假设: 现已上线 4 节点(server1 ~ 4),对应 hash 值为 hash1 ~ 4。现有5个数据(hash1 ~ 5)于存入节点中,结果如下图所示。

​ 本次实现采用的思路是

1
2
3
1. 计算存入数据的 hash 值
2. 寻找最近的(比数据 hash 值大的最小的节点 hash)节点并写入
3. 若 2 中未能寻找服务器,则写入第一个(hash 最小)节点中
  1. 节点上线

​ 新节点加入一致性哈希环中,原理是通过计算节点所代表的 hash 值,并根据计算值将节点映射在环上,最后迁移相邻节点数据到新节点上。

​ 场景假设: 现已上线 4 台服务器(server1 ~ 4),对应 hash 值为 hash1 ~ 4。现有一个新节点(hash5)节点上线到环上。结果如下图所示。

​ 本次实现采用的思路是

1
2
3
4
5
1. 计算上线节点 hash 值
2. 计算上线节点所新增的虚拟节点的 hash 值(若初始化指定虚拟节点数量)
3. 寻找最近的(比上线节点与虚拟节点 hash 值大的最小的节点 hash)节点,取出节点数据
4. 将1 2点节点加入到 hash 环中
5. 将 3 中取出的数据重新放入到 hash 环上
  1. 节点下线

​ 已有节点下线,原理是通过计算节点所代表的 hash 值,取出节点所含数据,下线节点,将取出数据重新放入 hash 环上。

​ 场景假设: 现已上线 5 台服务器(server1 ~ 5),对应 hash 值为 hash1 ~ 5。现节点 server4 下线。结果如下图所示。

​ 本次实现采用的思路是

1
2
3
4
1. 计算下线节点 hash 值
2. 取出下线节点以及虚拟节点(若初始化指定虚拟节点数量)存储数据
3. 将下线节点以及虚拟节点(若初始化指定虚拟节点数量)从 hash 环上移除
4. 将 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
package org.CommonAlgorithms.ConsistentHash;

import org.CommonAlgorithms.HashAlgorithm.HashService;
import java.util.List;

/**
* consistentHashing interface
* @author cartoon
* @since 10/01/2021
* @version 1.1
*/
public interface ConsistentHashing {

/**
* put data to hash loop
* @param data data list
* @return put result
*/
boolean putData(List<String> data);

/**
* put data to hash loop
* @param data data
* @return put result
*/
boolean putData(String data);

/**
* remove node from hash loop
* @param node removing node
* @return remove result
*/
boolean removeNode(String node);

/**
* add node to hash loop
* @param node adding node
* @return add result
*/
boolean addNode(String node);

/**
* inject hash method to hash loop
* @param hashService hash method
* @throws UnsupportedOperationException if loop already has node
*/
void setHashMethod(HashService hashService);

/**
* print all data in loop according ascending hash value with nodes
*/
void printAllData();

}
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package org.CommonAlgorithms.ConsistentHash;

import org.CommonAlgorithms.HashAlgorithm.HashService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;

/**
* consistent hash achieve
* @author cartoon
* @since 2021/01/17
*/
public class ConsistentHashingImpl implements ConsistentHashing {

private static final Logger log = LoggerFactory.getLogger(ConsistentHashingImpl.class);

/**
* virtual node name template
*/
private static final String virtualNodeFormat = "%s&&%d";

/**
* real node and its virtual node mapping
*/
private SortedMap<String, List<String>> realNodeToVirtualNode;

/**
* hash and its node mapping
*/
private SortedMap<Integer, String> hashToNodes;

/**
* node and its data mapping
*/
private Map<String, List<String>> nodeToData;

/**
* determine virtual node's number of each node
*/
private int virtualNodeNum;

/**
* inject hash method, if null, use loop default hash method
*/
private HashService hashService;


public ConsistentHashingImpl() {
this(0, new String[0]);
}

public ConsistentHashingImpl(String... nodes) {
this(0, nodes);
}

public ConsistentHashingImpl(int virtualNodeNum) {
this(virtualNodeNum, new String[0]);
}

public ConsistentHashingImpl(int virtualNodeNum, String... nodes) {
//1. intercept virtual num smaller than 0
if(virtualNodeNum < 0){
log.error("virtual num is not allow smaller than 0");
throw new IllegalArgumentException();
}
//2. initialize loop member attributes
this.virtualNodeNum = virtualNodeNum;
realNodeToVirtualNode = new TreeMap<>();
hashToNodes = new TreeMap<>();
nodeToData = new HashMap<>();
for(String server : nodes){
hashToNodes.put(getHash(server), server);
nodeToData.put(server, new LinkedList<>());
}
//3. if virtual node number bigger than 0, add virtual node
if(virtualNodeNum > 0){
for(String server : nodes){
addVirtualNode(server);
}
}
}

@Override
public boolean putData(List<String> data) {
//1. circulate call put data method to add data to loop
for(String incomingData : data){
if(!putData(incomingData)){
return false;
}
}
return true;
}

@Override
public boolean putData(String data) {
if(hashToNodes.isEmpty()){
log.error("put data, usable server is empty");
return false;
}
//1. calculate data's hash value
int currentHash = getHash(data);
//2. get usual node(node's hash value is bigger than data's hash value), if usual node list is empty, get first node in loop
SortedMap<Integer, String> usableNodes = hashToNodes.tailMap(currentHash);
String node = usableNodes.isEmpty() ? hashToNodes.get(hashToNodes.firstKey()) : usableNodes.get(usableNodes.firstKey());
//3. add data to node
List<String> dataList = nodeToData.get(node);
dataList.add(data);
log.info("put data, data {} is placed to server {}, hash: {}", data, node, currentHash);
return true;
}

@Override
public boolean removeNode(String node) {
//1. calculate hash value of removing node
int removeServerHash = getHash(node);
if(!hashToNodes.containsKey(removeServerHash)){
log.error("remove server, current server is not in server list, please check server ip");
return false;
}
//2. get data from removing node
List<String> removeServerData = nodeToData.get(node);
//3. get removing node's virtual node data, remove all virtual node with removing node
if(virtualNodeNum != 0){
for(String virtualNode : realNodeToVirtualNode.get(node)){
removeServerData.addAll(nodeToData.get(virtualNode));
hashToNodes.remove(getHash(virtualNode));
nodeToData.remove(virtualNode);
}
}
//4. remove node from hash loop
hashToNodes.remove(removeServerHash);
nodeToData.remove(node);
if(hashToNodes.size() == 0){
log.info("remove server, after remove, server list is empty");
return true;
}
//5. put data to loop by call put data method
putData(removeServerData);
log.info("remove server, remove server {} success", node);
return true;
}

@Override
public boolean addNode(String node) {
//1, calculate adding node's hash value
int addServerHash = getHash(node);
//2. add node and migrate data
if(hashToNodes.isEmpty()){
//2.1 add node and its virtual node to loop directly when current loop is empty
hashToNodes.put(addServerHash, node);
nodeToData.put(node, new LinkedList<>());
if(virtualNodeNum > 0){
addVirtualNode(node);
}
} else{
//2.2.1 get data to be migrated from loop
SortedMap<Integer, String> greatServers = hashToNodes.tailMap(addServerHash);
String greatServer = greatServers.isEmpty() ? hashToNodes.get(hashToNodes.firstKey()) : greatServers.get(greatServers.firstKey());
List<String> firstGreatServerData = new LinkedList<>(nodeToData.get(greatServer));
//2.2.2 add node and its virtual node to loop
hashToNodes.put(addServerHash, node);
nodeToData.put(greatServer, new LinkedList<>());
nodeToData.put(node, new LinkedList<>());
if(virtualNodeNum != 0){
addVirtualNode(node);
}
//2.2.3 migrate 2.2.1 data to loop by call put data method
putData(firstGreatServerData);
}
log.info("add server, server {} has been added", node);
return true;
}

@Override
public void printAllData() {
nodeToData.forEach((server, data) -> log.info("server {} contains data {}", server, data));
}

@Override
public void setHashMethod(HashService hashService) {
if(!hashToNodes.isEmpty()){
throw new UnsupportedOperationException();
}
this.hashService = hashService;
}

private void addVirtualNode(String realNode){
if(virtualNodeNum > 0){
List<String> virtualNodeList = new LinkedList<>();
for(int cnt = 0; cnt < this.virtualNodeNum; cnt++){
//1. generate virtual node name by default format
String virtualNodeName = String.format(virtualNodeFormat, realNode, cnt);
//2. calculate each virtual node's hash value
int virtualNodeHash = getHash(virtualNodeName);
//3. current node already exist in loop, continue
if(hashToNodes.containsKey(virtualNodeHash)){
continue;
}
//4. add node to loop
virtualNodeList.add(virtualNodeName);
hashToNodes.put(virtualNodeHash, virtualNodeName);
nodeToData.put(virtualNodeName, new LinkedList<>());
}
//5. map virtual node to real node
realNodeToVirtualNode.put(realNode, virtualNodeList);
}
}


private int getHash(String data){
return hashService == null ? defaultGetHash(data) : hashService.getHash(data);
}

private int defaultGetHash(String data){
int res = 0;
for(char tempChar : data.toCharArray()){
if(tempChar >= '0' && tempChar <= '9'){
res += tempChar;
}
}
return res;
}
}

测试结果

不带虚拟节点的一致性哈希
测试代码
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
package ConsistentHash;

import org.CommonAlgorithms.ConsistentHash.ConsistentHashing;
import org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author cartoon
* @date 2020/12/27
*/
public class ConsistentHashingWithoutVirtualNodeTest {

private static final Logger log = LoggerFactory.getLogger(ConsistentHashingWithoutVirtualNodeTest.class);

private ConsistentHashing consistentHashing;

private String[] servers;

private String[] data;

@Before
public void before(){
servers = new String[]{"000", "111", "222", "333", "555"};
consistentHashing = new ConsistentHashingImpl(servers);
data = new String[]{"000", "111", "222", "333", "555"};
}

@Test
public void testConsistentHashing(){
for(String str : data){
Assert.assertTrue(consistentHashing.putData(str));
}
consistentHashing.removeNode("333");
consistentHashing.addNode("444");
consistentHashing.putData("444");
consistentHashing.printAllData();
}
}
测试结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 000 is placed to server 000, hash: 144
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 111 is placed to server 111, hash: 147
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 222 is placed to server 222, hash: 150
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 333, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 555 is placed to server 555, hash: 159
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 555, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - remove server, remove server 333 success
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 555 is placed to server 555, hash: 159
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 444, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - add server, server 444 has been added
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 444 is placed to server 444, hash: 156
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 000 contains data [000]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 111 contains data [111]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 222 contains data [222]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 444 contains data [333, 444]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 555 contains data [555]
含虚拟节点的一致性哈希测试
测试代码
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
package ConsistentHash;

import org.CommonAlgorithms.ConsistentHash.ConsistentHashing;
import org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author cartoon
* @date 2021/01/17
*/
public class ConsistentHashingWithVirtualNodeTest {

private static final Logger log = LoggerFactory.getLogger(ConsistentHashingWithVirtualNodeTest.class);

private ConsistentHashing consistentHashing;

private String[] servers;

private String[] data;

@Before
public void before(){
servers = new String[]{"000", "111", "222", "333", "555"};
consistentHashing = new ConsistentHashingImpl(3, servers);
data = new String[]{"000", "111", "222", "333", "555"};
}

@Test
public void testConsistentHashing(){
for(String str : data){
Assert.assertTrue(consistentHashing.putData(str));
}
consistentHashing.removeNode("333");
consistentHashing.addNode("444");
consistentHashing.putData("444");
consistentHashing.putData("555&&0");
consistentHashing.printAllData();
}
}
测试结果
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
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 000 is placed to server 000, hash: 144
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 111 is placed to server 111, hash: 147
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 222 is placed to server 222, hash: 150
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 333, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 555 is placed to server 555, hash: 159
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 555, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - remove server, remove server 333 success
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 555 is placed to server 555, hash: 159
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 333 is placed to server 444, hash: 153
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - add server, server 444 has been added
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 444 is placed to server 444, hash: 156
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - put data, data 555&&0 is placed to server 555&&0, hash: 207
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 000&&2 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 000&&1 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 000&&0 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 111&&1 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 111&&2 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 555&&1 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 555&&2 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 222&&2 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 444&&0 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 444&&1 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 444&&2 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 555&&0 contains data [555&&0]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 000 contains data [000]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 111 contains data [111]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 222 contains data [222]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 222&&0 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 444 contains data [333, 444]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 555 contains data [555]
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 222&&1 contains data []
[main] INFO org.CommonAlgorithms.ConsistentHash.ConsistentHashingImpl - server 111&&0 contains data []

实现存在的缺陷

1
2
3
1. 哈希算法过于简单,哈希冲突概率较大
2. 真实节点含有虚拟节点的数量不均
3. 节点上线时真实节点与已存在的虚拟节点的顺序冲突尚未解决

后记

本次实现的所有代码已全部上传到 https://github.com/cartoonYu/CommonAlgorithms,项目主要包含一些常用的算法,如排序算法,限流算法的简单实现,欢迎提 issue。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io

前言

最近在写框架时遇到需要根据特定配置(可能不存在)加载 bean 的需求,所以就学习了下 Spring 中如何获取配置的几种方式。

Spring 中获取配置的三种方式

  1. 通过 @Value 方式动态获取单个配置
  2. 通过 @ConfigurationProperties + 前缀方式批量获取配置
  3. 通过 Environment 动态获取单个配置

通过 @Value 动态获取单个配置

  1. 作用

    1. 可修饰到任一变量获取,使用较灵活
  2. 优点

    1. 使用简单,且使用关联的链路较短
  3. 缺点

    1. 配置名不能被有效枚举到

    2. 每一个配置的使用都需重新定义,使用较为麻烦

    3. 项目强依赖配置的定义,配置不存在则会导致项目无法启动

  4. 使用场景

    1. 项目强依赖该配置的加载,想要从源头避免因配置缺失导致的未知问题

    2. 只想使用少数几个配置

  5. 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    public class ConfigByValueAnnotation {

    @Value("${server.port}")
    private String serverPort;

    public String getServerPort() {
    return serverPort;
    }
    }
  6. 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@DisplayName("multipart get config")
@SpringBootTest
public class MultipartGetConfigTest {

private static final Logger log = LoggerFactory.getLogger(MultipartGetConfigTest.class);

@Autowired
private ConfigByValueAnnotation configByValueAnnotation;

@Test
public void getByValueAnnotation(){
log.info("get by @Value, value: {}", configByValueAnnotation.getServerPort());
}
}
  1. 测试结果
1
org.spring.demo.MultipartGetConfigTest   : get by @Value, value: 7100

通过 @ConfigurationProperties + 前缀方式批量获取

  1. 作用

    1. 用于配置类的修饰或批量配置的获取
  2. 优点

    1. 使用配置只需确定 key 的前缀即能使用,有利于批量获取场景的使用

    2. 因采用前缀匹配,所以在使用新的相同前缀 key 的配置时无需改动代码

  3. 缺点

    1. 使用复杂,需定义配置类或者手动创建 bean 后引入使用

    2. 增加新的前缀相同 key 时可能会引入不稳定因素

  4. 使用场景

    1. 需要同时使用多前缀相同 key 的配置

    2. 期望增加新配置但不修改代码的 properties 注入

  5. 代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Component
    @ConfigurationProperties(prefix = "server", ignoreInvalidFields = true)
    public class ConfigByConfigurationProperties {

    private Integer port;

    public Integer getPort() {
    return port;
    }

    public ConfigByConfigurationProperties setPort(Integer port) {
    this.port = port;
    return this;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    public class ConfigByConfigurationPropertiesV2 {

    @Bean("configByValueAnnotationV2")
    @ConfigurationProperties(prefix = "server2")
    public Properties properties(){
    return new Properties();
    }

    }
    1. 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@DisplayName("multipart get config")
@SpringBootTest
public class MultipartGetConfigTest {

private static final Logger log = LoggerFactory.getLogger(MultipartGetConfigTest.class);

@Autowired
private ConfigByConfigurationProperties configByConfigurationProperties;

@Autowired
@Qualifier("configByValueAnnotationV2")
private Properties properties;

@Test
public void getByConfigurationProperties(){
log.info("get by @ConfigurationProperties, value: {}", configByConfigurationProperties.getPort());
log.info("get by @ConfigurationProperties and manual create bean, value: {}", properties.getProperty("port"));
}

}
  1. 测试结果
1
2
org.spring.demo.MultipartGetConfigTest   : get by @ConfigurationProperties, value: 7100
org.spring.demo.MultipartGetConfigTest : get by @ConfigurationProperties and manual create bean, value: 7100

通过 Environment 动态获取单个配置

  1. 作用

    1. 用于动态在程序代码中获取配置,而配置 key 不需提前定义
  2. 优点

    1. 获取的配置的 key 可不提前定义,程序灵活性高

    2. 配置 key 可使用枚举统一放置与管理

  3. 缺点

    1. 使用较复杂,需继承 Environment 接口形成工具类进行获取

    2. 获取 key 对应的枚举与 key 定义分离,value 获取链路较长

  4. 使用场景

    1. 只需使用少量的配置

    2. 获取配置的 key 无提前定义,需要根据对配置的有无进行灵活使用

  5. 代码示例

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
@Component
public class ConfigByEnvironment implements EnvironmentAware {

private static final Logger log = LoggerFactory.getLogger(ConfigByEnvironment.class);

private Environment environment;

public Optional<String> get(String configKey){
String config = environment.getProperty(configKey);
return Objects.isNull(config) ? Optional.empty() : Optional.of(config);
}

public void get(String configKey, Consumer<String> consumer){
Optional<String> config = get(configKey);
if(!config.isPresent()){
log.warn("application config, get config by key fail, key: {}", configKey);
}
config.ifPresent(consumer);
}

@Override
public void setEnvironment(@NonNull Environment environment) {
this.environment = environment;
}
}

public enum ConfigByEnvironmentKey {

SERVER_PORT("server.port", "server port");

private String key;

private String description;

ConfigByEnvironmentKey(String key, String description) {
this.key = key;
this.description = description;
}

public String getKey() {
return key;
}

public String getDescription() {
return description;
}
}
  1. 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DisplayName("multipart get config")
@SpringBootTest
public class MultipartGetConfigTest {

private static final Logger log = LoggerFactory.getLogger(MultipartGetConfigTest.class);

@Autowired
private ConfigByEnvironment configByEnvironment;

@Test
public void getByEnvironment(){
configByEnvironment.get(ConfigByEnvironmentKey.SERVER_PORT.getKey()).ifPresent(value -> log.info("get by environment, value: {}", value));
}

}
  1. 测试结果
1
org.spring.demo.MultipartGetConfigTest   : get by environment, value: 7100

总结

获取配置方式 优点 缺点 使用场景
通过 @Value 动态获取单个配置 使用简单,且使用关联的链路较短 1. 配置名不能被有效枚举到
2. 每一个配置的使用都需重新定义,使用较为麻烦
3. 项目强依赖配置的定义,配置不存在则会导致项目无法启动
1. 项目强依赖该配置的加载,想要从源头避免因配置缺失导致的未知问题
2. 只想使用少数几个配置
通过 @ConfigurationProperties + 前缀方式批量获取 1. 使用配置只需确定 key 的前缀即能使用,有利于批量获取场景的使用
2. 因采用前缀匹配,所以在使用新的相同前缀 key 的配置时无需改动代码
1. 使用复杂,需定义配置类或者手动创建 bean 后引入使用
2. 增加新的前缀相同 key 时可能会引入不稳定因素
1. 需要同时使用多前缀相同 key 的配置
2. 期望增加新配置但不修改代码的 properties 注入
通过 Environment 动态获取单个配置 1. 获取的配置的 key 可不提前定义,程序灵活性高
2. 配置 key 可使用枚举统一放置与管理
1. 使用较复杂,需继承 Environment 接口形成工具类进行获取
2. 获取 key 对应的枚举与 key 定义分离,value 获取链路较长
1. 只需使用少量的配置
2. 获取配置的 key 无提前定义,需要根据对配置的有无进行灵活使用

本代码示例已放置于 github测试代码位置,有需要的可自取。

本文首发于 cartoon的博客

前言

这段时间在构建自己的开发工具集,避不开的就是各种中间件访问层的搭建。而 spring cloud 唯二绕不开的就是 eureka 了,所以就重复造轮子,以后忘记了也有所参考。

正文

前期准备

maven/gradle

eureka 服务器搭建

  1. 新建 spring boot 空项目

    这一步其实是非必要的,你也可以新建 maven/gradle 空项目或者普通的 web项目, 只是 spring boot 的自动配置比较方便。

  2. 修改 pom.xml 或者 build.gradle 文件(此处示例为 pom.xml 文件)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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
    <?xml version="1.0" encoding="UTF-8"?>
    <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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com</groupId>
    <artifactId>eureka</artifactId>
    <version>1.0.0</version>
    <name>eureka</name>
    <description>Demo project for Spring Boot</description>

    <properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
    <exclusion>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    </dependencies>

    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <encoding>UTF-8</encoding>
    </configuration>
    </plugin>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>
  3. 修改项目配置文件 application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 8761
    eureka:
    instance:
    hostname: eureka-server
    client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  4. 启动类增加 @EnableEurekaServer 注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {

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

    }

经过上述步骤启动项目,eureka服务器就启动完成了。

服务提供者与消费者构建

服务提供者构建
  1. 新建 spring boot 空项目
  2. 修改 pom.xml 或者 build.gradle 文件(此处示例为 pom.xml 文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>eurekaclient</artifactId>
<version>1.0.0</version>
<name>EurekaClient</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
  1. 修改项目配置文件 application.yml
1
2
3
4
5
6
7
eureka:
client:
service-url:
defaultZone: http://cartoon-ali.com:6001/eureka/ # 取决于你的eureka地址
spring:
application:
name: DemoProducer
  1. 新建模拟接口
1
2
3
4
5
6
7
8
@RestController
public class TextController {

@GetMapping("text")
public String text(){
return "eureka producer";
}
}
服务消费者构建
  1. 新建 spring boot 空项目
  2. 修改 pom.xml 或者 build.gradle 文件(此处示例为 pom.xml 文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org</groupId>
<artifactId>eurekaclient2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eurekaclient2</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
</properties>

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

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

</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
  1. 修改项目配置文件 application.yml
1
2
3
4
5
6
7
8
9
eureka:
client:
service-url:
defaultZone: http://cartoon-ali.com:6001/eureka/ # 取决于你的eureka地址
spring:
application:
name: DemoConsumer
server:
port: 8081
  1. 模拟消费接口构建
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class TextController {

@Autowired
private RestTemplate restTemplate;

@RequestMapping("/text")
public String text(){
return restTemplate.getForObject("http://DEMOPRODUCER/text", String.class);
}
}

启动

同时启动服务提供者与消费者,调用消费者模拟消费接口,即能看到调用的结果。

后记

项目代码已收录在我的个人工具集。因为我在工具集全面应用了 easyopen 做接口的收拢,所以关于 eureka 相关的内容可能有点不一样,但是总体步骤是一样的,欢迎 star

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于 cartoon的博客
&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/spring-cloud/Eureka服务端与客户端搭建/

近段时间学习极客时间李玥老师的后端存储实战课时,看到一个很多意思的东西:用kafka存储点击流的数据,并重复处理。在以往的使用中,kafka只是一个消息传输的载体,消息被消费后就不能再次消费。新知识与印象相冲突,于是就有了本篇文章:kafka数据如何被重复消费。

前期理论了解

首先我先去官网纠正了我对kafka的整体了解。

官网对kafka的描述是:一个分布式流平台。怪自己的学艺不精。

其次,我重新看了一下kafka消费者的消费过程:kafka首先通过push/poll(默认为poll)获取消息,接收消息处理完成后手动/自动提交消费成功,kafka服务器则根据提交情况决定是否移动当前偏移量。

方案确定

kafka消费者读取数据的位置是通过偏移量判断,那如果我能将偏移量手动设置为起始位置,就能实现重复消费?这个有搞头。

如何手动设置偏移量是关键。

show me the code

代码的关键主要在于偏移量设置 api 的调用,其余没什么特别。

要注意的是,代码中我分别调用了作用不同的设置偏移量,仅作为展示,可按需取用。

最后消费者消息消息时,我只使用默认的拉取条数设置消费一次,可按需进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* repeat kafka message
* @param host kafka host
* @param groupId kafka consumer group id
* @param autoCommit whether auto commit consume
* @param topic consume topic
* @param consumeTimeOut consume time out
*/
private void textResetOffset(String host, String groupId, Boolean autoCommit, String topic, Long consumeTimeOut){
//form a properties to new consumer
Properties properties = new Properties();
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, host);
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, autoCommit.toString());
properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//subscribe incoming topic
consumer.subscribe(Collections.singletonList(topic));
//get consumer consume partitions
List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
List<TopicPartition> topicPartitions = new ArrayList<>();
for(PartitionInfo partitionInfo : partitionInfos){
TopicPartition topicPartition = new TopicPartition(partitionInfo.topic(), partitionInfo.partition());
topicPartitions.add(topicPartition);
}
// poll data from kafka server to prevent lazy operation
consumer.poll(Duration.ofSeconds(consumeTimeOut));
//reset offset from beginning
consumer.seekToBeginning(topicPartitions);
//reset designated partition offset by designated spot
int offset = 20;
consumer.seek(topicPartitions.get(0), offset);
//reset offset to end
consumer.seekToEnd(topicPartitions);
//consume message as usual
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> iterator = records.iterator();
while (iterator.hasNext()){
ConsumerRecord<String, String> record = iterator.next();
log.info("consume data: {}", record.value());
}
}
运行结果

需注意的点

在手动设置偏移量时,遇到了一个exception

1
java.lang.IllegalStateException: No current assignment for partition test-0

翻了一下stackoverflow以及官方文档后,才了解到设置偏移量是一个lazy operation,官网的解释如下。

Seek to the first offset for each of the given partitions. This function evaluates lazily, seeking to the first offset in all partitions only when poll(long) or position(TopicPartition) are called. If no partition is provided, seek to the first offset for all of the currently assigned partitions.

于是我先进行一次 poll 操作后再设置偏移量。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于 cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/message-queue/kafka数据如何被重复消费/

前言

近段时间在准备毕业设计的前期准备,基本确定了前后端分离的架构,于是就需要用到了nginx。

在之前nginx是放在docker上,所以没有端口更改跟配置文件配置的烦恼。但是现在是直接放在服务器上,但是跟tomcat或者apollo的端口发生了冲突,于是就动了改端口以及配置文件位置的想法。

正文

nginx在linux上的安装

1
sudo apt-get install nginx

nginx安装完成后,文件目录会在/etc/nginx中,跟docker安装的有点类似。

nginx更换配置文件的配置

(其实也不算更换,算是增加,配置起来没那么麻烦)

  • 修改 /etc/nginx 中的nginx.conf
1
2
vim nginx.conf  --用vim打开文件
include /root/nginx/conf.d/*.conf; -- 放在http块中,路径自定义切换
  • 新建文件夹并重启nginx
1
nginx -s reload

nginx更换默认端口

  • 参照上文的更改配置文件的修改,增加一个语句
1
include /etc/nginx/sites-enabled/*;  -- 默认有的,但是最好检查一下
  • 修改 /etc/nginx/sites-enabled 中的 default 文件
1
2
3
vim default  -- 用vim打开文件
listen 8091 default_server; -- 修改监听端口
listen [::]:8091 default_server; -- 修改监听端口
  • 重启nginx
1
nginx -s reload

重新输入ip跟端口就能看到效果,但是如果像我一样搭在服务器上,记得开放服务器的端口(小小踩了坑的我)。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于 cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/nginx/更换nginx默认端口以及配置文件位置/

前言

这段时间在接触分布式的内容,由于本身比较熟悉rpc的原理,所以我顺其自然地选择了 dubbo 作为我学习的框架。
看了任务清单,这篇文章应该是在6天前出来的,但是因为实习等等的一些事情耽误了,今天立下决心动笔了。

准备

必需
JAVA 环境
注册中心(我选用的是 nacos )

非必需
maven / gradle(本文使用gradle构建)
docker
idea(这个应该是必需吧?当然也可以用记事本(滑稽.jpg))

正文

  1. 新建普通的 gradle 项目(不勾选任何选项)

  2. 新建三个module,分别命名为 Common,DubboProducer,DubboConsumer

  • 模块作用
1
2
3
Common -- 普通 gradle 项目,用于定义 proucer 以及 consumer 交互的接口以及规范
DubboProducer -- Spring Boot 本地项目,用于为 Common 中定义的服务接口创建实体类
DubboConsumer -- Spring Boot web 项目,接收用户请求,调用 producer 处理请求并返回结果
  • 模块 gradle 定义

    • 根项目 setting.gradle 新增
    1
    2
    3
    include 'Common'
    include 'DubboConsumer'
    include 'DubboProducer'
    • consumer 新增

      • build.gradle 新增
      1
      2
      3
      4
      5
      6
      // https://mvnrepository.com/artifact/org.apache.dubbo/dubbo
      compile group: 'org.apache.dubbo', name: 'dubbo', version: '2.7.8'
      // https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-nacos
      compile group: 'org.apache.dubbo', name: 'dubbo-registry-nacos', version: '2.7.8'
      // https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client
      compile group: 'com.alibaba.nacos', name: 'nacos-client', version: '1.3.3'
      • setting.gradle 新增
      1
      includeFlat 'Common'
    • producer 新增

      • build.gradle 新增
      1
      2
      3
      4
      5
      6
      // https://mvnrepository.com/artifact/org.apache.dubbo/dubbo
      compile group: 'org.apache.dubbo', name: 'dubbo', version: '2.7.8'
      // https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-nacos
      compile group: 'org.apache.dubbo', name: 'dubbo-registry-nacos', version: '2.7.8'
      // https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client
      compile group: 'com.alibaba.nacos', name: 'nacos-client', version: '1.3.3'
      • setting.gradle 新增
      1
      includeFlat 'Common'
  • 模块配置文件

    • consumer 配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    spring:
    application:
    name: dubbo-consumer
    cloud:
    nacos:
    discovery:
    server-addr: cartoon-ali.com
    dubbo:
    protocol:
    port: -1
    name: dubbo
    registry:
    address: nacos://cartoon-ali.com:8848
    cloud:
    subscribed-services: dubbo-spring-cloud-provider
    application:
    name: consumer
    • producer 配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    dubbo:
    registry:
    address: nacos://cartoon-ali.com:8848
    application:
    name: dubbo-producer
    protocol:
    port: -1
    name: dubbo
  • 启动类需同时使用 @EnableDubbo 修饰

    • producer
    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDubbo
    public class DubboProducerApplication {
    public static void main(String[] args) {
    SpringApplication.run(DubboProducerApplication.class, args);
    }
    }
    • consumer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableDubbo
    public class DubboConsumerApplication {

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

    }
  1. 示例搭建(Hello World)
  • 接口 DubboService构建
1
2
3
4
public interface DubboService {

String say();
}
  • 服务提供类实现
1
2
3
4
5
6
7
8
9
10
@org.apache.dubbo.config.annotation.DubboService
@Service
public class DubboServiceImpl implements DubboService {

@Override
public String say() {
return "dubbo producer";
}

}

注意:@Service 注解是 Srping 的注解,@org.apache.dubbo.config.annotation.Service 已在版本 2.7.7 被 @org.apache.dubbo.config.annotation.DubboService取代

  • 消费者实现
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class TestController {

@DubboReference
private DubboService dubboService;

@RequestMapping("/test")
public String test(){
return dubboService.say();
}
}

dubbo 的服务消费应该在消费者中的 Service 层做整合消费后返回处理结果,这里仅为演示。

  1. 运行

先运行 provider 再运行 consumer,否则 dubbo 会因无法找到服务提供者自行关闭消费者。

  • nacos 的结果

  • 模拟调用

后记

虽然在网上已经有很多这方面的教程,但是大多是用 zookeeper 作为注册中心。
而个人喜欢接触新技术,nacos 在今年1月才由阿里开源出来。而且我比较喜欢 nacos 的界面风格,虽然 nacos 在功能上不如 zookeeper+dubboAdmin 强大,但是作为入门应该是足够的。

源码地址

本文所涉及代码都已上传到**github**

修改历史

  1. 2019 年 11 月 09 日
    1. 文章初始版本编写
  2. 2020 年 10 月 11 日
    1. 删减错误的描述
      1. 服务的消费方与生产方启动类都应添加 @EnableDubbo 注解,修改前为生产方启动类应添加 @EnableDubbo 注解
    2. 优化部分描述
      1. 第四点运行中的模拟调用不再使用 RestServices 进行调用,使用更为普遍的 postman 调用
    3. 优化 demo 的代码结构
      1. 使用 gradle 构建,修改前为 maven
      2. 使用多组件方式进行项目的构建,使 demo 不再局限于 demo,可成为更加常用的工具

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/dubbo/dubbo与springboot的结合/

前言

近段时间秋招上岸了,于是每天疯狂补各种分布式基础,每天都在痛苦与快乐中度过。
在学习 nginx 的时候,遇到配置上的问题:root 与 alias 的区别,卡了大概三个小时,记录下来警醒自己不要再犯了。

正文

在使用 “/” 进行配置时,两者没有区别,一样都是在 root 或者 alias 指定的路径寻找文件,所以以下的过程与结果都跟此无关。

  • 测试用例的构建

    1
    2
    3
    4
    5
    6
    7
    location /static2 {
    root /static;
    }

    location /static1 {
    alias /static;
    }
  • 图片示例及存放位置

2.jpeg : /static/

5.jpeg : /static/static2/

  • 启动 nginx 并输入 url

http://192.168.99.100:8091/static1/2.jpeg

http://192.168.99.100:8091/static2/5.jpeg

  • 结果

  • 结果分析
    从访问 url,映射关系 relation 以及文件位置 location 综合来看,可以得出以下规律:

2.jpeg : alias = location !=url

5.jpeg : root + location = url

  • 结论

个人认为,alias 起到一个文件路径重定向的功能,能有效隐藏文件真实路径。相对来说root 更像在指定 root 文件夹中寻找文件,文件路径树容易被猜测导致安全问题的发生。

&nbsp;&nbsp;&nbsp;&nbsp;本文首发于 cartoon的博客

&nbsp;&nbsp;&nbsp;&nbsp;转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/nginx/nginx中root与alias关键字的区别/

前言

这段时间上岸了,就有时间整理电脑的资料(强迫症重度患者),就向maven以及gradle的仓库位置动手了。

目的

改变maven的默认位置

步骤

  • 修改maven的配置文件setting.xml(maven安装位置:\conf)

将localRepository的标签值修改成想要设置的目录。

  • 复制修改后的setting.xml到仓库所在位置并重启

  • (非必须)修改idea中maven设置

    Setting->Build->Build Tools->Maven

修改圈起来的三个配置项就可以了

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/pm/修改maven包本地默认位置/

前言

近段时间在学dubbo,dubbo-admin死活装不上,无论是本地还是docker,所以把目光投向了其他配置中心,我选定的是阿里新开源的nacos。

正文

  • 拉取镜像到本地docker

    1
    docker pull nacos/nacos-server
  • 新建nacos容器

    1
    docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server

    其中env参数是指定容器所处环境,这里是指建立单机版的nacos。

  • 新建数据库用于节点以及数据的保存

    1
    create database nacos_config;

    其中数据库名自定义

  • 导入脚本
    可以到官网复制或者用我上传的脚本,提取码为jm6z

  • 修改nacos在conf的配置文件application.properties

主要修改的地方有:

db.url.0
db.url.1
db.user
db.password

由于我主要作为测试用的,所以没有设置主从数据库,注释了db.url.1

经过上面六步,nacos就能正常使用。

后记

这就是我配置nacos的历程。
因为nacos是阿里在19年1月才开源出来的,所以网上的资料不算多,大多都要翻官方文档,所以我就吃一下螃蟹。
而在开发连接nacos作为dubbo的配置中心中,我也遇到一点小坑,我打算把它放在另一篇文章中,还没动手写,所以应该会迟点出来。

本文首发于cartoon的博客

转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/docker/docker下配置nacos/

前言

说真的,平常看源码都是自己看完自己懂,很少有写出来的冲动。
但是在写算法的时候,经常用到java中各种集合,其中也比较常用到remove方法。
remove有重载函数,分别传入参数是索引index或者数据Object(指定泛型后自动转换),如果指定泛型是其他数据类型还好,但是指定的是Integer或者是int的话,或者就有点懵了。
这曾经也困惑过我,所以我就唯有用实践解惑了。

测试类设计

  • 测试类一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Text {

    public void remove(int index){
    System.out.println("调用传参为int的remove方法");
    }

    public void remove(Integer object){
    System.out.println("调用传参为Integer的remove方法");
    }

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }
  • 测试类二

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Text {

    public void remove(Integer object){
    System.out.println("调用传参为Integer的remove方法");
    }

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }
  • 测试类三

    1
    2
    3
    4
    5
    6
    public class Text {

    public void remove(Object object){
    System.out.println("调用传参为Object的remove方法");
    }
    }

结果

三个测试类分别传入int,Integer,Object型变量,观察效果。

  • 测试类一

    传入类型为int:调用传参为int的remove方法
    传入类型为Integer:调用传参为Integer的remove方法
    传入类型为Object:调用传参为Object的remove方法

  • 测试类二

    传入类型为int:调用传参为Integer的remove方法
    传入类型为Integer:调用传参为Integer的remove方法
    传入类型为Object:调用传参为Object的remove方法

  • 测试类三

    传入类型为int:调用传参为Object的remove方法
    传入类型为Integer:调用传参为Object的remove方法
    传入类型为Object:调用传参为Object的remove方法

从输出结果可以看出,当方法的传参的类层级逐渐变高时,层级较低的传参会进行向上转型适应传参的需要。

原因分析

下面我们先反编译各测试类的源码,结果如下

  • 测试类一

    invokevirtual #11 // Method remove:(I)V

    invokevirtual #15 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #18 // Method remove:(Ljava/lang/Object;)V

  • 测试类二

    invokevirtual #11 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #11 // Method remove:(Ljava/lang/Integer;)V

    invokevirtual #17 // Method remove:(Ljava/lang/Object;)V

  • 测试类三

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

    invokevirtual #10 // Method remove:(Ljava/lang/Object;)V

可以看出,反编译代码中都是调用实例方法的命令,所以结果中自动”向上转型”其实是jvm的功劳。jvm通过在编译时确定调用的传参类型,静态分派到具体方法的。
所以在前言中的困惑已经解除了,就是由于jvm中静态分派的实现,调用次序是int->Integer->Object。

后记

也没什么想说的,感觉在阅读源码的时候必须多想想为什么这样做,为什么要这样实现,同时通过断点或者反编译的手段找出自己的答案。keep going!

本文首发于cartoon的博客
转载请注明出处:https://cartoonyu.github.io/cartoon-blog/post/java/java的list接口的remove重载方法调用原理/