spring cloud使用zookeeper作為注冊中心和配置中心
- 為什麼使用zookeeper作為注冊中心和配置中心
- 建構product子產品
-
- 使用SpringInitializr搭建項目環境
- 注冊服務到zookeeper
- 編寫接口,暴露服務給consumer,并重新開機服務
- 建構consumer子產品
-
- 使用Spring Initializr建構
- 添加application.yml配置
- 啟動類開啟服務發現注釋
- 編寫controller,并使用loadBalancerClient來調用product服務
- 啟動服務,并通路zkui,檢視注冊情況
- 通路consumer接口位址,擷取生産者暴露的服務
- zookeeper注冊中心實作原理
-
- 附加一份比較完善的服務發現配置
- 跟蹤源碼,檢視服務注冊和服務發現的地方
-
- 服務注冊
- 服務發現
- 使用zookeeper做配置中心
-
- 将application.yml重命名為bootstrap.yml
- 取消pom裡面關于zookeeper-config的注釋
- 在bootstrap.yml中添加config的配置資訊
- 然後将注冊的配置放入zookeeper中進行維護
- 修改product子產品中controller代碼,引用配置中心的參數
- 啟動product子產品,通過浏覽器通路暴露的接口
- 測試配置更新
- 修改consumer子產品的配置檔案,也使用zookeeper來維護配置
- 啟動consumer,調用product服務
- 寫在後面
為什麼使用zookeeper作為注冊中心和配置中心
1.因為zookeeper的資料結構比較簡單,而且與傳統的磁盤檔案系統不同的是,zk将全量資料存儲在記憶體中,可謂是高性能,并且支援叢集,另外還支援事件監聽。這些特點決定了zk特别适合作為注冊中心(資料釋出/訂閱)。
2.zookeeper在cap領域中占據了資料一緻性和分區容錯性,這讓zookeeper在擁有高資料一緻性的需求情況下,有很大的優勢,使我們在這種保證高資料一緻性的需求下,能夠滿足我們的需求
建構product子產品
使用SpringInitializr搭建項目環境
這裡我使用的開發軟體是idea,,如果有用到eclipse作為開發軟體的話,那請建立一個maven項目,将我下面的pom檔案複制過去,然後建立響應的目錄結構即可
1.使用idea建立項目,選擇Spring Initializr,點選next
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL4VERNpXRE5UeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL4QTMzAzNxAjM2EDNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2.填寫maven項目結構資訊,點選next
3.這裡使用spring boot 2.3.10版本,然後選中以下圖中元件,點選next
4.修改項目建構目錄等資訊,點選finish
5.項目結構如下
注冊服務到zookeeper
注意: 這裡我的zookeeper服務通過我之前寫的部落格搭建的,部落格位址如下
: docker篇-(docker-compose安裝zookeeper叢集,并使用nginx實作負載均衡)
這裡現在pom檔案裡面把spring-cloud-starter-zookeeper-config的依賴注釋調,因為spring-cloud-starter-config需要的配置優先級比較高,需要加載bootstrap.yml裡面的配置,是以這裡先把元件先注釋調,如下圖
1.建立application.yml檔案,建立如下配置資訊
spring:
application:
name: product
cloud:
zookeeper:
connect-string: 192.168.101.180:2181
server:
port: 9090
2.啟動程式,通過zk ui檢視注冊資訊
編寫接口,暴露服務給consumer,并重新開機服務
package com.lhstack.cloud.product.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lhstack
*/
@RestController
@RequestMapping
public class ProductController {
@GetMapping("product")
public String product(){
return "this is product";
}
}
建構consumer子產品
使用Spring Initializr建構
這裡新增ribbon的依賴
同理,在開始之前,我們也先把config相關的依賴給注釋掉
添加application.yml配置
spring:
application:
name: consumer
cloud:
zookeeper:
connect-string: 192.168.101.180:2181
server:
port: 8080
啟動類開啟服務發現注釋
因為這裡要擷取服務,是以需要使用EnableDiscoveryClient注釋來開啟服務發現
編寫controller,并使用loadBalancerClient來調用product服務
package com.lhstack.cloud.consumer.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author lhstack
*/
@RestController
@RequestMapping
public class ConsumerController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("consumer")
public String consumer(){
ServiceInstance serviceInstance = loadBalancerClient.choose("product");
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(serviceInstance.getUri().toString() + "/product",String.class);
}
}
啟動服務,并通路zkui,檢視注冊情況
通路consumer接口位址,擷取生産者暴露的服務
zookeeper注冊中心實作原理
附加一份比較完善的服務發現配置
spring:
application:
name: product
cloud:
zookeeper:
connect-string: 192.168.101.180:2181 #這裡多個位址,用,隔開即可
discovery:
metadata:
version: v1 #這個配置,就是在注冊服務之後,可以添加一些業務資料,如版本,如region等等,可以在後續開發gateway的過程中,或者使用loadbalancer實作灰階釋出,藍綠釋出等等的功能
root: /discovery #這裡,設定服務注冊的跟節點,檢視zkui,預設的根節點是/services,可以設定成多級目錄,如第一級目錄為項目名,第二級目錄為環境名等等,如/cms/dev,則就是cms項目dev環境,千萬不要使用spring.cloud.zookeeper.prefix,我也不知道為什麼,加了這個參數之後,就無法發現服務了
server:
port: 9090
跟蹤源碼,檢視服務注冊和服務發現的地方
-
服務注冊
...
public class ZookeeperAutoServiceRegistrationAutoConfiguration {
...
//我們在這裡可以擴充實作自己的ServiceInstanceRegistration
//在這裡我透露一下,spring-cloud-starter-zookeeper-discovery預設建立的zookeeper節點是臨時節點,如果不能滿足需求,可以自己通過這種方式擴充
//這裡我說明一下,zookeeper的零時節點是根據session失效來實作的,除了服務當機,基本就不會出現服務下線的情況,這也是zookeeper的特點所在,通過session來實作心跳的功能,性能會比http協定高很多,因為zookeeper是基于tcp協定的,同時本身就附帶心跳檢測功能
@Bean
@ConditionalOnMissingBean({ZookeeperRegistration.class})
public ServiceInstanceRegistration serviceInstanceRegistration(ApplicationContext context, ZookeeperDiscoveryProperties properties) {
String appName = context.getEnvironment().getProperty("spring.application.name", "application");
String host = properties.getInstanceHost();
if (!StringUtils.hasText(host)) {
throw new IllegalStateException("instanceHost must not be empty");
} else {
properties.getMetadata().put("instance_status", properties.getInitialStatus());
ZookeeperInstance zookeeperInstance = new ZookeeperInstance(context.getId(), appName, properties.getMetadata());
//這裡,就是建構我們注冊的服務執行個體資訊
RegistrationBuilder builder = ServiceInstanceRegistration.builder().address(host).name(appName).payload(zookeeperInstance).uriSpec(properties.getUriSpec());
if (properties.getInstanceSslPort() != null) {
builder.sslPort(properties.getInstanceSslPort());
}
if (properties.getInstanceId() != null) {
builder.id(properties.getInstanceId());
}
return builder.build();
}
}
}
...
public class ServiceDiscoveryImpl<T> implements ServiceDiscovery<T> {
...
//這個方法,就是向zookeeper裡面寫入服務注冊的資訊
@VisibleForTesting
protected void internalRegisterService(ServiceInstance<T> service) throws Exception {
byte[] bytes = this.serializer.serialize(service);
String path = this.pathForInstance(service.getName(), service.getId());
int MAX_TRIES = true;
boolean isDone = false;
//這裡,我們之前看到的服務builder的那個類,預設CreateMode就是EPHEMERAL節點
for(int i = 0; !isDone && i < 2; ++i) {
try {
CreateMode mode;
switch(service.getServiceType()) {
case DYNAMIC:
mode = CreateMode.EPHEMERAL;
break;
case DYNAMIC_SEQUENTIAL:
mode = CreateMode.EPHEMERAL_SEQUENTIAL;
break;
default:
mode = CreateMode.PERSISTENT;
}
((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(mode)).forPath(path, bytes);
isDone = true;
} catch (NodeExistsException var8) {
this.client.delete().forPath(path);
}
}
}
public void unregisterService(ServiceInstance<T> service) throws Exception {
ServiceDiscoveryImpl.Entry<T> entry = (ServiceDiscoveryImpl.Entry)this.services.remove(service.getId());
this.internalUnregisterService(entry);
}
public ServiceProviderBuilder<T> serviceProviderBuilder() {
return (new ServiceProviderBuilderImpl(this)).providerStrategy(new RoundRobinStrategy()).threadFactory(ThreadUtils.newThreadFactory("ServiceProvider"));
}
...
}
-
服務發現
...
//這裡,實作了spring提供的DiscoveryClient,然後内部調用ServiceDiscovery,就是上面那個服務注冊那個類
public class ZookeeperDiscoveryClient implements DiscoveryClient {
...
private final ServiceDiscovery<ZookeeperInstance> serviceDiscovery;
}
使用zookeeper做配置中心
将application.yml重命名為bootstrap.yml
因為application.yml是spring cloud應用初始化之後加載的配置檔案,而bootstrap.yml是spring cloud應用初始化之前加載的配置,由于應用需要加載配置,是以spring-cloud-strater-zookeeper-config在加載的時候,是加載的bootstrap.yml的檔案内容,保證spring cloud應用在初始化之前就已經拿到配置
取消pom裡面關于zookeeper-config的注釋
在bootstrap.yml中添加config的配置資訊
spring:
profiles:
active: dev #指定dev環境
application:
name: product
cloud:
zookeeper:
config:
root: /discovery/config
profileSeparator: "," #這是與環境連接配接的分隔符,如現在配置的是dev環境,那麼加載的配置檔案名稱就是/discovery/config/product,dev /discovery/config/product /discovery/application,dev /discovery/application 優先級是環境,其次是spring.application.name然後再是 defaultContext,defaultContext是用來做預設配置的,用于比如全局的通用資訊等等
defaultContext: application #這是預設加載的命名空間
connect-string: 192.168.101.180:2181 #這裡多個位址,用,隔開即可
然後将注冊的配置放入zookeeper中進行維護
1.将注冊資訊放在application,dev環境下面,這種資訊應該由目前項目目前環境的所有服務共享
2.将特定資訊放在目前服務的特定環境下面
修改product子產品中controller代碼,引用配置中心的參數
package com.lhstack.cloud.product.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lhstack
*/
@RestController
@RequestMapping
@RefreshScope //開啟屬性更新功能,讓這個bean裡面的屬性會根據配置中心的修改而同步
public class ProductController {
@Value("${msg:hello}")
private String msg;
@GetMapping("product")
public String product(){
return msg;
}
}
啟動product子產品,通過浏覽器通路暴露的接口
也列印出來了配置中心的配置資訊
測試配置更新
修改zookeeper裡面product,dev環境下面的msg的值,然後在通路接口
這裡修改成2
檢視控制台的日志,修改的key也列印出來了
通過浏覽器通路
修改consumer子產品的配置檔案,也使用zookeeper來維護配置
對應的pom檔案也要取消注釋
application.yml也要重命名為bootstrap.yml
spring:
profiles:
active: dev
application:
name: consumer
cloud:
zookeeper:
config:
root: /discovery/config
profileSeparator: "," #這是與環境連接配接的分隔符,如現在配置的是dev環境,那麼加載的配置檔案名稱就是/discovery/config/consumer,dev /discovery/config/consumer /discovery/application,dev /discovery/application
defaultContext: application #這是預設加載的命名空間
connect-string: 192.168.101.180:2181 #這裡多個位址,用,隔開即可
server:
port: 8080
zookeeper中配置如下
啟動consumer,調用product服務
寫在後面
zookeeper實作配置中心的原理,大家有興趣可以去檢視ZookeeperConfigAutoConfiguration和ZookeeperConfigBootstrapConfiguration兩個類,ZookeeperConfigBootstrapConfiguration的作用是用于第一次啟動的時候,去加載zookeeper裡面的配置,然後記錄所有加載的keys,交給ZookeeperConfigAutoConfiguration去建立ConfigWatcher監聽對應的keys,通過監聽zk裡面的更新事件,去更新對應的keys,當然,要更新屬性的對象,必須加上RefreshScope注解