微服务概念
微服务架构是一种设计复杂应用的方式,它将单一应用程序分解为一组小的服务,每个服务都围绕着特定的业务能力构建,并且能够独立部署、扩展和维护。那么这些服务之间又如何通信呢?直接用http吗?其实我们可以使用一些微服务技术来实现服务通信和治理:
以上微服务技术各有千秋,本文主要介绍SpringCloud的快速上手。
官方文档地址:Spring Cloud
SpringCloud是基于SpringBoot开发的,其兼容版本如下:
引入springcloud依赖:
<properties>
<spring-cloud.version>2023.0.2</spring-cloud.version>
</properties>
<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>
如果把应用程序里的每一个服务都拆分的话,会出现一个问题:那就是如果有一个服务依赖于另一个服务的某个功能,比如我有一个订单服务,我需要查询这个订单的用户怎么办,最直接的方法就是使用http请求,在java中可以使用restTemplate来实现:
注册bean:
@SpringBootApplication
public class OrderApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
发送请求:
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Order queryOrderByUserId(Long orderId) {
// 1. 查询订单
Order order = orderMapper.findByOrderId(orderId);
// TODO 2. 查询用户
String url = "http://localhost:8081/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 3. 封装用户信息
order.setUser(user);
// 4. 返回
return order;
}
}
可以看到在代码中url是写死的,在实际的开发环境中一个微服务可能会被部署到多台服务器,有多个地址,如果url写死,那岂不是有一些服务器永远不会被访问,再加上这种写法可扩展性极低,每变一个url地址就变一次,那岂不是特别麻烦,于是就引入了下面的注册中心。
Eureka
Eureka的作用就像是一个路由中转,你只需要在请求服务方发送服务请求时url前缀只需要写上服务名即可,然后请求到Eureka,Eureka就会根据服务名返回真实的服务地址。
建立一个Eureka的注册中心SpringBoot项目:
引入服务依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
配置yml:
server:
port: 10086
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/ #配置eureka的地址
在启动类中添加@EnableEurekaServer注解
注册服务:
在需要注册的服务项目下添加以下依赖和配置以下参数:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/
服务拉取:
Ribbon
负载均衡
负载均衡策略
饥饿加载
Nacos
Nacos是阿里开发的第三方的注册中心,需要自行下载并启动:
下载地址:Nacos 快速开始
服务注册
给需要注册的服务添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置yml:
cloud:
nacos:
server-addr: localhost: 8848
这样服务就注册到了nacos里了,然后使用restTemplate访问服务名即可
服务集群
有时候一个服务可能会有多个集群,比如我杭州有一个集群,上海有一个集群,为了方便管理和后续的负载均衡,可以定义服务的集群属性:
权重
有时候某个服务器可能不如另一个服务器,在分解流量压力的时候,自然希望能者多劳,也就是将更多的流量分给性能好的服务器,所以引入了权重配置,负责负载均衡的任务会根据权重来分配服务
命名空间
在开发时有多个环境,比如生产环境,开发环境,测试环境,这些环境应该各自隔离互不干扰,而命名空间就可以让不同命名空间相互隔离开来互不干扰
临时实例和非临时实例
临时实例和非临时实例的区别在于,临时实例需要自己发送请求给注册中心已确认存活,如果该实例关闭了,注册中心会将其在注册中心的数据删除,而非临时实例则是由注册中心发送请求以确认是否存活,如果实例关闭,注册中心也不会将其删除,而是等待其再次开启。
Nacos和Eureka的对比
Nacos与Eureka的共同点:
- 都支持服务注册和和服务拉取
- 都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别:
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
配置中心
当多个服务被分配到不同的服务器,如果要重新配置这些服务的配置,是不是要把这些服务配置后再重启一遍,那多麻烦,而现在,在nacos中进行了配置就会自动更新到服务中那岂不是爽滋滋
服务读取配置默认是先读取配置中心的配置,那如果不先读本地配置,找不到配置中心咋办,不怕,有bootstrap.yml来相助
Feign
使用Http请问求,固然还是有一些不方便,于是引入了声明式的服务请求Feign:
Feign的使用
1.引入依赖:
<!-- 使用 Feign 步骤一:引入依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.在启动类中添加注解:
// 使用 Feign 步骤二:在启动类添加注解
@EnableFeignClients
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.编写客户端api:
// 使用 Feign 步骤三:编写 Feign 客户端
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
代码中定义了:
- 服务名称:userservice
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
Feign的配置
方式一:配置文件
# 全局生效
feign:
client:
config:
default: # 这里default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
# 局部生效
feign:
client:
config:
userservice: # 这里default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
方式二代码实现:
// 配置Feign日志的方式二:java代码方式,需要先声明一个Bean:
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.BASIC;
}
}
// ① 而后如果是全局配置,则把它放到@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
// ② 如果是局部配置,则把它放到@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)
Feign底层客户端
Feign 底层的客户端实现有以下三种:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
优化 Feign 的性能:
使用连接池代替默认的 URLConnection
日志级别,最好用 basic 或 none
Feign添加HttpClient的支持
引入依赖:
<!-- httpclient 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置yml:
# 配置连接池:
feign:
client:
config:
default: # 默认全局配置
loggerLevel: BASIC # 日志级别,BASIC 就是最基本的请求和响应信息
httpClient:
enabled: true # 开启 Feign 对 HttpClient 的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路由的最大连接数
最佳实践:
Gateway
创建网关
1.创建一个新的模块工程并引入依赖:
<!-- 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos 服务发现依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.在yml配置路由及nacos地址:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
uri: lb://userservice # 路由的目标地址,lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
断言工厂
过滤器
配置类路由过滤器
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # Nacos 地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
uri: lb://userservice # 路由的目标地址,lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service # 另外一个路由
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader=True, Itcast is freaking awesome! # 添加请求头
编程式全局过滤器
使用配置类过滤器很多其它细化的场景我们不能自定义,而使用编程式全局过滤器就可以针对访问的流量做鉴权处理,限流,加密,流量染色,日志记录等。
下面是一个全局过滤器的模板:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoggingFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 添加请求头
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-Request-ID", "unique-id-" + System.currentTimeMillis())
.build();
// 使用修改后的请求继续处理
return chain.filter(exchange.mutate().request(mutatedRequest).build())
.doOnNext(v -> {
// 添加响应头
response.getHeaders().add("X-Response-ID", "unique-id-" + System.currentTimeMillis());
});
}
@Override
public int getOrder() {
// 设置过滤器的优先级
// 数值越小,优先级越高
return 100; // 可以调整这个数字来改变执行顺序
}
}
ps:order类是实现过滤器的顺序的,order值越小越优先,如果多个过滤器的order值一样,会按照类名的字典序,其中全局过滤器先于路由过滤器
解决跨域问题
浏览器为了禁止一个恶意网站访问其它网站的资源而设置了一个同源策略,也就是一个网站只能访问和它同源的资源,同源必须是协议,域名,端口一致,我们在部署前后端项目的时候,前端和后端就算运行在同一服务器也还是属于非同源,因为端口不一致,解决同源问题通常可以使用cors机制,允许服务器明确表明可以接受其它来源的请求,不过这个请求也不是随便接收的,比如下面的配置就是只允许特定的网站请求。
springCloud项目下的解决方案:
spring:
cloud:
gateway:
globalcors: # 全局跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations: # 对应的路由前缀和跨域配置
'[/*]': # 匹配所有路由
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: # 允许在请求中携带的头信息
- "*"
allowCredentials: true # 是否允许携带cookie
maxAge: 3600000 # 此次跨域检测的有效期,当浏览器接收到同样的请求时,由于还在有效期内,缓存数据还在,就不发送options请求了
普通SpringBoot项目解决跨域的方案:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://example.com", "http://another-origin.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.allowCredentials(true)
.maxAge(3600); // 设置预检请求的有效期为 3600 秒(1小时)
}
}
ps:以上部分图片资料取自黑马:SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务_哔哩哔哩_bilibili