SpringCloud

SpringCloud

三 本章知识点

3.1 项目架构演变

3.1.1 单体应用架构

部署到一个war里 部署到一个web容器里(如tomcat) 公用一个DB 

优点:
容易测试 容易部署

缺点:
开发效率低 代码维护难 
部署不灵活(如构建时间特别长,如任意小的修改,需要重新构建整个项目)
稳定性不高(如任一一个小问题,可能让你整个系统挂掉)
扩展性不够(如购物场景,商品服务和订单服务,浏览的人比下单的人多,商品服务的流量会大一点。如果是微服务,商品服务部署10台,订单服务部署5台)

3.1.2 垂直应用架构体系

主要解决前后端、界面、控制逻辑和业务逻辑的分层问题。比较流行的技术栈SSM,SSH sm等。

优点:解决了单一架构面临的扩容问题,流量可以分散到各个子系统中,且体积可控,提高了开发效率。
缺点:垂直应用越来越多,应用间的相互交互,相互调用已无法避免,不同系统之间存在重叠的业务。

在这里插入图片描述

3.1.3 RPC远程过程调用

随着业务发展,业务规模的扩大,模块化逐步成为一种趋势。此时解决模块之间远程调用的RPC应用而生。

缺点: RPC本身不负责服务化。例如自动发现不管,服务的应用和发布不管、服务运维和治理不管。

在这里插入图片描述

3.1.4 SOA

Service-Oriented Architecture 面向服务的架构
为了解决垂直应用架构重复造轮子,提取出来作为单独的系统对外提供服务,形成业务之间的相互重用,这是SOA就出现了

SOA服务化架构,企业级资产重用和异构系统间的集成对接。SOA架构的现状在传统企业IT领域,主要是解决异构系统之间的互通和粗粒度的标准化(WebService)互联网领域,提供一套高效支撑应用快速迭代的服务化架构

    
    dubbo(远程调用)+ ZooKeeper(注册中心)

在这里插入图片描述

在这里插入图片描述

3.1.5 微服务架构

微服务是一种架构风格,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。

特征如下:
    1)小,且只干一件事情  
    2)独立部署和生命周期管理
    3)异构性        
    4)轻量级通信。

在这里插入图片描述

3.1.6 SpringCloud架构体系 与 组件


注册中心:  ZooKeeperEurekaApolloNacos

配置中心:  SpringCloud ConfigApolloNacos

API网关:   ZuulGateway
    
熔断限流:   HystrixSentinel
    
链路追踪:   ZipkinSkyWalking
    
管理监控:   SpringBootAdmin
    
服务调用:   FeignOpenFeign
    
分布式数据库:  Seata
    
分布式文件:  FastDFS

分布式任务调度: Quartz
    
日志:  ELK
    
自动化部署: Docker+Jenkins

消息队列: MQ
    
数据同步: Canal
    
缓存服务: redis
    
负载均衡: nginx
    
数据库中间件: mycat

在这里插入图片描述

3.1.7 cloud优秀项目推荐


https://gitee.com/majunwei2017/jbone    
https://gitee.com/log4j/pig

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzUbG6Qd-1666438471240)(assets/features.png)]

3.1.8 cloud项目官网

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.
    
    
Netflix 编辑 

    Netflix(Nasdaq NFLX) 译为奈飞或网飞,是一家会员订阅制的流媒体播放平台 [1]  ,总部位于美国加利福尼亚州洛斯盖图。成立于1997年,曾经是一家在线DVD及蓝光租赁提供商,用户可以通过免费快递信封租赁及归还Netflix库存的大量影片实体光盘。 [2] 
    Netflix已经连续五次被评为顾客最满意的网站。可以通过PC、TV及iPad、iPhone收看电影、电视节目,可通过WiiXbox360,PS3等设备连接TV。Netflix大奖赛从200610月份开始,Netflix公开了大约1亿个15的匿名影片评级,数据集仅包含了影片名称、评价星级和评级日期,没有任何文本评价的内容。比赛要求参赛者预测Netflix的客户分别喜欢什么影片,要把预测的效率提高10%以上。
    20171130日,Netflix买下《白夜追凶》播放权,这是该公司首次买下中国内地网络电视剧版权,计划在全球190多个国家和地区上线 

https://spring.io/projects/spring-cloud
https://spring.io/projects/spring-cloud-alibaba  
https://www.springcloud.cc/ 

在这里插入图片描述

springcloud  要依赖于springboot ,此时 cloud和boot就有一个版本的匹配
cloud的版本和传统的框架不太一样,传统框架版本号都是 1.0.5  2.5.1  cloud版本都是 地名

3.1.9 springcloud项目的搭建

注意 springcloud 搭建 也是一个父工程 我们在maven中需要将springcloud 添加成当前项目的parent。但是springcloud又是基于springboot的,此时我们项目中已经有springboot作为当前项目的parent。一个项目只能有一个parent,所以我们需要这样优化:

①  父项目 springboot  单独通过 dependenciesmangerment引入springcloud
②  父项目springcloud   单独通过 dependenciesmangerment引入springboot
③  springboot 和 springcloud 都通过dependenciesmangerment 引入


 <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-boot.version>2.5.1</spring-boot.version>
    <spring-cloud.version>2020.0.3</spring-cloud.version>
  </properties>

  <dependencyManagement>
    <dependencies>
    <!-- SpringCloud 微服务 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <!-- SpringBoot 依赖配置 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    </dependencies>
  </dependencyManagement>

3.2 注册中心之EUREKA

3.2.1 注册中心

ZooKeeperEurekaApolloNacos

注册中心:  注册、发现
    
    
注册中心在微服务项目中扮演着非常重要的角色,是微服务架构中的纽带,类似于通讯录,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用。


注册中心解决了服务发现的问题。在没有注册中心时候,服务间调用需要知道被调方的地址或者代理地址。当服务更换部署地址,就不得不修改调用当中指定的地址或者修改代理配置。而有了注册中心之后,每个服务在调用别人的时候只需要知道服务名称就好,继续地址都会通过注册中心同步过来。

3.2.2 注册中心EUREKA搭建

1 创建一个song-register的父项目 

2 在 父项目中引入jar包依赖

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

3 在song_register项目中创建一个 song_eureka7001项目
4 在项目中 创建启动类  Application
    
@SpringBootApplication
@EnableEurekaServer
public class Eureka7001 {

    public static void main(String[] args) {
        SpringApplication.run(Eureka7001.class,args);
    }

} 
5 在项目中创建一个 resources文件夹 并且mark as  添加  application.yml配置文件
server:
   port: 7001    #端口号

eureka:
  instance:
    hostname: eureka7001    #主机名称
  client:
    registerWithEureka: false   #注册 询问是否当前服务 要注册到注册中心 此时选择false 因为我们自己就是注册中心
    fetchRegistry: false        #发现  询问是否发现注册中心的服务 此时也是false  因为我们自己就是注册中心
    serviceUrl:                 #服务地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
6 修改电脑 C:\Windows\System32\drivers\etc  下的  hosts文件 配置ip别名

127.0.0.1   eureka7001
127.0.0.1   eureka7002
127.0.0.1   eureka7003

7 启动application项目 并且访问

http://eureka7001:7001

3.2.3 注册中心页面内容介绍

System Status   系统状态
Environment 	test
Data center 	default
Current time 	2021-01-08T11:51:23 +0800
Uptime 	00:03
Lease expiration enabled 	false     是否启用租约过期 , 自我保护机制关闭时,该值默认是true, 自我保护机制开启之后为false
Renews threshold 	1                 期望阈值  每分钟最少续约数
Renews (last min) 	0                 实际阈值  最后一分钟的续约数量(不含当前,1分钟更新一次)
    
    
DS Replicas   分片/ 副本 搭建集群 就能看见其他的内容

Instances currently registered with Eureka  当前注册中心的注册实例
Application	AMIs	Availability Zones	Status
No instances available
    
    
General Info       基本信息
Name	Value
total-avail-memory	395mb
environment	test
num-of-cpus	4
current-memory-usage	268mb (67%)
server-uptime	00:03
registered-replicas	
unavailable-replicas	
available-replicas	
    
Instance Info     注册中心信息
Name	Value
ipAddr	172.16.22.206
status	UP

3.2.4 自我保护机制 (面试题)

在这里插入图片描述


紧急情况!EUREKA可能错误地声称实例在没有启动的情况下启动了。续订小于阈值,因此实例不会为了安全而过期
当预值小于期望阀值的时候 就会出去问题。



服务保护模式(自我保护模式):一般情况下,微服务在Eureka上注册后,会每30秒发送心跳包,Eureka通过心跳来判断服务时候健康,同时会定期删除超过90秒没有发送心跳服务。



导致Eureka Server接收不到心跳包的可能:

一是微服务自身的原因(程序就是挂了)

二是微服务与Eureka之间的网络故障。



此时如果是网卡了 所以不应该在注册中心中剔除当前微服务。

此时EUREKA就有了一种自我保护机制 为了防止误杀

通常微服务的自身的故障只会导致个别服务出现故障,一般不会出现大面积故障,而网络故障通常会导致Eureka Server在短时间内无法收到大批心跳。考虑到这个区别,Eureka设置了一个阈值,当判断挂掉的服务的数量超过阈值时,Eureka Server认为很大程度上出现了网络故障,将不再删除心跳过期的服务。这个操作称之为防止误杀。

那么这个阈值是多少呢?Eureka Server在运行期间,会统计心跳失败的比例在15分钟内是否低于85%,如果低于85%Eureka Server则认为是网络故障,不会删除心跳过期服务。

这种服务保护算法叫做Eureka Server的服务保护模式。

这种不删除的,90秒没有心跳的服务,称为无效服务,但是还是保存在服务列表中。如果Consumer到注册中心发现服务,则Eureka Server会将所有好的数据(有效服务数据)和坏的数据(无效服务数据)都返回Consumer。

①  每30秒检测一次心跳。
②  90秒没有收到心跳就要剔除服务
③  为了防止误杀,eureka会有一个阀值 85%。低于85%的时候不剔除任何服务(大面积微服务没有收到心跳、此时认为是自己的问题 不是服务的问题)。在阀值范围以内 就提出。
例如:
A   一共有100个服务  其中有 3个检测不到 则剔除这三个。
B  一共有100个服务 其中有个 20个检测不到 则不剔除任何服务
从源代码中查看阀值的计算:

2021-01-08 14:08:02.813  INFO 5848 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry  : Running the evict task with compensationTime 0ms

此时在AbstractInstanceRegistry类中就有阀值的计算。在当前类的148
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {}
注册当前实例的方法 在方法里面

  synchronized(this.lock) {
      if (this.expectedNumberOfClientsSendingRenews > 0) {
              ++this.expectedNumberOfClientsSendingRenews; // 服务数量上自增1
              this.updateRenewsPerMinThreshold();          // 更新预值
      }
 }
 
 此时是这样更新预值的
protected void updateRenewsPerMinThreshold() {
        this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfClientsSendingRenews * (60.0D / (double)this.serverConfig.getExpectedClientRenewalIntervalSeconds()) * this.serverConfig.getRenewalPercentThreshold());
    }
    
    
//看当前用户是否配置  如果没有配置则是30 如果配置了 则是配置的值 
this.serverConfig.getExpectedClientRenewalIntervalSeconds()
    
 public int getExpectedClientRenewalIntervalSeconds() {
        int configured = configInstance.getIntProperty(this.namespace + "expectedClientRenewalIntervalSeconds", 30).get();
        return configured > 0 ? configured : 30;
    }   
// 阀值 0.85   
this.serverConfig.getRenewalPercentThreshold()
 public double getRenewalPercentThreshold() {
        return configInstance.getDoubleProperty(this.namespace + "renewalPercentThreshold", 0.85D).get();
    }
    
此时我们预值数值计算 :  当前服务的数量 *60/30* 0.85 

    
                   100*2*0.85
Renews threshold   170    
Renews (last min)  50  
服务保护模式的存在必要性

因为同时保留"好数据""坏数据"总比丢掉任何数据要更好,当网络故障恢复后,Eureka Server会退出"自我保护模式"Eureka还有客户端缓存功能(也就是微服务的缓存功能)。即便Eureka Server集群中所有节点都宕机失效,微服务的ProviderConsumer都能正常通信。

微服务的负载均衡策略会自动剔除死亡的微服务节点(Robbin)。

只要Consumer不关闭,缓存始终有效,直到一个应用下的所有Provider访问都无效的时候,才会访问Eureka Server重新获取服务列表。

3.2.5 EUREKA集群搭建

我们 的 eureka的集群搭建 其实就是 写好一个 注册中心 然后将其部署到 多态服务器中 就完成了集群的搭建

我们因为节约时间,所以搭建一个伪集群 (正规集群是 一个项目 部署到多态服务器上 , 伪集群 一台服务器上跑多个项目 )


server:
  port: 7001    #端口号

eureka:
  instance:
    hostname: eureka7001        #主机名称
  client:
    registerWithEureka: false   #注册 询问是否当前服务 要注册到注册中心 此时选择false 因为我们自己就是注册中心
    fetchRegistry: false        #发现  询问是否发现注册中心的服务 此时也是false  因为我们自己就是注册中心
    serviceUrl:                 #服务地址  集群其实就是注册中心互相注册   7001  7002  7003
      defaultZone: http://eureka131:7002/eureka,http://eureka132:7003/eureka


注意:defaultZone :  http://eureka7002:7002/eureka     
链接地址后面要加 /eureka  不然服务注册时可能出现问题!!!!!!!!!!!!!!!!!!!

3.2.9 项目打包

在总项目中添加编译插件
<build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>


在服务的pom中添加 打包插件
 <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

将整个项目进行install  即可



3.2.6 注册中心的CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中,

Consistency(数据一致性)、

Availability(服务可用性)、

Partition tolerance(分区容错性),三者不可兼得。

CAP由Eric Brewer2000年PODC会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的CAP定理。
img
EurekaZooKeeper的特性  nacos

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1GQGzAbP-1666438471242)(assets/wps6.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dc7O8Ai-1666438471243)(assets/image-20210802222133298.png)]

3.2.7 服务注册

A 项目结构
song_cloud 
├── song-register        // 注册中心 [7001]
├── song-gateway         // 网关模块 [8080]
├── song-auth            // 认证中心 [9200]
├── song-api             // 接口模块
│       └── song-api-system                          // 系统接口
├── song-common          // 通用模块
│       └── song-common-core                         // 核心模块
│       └── song-common-datascope                    // 权限范围
│       └── song-common-datasource                   // 多数据源
│       └── song-common-log                          // 日志记录
│       └── song-common-redis                        // 缓存服务
│       └── song-common-security                     // 安全模块
│       └── song-common-swagger                      // 系统接口
├── song-modules         // 业务模块
│       └── song-system                              // 系统模块 [9201]
│       └── song-job                                 // 定时任务 [9203]
│       └── song-file                                // 文件服务 [9300]
├── song-visual          // 图形化管理模块
│       └── song-visual-monitor                      // 监控中心 [9100]
├──pom.xml                // 公共依赖

B 创建服务并且注册到eureka
    
 ① 创建一个二级父项目 song-modules
 ② 在二级项目中创建一个song-system项目
 ③ 在项目中添加依赖
   <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
  ④ 创建启动类
    @SpringBootApplication
    @EnableEurekaClient
    public class System9201
    {
          public static void main( String[] args )
         {
              SpringApplication.run(System9210.class,args);
         }
    }
    ⑤ 添加配置文件 application.yml
        
server:
  port: 9201

spring:
  application:
    name: song_system

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001:7001/eureka/, http://eureka7002:7002/eureka/, http://eureka7003:7003/eureka/
  instance:
    instance-id: haha
    prefer-ip-address: true

3.2.8 common项目

A  创建song-common二级项目
B  在song-common二级项目中创建song-common-core项目,从来编写其他服务所需要的 工具类 、自定义异常、等内容
@Data
public class Result<E>  implements Serializable {

    private int  code;
    private String msg;
    private E  data;


    public Result() { }

    public Result(int code, String msg, E data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


    public static Result sucess(){
        return  new Result(1,"操作成功",null);
    }
    public static Result sucess(Object  data){
        return  new Result(1,"操作成功",data);
    }

    public static Result fail(){
        return  new Result(2,"操作失败",null);
    }
    public static Result fail(String  msg){
        return  new Result(2,msg,null);
    }


}
C  在微服务项目中 导入common的依赖
D  写接口并且完成POSTMan调用

3.3 服务调用

3.3.1 调用方式

顾名思义,就是服务之间的接口互相调用,在微服务架构中很多功能都需要调用多个服务才能完成某一项功能。


同步方式: Resttemplate(spring中原有的)       Feign (基于 ribbon)

异步方式: MQ传播

3.3.2 Ribbon

Ribbon是一种客户端负载平衡器,可让您对HTTP和TCP客户端的行为进行大量控制(我们java中操作http或者tcp或者UDP使用的是socket,但是太满烦了。ribbon基于RestTemplate  并且添加了负载均衡策越 让我们方便优雅的去远程调用)。

Ribbon中的中心概念是指定客户的概念。每个负载均衡器都是组件的一部分,这些组件可以一起工作以按需联系远程服务器,并且该组件具有您作为应用程序开发人员提供的名称(例如,使用`@FeignClient`批注)。根据需要,Spring Cloud通过使用`RibbonClientConfiguration`为每个命名的客户端创建一个新的集合作为`ApplicationContext`。其中包含`ILoadBalancer`,`RestClient`和`ServerListFilter`。
    
Spring Cloud既然把Netflix OSS套件大刀阔斧的砍掉了,那总归得有替代方案吧。那是必然的,Spring Cloud团队给我们推荐了用于替代的产品:  
Netflix推荐替代品说明
HystrixResilience4jHystrix自己也推荐你使用它代替自己
Hystrix Dashboard / TurbineMicrometer + Monitoring System说白了,监控这件事交给更专业的组件去做
RibbonSpring Cloud Loadbalancer忍不住了,Spring终究亲自出手
Zuul 1Spring Cloud Gateway忍不住了,Spring终究亲自出手
Archaius 1Spring Boot外部化配置 + Spring Cloud配置比Netflix实现的更好、更强大
以上替代品中,你可能最陌生、最好奇的是Spring Cloud Loadbalancer,它一度只是Spring Cloud 孵化器里的一个小项目,并且一度搁浅。后再经过重启,发展,现行使其伟大使命,正式用于完全替换 Ribbon,成为Spring Cloud负载均衡器唯一实现。

值得注意的是:Spring Cloud LoadBalancer首次引入是在Spring Cloud Commons 2.2.0时,也就是Hoxton发布时就引入了,只不过那会还只是备胎/备选,默认依旧是Ribbon挑大梁。负载均衡抽象LoadBalancerClient接口有两个实现,而到了Spring Cloud 2020.0版本后,BlockingLoadBalancerClient就是唯一实现了。

3.3.3 Restemplate配合ribbon远程调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jJVgfcUP-1666616892686)(assets/image-20210804005400430.png)]

A . 在注册中心中注册两个服务
B . 导入jar包
   以前是:
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>
    现在是:
    <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon-loadbalancer</artifactId>
            <version>2.3.0</version>
     </dependency>
    
C . 创建一个config 配置ribbon的负责均衡和Restemplate 实例
@Configuration
public class RibbonConfig {

    @Bean
    @LoadBalanced
    public RestTemplate  restTemplate(){
        return  new RestTemplate();
    }

    /**
     * 配置负载均衡策略
     * @return
     */
    @Bean
    public IRule ribbonRule() {
        return new RoundRobinRule();
    }
}  
负载均衡策略
1RoundRobinRule轮询,依次执行每个执行一次(默认)
2RandomRule  随机
3AvailabilityFilteringRule
    1、会先过滤掉多次访问故障而处于断路器跳闸状态的服务
    2、和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问
4WeightedResponseTimeRule
   1、根据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。
   2、刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule
5RetryRule
    1、先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败侧在指定的时间会进行重试,进行获取可用的服务
    2、如多次获取某个服务失败,这不会再再次获取该服务如(高德地图上某条道路堵车,司机不会走那条道路)
6BestAvailableRule
    会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
7ZoneAvoidanceRule
   默认规则,复合判断Server所在区域的性能和Server的可用性选择服务器



    
D .在每个服务中创建两个接口  在其中一个接口对应的service中模拟对另一个接口的远程调用

@Service
public class StudentService {

    @Autowired
    private RestTemplate  temp;

     public Result listAll(){
        
           //  duboo  tcp   cloud  http   注册中心作用 注意 微服务注册到注册中心的名字不能有下划线     http://song-test/test/haha
        Result result = temp.getForObject("http://song-test/test/haha", Result.class);
        
         return  Result.success(result);
     }
}


3.3.4 OpenFeign

Feign
FeignSpring Cloud Netflix组件中的一量级Restful的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架,封装了RibbonRestTemplate, 实现了WebService的面向接口编程,进一步降低了项目的耦合度。
    
    
    
为什么要使用Feign
Feign 旨在使编写 JAVA HTTP 客户端变得更加简单,Feign 简化了RestTemplate代码,实现了Ribbon负载均衡,使代码变得更加简洁,也少了客户端调用的代码,使用 Feign 实现负载均衡是首选方案,只需要你创建一个接口,然后在上面添加注解即可。
    
Feign 是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。让开发者调用远程接口就跟调用本地方法一样的体验,开发者完全无感知这是远程方法,无需关注与远程的交互细节,更无需关注分布式环境开发。    
    

Feign vs OpenFeign
Feign 内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务。
Feign 支持的注解和用法参考官方文档:https://github.com/OpenFeign/feign官方文档,使用 Feign 的注解定义接口,然后调用这个接口,就可以调用服务注册中心的服务。

Feign本身并不支持Spring MVC的注解,它有一套自己的注解,为了更方便的使用Spring Cloud孵化了OpenFeign。并且支持了Spring MVC的注解,如@RequestMapping@PathVariable等等。
OpenFeign@FeignClient可以解析Spring MVC的@RequestMapping注解下的接口,并通过动态代理方式产生实现类,实现类中做负载均衡调用服务    

3.3.5 OpenFeign使用

1 添加依赖
  <!-- spring cloud openfeign -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2 创建接口(value可以设置成常量)
@FeignClient(contextId = "test", value = "song-test")
public interface TestService
{
   
    @GetMapping(value = "/test/haha")
    Result getHaha();
}


3 调用
    
@Service
public class StudentServiceImpl  implements StudentService {

    @Autowired
    private TestService service;


    @Override
    public Result listStu() {
        
        Result  result = service.getHaha();
        return Result.sucess(result);
        
    }
}

4 启动类添加注册@EnableFeignClients


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yjHPpIOz-1666438471243)(assets/image-20210804012536469.png)]

3.3.6 OpenFeign传参

1 在song_common 中添加一个 com.aaa.entity.Student
2 在song_test中的 TestController中添加一个接口
    
  @PostMapping("add")
    public Result  add(@RequestBody Student  student){

        return   Result.sucess("添加"+student.getName()+"成功");
    }

3 在song_system中的TestService中添加对应的方法
    
@FeignClient(contextId = "test", value = "song-test")
public interface TestService
{
   
    @GetMapping(value = "/test/haha")
    Result getHaha();

    @PostMapping(value = "/test/add")
    Result addStudent(@RequestBody Student  s);
}

4 调用并传参

@Service
public class StudentServiceImpl  implements StudentService {

    @Autowired
    private TestService service;

    @Override
    public Result listStu() {

        Student student = new Student();
        student.setId(1);
        student.setName("张三");

        Result  result = service.addStudent(student);
        return Result.sucess(result);

    }
}

5 install项目并且重新运行与访问system

3.3.7 性能优化之GZip压缩

gzip是一种数据格式,采用deflate算法压缩数据。gzip大约可以帮我们减少70%以上的文件大小。
开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点
    
配置开启GZIP
server:
  port: 9201
  compression:
    # 是否开启压缩
    enabled: true
    # 配置支持压缩的 MIME TYPE
    mime-types: text/html,text/xml,text/plain,application/xml,application/json
feign:
  compression:
    request:
      # 开启请求压缩
      enabled: true
      # 配置压缩支持的 MIME TYPE
      mime-types: text/xml,application/xml,application/json
      # 配置压缩数据大小的下限
      min-request-size: 2048
    response:
      # 开启响应压缩
      enabled: true
spring:
  application:
    name: song-system

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001:7001/eureka/,http://eureka7002:7002/eureka/,http://eureka7003:7003/eureka/
  instance:
    instance-id: haha
    prefer-ip-address: true

3.3.8 性能优化之Http连接池


两台服务器建立HTTP连接的过程涉及到多个数据包的交换,很消耗时间。采用HTTP连接池可以节约大量的时间提示吞吐量。

Feign的HTTP客户端支持3种框架:HttpURLConnection、HttpClient、OkHttp。

默认是采用java.net.HttpURLConnection,每次请求都会建立、关闭连接,为了性能考虑,可以引入httpclient、okhttp作为底层的通信框架。

HttpURLConnection是JDK自带的HTTP客户端技术,并不支持连接池,如果要实现连接池的机制,还需要自己来管理连接对象。对于网络请求这种底层相对复杂的操作,如果有可用的其他方案,也没有必要自己去管理连接对象。

Apache提供的HttpClient框架相比传统JDK自带的HttpURLConnection,它封装了访问http的请求头,参数,内容体,响应等等;它不仅使客户端发送HTTP请求变得容易,而且也方便了开发人员测试接口(基于Http协议的),即提高了开发的效率,也方便提高代码的健壮性;另外高并发大量的请求网络的时候,还是用“HTTP连接池”提升吞吐量。

OKHttp是一个处理网络请求的开源项目,是安卓端最火热的轻量级框架。OKHttp拥有共享Socket,减少对服务器的请求次数,通过连接池,减少了请求延迟等技术特点。


例如将Feign的HTTP客户端工具修改为HttpClient。


1、添加依赖

<!-- feign httpclient -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

2、全局配置

feign:
  httpclient:
    # 开启httpclient
    enabled: true

3.3.9 性能优化之请求日志

日志配置
浏览器发起的请求可以通过F12查看请求和响应信息。如果想看微服务中每个接口我们可以使用日志配置方式进行查看详细信息。

配置文件logback.xml设置com.aaa日志级别为debug
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志存放路径 -->
	<property name="log.path" value="logs/song_system" />
   <!-- 日志输出格式 -->
	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

    <!-- 控制台输出 -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>${log.pattern}</pattern>
		</encoder>
	</appender>

    <!-- 系统日志输出 -->
	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
	    <file>${log.path}/info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
			<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
			<!-- 日志最大的历史 60-->
			<maxHistory>60</maxHistory>
		</rollingPolicy>
		<encoder>
			<pattern>${log.pattern}</pattern>
		</encoder>
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
	</appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
	    <file>${log.path}/error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
			<!-- 日志最大的历史 60-->
			<maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
			<!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
			<!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 系统模块日志级别控制  -->
	<logger name="com.aaa" level="debug" />
	<!-- Spring日志级别控制  -->
	<logger name="org.springframework" level="warn" />

	<root level="info">
		<appender-ref ref="console" />
	</root>
	
	<!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
</configuration>    
    
    

全局配置

@Bean
public Logger.Level getLog()
{
	return Logger.Level.FULL;
}

局部配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;

/**
 * Feign 客户端配置
 */
@Configuration
public class FeignConfiguration
{
    @Bean
    Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}

// ====== 在客户端接口指定此配置 ======

@FeignClient(contextId = "test", value = "song-test", configuration = FeignConfiguration.class)
public interface TestService
{
   
    @GetMapping(value = "/test/haha")
    Result getHaha();

    @PostMapping(value = "/test/add")
    Result addStudent(@RequestBody Student  s);
}

3.3.10 请求超时

Feign的负载均衡底层用的就是`Ribbon`,所以请求超时其实就只需要配置`Ribbon`参数。

# 全局配置 请求处理的超时时间
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000
  
#  局部配置 song-xxxx 为需要调用的服务名称
song-xxxx:
  ribbon:
    ReadTimeout: 10000
    ConnectTimeout: 10000

3.3.11 请求拦截器

在微服务应用中,通过feign的方式实现http的调用,可以通过实现feign.RequestInterceptor接口在feign执行后进行拦截,对请求头等信息进行修改。

例如项目中利用feign拦截器将本服务的token令牌传递给下游服务
    
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
    @Override
    public void apply(RequestTemplate requestTemplate)
    { 
         requestTemplate.header("token","123456");  
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0fbuWeK-1666438471245)(assets/image-20210804020242017.png)]

3.3.12 RemoteAPI远程API项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RkjhjubA-1666438471246)(assets/image-20210804021026263.png)]

Feign调用的程序一般都是公共的访问接口 ,肯定多个微服务都要调用 例如 权限验证等, 所以将feign的接口类放到 
song_api项目中

3.3.13 feign的原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10auircX-1666438471247)(assets/6271376-7635e2dc9b32e3ec.png)]

3.4 注册配置中心之Nacos

参考:bat 脚本执行cmd命令。(启动nacos)
https://blog.csdn.net/w1234567465/article/details/126336876
在这里插入图片描述

start cmd /k " cd /d D:\Users\33154\Desktop\app\nacos\bin && startup.cmd -m standalone"

3.4.1 nacos简介

nacos的官方网站: https://nacos.io/zh-cn/

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:

Kubernetes Service

gRPC & Dubbo RPC Service

Spring Cloud RESTful Service

Nacos 的关键特性包括:

    服务发现和服务健康监测

    Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。

    Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

    动态配置服务

    动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

    动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

    配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

    Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

    动态 DNS 服务

    动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。

    Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

    服务及其元数据管理

    Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。


3.4.2 nacos搭建


0.版本选择

您可以在Nacos的release notes及博客中找到每个版本支持的功能的介绍,当前推荐的稳定版本为2.0.31.预备环境准备

Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用:

    64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac64 bit JDK 1.8+;下载 & 配置。
    Maven 3.2.x+;下载 & 配置。

2.下载源码或者安装包

3.配置数据库MySQL0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:

1.安装数据库,版本要求:5.6.5+
2.初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow  
                            
4.启动服务器

Windows
启动命令(standalone代表着单机模式运行,非集群模式):  startup.cmd -m standalone
              
此时就可以通过  http://localhost:8848/nacos/index.html 
访问,账号密码默认是 nacos

3.4.3 nacos注册中心-服务注册

0 在总的pom中添加依赖

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-boot.version>2.5.1</spring-boot.version>
    <spring-cloud.version>2020.0.3</spring-cloud.version>
    <spring-cloud.alibaba.version>2021.1</spring-cloud.alibaba.version>
  </properties>

  <dependencyManagement>

    <dependencies>
      <!-- SpringCloud 微服务 -->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <!-- SpringCloud Alibaba 微服务 -->
        <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-alibaba-dependencies</artifactId>
          <version>${spring-cloud.alibaba.version}</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>

      <!-- SpringBoot 依赖配置 -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>com.aaa</groupId>
        <artifactId>song_common_core</artifactId>
        <version>1.0-SNAPSHOT</version>
      </dependency>

      <dependency>
        <groupId>com.aaa</groupId>
        <artifactId>song_api_test</artifactId>
        <version>1.0-SNAPSHOT</version>
      </dependency>
      
    </dependencies>

  </dependencyManagement>


1、在微服务项目中添加依赖

<!-- springcloud alibaba nacos discovery -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- SpringBoot Web -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、添加Nacos配置

# Spring
spring: 
  application:
    # 应用名称
    name: song-system
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
        
3、配置启动类 @EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class System9201
{
    @Bean
    public Logger.Level getLog()
    {
        return Logger.Level.FULL;
    }

    public static void main( String[] args )
    {
        SpringApplication.run(System9201.class,args);
    }
}

4  启动nacos
5  启动app启动类

3.4.4 nacos注册中心-服务发现与调用

因为我们使用了 nacos作为注册中心  ,此时项目中必须有  spring-cloud-starter-loadbalancer 这个包。    

song_system  导入 <artifactId>song_api_test</artifactId> 
在song_api_test 导入了 <artifactId>spring-cloud-starter-openfeign</artifactId>
在spring-cloud-starter-openfeign 中导入了
 <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
      <version>3.0.3</version>
      <scope>compile</scope>
      <optional>true</optional>
    </dependency>
此时我们发现 spring-cloud-starter-loadbalancer 是  <optional>true</optional> 此时就不会传递依赖

此时我们需要自己单独导一个spring-cloud-starter-loadbalancer 包即可
     <dependency>
     <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringCloud Loadbalancer -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

3.4.5 nacos配置中心

- 什么是配置中心

在微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余,如下图:

config

总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。
- 为什么要使用配置中心
配置中心将配置从各应用中剥离出来,对配置进行统一管理,应用自身不需要自己去管理配置。
    
- Nacos 配置中心
`Nacos`是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
    
配置中心的服务流程如下:
1、用户在配置中心更新配置信息。
2、服务A和服务B及时得到配置更新通知,从配置中心获取配置。

config

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcRn5hrG-1666438471249)(assets/image-20210804161237763.png)]

3.4.6 nacos搭建配置中心

1 导入nacos配置中心依赖的jar包

<!-- springcloud alibaba nacos config -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2  在bootstrap.yml添加Nacos配置
    
配置文件加载的优先级(由高到低)
bootstrap.properties ->bootstrap.yml -> application.properties -> application.yml
        
# Tomcat
server:
  port: 9201

# Spring
spring:
  application:
    # 应用名称
    name: song-system
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址    我们当前的微服务一定要注册到 nacos注册中心的 时候 才能使用nacos其他的操作 例如 配置中心
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址      prefix-active.file-extension    私有配置文件    song-system-dev.yml
        server-addr: 127.0.0.1:8848
        # 配置文件前缀
        prefix: ${spring.application.name}
        # 配置文件格式
        file-extension: yml
        # 共享配置      公共配置文件的名字为 :    application-dev.yml   也就是说  nacos中一定有一个配置文件叫做 application-dev.yml
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

3 项目pom中添加依赖   cloud 2020.x.x 版本以后需要添加
    <dependencies>
        <!-- bootstrap 启动器 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>


4 在nacos中- 配置管理 -配置列表-新建配置
 Data Id : 在Nacos Spring Cloud 中,数据集 的配置完整格式如下:
${spring.cloud.nacos.config.prefix}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            通俗一点就是前缀-环境.扩展名
            
 创建连个配置  application-dev.yml  共享配置     song-system-dev.yml 私服配置
               
            
            

5 在controller 中读取这个配置

springboot 读取配置文件信息的 四种方式

  
@RestController
@RequestMapping("stu")
public class StudentController {


    @Value("${haha}")
    private String  haha;


    @Autowired
    private StudentService  service;

    @GetMapping("list")
    public Result   list(){

        System.out.println(haha);

        return  service.listStu();
    }

}

3.4.7 动态刷新

通常会在Controller里边用@Value取出使用,但是你要是想改变他,就要重新改代码,打包,部署,十分麻烦,我们需要让配置文件的值变得动起来,Nacos也采用了Spring Cloud原生注解@RefreshScope实现配置自动更新。

@RefreshScope //动态刷新配置
@RestController
@RequestMapping("stu")
public class StudentController {


    @Value("${haha}")
    private String  haha;

    @Autowired
    private StudentService  service;

    @GetMapping("list")
    public Result   list(){

        System.out.println(haha);

        return  service.listStu();
    }

}
这个操作只能用于 通过 @value 注入的信息 在 controller中 进行刷新

    如果想实现 配置信息动态刷新 例如 MySQL  redis 需要写脚本  或者使用 阿波罗

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44Qw5HtH-1666438471250)(assets/image-20210804164739289.png)]

3.5 熔断器

3.5.1 服务雪崩

  多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的”扇出”,如扇出的链路上某个微服务的调用响应式过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”。

服务雪崩效应是一种因“服务提供者的不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象。
     
1  扇入  扇出
2  雪崩效应
     

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EArorKk9-1666438471250)(assets/image-20210804221702512.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K22To8Gb-1666438471250)(assets/image-20210804222224256.png)]

3.5.2 雪崩产生的原因

A  硬件故障
B  程序Bug
C  缓存击穿 

   我们如果在查询数据库比较多的时候,会添加redis二级缓存,此时如果有查询操作 先走缓存。如果项目运行很长时间,大量的数据存储到缓存中,突然redis宕机了,所有的请求在缓存中拿不到数据,此时全部走数据库,导致数据库无法承受大量请求,整个项目崩溃。
    
D  用户大量请求

3.5.3 解决方案

A 应用扩容: 增加机器数量  升级硬件
B 换一批能力更强的程序员 避免bug
C 缓存预加载  同步改为异步刷新

D  隔离 :将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务

E  限流:  限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回或者等待排队,不再调用后续资源

F 关闭重试机制

G  服务降级:当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。
    
    游戏活动的时候    打排位 打匹配  可以送礼品  此时大量用户都来玩游戏 。  此时就将游戏中一些无关紧要的 服务先关掉 
                    例如 自定义房间  各种花哨模式   
     双十一   可以抢购  可以下单  但是不能查物流

H  服务熔断 :  如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用  (设置兜底函数 fallback)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCMqVKKH-1666438471251)(assets/image-20210804225226090.png)]

3.5.4 断路器Hystrix豪猪

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyK9joJl-1666438471251)(assets/image-20210111084123411.png)]

hystrix是Netlifx开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控(Hystrix Dashboard)等功能
    
    
A    添加pom依赖
     <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>  
   因为最新版的cloud已经废弃 hystrix  所以需要添加版本号
B    在api-test中添加 fallback 兜底
   
@FeignClient(contextId = "test", value = "song-test",fallbackFactory = RemoteTestFallbackFactory.class)
public interface RemoteTestService {


    @GetMapping("/test/haha")
    Result getHahahahahahaha();


    @PostMapping("/dog/save")
    Result addDog(@RequestBody Dog dog);

} 
    
C  创建  RemoteTestFallbackFactory 这个类  并且实现 FallbackFactory接口   
    
@Component
public class RemoteTestFallbackFactory implements FallbackFactory<RemoteTestService>
{
    private static final Logger log = LoggerFactory.getLogger(RemoteTestFallbackFactory.class);
    
    @Override
    public RemoteTestService create(Throwable cause) {

        log.info(  "服务调用出现问题 :"+  cause.getLocalizedMessage() );


        return new RemoteTestService() {
            @Override
            public Result getHahahahahahaha() {
                return Result.fail("对不起 服务不可用  稍等片刻  已经熔断了");
            }

            @Override
            public Result addDog(Dog dog) {
                return Result.fail("对不起 添加:"+dog.getName()+"失败 服务不可用 熔断了 ");
            }
        };
    }
}  
    
D  nacos添加配置   开启熔断机制 
 feign:
  circuitbreaker:
    enabled: true       

3.5.5 sentinel 简介

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。`Sentinel`是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。

sentinel具有以下特征:(限流、熔断、削峰填谷、监控)

**丰富的应用场景:** Sentinel承接了阿里巴巴近十年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围),消息削峰填谷,集群流量控制,实时熔断下游不可用应用等

**完美的实时监控:** Sentinel同时提供实时的监控功能,您可以在控制台看到接入应用的单台机器秒级数据,甚至500台一下规模的集群的汇总运行情况

**广泛的开源生态:** Sentinel提供开箱即用的与其他框架/库的整合模块,例如与SpringCloud,Dubbo,gRPC的整合,您只需要引入相应的依赖并进行简单的配置即可快速接入Sentinel

**完美的SPI扩展点:** Sentinel提供简单易用的,完美的SPI扩展接口,可以通过实现扩展接口来快速定制逻辑,例如定制规则管理,适配动态数据源等
    
- 核心概念

`sentinel`的使用可以分为两个部分

**核心库**不依赖任何框架/库,同时对DubboSpringCloud等框架也有比较好的支持。
**控制台**主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
    
下面是`sentinel`的架构图: 

sentinel

3.5.6 sentinel 下载与启用

注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
    
Windows平台安装包下载
可以从https://github.com/alibaba/Sentinel/releases
下载sentinel-dashboard-$version.jar包。

使用如下命令启动控制台:

java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -Dcsp.sentinel.api.port=8719 -jar  sentinel-dashboard-1.8.2.jar
    
其中-Dserver.port=8718用于指定Sentinel控制台端口为8718
    
Sentinel提供了一个可视化的操作平台,安装好之后,在浏览器中输入(http://localhost:8718 (opens new window))就可以访问了,默认的用户名和密码都是sentinel    
    

3.5.7 sentinel与cloud微服务整合

1 添加依赖

<!-- springcloud alibaba sentinel -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>


2 添加Sentinel配置

spring: 
  application:
    # 应用名称
    name: song_system 
  cloud:
    sentinel:
      # 取消控制台懒加载  我们第一次访问的时候 sentinel才会去对我们的项目进行检测 
      eager: true
      transport:
        # 控制台地址
        dashboard: 127.0.0.1:8718

3.5.8 sentinel对OpenFeign 支持

其实不管是Hystrix还是Sentinel对于Feign的支持,核心代码基本上是一致的,只需要修改依赖和配置文件即可。
		
feign:
  sentinel:
    enabled: true

3.5.9 sentinel定义资源

资源是Sentinel中的核心概念之一。我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。最常用的资源是我们代码中的Java方法。Sentinel提供了@SentinelResource注解用于定义资源,并提供了AspectJ的扩展用于自动定义资源、处理BlockException等。

官网文档:https://github.com/alibaba/Sentinel/wiki/
如何使用#定义资源

#代码定义
@SentinelResource用于定义资源,并提供可选的异常处理和fallback配置项。
    
@Service
public class StudentServiceImpl  implements StudentService {


    @Autowired
    RemoteTestService service;


    @Override
    @SentinelResource(value = "fooHahaha", blockHandler = "listStuBlockHandler", fallback = "listStuFallback")
    public Result listStu() {


        Dog dog = new Dog();
        dog.setId(1);
        dog.setName("旺财");

        Result result = service.addDog(dog);

        return Result.sucess(result);

    }

    public Result listStuBlockHandler(BlockException  e) {
        return Result.sucess("限流了"+e.getMessage());
    }

    public Result listStuFallback(Throwable  t) {
        return Result.sucess("熔断了"+t.getMessage());
    }

}
//注意:返回值类型要保持一致,否则将无法达到预期效果!

属性说明

@SentinelResource注解包含以下属性:

参数描述
value资源名称,必需项(不能为空)
entryType资源调用方向,可选项(默认为EntryType.OUT
resourceType资源的分类
blockHandler对应处理BlockException的函数名称
blockHandlerClass处理类的Class对象,函数必需为static函数
fallback用于在抛出异常的时候提供fallback处理逻辑
defaultFallback用作默认的回退的方法
fallbackClass异常类的Class对象,函数必需为static函数
exceptionsToTrace异常类跟踪列表(默认为Throwable.class)
exceptionsToIgnore排除掉的异常类型

提示

注意:注解方式埋点不支持 private 方法。

3.5.10 sentinel 控制

A 流控
资源名: 唯一名称,默认请求路径
针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
    
阈值类型/单机阈值:
     QPS(每秒请求数量):当调用该api的QPS达到阈值的时候,进行限流
     线程数:当调用该api的线程数达到阈值的时候,进行限流
是否集群: 不需要集群
流控模式:
   直接:api达到限流条件时,直接限流
   关联:当关联的资源达到限流阈值时,就限流自己
   链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到峰值,就进行限流)【api级别的针对来源】
    
流控效果:
    快速失败:直接失败,抛异常
    Warm Up:根据coldFactor(冷加载因子,默认3)的值,从阈值/coldFactor,经过预热时长,才达到设置的QPS阈值
    排队等待:匀速排队,让请求以匀速通过,阈值类型必须设置为QPS,否则无效

    
B 熔断   
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7G9HzmMA-1666438471253)(assets/image-20210806013905739.png)]

3.5.11 动态规则

规则配置,都是存在内存中的。即如果应用重启,这个规则就会失效,可以整合动态配置系统,如ZooKeeper、Nacos、Apollo等,动态地实时刷新配置规则。

控制台   java代码    配置文件


当sentinel重新启动时,sentinel dashboard中原来的数据将会全部消失,这样就需要重新定义限流规则,无疑是不可取的。所以需要将sentinel中定义的限流规则保存到配置中心里面。 

1、在nacos中定义自定义限流策略sentinel-song-system
[
  {
    "resource": "fooHahaha",
    "count": 2,
    "grade": 1,
    "limitApp": "default",
    "strategy": 0,
    "controlBehavior": 0
  }
]
2、添加依赖

<!-- sentinel datasource nacos -->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

3、添加相关配置,sentinel下面的dataSource中配置nacos

spring: 
    sentinel:
      # 取消控制台懒加载
      eager: true
      transport:
        # 控制台地址
        dashboard: 127.0.0.1:8718
      # nacos配置持久化
      datasource:
        ds1:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: sentinel-song-system
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
            
4、启动sentinel应用,可以看到我们在nacos中配置的限流规则

流量控制规则(FlowRule)重要属性

参数描述描述
resource资源名,资源名是限流规则的作用对象
limitApp流控针对的调用来源,若为 default 则不区分调用来源default,代表不区分调用来源
grade限流阈值类型,QPS 模式(1)或并发线程数模式(0)QPS 模式
count限流阈值
strategy调用关系限流策略:直接、链路、关联根据资源本身(直接)
controlBehavior流量控制效果(直接拒绝、Warm Up、匀速排队)直接拒绝
clusterMode是否集群限流

熔断降级规则(DegradeRule)重要属性

参数描述描述
resource资源名,即规则的作用对象
grade熔断策略,支持慢调用比例/异常比例/异常数策略慢调用比例
count慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow熔断时长,单位为 s
minRequestAmount熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入)5
statIntervalMs统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入)1000 ms
slowRatioThreshold慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

同一个资源可以同时有多个降级规则。

什么是QPS:

QPS是每秒钟处理完请求的次数。这里的请求不是指一个查询或者数据库查询,是包括一个业务逻辑的整个流程,也就是说每秒钟响应的请求次数。
什么是响应时间:

响应时间即RT,处理一次请求所需要的平均处理时间。对于RT,客户端和服务端是大不相同的,因为请求从客户端到服务端,需要经过广域网,所以客户端RT往往远大于服务端RT,同时客户端的RT往往决定着用户的真实体验,服务端RT往往是评估我们系统好坏的一个关键因素。
最佳线程数的困扰:

在开发过程中,我们一定面临过很多的线程数量的配置问题,这种问题往往让人摸不到头脑,往往都是拍脑袋给出一个线程池的数量,但这可能恰恰是不靠谱的,过小的话会导致请求RT极具增加,过大也一样RT也会升高。所以对于最佳线程数的评估往往比较麻烦。
QPS和RT的关系:

单线程场景:

假设我们的服务端只有一个线程,那么所有的请求都是串行执行,我们可以很简单的算出系统的QPS,也就是:QPS = 1000ms/RT。假设一个RT过程中CPU计算的时间为49ms,CPU Wait Time 为200ms,那么QPS就为1000/49+200 = 4.01。

多线程场景

我们接下来把服务端的线程数提升到2,那么整个系统的QPS则为:2 *(1000/49+200)=8.02。可见QPS随着线程的增加而线性增长,那QPS上不去就加线程呗,听起来很有道理,公式也说得通,但是往往现实并非如此,后面会聊这个问题。
最佳线程数?

从上面单线程场景来看,CPU Wait time为200ms,你可以理解为CPU这段时间什么都没做,是空闲的,显然我们没把CPU利用起来,这时候我们需要启多个线程去响应请求,把这部分利用起来,那么启动多少个线程呢?我们可以估算一下 空闲时间200ms,我们要把这部分时间转换为CPU Time,那么就是200+49/49 = 5.08个,不考虑上下文切换的话,约等于5个线程。同时还要考虑CPU的核心数和利用率问题,那么我们得到了最佳线程数计算的公式:RT/CPU Time * coreSize * cupRatio
最大QPS?

得到了最大的线程数和QPS的计算方式:

QPS = Thread num * 单线程QPS = (CPU Time + CPU Wait Time)/CPU Time * coreSize * CupRatio * (1000ms/(CPU Time + CPU Wait Time)) = 1000ms/(CPU Time) * coreSize * cpuRatio

所以决定一个系统最大的QPS的因素是CPU Time、CoreSize和CPU利用率。看似增加CPU核数(或者说线程数)可以成倍的增加系统QPS,但实际上增加线程数的同时也增加了很大的系统负荷,更多的上下文切换,QPS和最大的QPS是有偏差的。
CPU Time & CPU Wait Time & CPU 利用率

CPU Time就是一次请求中,实际用到计算资源。CPU Time的消耗是全流程的,涉及到请求到应用服务器,再从应用服务器返回的全过程。实际上这取决于你的计算的复杂度。

CPU Wait Time是一次请求过程中对于IO的操作,CPU这段时间可以理解为空闲的,那么此时要尽量利用这些空闲时间,也就是增加线程数。

CPU 利用率是业务系统利用到CPU的比率,因为往往一个系统上会有一些其他的线程,这些线程会和CPU竞争计算资源,那么此时留给业务的计算资源比例就会下降,典型的像,GC线程的GC过程、锁的竞争过程都是消耗CPU的过程。甚至一些IO的瓶颈,也会导致CPU利用率下降(CPU都在Wait IO,利用率当然不高)。
增加CPU核数对QPS的提升

从上面的公式我们可以看出,假设CPU Time和CPU 利用率不变,增加CPU的核数能使QPS呈线性增长。但是很遗憾,现实中不是这样的....首先先看一下阿姆达尔定律:

阿姆达尔定律.png

阿姆达尔定律是一个很有意思的定律,简单的我们可以理解为,程序中可并行代码的比例决定你增加处理器(总核心数)所能带来的速度提升的上限
。换句话说就是串行化对于你系统吞吐量的影响。举个栗子:

1.坐车问题:

假设你想从望京去顺义,那么你智能坐着一辆车过去,虽然现在有十辆车,你也不能提升十倍的效率,这里F就是1,因为所有的动作都需要串行,speedup就等于1,效率没提升,虽然你有九辆车。

2.写代码问题:

假设你现在开发一个系统,你可以把所有的任务均分下去,假设10个人帮你开发,那么F就为0,N为10,那么speedup等于10,也就是说你提升了10倍的速率。

这里的N就是我们的核数。在F为0的时候可以成倍增加计算效率,但是很遗憾F不为0,同时随着你的请求数的增加,F的值也在增加,当这个串行率达到一定程度的时候,你的系统是没有任何效果的提升的。当F不变的时候,N增加,那么Speedup增加。但是当N->∞,那么整个公式就变成了1/F,也就是说当核数不断增大的时候,speedup是有上限的。
同样,对于1000ms/(CPU Time) * coreSize * cpuRatio我们不断的增加CoreSize或者说线程数的时候。我们的请求变多了,随之而来的就是大量的上下文切换、大量的GC、大量的锁征用,这些串行化的因素会大大增加F值,也会大大的增加CPU Time。假设我们的串行部分不变的话,增大核数,CPU不能得到充分的利用,利用率也会降低。所以,对于阿姆达尔定律而言,串行化的比率才是决定着是否能成倍增长效率的关键。也就是说最佳线程数也好,最大QPS也好,增加内核数量不一定能是系统指标有成倍的增长。更关键的是能改变自己的架构,减小串行的比率,让CPU更充分的利用,达到资源的最大利用率。
再看一下最佳线程数和最大QPS

通过上面一些例子,我们发现当线程数增加的时候,线程的上下文切换会增加,GC Time会增加。这也就导致CPU time 增加,QPS减小,RT也会随着增大。这显然不是我们希望的,我们希望的是在核数一定的情况下找到某个点,使系统的QPS最大,RT相对较小。所以我们需要不断的压测,调整线程池,找到这个QPS的峰值,并且使CPU的利用率达到100%,这样才是系统的最大QPS和最佳线程数。

3.6 Gateway网关

3.6.1 网关简介

什么是服务网关
API Gateway(APIGW / API 网关),顾名思义,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供定制的API。 近几年来移动应用与企业间互联需求的兴起。从以前单一的Web应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。 这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API网关成为了微服务架构的一个标配组件。

为什么要使用网关
微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想 要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多时,这是非常难以记忆的,对 于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识 解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。

Zuul  /   Spring Cloud Gateway
Spring Cloud Gateway是基于Spring生态系统之上构建的API网关,包括:Spring 5.x,Spring Boot 2.x和Project ReactorSpring Cloud Gateway旨在提供一种简单而有效的方法来路由到API,并为它们提供跨领域的关注点,例如:安全性,监视/指标,限流等。
    
    
核心概念
    
路由(Route):路由是网关最基础的部分,路由信息由 ID、目标 URI、一组断言和一组过滤器组成。如果断言 路由为真,则说明请求的 URI 和配置匹配。

断言(Predicate):Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中 的 ServerWebExchangeSpring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任 何信息,比如请求头和参数等。
        
过滤器(Filter):一个标准的 Spring Web FilterSpring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway FilterGlobal Filter。过滤器将会对请求和响应进行处理。
    
总结:目前网关服务,用于路由转发、异常处理、限流、降级、接口、鉴权等等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztjoO8oz-1666438471253)(assets/image-20210807121434261.png)]

3.6.2 网关项目基本使用

1 创建一个二级项目  song_gateway  作为当前微服务系统的 网关

2 在项目中导入依赖

  <dependencies>

        <!-- SpringCloud Gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Sentinel Gateway -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

        <!-- Sentinel Datasource Nacos -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
     

    </dependencies>
3   创建resources/application.yml配置文件

server:
  port: 8080

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
        - id: song-test
          uri: http://localhost:9202/
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1

3.6.3 路由配置:url三种方式

在spring cloud gateway中配置uri有三种方式,包括

#websocket配置方式

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: ws://localhost:9201/
          predicates:
            - Path=/system/**
            
#http地址配置方式
spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**
            
#注册中心配置方式
spring: 
  application:
    name: song-gateway
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: song-system
          uri: lb://song-system
          predicates:
            - Path=/system/**
            
#注意 此时 lb://注册中心的服务名   此时我们需要导入依赖
 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringCloud Loadbalancer -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
            

3.6.4 路由规则: 谓词/断言

参考:Spring Cloud中文网官方文档集合
https://zhuanlan.zhihu.com/p/82536842
Spring Cloud
https://www.springcloud.cc/spring-cloud-greenwich.html#_creating_a_key_store_for_testing

Spring Cloud Gateway创建Route对象时, 使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给RouteSpring Cloud Gateway包含许多内置的Route Predicate Factories。
所有这些断言都匹配 HTTP 请求的不同属性。
多个Route Predicate Factories可以通过逻辑与(and)结合起来一起使用。
路由断言工厂RoutePredicateFactory包含的主要实现类如图所示,包括Datetime、请求的远端地址、路由权重、请求头、Host 地址、请求方法、请求路径和请求参数等类型的路由断言   

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WP8YBMoH-1666438471254)(assets/image-20210112095529061.png)]

#Path
匹配请求路径

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**


#Datetime  before  after  between
匹配日期时间之后发生的请求
spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
       routes:
        # 系统模块
        - id: song-system
          uri: lb://song-system
          predicates:
            - Path=/system/**
            - After=2021-08-07T21:47:00.000+08:00[Asia/Shanghai]
    代表 在北京时间 21:47分 之后 才能访问当前路由        
            
#Cookie
匹配指定名称且其值与正则表达式匹配的cookie

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Cookie=token, 123123
            
测试 curl http://localhost:8080/system/config/1 --cookie "token=123123"

#Header
匹配具有指定名称的请求头,\d+值匹配正则表达式

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Header=token, \d+
#Host
匹配主机名的列表

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Host=**.somehost.org,**.anotherhost.org
#Method
匹配请求methods的参数,它是一个或多个参数

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Method=GET,POST

#Query
匹配查询参数

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Query=username, abc.
            
#RemoteAddr
匹配IP地址和子网掩码

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - RemoteAddr=192.168.10.1/0
#Weight
匹配权重

spring: 
  application:
    name: song-gateway
  cloud:
    gateway:
      routes:
        - id: song-system-a
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 8
        - id: song-system-b
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 2

3.6.5 过滤器

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8P5lh9uP-1666438471255)(assets/image-20210112105335733.png)]

StripPrefix GatewayFilter工厂

StripPrefix GatewayFilter工厂采用一个参数partsparts参数指示在向下游发送请求之前,要从请求中剥离的路径中的零件数。

application.yml。

spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: song-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1

通过网关发送到/system/bar/foo的请求时,对nameservice的请求将类似于http://nameservice/bar/foo

3.6.6 自定义过滤器:黑名单过滤


cloud提供的过滤器 很多时候不满足我们的业务需求 此时我们需要自定义过滤器。
例如:黑名单过滤  BlackListUrlFilter

A 创建一个类 BlackListUrlFilter 继承AbstractGatewayFilterFactory

@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory {

    /**
     * 用来返回我们的自定义过滤器
     * @param config
     * @return
     */
    @Override
    public GatewayFilter apply(Object config) {

        return (exchange,chain)->{

            // 1 获取当前请求的ip地址  我们实际开发中  需要使用 IPUtils类 用来获取请求的真实IP
            String ip = exchange.getRequest().getRemoteAddress().getHostString();

            // 2 判断当前请求的ip是否在黑名单中   黑名单的收集 使用redis
            if( "127.0.0.1".equals(ip) ) {
                // 3 如果是黑名单 则进行拦截
                ServerHttpResponse response = exchange.getResponse();

                response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

                byte[] bytes = JSON.toJSONBytes(  Result.fail("对不起  您的IP禁止访问")   );
                DataBuffer wrap = response.bufferFactory().wrap(bytes);

                Mono<DataBuffer> just = Mono.just(wrap);
                return   response.writeWith(just);

            }

            // 4 如果不在则放行
            return  chain.filter(exchange);
        };
    }

}
B 在需要使用当前拦截器的类中添加配置
 # 系统模块
        - id: song-system
          uri: lb://song-system
          predicates:
            - Path=/system/**
            - Method=GET
          filters:
            - StripPrefix=1
            - BlackListUrlFilter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CfZrYLd-1666438471255)(assets/image-20210808223259859.png)]

3.6.7 全局过滤器:白名单过滤和鉴权过滤

当有请求进入(并与路由匹配)时,过滤Web处理程序会将GlobalFilter的所有实例和GatewayFilter的所有特定于路由的实例添加到过滤器链中。该组合的过滤器链通过org.springframework.core.Ordered接口排序,可以通过实现getOrder()方法进行设置。

全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,
比如权限认证,IP访问限制等等。

单独定义只需要实现GlobalFilter, Ordered这两个接口就可以了。
    
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
    {
         //从请求参数中获取token 用在 app中
        // String token = exchange.getRequest().getQueryParams().getFirst("token");
        // 从请求头中获取 token
        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (null == token)
        {
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
            
            DataBuffer buffer = response.bufferFactory().wrap(JSON.toJSONBytes(Result.fail("对不起 token不能为null")););
            return response.writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder()
    {
        return 0;
    }
}    

在实际业务中 登录验证等请求是 不需要验证token的  所以我们需要配置  白名单 放行
在配置文件中添加白名单
ignore:
  whites:
    - /auth/logout
    - /auth/login
    - /*/v2/api-docs
    - /system/stu/list
创建配置类 读取白名单信息
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "ignore")
public class IgnoreWhiteProperties
{
    /**
     * 放行白名单配置,网关不校验此处的白名单
     */
    private List<String> whites = new ArrayList<>();

    public List<String> getWhites()
    {
        return whites;
    }

    public void setWhites(List<String> whites)
    {
        this.whites = whites;
    }
}



重写java逻辑
    
@Component
public class AuthFilter implements GlobalFilter, Ordered
{

    @Autowired
    private IgnoreWhiteProperties ignoreWhite;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
    {
        String url = exchange.getRequest().getURI().getPath();
        // 跳过不需要验证的路径
        if (ignoreWhite.getWhites().contains(url))
        {
            return chain.filter(exchange);
        }

        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (null == token)
        {
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
            
            DataBuffer buffer = response.bufferFactory().wrap(JSON.toJSONBytes(Result.fail("对不起 token不能为null")));
            return response.writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder()
    {
        return 0;
    }
}  

3.6.8 sentinel限流

sentinel:
      # 取消控制台懒加载
      eager: true
      transport:
        # 控制台地址
        dashboard: 127.0.0.1:8718
      # nacos配置持久化
      datasource:
        ds1:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: sentinel-song-gateway
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
            
 在nacos中添加 sentinel-song-gateway配置
 
 [
    {
        "resource": "song-system",
        "count": 1,
        "grade": 1,
        "limitApp": "default",
        "strategy": 0,
        "controlBehavior": 0
    },
	{
        "resource": "song-test",
        "count": 1,
        "grade": 1,
        "limitApp": "default",
        "strategy": 0,
        "controlBehavior": 0
    }
]

3.6.9 sentinel自定义异常

为了展示更加友好的限流提示, Sentinel支持自定义异常处理。

方案一:yml配置

# Spring
spring: 
  cloud:
    sentinel:
      scg:
        fallback:
          mode: response
          response-body: '{"code":403,"msg":"请求超过最大数,请稍后再试"}'

方案二:GatewayConfig注入Bean

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
	return new SentinelFallbackHandler();
}

SentinelFallbackHandler.java


public class SentinelFallbackHandler implements WebExceptionHandler
{
    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
    {
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] datas = "{\"code\":429,\"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
    {
        if (exchange.getResponse().isCommitted())
        {
            return Mono.error(ex);
        }
        if (!BlockException.isBlockException(ex))
        {
            return Mono.error(ex);
        }
        return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
    {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}

3.6.10 Gateway全局异常

@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler
{
    private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
    {
        ServerHttpResponse response = exchange.getResponse();

        if (exchange.getResponse().isCommitted())
        {
            return Mono.error(ex);
        }

        String msg;

        if (ex instanceof NotFoundException)
        {
            msg = "服务未找到";
        }
        else if (ex instanceof ResponseStatusException)
        {
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            msg = responseStatusException.getMessage();
        }
        else
        {
            msg = "内部服务器错误";
        }

        log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(HttpStatus.OK);

        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            return bufferFactory.wrap(JSON.toJSONBytes(Result.fail(msg)));
        }));
    }
}

3.6.11 网关配置到nacos

bootstrap.yml
    
# Tomcat
server:
  port: 8080

# Spring
spring: 
  application:
    # 应用名称
    name: song-gateway
  profiles:
    # 环境配置
    active: dev
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        # 配置文件前缀
        prefix: ${spring.application.name}
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
    sentinel:
      # 取消控制台懒加载
      eager: true
      transport:
        # 控制台地址
        dashboard: 127.0.0.1:8718
      # nacos配置持久化
      datasource:
        ds1:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: sentinel-song-gateway
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow
            
在nacos创建中创建  song-gateway-dev.yml  配置


spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: song-system
          uri: lb://song-system
          predicates:
            - Path=/system/**
            - Method=GET
          filters:
            - StripPrefix=1
            - BlackListUrlFilter

        - id: song-test
          uri: lb://song-test
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1

ignore:
  whites:
    - /auth/logout
    - /auth/login
    - /*/v2/api-docs
    - /system/stu/list           

3.7 CAS中央认证

3.7.1认证服务流程

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的

CAS是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS 是 耶鲁大学(Yale University)发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法

OAuth 2.0关注客户端开发者的简易性。要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CjWsKo07-1666438471256)(assets/image-20210112154221892.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfRQCNzp-1666438471257)(assets/image-20210809225825561.png)]

3.7.2 创建数据库


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_emp_info
-- ----------------------------
DROP TABLE IF EXISTS `sys_emp_info`;
CREATE TABLE `sys_emp_info`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `sex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `headImg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `tel` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `loginID` int(11) NULL DEFAULT NULL,
  `status` int(255) NULL DEFAULT NULL,
  `hiredate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `admin` int(255) NULL DEFAULT NULL COMMENT '0 超管 1 不是  ',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_emp_info
-- ----------------------------
INSERT INTO `sys_emp_info` VALUES (1, '张三', '1', '北京', NULL, '15166333333', 1, 1, '2010-02-03', 0);
INSERT INTO `sys_emp_info` VALUES (2, '李四', '1', '南京', NULL, '7984561234', 2, 1, '2012-02-02', 1);
INSERT INTO `sys_emp_info` VALUES (3, '王五', '1', '开封', NULL, '243234234', 3, 2, '2012-02-01', 1);

-- ----------------------------
-- Table structure for sys_emp_login
-- ----------------------------
DROP TABLE IF EXISTS `sys_emp_login`;
CREATE TABLE `sys_emp_login`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `status` int(255) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_emp_login
-- ----------------------------
INSERT INTO `sys_emp_login` VALUES (1, '123456', '0b863dd81107464279c5d6b30fffebaff378eb2b916f83f402675256fa03a0daddda0e83b79327447577e9911db96763036b5ed33ea9db8f0026c58c1c156e65', 1);
INSERT INTO `sys_emp_login` VALUES (2, '789789', '55f273db00d19bae373baabbc13014836e8624615d31cf0e10b0b87f1944c75e158614962b7e36d0b63292901690d450d4e28c1a68717cc5550a175cf04b1dca', 1);
INSERT INTO `sys_emp_login` VALUES (3, '654321', '55f273db00d19bae373baabbc13014836e8624615d31cf0e10b0b87f1944c75e158614962b7e36d0b63292901690d450d4e28c1a68717cc5550a175cf04b1dca', 1);

-- ----------------------------
-- Table structure for sys_emp_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_emp_role`;
CREATE TABLE `sys_emp_role`  (
  `eid` int(11) NOT NULL,
  `rid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`eid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_emp_role
-- ----------------------------
INSERT INTO `sys_emp_role` VALUES (2, 1);
INSERT INTO `sys_emp_role` VALUES (3, 2);

-- ----------------------------
-- Table structure for sys_operation_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_operation_log`;
CREATE TABLE `sys_operation_log`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `eid` int(11) NULL DEFAULT NULL,
  `ename` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `context` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `result` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `optime` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_operation_log
-- ----------------------------
INSERT INTO `sys_operation_log` VALUES (1, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:33:23 CST 2021');
INSERT INTO `sys_operation_log` VALUES (2, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:34:48 CST 2021');
INSERT INTO `sys_operation_log` VALUES (3, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:38:42 CST 2021');
INSERT INTO `sys_operation_log` VALUES (4, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:40:06 CST 2021');
INSERT INTO `sys_operation_log` VALUES (5, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:45:37 CST 2021');
INSERT INTO `sys_operation_log` VALUES (6, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=张, minAge=null, maxAge=null)]', 'Tue Jul 06 12:45:43 CST 2021');
INSERT INTO `sys_operation_log` VALUES (7, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:55:07 CST 2021');
INSERT INTO `sys_operation_log` VALUES (8, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=张飒, minAge=null, maxAge=null)]', 'Tue Jul 06 12:55:14 CST 2021');
INSERT INTO `sys_operation_log` VALUES (9, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:57:18 CST 2021');
INSERT INTO `sys_operation_log` VALUES (10, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 12:59:06 CST 2021');
INSERT INTO `sys_operation_log` VALUES (11, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:01:31 CST 2021');
INSERT INTO `sys_operation_log` VALUES (12, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:02:58 CST 2021');
INSERT INTO `sys_operation_log` VALUES (13, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:04:27 CST 2021');
INSERT INTO `sys_operation_log` VALUES (14, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:04:33 CST 2021');
INSERT INTO `sys_operation_log` VALUES (15, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:04:37 CST 2021');
INSERT INTO `sys_operation_log` VALUES (16, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:08:38 CST 2021');
INSERT INTO `sys_operation_log` VALUES (17, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:11:23 CST 2021');
INSERT INTO `sys_operation_log` VALUES (18, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=2, minAge=null, maxAge=null)]', 'Tue Jul 06 13:11:30 CST 2021');
INSERT INTO `sys_operation_log` VALUES (19, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=z, minAge=null, maxAge=null)]', 'Tue Jul 06 13:11:39 CST 2021');
INSERT INTO `sys_operation_log` VALUES (20, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=l, minAge=null, maxAge=null)]', 'Tue Jul 06 13:11:44 CST 2021');
INSERT INTO `sys_operation_log` VALUES (21, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:14:47 CST 2021');
INSERT INTO `sys_operation_log` VALUES (22, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:16:37 CST 2021');
INSERT INTO `sys_operation_log` VALUES (23, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=zhang, minAge=null, maxAge=null)]', 'Tue Jul 06 13:16:42 CST 2021');
INSERT INTO `sys_operation_log` VALUES (24, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:18:18 CST 2021');
INSERT INTO `sys_operation_log` VALUES (25, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:19:55 CST 2021');
INSERT INTO `sys_operation_log` VALUES (26, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=1, minAge=null, maxAge=null)]', 'Tue Jul 06 13:20:01 CST 2021');
INSERT INTO `sys_operation_log` VALUES (27, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:21:51 CST 2021');
INSERT INTO `sys_operation_log` VALUES (28, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:22:33 CST 2021');
INSERT INTO `sys_operation_log` VALUES (29, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:22:37 CST 2021');
INSERT INTO `sys_operation_log` VALUES (30, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:25:10 CST 2021');
INSERT INTO `sys_operation_log` VALUES (31, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=张, minAge=null, maxAge=null)]', 'Tue Jul 06 13:25:14 CST 2021');
INSERT INTO `sys_operation_log` VALUES (32, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=李, minAge=null, maxAge=null)]', 'Tue Jul 06 13:28:19 CST 2021');
INSERT INTO `sys_operation_log` VALUES (33, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=, minAge=null, maxAge=null)]', 'Tue Jul 06 13:28:22 CST 2021');
INSERT INTO `sys_operation_log` VALUES (34, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:28:44 CST 2021');
INSERT INTO `sys_operation_log` VALUES (35, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:28:49 CST 2021');
INSERT INTO `sys_operation_log` VALUES (36, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:28:52 CST 2021');
INSERT INTO `sys_operation_log` VALUES (37, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:31:13 CST 2021');
INSERT INTO `sys_operation_log` VALUES (38, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:32:18 CST 2021');
INSERT INTO `sys_operation_log` VALUES (39, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:32:58 CST 2021');
INSERT INTO `sys_operation_log` VALUES (40, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:34:41 CST 2021');
INSERT INTO `sys_operation_log` VALUES (41, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:36:28 CST 2021');
INSERT INTO `sys_operation_log` VALUES (42, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 13:39:29 CST 2021');
INSERT INTO `sys_operation_log` VALUES (43, 1, '张三', '127.0.0.1', '河南郑州', '获取信息', '操作成功', '[]', 'Tue Jul 06 14:09:02 CST 2021');
INSERT INTO `sys_operation_log` VALUES (44, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 14:09:53 CST 2021');
INSERT INTO `sys_operation_log` VALUES (45, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 14:09:55 CST 2021');
INSERT INTO `sys_operation_log` VALUES (46, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Tue Jul 06 14:09:56 CST 2021');
INSERT INTO `sys_operation_log` VALUES (47, 1, '张三', '127.0.0.1', '河南郑州', '获取信息', '操作成功', '[]', 'Wed Jul 07 17:17:32 CST 2021');
INSERT INTO `sys_operation_log` VALUES (48, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Wed Jul 07 17:17:41 CST 2021');
INSERT INTO `sys_operation_log` VALUES (49, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Wed Jul 07 17:17:44 CST 2021');
INSERT INTO `sys_operation_log` VALUES (50, 1, '张三', '127.0.0.1', '河南郑州', '获取信息', '操作成功', '[]', 'Wed Jul 07 17:18:05 CST 2021');
INSERT INTO `sys_operation_log` VALUES (51, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Wed Jul 07 17:18:05 CST 2021');
INSERT INTO `sys_operation_log` VALUES (52, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Wed Jul 07 17:18:10 CST 2021');
INSERT INTO `sys_operation_log` VALUES (53, 1, '张三', '127.0.0.1', '河南郑州', '查询学生', '操作成功', '[StudentQuery(name=null, minAge=null, maxAge=null)]', 'Wed Jul 07 17:18:14 CST 2021');

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pid` int(11) NULL DEFAULT NULL,
  `pname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `pmark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'M 目录  C权限   F 按钮',
  `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 0, '系统管理', NULL, NULL, '1', 'M', 'el-icon-platform-eleme');
INSERT INTO `sys_permission` VALUES (2, 0, '业务管理', NULL, NULL, '1', 'M', 'el-icon-goods');
INSERT INTO `sys_permission` VALUES (3, 2, '学生管理', 'service:stu:view', '/stu', '1', 'C', 'el-icon-user-solid');
INSERT INTO `sys_permission` VALUES (4, 3, '学生添加', 'service:stu:add', NULL, '1', 'F', 'el-icon-circle-plus');
INSERT INTO `sys_permission` VALUES (5, 3, '学生删除', 'service:stu:delete', NULL, '1', 'F', 'el-icon-delete-solid');
INSERT INTO `sys_permission` VALUES (6, 3, '学生修改', 'service:stu:update', NULL, '1', 'F', 'el-icon-edit');
INSERT INTO `sys_permission` VALUES (7, 2, '班级管理', 'service:grade:view', '/dept', '1', 'C', 'el-icon-office-building');
INSERT INTO `sys_permission` VALUES (8, 7, '班级添加', 'service:grade:add', NULL, '1', 'F', 'el-icon-circle-plus');
INSERT INTO `sys_permission` VALUES (9, 7, '班级删除', 'service:grade:delete', NULL, '1', 'F', 'el-icon-delete-solid');
INSERT INTO `sys_permission` VALUES (10, 7, '班级修改', 'service:grade:update', NULL, '1', 'F', 'el-icon-edit');
INSERT INTO `sys_permission` VALUES (11, 1, '登录日志', 'sys:log:view', NULL, '1', 'C', 'el-icon-s-flag');
INSERT INTO `sys_permission` VALUES (12, 1, '员工管理', 'sys:emp:view', NULL, '1', 'C', 'el-icon-user-solid');
INSERT INTO `sys_permission` VALUES (13, 12, '员工添加', 'sys:emp:add', NULL, '1', 'F', 'el-icon-circle-plus');
INSERT INTO `sys_permission` VALUES (14, 12, '员工删除', 'sys:emp:delete', NULL, '1', 'F', 'el-icon-delete-solid');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `rmark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员', 'sysadmin', '1');
INSERT INTO `sys_role` VALUES (2, '业务员', 'servicePeople', '1');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rid` int(11) NULL DEFAULT NULL,
  `pid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 1, 1);
INSERT INTO `sys_role_permission` VALUES (2, 1, 2);
INSERT INTO `sys_role_permission` VALUES (3, 1, 3);
INSERT INTO `sys_role_permission` VALUES (4, 1, 4);
INSERT INTO `sys_role_permission` VALUES (5, 1, 5);
INSERT INTO `sys_role_permission` VALUES (6, 1, 6);
INSERT INTO `sys_role_permission` VALUES (7, 1, 7);
INSERT INTO `sys_role_permission` VALUES (8, 1, 8);
INSERT INTO `sys_role_permission` VALUES (9, 1, 9);
INSERT INTO `sys_role_permission` VALUES (10, 1, 10);
INSERT INTO `sys_role_permission` VALUES (11, 1, 11);
INSERT INTO `sys_role_permission` VALUES (12, 1, 12);
INSERT INTO `sys_role_permission` VALUES (13, 1, 13);
INSERT INTO `sys_role_permission` VALUES (14, 1, 14);
INSERT INTO `sys_role_permission` VALUES (15, 2, 2);
INSERT INTO `sys_role_permission` VALUES (16, 2, 3);
INSERT INTO `sys_role_permission` VALUES (17, 2, 4);
INSERT INTO `sys_role_permission` VALUES (18, 2, 5);
INSERT INTO `sys_role_permission` VALUES (19, 2, 6);
INSERT INTO `sys_role_permission` VALUES (20, 2, 7);
INSERT INTO `sys_role_permission` VALUES (21, 2, 8);
INSERT INTO `sys_role_permission` VALUES (22, 2, 9);
INSERT INTO `sys_role_permission` VALUES (23, 2, 10);

-- ----------------------------
-- Procedure structure for test4
-- ----------------------------
DROP PROCEDURE IF EXISTS `test4`;
delimiter ;;
CREATE PROCEDURE `test4`(userId int)
begin
            declare username varchar(32) default '';
            declare ordercount int default 0;
            select name into username from users where id=userId;
            select username;
    end
;;
delimiter ;

SET FOREIGN_KEY_CHECKS = 1;

3.7.3 创建CAS微服务

A 创建song_sso_auth 二级项目
B 添加pom依赖
       <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        
        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        
        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
		
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
C 添加配置文件 bootstrap.yml
    
# Tomcat
server:
  port: 9200

# Spring
spring:
  application:
    # 应用名称
    name: song-auth
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址    我们当前的微服务一定要注册到 nacos注册中心的 时候 才能使用nacos其他的操作 例如 配置中心
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址      prefix-active.file-extension    私有配置文件    song-system-dev.yml
        server-addr: 127.0.0.1:8848
        # 配置文件前缀
        prefix: ${spring.application.name}
        # 配置文件格式
        file-extension: yml
        # 共享配置      公共配置文件的名字为 :    application-dev.yml   也就是说  nacos中一定有一个配置文件叫做 application-dev.yml
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

D 在nacos中添加  song-auth-dev.yml配置       

3.7.4 加密工具类

参考:SpringCloud集成Security安全(Config配置中心)
https://www.jianshu.com/p/2e88d4df4ae7

在song_common_core 中创建 SecurityUtils 工具类


public class SecurityUtils
{
    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }

    /**
     * 判断密码是否相同
     *
     * @param rawPassword 真实密码
     * @param encodedPassword 加密后字符
     * @return 结果
     */
    public static boolean matchesPassword(String rawPassword, String encodedPassword)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

BCryptPasswordEncoder
spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的

我们通过encryptPassword生成密码 添加到数据库中!!!!!

3.7.5 登录接口并测试接参

@RestController
public class AuthController {

    @PostMapping("login")
    public Result   login(@RequestBody LoginVO login){

        System.out.println(login);

        return  null;
    }

}


@Data
public class LoginVO implements Serializable {

    private  String  username;
    private  String  password;

}

在网关中配置当前服务映射代理
        - id: song-auth
          uri: lb://cloud-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
            - BlackListUrlFilter


3.7.6 业务处理与远程调用

A  创建AuthService 处理登录业务
    
public interface AuthServie {
    Result   login(String  username,String password);
}


@Service
public class AuthServiceImpl implements AuthServie {
    @Override
    public Result login(String username, String password) {
        // 1 账号密码准确性校验 或者 在controller进行入参校验
        // 2 根据用户名 查询用户信息 (远程调用system中的接口)
        return null;
    }
}

@RestController
public class AuthController {

    @Autowired
    private AuthServie  authServie;

    @PostMapping("login")
    public Result   login(@RequestBody LoginVO login){

        Result r = authServie.login(login.getUsername(), login.getPassword());
        
        return  r;
    }

}

B 在song_system中创建SysUserController  用来处理用户信息
    
@RestController
@RequestMapping("user")
public class SysUserController {

    @GetMapping("/info/{username}")
    public Result info(@PathVariable("username") String username)
    {
        return Result.sucess();
    }

}

C 在song_api_test中创建API完成远程访问
    
@FeignClient(contextId = "SysUser", value = "song-system" , fallbackFactory =  RemoteSysUserFallbackFactory.class     )
public interface RemoteSysUserService {


    /**
     * 通过用户名查询用户信息
     *
     * @param username 用户名
     * @return 结果
     */
    @GetMapping(value = "/user/info/{username}")
    Result<SysEmpInfo> getUserInfo(@PathVariable("username") String username);


}
@Component
public class RemoteSysUserFallbackFactory implements FallbackFactory<RemoteSysUserService> {

    private static final Logger log = LoggerFactory.getLogger(RemoteTestFallbackFactory.class);

    @Override
    public RemoteSysUserService create(Throwable cause) {
        log.info(  "服务调用出现问题 :"+  cause.getLocalizedMessage() );

        return new RemoteSysUserService() {

            @Override
            public Resul<SysEmpInfo>t getUserInfo(String username) {
                return Result.fail("熔断了  降级了  看着办吧!");
            }
        };
    }
}

D 在 song_auth 中导入jar包并完成调用
@Service
public class AuthServiceImpl implements AuthServie {


    @Autowired
    private RemoteSysUserService  userService;

    @Override
    public Result login(String username, String password) {


        // 1 账号密码准确性校验 或者 在controller进行入参校验

        // 2 根据用户名 查询用户信息 (远程调用system中的接口)

        Result userInfo = userService.getUserInfo(username);

        System.out.println(userInfo);

        return null;
    }
}

E 启动类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Auth9200
{
    public static void main( String[] args ){
        SpringApplication.run(Auth9200.class,args);
    }
}


3.7.7 用户信息查询

A 添加jar包

在总pom中导入
<spring-boot.mybatis>2.1.4</spring-boot.mybatis>
<pagehelper.boot.version>1.3.1</pagehelper.boot.version>
<druid.version>1.2.6</druid.version>
<!-- Mybatis 依赖配置 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${spring-boot.mybatis}</version>
            </dependency>

 <!-- pagehelper 分页插件 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper.boot.version}</version>
            </dependency>
<!-- Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

在song-commone-core中导入
 <!-- Mybatis 依赖配置 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- pagehelper 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>
        <!-- Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

B 配置信息到nacos中的 song-system-dev.yml

spring:
  ##数据库连接信息
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    # 下面为连接池的补充设置,应用到上面所有数据源中
    # 初始化大小,最小,最大
    initialSize: 5
    minIdle: 5
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打开PSCache,并且指定每个连接上PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    # 合并多个DruidDataSource的监控数据
    useGlobalDataSourceStat: true

# mybatis配置
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.aaa.cloud.pojo.entity
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
    mapperLocations: classpath:mapper/*.xml

C 创建service mapper 完成调用

@RestController
@RequestMapping("user")
public class SysUserController {

    @Autowired
    private SysUserService  sysUserService;

    @GetMapping("/info/{username}")
    public Result<SysEmpInfo> info(@PathVariable("username") String username)
    {
        return sysUserService.info(username);
    }

}




@Data
public class SysEmpInfo implements Serializable {
    private static final long serialVersionUID = -42435457108300168L;

    private Integer id;

    private  String  username;
    private  String  password;

    private String name;

    private String sex;

    private String address;

    private String headimg;

    private String tel;

    private Integer loginid;

    private Integer status;

    private String hiredate;
    /**
     * 0 超管 1 不是
     */
    private Integer admin;


    private Set<String>   permissions;
    private Set<String>   roles;


}




@Service
public class SysUserServiceImpl implements SysUserService {


    @Autowired
    private SysEmpInfoMapper mapper;

    @Override
    public Result info(String username) {

        // 根据用户名 查询用户信息
        SysEmpInfo infoByUsername = mapper.getInfoByUsername(username);

        if (infoByUsername == null){
            return  Result.fail("账号不存在");
        }

        // 查询权限信息
        if(infoByUsername.getAdmin() == 0){
            HashSet<String> ap = new HashSet<>();
            ap.add("*:*:*");
            infoByUsername.setPermissions( ap  );

        }else{
            Set<String> userPmarkByUID = mapper.getUserPmarkByUID(infoByUsername.getId());
            infoByUsername.setPermissions( userPmarkByUID  );
        }

        return Result.sucess( infoByUsername  );
    }
}

public interface SysEmpInfoMapper {


    SysEmpInfo  getInfoByUsername(String  username);

    Set<String> getUserPmarkByUID(Integer uid);


}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.aaa.mapper.SysEmpInfoMapper">


    <select id="getInfoByUsername" resultType="com.aaa.entity.SysEmpInfo" >

select  sys_emp_info.* ,sys_emp_login.username,sys_emp_login.password  from sys_emp_login

INNER JOIN  sys_emp_info  on  sys_emp_login.id = sys_emp_info.loginID

where  sys_emp_login.username = #{username}


    </select>

    <select id="getUserPmarkByUID" resultType="string" >

select  p.pmark from  sys_emp_role   er
INNER JOIN  sys_role  r on  er.rid  = r.id
INNER JOIN  sys_role_permission  rp  on  rp.rid = er.rid
INNER JOIN  sys_permission  p  on  rp.pid = r.id

where  r.`status` = 1 and p.`status` = 1 and  p.pmark != ''  and er.eid = #{uid}


    </select>


</mapper>
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.aaa.mapper")
public class System9201
{
    @Bean
    public Logger.Level getLog()
    {
        return Logger.Level.FULL;
    }

    public static void main( String[] args )
    {
        SpringApplication.run(System9201.class,args);
    }
}

3.7.8 song_common_redis项目

在common中创建 song_common_redis 项目  用来集成redis

导入依赖
    <dependencies>
        <!-- SpringBoot Boot Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
    </dependency>

        <dependency>
            <groupId>com.aaa</groupId>
            <artifactId>song_api_test</artifactId>
        </dependency>
    </dependencies>

创建配置类
    @Configuration
public class RedisConfig {

    @Bean
    public MyRedisProperties    myRedisProperties(Environment environment){
        MyRedisProperties myRedisProperties = Binder.get(environment).bind(MyRedisProperties.REDIS_PREFIX, MyRedisProperties.class).get();
        return  myRedisProperties;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig, JedisClientConfiguration clientConfig){

        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(standaloneConfig,clientConfig);
        return jedisConnectionFactory;
    }

    @Bean
    public RedisStandaloneConfiguration  redisStandaloneConfiguration(MyRedisProperties  myRedisProperties){

        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();

        redisStandaloneConfiguration.setDatabase(   myRedisProperties.getDatabase()    );
        redisStandaloneConfiguration.setHostName(   myRedisProperties.getHost()  );
        redisStandaloneConfiguration.setPassword(   myRedisProperties.getPassword() );
        redisStandaloneConfiguration.setPort(  myRedisProperties.getPort() );

        return  redisStandaloneConfiguration;
    }

    @Bean
    public JedisClientConfiguration  jedisClientConfiguration( GenericObjectPoolConfig poolConfig ){

        JedisClientConfiguration.DefaultJedisClientConfigurationBuilder builder =
                (JedisClientConfiguration.DefaultJedisClientConfigurationBuilder) JedisClientConfiguration.builder();

        JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jedisPoolingClientConfigurationBuilder = builder.poolConfig(poolConfig);

        JedisClientConfiguration jcc = jedisPoolingClientConfigurationBuilder.build();

        return  jcc;
    }

    @Bean
    public  GenericObjectPoolConfig genericObjectPoolConfig( MyRedisProperties myRedisProperties  ){

        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();

        genericObjectPoolConfig.setMaxIdle(  myRedisProperties.getMaxIdle() );
        genericObjectPoolConfig.setMinIdle(  myRedisProperties.getMinIdle() );
        genericObjectPoolConfig.setMaxTotal( myRedisProperties.getMaxTotal());
        genericObjectPoolConfig.setMaxWaitMillis( myRedisProperties.getMaxWaitMillis() );
        genericObjectPoolConfig.setEvictorShutdownTimeoutMillis( myRedisProperties.getTimeout() );

        return  genericObjectPoolConfig;
    }



}

创建属性读取类
@Data
@ConfigurationProperties(prefix = MyRedisProperties.REDIS_PREFIX )
public class MyRedisProperties {

    public static  final  String  REDIS_PREFIX = "spring.redis";

    private  Integer       database;
    private  String        host;
    private  Integer       port;
    private  String        password;
    private  Integer       maxTotal;
    private  Integer       maxWaitMillis;
    private  Integer       maxIdle;
    private  Integer       minIdle;
    private  Integer       timeout;
}
   

创建RedisService@Component
public class RedisService
{
    @Autowired
    public JedisConnectionFactory    factory;



    /**
     * 缓存数据
     */
    public boolean cacheObject(String key, Object  value ,Long  timeout)
    {
        RedisConnection connection = factory.getConnection();

        byte[] keyBytes = key.getBytes();
        byte[] valueBytes = new JdkSerializationRedisSerializer().serialize(value);
        Boolean aBoolean = connection.setEx(keyBytes, timeout, valueBytes);
        connection.close();

        return aBoolean;
    }


    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(String key)
    {
        byte[] keyBytes = key.getBytes();
        RedisConnection connection = factory.getConnection();

        byte[] valueBytes = connection.get(keyBytes);

        Object deserialize = new JdkSerializationRedisSerializer().deserialize(valueBytes);

        connection.close();

        return (T) deserialize;
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return resetKeyTime(key,0L);
    }

    /**
     * 重置过期时间
     *
     * @param key
     */
    public boolean resetKeyTime(String key,Long timeout)
    {
        RedisConnection connection = factory.getConnection();
        Boolean aBoolean = connection.expire(key.getBytes(), timeout);
        connection.close();
        return aBoolean;
    }


}
redis配置信息  

# Spring
spring:
  redis:
    # Redis数据库索引(默认为0)
    database: 0
    # Redis服务器地址
    host: 127.0.0.1
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接池最大连接数(使用负值表示没有限制)
    maxTotal: 20
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    maxWaitMillis: -1
    # 连接池中的最大空闲连接
    maxIdle: 10
    # 连接池中的最小空闲连接
    minIdle: 0
    # 连接超时时间(毫秒)
    timeout: 1000

3.7.9song_common_security项目

song_common_security 用来生成token 刷新token  鉴权 验证等工作
A 创建工程并导包
   <dependencies>
        <dependency>
            <groupId>com.aaa</groupId>
            <artifactId>song_common_redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aaa</groupId>
            <artifactId>song_common_core</artifactId>
        </dependency>
    </dependencies>

B 添加TokenService
    
@Component
public class TokenService
{
    @Autowired
    private RedisService redisService;

    protected static final long MILLIS_SECOND = 1000;

    /**
     * 创建令牌
     */
    public String createToken(SysEmpInfo loginUser)
    {
        // 生成token
        String token = UUID.randomUUID().toString();
        redisService.cacheObject(token,loginUser,MILLIS_SECOND*60*30);
        return token;
    }

}

3.7.10 登录业务处理

@Service
public class AuthServiceImpl implements AuthServie {


    @Autowired
    private RemoteSysUserService  userService;

    @Autowired
    private TokenService  tokenService;

    @Override
    public Result login(String username, String password) {


        // 1 账号密码准确性校验 或者 在controller进行入参校验

        // 2 根据用户名 查询用户信息 (远程调用system中的接口)

        Result<SysEmpInfo> userInfo = userService.getUserInfo(username);

        if (userInfo.getCode() == 2){
            return  Result.fail("账号不存在");
        }

        SysEmpInfo info =  userInfo.getData();

        if(info.getStatus() == 2){
            return  Result.fail("账号被冻结");
        }

        boolean b = SecurityUtils.matchesPassword(password, info.getPassword());
        if( !b ){
            return  Result.fail("密码错误");
        }
        //  3 生成token 存储到redis中

        String token = tokenService.createToken(info);

        return Result.sucess(token);
    }
}



3.7.11 GATEWAY网关校验token


@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private IgnoreWhiteProperties  ignoreWhite;

    @Autowired
    private TokenService  tokenService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //1 r如果是白名单 则直接放行
        String url = exchange.getRequest().getURI().getPath();
        // 跳过不需要验证的路径
        if (ignoreWhite.getWhites().contains(url))
        {
            return chain.filter(exchange);
        }

        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
        //从请求参数中获取token 用在 app中
        // String token = exchange.getRequest().getQueryParams().getFirst("token");
        // 从请求头中获取 token
        String token = exchange.getRequest().getHeaders().getFirst("token");

        if (null == token)
        {
            DataBuffer buffer = response.bufferFactory().wrap(JSON.toJSONBytes(Result.fail("对不起 token不能为null")));
            return response.writeWith(Mono.just(buffer));
        }

        // 验证令牌有效性
        SysEmpInfo info = tokenService.getToken(token);
        if(info == null){
            DataBuffer buffer = response.bufferFactory().wrap(JSON.toJSONBytes(Result.fail("token失效")));
            return response.writeWith(Mono.just(buffer));
        }

        // 刷新令牌
        tokenService.refreshToken(token);

        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("userid", info.getId().toString()).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}


@Component
public class TokenService
{
    @Autowired
    private RedisService redisService;

    protected static final long MILLIS_SECOND = 1000;

    /**
     * 创建令牌
     */
    public String createToken(SysEmpInfo loginUser)
    {
        // 生成token
        String token = UUID.randomUUID().toString();
        redisService.cacheObject(token,loginUser,MILLIS_SECOND*60*30);
        return token;
    }

    /**
     * 获取令牌对象
     */
    public <T>T getToken(String token)
    {
        Object cacheObject = redisService.getCacheObject(token);
        return (T) cacheObject;
    }

    /**
     * 刷新令牌
     */
    public void refreshToken(String token)
    {
         redisService.resetKeyTime(token,MILLIS_SECOND*60*30);
    }
    
    /**
     * 获取当前登录用户
     * @return
     */
    public SysEmpInfo getUser(){

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        String token = requestAttributes.getRequest().getHeader("token");

        return getToken(token);

    }


}

3.7.12 服务鉴权

A 自定义权限注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {

    String  value() default "";

}


B AOP切面

@Aspect
@Component
public class PreAuthorizeAspect {

    @Autowired
    private TokenService tokenService;

    @Around("@annotation(com.aaa.annotation.RequirePermission)")
    public Object around(ProceedingJoinPoint point) throws Throwable
    {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        RequirePermission annotation = method.getAnnotation(RequirePermission.class);

        if(annotation == null){
            return point.proceed();
        }

        String needPermission = annotation.value();

        Set<String> permissions = tokenService.getUser().getPermissions();

        if (permissions.contains("*:*:*") || permissions.contains(needPermission)){
            return point.proceed();
        }

        throw   new RuntimeException("对不起 暂无该权限");

    }

}
C 全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(RuntimeException.class)
    public Result baseException(RuntimeException e)
    {
        return Result.fail(e.getLocalizedMessage());
    }

}
D 在需要权限校验的Controller中添加注解

@RestController
@RequestMapping("stu")
public class StudentController {


    @Autowired
    private StudentService  service;


    @RequirePermission("sys:stu:foo")
    @GetMapping("list")
    public Result  list(String   id)  {
        return  service.listStu(id);
    }

}

3.8 服务监控

3.8.1 服务监控概述

什么是服务监控
监视当前系统应用状态、内存、线程、堆栈、日志等等相关信息,主要目的在服务出现问题或者快要出现问题时能够准确快速地发现以减小影响范围。

为什么要使用服务监控
服务监控在微服务改造过程中的重要性不言而喻,没有强大的监控能力,改造成微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是一场灾难。

    

spring boot actuator 服务监控接口
actuator是监控系统健康情况的工具。

    
    
spring boot admin 服务监控管理
 Spring Boot Admin是一个针对spring-boot的actuator接口进行UI美化封装的监控工具。他可以:在列表中浏览所有被监控spring-boot项目的基本信息,详细的Health信息、内存信息、JVM信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表和命中率)等,还可以直接修改logger的level。

3.8.2 基本构建

搭建项目层级

song_cloud
└── song_visual
     └── song_monitor -- 服务监控 [9100]


在song-monitor项目中导入依赖
   
    
    <dependencies>
        
       <!-- SpringBoot Web -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- SpringBoot Actuator -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
		
    </dependencies>

在application.yml配置暴露所有监控端点
server:
  port: 9100
management:
  endpoints:
    web:
      exposure:
        include: '*'
            
            
监控启动类            
@SpringBootApplication
public class Monitor9100
{
    public static void main(String[] args)
    {
        SpringApplication.run(Monitor9100.class, args);
    }
}    
地址描述
/beans显示所有的Spring bean列表
/caches显示所有的缓存相关信息
/scheduledtasks显示所有的定时任务相关信息
/loggers显示所有的日志相关信息
/configprops显示所有的配置信息
/env显示所有的环境变量信息
/mappings显示所有控制器相关信息
/info显示自定义用户信息配置
/metrics显示应用指标相关信息
/health显示健康检查状态信息,up表示成功 down表示失败
/threaddump显示程序线程的信息

http://127.0.0.1:9100/actuator/beans

3.8.3整合Admin-Ui

总pom中添加 并导入依赖
<spring-boot-admin.version>2.4.1</spring-boot-admin.version>

<!--  SpringBoot 监控 -->
            <dependency>
                <groupId>de.codecentric</groupId>
                <artifactId>spring-boot-admin-starter-server</artifactId>
                <version>${spring-boot-admin.version}</version>
            </dependency>


在Monitor项目中导入依赖

 <!-- SpringBoot Admin -->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            
        </dependency>
		
        <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
		
        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
		
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

添加bootstrap.yml
# Tomcat
server:
  port: 9100

# Spring
spring: 
  application:
    # 应用名称
    name: song-monitor
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        # 配置文件格式
        file-extension: yml
        # 配置文件前缀
        prefix: ${spring.application.name}
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}


在nacos中添加song-monitor-dev.yml配置文件

# spring
spring: 
  security:
    user:
      name: root
      password: 123456
  boot:
    admin:
      ui:
        title: song服务状态监控

添加配置类 配置登录校验

/**
 * 监控权限配置
 * 
 */
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter
{
    private final String adminContextPath;

    public WebSecurityConfigurer(AdminServerProperties adminServerProperties)
    {
        this.adminContextPath = adminServerProperties.getContextPath();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        successHandler.setDefaultTargetUrl(adminContextPath + "/");

        http
            .headers().frameOptions().disable()
            .and().authorizeRequests()
            .antMatchers(adminContextPath + "/assets/**"
                , adminContextPath + "/login"
                , adminContextPath + "/actuator/**"
                , adminContextPath + "/instances/**"
            ).permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().loginPage(adminContextPath + "/login")
            .successHandler(successHandler).and()
            .logout().logoutUrl(adminContextPath + "/logout")
            .and()
            .httpBasic().and()
            .csrf()
            .disable();
    }
}


启动类配置
    
@EnableAdminServer    
@SpringBootApplication
public class Monitor9100
{
    public static void main(String[] args)
    {
        SpringApplication.run(Monitor9100.class, args);
    }
} 


其他的项目需要导入 监控依赖

<!-- SpringBoot Actuator -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>



在 application-dev.yml中配置
logging:
  file:
    name: logs/${spring.application.name}/info.log    
    
# 暴露监控端点    
management:
  endpoints:
    web:
      exposure:
        include: '*'
3.8.4 自定义通知
可以通过添加实现Notifier接口的Spring Beans来添加您自己的通知程序。

import org.springframework.stereotype.Component;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier;
import reactor.core.publisher.Mono;

/**
 * 通知发送配置
 * 
 */
@Component
public class SongStatusChangeNotifier extends AbstractStatusChangeNotifier
{
    public SongStatusChangeNotifier(InstanceRepository repository)
    {
        super(repository);
    }

    @Override
    protected Mono<Void> doNotify(InstanceEvent event,
            de.codecentric.boot.admin.server.domain.entities.Instance instance)
    {
        return Mono.fromRunnable(() -> {
            if (event instanceof InstanceStatusChangedEvent)
            {
                String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
                switch (status)
                {
                    // 健康检查没通过
                    case "DOWN":
                        System.out.println("发送 健康检查没通过 的通知!");
                        break;
                    // 服务离线
                    case "OFFLINE":
                        System.out.println("发送 服务离线 的通知!");
                        break;
                    // 服务上线
                    case "UP":
                        System.out.println("发送 服务上线 的通知!");
                        break;
                    // 服务未知异常
                    case "UNKNOWN":
                        System.out.println("发送 服务未知异常 的通知!");
                        break;
                    default:
                        break;
                }
            }
        });
    }
}
3.9 链路追踪
3.9.1 简介
什么是链路追踪
随着微服务分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如分布式服务、分布式数据库、分布式缓存等,使得后台服务构成了一种复杂的分布式网络。在服务能力提升的同时,复杂的网络结构也使问题定位更加困难。在一个请求在经过诸多服务过程中,出现了某一个调用失败的情况,查询具体的异常由哪一个服务引起的就变得十分抓狂,问题定位和处理效率是也会非常低。
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

为什么要使用链路追踪
链路追踪为分布式应用的开发者提供了完整的调用链路还原、调用请求量统计、链路拓扑、应用依赖分析等工具,可以帮助开发者快速分析和诊断分布式应用架构下的性能瓶颈,提高微服务时代下的开发诊断效率。

    
skywalking 链路追踪
SkyWalking是一个可观测性分析平台(Observability Analysis Platform 简称OAP)和应用性能管理系统(Application Performance Management 简称 APM)。

提供分布式链路追踪,服务网格(Service Mesh)遥测分析,度量(Metric)聚合和可视化一体化解决方案。

SkyWalking 特点

多语言自动探针,java,.Net Code ,Node.Js
多监控手段,语言探针和Service Mesh
轻量高效,不需要额外搭建大数据平台
模块化架构,UI ,存储《集群管理多种机制可选
支持警告
优秀的可视化效果。
下面是`SkyWalking`的架构图: 

skywalking

3.9.2 下载安装
Windows平台安装包下载
可以从http://skywalking.apache.org/downloads
下载 apache-skywalking-apm-$version.tar.gz包。

Windows下载解压后(.tar.gz),直接点击bin/startup.bat就可以了,这个时候实际上是启动了两个项目,一个收集器,一个web页面。
    
skywalking提供了一个可视化的监控平台,安装好之后,在浏览器中输入 (http://localhost:8080 (opens new window))就可以访问了。
3.9.3 项目整合
配置vm参数
idea配置vm参数:
    
-javaagent:C:\Users\Administrator\Desktop\ali\apache-skywalking-apm-es7-8.7.0\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar
-Dskywalking.agent.service_name=cloud-monitor
-Dskywalking.collector.backend_service=localhost:11800    

参数描述
javaagent配置skywalking-agent.jar的地址
service_name配置需要监控的服务名
javaagentskywalking收集器服务的地址
3.10 微服务多数据源
3.10.1 多数据源概述
在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况 
    
微服务版本采用了dynamic-datasource动态多数据源组件
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D9TJyVIu-1666438471259)(assets/image-20210815151151400.png)]

3.10.2 DS简介
https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611

特性

    支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
    支持数据库敏感配置信息 加密 ENC()。
    支持每个数据库独立初始化表结构schema和数据库database。
    支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
    支持 自定义注解 ,需继承DS(3.2.0+)。
    提供并简化对DruidHikariCpBeeCpDbcp2的快速集成。
    提供对Mybatis-PlusQuartzShardingJdbcP6syJndi等组件的集成方案。
    提供 自定义数据源来源 方案(如全从数据库加载)。
    提供项目启动后 动态增加移除数据源 方案。
    提供Mybatis环境下的 纯读写分离 方案。
    提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
    支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
    提供 基于seata的分布式事务方案。
    提供 本地多数据源事务方案。 附:不能和原生spring事务混用。

约定

    本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
    配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
    切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
    默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
    方法上的注解优先于类上注解。
    DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
3.10.3 多数据源使用
在common中创建一个 song_common_datasource 项目
 总 pom中添加依赖
         <druid.version>1.2.6</druid.version>
         <dynamic-ds.version>3.4.0</dynamic-ds.version>
    
        <!-- Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!-- Dynamic DataSource -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>${dynamic-ds.version}</version>
        </dependency>

在song_common_datasource 直接引用项目

创建注解(其实可以不用创建 使用原生的DS注解)
/**
 * 主库数据源
 * 
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("master")
public @interface Master
{

}

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("slave")
public @interface Slave
{

}
在nacos中添加配置
# spring配置
spring: 
  datasource:
    druid:
      stat-view-servlet:
        enabled: true
        loginUsername: admin
        loginPassword: 123456
    dynamic:
      druid:
        initial-size: 5
        min-idle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,slf4j
        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: 123456
          # 从库数据源
          slave:
             username: root
             password: 123456
             url: jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
             driver-class-name: com.mysql.cj.jdbc.Driver
    





3.11 分布式事务
3.11.1分布式事务产生背景

单体系统访问多个数据库

一个服务需要调用多个数据库实例完成数据的增删改操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7C2hwKq-1666438471259)(https://p6-tt.byteimg.com/origin/pgc-image/95b65d22000e4730b771a716b7d31f3e?from=pc)]

多个微服务访问同一个数据库

多个服务需要调用一个数据库实例完成数据的增删改操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9busPluz-1666438471260)(https://p6-tt.byteimg.com/origin/pgc-image/483af2272c42438f89429a6e77729be0?from=pc)]

多个微服务访问多个数据库

多个服务需要调用一个数据库实例完成数据的增删改操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-veKoSCiQ-1666438471261)(https://p1-tt.byteimg.com/origin/pgc-image/0d2d440f9bb44b81aaa772e186163a5e?from=pc)]

3.11.2分布式事务理论基础
#### 两阶段提交协议(对应阿里的AT模式)

事务管理器分为两个阶段来协调资源管理器,第一阶段准备资源,也就是预留事务所需资源,如果资源管理器资源预留成功,则进行第二阶段资源提交,否则协调资源管理器回滚资源

例如:扣款流程,第一步先冻结资金,冻结成功后进行第二步扣款,扣除成功则把事务提交。若第二步扣款失败则将第一步回滚,恢复冻结资金

#### TCC (Try-Confirm-Cancel)

实际上是对服务化的两个阶段提交协议,业务开发者需要实现这三个服务接口,第一阶段服务自由业务代码编排来调用Try接口进行资源预留,所有参与者的Try接口都成功了,事务管理器会提交事务,并调用每个参与者的Confirm接口真正提交业务操作,否则调用每个参与者的Cancel接口回滚事务

#### Saga

- saga是一种补偿协议,在Saga模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作
- 分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态
- Saga正向服务与补偿服务也需要业务开发者实现

其实TCC、Saga都是两阶段提交的补充
3.11.3 Seata概述
https://seata.io/zh-cn/index.html
- Simple Extensible Autonomous Transaction Architecture  简单可扩展自治事务框架。20191月开源


- 三大模块,TM、RM、TC。其中TM和RM是作为Seata的客户端与业务系统集成在一起,TC作为Seata的服务端独立部署
  #### 三大模块

 TC(Transaction Coordinator):事务协调器   维护全局和分支事务的状态,用于全局性事务的提交和回滚

 TM(Transaction Manager): 事务管理器

  定义全局事务域:开启全局事务、提交或者回滚全局事务。向事务指定标识,监视它们的进场,并负责处理事务的完成和失败。事务分支标识(称为XID)由TM指定,以标识一个RM内的全局事务和特定分支

-RM(Resource Manager):资源管理器
  管理分支事务所使用的资源,向TC注册分支事务并报告分支事务状态,接受TC的命令来提交或者回滚分支事务

    
 #### Seata管理分布式事务典型的生命周期

- TM向TC请求开始全局事务,TC生成一个代表全局性事务的XID
- XID通过微服务的调用链传播
- RM向TC注册本地事务,作为XID对应的全局事务的分支
- TM要求TC提交或者回滚XID相应的全局事务
- TC驱动XID相应的全局事务下的分支事务完成分支的提交或者回滚   

image-20200712173947722

3.11.4 四种分布式解决方案
AT模式、TCC模式、Saga模式和XA模式
    
AT 模式:参见(Seata AT 模式》 (opens new window))文档
TCC 模式:参见(Seata TCC 模式》 (opens new window))文档
Saga 模式:参见(《SEATA Saga 模式》 (opens new window))文档
XA 模式:正在开发中...
 目前使用的流行度情况是:AT > TCC > Saga。因此,我们在学习Seata的时候,可以花更多精力在AT模式上,最好搞懂背后的实现原理,毕竟分布式事务涉及到数据的正确性,出问题需要快速排查定位并解决。
3.11.5 seata下载与安装
Windows平台安装包下载
可以从https://github.com/seata/seata/releases
下载seata-server-$version.zip包。

Windows下载解压后(.zip),直接点击bin/seata-server.bat就可以了。
3.11.5 seata使用
1 每个数据库创建 undo_log表
  创建 UNDO_LOG 表

SEATA AT 模式需要 UNDO_LOG 表

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2 导入seatajar包

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


3  配置seata

#一定要设置spring.datasource.dynamic.seata配置项为true,开启对Seata的集成,否则会导致Seata全局事务回滚失败。
spring:
  datasource:
      dynamic:
         seata: true  #开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭

# seata配置
seata:
  enabled: true
  # Seata 应用编号,默认为 ${spring.application.name}
  application-id: ${spring.application.name}
  # Seata 事务组编号,用于 TC 集群名
  tx-service-group: ${spring.application.name}-group
  # 关闭自动代理
  enable-auto-data-source-proxy: false
  # 服务配置项
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      song-system-group: default
    # 分组和 Seata 服务的映射
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: file
  registry:
    type: file



4 添加业务 模拟分布式事务场景


@RefreshScope
@RestController
@RequestMapping("stu")
public class StudentController {


    @Autowired
    private TXService service;



    @RequirePermission("sys:stu:haha")
    @GetMapping("list")
    public Result   list(String   id)  {
        return  service.buy();
    }

}

@Service
public class TXServiceImpl implements TXService {


    @Autowired
    private StudentService    studentService;

    @Autowired
    private OrderService     orderService;


    @Override
    @Transactional
    @GlobalTransactional // 重点 第一个开启事务的需要添加seata全局事务注解
    public Result buy() {

        // 1  调用 studentservice 添加数据    模拟   谁购买的商品   test库   student
        studentService.inserStu();

        // 2 调用 oderservie   添加订单    模拟     生成的订单      test1库  orders
        orderService.addOrder();

        return Result.sucess("ok");
    }
}


    /**
     * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
     */
    @Override
    @Master
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Result inserStu() {


        int i = studentMapper.addStu();
        return Result.sucess(i);
    }

    @Override
    @Slave
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int addOrder() {

        int i = orderMapper.addOrder();
        int x = 1/0;
        return i;
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQauVQYN-1666438471262)(assets/image-20210816002558895.png)]

AT模式 实现原理
https://www.toutiao.com/i6977544649963782693
3.11.6 seata配置信息到nacos
1、修改conf/registry.conf文件:

registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}
由于使用nacos作为注册中心,所以conf目录下的file.conf无需理会。然后就可以直接启动bin/seata-server.bat,可以在nacos里看到一个名为seata-server的服务了。


2、由于seata使用mysql作为db高可用数据库,故需要在mysql创建一个song-seata库,并导入数据库脚本。
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) 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';      
# 3 config.txt文件复制到seata目录
service.vgroupMapping.song-system-group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/song-seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
4 nacos-config.sh复制到seata的conf目录

#!/usr/bin/env bash
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

while getopts ":h:p:g:t:u:w:" opt
do
  case $opt in
  h)
    host=$OPTARG
    ;;
  p)
    port=$OPTARG
    ;;
  g)
    group=$OPTARG
    ;;
  t)
    tenant=$OPTARG
    ;;
  u)
    username=$OPTARG
    ;;
  w)
    password=$OPTARG
    ;;
  ?)
    echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] "
    exit 1
    ;;
  esac
done

urlencode() {
  for ((i=0; i < ${#1}; i++))
  do
    char="${1:$i:1}"
    case $char in
    [a-zA-Z0-9.~_-]) printf $char ;;
    *) printf '%%%02X' "'$char" ;;
    esac
  done
}

if [[ -z ${host} ]]; then
    host=localhost
fi
if [[ -z ${port} ]]; then
    port=8848
fi
if [[ -z ${group} ]]; then
    group="SEATA_GROUP"
fi
if [[ -z ${tenant} ]]; then
    tenant=""
fi
if [[ -z ${username} ]]; then
    username=""
fi
if [[ -z ${password} ]]; then
    password=""
fi

nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"

echo "set nacosAddr=$nacosAddr"
echo "set group=$group"

failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$(urlencode $1)&group=$group&content=$(urlencode $2)&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null
  if [[ -z $(cat "${tempLog}") ]]; then
    echo " Please check the cluster status. "
    exit 1
  fi
  if [[ $(cat "${tempLog}") =~ "true" ]]; then
    echo "Set $1=$2 successfully "
  else
    echo "Set $1=$2 failure "
    (( failCount++ ))
  fi
}

count=0
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
  (( count++ ))
	key=${line%%=*}
    value=${line#*=}
	addConfig "${key}" "${value}"
done

echo "========================================================================="
echo " Complete initialization parameters,  total-count:$count ,  failure-count:$failCount "
echo "========================================================================="

if [[ ${failCount} -eq 0 ]]; then
	echo " Init nacos config finished, please start seata-server. "
else
	echo " init nacos config fail. "
fi



5、执行命令,后面填写nacos的IP地址,我的是本机所以是127.0.0.1

sh nacos-config.sh 127.0.0.1
6、修改服务配置文件

# spring配置
spring: 
  datasource:
    dynamic:
      # 开启seata代理
      seata: true
	  
# seata配置
seata:
  enabled: true
  # Seata 应用编号,默认为 ${spring.application.name}
  application-id: ${spring.application.name}
  # Seata 事务组编号,用于 TC 集群名
  tx-service-group: ${spring.application.name}-group
  # 关闭自动代理
  enable-auto-data-source-proxy: false
  # 服务配置项
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      song-system-group: default
  config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:
spring:
  main:
    allow-bean-definition-overriding: true
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

#请求处理的超时时间
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000

# feign 配置
feign:
  sentinel:
    enabled: true
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
  compression:
    request:
      enabled: true
    response:
      enabled: true

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

3.11.7 远程feign调用
1 完成远程调用
    
    
    
    
    
2 添加seata应用配置文件config.txt
    service.vgroupMapping.song-test-group=default



[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGPorFne-1666438471262)(assets/image-20210816224221451.png)]

3.12 消息中间件
3.12.1 什么是消息中间件

面向消息的系统(消息中间件)是在分布式系统中完成消息的发送和接收的基础软件。消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息队列模型,可以在分布式环境下扩展进程的通信。

当今市面上有很多主流的消息中间件,如老牌的ActiveMQRabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。
1

img

3.12.2 消息中间件常用协议
A AMQP协议

AMQP即Advanced Message Queuing  Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 
优点:可靠、通用

B MQTT协议

MQTT(Message Queuing Telemetry  Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。 
优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统

C STOMP协议

STOMP(Streaming Text Orientated Message  Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented  Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。 
优点:命令模式(非topic\queue模式)

D XMPP协议

XMPP(可扩展消息处理现场协议,Extensible Messaging and  Presence  Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。 
优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大

E 其他基于TCP/IP自定义的协议

有些特殊框架(如:redis、kafka、zeroMq等)根据自身需要未严格遵循MQ规范,而是基于TCP\IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。
3.12.3 ROCKETMQ简介
消息队列RocketMQ版是阿里云基于Apache RocketMQ构建的低延迟、高并发、高可用、高可靠的分布式消息中间件。
消息队列RocketMQ版既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 
    
阿里系下开源的一款分布式、队列模型的消息中间件,原名Metaq3.0版本名称改为RocketMQ,是阿里参照kafka设计思想使用java实现的一套mq。同时将阿里系内部多款mq产品(Notify、metaq)进行整合,只维护核心功能,去除了所有其他运行时依赖,保证核心功能最简化,在此基础上配合阿里上述其他开源产品实现不同场景下mq的架构,目前主要多用于订单交易系统。

具有以下特点:

    能够保证严格的消息顺序
    提供针对消息的过滤功能
    提供丰富的消息拉取模式
    高效的订阅者水平扩展能力
    实时的消息订阅机制
    亿级消息堆积能力
      
  
3.12.4 核心名词
    
Topic
    消息主题,一级消息类型,通过Topic对消息进行分类。 

消息(Message)
    消息队列中信息传递的载体。 

Message ID
    消息的全局唯一标识,由 消息队列RocketMQ版系统自动生成,唯一标识某条消息。 

Message Key
    消息的业务标识,由消息生产者(Producer)设置,唯一标识某个业务逻辑。 

Tag
    消息标签,二级消息类型,用来进一步区分某个Topic下的消息分类。

Producer
    消息生产者,也称为消息发布者,负责生产并发送消息。 

Producer实例
    Producer的一个对象实例,不同的Producer实例可以运行在不同进程内或者不同机器上。Producer实例线程安全,可在同一进程内多线程之间共享。 

Consumer
    消息消费者,也称为消息订阅者,负责接收并消费消息。可分为两类:

        Push Consumer:消息由消息队列RocketMQ版推送至ConsumerPull Consumer:该类Consumer主动从消息队列RocketMQ版拉取消息。目前仅TCP Java SDK支持该类Consumer。
        注意 如需使用Pull Consumer,请确保您的 消息队列RocketMQ版实例为企业铂金版。


分区
    即Topic Partition,物理上的概念。每个Topic包含一个或多个分区。 

消费位点
    每个Topic会有多个分区,每个分区会统计当前消息的总条数,这个称为最大位点MaxOffset;分区的起始位置对应的位置叫做起始位点MinOffset。

    消息队列RocketMQ版的Pull Consumer会按顺序依次消费分区内的每条消息,记录已经消费了的消息条数,称为消费位点ConsumerOffset。剩余的未消费的条数(也称为消息堆积量)= 最大位点MaxOffset-消费位点ConsumerOffsetConsumer实例
    Consumer的一个对象实例,不同的Consumer实例可以运行在不同进程内或者不同机器上。一个Consumer实例内配置线程池消费消息。 

Group

    一类ProducerConsumer,这类ProducerConsumer通常生产或消费同一类消息,且消息发布或订阅的逻辑一致。

Group ID
    Group的标识。 

队列
    每个Topic下会由一到多个队列来存储消息。每个Topic对应队列数与消息类型以及实例所处地域(Region)相关,具体的队列数可 提交工单咨询。


Exactly-Once投递语义
    Exactly-Once投递语义是指发送到消息系统的消息只能被Consumer处理且仅处理一次,即使Producer重试消息发送导致某消息重复投递,该消息在Consumer也只被消费一次。

集群消费
    一个Group ID所标识的所有Consumer平均分摊消费消息。例如某个Topic9条消息,一个Group ID有3Consumer实例,那么在集群消费模式下每个实例平均分摊,只消费其中的3条消息。

广播消费
    一个Group ID所标识的所有Consumer都会各自消费某条消息一次。例如某个Topic9条消息,一个Group ID有3Consumer实例,那么在广播消费模式下每个实例都会各自消费9条消息。

定时消息
    Producer将消息发送到 消息队列RocketMQ版服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到Consumer进行消费,该消息即定时消息。

延时消息
    Producer将消息发送到 消息队列RocketMQ版服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。

事务消息
    消息队列RocketMQ版提供类似XA或Open XA的分布事务功能,通过 消息队列RocketMQ版的事务消息能达到分布式事务的最终一致。
    
顺序消息
    消息队列RocketMQ版提供的一种按照顺序进行发布和消费的消息类型,分为全局顺序消息和分区顺序消息。

全局顺序消息
    对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

分区顺序消息
    对于指定的一个Topic,所有消息根据Sharding Key进行区块分区。同一个分区内的消息按照严格的FIFO顺序进行发布和消费。Sharding Key是顺序消息中用来区分不同分区的关键字段,和普通消息的Message Key是完全不同的概念。 

消息堆积
    Producer已经将消息发送到 消息队列RocketMQ版的服务端,但由于Consumer消费能力有限,未能在短时间内将所有消息正确消费掉,此时在 消息队列RocketMQ版的服务端保存着未被消费的消息,该状态即消息堆积。 

消息过滤
    Consumer可以根据消息标签(Tag)对消息进行过滤,确保Consumer最终只接收被过滤后的消息类型。消息过滤在 消息队列RocketMQ版的服务端完成。

消息轨迹
    在一条消息从Producer发出到Consumer消费处理过程中,由各个相关节点的时间、地点等数据汇聚而成的完整链路信息。通过消息轨迹,您能清晰定位消息从Producer发出,经由 消息队列RocketMQ版服务端,投递给Consumer的完整链路,方便定位排查问题。
重置消费位点
    以时间轴为坐标,在消息持久化存储的时间范围内(默认3天),重新设置Consumer对已订阅的Topic的消费进度,设置完成后Consumer将接收设定时间点之后由Producer发送到 消息队列RocketMQ版服务端的消息。 

死信队列

    死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列RocketMQ版会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明Consumer在正常情况下无法正确地消费该消息。此时,消息队列RocketMQ版不会立刻将消息丢弃,而是将这条消息发送到该Consumer对应的特殊队列中。

    消息队列RocketMQ版将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。


消息路由
    消息路由常用于不同地域之间的消息同步,保证地域之间的数据一致性。 消息队列RocketMQ版的全球消息路由功能依托阿里云优质基础设施实现的高速通道专线,可以高效地实现不同地域之间的消息同步复制。
3.12.5 消息收发模型

消息队列RocketMQ版支持发布和订阅模型,消息生产者应用创建Topic并将消息发送到Topic。消费者应用创建对Topic的订阅以便从其接收消息。通信可以是一对多(扇出)、多对一(扇入)和多对多。

​ 具体通信如下图所示。

​ 图 1. 消息收发模型 messagingmodel

  • 生产者集群:用来表示发送消息应用,一个生产者集群下包含多个生产者实例,可以是多台机器,也可以是一台机器的多个进程,或者一个进程的多个生产者对象。

    一个生产者集群可以发送多个Topic消息。发送分布式事务消息时,如果生产者中途意外宕机,消息队列RocketMQ版服务端会主动回调生产者集群的任意一台机器来确认事务状态。

  • 消费者集群:用来表示消费消息应用,一个消费者集群下包含多个消费者实例,可以是多台机器,也可以是多个进程,或者是一个进程的多个消费者对象。

    一个消费者集群下的多个消费者以均摊方式消费消息。如果设置的是广播方式,那么这个消费者集群下的每个实例都消费全量数据。

    一个消费者集群对应一个Group ID,一个Group ID可以订阅多个Topic,如图 1中的Group 2所示。Group和Topic的订阅关系可以通过直接在程序中设置即可,具体设置方法可参见产品更新日志中的资源申请流程优化部分。

3.12.6 适用场景
https://help.aliyun.com/document_detail/112010.html

异步解耦是消息队列RocketMQ版的主要特点,主要目的是减少请求响应时间和解耦。主要的适用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时,由于使用了消息队列RocketMQ版,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦。 
    
分布式事务的数据一致性
注册系统注册的流程中,用户入口在网页注册系统,通知系统在邮件系统,两个系统之间的数据需要保持最终一致。    
 
消息的顺序收发

消息队列RocketMQ版顺序消息分为两种情况:

    全局顺序:对于指定的一个Topic,所有消息将按照严格的先入先出(FIFO)的顺序,进行顺序发布和顺序消费。
    分区顺序:对于指定的一个Topic,所有消息根据Sharding Key进行区块分区,同一个分区内的消息将按照严格的FIFO的顺序,进行顺序发布和顺序消费,可以保证一个消息被一个进程消费。

    在注册场景中,可使用用户ID作为Sharding Key来进行分区,同一个分区下的新建、更新或删除注册信息的消息必须按照FIFO的顺序发布和消费。

    
削峰填谷

流量削峰也是消息队列RocketMQ版的常用场景,一般在秒杀或团队抢购活动中使用广泛。
在秒杀或团队抢购活动中,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入 消息队列RocketMQ版。 
    
    
大规模机器的缓存同步

双十一大促时,各个分会场会有玲琅满目的商品,每件商品的价格都会实时变化。使用缓存技术也无法满足对商品价格的访问需求,缓存服务器网卡满载。访问较多次商品价格查询影响会场页面的打开速度。

此时需要提供一种广播机制,一条消息本来只可以被集群的一台机器消费,如果使用消息队列RocketMQ版的广播消费模式,那么这条消息会被所有节点消费一次,相当于把价格信息同步到需要的每台机器上,取代缓存的作用。 
3.12.7 系统部署架构

​ 系统部署架构如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IFH285Qu-1666438471263)(assets/image-20210822210340651.png)]

图中所涉及到的概念如下所述:

  • Name Server:是一个几乎无状态节点,可集群部署,在消息队列RocketMQ版中提供命名服务,更新和发现Broker服务。
  • Broker:消息中转角色,负责存储消息,转发消息。分为Master Broker和Slave Broker,一个Master Broker可以对应多个Slave Broker,但是一个Slave Broker只能对应一个Master Broker。Broker启动后需要完成一次将自己注册至Name Server的操作;随后每隔30s定期向Name Server上报Topic路由信息。
  • 生产者:与Name Server集群中的其中一个节点(随机)建立长连接(Keep-alive),定期从Name Server读取Topic路由信息,并向提供Topic服务的Master Broker建立长连接,且定时向Master Broker发送心跳。
  • 消费者:与Name Server集群中的其中一个节点(随机)建立长连接,定期从Name Server拉取Topic路由信息,并向提供Topic服务的Master Broker、Slave Broker建立长连接,且定时向Master Broker、Slave Broker发送心跳。Consumer既可以从Master Broker订阅消息,也可以从Slave Broker订阅消息,订阅规则由Broker配置决定。
3.12.8 MQ安装与部署
1   在http://rocketmq.apache.org/release_notes/release-notes-4.2.0/
    下载 Binary: rocketmq-all-4.2.0-bin-release.zip


2   系统环境变量配置

        变量名:ROCKETMQ_HOME
        变量值:MQ解压路径\MQ文件夹名




3  启动NAMESERVER

        Cmd命令框执行进入至‘MQ文件夹\bin’下,然后执行‘start mqnamesrv.cmd’,启动NAMESERVER。成功后会弹出提示框,此框勿关闭。

   启动BROKER

        Cmd命令框执行进入至‘MQ文件夹\bin’下,然后执行‘start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true’,启动BROKER。成功后会弹出提示框,此框勿关闭。

        假如弹出提示框提示‘错误: 找不到或无法加载主类 xxxxxx’。打开runbroker.cmd,然后将‘%CLASSPATH%’加上英文双引号。保存并重新执行start语句。

 RocketMQ插件部署

1. 下载

        地址:https://github.com/apache/rocketmq-externals.git

        下载完成之后,进入‘rocketmq-externals\rocketmq-console\src\main\resources’文件夹,打开‘application.properties’进行配置。

2. 编译启动

            进入‘\rocketmq-externals\rocketmq-console’文件夹,执行‘mvn clean package -Dmaven.test.skip=true’,编译生成。

        编译成功之后,Cmd进入‘target’文件夹,执行‘java -jar rocketmq-console-ng-1.0.0.jar’,启动‘rocketmq-console-ng-1.0.0.jar’。

3.测试

        浏览器中输入‘127.0.0.1:配置端口’,成功后即可查看。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Cloud是一个用于构建分布式系统的开发工具集合。它提供了一些常用的组件和框架,包括服务注册和发现、负载均衡、断路器、分布式配置等等。在使用Spring Cloud时,有一些常见的错误和注意事项需要注意。 首先,关于Spring Boot和Spring Cloud版本对应错误。在使用Spring Cloud时,需要确保Spring Boot和Spring Cloud的版本兼容。不同版本之间可能存在依赖冲突或不兼容的情况,因此需要根据官方文档或者相关文档来选择合适的版本。 另外,Spring Cloud Config是一个用于集中管理和动态获取配置的工具。它支持从Git、SVN或本地文件系统中获取配置文件,并提供了服务器和客户端支持。你可以通过官方使用说明文档了解更多关于Spring Cloud Config的详细信息。 此外,关于选择使用Nacos还是Eureka作为服务注册和发现组件的问题。Nacos是一个功能更强大的服务注册和发现组件,它整合了Spring Cloud Eureka、Spring Cloud Config和Spring Cloud Bus的功能。使用Nacos可以实现配置的中心动态刷新,而不需要为配置中心新增集群或使用消息队列。另一方面,Eureka是Spring Cloud原生全家桶的一部分,相对来说更加稳定一些。选择使用哪个组件需要根据具体的需求和项目特点来决定。 综上所述,Spring Cloud是一个用于构建分布式系统的开发工具集合,它提供了一些常用的组件和框架。在使用Spring Cloud时,需要注意Spring Boot和Spring Cloud版本的兼容性,并可以使用Spring Cloud Config来动态获取配置。同时,可以选择使用Nacos或Eureka作为服务注册和发现组件,具体选择需要根据项目需求来决定。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值