1. 远程调用
通过RestTemplate实现基于http的远程调用:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate () {
return new RestTemplate();
}
}
在controller中即可通过http地址调用其它服务的方法:
@Autowired
private RestTemplate restTemplate;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
Order order = orderService.queryOrderById(orderId);
// 发起http请求
User user = restTemplate.getForObject("http://userservice/user/" + order.getUserId(), User.class);
order.setUser(user);
return order;
}
2. Eureka
Eureka是基于REST的服务发现框架,基本操作:
注 REST: Representational State Transfer,表现状态转移,是一种前后端分离的接口规范
- 每一个URI代表一种资源;
- 同一种资源有多种表现形式(xml/json);
- 所有的操作都是无状态的。
- 规范统一接口。
- 返回一致的数据格式。
- 可缓存(客户端可以缓存响应的内容)。
代码如下:
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
@Bean
public IRule randomRule() {
return new RandomRule();
}
}
3. Ribbon
为了:
如何在配置Eureka Client注册中心时不去硬编码Eureka Server的地址?
在微服务不同模块间进行通信时,如何不去硬编码服务提供者的地址?
当部署多个相同微服务时,如何实现请求时的负载均衡?
因此,Ribbon是一个客户端负载均衡器,基本原理如图:
可以通过实现IRule接口实现全局的负载均衡策略:
7种负载均衡策略:
- RoundRobinRule: 轮询
- ZoneAvoidanceRule: 区域敏感策略(默认),根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,即为轮询。
- WeightedResponseTimeRule: 权重策略,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大
- RandomRule: 随机
- BestAvailableRule: 最小并发数策略,遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。
- RetryRule: 重试策略,轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null
- AvailabilityFilteringRule: 可用敏感性策略,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例
@Bean
public IRule randomRule() {
return new RandomRule();
}
也可以通过配置文件指定某微服务的负载均衡策略:
userservice: # 服务名
ribbon:
NFloadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- 懒加载:Ribbon会在首次访问时去创建负载均衡器,后续可以直接访问。这会造成一定的速度差异和某些bug,可以启用饥饿加载,即服务启动时拉取服务列表解决:
ribbon:
eager-load:
enabled: true
clients: userservice # 指定对userservice启用饥饿加载
- userservice # 指定多个服务时换行加-
- orderservice
4. Nacos
我人傻了…公司的jdk安装包只有jre,合着我用了这么久的open jdk o(╥﹏╥)o 赶紧重装一个
…
解决了,继续继续
nacos是阿里巴巴的注册中心,单机模式记得改startup.cmd
中的配置为standalone
,使用nacos就要把eureka的配置都注掉,两者都遵循spring cloud Commons。
使用nacos:
// 单机模式启动nacos
E:\nacos\bin>startup.cmd -m standalone
// 父项目引入依赖
<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>
// 子项目引入服务发现依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
// 子项目yml配置地址
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: BJ # 集群名称
即可在8848端口进入nacos管理界面
localhost:8848
4.1 服务多级存储模型
优先访问本地集群,跨地区调用延迟较高,通过cluster-name配置集群名称
通过配置
userservice:
ribbon:
NFloadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
优先选择本地集群,在本地内随机,本地不可用则跨集群访问,并输出警告信息。
4.2 权重配置
在nacos页面可以管理服务的权重,权重配置在[0,1],越小越低,且成比例。e.g. A-0.1, B-1, 则B的访问概率是A的十倍。
0权重的作用:0不会被访问,比如版本升级时替换服务器设0升级,升级后用小权重测试,没问题再把旧服务器设0升级
4.3 环境隔离
使用命名空间隔离开发、测试、上线服务,不同空间服务不可见
// 子项目yml配置地址
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: BJ # 集群名称
namespace: 001 # 命名空间,不自己写则自动生成uuid
ephemeral: false # 非临时实例
4.4 nacos和eureka的异同
服务拉取: eureka没30秒从注册中心拉取一次,nacos不仅拉取,当服务变更时注册中心会主动推送;
服务注册: eureka心跳检测,nacos临时实例心跳检测,异常服务自动踢出,非临时实例不用检测,注册中心主动询问,不会自动踢出。
集群: 都默认AP模式,nacos集群中有临时实例时切换到CP模式。
4.5 nacos配置管理
以服务名称-环境.yaml格式命名配置文件(userservice-dev.yaml
),写所有有热更新需求的配置。
由于nacos地址放在application.yml
中会导致顺序错误,所以需要用优先级最高的bootstrap.yml
存放nacos相关配置。
使用方法:
// 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
// bootstrap配置
spring:
application:
name: userservice #服务名
profiles:
active: dev # 开发环境
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: BJ # 集群名称
config:
file-extension: yaml # 文件后缀名
热更新方法:
- 在
@Value("${pattern.dateformat}")
属性注入的方法所在类上加注解@RefreshScope
- 使用配置类并加上
@ConfigurationProperties(prefix = "pattern")
多环境配置:
多配置优先级:
4.6 nacos集群
集群配置存入数据库,所以:
- 搭建mysql集群
- 配置nacos节点信息、数据库信息
- 启动nacos节点
- 配置Nginx反向代理即可
5. Feign
因为RestTemplate
需要拼接URL,在实际项目中不现实,所以可以使用声明式http客户端Feign优雅的发送http:
// 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
// 启用feign
@EnableFeignClients
// feign配置类
@FeignClient("userservice")
public interface UserClient {
/**
* 根据用户id查找用户表
* @return User
*/
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
// 注入并使用
@Autowired
private UserClient userClient;
配置类或yml中feign可以配置的内容:
性能优化: feign底层的客户端通过URLConnection
实现,不支持连接池。因此可以用支持连接池的Apache HttpClient或者OKHttp代替,同时把日志级别调成NONE或BASIC节省性能。
// 引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
// 配置
feign:
# client:
# config:
# default: # 所有,或者针对某个服务如userservice
# loggerLevel: FULL
httpclient:
enabled: true # 开启feign对httpclient的支持
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 每个路径的最大连接数
Feign的实现方案:
controller
和feignclient
继承同一接口,但会造成紧耦合feignclient
、POJO和feign的默认配置都定义到一个项目中,会造成引入多余的方法。
6. 网关
目前主要有Zuul和spring cloud gateway两种,前者是基于servlet的阻塞式,后者是基于webflux的响应式,所以一般都用后者。配置方法:
创建gateway模块,引入依赖、配置并启动服务即可:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
// 配置
server:
port: 10010
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes: # 路由网关配置
- id: user-service # 路由id,唯一即可
uri: lb://userservice # 路由目标地址交给负载均衡,或者写http交给固定地址
predicates: # 路由断言,根据断言规则判断是否符合路由规则
- Path=/user/** #按路径匹配
路由原理解释:
6.1 断言工厂
根据官方教程使用即可
6.2 过滤器
过滤器可以对进入网关的请求和微服务返回的响应做处理,并形成过滤器链,配置文件中添加即可。
全局过滤器的优先级需要手动指定,另两种过滤器自动从1递增,优先级不同按优先级执行,相同则默认>路由>全局。
filters: # 对某个路由生效
- AddRequestHeader=X-Request-red, blue
default-filters: # 对所有路由生效
- AddRequestHeader=X-Request-red, blue
可以在方法中通过@RequestHeader(value = "truth", required = false)
非必须的添加请求头以实现过滤。
6.2 全局过滤器
网关过滤器只能用默认配置,可以实现GlobelFilter
接口实现自定义的过滤器。
// 指定过滤器的优先级[-2147483648, 2147483647],越小越高
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 2.获取参数中需要的属性
String authorization = queryParams.getFirst("authorization");
// 3.判断参数值放行或拦截
if ("admin".equals(authorization)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
6.3 网关跨域问题
跨域问题: 浏览器禁止请求的发起者和服务端发生跨域(域名不一致)Ajax请求,导致请求被浏览器拦截。在配置文件中配置网关即可。
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许哪些ajax请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许的头信息
allowCredentials: true # 是否允许cookie
maxAge: 360000 # 跨域检测有效期,有效期内直接放行,不再检查
7. Docker
Docker是一个快速交付和运行应用的技术,解决了两个部署时的问题:
- 大型项目依赖关系复杂,各个组件兼容性冲突:将应用+依赖+函数库+配置一起打包,形成可移植的只读镜像,实例化成容器,利用沙箱机制相互隔离。
- 开发、测试、生产环境差异:镜像中包含了系统函数库,使应用不依赖系统,直接操作Linux内核,所以可以在任意Linux系统中运行。
Tips: Linux操作系统=硬件+Linux内核+系统应用(Ubuntu, centOS…),容器跳过了系统应用直接操作内核。
对比虚拟机,虚拟机是在操作系统中模拟硬件,然后运行另一个系统。虚拟机多了一层从系统,并且Hypervisor会对硬件资源也进行虚拟化,而容器Docker会直接使用宿主机的硬件资源。
基本命令:
docker images
docker pull/push
docker rmi
删除镜像docker save/load
docker ps/rm/logs/exec
- 查看、删除容器
logs
name 日志(-f 持续跟踪)exec -it name bash
it创建容器输入输出终端,bash进入容器后执行Linux终端命令,此时进入容器内部Linux环境,具体文件位置查询官方文档
docker run
根据文档配置参数docker --help
核心命令☺
Tips: 小坑——容器stop之后仍然存在(等待着重新启动),所以不能修改,只有rm之后重新创建容器才行。
7.1 数据卷
镜像中一般不包括vim,所以对容器的修改很麻烦,因此docker提供了数据卷修改的方式,即数据卷同步映射到容器内文件——高级的说法就是容器与数据解耦。通过docker volume
操作数据卷,命令如图:
创建的数据卷会映射到宿主机的固定位置,如图所示:
通过-v 数据卷:容器内地址
挂载数据卷,不存在的数据卷自动创建,然后直接操作数据卷即可:
docker run --name ng -p 80:80 -d -v html:/usr/share/nginx/html nginx
7.2 自定义镜像
通过分层提高镜像的复用性
通过dockerfile自定义镜像,其本质是描述镜像构建过程的指令文件,构建过程依次是:
其中FROM-ENV-RUN是安装环境过程,可以利用现有的基础镜像简化(如java-alpine)
7.3 docker compose
docker compose基于compose文件快速部署分布式应用,无需手动一个个创建和运行容器,将dockfile转换成yaml格式的配置文件指定每个容器如何运行。
——了解即可,Portainer,k8s才是主流
7.4 镜像仓库
搭建docker私服:修改daemon.json—重命名镜像,以服务器地址为前缀—push/pull
8. 消息队列
为什么需要消息队列(异步通信),同步调用的缺点:
- 耦合度高,每次业务调整都需要修改代码
- 调用链过长,导致性能下降
- 资源浪费,每个服务在等待过程中占用资源
- 级联问题,一崩全崩
异步调用的流程:
异步调用的优点:
- 削峰限流
- 耦合度低
- 吞吐量提升
带来的问题:
- 可靠性依赖于消息队列
- 业务解耦,不易跟踪
常用消息队列对比:
8.1 MQ安装
15672
为管理端口,5672
为消息端口
默认账号密码均为guest
rabbitMQ的结构:
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对队列和交换机的逻辑分组,可以创建多个,互相隔离
消息队列基本例子:
// 生产者
public class producerTest {
@Test
public void testSend() throws IOException, TimeoutException {
// 创建连接参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("114.116.88.244");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 连接
Connection connection = factory.newConnection();
// 创建通道
Channel channel = connection.createChannel();
// 创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 发送消息
String message = "hello world!";
channel.basicPublish("", queueName, null, message.getBytes());
// 后续操作
System.out.println("消息【" + message + "】发送成功!");
channel.close();
connection.close();
}
}
// 消费者
public class ConsumerTest {
@Test
public void consumer() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("114.116.88.244");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 连接
Connection connection = factory.newConnection();
// 创建通道,避免消费者先到,不会重复创建
Channel channel = connection.createChannel();
// 创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 消费消息
channel.basicConsume(queueName,true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("消息【" + message + "】获取成功!");
}
});
// 后续处理,消费消息时只是使回调函数与通道绑定,然后便继续执行
System.out.println("等待接收消息。。。");
}
}
// 输出
等待接收消息。。。
消息【hello world!】获取成功!
可见消费者是通过回调函数接收消息,不影响后续流程。
注意: rabbitmq具有消费预取机制,perfetch=100,即每个队列先平均拿100条消息,处理完再拿,会影响自定义的分配机制,可以修改为1。
消息队列基本流程,生产者和消费者都需要声明队列,避免队列不存在:
8.2 SpringAMQP
AMQP:高级消息队列协议,用于应用程序间传递业务消息的开放标准;
SpringAMQP:基于AMQP的API规范,spring-amqp为基础抽象,spring-rabbit为底层实现
发布订阅模型:
注: 消息发送和接收必须使用相同的序列化方式,默认时jdk序列化,推荐改为json.
9. ElasticSearch
一个开源的分布式搜索引擎,用于搜索、日志统计分析、系统监控等功能,是目前最流行的搜索引擎技术。
elastic search搜索过程:
与MySQL的区别:
- MySQL适合事务性操作,确保数据的安全和一致性;
- ES适合海量数据的搜索、分析、计算;
- 即搜索业务用ES,存储业务用MySQL
9.1 ES的安装
黑马yyds!
9.1.1 部署单点es
- 创建网络
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
- 加载镜像
这里我们采用elasticsearch的7.17.1版本的镜像,
docker pull elasticsearch:7.17.1
同理还有kibana
也需要这样做。
- 运行
运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.17.1
命令解释:
-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置
在浏览器中输入:http://ip:9200
即可看到elasticsearch的响应结果:
9.1.2 部署kibana
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
- 部署
运行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.17.1
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601
:端口映射配置
kibana启动一般比较慢,需要多等待一会,可以通过命令:
docker logs -f kibana
查看运行日志,在浏览器输入地址访问:http://192.168.150.101:5601
,即可看到结果
9.1.3 ik分词
ES自带分词器对中文支持很差,所以需要额外安装ik分词器,ik分词器两种模式:
- ik_smart:最少切分
- ik_max_word:最细切分
POST _analyze
{
"text": ["ES真好用!"],
"analyzer": "ik_max_word"
}
但分词器本质上是通过自己的字典进行匹配,会有大量不包含的词语,所以可以自己配置字典,在IKAnalyzer.cfg.xml
中配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
9.2 ES的操作
es中mapping的使用,ES不允许修改索引库,只能添加字段:
索引库操作:
文档操作:
结语
到此对于微服务的各种中间件有了基础的认识,此教程的ES详细使用和高级篇将在后续项目的实践中进行总结,开启微服务项目!