springcloud笔记
官网
功能

版本兼容

架构对比
单体架构

分布式架构
根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
- 优点:降低服务耦合有利于服务升级和拓展
- 缺点:服务调用关系错综复杂
分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考: - 服务拆分的粒度如何界定?
- 服务之间如何调用?
- 服务的调用关系如何管理? 人们需要制定一套行之有效的标准来约束分布式架构。
微服务
微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。
但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案。
其中在Java领域最引人注目的就是SpringCloud提供的方案了。 
总结
- 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
- 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:东、淘宝
- 微服务:一种良好的分布式架构方案
优点:拆分粒度更小、服务更独立、耦合度更低 缺点:架构非常复杂,运维、监控、部署难度提高 - SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件
服务拆分和远程调用
服务拆分原则
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用

微服务请求
openFeign(常用)
依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>开启feignClient功能
// ↓这个注解用户开启feign客户端的功能
@EnableFeignClients(basePackages = "com.hmall.api.client"/*,defaultConfiguration = DefaultFeignConfig.class*/)
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}编写feign客户端接口
// ↓使用fegin客户端,并设定要拉去的服务名为item-service;指定配置类为DefaultFeignConfig.class,指定回调工厂类为ItemClientFallbackFactory.class
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class
, fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
// 表示使用get请求,请求地址为/items
@GetMapping("/items")
// 值根据接口接收值名称传,由于传输过程是用json,但是请求前的值是Set集合,所以此处用Collection来接收值
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}使用
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
// 注入fegin客户端,注意上面需要写@RequiredArgsConstructor
private final ItemClient itemClient;
}中间注释的代码是使用了feign后被简化掉的(简化之前用的是restTemplate)
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// 2.1.根据服务名称获取服务的实例
// List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
// if (CollUtils.isEmpty(instances)) {
// return;
// }
//// 2.2.手写负载均衡,从实例列表中挑选一个实例
// ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
//// 2.3.利用RestTemplate发起http请求,得到http响应
// ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(serviceInstance.getUri() + "/items?ids={ids}",
// HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() {
// }, Map.of("ids", CollUtil.join(itemIds, ",")));
//// 2.2解析响应
// if (!response.getStatusCode().is2xxSuccessful()) {
//// 查询失败,直接结束
// return;
// }
// List<ItemDTO> items = response.getBody();
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);Feign使用优化
优化Feign的性能主要包括
- 使用连接池代替默认的URLConnection
- 日志级别,最好用basic或none
链接池
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池 因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
openfeign请求底层实现调用的是Default in Client,具体源码可以参考FeignBlockingLoadBalancerClient中的delegate成员变量(点开client后按下ctrl+h可以查看client对应的子类)
根据断点运行后可以看到该默认实现类为HttpURLConnection

整合okhttp(我用的是这个)
引入okhttp依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>配置连接池
在order-service的application.yml中添加配置:
feign:
okhttp:
enabled: true #开启OKHTTP连接池支持
sentinel:
enabled: true #开启sentinel支持(需要引入sentinel,在后面的sentinel章节用到)整合httpClient
引入httpClient依赖
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>配置连接池
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数测试 接下来,在FeignClientFactoryBean中的loadBalance方法中打断点:
最佳实现
方式1
将请求所需的接口和实体类引入其他微服务
缺点
- 服务高耦合
- 父接口参数列表中的映射不会被继承

方式2(选用该方法)
将feginClient抽取为独立求模块,并把接口有关的OPJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
例如cart服务需要请求item服务(记得加上包路径扫描——最后一步)
将item服务中的实体放到公共的请求包中(item自己不留该实体) 
为hm-api包引入所需的依赖
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>为cart服务引入hm-api包
<!--api-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>将cart服务中的itemService引入路径改为公共的hm-api下的包路径
import com.hmall.api.client.ItemClient;
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final ItemClient itemClient;
}加入扫描路径 当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决: 方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "com.hmall.api.clients")方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})此处使用的是方式一 由于当前服务属于cart-service,所以springboot扫包不会扫到api服务中,也就无法根据反向代理生成FeignClient实体类,因此需要手动将Feign的basePackages设置为api服务下的client接口所在包的路径
package com.hmall.cart;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
// 这一步用于将请求服务加入springboot的扫描↓
@EnableFeignClients(basePackages = "com.hmall.api.client"/*,defaultConfiguration = DefaultFeignConfig.class*/)
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
// @Bean
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
}日志级别
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
日志的配置方式
声明日志级别bean
要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}feign采用bean来配置日志级别,因此将其放入api模块下,以供其他微服务调用,自己写一个feign的配置类,示例:(此时,配置还没生效) 
局部生效
在某个FeignClient中配置,只对当前的FeignClient生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)全局生效
在@EnableFeignClients中配置,针对所有FeignClient生效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
日志输出结果查看

RestTemplate
注册bean
@EnableFeignClients(basePackages = "com.hmall.api.client"/*,defaultConfiguration = DefaultFeignConfig.class*/)
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}使用方式
url

请求方法:在HttpMethod枚举中找
public enum HttpMethod {
GET,
HEAD,
POST,
PUT,
PATCH,
DELETE,
OPTIONS,
TRACE;
}requestEntity:请求头、体
ParameterizedTypeReference:由于不能直接传List<泛型>.class,所以需要一个类型解析器来解析

urlVariables:这里传入的是请求参数,由于请求参数需要的是以逗号分隔的字符串,所以使用hutool包将set集合转化为string

关于spring的自动注入
通常是这样写
// 使用@Autowired将其注入,但是spring不推荐这样写
@Autowired
private RestTemplate restTemplate;spring推荐的写法
@Service
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final RestTemplate restTemplate;
// 用构造函数的方式注入是spring推荐的写法
// spring会自动注入这个对象↓
public CartServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
}利用lombok简化写法
利用lombok的@RequiredArgsConstructor可以方便的将final修饰的成员变量加上构造函数,就不用自己去写构造函数了
@Service
// 该注解可以为final修饰的成员变量加上构造函数
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
// 需要注入的成员变量加上final关键字
private final RestTemplate restTemplate;
}服务注册与发现
总结
服务治理中的三个角色分别是什么?
- 服务提供者:暴露服务接口,供其它服务调用
- 服务消费者:调用其它服务提供的接口
- 注册中心:记录并监控微服务各实例状态,推送服务变更信息
消费者如何知道提供者的地址?
- 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何得知服务状态变更?
- 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
当提供者有多个实例时,消费者该选择哪一个?
- 消费者可以通过负载均衡算法,从多个实例中选择一个
Eureka注册中心
基本使用思路
1.搭建EurekaServer
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml中配置eureka地址 2.服务注册
- 引入eureka-client依赖
- 在application.yml中配置eureka地址 3.服务发现
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 给RestTemplate添加@LoadBalanced注解
- 用服务提供者的服务名称远程调用
依赖
服务端(eureka)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>客户端(非eureka的服务)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>注解
@EnableEurekaServer 需要在eureka的服务端使用,非eureka的服务不用写该注解 
基本配置
eureka服务端 eureka会按名字将服务注册进去(包括eureka自己)
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver # eureka的服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka客户端(非eureka)
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # 这个会被当做当前服务注册的名字
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka通过负载均衡调用service
注解
@LoadBalanced 被这个注解标记的RestTemplate发起的请求会被ribbon拦截和处理
在发请求的方法上加上负载均衡注解 
用服务在eureka中注册的名称替换ip地址 
ribbon负载均衡
基本原理图解

配置负载均衡规则

早期一般是用ribbon做负载均衡,但是现在直接用springcloud的loadbalance做负载均衡
nacos
安装与启动
下载地址
https://github.com/alibaba/nacos
端口配置(默认8848)

windows端启动命令
初始账号密码都是nacos
# 进到bin目录下
# 单机启动
startup.cmd -m standalone
#集群启动
startup.cmdlinux端
# 配置环境变量
vim /etc/profile
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin
# 重新加载环境变量
source /etc/profile
# 启动命令
sh startup.sh -m standalonedocker启动nacos
依赖管理
父工程(放到dependencyManagement中)
<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>配置文件
nacos是属于spring的
环境隔离

配置集群


负载均衡策略

配置命名空间
相同命名空间的服务才能相互调用

在配置文件中填入对应的命名空间



配置临时实例
临时实例心跳不正常会被剔除,非临时实例不会

配置方式 
nacos注册中心原理

注册中心对比
AP:主要强调数据的可用性 CP:主要强调数据的可靠性和一致性

注意:当集群挂了之后,会进行跨集群调用,并且调用方会产生警告日志

服务管理
restTemplate

DiscoveryClient 

配置管理
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>配置方法

例子


热更新
在使用了配置类的文件中加入@RefreshScope,两种读取配置的方法任选其一 
热更新配置字段的方式①
使用@Value注入
热更新配置字段的方式②
使用@ConfigurationProperties注入

- 此处得用@Autowired来注入对象来读取值

如果不想给他注册成component,就用下面这个方法
读取配置类(这里没有@Component)

使用@EnableConfigurationProperties(AuthProperties.class)将其注入进去

多服务共享配置
优先级

新建配置环境

共享配置userservice.yaml

dev环境配置userservice-dev.yaml

测试结果


8081为dev

8082为test

启动失败踩坑
①读取nacos配置文件失效 
②@RefreshScope加上后不生效,重新打包
这些坑都在照做完成,并且正确启动过一次后消失,甚至都可以直接删除第一个坑中的配置,推测应该是nacos版本的问题(不想测了,等之后测吧)
nacos集群搭建
文档参考
https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html
架构图

nacos子节点配置
初始化数据库
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');配置nacos
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
添加内容
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847
然后修改application.properties文件spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123将nacos复制三份
将端口分别改为8845、8846、8847
分别启动nacos集群
startup.cmd
nginx节点配置
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}而后在浏览器访问:http://localhost/nacos即可
网关
参考文档
快速入门
predicates可以按照图片上配,也可以拆开配(key=value形式或key=value,value,value...)



路由断言

路由过滤器

单个路由生效


校验逻辑
注意事项

网关请求处理流程

自定义过滤器


GlobalFilter



GatewayFilter
基本使用


使用过滤器

传参使用

代码,注意shortcutFieldOrder中的值是Config中参数的名称

配置文件传参

登录校验逻辑编写
实现登录校验
注意:authProperties用于获取需要排除的路径


需要排除的路径(这些路径需要用spring提供的AntPathMatcher来进行匹配)

网关传递用户信息


创建微服务拦截器

在配置中使用拦截器

openfeign传递用户
在用户点击结算后,支付服务会创建订单,将订单id传到购物车服务,购物车服务需要从ThreadLocal中取出用户id,但此时购物车取不到,因为请求不是从网关过来的,而是从支付服务过来的,所以我们需要让支付服务在请求时带上请求头来传递用户信息,购物车拦截器拦截到之后,就可以将用户信息set到Threadlocal中了

创建拦截器

指定feign使用的配置文件
不指定的话是不会执行配置文件中的请求拦截器的 
总结流程 
跨域
网关处理跨域采用的是CORS方案
spring:
gateway:
globalcors: # 全局跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
cors-configurations:
'[/**]':
allowedOrigins: # 允许哪些网站跨域请求
- "http://localhost:18080"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期服务保护
服务保护方案
为什么需要保护
雪崩


保护方案
请求限流

线程隔离

服务熔断

服务降级

服务保护技术
介绍

sentinel
介绍
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
主要特性

官网
https://sentinelguard.io/zh-cn/
github
https://github.com/alibaba/Sentinel/wiki/介绍
控制台下载
https://github.com/alibaba/Sentinel/releases
启动控制台
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar访问地址\http://localhost:8090
账号和密码,默认都是:sentinel
请求之后才会显示对应的服务
引入依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>配置文件

当前的理解(待更新)
限流限制的是qps,针对接口(簇点链路的根节点),线程隔离、服务降级和服务熔断限制的是处理的线程数,针对需要调用远程服务的接口(簇点链路的子节点)
请求限流
需要请求限流的原因
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

实践


线程隔离
作用
限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。 
开启Feign的sentinel功能
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持重启cart-service服务,调用一次购物车接口(购物车接口会远程调用商品服务!!!)
观察到在簇点链路中的购物车请求下出现商品服务的请求节点
此时,就可以对商品服务的调用线程数做限制,以达到线程隔离的目的

配置限制5个线程可以远程调用商品服务方法
1s可以允许处理5个线程
代码中500ms才能处理一个请求,所以1s可以处理2个请求
1s=5个线程
1s=2个请求
所以1s=线程数*请求数=10qps测试条件中的qps为5000/50=100
测试结果可得,商品服务qps为10
此时查询购物车时大概率会显示查询失败(100个请求只会通过10个),表示请求已经被拒绝了,但是这样并不好,从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好,因此我们可以给查询失败设置一个降级处理逻辑(看下面的服务熔断)
服务熔断
使用原因
线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。 
处理方法
编写服务降级逻辑
就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
异常统计和熔断
统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
服务降级(只用到FallbackFactory)
优点
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
给FeignClient编写失败后的降级逻辑有两种方式
- FallbackClass 无法对远程调用的异常做处理
- FallbackFactory 可以对远程调用的异常做处理,我们一般选择这种方式。

Fallback
写法
客户端
定义fellback工厂


注意:还需要在客户端中指定bean所在的配置文件哦!(找到下方的:指定feign客户端需要扫描的配置类)
实践
编写回调工厂类

指定对应的回调工厂

将回调工厂配置为bean

指定feign客户端需要扫描的配置类
查看压测结果
也可以在这里指定配置类
调用购物车查询服务
压测前
可以看到库存不足,说明商品信息的远程调用成功 
压测时
查询成功,并没有报错内容,走的fallback逻辑(服务降级了),并且无法看到库存不足,说明商品信息远程调用失败 
从sentinel监控界面可以看出,商品服务被限流,允许的qps为10,拒绝的qps为90

服务熔断
作用
应对微服务雪崩效应的一种链路保护机制;服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用 
实践
配置熔断规则 
调用购物车服务
压测前(熔断前) 可以查到商品库存不足

压测时 购物车元辰调用的商品服务几乎被拒绝


配置持久化
引入sentinel的nacos依赖
<!--sentinel规则持久化存储在nacos中,版本与sentinel保持一致-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.6</version>
</dependency>在nacos中创建配置文件(随便起名,但是建议见名识意)
文件名
degrade.json
配置内容
[
{
"resource":"GET:http://item-service/items",
"count":200.0,
"grade":0,
"slowRatioThreshold":0.5,
"timeWindow":10
}
]
如何查找配置参数
查找并使用对应的类参数
拉取配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # 控制台地址
http-method-specify: true # 开启请求方式前缀
datasource:
flow: #配置文件的数据源名称(这里可以随便起名,此处起名为flow)
nacos:
server-addr: 192.168.111.128:8848 # nacos地址
dataId: degrade.json
groupId: DEFAULT_GROUP
data-type: json
rule-type: degrade # 还可以是:degrade、authority、param-flow查看是否成功
分布式事务
seata
官网
https://seata.apache.org/zh-cn/
架构

准备
数据库文件
找到script

找到sercer下面的db中的对应的sql文件,然后将其运行
一共四张表
例子项目
整个server就是例子项目,本次使用的是黑马提供的资源
部署TC服务
docker部署seata
如果因为登录不上nacos报错,就直接删除配置文件中的用户名和密码或者时更换nacos版本
在注册中心中看到seata,则说明部署成功
引入seata依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>微服务集成seata
准备

将其加入到nacos配置中
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.111.128:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"报错
如果使用的是jdk17,则会在启动时报错
解决方式 在jvm启动参数上添加右边这个参数
参考文章:https://developer.aliyun.com/article/939377
事务执行模式
XA模式


AT模式



使用事务
使用XA模式进行事务处理
nacos编写配置

seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.111.128:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
data-source-proxy-mode: XA # 开启XA模式让需要使用分布式事务的服务拉取配置并加入依赖(看上方引入依赖的步骤)

在事务的入口加上@GlobalTransactional注解

在事务的每个分支方法上加上@Transactional注解


加在接口上也行,根据具体情况加

加载接口上也行,根据具体情况加
使用AX模式进行事务处理
在每个微服务数据库中加入undo_log表
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
更改nacos中的模式为AT模式(AT模式为默认模式,只用将data-source-proxy-mode删除即可)

让需要使用分布式事务的服务拉取配置并加入依赖(看上方引入依赖的步骤)

在事务的入口加上@GlobalTransactional注解

在事务的每个分支方法上加上@Transactional注解


加在接口上也行,根据具体情况加

加在接口上也行,根据具体情况加
测试
在会报错的地方打个断点

查看快照
查看购物车中的数据是否被删除
查看seata控制台
事务信息
锁信息
解开断点,完成所有代码执行
发现数据已经回滚
购物车表的数据已经回滚
快照表的数据已经删除
发现seata控制台事务信息和锁信息已经清空
总结
这是一个只出现在面试中的技术,需要和面试官讲清楚分布式事务的解决方案即可,建议用mq来解决,因为创建快照和回滚快照都是会有性能损耗的
待补充。。。






代码中500ms才能处理一个请求,所以1s可以处理2个请求 
测试结果可得,商品服务qps为10 


查看压测结果 



查找并使用对应的类参数 

一共四张表 



参考文章:








