微服务,简单来说,就是将很多功能拆分称一个一个的服务集群,同时服务网关进行服务的筛选。注册中心中注册了所有服务的接口,配置中心中集中了各个服务的配置。在此基础上,又进行分布式日志服务,进行日志采集,并使用系统监控,链路追踪技术,找到每个服务的调用链路。其中还涉及到分布式缓存技术。同时,Jenkins加docker等技术,实现了持续集成。
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
【注:服务提供者每隔30秒会向Eureka发送心跳,保持健康状态,若健康状态不正常,则将该服务的信息从服务列表中剔除,保证消费者拉取的都是最新的信息】
pom文件,这里继承了父工程的依赖,父工程中已经引入了springboot,版本需要对应,我这里springboot的版本是2.6.11,对应的springcloud版本是2021.0.3,Eureka的版本由spring版本控制自动匹配。【父工程的pom文件附后】
<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>study_springcloudartifactId>
<groupId>com.studygroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>eureka-serverartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
project>
父工程pom文件
<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.0modelVersion>
<groupId>com.studygroupId>
<artifactId>study_springcloudartifactId>
<version>0.0.1-SNAPSHOTversion>
<modules>
<module>user-servicemodule>
<module>order-servicemodule>
<module>eureka-servermodule>
modules>
<packaging>pompackaging>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.11version>
<relativePath/>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<spring-cloud.version>2021.0.3spring-cloud.version>
<mysql.version>5.1.47mysql.version>
<mybatis.version>2.2.2mybatis.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.yml,我这里定义了Eureka的端口为10086
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
main函数
package com.study.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
待启动完成后,访问http://localhost:10086,即可看到Eureka界面
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】
server:
port: 8552
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice # user服务的名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
package com.study.order.service;
import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;
/**
* @author wangchaoyang
* @since 2023/2/14 15:37
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@GetMapping("{orderId}")
public Order queryOrderById(@PathVariable("orderId") Long orderId) {
Order order = orderMapper.findById(orderId);
//原来:String url = "http://localhost:8551/user/" + order.getUserId();
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
package com.study;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
在Nacos官网,下载安装包。可以在网上搜索下,Springboot版本和Nacos版本的对应关系。
创建nacos_config的MySQL数据库,并执行conf文件夹下的nacos-mysql.sql文件。
添加application.properties文件,并添加如下配置。另外如果想要修改nacos的端口,也可在此文件中修改。
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456
进入bin目录下使用cmd输入 startup.cmd -m standalone 命令,进行单机运行。【注意路径不能有中文】
点击上图中nacos图标旁边的地址,进入浏览器查看页面。登录的默认账号和密码都是nacos。
引入依赖,在父工程中的dependencyManagement中添加nacos管理依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.5.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
在各个服务中,添加nacos依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
在application.yml文件中,写入nacos配置。主要是spring.cloud.nacos.server-addr的值。
server:
port: 8552
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
分别开启生产者和消费者服务,可以在nacos页面看到注册的服务
方法是,修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性。
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice # user服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
在注册中心页面可以查看到集群名称和实例。
现在给userservice配置集群HZ,开启两个实例;给userservice配置集群SH,开启一个实例。给orderservice配置集群HZ,开启一个实例。
userservice配置两个集群,三个实例
orderservice配置一个集群,一个实例
要做到的是,在HZ集群的orderservice远程调用userservice服务的时候,只访问HZ集群内的userservice,即只访问本地集群,因为跨集群访问的速度会很慢,不建议跨集群访问。
方法:在orderservice中的application.yml文件中,添加配置user.ribbon.NFLoadBalancerRuleClassName即可。
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西, 用来做最外层隔离。不通namespace中的服务互不可见。
可以在Nacos的页面中,创建命名空间
此时,会生成一个唯一的命名空间ID
可以看到,dev命名空间下没有服务
在服务中添加配置,主要是spring.cloud.nacos.discovery.namespace参数
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
namespace: 23312037-1af0-4fd3-b146-25cf8de5d839 # 命名空间-dev环境
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
Nacos不仅可以当注册中心,也可以当配置中心。就是将服务中的一些配置,放置到nacos中管理,这里配置往往是平时需要修改的配置。
在Nacos的配置管理中,点击“+”进行添加配置。
在项目启动后,会先读取Nacos中配置文件的内容,再读取本地application.yml文件中的配置,将两者合并后使用。
将Nacos配置文件的地址放bootstrap.yml配置文件中,因为如果放到application.yml中,这个文件在Nacos配置文件后加载,所以读取不到。而bootstrap.yml文件的优先级高于application.yml,所以放在bootstrap.yml中。
1. 在Nacos中添加配置文件,如5.1所示
2. 在微服务中引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
3. 创建bootstrap.yaml文件,并写入配置
主要写服务名称,环境,后缀名,来找到Nacos配置的名称
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名
4. 删除application.yml中的重复配置
5. 通过@Value注解,可以获取到Nacos配置中的值
package com.study.user.web;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 获取Nacos配置中的值
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
6. 开启服务,访问http://localhost:8552/user/now请求,可以获取到Nacos配置中的值.
当Nacos配置文件更新时,Nacos可以支持不重启项目,就能热更新这些更新的配置。
方式一:在@Value注入的变量所在类上添加注解@RefreshScope
package com.study.user.web;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
方式二【推荐】:通过配置类进行加载
创建PatternProperties类,通过@ConfigurationProperties注解的profix匹配配置中的前缀,类里面的属性值为profix下面的值。
package com.study.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
获取配置时,通过注入类,调用类的getter方法获取值即可。
package com.study.user.web;
import com.study.config.PatternProperties;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}
在多服务中,如果Nacos的配置中有相同的部分,可以抽取出来,放到[服务名].yaml文件中。
更改nacos/conf下的cluster.conf.example文件名为cluster.conf,并修改cluster.conf配置文件,增加集群的ip和port
注意这里的ip和port,三台集群的端口号要相差2,相差1会出现端口被占用问题,是因为Nacos2以上版本,会增加gRPC端口,会有1000和1001的偏移量
127.0.0.1:8842
127.0.0.1:8844
127.0.0.1:8846
修改每个Nacos的端口,分别为配置的三个端口号【这里以8842为例】
分别启动三个Nacos,在每个Nacos的bin目录下,进入命令提示符敲命令startup.cmd即可。其中一个启动的界面如下
1. 下载nginx的包,修改conf/nginx.conf文件配置,将如下代码添加到http块中
upstream nacos-cluster是让nginx对这个集群做负载均衡,集群中有三台机器;server是监听81端口,location /nacos是当访问localhost:81/nacos的时候,nginx会代理到集群中去。
upstream nacos-cluster {
server 127.0.0.1:8842;
server 127.0.0.1:8844;
server 127.0.0.1:8846;
}
server {
listen 81;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
2. 在nginx目录下,start nginx.exe启动即可
3. 将bootstrap.yaml文件中的nacos地址,配置为负载地址
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:81 # nacos负载地址
config:
file-extension: yaml # 文件后缀名
4. 这里启动两台服务,在浏览器访问http://localhost:81/nacos,打开nacos页面
可以看到,Nacos健康实例为2个,集群搭建成功。
Feign是一种远程调用的方式,前几章节我们远程调用,是用RestTemplate发起http请求,但代码可读性差,url维护不方便,使用Feign来解决这些问题。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
因为ribbon的负载均衡会和Feign的相冲突,所以要排出ribbon依赖,如下
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
exclusion>
exclusions>
dependency>
package com.study;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package com.study.order.clients;
import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
package com.study.order.service;
import com.study.order.clients.UserClient;
import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(@PathVariable("orderId") Long orderId) {
Order order = orderMapper.findById(orderId);
User user = userClient.findById(order.getUserId());
order.setUser(user);
return order;
}
}
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
方式一:配置文件方式
在OrderService的配置文件中,添加Feign自定义配置
feign.client.config.default.loggerLevel配置是全局配置,如果default改成某个服务的名称,则是针对某个微服务的配置
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
feign:
client:
config:
default:
loggerLevel: FULL
方式二:Java代码方式
创建Feign配置类
package com.study.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel() {
return Logger.Level.BASIC;
}
}
如果这个配置仅想某个服务有效,就在某个服务上添加@FeignClient 的configuration参数
package com.study.order.clients;
import com.study.order.config.DefaultFeignConfiguration;
import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
如果想让配置全局有效,那么可以在启动类上添加@EnableFeignClients的configuration参数
package com.study;
import com.study.order.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
引入依赖
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
在application.yaml文件中添加配置
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
feign:
client:
config:
default:
loggerLevel: BASIC
httpclient:
enable: true # 支持httpClint开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
将FeignClient抽取为独立模块,并且把有关的pojo、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
新建一个模块,命名为feign-api,然后引入feign的starter依赖
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
在order-service中引入feign-api依赖
当前定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,可以在启动类上指定FeignClient的字节码@EnableFeignClients(clients = UserClient.class)
package com.study.order;
import com.study.feign.clients.UserClient;
import com.study.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
创建gateway的模块,并修改pom.xml文件
<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.0modelVersion>
<groupId>com.study.gatewaygroupId>
<artifactId>gatewayartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gatewayname>
<description>gatewaydescription>
<parent>
<artifactId>study_springcloudartifactId>
<groupId>com.studygroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
创建application.yaml文件,修改里面的网关配置
server:
port: 10010
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 120.53.238.31:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
启动网关服务
package com.study.gateway.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
spring提供了31种不同的路由过滤器工厂(详细可见spring官网)
在配置文件中某个路由下加入filter配置,指定这个路由的过滤规则;要统一指定所有路由的过滤器,则可以在routes下增加default-filter配置
server:
port: 10010
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 120.53.238.31:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
filters:
- AddRequestHeader=Truth, Hello World!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
# default-filter:
# - AddRequestHeader=Truth, All!
Docker是一个CS架构,由两部分组成
docker build指令,会被docker daemon守护进程接收和处理,由它将提供的数据,构建成一个镜像;
docker pull指令,会被docker daemon守护进程接收和处理,由它进行去Registry服务端拉取指定的镜像;
docker run指令,会被docker daemon守护进程接收和处理,告诉server要去创建容器,这时守护进程就会帮助创建容器。
DockerHub是一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。
Docker CE 分为 stable
test
和 nightly
三个更新频道。
先卸载原有的Docker
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce
安装Docker,使用yum安装
更新本地镜像源
# 设置docker镜像源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast
安装Docker
yum install -y docker-ce
在启动Docker前,关闭防火墙
systemctl start docker
查看Docker版本
docker -v
配置镜像加速,针对docker客户端版本大于1.10.0的用户
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://n0dwemtq.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
镜像名称一般分为两部分:[repository]:[tag],在没有指定tag时,默认是latest,代表最新版本镜像
打开DockerHub网站,在搜索栏搜索想要的镜像并选择其中一个
上面会有指令:docker pull nginx
,在linux下执行这个命令
使用docker save -o AA.tar nginx:latest
命令,进行对nginx:latest镜像进行打包,其中AA.tar是定义打出包的包名,nginx:latest是将哪个镜像进行打包
使用docker rmi nginx:latest
进行删除nginx:latest这个镜像,也可以通过docker rmi 镜像id
进行删除
使用docker load -i xxxx
这个命令,来将xxxx镜像导入
命令:docker run --name containerName -p 80:80 -d nginx
命令解读:
命令:docker ps
查看运行中的容器
命令:docker ps -a
查看所有容器
命令:docker logs -f 容器名
命令解读: -f 参数意思是持续跟踪日志
命令:docker start 容器名
启动一个容器
命令:docker stop 容器名
停止一个容器的运行
命令:docker rm 容器名
删除一个容器,加上-f参数是强制删除运行中的容器
命令:docker exec -it 容器名 bash
命令解读:
Dockerfile就是一个文本文件,其中包含一个个指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层layer
同步调用就是按顺序一个一个调用服务,等待上一个服务调用完成后,才开始调用下一个服务,时效性强,可以立即得到结果。
同步调用的问题:
异步调用会设置一个Broker,由它来管理服务。当服务调用者请求Broker时候,Broker会向它管理的服务发布消息,然后这些收到消息的服务就会去处理相关业务。前提是Broker管理的服务要先订阅Broker,只有订阅了Broker的服务才能收到Broker发布的消息。
异步通信优点:
异步通信缺点:
MQ即MessageQueue,消息队列,也就是Broker
利用Docker在线拉取镜像
docker pull rabbitmq:3-management
安装启动MQ
docker run -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
其中,15672端口是管理端界面的访问端口,5672是后续通讯端口。启动MQ后,可以使用ip:15672来访问管理端界面,账号就是上面设置的root,密码就是123
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
Advanced Message Queuing Protocol,是用于在应用程序或之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求。
SpringAMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中Spring-amqp是基础抽象,Spring-rabbit是底层的默认实现。
在父工程中引入spring-amqp依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
配置SpringAMQP,在application.yml中配置相关配置
spring:
rabbitmq:
host: 8.130.111.12
port: 5672
username: root
password: xxxxxx
virtual-host: /
在publisher服务中利用RabbitTemplate发送消息到simple.queue队列
package com.study.publisher.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String queueName = "simple.queue";
String message = "hello, spring amqp";
rabbitTemplate.convertAndSend(queueName, message);
}
}
在管理端查看发布的消息
配置SpringAMQP,在application.yml中配置相关配置
spring:
rabbitmq:
host: 8.130.111.12
port: 5672
username: root
password: xxxxxx
virtual-host: /
在consumer中编写消费逻辑,使用@RabbitListener注解指定队列名称,就能监听simple.queue,接收消息了
package com.study.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
System.out.println("消费者接收到simple.queue消息:" + msg);
}
}
启动consumer后,发现消息已经被消费了
Work Queue,工作队列,可以提高消息处理速度,避免队列消息堆积
在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
package com.study.publisher.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2WorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello, spring__";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
}
在consumer服务中定义两个消息监听者,都监听simple.queue队列,消费者1每秒处理50条消息,消费者2每秒处理10条消息
package com.study.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到simple.queue消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.out.println("消费者2........接收到simple.queue消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(200);
}
}
我们发现,消费者1会处理偶数条的消息,而消费者2会处理奇数条的消息,这是消息预取导致的,消费者2处理的慢,但也会去取偶数条的所有消息,导致消费者1处理完所有奇数消息后就不处理了,造成了资源浪费
消费预取限制。修改application.yml文件,设置prefetch值,设置消息预取为1,意思是每次只取一条消息,消费完了后再取。
spring:
rabbitmq:
host: 8.130.111.12
port: 5672
username: root
password: xxxxxx
virtual-host: /
listener:
simple:
prefetch: 1 # 消息预取
发布订阅模型与之前的区别在于,允许将同一消息发送给多个消费者,实现方式是加入了exchange交换机
exchange类型包括:Fanout广播、Direct路由、Topic话题
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
在consumer服务中,利用代码声明队列、交换机,并将两者绑定
package com.study.consumer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
// 声明交换机 itcast.fanout
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcast.fanout");
}
// 声明队列1 fanout.queue1
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
// 声明队列2 fanout.queue1
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
// 绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
// 绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder
.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
package com.study.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者接收到fanout.queue1消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者接收到fanout.queue2消息:【" + msg + "】");
}
}
在publisher中编写测试方法,向itcast.fanout发送消息
package com.study.publisher.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendFanoutExchange() {
// 交换机名称
String exchangeName = "itcast.fanout";
// 消息
String message = "hello, everyone!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
}
测试,发现两个队列均收到了消息
消费者consumer编写两个消费者方法,分别监听direct.queue1和direct.queue2,并利用@RabbitListener声明Exchange、Queue、RoutingKey
package com.study.consumer.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenerDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenerDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2消息:【" + msg + "】");
}
}
在publisher服务的测试类中添加测试方法
package com.study.publisher.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "hello, red!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
}
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以.
分割
Queue与Exchange指定BindingKey时可以使用通配符
利用@RabbitListener声明Exchange、Queue、RoutingKey,在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
package com.study.consumer.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenerTopicQueue1(String msg) {
System.out.println("消费者接收到topic.queue1消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenerTopicQueue2(String msg) {
System.out.println("消费者接收到topic.queue2消息:【" + msg + "】");
}
}
在publisher中编写测试方法,向itcast.topic发送消息
package com.study.publisher.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "itcast.topic";
// 消息
String message = "这是一条新闻!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
}
启动后,可以看到满足条件的queue1和queue2都收到了消息
声明一个队列
@Bean
public Queue objectQueue() {
return new Queue("object.queue");
}
消息发送方发送Map类型的参数
@Test
public void testSendObjectQueue() {
Map<String, Object> msg = new HashMap<>();
msg.put("name", "张三");
msg.put("age", 21);
rabbitTemplate.convertAndSend("object.queue", msg);
}
会发现消息无法识别成Map集合类型
修改序列化为json形式,首先引入依赖
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
序列化为json
package com.study.publisher;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class, args);
}
// 序列化
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
再次发送消息,查看已经序列化为json
消息接收
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg) {
System.out.println("接收到object.queue消息:" + msg);
}