微服务学习-SpringCloud -Nacos (服务注册源码学习)
Posted 空白Q
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务学习-SpringCloud -Nacos (服务注册源码学习)相关的知识,希望对你有一定的参考价值。
文章目录
源码版本及下载
此次源码版本为1.4.1,2.x版本在服务请求时使用了grpc的方式,所以先以1.4.1版本学习,后续再看2.x版本。
源码下载地址:: link
打开页面后,下拉到最下面,下载nacos-1.4.1.zip,解压导入idea即可。
服务注册核心流程图(看不清请双击打开大图)
此图主要是对核心注册流程进行了大概梳理,可以在后续详细看源码时配合互补。
源码详解
客户端注册源码
在微服务使用nacos需要在pom文件中引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
- 开始分析客户端源码
SpringCloud这一套组件都是基于SpringBoot开发的,那么SpringBoot引入其它组件的方式是什么呢?
自动装配
所以第一步,我们先找它的在自动装配类。
打开文件:
由类名推断服务注册的自动装配类为:
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration
我们打开此类:
对应我画的流程图,我们重点关注第一个和第三个bean。
先看NacosAutoServiceRegistration。
当Springboot启动时,会发布事件。我们来看NacosAutoServiceRegistration,继承了AbstractAutoServiceRegistration,
在看AbstractAutoServiceRegistration类,我们发现它实现了ApplicationListener。
当我们实现了ApplicationListener时,我们就需要实现它的onApplicationEvent方法。
public void onApplicationEvent(WebServerInitializedEvent event)
this.bind(event);
/** @deprecated */
@Deprecated
public void bind(WebServerInitializedEvent event)
ApplicationContext context = event.getApplicationContext();
if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace()))
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
onApplicationEvent监听了WebServerInitializedEvent事件。调用this.start()方法。
进入start方法,忽略分支内容,直接看主线,也就是this.register().
走到这个方法,可以在这边打断点,debug看代码。
往下走,进入namingService.registerInstance(serviceId, group, instance)方法。
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException
// 心跳检测
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral())
BeatInfo beatInfo = this.beatReactor.buildBeatInfo(groupedServiceName, instance);
this.beatReactor.addBeatInfo(groupedServiceName, beatInfo);
// 进入注册核心方法
this.serverProxy.registerService(groupedServiceName, groupName, instance);
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException
LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] registering service with instance: ", new Object[]this.namespaceId, serviceName, instance);
// 配置参数写入map中
Map<String, String> params = new HashMap(16);
params.put("namespaceId", this.namespaceId);
params.put("serviceName", serviceName);
params.put("groupName", groupName);
params.put("clusterName", instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
// 通过http请求发生注册信息
this.reqApi(UtilAndComs.nacosUrlInstance, params, "POST");
UtilAndComs.nacosUrlInstance=/nacos/v1/ns/instance
这个地址就是服务注册的地址,我们来看官网API:
官网API
具体参数可以看官网。
再往下,简单看看主要代码:
HttpRestResult<String> restResult = this.nacosRestTemplate.exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);
this.execute(url, httpMethod, requestHttpEntity, responseType);
response = this.requestClient().execute(uri, httpMethod, requestEntity);
底层是调用了JdkHttpClientRequest方法发送请求。
客户端主要注册源码就结束了。
服务端注册源码
服务端就是第一步下载并导入的源码。
找到服务注册方法类:
找到对应的注册的方法
/**
* Register new instance.
*
* @param request http request
* @return 'ok' if success
* @throws Exception any error during register
*/
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = parseInstance(request);
// 服务注册核心方法
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
看注释注册一个新的实例。
往下走。
// 以AP模式创建注册实例
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException
// 创建空的服务实例
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
Service service = getService(namespaceId, serviceName);
if (service == null)
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
// 添加实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
注意,此处是以AP模式创建的实例,是临时实例。
nacos注册实例分为AP和CP,我们看到服务注册时传入的参数有一个参数控制我们注册的实例是AP还是CP,就是这个参数:
此参数默认为true,也就是默认创建临时实例。后面会重点讲一下AP和CP架构。
我们先加入createEmptyService方法。
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException
createServiceIfAbsent(namespaceId, serviceName, local, null);
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException
Service service = getService(namespaceId, serviceName);
if (service == null)
Loggers.SRV_LOG.info("creating empty service :", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null)
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
service.validate();
putServiceAndInit(service);
if (!local)
addOrReplaceService(service);
- 先看下这个方法
Service service = getService(namespaceId, serviceName);
主要是用来从注册表中通过namespace获取对应的服务,如果没有获取到,返回null;如果有,返回对应的service。
看一下对应的getService方法:
public Service getService(String namespaceId, String serviceName)
if (serviceMap.get(namespaceId) == null)
return null;
return chooseServiceMap(namespaceId).get(serviceName);
public boolean containService(String namespaceId, String serviceName)
return getService(namespaceId, serviceName) != null;
然后我们来看一下注册表的结构,也就是serviceMap。
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
其实主要是这样一个结构:
Map<namespace, Map<group::serviceName, Service>>
在我们平时使用中,大概是这样一个结构:
- 再看下下面的这个主要方法,点击进入:
putServiceAndInit(service);
private void putServiceAndInit(Service service) throws NacosException
// 将服务信息写入serviceMap
putService(service);
// 初始化服务
service.init();
// 监听数据变化
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] ", service.toJson());
将service数据写入serviceMap后,创建临时实例的方法就结束了,现在我们代码回到创建临时实例createEmptyService处继续向下走,然后再判断一次注册表中是否有service,此次没有的话就会抛出异常。
再往下走,进入核心方法:
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName);
synchronized (service)
// 比较并获取新的实例列表,这个列表中包含了此次新增的实例
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
consistencyService.put(key, instances);
先看一下此方法:
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
public static String buildInstanceListKey(String namespaceId, String serviceName, boolean ephemeral)
return ephemeral ? buildEphemeralInstanceListKey(namespaceId, serviceName)
: buildPersistentInstanceListKey(namespaceId, serviceName);
ephemeral前面我们了解过,是否是临时实例,默认传true,所以一般返回 buildEphemeralInstanceListKey(namespaceId, serviceName)这个结果,所以这里主要是区分nacos是AP还是CP架构的地方。
再往下走:
consistencyService.put(key, instances);
public void put(String key, Record value) throws NacosException
onPut(key, value);
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
public void onPut(String key, Record value)
// 健壮性校验
...
// 核心方法,将任务添加到阻塞队列
notifier.addTask(key, DataOperation.CHANGE);
public void addTask(String datumKey, DataOperation action)
if (services.containsKey(datumKey) && action == DataOperation.CHANGE)
return;
if (action == DataOperation.CHANGE)
services.put(datumKey, StringUtils.EMPTY);
// 放入阻塞队列中,如果队列中无数据时会阻塞
tasks.offer(Pair.with(datumKey, action));
放入阻塞队列后,注册基本结束。
nacos会启用一个线程,一直循环将阻塞队列中的客户端的注册信息拿出来实现真正的注册。
下面看下源码:
实现了Runnable接口,所以我们主要看一下run方法:
可以看到一个线程一直循环取数据,当队列为空时,阻塞线程。
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
// 进行实际的注册任务
handle(pair);
服务注册基本流程已经结束。
SpringCloud学习--- 什么是微服务什么是SpringCloud?
一、微服务简介
我们直接举个例子来理解微服务
如图,一个购物服务会拆分为多个子功能,子功能分别看作一个微服务,然后他们有自己的端口,自己的数据库,这样的话,每个功能就可以给公司里不同小组进行开发,他们之间开发都是独立的。还有数据库,相比以前,都连接同一个数据库,如果数据库崩溃,那么整个系统就崩溃了,而现在每个微服务可以有自己的数据库,也可以多个连同一个,微服务满足松耦合的涉及理念,易于被开发人员理解,修改和维护,非常有学习的必要。
二、SpringCloud
SpringCloud是基于SpringBoot提供了一套微服务解决方案,包括服务注册与发现,配置中心,全链路监控,服务网关,负载均衡等。
SpringBoot和SpringCloud之间又有什么关系呢?
1、SpringBoot专注于快速方便的开发单个个体微服务,也就是开发一个个功能,最后打成jar包放到服务器上运行
2、SpringCloud是关注全局的微服务协调治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个服务之间提供:配置管理,服务发现,断路器,路由,微代理,事件总线,全局锁,分布式会话等集成服务。
3、SpringBoot可以独立于SpringCloud使用,但SpringCloud离不开SpringBoot,属于依赖关系
4、SpringBoot专注于快速、方便的开发单个个体微服务,SpringCloud关注全局的服务治理框架。
两个学习SpringCloud的网站:
1、https://spring.io/projects/spring-cloud-netflix
2、https://www.springcloud.cc/spring-cloud-dalston.html
SpringCloud版本不是用数字的,它是按照名字来的,ABCDEFG,现在一般都用Hoxton SR5以上版本了,也比较稳定。我是跟着狂神视频学的,还跟着用下Greenwich,如果用Hoxton注意和SpringBoot的兼容即可,要版本对应上,比如Hoxton.SR8对应SpringBoot2.3.x
三、环境搭建:服务提供者
学SpringCloud之前,可以先看看这个Dubbo+Zookeeper+SpringBoot服务注册
新建一个Maven项目,取名springcloud,删除src目录,剩下如下图,然后先配置pom.xml中相关依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yx</groupId>
<artifactId>springcloud</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打包方式pom-->
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.13</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<!--SpringCloud依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.3</version>
</dependency>
<!--SpringBoot启动器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--日志测试-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
项目中新建一个module
新建的模块pom文件,会有父类,父类就是这个springcloud项目,这个springcloud子类可以用父类中的依赖
springcloud-api只要引入父类中的依赖,就会导入,而父类中dependencyManagement下的依赖,它并没有导入,只是管理,即依赖管理
接下来创建数据库,因为是微服务,会有很多数据库,所以名字就DB01,DB02, … 一直往后,先创建DB01,然后在数据库中建表,建表sql如下
create table table_name
(
dept_no bigint auto_increment,
dept_name varchar(50) not null,
db_source varchar(50) not null,
constraint table_name_pk
primary key (dept_no)
);
插入数据,DATABASE()表示获取数据库名
insert into dept(dept_name,db_source) values('研发部',DATABASE());
insert into dept(dept_name,db_source) values('人事部',DATABASE());
insert into dept(dept_name,db_source) values('运维部',DATABASE());
insert into dept(dept_name,db_source) values('财务部',DATABASE());
insert into dept(dept_name,db_source) values('市场部',DATABASE());
然后接下来就要写实体类,注意实体都要实现序列化,不然传输可能报错,这个api模块就管实体,拆分思想
@Data
@NoArgsConstructor
public class Dept implements Serializable {
private Long deptNo;
private String deptName;
private String dbSource;
public Dept(String deptName) {
this.deptName = deptName;
}
}
再创建一个provider模块,最后取名时也写上端口号
这里可以将之前写的模块引入,其pom.xml如下
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud</artifactId>
<groupId>com.yx</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloud_provider_dept_8001</artifactId>
<dependencies>
<dependency>
<groupId>com.yx</groupId>
<artifactId>springcloud_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
</dependencies>
</project>
配置mybatis-config.xml和application.yml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
server:
port: 8001
mybatis:
type-aliases-package: com.yx.pojo
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
spring:
application:
name: springcloud_provider_dept
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
之后就可以写dao层,service层,controller层
import com.yx.dao.DeptDao;
import com.yx.pojo.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public List<Dept> queryAll() {
return deptDao.queryAll();
}
}
import com.yx.pojo.Dept;
import java.util.List;
public interface DeptService {
List<Dept> queryAll();
}
import com.yx.dao.DeptDao;
import com.yx.pojo.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public List<Dept> queryAll() {
return deptDao.queryAll();
}
}
DeptProvider_8001启动类
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DeptProvider_8001 {
public static void main(String[] args) {
SpringApplication.run(DeptProvider_8001.class,args);
}
}
启动后访问 localhost:8001/dept/list
四、消费者Consumer
consumer操作就简单多了
@Configuration
public class ConfigBean {//@Configuration相当于spring中applicationContext.xml
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
@RestController
public class DeptConsumerController {
//消费者不应该有service层
//(url,参数实体(前提参数有),Class<T> responseType)
@Autowired
private RestTemplate restTemplate; //提供多种便捷访问远程http服务的方法,一种简单的restful服务
private static final String REST_URL_PREFIX = "http://localhost:8001";
@RequestMapping("/consumer/dept/list")
public List<Dept> list(){
return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list",List.class);
}
}
@SpringBootApplication
public class DeptConsumer_80 {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer_80.class,args);
}
}
application.yml只需要配置一个端口
server:
port: 80
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=SpringCloud学习--- 什么是微服务什么是SpringCloud?
微服务-SpringCloud学习系列:注册中心Eureka
微服务-SpringCloud学习系列:负载均衡Ribbon