# 认识微服务

  • 单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署
  • 分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务
  • 微服务技术是分布式架构(把服务做拆分)的一种

微服务技术栈

  • springcloud仅仅是解决了拆分时的微服务治理的问题,其他更复杂的问题并没有给出解决方案

微服务技术栈

  • 微服务框架:SpringCloud和阿里的Dubbo、SpringCloudAlibaba,SpringCloudAlibaba兼容前两种

微服务技术栈

微服务技术栈

https://start.aliyun.com
  • 微服务框架,创建一个Maven项目,引入关键依赖
<?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 
		                     http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.qianshun</groupId>
    <artifactId>Nacos</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>order</module>
        <module>stock</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.8.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.spring.boot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <dependencies>
	    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>
	
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

注意

SpringBoot2.4(spring-cloud-dependencies 2020)版本之后不会默认加载bootstrap.yaml

# 远程调用 Feign

# RestTemplate

RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版

# 使用步骤

  • 注册RestTemplate
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    } 
	
    //在order-service的OrderApplication中注册RestTemplate
    @Bean
	//实现负载均衡
	@LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  • 服务远程调用RestTemplate
//修改order-service中的OrderService的queryOrderById方法
@Service
public class OrderService {
    
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // TODO 2.查询用户 
        String url = "http://localhost:8081/user/" +  order.getUserId();
        User user = restTemplate.getForObject(url, User.class);
        // 3.封装user信息
        order.setUser(user);
        // 4.返回
        return order;
    }
}

# 请求方法

//POST请求
调用postForObject方法
调用postForEntity方法 
调用exchange方法
  • postForObject和postForEntity方法的区别主要在于可以在postForEntity方法中设置header的属性
  • exchange方法和postForEntity类似,但是更灵活,exchange还可以调用get请求

GET请求方式同上

//1. 简单Get请求
String result = restTemplate.getForObject(rootUrl + "get1?para=my", String.class);
System.out.println("简单Get请求:" + result);

//2. 简单带路径变量参数Get请求
result = restTemplate.getForObject(rootUrl + "get2/{1}", String.class, 239);
System.out.println("简单带路径变量参数Get请求:" + result);

//3. 返回对象Get请求
ResponseEntity<Test1> resEntity=restTemplate.getForEntity(rootUrl + "get3/3",Test1.class);
System.out.println("返回:" + resEntity);
System.out.println("返回对象Get请求:" + responseEntity.getBody());

//4. 设置header的Get请求
HttpHeaders headers = new HttpHeaders();
headers.add("token", "123");
ResponseEntity<String> res = restTemplate.exchange(rootUrl + "get4", HttpMethod.GET, 
									        new HttpEntity<String>(headers), String.class);
System.out.println("设置header的Get请求:" + response.getBody());

//5. Post对象
Test1 test1 = new Test1();
test1.name = "buter";
test1.sex = 1;

result = restTemplate.postForObject(rootUrl + "post1", test1, String.class);
System.out.println("Post对象:" + result);

//6. 带header的Post数据请求
response = restTemplate.postForEntity(rootUrl + "post2", 
                                      new HttpEntity<Test1>(test1, headers),String.class);
System.out.println("带header的Post数据请求:" + response.getBody());

//7. 带header的Put数据请求
//无返回值
restTemplate.put(rootUrl + "put1", new HttpEntity<Test1>(test1, headers));
//带返回值
response = restTemplate.exchange(rootUrl + "put1", HttpMethod.PUT, 
                                 new HttpEntity<Test1>(test1, headers), String.class);
System.out.println("带header的Put数据请求:" + response.getBody());

//8. del请求
//无返回值
restTemplate.delete(rootUrl + "del1/{1}", 332);
//带返回值
response = restTemplate.exchange(rootUrl + "del1/332", HttpMethod.DELETE, 
                                                       null, String.class);
System.out.println("del数据请求:" + response.getBody());

# HTTP请求上传文件和参数

  • 上传的文件是File类型
    如果文件保存在本地,即可以通过new File(path)获取到指定文件
public String uploadFile(File file) {
    // 1、封装请求头
    HttpHeaders headers = new HttpHeaders();
    MediaType type = MediaType.parseMediaType("multipart/form-data");
    headers.setContentType(type);
    headers.setContentLength(file.length());
    headers.setContentDispositionFormData("media", file.getName());
	
    // 2、封装请求体
    MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
    FileSystemResource resource = new FileSystemResource(file);
    param.add("file", resource);
	
    // 3、封装整个请求报文
    HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(param, headers);
	
    // 4、发送请求
    ResponseEntity<String> data = restTemplate.postForEntity(tempMaterialUploadUrl, 
	                                                             formEntity, String.class);
	
    // 5、请求结果处理
    JSONObject weChatResult = JSONObject.parseObject(data.getBody());
    return weChatResult;
}
  • 上传的文件是InputStream流
    文件不存在本地只能够通过URL获取文件流,并且不想将文件存到本地而直接通过restTemplate发送
//在需要输入流进行上传文件时,需要使用InputStreamResource构建资源文件
//注意要重写contentLength() 和 getFilename()方法,否则不成功
public String uploadInputStream(InputStream inputStream,String fileName,long cententLength){
    // 1、封装请求头
    HttpHeaders headers = new HttpHeaders();
    MediaType type = MediaType.parseMediaType("multipart/form-data");
    headers.setContentType(type);
    headers.setContentDispositionFormData("media", fileName);
	
    // 2、封装请求体
    MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
    InputStreamResource resource = new InputStreamResource(inputStream){
        @Override
        public long contentLength(){
            return cententLength;
        }
        @Override
        public String getFilename(){
            return fileName;
        }
    };
    param.add("file", resource);
	
    // 3、封装整个请求报文
    HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(param, headers);
	
    // 4、发送请求
    ResponseEntity<String> data = restTemplate.postForEntity(tempMaterialUploadUrl, 
	                                                              formEntity, String.class);
	
    // 5、请求结果处理
    JSONObject weChatResult = JSONObject.parseObject(data.getBody());
	
    // 6、返回结果
    return weChatResult;
}
  • 上传的是MultipartFile类型文件
    有时候我们需要直接将系统上传SpringMVC通过MultipartFile类型接收的文件
public String uploadFileWithInputStream(MultipartFile file) throws IOException {
    // 1、封装请求头
    HttpHeaders headers = new HttpHeaders();
    MediaType type = MediaType.parseMediaType("multipart/form-data");
    headers.setContentType(type);
    headers.setContentLength(file.getSize());
    headers.setContentDispositionFormData("media", file.getOriginalFilename());
	
    // 2、封装请求体
    MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
    // 将multipartFile转换成byte资源进行传输
    ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
        @Override
        public String getFilename() {
            return file.getOriginalFilename();
        }
    };
    param.add("file", resource);
	
    // 3、封装整个请求报文
    HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(param, headers);
    // 4、发送请求
    ResponseEntity<String> data = restTemplate.postForEntity(tempMaterialUploadUrl, 
	                                                              formEntity, String.class);
    // 5、请求结果处理
    JSONObject weChatResult = JSONObject.parseObject(data.getBody());
    // 6、返回结果
    return weChatResult;
}

# 服务调用关系

  • 服务提供者:暴露接口给其它微服务调用
  • 服务消费者:调用其它微服务提供的接口
  • 提供者与消费者角色其实是相对的
  • 一个服务可以同时是服务提供者和服务消费者

# Feign

  • Feign (opens new window)是一个声明式的http客户端,其作用就是帮助我们优雅的实现http请求的发送
  • Feign 内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务
  • Feign本身并不支持Spring MVC的注解,它有一套自己的注解,为了更方便的使用Spring Cloud孵化了OpenFeign。并且支持了Spring MVC的注解,如@RequestMapping,@PathVariable等等

注意

从Spring Cloud 2020版本开始,Spring Cloud移除了 Ribbon,使用Spring Cloud Loadbalancer作为客户端的负载均衡组件

# 使用步骤

  • 引入依赖
<!--我们在order-service服务的pom文件中引入feign的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 添加注解
//在order-service的启动类添加注解开启Feign的功能
@EnableFeignClients
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
  • 编写Feign的客户端接口,注意:是接口
//在order-service中新建一个接口
@FeignClient(contextId = "remoteUserService", value ="userService", 
            //对应RequestMapping("/user"),下面就可以写 @GetMapping("/{id}")
            path="/user",
			//需要设置 feign.hystrix.enabled: true 或者 feign.sentinel.enabled: true
            fallbackFactory = RemoteFileFallbackFactory.class)
public interface UserClient {
	
    @GetMapping("/user/{id}")
	//spingMVC中"id"参数可以省略,但是在此处不能省略
    User findById(@PathVariable("id") Long id);
}
  • 调用测试
//修改order-service中的OrderService类中的queryOrderById方法
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
	Order order = orderService.queryOrderById(orderId);
	User user = userClient.findById(order.getUserId());
	order.setUser(user);
	return  order;
}

使用feign远程调用 could not be found cloud

原因:没在启动类扫描feign 接口所在的包,需要加上@EnableFeignClients("com.ruoyi")或者@EnableFeignClients(basepackages="com.ruoyi")

RequestParam.value() was empty on parameter 0

这个时候找到具体的client就会发现问题出在参数上,spring在构建bean时没找到具体的参数

//类似的参数括号里的userId忘记写是出现的原因
@RequestParam"userId"String userId

//@PathVariable也同样
@PathVariable("userId") String storeId

GET方式调用实体类参数

关于实体类参数,feign默认是支持post请求的。直接调用GET型的实体类参数接口会产生405报错或参数为空,可以将参数转map

@PostMapping()
String getByBody1(@Requestbody Product product);

@GetMapping()
String getByBody2(@RequestParam("product") Map product);

# 参数设置

  • GET方式:需要在请求参数前加上@RequestParam注解修饰,Controller里面可以不加
@RequestMapping(value="/test", method=RequestMethod.GET)  
Model test(@RequestParam("name") final String name,@RequestParam("age")  final int age);  
  • POST方式:可以有多个@RequestParam,但只能有一个@RequestBody
public int save(@RequestBody Person p,@RequestParam("userId") String userId);
  • 如果调用feign接口上传文件时,@requestmapping 属性 需要加上consumes
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysFile> upload(@RequestPart(value = "file") MultipartFile file);

注意

List< String > 类型需要添加@RequestParam

downFile(@RequestParam(value ="lImgs") List<String> lImgs, HttpServletResponse response)

# 自定义配置

Feign可以支持很多的自定义配置,如下表所示:

类型 作用 说明
feign.Logger.Level 修改日志级别 四种级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 解析http远程调用的结果,如解析json为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign.Contract feigin原生注解格式 默认是SpringMVC的注解
feign.Retryer 请求失败重试机制 默认是没有,不过会使用Ribbon的重试

一般情况下默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可

# 基于配置文件的方式修改配置

  • 修改feign的日志级别可以针对单个服务
feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        logger-level: full #  日志级别 
  • 也可以针对所有服务
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        logger-level: full #  日志级别 
		
		contract: feign.Contract.Default # 还原成原生注解
  • 而日志的级别分为四种:日志级别尽量用basic
NONE    # 不记录任何日志信息,这是默认值
BASIC   # 仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS # 在BASIC的基础上,额外记录了请求和响应的头信息
FULL    # 记录所有请求和响应的明细,包括头信息、请求体、元数据

注意

springboot 默认的日志级别是 info,feign 的 dubug 日志级别就不会输出,需要修改

logging:
   level:
      com.sylone.order.feign: debug

或者修改logback的配置

<logger name="com.ruoyi" level="debug" />
<logger name="com.netflix" level="debug" />

# Java代码方式修改配置

  • 先声明一个类,然后声明一个Logger.Level的对象
@Configuration
public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}
  • 如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 
  • 如果是局部生效,则把它放到对应的@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 

# 请求超时

  • SpringCloud下Feign单独使用超时时间设置
# 针对某个微服务的配置
feign:  
  client:
    config: 
      userservice: 
        # 连接超时时间,默认2s
		connectTimeout: 5000
		# 请求处理超时时闻,默认5s
		readTimeout: 3000
		
# 针对所有服务的配置
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        # 连接超时时间,默认2s
		connectTimeout: 5000
		# 请求处理超时时闻,默认5s
		readTimeout: 3000
  • SpringCloud下通过Ribbon来设置,超时时间可以由Ribbon配置(Feign默认集成了Ribbon)
# 全局配置
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000
  
  
# 局部配置,,royi-xxxx 为需要调用的服务名称
ruoyi-xxxx:
  ribbon:
    ReadTimeout: 10000
    ConnectTimeout: 10000
  • Java代码方式修改请求超时
@Configuration
public class FeignConfig{
	@Bean
	public Request.Otions options(){
		//第一个参数是连接超时时间(ms),默认2s
		//第二个参数是请求处理超时时间(ms),默认是5s
		return new Request.Option(5000,10000)
	}
}

注意

Ribbon默认连接和读超时时间只有1s,所以在默认情况下,Feign的超时时间只有1s

如何在默认情况下将超时时间交给Ribbon管理 (opens new window)

# Gzip压缩

gzip是一种数据格式,采用deflate算法压缩数据。gzip大约可以帮我们减少70%以上的文件大小。

  • 全局配置
server:
  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  

注意

  • 全局配置是针对所有的feign调用无法在前端看到效果,需要配合局部配置使用
  • 开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把压缩的文档适度调大一点

# Feign使用优化

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

URLConnection  # 默认实现,不支持连接池
Apache HttpClient  # 支持连接池
OKHttp  # 支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection

这里我们用Apache的HttpClient来演示:

  • 引入依赖:在order-service的pom文件中引入Apache的HttpClient依赖
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

<dependency>
    <groupId>com.squareup.okhttp</groupId>
    <artifactId>okhttp</artifactId>
    <version>2.7.5</version>
</dependency>

注意

spring-cloud-starter-openfeign中已经集成

  • 配置连接池:在order-service的application.yml中添加配置
feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

# 请求拦截器

可以通过实现feign.RequestInterceptor接口在feign执行后进行拦截,对请求头等信息进行修改

//利用feign拦截器将本服务的userId、userName、authentication传递给下游服务
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
    @Override
    public void apply(RequestTemplate requestTemplate)
    {
        HttpServletRequest httpServletRequest = ServletUtils.getRequest();
        if (StringUtils.isNotNull(httpServletRequest))
        {
            Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
            // 传递用户信息请求头,防止丢失
            String userId = headers.get(CacheConstants.DETAILS_USER_ID);
            if (StringUtils.isNotEmpty(userId))
            {
                requestTemplate.header(CacheConstants.DETAILS_USER_ID, userId);
            }
            String userName = headers.get(CacheConstants.DETAILS_USERNAME);
            if (StringUtils.isNotEmpty(userName))
            {
                requestTemplate.header(CacheConstants.DETAILS_USERNAME, userName);
            }
            String authentication = headers.get(CacheConstants.AUTHORIZATION_HEADER);
            if (StringUtils.isNotEmpty(authentication))
            {
                requestTemplate.header(CacheConstants.AUTHORIZATION_HEADER, authentication);
            }
        }
    }
}

使用方式

feign:
  client:
    config:
	  order-service:
	    requestInterceptors[0]: com.sylone.interceptor.FeignRequestInterCeptor

# @InnerAuth注解的使用

接口访问分为两种,一种是由Gateway网关访问进来,一种是内网通过Feign进行内部服务调用

如果开放了内部访问的接口,就会访问到localhost:8080/user/1,这样暴露出去就很危险了,所以对于这种情况我们可以使用@InnerAuth内部注解

  • 服务被调用方新增Header参数from-source
@FeignClient(value ="userservice")
public interface UserClient {
	
    @GetMapping("/user/{id}")
	//新增Header参数from-source
    User findById(@PathVariable("id") Long id, 
	              @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}
  • 服务调通方
@InnerAuth
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
	Order order = orderService.queryOrderById(orderId);
	User user = userClient.findById(order.getUserId());
	order.setUser(user);
	return  order;
}

加了@InnerAuth注解每次会先进去到InnerAuthAspect.java处理,验证请求头是否为from-source,且携带内部标识参数inner。如果非内部请求访问会直接抛出异常。

但是网关访问的时候,可以手动带上这个from-source参数。所以在网关AuthFilter.java过滤器中对来源参数做了清除,防止出现安全问题。

// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);

# FeignClient整合Sentinel

微服务远程调用都是基于Feign来完成的,因此需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断

  • 修改配置,开启sentinel功能
feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持
  • 编写失败降级逻辑
@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
                log.error("查询用户异常", throwable);
                return new User();
            }
        };
    }
}
  • 在UserClient接口中使用UserClientFallbackFactory
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
  • 然后查看 sentinel 控制台,可以看到新的簇点链路

FeignClient整合Sentinel

注意 启动报错 Requested bean is currently in creation: Is there an unresolvable circular reference?

修改 Spring Cloud 版本为 Hoxton.SR8 启动成功

Spring Boot 版本为:2.3.9.RELEASE
Spring Cloud 版本为:Hoxton.SR8
Spring Cloud Alibaba 版本为:2.2.5.RELEASE

# 注册中心 Eureka

Eureak 是Netflix 开源微服务框架中一系列项目中的一个

Eureka的作用

  • EurekaServer:服务端,注册中心
    • 记录服务信息
    • 心跳监控
  • EurekaClient:客户端
    • Provider:服务提供者,例如案例中的 user-service
      • 注册自己的信息到EurekaServer
      • 每隔30秒向EurekaServer发送心跳
    • consumer:服务消费者,例如案例中的 order-service
      • 根据服务名称从EurekaServer拉取服务列表
      • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

# 搭建Eureka服务端

  • 创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  • 编写启动类,添加@EnableEurekaServer注解
  • 添加application.yml文件,编写下面的配置:
server:
  port: 10086
spring:
  application:
    name: eurekaserver
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka/

eureka了为什么还要配置自己的地址信息

  • 因为eureka自己也是一个微服务,eureka在启动时,会将自己也注册到eureka上
  • 为了将来eureka集群之间通信用的。这里的配置服务名称和服务地址是为了做服务注册

# 搭建Eureka客户端

  • 在user-service项目引入spring-cloud-starter-netflix-eureka-client的依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 在application.yml文件,编写下面的配置
spring:
  application:
    name: userservice # user服务的服务名称
eureka:
  client:
    service-url:  #eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka/

# 模拟多实例部署方式一

找到Services中的UserApplication,右键,点击Copy Configuration

模拟多实例部署

模拟多实例部署

  • 设置名称
  • 在VM options中加:-Dserver.port=8082 -D代表参数
  • 在Active profiles中设置:运行环境(dev、pro、test)

# 模拟多实例部署方式二

spring:
  application:
    name: user-service
server:
  servlet:
    context-path: /user

---
spring:
  profiles: user-service-master
server:
  port: 9091

---
spring:
  profiles: user-service-slave
server:
  port: 9092

# 负载均衡的实现

  • 导入依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 在application.yml文件,编写下面的配置
spring:
  application:
    name: orderService # user服务的服务名称
eureka:
  client:
    service-url:  #eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka/
  • 修改OrderService的代码,修改访问的url路径,用服务名代替ip,端口
String url = "http://userService/user/"+order.getUserId();
User user = restTemplate.getForObject(url, User.class);
  • 在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

# 负载均衡 Ribbon

# 负载均衡的原理

Spring Cloud Ribbon是基于Netflix实现的一套客户端的负载均衡工具,通过LoadBalancer获取到所有的服务提供者,IRule会自动基于某种规则(轮询、随机)调用这些服务

  • 请求进入Ribbon之后会被一个拦截器(LoadBalancerInterceptor负载均衡拦截器)拦住
  • 这个拦截器实现了一个接口(ClientHttpRequestInterceptor)它会去拦截由客户端发起的http请求
  • 在Interceptor方法中得到请求中的服务名称,然后把它交给(RibbonLoadBanlancerClient)
  • RibbonLoadBanlancerClient又将服务交给DynamicServerListLoadBalancer动态服务列表负载均衡器
  • 然后DynamicServerListLoadBalancer又会去eureka-server里拉取服务列表
  • 之后,DynamicServerListLoadBalancer会去找 IRule
  • 而IRule会基于规则选择某个服务,并将选中的服务返回给RibbonLoadBanlancerClient
  • 最后RibbonLoadBanlancerClient就会用得到的真实请求地址然后就去完成请求 负载均衡原理

# 负载均衡的策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,默认的实现是ZoneAvoidanceRule

RoundRobinRule # 轮询
RandomRule # 随机
ZoneAvoidanceRule # 复合判断server所在区域的性能和server可用性选择服务器
RetryRule # 在指定的时间内重试,获取可用的服务
WeightedResponseTimeRule # 根据权重,服务响应时间越短,权重越大,被选中的概率也越大
AvailabilityFilteringRule # 滤掉有故障的服务,然后对剩余的服务列表进行轮询
BestAvailableRule # 滤掉有故障的服务,然后选择并发量最小的服务

# 修改负载均衡规则

  • 配置类的方式
//指定负载均衡策略
@Configuration
public class RibbonConfig {
    @Bean
    public IRule iRule(){
        // Nacos提供的负载均衡策略(优先调用同一集群下的实例,基于随机权重)
        return new NacosRule();
    }
}

//利用@RibbonClient指定微服务及其负载均衡策略
@SpringBootApplication
@RibbonClients(value={
	@RibbonClient(name = "stock-server",configuration = RibbonConfig.class),
	@RibbonClient(name = "product-server",configuration = RibbonConfig.class)
})
public class OrderApplication{
	public static void main(String[] args){
		SpringApplication.run(OrderApplication.class,args);
	}
}

注意

配置类不能写在@SpringbootApplication注解的@CompentScan扫描的到的地方,否则自定义的配置类就会被所有的RibbonClients共享,所以可放在其他目录下

配置类的方式

  • 配置文件的方式
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos权重策略

# 自定义负载均衡

继承AbstractLoadBalancerRule即可,比如挨个每个访问5次

public class MyRibbonConfig extends AbstractLoadBalancerRule {
    /**
     * 默认等于0,如果等于5,指向下一个服务
     */
    private int total = 0;
    /**
     * 默认=0,如果total=5,currentIndex++
     */
    private int currentIndex = 0;

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        } else {
            Server server = null;

            while (server == null) {
                if (Thread.interrupted()) {
                    return null;
                }
                //获得所有的服务
                List<Server> upList = lb.getReachableServers();
                //获得活着的服务
                List<Server> allList = lb.getAllServers();

                int serverCount = allList.size();
                if (serverCount == 0) {
                    return null;
                }
                if (total < 5) {
                    server = upList.get(currentIndex);
                    total++;
                } else {
                    total = 0;
                    currentIndex++;
                    if (currentIndex > upList.size()) {
                        currentIndex = 0;
                    }
                    server = upList.get(currentIndex);

                }

                if (server == null) {
                    Thread.yield();
                } else {
                    if (server.isAlive()) {
                        return server;
                    }

                    server = null;
                    Thread.yield();
                }
            }

            return server;
        }
    }

    protected int chooseRandomInt(int serverCount) {
        return ThreadLocalRandom.current().nextInt(serverCount);
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // TODO Auto-generated method stub

    }
}

注入我们自己写的负载均衡策略

@Configuration
public class RibbonConfig {
    /**
     * 指定负载均衡策略
     */
    @Bean
    public IRule iRule(){
        // Nacos提供的负载均衡策略(优先调用同一集群下的实例,基于随机权重)
        return new MyRibbonConfig();
    }
}

# 饥饿加载/懒加载

为什么第一次请求的耗时如此之长呢?
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,因为创建的过程中要去做服务拉取,所以请求时间会很长。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时。

# 开启饥饿加载
ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients:  # 指定饥饿加载的服务名称
      - userservice

# 使用LoadBalancer替代Ribbon

Spring Cloud LoadBalancer是Spring Cloud官方自己提供的客户端负载均衡器,用来代替Ribbon
Spring Cloud 2021版已经移除了Ribbon

Spring官方提供了两种负载均衡客户端:

  • RestTemplate:用于访问Rest服务的客户端
  • WebClient:是一个非阻塞的基于响应式编程的HTTP请求客户端。它的响应式编程基于Reactor

整合LoadBalancer只需要替换依赖即可

<!--添加loadbalancer依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--Nacos服务注册与发现,移除Ribbon支持-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	<!--2021版后已经自动移除-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </exclusion>
    </exclusions>
</dependency>

也可以在yml进行配置剔除

spring:
  application:
    name: order-server
  cloud:
    nacos:
	  discovery:
	    server-addr: 127.0.0.1:8848
    loadbalancer:
      ribbon:
        enable: false

LoadBalancer默认的负载均衡是:RoundRobinLoadBalancer(线性轮询负载均衡器)可以仿照此方法自定义负载均衡器

public class CustomLoadBalancerConfiguration {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

@Configuration
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {

    @Bean
    @LoadBalanced
    public WebClient.Builder loadBalancedWebClientBuilder() {
        return WebClient.builder();
    }
}

# 注册配置 Nacos

Nacos (opens new window)是阿里巴巴的产品,默认端口是8848
现在是SpringCloud中的一个组件,比Eureka功能更加丰富,在国内受欢迎程度较高

注意

Nacos 2021版已经没有自带的Ribbon,需要单独添加依赖Spring Cloud LoadBalancer

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

# 启动Nacos

# -m代表模式,standalone代表单机启动(还有集群启动)
startup.cmd -m standalone

# 修改配置文件启动数据库

Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库

  • derby-schema.sql:derby数据库的SQL脚本
  • mysql-schema.sql:mysql数据库的SQL脚本
# If use MySQL as datasource:
spring.datasource.platform=mysql

# Count of DB:
db.num=1

# Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=nacos
db.password.0=nacos

# 问题Unable to start embedded Tomcat

在conf文件目录下,将cluster.conf.example文件名后面的example删掉,且将文件内部的地址修改为我们的本机地址

# example
# 192.168.16.101:8847
# 192.168.16.102
# 192.168.16.103

192.168.31.171

# Nacos 开启鉴权

nacos 默认不需要登录,开启 nacos 账号密码登录,首先要修改3个配置

nacos.core.auth.enabled=true #默认为false

# 开启服务身份识别功能
# 所有集群均需要配置相同的server.identity信息,否则可能导致服务端之间数据不一致
nacos.core.auth.server.identity.key=nacosKey #默认为空
nacos.core.auth.server.identity.value=nacosValue #默认为空

# 在这个网址:https://www.sojson.com/base64.html 随便输入一个32位的字符串
nacos.core.auth.plugin.nacos.token.secret.key=默认为空

修改 nacos 的默认密码需要在 nacos 的管理平台或者数据库中

# 监听的端口

由于 Nacos 2x版本引入了gRPC,它会额外占用其他三个端口

7848 # 实现Nacos集群通信,一致性选举,心跳检测等功能
8848 # Nacos主端口,对外提供服务的Http端口
9848 # 客户端gRPC请求服务端端口,用于客户端向服务端发起连接和请求,主端口(8848)+ 1000 偏移量
9849 # 服务端gRPC请求服务端端口,用于服务间同步等,该端口的配置为:主端口 + 1001偏移量

# Spring Cloud Commons

Spring Cloud Commons (opens new window)主要是定义了通用接口规范:

DiscoveryClient Interface # 服务发现接口
ServiceRegistery Interface # 服务注册接口

所以不论是Eureka还是Nacos,只要是做服务注册发现,都会遵循这些接口,改变的只是依赖和服务地址

# 服务注册到Nacos

  • 在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  • 添加nacos的客户端依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 修改user-service&order-service中的application.yml文件,注释eureka地址,添加nacos地址
spring:
  cloud:
    nacos:
	  server-addr:localhost:8848 #nacos 服务地址

注意

需要添加依赖spring-boot-starter-web,否则服务启动后就关闭了

# 命名空间

可以通过命名空间基于开发环境进行隔离

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID

创建namespace

# 多级存储模型

  • 一级是服务,例如:UserService
  • 二级是集群,例如:上海或杭州,默认DEFAULT没有集群,优先选择同集群服务
  • 三级是实例,例如:杭州机房的某台部署userservice的服务器
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

多级存储模型

# 权重设置

  • 但默认情况下NacosRule是同集群内随机挑选,同集群内看权重
  • Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
  • 如果权重修改为0,则该实例永远不会被访问,可以作为服务升级的一种方案

权重设置

注意

同个命名空间看集群,同个集群看权重

# 永久实例(非临时实例)

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例

# Nacos与Eureka的对比

Nacos与eureka的共同点:

# 都支持服务注册和服务拉取
# 都支持服务提供者心跳方式做健康检测,15s没有收到心跳会被剔除

Nacos与Eureka的区别:

# Nacos支持服务端主动检测提供者状态
  # 临时实例采用心跳模式(5s发送一次心跳),不正常会被剔除
  # 非临时实例采用主动检测模式,不会从服务列表剔除
# Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
# Nacos集群默认采用AP模式,集群中存在非临时实例时,采用CP模式,支持AP和CP的切换,Eureka采用AP模式

# 保护阈值

  • 保护阈值(设置0-1之间的值)与集群中健康实例的占比有关
  • 若健康实例占比<=此值,Nacos 会将全部实例(健康实例 + 非健康实例)返回给调用者
  • 防止造成雪崩效应,导致系统崩溃
  • 保护阈值未触发时,Nacos 只会把健康实例返回给调用者

保护阈值

# 统一配置管理

  • 引入nacos-config依赖
<!--nacos配置管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  • 添加bootstrap.yaml
spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev #开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名
  • 配置文件ID设置
${spring.application.name}-
${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

统一配置管理

注意

  • Nacos默认是Properties的扩展名,如果设置成了非Properties必须通过file-extension进行设置
  • 配置管理里面也区分命名空间,默认是在public下,否则config下需要配置namespace
spring:
  cloud:
    nacos:
      config:
        file-extension: yaml
        namespace: fac8bfac-75a3-496e-b7f3-b89ff2c7db2b

# 从微服务拉取配置

  • 方式一:在@Value注入的变量所在类上添加注解@RefreshScope
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;
    
    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}
  • 方式二:使用@ConfigurationProperties注解代替@Value注解
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("now")
    public String now(){
        return DateTimeFormatter.ofPattern(patternProperties.getDateformat());
    }
}

注意

@Value需要设置@RefreshScope实现热更新,@ConfigurationProperties课自动实现热更新

# 配置共享

nacos可以添加多种配置文件:

bootstrap.xml
userservice-dev.yaml
userservice.yaml # 不包含环境,因此可以被多个环境共享
application.yml
  • bootstrap.yaml文件:会在application.yml之前被读取,这样就可以与application.yml配置合并

从微服务拉取配置

  • 当nacos、服务本地同时出现相同属性时,优先级有高低之分:

统一配置管理

注意

只有默认的配置文件(服务名对应的配置文件)才能结合profile使用

# 其它扩展配置

nacos:
  discovery:
	 # 服务注册地址
	 server-addr: 127.0.0.1:8848
  config:
	 # 配置中心地址
	 server-addr: 127.0.0.1:8848
	 file-extension: yml
	 # nacos客户端将无法感知配置的变化
	 refresh-enabled: false
	 # 共享配置
	 shared-configs:
	   - data-id: application-dev.${spring.cloud.nacos.config.file-extension}
	     refresh: true
	 # extension的优先级高于shared-configs
	 extension-configs[0]:
	   data-id: application-dev.${spring.cloud.nacos.config.file-extension}
	   refresh: true

# 搭建Nacos集群

Nacos生产环境下一定要部署为集群状态

统一配置管理

  • 修改配置文件cluster.conf.example,重命名为cluster.conf
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847
  • 然后修改application.properties文件,添加数据库配置
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&serverTimezone=UTC
db.user.0=root
db.password.0=123
  • 将nacos文件夹复制三份,分别为:nacos1/nacos2/nacos3,然后修改其中的application.properties
# nacos1:
server.port=8845

# nacos2:
server.port=8846

# nacos3:
server.port=8848

注意

不能设置为相邻的2个端口号

  • nginx反向代理
upstream nacos-cluster {
    server 127.0.0.1:8845;
	server 127.0.0.1:8846;
	server 127.0.0.1:8847;
}

server {
    listen       80;
    server_name  localhost;

    location /nacos {
        proxy_pass http://nacos-cluster;
    }
}
  • 代码中application.yml文件配置如下
spring:
  cloud:
    nacos:
      server-addr: localhost:80 # Nacos地址
	  
	  # 或者 不需要nginx
	  server-addr: 192.168.100.101:8848,192.168.100.102:8848,192.168.100.103:8848

# 服务网关 Gateway

  • Spring Cloud Gateway是一个基于Spring Boot、Spring WebFlux、Project Reactor构建的高性能网关
  • Spring Cloud Gateway基于Netty运行,因此在传统Servlet容器中或者打成war包是不能正常运行的

服务网关

# 网关的核心功能特性

  • 路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡
  • 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截
  • 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大

# 在SpringCloud中网关的实现包括两种

  • gateway:基于Spring5中提供的WebFlux,属于响应式编程的实现
  • zuul:是Netflix中提供的基于Servlet的实现,属于阻塞式编程

# gateway快速入门

  • 创建SpringBoot工程gateway,引入网关依赖
<!--网关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 编写基础配置和路由规则
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
		  filters:
		    # 在将请求发送到下游之前从请求中剥离的路径个数
		    - StripPrefix=1 # 表示网关转发到业务模块时候会自动截取前缀
			
# 将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务,实现负载均衡
  • 代码的实现方式
@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("example_route", r -> r.path("/example/**")
                .and().method(HttpMethod.GET)
                .and().query("param=value")
                .uri("http://example.com"))
            .build();
    }
}
  • 启动网关服务进行测试
访问http://localhost:10010/user/1时,符合/user/**规则,请求转发到:http://userservice/user/1

# 路由配置

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

spring:
  cloud:
    gateway:
      routes:
        - id: ruoyi-api
		  # websocket配置方式
          uri: ws://localhost:9090/
		  # http地址配置方式
		  uri: http://localhost:9090/
		  # 注册中心配置方式
		  uri: lb://ruoyi-api
          predicates:
            - Path=/api/**

# 断言工厂

例如Path=/user/**是按照路径匹配,这个规则是由

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory

来处理的,像这样的断言工厂在SpringCloudGateway还有十几个:

名称 说明 示例
After 某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00
Before 某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00
Between 两个时间点之前的请求 - Between=开始时间,结束时间
Cookie 匹配名称且其值与正则表达式匹配 - Cookie=chocolate, ch.p
Header 匹配名称且其值与正则表达式匹配 - Header=X-Request-Id, \d+
Host 匹配主机名的列表 - Host=.somehost.org,.anotherhost.org
Method 请求方式必须是指定方式 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/**
Query 请求必须包含指定参数 - Query=name, Jack或者- Query=name
RemoteAddr 匹配IP地址和子网掩码 - RemoteAddr=192.168.1.1/24
Weight 匹配权重 - Weight=group1, 2

# 过滤器工厂

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:

过滤器工厂

Spring提供了31种不同的路由过滤器工厂。例如:

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量
  • 局部过滤器
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
		
# 当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效
  • 默认过滤器
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      default-filters: # 默认过滤项
      - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
	  
# 对所有的路由都生效,则可以将过滤器工厂写到default下
  • 自定义全局过滤器:实现GlobalFilter接口
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
        // 2.获取authorization参数
        String auth = params.getFirst("authorization");
        // 3.校验
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.拦截
        // 4.1.禁止访问,设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.结束处理
        return exchange.getResponse().setComplete();
    }
}

# 过滤器执行顺序

请求进入网关会碰到三类过滤器:

  • 当前路由的过滤器
  • DefaultFilter
  • GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

过滤器执行顺序

排序的规则是:

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

# 解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置:

spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

代码的方式:新增CorsConfig.java跨域代码配置

@Configuration
public class CorsConfig
{
    private static final String ALLOWED_HEADERS = "*";
    private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS,HEAD";
    private static final String ALLOWED_ORIGIN = "*";
    private static final String ALLOWED_EXPOSE = "*";
    private static final String MAX_AGE = "18000L";

    @Bean
    public WebFilter corsFilter()
    {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request))
            {
                ServerHttpResponse response = ctx.getResponse();
                HttpHeaders headers = response.getHeaders();
                headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
                headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
                headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
                headers.add("Access-Control-Expose-Headers", ALLOWED_EXPOSE);
                headers.add("Access-Control-Max-Age", MAX_AGE);
                headers.add("Access-Control-Allow-Credentials", "true");
                if (request.getMethod() == HttpMethod.OPTIONS)
                {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }
}

# 限流配置

通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的,常见的限流算法有:

  • 计数器算法
  • 漏桶(Leaky Bucket)算法
  • 令牌桶(Token Bucket)算法

Spring Cloud Gateway官方提供了 RequestRateLimiterGatewayFilterFactory 过滤器工厂,使用 Redis 和 Lua 脚本实现了令牌桶的方式

  • 添加依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  • 限流规则,根据URI限流
spring:
  redis:
    host: localhost
    port: 6379
    password: 
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
                key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
  • 编写URI限流规则配置类
@Configuration
public class KeyResolverConfiguration
{
    @Bean
    public KeyResolver pathKeyResolver()
    {
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }
}
  • 其他限流规则
//参数限流:key-resolver: "#{@parameterKeyResolver}"
@Bean
public KeyResolver parameterKeyResolver()
{
	return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}

//IP限流:key-resolver: "#{@ipKeyResolver}"
@Bean
public KeyResolver ipKeyResolver()
{
	return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
  • 测试服务验证限流

启动网关服务RuoYiGatewayApplication.java和系统服务RuoYiSystemApplication.java。 因为网关服务有认证鉴权,可以设置一下白名单/system/**在进行测试,多次请求会发现返回HTTP ERROR 429,同时在redis中会操作两个key,表示限流成功

request_rate_limiter.{xxx}.timestamp
request_rate_limiter.{xxx}.tokens

# 熔断降级

  • 添加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 配置需要熔断降级服务
spring:
  redis:
    host: localhost
    port: 6379
    password: 
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            # 降级配置
            - name: Hystrix
              args:
                name: default
                # 降级接口的地址
                fallbackUri: 'forward:/fallback'

提示

上面配置包含了一个Hystrix过滤器,该过滤器会应用Hystrix熔断与降级,会将请求包装成名为fallback的路由指令RouteHystrixCommand,RouteHystrixCommand继承于HystrixObservableCommand,其内包含了Hystrix的断路、资源隔离、降级等诸多断路器核心功能,当网关转发的请求出现问题时,网关能对其进行快速失败,执行特定的失败逻辑,保护网关安全。

fallbackUri是可选参数,当前只支持forward模式的URI。如果服务被降级,请求会被转发到该URI对应的控制器。控制器可以是自定义的fallback接口;也可以是自定义的Handler,需要实现接口org.springframework.web.reactive.function.server.HandlerFunction < T extends ServerResponse >

  • 实现添加熔断降级处理返回信息
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse>
{
    private static final Logger log = LoggerFactory.getLogger(HystrixFallbackHandler.class);

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest)
    {
        Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(JSON.toJSONString(R.fail("服务已被降级熔断"))));
    }
}
  • 路由配置信息加一个控制器方法用于处理重定向的/fallback请求
@Configuration
//RouterFunctionConfiguration用来注册一个路由和它的处理程序
public class RouterFunctionConfiguration
{
    @Autowired
    private HystrixFallbackHandler hystrixFallbackHandler;

    @Autowired
    private ValidateCodeHandler validateCodeHandler;

    @SuppressWarnings("rawtypes")
    @Bean
	
	//RouterFunction为我们应用程序添加一个新的路由
	//这个路由需要绑定一个HandlerFunction,做为它的处理程序,里面可以添加业务代码
    public RouterFunction routerFunction()
    {
        return RouterFunctions.route(
		       RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN))
			   ,hystrixFallbackHandler).andRoute(
			   RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN))
			   ,validateCodeHandler);
    }
}
  • 测试服务熔断降级

启动网关服务RuoYiGatewayApplication.java,访问/system/**在进行测试,会发现返回服务已被降级熔断,表示降级成功

# 黑名单配置

就是不能访问的地址。实现自定义过滤器BlackListUrlFilter,需要配置黑名单地址列表blacklistUrl,当然有其他需求也可以实现自定义规则的过滤器

spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            - name: BlackListUrlFilter
              args:
                blacklistUrl:
                - /user/list
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{
    @Override
    public GatewayFilter apply(Config config)
    {
        return (exchange, chain) -> {

            String url = exchange.getRequest().getURI().getPath();
            if (config.matchBlacklist(url))
            {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
            }

            return chain.filter(exchange);
        };
    }

    public BlackListUrlFilter()
    {
        super(Config.class);
    }

    public static class Config
    {
        private List<String> blacklistUrl;

        private List<Pattern> blacklistUrlPattern = new ArrayList<>();

        public boolean matchBlacklist(String url)
        {
            return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
        }

        public List<String> getBlacklistUrl()
        {
            return blacklistUrl;
        }

        public void setBlacklistUrl(List<String> blacklistUrl)
        {
            this.blacklistUrl = blacklistUrl;
            this.blacklistUrlPattern.clear();
            this.blacklistUrl.forEach(url -> {
                this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
            });
        }
    }
}

# 实现Sentinel限流

Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流

  • 添加依赖
<!-- 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>
  • 限流规则配置类
@Configuration
public class GatewayConfig
{
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelFallbackHandler sentinelGatewayExceptionHandler()
    {
        return new SentinelFallbackHandler();
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter()
    {
        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit()
    {
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则   
     */
    private void initGatewayRules()
    {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("ruoyi-system")
                .setCount(3) // 限流阈值
                .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
    }
}
  • 测试验证,一分钟内访问三次系统服务出现异常提示表示限流成功

# Sentinel分组限流

对ruoyi-system、ruoyi-gen分组限流配置

  • application.yml配置文件
spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
        # 代码生成
        - id: ruoyi-gen
          uri: lb://ruoyi-gen
          predicates:
            - Path=/code/**
          filters:
            - StripPrefix=1
  • 限流规则配置类
@Configuration
public class GatewayConfig
{
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelFallbackHandler sentinelGatewayExceptionHandler()
    {
        return new SentinelFallbackHandler();
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter()
    {
        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit()
    {
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则   
     */
    private void initGatewayRules()
    {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("system-api")
                .setCount(3) // 限流阈值
                .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
        rules.add(new GatewayFlowRule("code-api")
                .setCount(5) // 限流阈值
                .setIntervalSec(60));
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
        // 加载限流分组
        initCustomizedApis();
    }

    /**
     * 限流分组   
     */
    private void initCustomizedApis()
    {
        Set<ApiDefinition> definitions = new HashSet<>();
        // ruoyi-system 组
        ApiDefinition api1 = new ApiDefinition("system-api").setPredicateItems(new HashSet<ApiPredicateItem>()
        {
            private static final long serialVersionUID = 1L;
            {
                // 匹配 /user 以及其子路径的所有请求
                add(new ApiPathPredicateItem().setPattern("/system/user/**")
                        .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
            }
        });
        // ruoyi-gen 组
        ApiDefinition api2 = new ApiDefinition("code-api").setPredicateItems(new HashSet<ApiPredicateItem>()
        {
            private static final long serialVersionUID = 1L;
            {
                // 只匹配 /job/list
                add(new ApiPathPredicateItem().setPattern("/code/gen/list"));
            }
        });
        definitions.add(api1);
        definitions.add(api2);
        // 加载限流分组
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }
}
  • 测试验证
访问:http://localhost:8080/system/user/list (触发限流 )
访问:http://localhost:8080/system/role/list (不会触发限流)
访问:http://localhost:8080/code/gen/list (触发限流)
访问:http://localhost:8080/code/gen/xxxx (不会触发限流)

# Sentinel自定义异常

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

  • 方案一:yml配置
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);
    }
}

# 服务保护 Sentinel

Sentinel (opens new window) 是阿里巴巴开源的一款微服务流量控制组件,收费版的是AHAS

# 雪崩问题及解决方案

微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况

解决雪崩问题的常见方式有四种:

超时处理 # 设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
仓壁模式 # 限定每个业务能使用的线程数,避免耗尽整个 tomcat 的资源,因此也叫线程隔离
断路器模式 # 统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求
流量控制 # 限制业务访问的 QPS,避免服务因流量的突增而故障

# 服务保护技术对比

早期比较流行的是 Hystrix 框架,但目前国内使用最广泛的还是阿里巴巴的 Sentinel 框架,做下对比:

功能 Sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于慢调用比例或异常比例 基于失败比率
限流 基于 QPS,支持基于调用关系的限流 有限的支持
流量整形 支持慢启动、匀速排队模式 不支持
实时指标实现 滑动窗口 滑动窗口(基于 RxJava)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
系统自适应保护 支持 不支持
控制台 支持 不完善
常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix

# Sentinel Dashboard 下载运行

java -jar sentinel-dashboard-1.8.6.jar
  • 如果要修改 Sentinel 的默认端口、账户、密码,可以通过下列配置:
配置项 默认值 说明
server.port 8718 服务端口
sentinel.dashboard.auth.username sentinel 默认用户名
sentinel.dashboard.auth.password sentinel 默认密码
java -Dserver.port=8718 -jar sentinel-dashboard-1.8.1.jar
  • 访问 http://localhost:8718 页面,默认的账号和密码默认都是:sentinel

# 通过代码自定义流量控制

  • 创建一个springboot项目,并引入sentinel核心库
<!-- Sentinel核心服务 -->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-core</artifactId>
	<version>1.8.6</version>
</dependency>
<!-- Sentinel本地应用接入控制台 -->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-transport-simple-http</artifactId>
	<version>1.8.6</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  • 编写一个controller,自定义流量控制
@RestController
@Slf4j
public class HelloController {
    private static final String RESOURCE_NAME = "hello";
    @RequestMapping("/hello")
    public String hello(){
        Entry entry = null;
        try {
            // 添加需要流控的资源
            entry = SphU.entry(RESOURCE_NAME);
            // 被保护的业务逻辑
            log.info("======="+str+"=======");
            return = "hello world";
        }catch (BlockException e){
            // 资源访问阻止,被限流或被降级,进行响应的处理操作
            log.info("block");
            return "被监控了";
        }catch (Exception ex){
            // 若需要配置降级规则,则需要通过这种方式记录业务异常
            Tracer.traceEntry(ex,entry);
        }finally {
            if(entry != null){
                entry.exit();
            }
        }
        return null;
    }
    
	//spring的初始化方法
    @PostConstruct
    private static void initFlowRules(){
        // 流控规则
        List<FlowRule> rules = new ArrayList<>();
        // 流控
        FlowRule rule = new FlowRule();
        // 为哪个资源设置流控
        rule.setResource(RESOURCE_NAME);
        // 设置流程规则 QPS(每秒的一个访问数)
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 设置受保护的资源阈值(1秒当中访问一次)
        rule.setCount(1);
        // 加入集合
        rules.add(rule);
        // 加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }
}
  • 在 idea 中设置本地应用的 JVM 启动参数
-Dcsp.sentinel.dashboard.server=127.0.0.1:9000 # Sentinel控制台的地址和端口号
-Dproject.name=sentinel # 本地应用在控制台中的名称

# @SentinelResource 无侵入定义资源,并配置 blockHandler 函数来进行限流之后的处理

  • 添加依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.4</version>
</dependency>
  • AspectJ 的配置类
@Configuration
public class SentinelAspectConfiguration {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect(){
        return new SentinelResourceAspect();
    }
}
  • 在controller层进行代码实现
@RestController
@Slf4j
public class HelloController {
    private static final String RESOURCE_NAME = "hello";
    /**
     * value: 定义的资源
     * blockHandler: 设置流控降级后的处理方法(默认该方法必须声明在同一个类)
     *   如果不想在同一个类中通过blockHandLerClass设置,但是方法必须是static静态的
     * fallback : 当接口出现了异常,可以调用的方法
     *    如果不想在同一个类中通过fallbackClass设置,但是方法必须是static静态的
     * 如果blockHandler和fallback同时指定,则blockHandler优先级更高
     * exceptionsToIgnore 排除哪些异常不处理
     **/
    @RequestMapping("/hello")
    @SentinelResource(value = RESOURCE_NAME,blockHandler = "blockHandlerForHello",
	                                        fallback = "fallbackHandlerHello",
											exceptionsToIgnore = {})
    public String hello(String id){
        // int i = 1/0;
        return "hello world";
    }
    /**
     * 注意:
     * 1、一定要public
     * 2、返回值一定要和原方法一直,参数也要包含原函数的参数
	 * 3、可以在参数最后添加BlockException 可以区分是什么规则的处理方法
     */
    public String blockHandlerForHello(String id , BlockException ex){
        ex.printStackTrace();
        return "流控";
    }
    /**
     * 异常会走这个方法
     */
    public String fallbackHandlerHello(String id , Throwable e){
        e.printStackTrace();
        return "异常处理";
    }

    //spring的初始化方法,流控规则
    @PostConstruct
    private static void initFlowRules(){
        // 流控规则
        List<FlowRule> rules = new ArrayList<>();
        // 流控
        FlowRule rule = new FlowRule();
        // 为哪个资源设置流控
        rule.setResource(RESOURCE_NAME);
        // 设置流程规则 QPS(每秒的一个访问数)
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 设置受保护的资源阈值(1秒当中访问一次)
        rule.setCount(1);
        // 加入集合
        rules.add(rule);
        // 加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }
	
	//spring的初始化方法,熔断降级规则
	@PostConstruct
	private static void initDegradeRule(){
		List<DegradeRule> degradeRules = new ArrayList<>();
		DegradeRule degradeRule = new DegradeRule();
		degrade.setResource(RESOURCE_NAME);
		//设置熔断策略:异常数
		degradeRule.setGerade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT)
		//设置熔断策略:慢调用比例
		//degradeRule.setGerade(RuleConstant.DEGRADE_GRADE_RT)
		//触发熔断异常数:2
		degradeRule.setCount(2);
		//触发熔断最小请求数:2
		degradeRule.setMinRequestAmount(2);
		//统计时长 单位:ms  1分钟
		degradeRule.setStatIntervalMs(60*1000);
		//熔断持续时长 单位:ms
		//一旦触发了熔筋, 再次请求对应的接口就会直接调用,降级方生
		//10秒过了后一半开状态: 恢复接口请求调用, 如果第一次请求就异常,会直接熔断
		degradeRule.setTimeWindow(10);
		
		degradeRules.add(degradeRule);
		DegradeRuleManager.loadRules(degradeRules);
	}
}

# 微服务整合 Sentinel

  • order-service 引入 sentinel 依赖
<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 修改 order-service 的 application.yaml 文件,添加下面内容
server:
  port: 8088
spring:
  cloud: 
    sentinel:
      transport:
        dashboard: localhost:8080
  • 访问 order-service 的任意端点,这样才能触发 sentinel 的监控

# 簇点链路

当请求进入微服务时,首先会访问 DispatcherServlet,然后进入 Controller、Service、Mapper,这样的一个调用链就叫做簇点链路

1、簇点链路中被监控的每一个接口就是一个资源
2、默认情况下 sentinel 会监控 SpringMVC 的每一个端点(Endpoint,也就是 controller 中的方法)
3、因此 SpringMVC 的每一个端点(Endpoint)就是调用链路中的一个资源

簇点链路

流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:

流控:# 流量控制
降级:# 降级熔断
热点:# 热点参数限流,是限流的一种
授权:# 请求的权限控制

每个url的方法都可以通过@SentinelResource定义为一个资源

  • 如果配置了SentinelWebInterceptor拦截器则不需要添加@SentinelResource
  • Sentinel默认只标记Controller中的方法为资源,如果要标记其它方法,需要@SentinelResource注解
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    //重写父类方法
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Add Sentinel interceptor
        addSpringMvcInterceptor(registry);
    }

    private void addSpringMvcInterceptor(InterceptorRegistry registry) {
        SentinelWebMvcConfig config = new SentinelWebMvcConfig();

        config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());

        config.setHttpMethodSpecify(true);

        config.setWebContextUnify(false);
        config.setOriginParser(request -> request.getHeader("S-user"));

        // 注册一个 sentinel interceptor(拦截器),是系统内置的一个拦截器,拦截所有的url
        registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
    }
}

注意

  • 限流:服务提供方,熔断:服务消费方

# 流量控制(flow control)

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

resource: # 资源名,唯一名称,默认为请求路径
limitApp: # 针对来源,可以针对调用者进行限流,填写微服务名,默认 default(不区分)
grade: # 限流阈值类型(0并发线程数、1流量的QPS)
count: # 单机阈值

# 直接:api 达到限流条件时,直接限流
# 关联:当关联的资源达到阈值时,对当前资源限流(如下单时限流积分)
# 链路:从指定链路访问到本资源的请求,触发阈值时,对入口资源限流
strategy: # 流控模式

# 快速失败:达到阈值后,新的请求会被立即拒绝并抛出 FlowException 异常
# Warm Up:根据coldFactor(冷加载因子,默认3)的值,从阈值/coldFactor,经过预热时长后才会达到阈值
# 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为 QPS,否则无效,等待的超时时间为毫秒
controlBehavior: # 流量控制效果:请求达到流控阈值时应该采取的措施

流量控制

注意

链路模式时需要关闭context整合

# springboot下在SentinelWebMvcConfig对象中
config.setWebContextUnify(false);

# springcloud
spring: 
  cloud: 
	sentinel:
	  # 取消控制台懒加载
	  eager: true
	  web-context-unify: false

注意

Warm Up:主要是为了处理突然出现的激增流量;比如秒杀系统开启瞬间,会有很多流量上来
排队等待:主要用于处理间隔性突发的脉冲流量,例如消息队列

# 熔断降级(DegradeRule)

限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会出现故障,因此需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩

断路器控制熔断和放行是通过状态机来完成的: 状态机

熔断降级规则包含下面几个重要的属性:

resource:# 资源名,即规则的作用对象	
grade:# 熔断策略,支持慢调用比例/异常比例/异常数策略
count:# 慢调用比例模式下:RT(请求时长),异常比例/异常数模式下:对应的阈值	
slowRatioThreshold:# 慢调用比例阈值,仅慢调用比例模式有效
timeWindow:# 熔断时长(单位为 s)
minRequestAmount:# 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断
statIntervalMs:# 统计时长(单位为 ms)

熔断降级

注意

  • 慢调用:指定时间内,若请求数量超过最小请求数,且慢调用比例大于阈值比例,则触发熔断
  • 异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效
  • 经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断

# 热点参数限流(ParamFlow)

热点即经常访问的数据,热点参数限流仅对包含热点参数的资源调用进行限流

# 需要设置 @sentinelResource注解
# @GetMapping("/info"):http://127.0.0.1:8800/order/info?id=1
# @GetMapping("/info/{id}"):http://127.0.0.1:8800/order/info/1
# 对资源info中的第一个参数整体限流10,但对其中的id=1的值限流1

关联模式

# 授权规则

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式

# 白名单:来源(origin)在白名单内的调用者允许访问
# 黑名单:来源(origin)在黑名单内的调用者不允许访问

授权规则

# 系统规则

当容量评估不到位,或者突然发现机器的load和CPU等开始飙升,但不能快速确认是什么原因造成等,这时候,需要一个全局的兜底防护方案,即使缺乏容量评估也能有一定的保护机制。这就是系统保护规则

# Load 自适应(仅对 Linux/Unix机器生效):系统的 load1 作为启发指标,进行自适应系统保护。
# 当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护。
# 系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。

# 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
# 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
# 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
# CPU使用率:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。

授权规则

# 统一异常处理

  • 需要实现 BlockExceptionHandler 接口
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response
	                  , BlockException e) throws Exception {
		//包含资源信息
        AbstractRule rule = e.getRule();
					  
        String msg = "未知异常";
        int status = 429;
        //FlowException:限流异常
        if (e instanceof FlowException) {
            msg = "请求被限流了";
	    //ParamFlowException:热点参数限流异常
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
	    //DegradeException:降级异常
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
		//AuthorityException:授权规则异常
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
		//SystemBlockException:系统规则异常
        } else if (e instanceof SystemBlockException) {
            msg = "系统错误";
            status = 500;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}
  • 在SentinelWebMvcConfig中指定并使用SentinelExceptionHandler
config.setBlockExceptionHandler(new SentinelExceptionHandler());

注意

添加了@SentinelResource注解后,无法使用统一异常处理

# 如何获取 origin

Sentinel 是通过 RequestOriginParser 这个接口的 parseOrigin 来获取请求的来源的

public interface RequestOriginParser {
    /**
     * 从请求 request 对象中获取 origin,获取方式自定义
     */
    String parseOrigin(HttpServletRequest request);
}
  • 例如 order-service 服务中,我们定义一个 RequestOriginParser 的实现类
@Component
public class HeaderOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1.获取请求头
        String origin = request.getHeader("origin");
        // 2.非空判断
        if (StringUtils.isEmpty(origin)) {
            origin = "blank";
        }
        return origin;
    }
}
  • 所有从 gateway 路由到微服务的请求都带上 origin 头
spring:
  cloud:
    # 修改 gateway 服务中的 application.yml,添加一个 defaultFilter
    gateway:
      default-filters:
        - AddRequestHeader=origin,gateway
      routes:
       # ...略

# 规则持久化

sentinel 的所有规则都是内存存储,重启后所有规则都会丢失。sentinel 支持三种规则管理模式:

# 原始模式:Sentinel 的默认模式,将规则保存在内存,重启服务会丢失
# pull 模式:保存在本地文件或数据库,定时去读取
# push 模式:保存在 nacos,监听变更实时更新(推荐)

push 模式的实现依赖于 nacos,且需要修改 Sentinel 控制台的源码,否则 Sentinel 客户端调整后 nacos 配置里面需要手动调整

  • 导入依赖,在 order-service 中引入 sentinel 监听 nacos 的依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

<!--网关流控需要增加-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
  • 配置 nacos 地址,在 order-service 中的 application.yml 文件配置 nacos 地址及监听的配置信息
spring:
  cloud:
    sentinel:
      datasource:
        flow: # 名称随意
          nacos:
            server-addr: localhost:8848 # nacos 地址
            dataId: orderservice-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow # 还可以是:degrade、authority、param-flow
			# 网关的流控类型才是gw-flow,普通是flow

注意

需要将spring.cloud.sentinel.filter.enabled 配置项置为 false(网关流控默认粒度为route和自定义API分组维度,不支持URL粒度)

  • 修改 sentinel-dashboard 源码

    • 解压sentinel源码包,然后用IDEA打开这个项目
    • 在sentinel-dashboard的pom文件中,nacos的依赖默认的scope是test(去除),只能在测试时使用
    • 在sentinel-dashboard的test包下,已经编写了对nacos的支持,我们需要将其拷贝到main下

    已经编写了对nacos的支持,我们需要将其拷贝到main下

    • 修改测试代码中的NacosConfig类,修改其中的nacos地址,让其读取application.properties中的配置

    读取application.properties中的配置

    • 在sentinel-dashboard的application.properties中添加nacos配置:nacos.addr=localhost:8848
    • 修改com.alibaba.csp.sentinel.dashboard.controller.v2包下的FlowControllerV2类

    FlowControllerV2类

    • 修改src/main/webapp/resources/app/scripts/directives/sidebar/目录下的sidebar.html文件
    <!--将其中的这部分注释打开-->
    <li ui-sref-active="active" ng-if="entry.appType==0">
      <a ui-sref="dashboard.flow({app: entry.app})">
    	<i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则 -NACOS
      </a>
    </li>
    
    • 运行IDEA中的maven插件,编译和打包修改好的Sentinel-Dashboard

    编译和打包修改好的Sentinel-Dashboard

  • 启动运行 Sentinel

# 启动方式跟官方一样
java -jar sentinel-dashboard.jar

# 如果要修改 nacos 地址,需要添加参数:
java -jar -Dnacos.addr=localhost:8848 sentinel-dashboard.jar

# 分布式事务 Seata

# 本地事务

在传统数据库事务中,必须要满足以下四个原则:原子性、一致性、隔离性、持久性 事务acid

# 分布式事务

在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务

此时 ACID 难以满足,这是分布式事务要解决的问题

# CAP 定理

分布式系统有三个指标,但无法同时满足这三个指标。这个结论就叫做 CAP 定理

Consistency(一致性)
# 用户访问分布式系统中的任意节点,得到的数据必须一致

Availability(可用性)
# 用户访问分布式系统中的任意健康节点,必须能得到响应,而不是超时或拒绝

Partition tolerance (分区容错性)
  Partition(分区)
  # 因网络故障或其它原因导致分布式系统中的节点与其它节点失去连接,形成独立分区
  Tolerance(容错)
  # 在集群出现分区时,整个系统也要持续对外提供服务

CAP 定理

# CAP 矛盾

在分布式系统中,系统间的网络不能 100% 保证健康,一定会有故障的时候,而又必须对外保证服务,因此 Partition Tolerance 不可避免

当节点接收到新的数据变更时,就会出现问题了:

  • 如果要保证一致性,就必须等待网络恢复,让服务处于阻塞不可用状态,完成数据同步后对外提供服务
  • 如果要保证可用性,就不能等待网络恢复,那么node01、node02 与node03 之间就会出现数据不一致
  • 也就是说,在 P 一定会出现的情况下,A 和 C 之间只能实现一个

CAP 定理

# BASE 理论

BASE 理论是对 CAP 的一种解决思路,包含三个思想:

Basically Available (基本可用)
# 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用

Soft State(软状态)
# 在一定时间内,允许出现中间状态,比如临时的不一致状态

Eventually Consistent(最终一致性)
# 虽无法保证强一致性,但在软状态结束后,最终达到数据一致

# 解决分布式事务的思路

分布式事务最大的问题是各个子事务的一致性问题,因此借鉴 CAP 定理和 BASE 理论,有两种解决思路:

  • AP 模式:各子事务分别执行提交,允许出现结果不一致,然后采用弥补措施恢复数据,实现最终一致
  • CP 模式:各子事务执行后互相等待,同时提交/回滚,达成强一致。但事务等待过程中,是弱可用状态

不管是哪一种模式,都需要一个事务协调者(TC)在子系统事务之间互相通讯,协调事务状态

  • 分支事务:这里的子系统事务,称为分支事务
  • 全局事务:有关联的各个分支事务在一起称为全局事务

# Seta 架构

Seata (opens new window) 是一款开源的分布式事务解决方案,可在微服务架构下提供高性能和简单易用的分布式事务服务

Seata 事务管理中有三个重要的角色:

TC(Transaction Coordinator)- 事务协调者
# 维护全局和分支事务的状态,协调全局事务提交或回滚

TM(Transaction Manager)- 事务管理器
# 定义全局事务范围、开始全局事务、提交或回滚全局事务

RM(Resource Manager)- 资源管理器
# 管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

CAP 定理

Seata 基于上述架构提供了四种不同的分布式事务解决方案,默认是AT模式:

AT 模式 (Auto Transaction)
# 最终一致的分阶段事务模式,无业务侵入,也是 Seata 的默认模式

TCC 模式 (Try、Commit、Cancel)
# 最终一致的分阶段事务模式,有业务侵入

XA 模式 
# 强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

SAGA 模式 
# 长事务模式,有业务侵入

无论哪种方案,都离不开 TC,也就是事务的协调者

# 部署 TC 服务

数据库脚本:seata\script\server\db\mysql.sql
  • 配置 seata server,修改 appplication.yml 的配置中心和注册中心的方式为 nacos ,数据存储模式为 db
appplication.yml文件:seata\conf\appplication.yml
  • 为了让tc服务的集群可以共享配置,使用nacos作为统一配置中心,dataId为seataServer.properties,内容是config.txt中的
# config.txt文件:seata\script\config-center\config.txt

# 修改以下
store.mode=db
store.lock.mode=db
store.session.mode=db

store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=StQzZ&YW6Kt!PcXP
  • 启动 seata server,进入seata\bin目录,双击seata-server.bat启动成功后,查看nacos、seata控制台
http://127.0.0.1:7091/

启动闪退或者报错:console.user.username

配置文件里必须加上这五个值才行,conf/application.yml

# 放到seata下面
seata:
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,
	        /**/*.ico,/console-fe/public/**,/api/v1/auth/login

# 放到根目录
console: 
  user:
    # 7091控制台登录需要的账号和密码
    username: seata
    password: seata

# Docker 安装 Seata

  • 下载镜像
docker pull seataio/seata-server:1.6.0
  • 启动容器
docker run -d \
--restart always \
--name seata-server \
-p 7091:7091 \
-p 8091:8091 \
-v /seata/conf/application.yml:/seata-server/resources/application.yml \
seataio/seata-server:1.6.0

# 微服务集成 Seata

  • 引入依赖
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	<version>2021.0.5.0</version>
</dependency>
  • 业务回滚日志表:AT模式必须创建的表,主要用于分支事务的回滚
-- 日志文件表 --
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `id`            BIGINT       NOT NULL AUTO_INCREMENT,
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
	PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
  • 配置 TC 地址
seata:
  application-id: seata-client
  enable-auto-data-source-proxy: false # 不使用自动代理数据源 不然使用druid数据源会冲突
  tx-service-group: default_tx_group #事务组名称 根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: #事务组与TC服务cluster的映射关系
      default_tx_group: default  
  # 配置中心按照这个要求配置和服务端类似
  config:
    type: nacos
    nacos:
      group: DEFAULT_GROUP
      server-addr: 127.0.0.1:8848
      username:
      password:
  # 注册中心按照这个要求配置和服务端类似
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      username:
      password:
      cluster: default
      group: DEFAULT_GROUP

seata 客户端获取 tc 的 cluster 名称方式

  • 以 tx-group-service 的值为 key 到 vgroupMapping 中查找
  • 去nacos配置中心添加一个配置
Data ID:service.vgroupMapping.default_tx_group
配置格式:txt
配置内容:default
  • 如果是单数据库,需要配置seata代理数据源
@Configuration
public class DataSourceProxyConfig {
 
    @Bean
    public DataSource druidDataSource() {
        // 其实这里的信息可以拿配置文件的,我这里写死了
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("123456");
        // 这里就是把我们德鲁伊数据源交给seata代理
        DataSourceProxy dataSourceProxy = new DataSourceProxy(druidDataSource);
        return dataSourceProxy;
    }
}
  • 添加@GlobalTransactional注解
@Service
public class OrderServiceImpl implements OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Resource
    private OrderMapper orderMapper;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductService productService;

    @DS("order")
    @Override
    @Transactional
    @GlobalTransactional // 重点 第一个开启事务的需要添加seata全局事务注解
    public void placeOrder(PlaceOrderRequest request)
    {
        log.info("=============ORDER START=================");
        Long userId = request.getUserId();
        Long productId = request.getProductId();
        Integer amount = request.getAmount();
        log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);

        log.info("当前 XID: {}", RootContext.getXID());

        Order order = new Order(userId, productId, 0, amount);

        orderMapper.insert(order);
        log.info("订单一阶段生成,等待扣库存付款中");
        // 扣减库存并计算总价
        Double totalPrice = productService.reduceStock(productId, amount);
        // 扣减余额
        accountService.reduceBalance(userId, totalPrice);

        order.setStatus(1);
        order.setTotalPrice(totalPrice);
        orderMapper.updateById(order);
        log.info("订单已成功下单");
        log.info("=============ORDER END=================");
    }
}

注意:通过Feign服务调用时,事物不生效的情况下检查服务的xid是不是为空或者不一致

spring-cloud-starter-alibaba-seata依赖会传递Seata的XID,否则的话需要自己通过Feign拦截器在header里传递XID

注意:全局异常捕捉或者开启feign降级处理后,seata无法捕获异常无法进行事务回滚

//全局异常捕捉后导致事务不滚优雅方案解决
if(StringUtils.isNotBlank(RootContext.getXID())){
	response.setStatus(5001);
}

//feign降级的处理,继续抛出异常
@Component
public class TestFactory implements FallbackFactory<TestApi> {

  @Override
  public TestApi create(final Throwable throwable) {
    FeignException ex = (FeignException) throwable;
    JSONObject jsonObject = JSONObject.parseObject(ex.contentUTF8());
    return new TestApi() {
      @Override
      public String test() {
		  String message = jsonObject.getString("message");
		  throw new RuntimeException(message);
      }
    };
  }
}

实现高可用

搭建 TC 服务集群非常简单,启动多个 TC 服务,注册到 nacos 即可,如果要求较高,一般都会做异地多机房容灾,比如一个 TC 集群在上海,另一个 TC 集群在杭州

# XA 模式

XA 是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交协议(2PC)
缺点是:一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差

两阶段提交协议

实现XA 模式的方式

seata:
  data-source-proxy-mode: XA

# AT 模式

AT 模式通过记录更新前后的快照(undo_log)弥补了 XA 模型中资源锁定周期过长的缺陷
XA 模式强一致;AT模式最终一致

AT 模式

在多线程并发访问 AT 模式的分布式事务时,有可能出现脏写问题

引入 全局锁 的概念,在释放 DB 锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据

# TCC 模式

TCC 模式与 AT 模式非常相似,不同的是 TCC 通过人工编码实现三个方法(Try、Confirm、Cancel)
相比 AT 模型,无需生成快照,无需使用全局锁,性能最强

TCC 模式

允许空回滚,拒绝业务悬挂

空回滚:当某分支事务的 try 阶段阻塞时,可能导致全局事务超时而触发二阶段的 cancel 操作,在未执行 try 操作时先执行了 cancel 操作,这时 cancel 不能做回滚,就是空回滚

执行 cancel 操作时,应当判断 try 是否已经执行,如果尚未执行,则应该空回滚

业务悬挂:对于已经空回滚的业务,之前被阻塞的 try 操作恢复,继续执行 try,就永远不可能 confirm 或 cancel,事务一直处于中间状态

执行 try 操作时,应当判断 cancel 是否已经执行了,若已经执行,应当阻止空回滚后的 try 操作

# SAGA 模式

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献,实现异步调用,吞吐高
没有锁,没有事务隔离,会有脏写

Saga 也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

# 链路追踪 SkyWalking

SkyWalking (opens new window)是一个国产开源框架,2015年由吴晟开源,2017年加入Apache孵化器。SkyWalking是分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。它是一款优秀的 APM(Appication Perfomance Management)工具,包括了分布式追踪、性能指标分析、应用和服务依赖分析等。

# 链路追踪框架对比

  • Zipkin是Twiter开源的。特点是轻量,使用部署简单
  • Pinpoint是韩国人开源的。对代码无侵入,支持Java和PHP语言,采用HBase存储数据,UI功能强大
  • SkyWaking是本士开源的。对代码无侵入,支持Java、PHP、net、go、nodeJS语言,UI功能强大
  • CAT是大众点评开源的。Java语言开发,提供Java、C/C++、Node.js、Python、Go等语言的客户端

# 技术架构

技术架构

# 上部分 Agent:负责从应用中,收集链路信息,发送给 SkyWalking OAP 服务器
# 下部分 SkyWalking OAP:负责接收 Agent 发送的数据信息,然后进行分析、存储,最终提供查询功能
# 右部分 Storage:数据存储,目前支持 ES、MySQL、Sharding Sphere、TiDB、H2,采用较多的是 ES
# 左部分 SkyWalking UI:负责提供控制台,查看链路等等

# APM和Agent下载

SkyWalking9.0之后APM没有agent报了,需要单独下载,下载地址 (opens new window)

技术架构

# 搭建服务端

启动apache-skywalking-apm-bin\bin\startup.bat,成功后有两个服务oapService.bat和webappService.bat

# skywalking-oap-server服务启动后会暴露11800 和 12800 两个端口
# 分别为收集监控数据的端口11800和接受前端请求的端口12800,修改端口可以修改config/applicaiton.yml

# skywalking-web-ui服务会占用 默认是8080, 修改端口可以修改webapp/application.yml

# 接入微服务

在IDEA中使用Skywalking,在VM中添加如下参数:

#!!!每个服务都需要配置,配置时去掉注释
 
 
# skywalking‐agent.jar的本地磁盘的路径
-javaagent:F:\soft\skywalking-agent\skywalking-agent.jar
# 在skywalking上显示的服务名
-DSW_AGENT_NAME=api-gateway  
# skywalking的collector服务的IP及端口
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800

注意:此处存在bug,跟踪链路不显示gateway服务

拷贝skywalking-agent/optional-plugins目录下的gateway插件到skywalking-agent/plugins目录

# 使用MySQL持久化链路数据

  • 修改配置:apache-skywalking-apm-bin\config\application.yml
storage:
  selector: ${SW_STORAGE:mysql}
 
mysql:
    properties:
      jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://localhost:3306/swtest?rewriteBatchedStatements=true&serverTimezone=UTC"}
      dataSource.user: ${SW_DATA_SOURCE_USER:root}
      dataSource.password: ${SW_DATA_SOURCE_PASSWORD:root}
      dataSource.cachePrepStmts: ${SW_DATA_SOURCE_CACHE_PREP_STMTS:true}
      dataSource.prepStmtCacheSize: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_SIZE:250}
      dataSource.prepStmtCacheSqlLimit: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_LIMIT:2048}
      dataSource.useServerPrepStmts: ${SW_DATA_SOURCE_USE_SERVER_PREP_STMTS:true}
    metadataQueryMaxSize: ${SW_STORAGE_MYSQL_QUERY_MAX_SIZE:5000}
    maxSizeOfBatchSql: ${SW_STORAGE_MAX_SIZE_OF_BATCH_SQL:2000}
    asyncBatchPersistentPoolSize: ${SW_STORAGE_ASYNC_BATCH_PERSISTENT_POOL_SIZE:4}
  • 添加mysql驱动包到apache-skywalking-apm-bin\oap-libs
mysql-connector-java-8.0.21.jar
  • 创建对应数据库swtest,数据库中会自动出现对应表,重启skywalking

# 自定义链路追踪

  • 导入依赖
<dependency>
	<groupId>org.apache.skywalking</groupId>
	<artifactId>apm-toolkit-trace</artifactId>
	<version>8.12.0</version>
</dependency>
  • @Trace将方法加入追踪链路
@Trace
public List<Order> all() throws InterruptedException {
	TimeUnit.SECONDS.sleep(2);
	return orderMapper.selectAll();
}


@Trace
public Order get(Integer id) {
	return orderMapper.selectByPrimaryKey(id);
}
  • 加入@Tags或@Tag,使用@Tags或@Tag的前提是此方法添加了@Trace注解将返回值或参数加入链路详情
@Trace
@Tag(key="getAll",value="returnedObj") //returnedObj代表返回值
public List<Order> all() throws InterruptedException {
	TimeUnit.SECONDS.sleep(2);
	return orderMapper.selectAll();
}


@Trace
@Tags({@Tag(key="getAll",value="returnedObj"),
		@Tag(key="getAll",value="arg[0]")}) //arg[0]代表第一个参数
public Order get(Integer id) {
	return orderMapper.selectByPrimaryKey(id);
}

# 集成日志框架

  • 添加在需要集成日志的服务上引入依赖
<dependency>
	<groupId>org.apache.skywalking</groupId>
	<artifactId>apm-toolkit-logback-1.x</artifactId>
	<version>8.12.0</version>
</dependency>
  • 添加logback-spring.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 引入 Spring Boot 默认的 logback XML 配置文件  -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
 
 
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志的格式化 -->
        <encoder  class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <Pattern>-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} [%tid] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}</Pattern>
            </layout>
        </encoder>
 
    </appender>
 
    <appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
                <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern>
            </layout>
        </encoder>
    </appender>
 
    <!-- 设置 Appender -->
    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="grpc-log"/>
    </root>
 
</configuration>
  • 打开agent/config/agent.config配置文件,添加如下配置信息(agent与oap在本地的以下可以不配)
plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:192.168.3.100}
plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800}
plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760}
plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30}

# skywalking高可用

Skywalking集群是将Skywalking OAP作为一个服务注册到nacos上,只要Skywalking OAP服务没有全部宕机,保证有一个Skywalking OAP在运行,就能进行跟踪。

搭建一个skywalking oap集群需要:

# 至少一个Nacos(也可以是nacos集群)
# 至少一个ElasticSearch/mysql(也可以是es/msql集群)
# 至少2个skywalking oap服务;
# 至少1个UI(UI也可以集群多个,用Nginx代理统一入口)
  • 修改config/application.yml文件,使用nacos作为注册中心,修改nacos配置
cluster:
  selector: ${SW_CLUSTER:nacos}
  nacos:
    serviceName: ${SW_SERVICE_NAME:"SkyWalking_OAP_Cluster"}
    hostPort: ${SW_CLUSTER_NACOS_HOST_PORT:192.168.0.180:8848}
    # Nacos Configuration namespace
    namespace: ${SW_CLUSTER_NACOS_NAMESPACE:"public"}
    # Nacos auth username
    username: ${SW_CLUSTER_NACOS_USERNAME:""}
    password: ${SW_CLUSTER_NACOS_PASSWORD:""}
    # Nacos auth accessKey
    accessKey: ${SW_CLUSTER_NACOS_ACCESSKEY:""}
    secretKey: ${SW_CLUSTER_NACOS_SECRETKEY:""}
    internalComHost: ${SW_CLUSTER_INTERNAL_COM_HOST:""}
    internalComPort: ${SW_CLUSTER_INTERNAL_COM_PORT:-1}
  • 修改存储策略,使用es或者mysql作为storage
storage:
  selector: ${SW_STORAGE:elasticsearch}
  elasticsearch:
      namespace: ${SW_NAMESPACE:""}
      clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:192.168.0.180:9200}
  • 配置ui服务webapp.yml文件的listOfServers,写两个地址

技术架构

  • 启动Skywalking服务,指定springboot应用的jvm参数
 -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.10:11800,192.168.0.180:1180

# 持续集成 Jenkins

开源免费持续集成工具

# 压力测试 JMeter

Jmeter 依赖于 JDK,所以必须确保当前计算机上已经安装了 JDK,并且配置了环境变量
下载地址 (opens new window)

# 运行使用

  • 双击bin/jmeter.bat即可运行,但是有两点注意:
1、启动速度比较慢,要耐心等待
2、启动后黑窗口不能关闭,否则 Jmeter 也跟着关闭了
  • 设置中文语言:在 bin 目录中找到 jmeter.properties,添加下面配置:
language=zh_CN

# 基本用法

  • 在测试计划上点鼠标右键,选择 添加 > 线程(用户)> 线程组,在新增的线程组中,填写线程信息

新增的线程组

  • 线程组点鼠标右键,添加 http 取样器,编写取样器内容

取样器内容

  • 添加汇总报告和察看结果树

汇总报告和察看结果树

  • 启动,不要点击菜单中的执行按钮来运行,如下

启动