微服务架构 基础(一)
待续…
SpringCloud技术栈大致概览
单体架构和分布式架构
微服务初识
微服务拆分原则
- 不同微服务,不要重复开发相同的业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
SpringCloud版本选择
"spring-cloud": {
"Hoxton.SR12": "Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
"2020.0.4": "Spring Boot >=2.4.0.M1 and <2.5.7-SNAPSHOT",
"2020.0.5-SNAPSHOT": "Spring Boot >=2.5.7-SNAPSHOT and <2.6.0-M1",
"2021.0.0-M1": "Spring Boot >=2.6.0-M1 and <2.6.0-M3",
"2021.0.0-M3": "Spring Boot >=2.6.0-M3 and <2.6.0-SNAPSHOT",
"2021.0.0-SNAPSHOT": "Spring Boot >=2.6.0-SNAPSHOT"
}
创建总父工程
选择Maven
编码设置
注解生效
选择Java编译版本为8
为了使得界面更加简洁,建议配置将.idea以及.iml文件进行忽略,但是在此之前我们首先进入Maven父工程下找到workspace.xml配置文件,添加如下代码(作用是可以更好管理子模块,后续会有作用的… ):
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
然后开始文件过滤:
然后重启…
修改父级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>cn.wu</groupId>
<artifactId>Project</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>micro-service-8001</module>
<module>micro-service-8002</module>
<module>main-service-80</module>
</modules>
<!-- 规定为父Maven -->
<packaging>pom</packaging>
<build>
<finalName>Project</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.6.RELEASE</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
最后删除src目录
添加子模块
持久层设计
8001/8002端口数据库结构图
数据表结构图
子模块创建以及结构
micro-service-8001子模块
Maven配置
<?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>Project</artifactId>
<groupId>cn.wu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>micro-service-8001</artifactId>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.wu</groupId>
<artifactId>api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.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>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>cn.wu.Application8001</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml:
server:
port: 8001
spring:
application:
name: micro-service-8001
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.123.188:3333/DB_1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
name: defaultDataSource
username: root
password: root
mybatis-plus:
mapper-locations: classpath:/mappers/*.xml
type-aliases-package: cn.wu.entity
logging:
level:
cn.wu.service: debug
cn.wu.dao: debug
控制层:
package cn.wu.cotroller;
import cn.wu.entity.Book;
import cn.wu.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookController {
private BookService bookService;
@Autowired
@Qualifier("bookServiceBean")
public void setBookService(BookService bookservice) {
this.bookService = bookservice;
}
@PostMapping(value = "/book/add")
public boolean addBook(Book book){
try {
bookService.addBook(book);
return true;
}catch(Exception e){
return false;
}
}
@GetMapping(value = "/book/get/{id}")
public Book getBookById(@PathVariable("id") String id){
try {
return bookService.getBookById(id);
}catch(Exception e){
e.printStackTrace();
return null;
}
}
}
持久层:
package cn.wu.dao;
import cn.wu.entity.Book;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository("bookDaoBean")
public interface BookDao {
public void addBook(Book book);
public Book getBookById(@Param("id") String id);
}
实体类:
package cn.wu.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
private String bookId;
private String bookName;
private float bookPrice;
}
业务层:
package cn.wu.service;
import cn.wu.dao.BookDao;
import cn.wu.entity.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service("bookServiceBean")
public class BookService {
private BookDao bookDao;
@Autowired
@Qualifier("bookDaoBean")
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void addBook(Book book){
bookDao.addBook(book);
}
public Book getBookById(String id){
return bookDao.getBookById(id);
}
}
主启动类:
package cn.wu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("cn.wu.dao")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
映射配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "cn.wu.dao.BookDao">
<resultMap id="bookResultMap" type="cn.wu.entity.Book">
<id property="bookId" column="book_id"></id>
<result property="bookName" column="book_name"></result>
<result property="bookPrice" column="book_price"></result>
</resultMap>
<insert id="addBook" parameterType="Book">
insert into books(book_id,book_name,book_price)
values(#{bookId},#{bookName},#{bookPrice})
</insert>
<select id="getBookById" parameterType="String" resultMap="bookResultMap">
select * from books where book_id = #{id}
</select>
</mapper>
micro-service-8002子模块大致代码同上
main-service-80主模块核心代码
依赖配置:
<?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>Project</artifactId>
<groupId>cn.wu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>main-service-80</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
</dependencies>
</project>
配置类:
package cn.wu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationConfig {
@Bean("restTemplateBean") // 注册RestTemplate对象到Spring容器中
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
控制层:
package cn.wu.controller;
import cn.wu.entity.Book;
import cn.wu.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class MainController {
private static final String URL8001 = "http://localhost:8001";
private static final String URL8002 = "http://localhost:8002";
private RestTemplate restTemplate;
@Autowired
@Qualifier("restTemplateBean")
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/student/id/{id}")
public String findStudentById(@PathVariable("id") String id){
try{
return restTemplate.
getForObject(URL8002+"/student/id/"+id, String.class);
}catch(Exception e){
e.printStackTrace();
return null;
}
}
@GetMapping("/book/id/{id}")
public String findBookById(@PathVariable("id") String id){
try{
return restTemplate.
getForObject(URL8001+"/book/id/"+id, String.class);
}catch(Exception e){
return null;
}
}
@PostMapping("/student/add")
public boolean addStudent(Student student){
try{
return restTemplate.
postForObject(URL8002+"/student/add",student,Boolean.class);
}catch(Exception e){
return false;
}
}
@PostMapping("/book/add")
public boolean addBook(Book book){
try{
return restTemplate.
postForObject(URL8001+"/book/add",book,Boolean.class);
}catch(Exception e){
return false;
}
}
}
如此就完成了最基本的模块通信,当然最后一步不要忘记测试每个API了…
完成以上的基本过程后,我们可以找到一个问题,那就是发现entity包下是代码冗余的,因此,接下来我们进行重构过程,一步一步优化,一步一步完善…
添加子模块
删除所有子模块的实体类,并在pom.xml引入实体类坐标
<!-- 引入api-commons子模块 -->
<dependency>
<groupId>cn.wu</groupId>
<artifactId>api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
最后不要忘记了 Reimport All Maven Project
服务注册与发现
Eureka
Eureka架构
Eureka主要角色
- EurekaServer:服务器,注册中心
- 记录服务信息
- 心跳记录
- EurekaClient:客户端
- Provider:服务提供者
- 注册自己的信息到EurekaServe
- 每间隔30秒向EurekaServer发送心跳
- Consumer:服务消费者
- 根据服务名称从EurekaServer拉取服务列表
- 基于服务列表做负载均衡,选中一个微服务后发起远程调用
Eureka子模块
Eureka服务端模块
通过 Spring Initializr 创建子模块后,查看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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server-7001</artifactId>
<parent>
<artifactId>Project</artifactId>
<groupId>cn.wu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.wu</groupId>
<artifactId>api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<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-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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.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>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>cn.wu.MainEurekaApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
修改application.yml:
server:
port: 7001
eureka:
instance:
hostname: localhost # Eureka服务端实例名
client:
register-with-eureka: false # 设置不向注册中心注册该模块
fetch-registry: false # 设置该模块为注册中心,职责是维护服务实例,不需要像客户端检索服务
service-url:
# 设置与服务端交互和注册的地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
最后在主启动类中添加注解@EnableEurekaServer,完成
Eureka客户端配置
除Eureka服务端外对其余所有的子模块进行如下操作
pom.xml配置添加:
<!-- 服务注册 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
applicaint.yml添加:
eureka:
client:
register-with-eureka: true # 注册
fetch-registry: true # 是否从Eureka服务端中抓取已有的注册信息
service-url:
defaultZone: http://localhost:7001/eureka # 服务端网址
最后在主启动类中添加注解@EnableEurekaClient,完成
可以发现已经多了三个实例
构建Eureka集群
再次创建一个Eureka并且端口为7002的服务端
如下为当前文件的整体结构:
修改application.yml文件
server:
port: 7001
eureka:
instance:
hostname: eureka7001 # Eureka服务端实例名
client:
register-with-eureka: false # 设置不向注册中心注册该模块
fetch-registry: false # 设置该模块为注册中心,职责是维护服务实例,不需要像客户端检索服务
service-url:
# 设置与服务端交互和注册的地址
defaultZone: http://eureka7002.cn:7002/eureka/ # 集群填写其余的服务端口地址
模拟不同主机,修改本地host表,添加如下内容
windows系统host表位置为:C:\Windows\System32\drivers\etc,记得修改完毕保存
然后修改所的非eureka服务端的子模块的application.yml文件
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
# 集群版本的服务端口地址,直接填写所有的服务端地址即可
defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka
最后测试即可,首先应该启动Eureka服务端,其次才能启动Eureka客户端,如下:
负载均衡测试,这里将8001端口业务同9001业务重合(如提高系统的稳定性…),构建子模块
这里不要忘记将端口号改为9001,并且将spring.application.name设置为与8001模块的一致,同时为了便于测试,需要稍微修改一下8001和9001模块的controller层
@Value("${server.port}")
private String port;
...
return bookService.getBookById(id).toString()+
">>>>> 当前应用端口为:"+port;
修改80端口应用已经固定的单机访问URL,改为集群URL
// 这里为集群spring.application.name名称
private static final String URL1 = "http://micro-service-1";
修改80端口模块的配置类型信息,添加注解@LoadBalanced:
@Configuration
public class ApplicationConfig {
@Bean("restTemplateBean") // 注册RestTemplate对象到Spring容器中
@LoadBalanced // 用于负载均衡注解,默认为轮询策略
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
最终所有模块依次启动,测试情况如下:
多次测试同一个URL,可以发现是不同的访问端口:
以上就起到了负载均衡的效果,不需要关心地址和端口号…
补充知识
- 健康监控
<dependency>
<!-- 添加监控系统健康情况的工具 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
eureka:
instance:
# 监控健康情况实例名
instance-id: MicroService9001
- 服务发现Discovery
@MapperScan("cn.wu.dao")
@SpringBootApplication
@EnableDiscoveryClient
public class Application9001 {
public static void main(String[] args) {
SpringApplication.run(Application9001.class,args);
}
}
...
private DiscoveryClient discoveryClient;
@Autowired
public void setDiscoveryClient(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@GetMapping("/discovery")
public String discovery() {
return discoveryClient.getServices().toString() + "<br>"
+discoveryClient.getInstances("micro-service-1").toString();
}
...
- 自我保护机制
某时刻某一个服务不可用了,Eureka不会立刻清理,依旧会对该为服务的信息进行保护
Eureka首页常常看到以下消息,就是说明进入了自我保护机制:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
关闭自我保护机制配置:
# Eurek服务端
eureka:
server:
enable-self-preservation: false # 关闭该服务端自我保护机制
eviction-interval-timer-in-ms: 4000 # 每4s清理无效节点一次
# Eurek客户端
eureka:
instance:
lease-renewal-interval-in-seconds: 3 # Eureka客户端向服务端发送心跳时间的时间间隔,单位为秒
lease-expiration-duration-in-seconds: 2 # Eureka服务端收到最后一次心跳后等待时间上限,单位为秒