稻草问答-采纳答案与SpringCloud

稻草问答-采纳答案与SpringCloud

1 学员评论和采纳答案

1.1 创建学生的问题详情页面

学生发布了问题,讲师进行了回答,学员可以对讲师的回答进行评论。目前详情界面是老师对界面,包含回复问题和评论功能。学员进入问题详情界面不需要回答问题,只能发布评论。

如何解决这个问题呢?这里采用类似与index页面的解决方案,也就是进入问题详情页面时候根据用户的角色,如果是老师就进入讲师的detail.html,如果是学生就进入到detail_student.html。

首先将 detail.html 复制为detail_teacher.html, detail.html 是学生的问题详情页面,detail_teacher.html作为老师的问题详情页面:

然后对视图进行功能重构:

  • 因为学生界面detail.html中的不需要有的回答问题功能,将这部分视图删除
  • 学生不能删除问题,也回答问题,就将问题的删除按钮,回答按钮按钮删除,删除后,重构detail.js代码
  • 视图是两份,视图模型共享同一份即可
  • 将视图模型postAnswerApp剥离出来,存储到 post_answert.js
    • 老师页面detail_teacher.html同时引用detail.js 和 post_answer.js
    • 学生页面detail.html引用detail.js
    • 学生页面detail.html 中没有提交答案的Summernote组件,所以要删除
      summernote_init.js。

重构控制器HomeController,在detail方法中根据当前用户到角色转发页面:

  • 如果是学生就转发到detail.html
  • 如果是老师就转发到detail_teacher.html.
@GetMapping("/question/detail.html")
public ModelAndView detail(@AuthenticationPrincipal User user){
    if (user.getAuthorities().contains(STUDENT)) {
        return new ModelAndView("/question/detail");
    }else if (user.getAuthorities().contains(TEACHER)){
        return new ModelAndView("/question/detail_teacher");
    }
    throw new ServiceException("需要登录");
}

测试......

测试学生评论答案功能

1.2 采纳答案功能界面与控制器

学员或者老师可以执行采纳答案功能:

  • 学员采纳答案表示学员认可问答答案
  • 老师采纳答案,是老师认可这个答案已经很完美
  • 学员和老师采纳答案之后可以继续评论问题,这个表示学无止境
  • 采纳答案之后,将对应的问题标记为已接受状态

首先重构学生问题详情界面datail.html,添加点击采纳答案按钮时候需要二次确认的功能

<a class="btn btn-primary mx-2 text-while"
   onclick="$(this).next().toggle(300)">采纳答案</a>
<a class="badge-pill bg-success text-white"
   style="display: none; cursor: pointer"
   @click="answerSolved(answer.id,answer)">
    <i class="fa fa-check-square"></i>
</a>

这个效果的设计类似于删除功能的确认过程。

为了完成采纳答案这个功能就需要添加适当的视图模型代码和控制器,在detail.js中声明事件处理方法:

answerSolved:function (answerId,answer) {
    if (answer.acceptStatus===1){
        alert("已经解决!");
        return;
    }
    $.ajax({
        url:'/v1/answers/'+answerId+'/solved',
        method:'GET',
        success:function (r) {
            console.log(r);
            if (r.code===ACCEPTED){
                alert("解决了!");
                answer.acceptStatus=1;
            }else{
                alert(r.message);
            }
        }
    });
}

在AnswerController中声明控制器方法,测试浏览器发起的采纳答案功能请求:

@GetMapping("/{answerId}/solved")
public R solved(@PathVariable Integer answerId){
    log.debug("收到参数:{}",answerId);
    return R.accepted("接受了");
}

测试......

1.3 采纳答案功能的数据层和业务层

在AnswerMapper方法添加方法更新答案状态:

@Update("UPDATE answer SET accept_status = #{acceptStatus} " +
        "WHERE id=#{answerId}")
Integer updateAcceptStatus(@Param("answerId") Integer answerId,
                           @Param("acceptStatus") Integer acceptStatus);

AnswerMapprTests编写测试案例:

@Test
public void updateAcceptStatus(){
    int rows=answerMapper.updateAcceptStatus(36,1);
    log.debug("{}",rows);
}

在Question中定义问题常量,提升软件可读性:

public class Question implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * 已经发布待解决
     * 老师已经回复,正在解决中
     * 已经接受答案,已经解决问题
     */
    public static final Integer POSTED=0;
    public static final Integer SOLVING=1;
    public static final Integer SOLVED=2;
}

在QuestionMapper方法添加方法更新问题状态:

@Update("UPDATE question SET status=#{status} WHERE id=#{questionId}")
Integer updateStatus(@Param("questionId") Integer questionId,
                     @Param("status") Integer status);

在QustionMapperTests中添加测试案例:

@Test
public void updateStatus(){
    int rows=questionMapper.updateStatus(151, Question.SOLVING);
    log.debug("{}",rows);
}

编写IAnswerService控制器方法:

    /**
     * 接受答案,将答案状态更新,将问题状态更新
     * @param answerId 答案的ID
     * @return 更新成功就返回true,否则返回false
     */
boolean accept(Integer answerId);

在AnswerServicelmpl中实现方法,首先要更新答案的接受状态,然后更新一下问题,让问题变成解决状态:

@Override
@Transactional//两个表同时更新,不间断,非常重要
public boolean accept(Integer answerId) {
    //查询当前的答案
    Answer answer=answerMapper.selectById(answerId);
    if (answer==null){
        throw ServiceException.notFound("没有找到数据");
    }
    //如果被接受过,则不处理
    if (answer.getAcceptStatus().equals(1)){
        return false;
    }
    int rows=answerMapper.updateAcceptStatus(answerId,1);
    if (rows !=1){
        throw ServiceException.notFound("数据错误或找不到!");
    }
    rows=questionMapper.updateStatus(answer.getQuestId(), Question.SOLVED);
    if(rows !=1){
        throw ServiceException.notFound("数据错误或找不到!");
    }
    log.debug("将问题和答案更新为已经解决的状态");
    return true;
}

在AnswerServiceTests测试:

@Test
public void accept(){
    int answerId=92;
    boolean b=answerService.accept(answerId);
    System.out.println(b);
}

重构AnswerController,调用业务层处理接受答案功能:

@GetMapping("/{answerId}/solved")
public R solved(@PathVariable Integer answerId){
    log.debug("收到answerId{}",answerId);
    boolean accepted=answerService.accept(answerId);
    if (accepted){
        return R.accepted("接受了");
    }else{
        return R.gone("失败");
    }
}

1.4 重构POM文件

Maven聚合项目的好处是,可以统一管理组件,这样可以方便组件的升级等。目前项目pom文件中的组件需要进行重构,解决每个组件冗余依赖的问题,为后续使用SpringCloud打下基础

将lombok、spring-boot-starter-web、 spring-boot-starter-test 组件抽取到straw项目中,简化子项目的依赖。

2 微服务与SpringCloud

2.1 什么是微服务

2014年,Martin Fowler (马丁福勒)提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部看,与其他服务使用HTTP API通信。同时服务会使用最小的规模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现。

马丁.福勒是敏捷联盟的成员,于2001 年,同其他16名合著者一起协助创作了“敏捷软件开发宣言”。他负责维护一个bliki网站 ---一种blog和wiki的混合衍生物,他还使控制反转(Inversion of Control)“ 依赖注入模式(Dependency lnjection)" 一词得到普及。

为何使用微服务呢?

微服务就像集群作战,可以提升性能

单体应用比较适合于小项目,优点是:

  • 开发简单直接,集中式管理
  • 基本不会重复开发
  • 功能都在本地,没有分布式的管理开销和调用开销

微服务架构的优点,更适合大型系统的开发:

  • 它解决了复杂性问题。它将单体应用分解为一组服务。每个服务功能单一业务目的明确,
    开发的速度要快很多,更容易理解和维护。
  • 这种体系结构使得每个服务都可以由专注于此服务的团队独立开发。服务模块之间影响很
    小。
  • 微服务架构可以使每个微服务独立部著。减少了整体服务部署的协调问题.
  • 微服务架构使得每个服务都可独立扩展。可以实现对每个服务分别调整优化。

2.2 SpringCloud

实现微服务体系结构非常困难。任何微服务体系结构都需要解决许多问题:

  • API Gateway
  • 服务间调用
  • 服务发现
  • 服务容错
  • 服务部署
  • 数据调用

SpringCloud,它我们提供了一整套的微服务解决方案,大大的降低了微服务开发的门槛,同时也减少了开发成本。

SpringCloud并不是特指某个框架,它其实是一系列成熟框架的组合,通过SpringBoot风格的封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、容易部署的分布式系统开发工具包。

2.3 注册中心Eureka

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud- -netflix中,以实现SpringCloud的服务发现功能。

采用微服务以后,软件微服务组件各自独立,但最终还要组合为一个整体作为一个软件系统服务于最终客户,这时软件组件之间也需要彼此通讯,彼此调用方法。

微服务架构内部发起通讯调用方法的一方成为“服务消费者",提供远程方法调用的服务器称为“服务提供者”,往往为了提高系统性能,会提供多个服务器作为服务提供者,此时服务消费者找到服务提供者的过程,就类似于用户在找房间的过程。为了帮助服务消费者快速的发现服务提供者,在微服务框架中都会引入注册中心。

注册中心类似于酒店的前台,提供在软件服务的注册和发现功能,服务提供者会先在注册中心进行注册,声明可以对外提供服务,而服务消费者只需要在注册中心就可以快速发现找到可以使用的服务,快速使用服务。注册中心实现了服务提供和服务消费的快速撮合功能。

Spring Cloud中采用Eureka作为注册中心。每台服务器启动后就会到“注册中心”进行注册,告之“注册中心”说“我已经启动了,可以和别的服务器一起协同完成任务了”, 并且,Eureka服务器接收到了之后,会将各服务器的注册信息收集起来进行整理,并下发到每个服务器,使得各服务器都能知道其它服务器的存在,及其它服务器的状态。

2.4 创建Eureka注册中心项目

Spring Eureka注册中心使用便捷,少量配置即可。

在项目中使用New Module创建模块,按照SpringBoot是创建向导,项目Group和以前的各子模块保持一致,Artifact的名称添加straw eureka单词,在创建过程中选择依赖时勾选Eureka Server即可!

创建好的项目的pom.xml 中,已经添加了< dependencyManagement>节点管理SpringCloud家族的依赖,先将这些代码复制到父级项目的pom.xml 中,并且将所管理的SpringCloud的版本号声明在父级项目的pom.xml 中,暂时使用Hoxton.SR3版本,然后在整理一下依赖关系,将公共包spring- boot-starter-web、Lombok、spring-boot-starter-test,移动到dependencies,这样就可以简化子模块项目的定义,最终父项目straw的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>straw</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>straw</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <mybatis.plus.version>3.3.1</mybatis.plus.version>
        <pagehelper.starter.version>1.3.0</pagehelper.starter.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <straw-commons.version>0.0.1-SNAPSHOT</straw-commons.version>
        <pagehelper.version>5.2.0</pagehelper.version>
    </properties>

    <packaging>pom</packaging>

    <modules>
        <module>straw-portal</module>
        <module>straw-generator</module>
        <module>straw-resource</module>
        <module>straw-eureka</module>
        <module>straw-commons</module>
        <module>straw-search</module>
        <module>straw-kafka</module>
    </modules>
    <dependencies>
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot</artifactId>-->
        <!--        </dependency>-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-autoconfigure</artifactId>-->
        <!--        </dependency>-->
        <!--        <dependency>-->
        <!--            <groupId>org.testng</groupId>-->
        <!--            <artifactId>testng</artifactId>-->
        <!--            <version>7.1.0</version>-->
        <!--            <scope>test</scope>-->
        <!--        </dependency>-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-test</artifactId>-->
        <!--            <scope>test</scope>-->
        <!--        </dependency>-->
        <!-- 重构测试包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 重构依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>cn.tedu</groupId>
                <artifactId>straw-commons</artifactId>
                <version>${straw-commons.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.plus.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-extension</artifactId>
                <version>${mybatis.plus.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>${mybatis.plus.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus</artifactId>
                <version>${mybatis.plus.version}</version>
            </dependency>
            <!-- MyBatis 翻页查询工具 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper.starter.version}</version>
            </dependency>
            <!-- 添加spring cloud 家族依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper</artifactId>
                <version>${pagehelper.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>
</project>

在Eureka子模块项目中,指定父级项目为straw ,并删除不需要的各节点配置即可,最终代码为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.tedu</groupId>
        <artifactId>straw</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <artifactId>straw-eureka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>straw-eureka</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

调整完毕后,打开Maven面板,点击刷新按钮!
在application.properties中添加配置:

# Eureka 服务的默认端口号
server.port=8761

# Eureka 不自己注册到自己
eureka.client.register-with-eureka=false

# 不抓去注册表
eureka.client.fetch-registry=false

最后,在启动类的声明之前添加@EnableEurekaServer 注解:

package cn.tedu.straw.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class StrawEurekaApplication {

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

完成后,即可启动该项目了,然后,在浏览器中,访问http://localhost:8761即可看到Eureka的信息页面:

2.5 配置Eureka Client

在整个集群中,在Eureka Server中注册的其它所有服务器都称之为Eureka Client。在straw-resource 子模块项目的pom.xml 中添加:

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

由于在父项目straw 的pom.xml 中已经管理了SpringCloud家族的依赖,所以,在子模块项目中直接按照名称添加进行即可!
添加上述依赖以后,还需要在SpringBoot启动类上添加注解@EnableEurekaClient:

package cn.tedu.straw.resource;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class StrawResourceApplication {

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

当straw-resource项目添加了eureka-client 依赖后,启动项目,在Eureka服务器端的状态列表中就可以看到该项目,但是,在状态列表中,其应用程序名称(Application) 显示的是UNKNOWN,是不便于识别的,当EurekaClient更多时,更加分不清楚,所以,应该在straw-resource项目的application.properties 中添加:

spring.application.name=resource-server

其实,还可以在以上配置文件中添加配置:

eureka.client.service-url.defaultZone=http://localhost:8761/eureka

以上配置是指定Eureka注册中心的地址,其中,默认情况下,会假定当前计算机(localhost) 为Eureka Server, 并且,8761是默认的端口号,以上属性中末尾的/eureka 是固定值,所以,在本机上将Eureka Server部署到8761 端口时,各EurekaClient不需要添加以上配置!
另外:

eureka.client.fetch-registry=true
eureka.client.register-with-eureka=true

在Eureka Client中还可以配置以上2个属性,由于默认值都是true ,则可以不必配置!

2.6 Eureka的相关术语

  • Eureka Server

    Eureka服务器项目,用于接收其它服务器的注册,并整理出“注册表”,该注册表会被其它服务器抓取;
    通常,Eureka服务器项目也称之为“注册中心”。

  • Eureka Cllent
    在Eureka Server中注册的其它服务器,可能是应用服务器,也可能是其它作用的服务器,例如网关服务器等;

  • Registry (注册)
    各Eureka Client会向Eureka Server提交注册信息,主要包括:实例名(ApplicationName) . 主机地址、端口号等;

  • 注册表

    记录各Eureka Client信息的数据,也可以称之为“实例列表”;

  • fetch (抓取)

    Eureka Client从Eureka Server获取注册表;
    每当Eureka Client抓取到了注册表之后,会将注册表在本机进行缓存,以保证当Eureka Server不可用时,仍能够找到其它Eureka Client!

  • 续订租约
    Eureka Client会每间隔一段时间向Eureka Server发送消息,以进行“续订”,通过“续订"表示“我还好好的运行着”的状态;

  • 心跳周期

    Eureka Client向Eureka Server发送“续订”的间隔时间,默认时间为30秒;

  • 服务剔除

    如果某个Eureka Client在若干个心跳周期之后都没有被Eureka Server“发现",也就是长期没有发送“续订”的消息,Eureka Server就会将其从注册表中移除,即注销该实例。

  • Eureka Client缓存注册表

    Eureka Client会将从Eureka Server抓取的注册表进行本地缓存,然后,是根据本地缓存来查找其它务器的!
    Eureka在处理注册表时,是采取“增量更新”的做法的,所以,在处理过程中,消耗的时间、性能会相对较低。

2.7 Eureka Server集群

如果Eureka Server宕机了,其实各Eureka Client还是可以正常运行的,毕竟各EurekaClient缓存了注册表,通过缓存的注册表依赖可以访问到其它Eureka Client!但是,如果此时在整个集群中添加了新的Eureka Client, 或原有的某个Eureka Client也变得不可用,则缓存的注册表就是不准确的,可能导致各Eureka Client之间的访问出现问题!

为了解决这个问题,就可以使用Eureka Server集群,也就是准备多个Eureka Server,即使出现其中的某个EurekaServer不可用,只要还存在可用的Eureka Server,就可以保证整个架构的其它Eureka Client还是可以正常注册和抓取正确的注册表!

Eureka Server集群的本质就是:将每个Eureka Server也当成一个Eureka Client,彼此互相注册,并彼此提供注册被对方抓取。

假设存在3个Eureka Server,为了便于表达,给它们1 、2、 3的编号,假设它们分别部著在localhost:8761. localhost:8762 和localhost:8763.
在Eureka Server 1中进行配置:

server.port=8761
spring.application.name=eurekal
eureka.client.service-url.defaultZone=http://localhost:8762/eureka,
http://localhost:8763/eureka
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true

注:在实际的集群中,各Eureka Server使用相同的端口是可以的,毕竟它们是运行在不同的服
务器计算机上的。

则在Eureka Server 2中配置:

server.port=8762
spring.application.name=eureka2
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,
http://localhost:8763/eureka
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true

在Eureka Server 3中配置:

server.port=8763
spring.application.name=eureka3
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,
http://localhost:8762/eureka
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true

2.8 Eureka Server的自我保护机制

Eureka Server会在运行期间统计“丢失心跳比例”,任何连续3次心跳续订失败的EurekaClient都会被认为存在不干净的终止,如果15分钟之内超过85%的Eureka Client都没有正常的心跳,则会开启“自我保护机制”,此时,在Eureka Server的状态页面就会出现警告:

提示:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN
    THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES
    ARE NOT BEING EXPIRED JUST TO BE SAFE.

其大意如下:

注意! Eureka可能错误将某些已经下线的实例声明为在线,为了安全起见,由于续订比例尚且低于國值,因此这些实例并不会声明为过期。

一旦开启自我保护机,会有如下表现:

  • Eureka Server不再从注册表中剔除长时间没有心跳的Eureka Client;

  • Eureka Server不再同步注册表到其它Eureka Client,即使存在新的Eureka Client注册;

  • 当故障排除后,会自动恢复正常。

    一般,在正常的生产环境中,导致出现自我保护的原因可能有:

  • 启动了Eureka Server, 但是,在接下来较长的时间里,没有启动任何Eureka Client;

  • 启动了整个架构后,由于网络波动、网络设备故障、网络负载、某些Eureka Client负载等故障,导致大量Eureka Client不可用或其网络不可用;

  • 异常关闭了Eureka Client,例如机房断电、死机、强行终止进程等。

当Eureka Server进行自我保护机制状态后,再有任何Eureka Client出现任何故障,对于其它的Eureka Client来说,都是未知的,就仍可能尝试远程调用,这些远程调用大概率是会失败的,并且,即使新上线了一些Eureka Client,对于原有的Eureka Client也是未知的!所以,当Eureka Server进入自我保护机制后,应该及时排查各Eureka Client是否出现故障,并且,如果需要下线某个Eureka Client,应该遵循Eureka协议的要求, 通过调用以下方法,下线Eureka Client:

DiscoveryManager.getInstance().shutdownComponent();

3 Zuul网关

3.1 网关概述

在分布式系统中,软件系统的服务模块部署在多个服务器上,这样客户端访问就需要访问不同的服务器。ZuuI网关路由可以提供整套架构的统一入口, 在基于微服务的项目及集群项目中,可以实现外部与内部的隔离,对外隐藏所有细节,增强了整套后台服务系统的稳定性和安全性。

Zuul网关路由是由一系列的过滤器( Fiter) 来实现的,其主要作用有:

  • 动态路由;

  • 洞察与监控;

  • 身份验证与授权等安全控制;

  • 其它(查阅资料)。

    Zuul网关路由的底层是使用Ribbon实现的路由,并内置了Hystrix,可选择性提供网关
    fallback逻辑。

3.2 创建Zuul网关项目

在项目中,再次New Module,使用SpringBoot的创建向导, 项目Group 和以前的各子模块保持一致,Artifact 使用straw-gateway ,在创建过程中选择依赖时勾选Zuul即可!

当子模块项目创建出来后,先修改其父项目为straw项目,保留zuul 的依赖,并添加eureka-client的依赖即可:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.tedu</groupId>
        <artifactId>straw</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <artifactId>straw-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>straw-gateway</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>cn.tedu</groupId>
            <artifactId>straw-commons</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3.3 通过网关代理straw-resource项目

在straw-gateway的application.properties 中添加配置:

server.port=9000
spring.application.name=gateway
## 配置路由规则:请求/resource/**转到resource-server
zuul.routes.resource.path=/resource/**
zuul.routes.resource.service-id=resource-server

并在StrawGatewayApplication启动类之前添加@EnablezuulProxy 注解:

package cn.tedu.straw.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableZuulProxy
@EnableRedisHttpSession
public class StrawGatewayApplication {

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

    /**
     * 创建RestTemplate,封装了Ribbon的客户端
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

然后,启动straw-gateway项目.
原本,可以通过http://localhsot:8899/demo.png访问到某张图片,现在就可以改为http://localhsot:9000/resource/demo.png来访问了!

注意:在执行时,务必保证名为resource-server的项目是可以正常运行且在Eureka Server注册了的!整个过程中,网关只是起到了“代理’的作用,用于被客户端访问,其实并不提供实质的请求处理,真正处理请求的还是名为resource-server的项目!

注意:当能够正常访问时,其实通过straw-resource和straw-gateway都可以访问到相同的资源,看似Straw-gateway的价值并不大!在实际的生产环境里,只需要将straw-gateway部署的服务器连接到因特网,而straw-resource只需要与straw-gateway通过局域网连接即可,在外界是不可能直接访问到straw-resource项目的,使得入口更加统一,并且,各具体的应用服务器更加安全,如果外界无法攻克straw-gateway项目部著的服务器,是不可能侵入到各应用服务器的!

posted @ 2022-04-12 22:52  指尖上的未来  阅读(10)  评论(0)    收藏  举报