Skip to content

springcloud笔记

官网

打开springcloud官网

功能

版本兼容

架构对比

单体架构

分布式架构

根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。

  • 优点:降低服务耦合有利于服务升级和拓展
  • 缺点:服务调用关系错综复杂 分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:
  • 服务拆分的粒度如何界定?
  • 服务之间如何调用?
  • 服务的调用关系如何管理? 人们需要制定一套行之有效的标准来约束分布式架构。

微服务

微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。

因此,可以认为微服务是一种经过良好架构设计的分布式架构方案

但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案。

其中在Java领域最引人注目的就是SpringCloud提供的方案了。

总结

  • 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
  • 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:东、淘宝
  • 微服务:一种良好的分布式架构方案
    优点:拆分粒度更小、服务更独立、耦合度更低 缺点:架构非常复杂,运维、监控、部署难度提高
  • SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件

服务拆分和远程调用

服务拆分原则

  • 不同微服务,不要重复开发相同业务
  • 微服务数据独立,不要访问其它微服务的数据库
  • 微服务可以将自己的业务暴露为接口,供其它微服务调用

微服务请求

openFeign(常用)

依赖

xml
  <!--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功能

java
// ↓这个注解用户开启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客户端接口

java
// ↓使用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);
}

使用

java
@Service  
@RequiredArgsConstructor  
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
	// 注入fegin客户端,注意上面需要写@RequiredArgsConstructor
    private final ItemClient itemClient;
}

中间注释的代码是使用了feign后被简化掉的(简化之前用的是restTemplate)

java
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依赖

xml
<!--OK http 的依赖 -->  
<dependency>  
    <groupId>io.github.openfeign</groupId>  
    <artifactId>feign-okhttp</artifactId>  
</dependency>

配置连接池

在order-service的application.yml中添加配置:

yml
feign:
  okhttp:
    enabled: true #开启OKHTTP连接池支持
  sentinel:
    enabled: true #开启sentinel支持(需要引入sentinel,在后面的sentinel章节用到)
整合httpClient

引入httpClient依赖

xml
<!--httpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

配置连接池

yml
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包引入所需的依赖

xml
<!--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包

xml
<!--api-->  
<dependency>  
    <groupId>com.heima</groupId>  
    <artifactId>hm-api</artifactId>  
    <version>1.0.0</version>  
</dependency>

将cart服务中的itemService引入路径改为公共的hm-api下的包路径

java
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所在包

java
@EnableFeignClients(basePackages = "com.hmall.api.clients")

方式二:指定FeignClient字节码

java
@EnableFeignClients(clients = {UserClient.class})

此处使用的是方式一 由于当前服务属于cart-service,所以springboot扫包不会扫到api服务中,也就无法根据反向代理生成FeignClient实体类,因此需要手动将Feign的basePackages设置为api服务下的client接口所在包的路径

java
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,在其中定义日志级别:

java
public class DefaultFeignConfig {    
	@Bean    
	public Logger.Level feignLogLevel(){        
		return Logger.Level.FULL;  
    }  
}

feign采用bean来配置日志级别,因此将其放入api模块下,以供其他微服务调用,自己写一个feign的配置类,示例:(此时,配置还没生效)

局部生效

在某个FeignClient中配置,只对当前的FeignClient生效

java
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
全局生效

@EnableFeignClients中配置,针对所有FeignClient生效

java
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

日志输出结果查看

RestTemplate

注册bean

java
@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枚举中找

java
public enum HttpMethod {  
    GET,  
    HEAD,  
    POST,  
    PUT,  
    PATCH,  
    DELETE,  
    OPTIONS,  
    TRACE;
}
  • requestEntity:请求头、体

  • ParameterizedTypeReference:由于不能直接传List<泛型>.class,所以需要一个类型解析器来解析

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

关于spring的自动注入

通常是这样写

java
// 使用@Autowired将其注入,但是spring不推荐这样写
@Autowired  
private  RestTemplate restTemplate;

spring推荐的写法

java
@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修饰的成员变量加上构造函数,就不用自己去写构造函数了

java
@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)

xml
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

客户端(非eureka的服务)

xml
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

注解

@EnableEurekaServer 需要在eureka的服务端使用,非eureka的服务不用写该注解

基本配置

eureka服务端 eureka会按名字将服务注册进去(包括eureka自己)

yaml
server:
  port: 10086 # 服务端口
spring:
  application:
    name: eurekaserver # eureka的服务名称
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

客户端(非eureka)

yaml
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

sh
# 进到bin目录下

# 单机启动
startup.cmd -m standalone

#集群启动
startup.cmd
linux端
sh
# 配置环境变量
vim /etc/profile
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin

# 重新加载环境变量
source /etc/profile

# 启动命令
sh startup.sh -m standalone
docker启动nacos

docker部署nacos

依赖管理

父工程(放到dependencyManagement中)

xml
<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>

客户端

xml
<!-- nacos客户端依赖包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置文件

nacos是属于spring的

环境隔离

配置集群

负载均衡策略

配置命名空间

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

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

配置临时实例

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

配置方式

nacos注册中心原理

注册中心对比

AP:主要强调数据的可用性 CP:主要强调数据的可靠性和一致性

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

服务管理

restTemplate

DiscoveryClient

配置管理

依赖
xml
<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子节点配置

初始化数据库

sql
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文件
yml
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节点配置
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即可

网关

参考文档

https://docs.spring.io/spring-cloud-gateway/docs/3.1.9/reference/html/#the-addrequestheader-gatewayfilter-factory

快速入门

predicates可以按照图片上配,也可以拆开配(key=value形式或key=value,value,value...)

路由断言

路由过滤器

单个路由生效

校验逻辑

注意事项

网关请求处理流程

自定义过滤器

GlobalFilter

GatewayFilter
基本使用

使用过滤器

传参使用

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

配置文件传参

登录校验逻辑编写

实现登录校验

注意:authProperties用于获取需要排除的路径

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

网关传递用户信息

创建微服务拦截器

在配置中使用拦截器

openfeign传递用户

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

创建拦截器

指定feign使用的配置文件

不指定的话是不会执行配置文件中的请求拦截器的

总结流程

跨域

网关处理跨域采用的是CORS方案

yml
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

启动控制台
sh
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

访问地址\http://localhost:8090

账号和密码,默认都是:sentinel

请求之后才会显示对应的服务

引入依赖
xml
<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置文件

相关配置 https://github.com/alibaba/Sentinel/wiki/启动配置项

当前的理解(待更新)

限流限制的是qps,针对接口(簇点链路的根节点),线程隔离、服务降级和服务熔断限制的是处理的线程数,针对需要调用远程服务的接口(簇点链路的子节点)

请求限流

需要请求限流的原因

服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

实践

线程隔离

作用

限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。

开启Feign的sentinel功能
yml
feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持

重启cart-service服务,调用一次购物车接口(购物车接口会远程调用商品服务!!!)

观察到在簇点链路中的购物车请求下出现商品服务的请求节点

此时,就可以对商品服务的调用线程数做限制,以达到线程隔离的目的

配置限制5个线程可以远程调用商品服务方法

1s可以允许处理5个线程 代码中500ms才能处理一个请求,所以1s可以处理2个请求

txt
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依赖

xml
<!--sentinel规则持久化存储在nacos中,版本与sentinel保持一致-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.8.6</version>
</dependency>

在nacos中创建配置文件(随便起名,但是建议见名识意)

文件名

degrade.json

配置内容

json
[
    {
        "resource":"GET:http://item-service/items",
        "count":200.0,
        "grade":0,
        "slowRatioThreshold":0.5,
        "timeWindow":10
    }
]

如何查找配置参数 查找并使用对应的类参数

拉取配置

yml
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/

架构

准备

数据库文件

进入github https://github.com/apache/incubator-seata

找到script

找到sercer下面的db中的对应的sql文件,然后将其运行 一共四张表

例子项目

整个server就是例子项目,本次使用的是黑马提供的资源

部署TC服务

docker部署seata

挂载seata

如果因为登录不上nacos报错,就直接删除配置文件中的用户名和密码或者时更换nacos版本

在注册中心中看到seata,则说明部署成功

引入seata依赖

xml
  <!--seata-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  </dependency>

微服务集成seata

准备

将其加入到nacos配置中

yml
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表
sql
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来解决,因为创建快照和回滚快照都是会有性能损耗的

待补充。。。