SpringCloud-Alibaba最新教程

SOA架构

image-20220307185647873

SpringCloud和SprinvCloudAlibaba有什么区别?

SpringCloudAlibaba实际上对我们的SpringCloud2.x和1.x实现拓展组件功能。

nacos是分布式配置中心+分布式注册中心=Eureka+config。

研发SpringCloudAlibaba目的是为了推广阿里的产品,如果使用了SpringCloudAlibaba,最好使用alibaba整个体系产品。

面试题:

本地负载均衡器和 服务器负载均衡器有什么区别?

本地负载均衡器 负载均衡算法都是在本地实现

依赖注册中心

​ 应用场景:Dubbo,feign客户端/rpc远程调用框架

​ 框架:客户端Ribbon

服务器负载均衡器 负载均衡算法都是服务器端实现

依赖于Nginx

场景:tomcat 负载均衡

框架:nginx lvs

Nacos和Eureka区别

​ 最大区别:Nacos支持两种模式CP/AP模式 从nacos1.0版本开始,注意:默认就说AP模式

注册中心有哪些?又有什么区别?

这里先谈到Eureka与zookeeper中的ap和cp,然后再谈到nacos的双模式

Eureka,nacos,consul,zookeeper

核心:

  1. Eureka与Zookeeper实现注册的区别
  2. Eureka与Nacos实现注册区别

CAP定律概念

一致性(C):在分布式相同中,如果服务器是集群的情况下,每个节点同一时刻查询的数据必须保存一致性的问题。

可用性(A):集群节点中,部分的节点出现了故障后任然可以使用

分区容错性(P):在分布式系统中网络存在脑裂(网络故障)的问题,部分的server与整个集群失去联系无法形成一个群体。

取舍:只有CP/AP平衡点

采用:
Cp情况下 虽然我们服务不能用,但是需要要保证数据的一致性

Ap情况下 可以短暂保证数据不一致性,但是最终可以一致性,不管怎么样,能够保证我们的服务可用。

大多数的注册中心都是使用的Ap

相同点,不同点。中心实现

相同点:

都是可用实现分布式注册中心

不同点:

zookeeper采用CP保证数据的一致性,原理是Zab原子广播协议,当我们zk主服务因为某种原因宕机的情况下,会自动重新选举一个主服务,整个选举的过程中保证数据一致性的问题,在选举的过程中整个zk环境是不可以使用的,可能断站无法使用的zk,意味着微服务采用该模式的情况下,可能无法实现通讯。(本地有缓存的除外)

Eureka采用ap的设计模式注册中心,完全去中心化思想,也就没有只从之分,每个节点都是同等的,采用相互注册原理,你中有我我中有你,只要最后一个eureka节点存在就可以保证整个微服务可用实现通讯。

我们在使用注册中心时,可用性在优先级最高,可用读取数据暂时不一致,但是至少要能够保证注册中心可用性

中心化:

集群中要有一个老大,其他服务器都要听这个老大的,如果老大死了,那么就需要重新选举老大

去中心化:

所有人都一样,没有级别之分,只要还有一个节点,那么整个微服务都可以正常使用。

Nacos与Eureka区别:

Nacos从1.0开始就支持混合型,即支持Ap也支持Cp模式,默认支采用的时Ap模式保证服务的高可用性,Cp的模式底层集群raft(选举模式)保证数据强一致性的问题。

如果我们采用Ap模式的情况下,出现网络波动任然可用实现我们注册中心注册服务列表。

那么如果选择Cp默认必须保证数据一致性问题,如果网络产生波动的情况下,是无法注册到我们的服务列表的,选择cp模式可以注册实例持久。

什么情况下选择Ap和Cp呢?
如果我们系统需要保证强一致性的问题,那么我们可以采用Cp模式,一般我们都是选择Ap模式保证高可用

image-20220310153938337

BUG

调用Feign客户端没有在启动类上添加@EnableFeignClients注解

Description:

Field memberServiceFeign in com.mayikt.service.impl.order.orderServiceImpl required a bean of type 'com.mayikt.service.openfeign.MemberServiceFeign' that could not be found.


Action:

Consider defining a bean of type 'com.mayikt.service.openfeign.MemberServiceFeign' in your configuration.

Feign1.1.4客户端有bug,必须要提交post才行,解决方案:在我们feign接口方法参数上都要加@RequestParam(“对应参数名称”)

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Mar 09 09:56:54 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
status 405 reading MemberServiceFeign#getUser(Integer); content: {"timestamp":"2022-03-09T01:56:54.751+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/getUser"}

image-20220309095750782

image-20220309100434478

feign客户端不支持使用下划线命名的服务远程调用

java.lang.IllegalStateException: Service id not legal hostname (mykt_member)
	at org.springframework.util.Assert.state(Assert.java:73) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]

image-20220309110843953

再gateway中引入spring-cloud-starter-gateway就不要再引入spring-boot-starter-web依赖,不然会出现冲突

Parameter 0 of method modifyRequestBodyGatewayFilterFactory in org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' in your configuration.

image-20220310101844522

Nacos

分布式配置中心和分布式服务注册中

分布式服务注册中心

RPC远程调用存在问题:

  1. 超时时间 5s,3s

    超时问题 客户端已经发送请求到服务器端,服务器端没有及时的响应请求给客户端。

    防止客户端一直阻塞等待

    1.1接口的超时时间与链接不上区别:

    链接不上:服务器已经宕机

    接口超时:客户端已经发送请求到达了服务器端:只是服务器没有及时的响应给客户端

  2. 安全控制

    加密,https,令牌传输,限流,服务保护中心等

  3. 服务治理

    在分布式和微服务中,服务与服务之间依赖关系非常复杂的,接口调用如果写死的情况下后期接口地址发生变化的情况下,需要人工刷新修改地址。

image-20220308100227287

老式实现服务治理:使用数据库

建立一张表

Redis 缓存

ServoceId ServiceIp

admin 192.168.0.11:8099

缺点:没有真正动态化,不支持的节点扩容与缩容,以及很消耗资源

注册中心:实际上就说存放接口的调用地址

只会存放IP和端口后,接口名称一般不会存放,因为接口名称一般不会改变

接口地址:IP:端口号/接口名称

知名注册中心:注册中心:Dubbo依赖zookeeper,Eureka,Consul,Nacos,Redis,数据库

特性:能够实现动态感知。

注册中心底层实现原理

  1. 微服务架构常用名称

     生产者:提供接口被其他服务调用
    

​ 消费者:调用别人写好的接口

​ 注册中心:存放调用接口地址和动态感知

  1. 我们服务启动的时候去自动去注册中心去注册
  2. 如果我的服务宕机了,注册中心会自动检测我们服务的心跳,进行踢出服务
  1. 会员启动的时候,会将该服务的接口地址注册到服务注册中心存放

    key:服务名称 value:接口地址

  2. 服务注册中心 采用key=服务名称 value服务器接口的地址列表

    Map<String,List>

​ key=xcwl.member value=[ip1,ip2,ip3]

  1. 消费者调用生产者接口的时候,从注册中心上获取接口调用地址列表并不是一个地址

    key=xcwl-member value IP1,IP2,IP3

  2. 消费者采用负载均衡器,选择一个地址,实现本地RPC远程调用

image-20220308112701833

RPC远程调用

导包

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>mykt-order-2021</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>


    </dependencies>

</project>

RPC远程调用

package com.order;

import com.Balancing.Balancing;
import com.Balancing.PollService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.hypermedia.DiscoveredResource;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.util.List;

@RestController
public class OrderService {
    //获取Nacos服务对象
    @Autowired
    private DiscoveryClient discoveryClient;
    //远程调用对象
    @Autowired
    private RestTemplate restTemplate;
    //手写负载均衡算法
    @Autowired
    private Balancing balancing;
    //使用loadBalancerClient对象实现负载均衡算法
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    //通过HttpClient对象RPC远程调用接口
    @RequestMapping("getOrder")
    public String getOrder(){
        //通过服务名称先去注册中心获取接口列表
        List<ServiceInstance> instances = discoveryClient.getInstances("mykt-member");
//        //通过下标去获取注册中服务的接口
        ServiceInstance serviceInstance = instances.get(0);
        //通过httpclient远程调用
            //先获负载均衡的服务接口地址
        URI uri = serviceInstance.getUri();
            //远程调用
        String result = restTemplate.getForObject(uri + "/getUser", String.class);
        return result;
    }


    //在HttpClient对象上实现负载均衡
    @RequestMapping("getOrder1")
    public String getOrder1(){
        //通过服务名称先去注册中心获取接口列表
        List<ServiceInstance> instances = discoveryClient.getInstances("mykt-member");
        //然后通过负载均衡器选择接口
        ServiceInstance poll = balancing.poll(instances);       //轮询
//        ServiceInstance random = balancing.random(instances); //随机算法
        //通过httpclient远程调用
        //先获负载均衡的服务接口地址
        URI uri = poll.getUri();
        //远程调用
        String result = restTemplate.getForObject(uri + "/getUser", String.class);
        return result;
    }

    //使用loadBalancerClient对象实现负载均衡算法
    @RequestMapping("getOrder2")
    public Object getOrder2(){
        //使用choose自动获取对应的接口返回,并且自动实现轮询
        ServiceInstance choose = loadBalancerClient.choose("mykt-member");
        return restTemplate.getForObject(choose.getUri() + "/getUser", String.class);
    }
}

配置文件 application.yml

server:
  port: 9000
spring:
  application:
    name: mayikt-order  #服务名称 在 注册中心展示服务名称 --
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

启动类

package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

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

本地负载均衡算法

接口

package com.Balancing;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;
//负载均衡接口
public interface Balancing {
    //轮询
    ServiceInstance poll(List<ServiceInstance> list);
    //随机
    ServiceInstance random(List<ServiceInstance> list);
}
package com.Balancing;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class PollService implements Balancing{
    //创建一个原子性对象,如果我们自己创建一个变量的话可能会出现线程安全问题
    private AtomicInteger atomicInteger=new AtomicInteger(0);

    //随机数对象
    private static Random random=new Random();


    //轮询算法
    @Override
    public ServiceInstance poll(List<ServiceInstance> list) {
        //计数,执行一次,计数+1
        int count = atomicInteger.incrementAndGet();
        //通过当前计数%list的大小,获得下标
        int index = count%list.size();
        //通过下标拿list的元素
        return list.get(index);
    }

    @Override
    public ServiceInstance random(List<ServiceInstance> list) {
       return list.get(random.nextInt(list.size()));
    }
}

使用Feign客户端调用

Feign客户端自动实现 负载均衡

原理: 在我们接口的类上添加@FeignClient(“mykt-member”) 表示去对应的nacos注册中心寻找对应的服务地址

接口的抽象方法上使用@RequestMapping(“/getUser”) 表示服务的接口

最后Feign将我们的 服务的地址+接口名称拼 接就可以了.

导包

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>

接口

package com.User;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("mykt-member")
public interface UserService {
    @RequestMapping("/getUser")
    String getUser();
}

实现接口

package com.User;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * 通过Feign客户端远程调用
 */
@RestController
public class UserServiceImpl {
    
    @Autowired
    private UserService userService;
    
    @RequestMapping("/getuserfeign")
    public String getUser(){
      return  userService.getUser();
    }
}

注意: 需要在启动类上添加@EnableFeignClients注解

image-20220308160110986

Nacos 分布式配置中心

什么是配置中心?

​ 使用专门的服务器统一存放关联我们整个的微服务的配置文件,能够完全动态实现对配置文件修改,新增,是不需要重启我们的服务器。

分布式配置中心框架有哪些:

​ 携程阿波罗(重量级),Nacos(轻量级),SpringCloud Config()没有可视化界面,disConfig

轻量级:部署,架构设计原理比较简单,学习成本也比较低;

重量级:部署,架构设计都是非常麻烦,学习成本是比较高的;

配置中心原理

  1. 本地一样读取我们云端分布式配置中心文件(第一次建立长连接)

  2. 本地一样读取到配置文件后,本地的内存和硬盘中会各存放一份

  3. 本地一样与分布式配置中心服务器端一直保持长连接

  4. 当我们配置中心文件发送变化(MD5|版本号)实现区分,然后将变化通知的结果发送给我们本地的应用,进行刷新我们的配置文件。

    完全百分比实现动态化修改我们配置文件。

我们nacos的门户网站和中心服务器(接口)以及注册中心全部都整合在一起了

image-20220309152146612

Nscos配置中心发布规则:

Dataid名称:默认的情况下使用 服务名称-版本.yml|properties

应用读取配置文件

添加注解

先创建一个云端配置文件

image-20220309161455533

文件格式

groudid对应服务名称.文件格式

注意:配置文件格式一定要相同

image-20220309161939143

image-20220309161845999

配置成功后

image-20220309162046912

代码

image-20220309162127217

多环境配置

image-20220309182211407

image-20220309182259804

nacos链接数据库

假如在我们在集群的情况下怎么实现多个nacos数据同步呢?

nacos解决方案是:链接mysql数据库

image-20220309182508609

image-20220309182534235

spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=0000

Nacos集群

  1. nacos集群部署有哪些原理
  2. nacos集群部署在不同环境下需要注意那些问题
  3. nacosClient如果链接我们nacos集群
  4. nacos与eureka,zookeeper的区别

集群搭建

在Windos情况下只能启动单机,伪集群。

在Iinux版本的中运行的时候默认是集群模式,如果需要改为单机启动,修改配置。

注意:

我们在搭建集群后,我们不需要链接数据库,也可以实现数据同步

  1. nacos在windos版本下运行默认是单机版本 需要指定 startup.cmd -m cluster
  2. nacos在linux版本下运行默认是集群版本,如果想链接单机版本 startup.cmd -m standalone
Windos
  1. 我们复制多个nacos文件

    image-20220309195150336

  2. 修改集群ip,并且将cluster.conf.example修改成cluster.conf

image-20220309195235639

  1. 我们进入每个nacos中使用cmd集群运行

    image-20220309195053766

  2. 进入nacos后台

    image-20220309195405953

注意:

​ 集群的ip地址不能采用127.0.0.1 集群选举是用心跳模式

分布式一致性算法raft协议

  1. 分布式系统中一致性协议有哪些
  2. 如果理解Eureka的PeerToPeer集群架构
  3. ZAB协议与Paxos协议实现的区别
  4. NacoasRaft一致性心跳的实现原理

分布式事务一致性框架与分布式系统一致性算法有哪些区别?

分布式事务一致性框架:

核心解决我们再实际系统中产生跨事务问题,核心靠的就说最终一致性:比如rockrtmq事务消息,rebbitmq补单,lcn,seata等。

分布式一致性算法

解决我们系统之间集群之后每个节点数据保持一致性。

比如:raft(nacos),zab(zookeeper),paxos等

Geteway

微服务网关是整个微服务API接口的入口,可以实现过滤API接口。

作用:就是可以实现用户的登录验证,解决跨域,日志拦截,权限控制,限流,熔断,负载均衡,黑名单和白名单机制等。

微服务中的结果模式采用前后端分离,前端调用接口地址都能够被抓包分析到。

传统的方式我们跨域使用过滤器拦截用户会话信息,这个过程所有的服务器都必须要写入验证会话登录的代码,很冗余。

过滤器适合于单个服务实现过滤请求,

网关拦截整个微服务实现过滤请求,能够解决整个微服务中的冗余代码

过滤器只能局部拦截,网关跨域全局拦截。

Zuul与GateWay有哪些区别

Zuul网关属于NetFix公司开源框架,属于第一代微服务网关

GateWay属于SpringCloud自己研发的网关框架,属于第二代微服务网关

相比来说GateWay比Zuul网关的性能要好很多。

主要:

​ Zuul网关底层基于Servlet实现,阻塞式api,不支持长连接 依赖SpringBoot-Web

SpringCloudGateWay基于Spring5构建,能够实现响应式非阻塞式api,支持长连接,能够更好的支持Spring体系产品,依赖SpringBoot-WabFux

搭建网关

网关一般端口号为80或者443

新建一个maven项目

依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
</dependencies>

application.yml

server:
  port: 80
####服务网关名称
spring:
  application:
    name: mayikt-gateway
  cloud:
    gateway:
        ###路由策略
      routes:
        ###路由id
        - id: mayikt
          ####转发http://www.mayikt.com/  这里一定要加/不然会报错
          uri: http://www.mayikt.com/
          ###匹配规则
          predicates:
            - Path=/xcwl/**
### 127.0.0.1/xcwl   转发到http://www.mayikt.com/

项目结构

image-20220310102221051

网关转发微服务接口

这里我们开启了两个mykt-member服务,然后通过网关转发,我们会发现网关也会自动帮我们做负载均衡

server:
  port: 80
####服务网关名称
spring:
  application:
    name: mayikt-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
        ###路由策略
      routes:
        ###路由id
        - id: mykt-member
          ####转发 lb://注册中心服务名称
          uri: lb://mykt-member
          filters:
            - StripPrefix=1
          ###匹配规则
          predicates:
            - Path=/mamber/**
      ### 127.0.0.1/mamber  

#      运行通过注册中心获取地址
      discovery:
        locator:
          enabled: true

网关和Nginx有哪些区别?

  1. 微服务网关能够做的事情,Nginx也可以实现

相同点:

都是开源实现Api的拦截,负载均衡,反向代理,请求过滤,可以完全和网关实现一样的效果

不同点

Nginx采用整个c语言编写的

再每个编程语言中,都有微服务的概念,比如我们使用java构建微服务项目,Gateway也是java语言编写的

毕竟GateWay属于我们java语言编写的,能够更好的对我们的微服务实现拓展功能,相比Nginx如果实现拓展功能的话,我们必须要要学习lua语言或者c语言,那么这样的话我们学习成本就变高了。

网关全局过滤

我们需要定义一个类并且实现GlobalFilter接口重写里面的filter方法,如果想要过滤器级别高一点,再实现Ordered 即可

package com.mayikt.filter;

import ch.qos.logback.core.status.Status;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

//实现网关拦截,需要实现GlobalFilter接口重新里面的filter方法
@Component
public class TokenGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //通过exchange获取req和resp对象,再通过getQueryParams().getFirst方法获取参数
       String token = exchange.getRequest().getQueryParams().getFirst("token");
        //判断当前token为空的情况
       if (token==null){
           //获取响应对象
           ServerHttpResponse response = exchange.getResponse();
           //给响应对象结果内容
           String msg="token is null";
           //设置响应状态号
           response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
           //将内存转换成DataBuffer对象
           DataBuffer wrap = response.bufferFactory().wrap(msg.getBytes());
           //最后返回响应对象
           return response.writeWith(Mono.just(wrap));
       }
       //如果不为空,就运行通行
       return chain.filter(exchange);
    }
}

网关集群

在我们微服务项目中,网关绝对不可以为单机版本,一定要做集群,不然一旦宕机了,整个微服务项目使用不了

image-20220310124852899

集群可以使用Nginx实现

网关实现了集群如何当客户端访问能够网关?

​ 使用nginx或者lvs虚拟vip技术

环境配置:
在集群环境下,网关就不能为80了,因为我们需要配合nginx

网关1 127.0.0.1:81

网关2 127.0.0.1:82

网关3 127.0.0.1:83

Nginx服务器 127.0.0.1:80

我们实现nginx给网关做负载均衡,然后网关在给我们微服务做负载均衡

  1. 我们在网关转发微服务的时候在请求头传递一个当前网关端口号
  2. 我们微服务在请求头中拿到网关端口号

网关:我们启动两个网关端口号为81和82,共nginx做负载均衡

 //如果不为空,就运行通行,将请求头中存放一个当前网关的端口号
ServerHttpRequest gatewayPort = exchange.getRequest().mutate().header("gatewayPort", Port).build();
return chain.filter(exchange.mutate().request(gatewayPort).build());

image-20220310131827660

微服务,也启动两个并且注册到注册中心,共网关负载均衡

@GetMapping("/get")
public String get(HttpServletRequest request){
    return "我是订单服务,端口号"+port+",网关端口"+request.getHeader("gatewayPort");
}

image-20220310132003961

Nginx

image-20220310132103945

到这里我们一共开启了一共nginx,一个网关集群,两个服务集群

image-20220310132220845

image-20220310132312209

image-20220310132324815

动态网关

所谓的动态网关,就是我们网关的配置存放者远程,这样我们任何配置修改的时候,网关就不需要重启。

image-20220310132543543

实现思路:

  1. 分布式配置中心,不建议使用 阅读性差 需要定义json格式配置 阅读性差
  2. 基于数据库表结构设计 特别建议 阅读性比较高。

基于数据库表形式的设计

网关以及提供了api接口

  1. 直接新增
  2. 直接修改

思路:

默认加载时候

1. 当我们的网关服务启动的时候,从我们数据库查询网关的配置
1. 将数据库的内容读取到网关内存中

网关配置要更新的

伪代码:

1. 更新数据库
1. 调用网关api更新

image-20220310152748497

依赖

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>mayikt-gateway</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- 阿里巴巴数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.14</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>

数据库

CREATE TABLE `mayikt_gateway` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `route_id` varchar(11) DEFAULT NULL,
  `route_name` varchar(255) DEFAULT NULL,
  `route_pattern` varchar(255) DEFAULT NULL,
  `route_type` varchar(255) DEFAULT NULL,
  `route_url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

pojo

package com.mayikt.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GateWay {
    private int id;
    private String route_id;
    private String route_name;
    private String route_pattern;
    private String route_type;
    private String route_url;
}

mapper

package com.mayikt.mapper;

import com.mayikt.pojo.GateWay;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import java.util.List;

@Component
public interface GateWayMapper {
    List<GateWay> queryGateWayList();
}

mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mayikt.mapper.GateWayMapper">
    <select id="queryGateWayList" resultType="gateWay" parameterType="gateWay">
        select * from mayikt_gateway
    </select>
</mapper>

service

package com.mayikt.service;

import com.mayikt.mapper.GateWayMapper;
import com.mayikt.pojo.GateWay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class GatewayService implements ApplicationEventPublisherAware {
    //mapper
    @Autowired
    private GateWayMapper gateWayMapper;

    //请了解下Spring中事件机制:发布ApplicationEventPublisher,实现监听ApplicationEvent。结合异步操作,哎呀,真香!你值得拥有!
    private ApplicationEventPublisher publisher;


    //网关
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

//   重写ApplicationEventPublisherAware接口中方法
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }


    //链接数据库,查询出的的数据,循环写入到网关中
    public void initAllRoute(){
        List<GateWay> gateWays = gateWayMapper.queryGateWayList();
        for (GateWay gateWay : gateWays) {
            loadRoute(gateWay);
        }
    }

    //写入网关配置方法
    public String loadRoute(GateWay gateWay) {
        RouteDefinition definition = new RouteDefinition();
        Map<String, String> predicateParams = new HashMap<>(8);
        PredicateDefinition predicate = new PredicateDefinition();
        FilterDefinition filterDefinition = new FilterDefinition();
        Map<String, String> filterParams = new HashMap<>(8);
        // 如果配置路由type为0的话 则从注册中心获取服务
        URI uri=null;
        if ("0".equals(gateWay.getRoute_type())){
            uri = UriComponentsBuilder.fromUriString("lb://"+gateWay.getRoute_name()).build().toUri();
        }else{
            uri = UriComponentsBuilder.fromHttpUrl(gateWay.getRoute_url()).build().toUri();
        }
        // 定义的路由唯一的id
        definition.setId(gateWay.getRoute_id());
        predicate.setName("Path");
        //路由转发地址
        predicateParams.put("pattern", gateWay.getRoute_pattern());
        predicate.setArgs(predicateParams);

        // 名称是固定的, 路径去前缀
        filterDefinition.setName("StripPrefix");
        filterParams.put("_genkey_0", "1");
        filterDefinition.setArgs(filterParams);
        definition.setPredicates(Arrays.asList(predicate));
        definition.setFilters(Arrays.asList(filterDefinition));
        definition.setUri(uri);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }

}

controller

package com.mayikt.controller;

import com.mayikt.service.GatewayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RouterController {
    @Autowired
    private GatewayService gatewayService;

    @RequestMapping("/sycGateWay")
    public String sycGateWay(){
        try {
            gatewayService.initAllRoute();
            return "success";
        }catch (Exception e){
            return e.getMessage();
        }
    }
}

application.yml

server:
  port: 82
####服务网关名称
spring:

  #  配置数据库
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud-gateway
    username: root
    password: "0000"

  application:
    name: mayikt-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      #      运行通过注册中心获取地址
      discovery:
        locator:
          enabled: true
mybatis:
  type-aliases-package: com.mayikt.pojo
  mapper-locations: classpath:com/mayikt/mapper/*.xml

app

package com.mayikt;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.mayikt.mapper")
public class app {
    public static void main(String[] args) {
        SpringApplication.run(app.class);
    }
}

源码分析

  1. GateWay的谓词有哪些
  2. GateWay的整体执行流程
  3. GateWay深度源码分析
  4. GateWay解决跨域问题

GateWay主要组成

route(路由),Predicate(谓词),Filter(过滤器)

路由:路由名称,路由转发的url,匹配规则

谓词:匹配规则

过滤器:可以过滤我们的请求,比如客户端进入网关这里我们过滤不存在token的请求

核心配置分析

image-20220310172005027

源码分析

作用检查我们是否配置了webfux依赖,
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,
加载了我们Gateay需要注入的类(bean)
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
根据服务名称去注册中心查询接口,并且支持负载均衡
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\
网关整合Redis+Lua实现限流
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
服务注册与发现,将服务注册到nacos中
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration

Sentinel

服务保护有哪些方案:

  1. 黑名单和白名单
  2. 对IP实现限流,熔断机制
  3. 服务降级
  4. 服务隔离机制

服务限流

目的式为了保护我们服务,在高并发的情况下,如果客户端的请求服务器到一定的极限(阈值),请求的数据超出我们设置的阈值,开启我们的自我保护机制。直接执行我们的服务降级方法,不会执行我们业务逻辑,走本地falback方法。

限流有两种就是QPS和线程数,pqs表示每秒可以支持访问多少次,线程数表示当前接口有最大多少个线程,如果超出了阈值那么就走falback方法返回友好提示

服务降级

在高并发的情况下,为了防止用户一直等待,采用限流或者熔断机制,保护我们服务,不会执行我们的业务逻辑,走本地的falback方法,返回一个友好提示给客户端。

​ 比如:当前排队人数过多,等稍后重试。

服务雪崩效应

默认的情况下,tomcat和jetty服务器只会有一个线程处理所有接口的请求。

这样在高并发情况下所有的请求都堆积到同一个接口上,那么会产生该服务器的所有线程都在处理该接口,可能会导致其他的接口无法访问,短暂没有线程处理。

如何去证明我们的tomcat服务器只有一个线程处理我们所有接口的请求

​ 打印线程名称 现场名称组合:线程池名称+线程id名称。

解决方案:

服务隔离机制:

  1. 线程池隔离
  2. 信号量隔离

线程池隔离

​ 每个接口都有自己独立的线程池维护我们的请求,每个线程互不影响,缺点:占用服务器内存非常大,如果我们有一万个接口,那么就需要开启一万个线程池,非常消耗内存资源

信号量隔离

​ 设置最多允许我们某个接口有一定的阈值的线程数量处理我们的接口,如果超出该线程数量,则拒绝访问。

Sentinel和Hystrix区别

image-20220310195302669

限流规则

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel</artifactId>
    <version>0.2.2.RELEASE</version>

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

手动代码实现

package com.mayikt.service.impl.order;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.mayikt.service.openfeign.MemberServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

@RestController
public class orderServiceImpl {
    //限流规则名称
    private static final String GETORDER_KEY = "getOrder";
	
    
	//限流接口
    @GetMapping("/getUser")
    public String getUser(){
        Entry entry=null;
        try {
          entry=SphU.entry("getOrder");
            return "我是订单服务,端口号"+port;
        }catch (Exception e){
            e.printStackTrace();
            return "当前访问人数过多,请稍等重试!";
        }finally {
            // SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
            if (entry != null) {
                entry.exit();
            }
        }
    }


    //限流配置方法,需要先执行这个方法才能配置限流
    @RequestMapping("/initFlowQpsRule")
    public String initFlowQpsRule() {
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(GETORDER_KEY);
        // QPS控制在1以内
        rule1.setCount(1);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
        return "....限流配置初始化成功..";
    }

}
启动自动执行规则方法

实现ApplicationRunner接口重写里面的run方法即可

package com.mayikt.service.config;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Slf4j
public class SentinelApplicationRunner implements ApplicationRunner {
    private static final String GETORDER_KEY = "getOrder";
    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(GETORDER_KEY);
        // QPS控制在1以内
        rule1.setCount(1);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
        log.info("限流启动成功!");
    }
}

注解实现

缺点:没有办法修改限流的大小

package com.mayikt.service.impl.order;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.mayikt.service.openfeign.MemberServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

@RestController
public class orderServiceImpl {
    //规则名称
    private static final String GETORDER_KEY="getOrder";

    @SentinelResource(value = GETORDER_KEY,blockHandler ="fallback")
    @RequestMapping("/order")
    public String Order(){
        return "订单服务哦";
    }


    //限流后的提示方法
    public String fallback(BlockException e){
        e.printStackTrace();
        return "接口限流了";
    }
}

可视化平台

Sentinel 环境快速搭建

下载对应Sentinel-Dashboard

https://github.com/alibaba/Sentinel/releases/tag/1.7.1 运行即可。

运行执行命令

java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -Dcsp.sentinel.api.port=8719 -jar

8718属于 界面端口号 8719 属于api通讯的端口号

image-20220310210339965

浏览器打开登录

img

登录进入:

img

SpringBoot链接

image-20220310210446165

注意:

如果我们的接口没有使用@SentinelResource注解,我们添加限流规则的时候适用接口的url地址做名称,如果使用的注解,那么就使用value中的值做名称

没有加注解,流控名称对应接口名称,不加注解没办法执行返回内容

image-20220310212050869

加了注解,可以自定义限流名称和自定义返回信息

image-20220310212249032

sentienl限流的规则默认情况下式没有持久化的,如果需要持久化就需要配合zookeeper,nacos,携程阿波罗等!

QPS,线程数

QPS,每秒可以访问多少次

QPS限流

image-20220310212050869

线程数,信号隔离,就表示当前接口有多少个线程在执行

线程限流

image-20220310213643235

Sentinel持久化

image-20220310220356347

其实我们服务里面定义的规则都是保存在服务的内存中,而sentinel可视化平台读取的就是我们服务内存中的规则展现出来,然而我们服务关闭后,sentinel可视化平台就读取不到我们服务了,那么就显示不了我们的服务和规则了。

sentinel规则是保存在我们服务的内存中,我们服务重启后就全部没有了。

所以我们要整合nacos的分布式配置中心,nacos是可以绑定我们数据库做持久化的,在我们服务中绑定nacos配置中心,让我们服务读取云端规则保存在内存,然后sentinel可视化平台在读取我们服务内存的规则展现出来。

操作流程

  1. 依赖
<!--sentinel 整合nacos配置中心 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.5.2</version>
</dependency>
  1. 在nacos上编写配置文件

image-20220310222010427

resource:资源名,即限流规则的作用对象
limitApp:流控针对的调用来源,若为 default 则不区分调用来源
grade:限流阈值类型(QPS 或并发线程数);0代表根据并发数量来限流,1代表根据QPS来进行流量控制
count:限流阈值
strategy:调用关系限流策略
controlBehavior:流量控制效果(直接拒绝、Warm Up、匀速排队)
clusterMode:是否为集群模式
  1. 在服务中配置

image-20220310222113554

#      sentinel整合nacos配置中心
      datasource:
        ds:
          nacos:
            ### nacos连接地址
            server-addr: 127.0.0.1:8848
            ## nacos连接的分组
            group-id: DEFAULT_GROUP
            ###路由存储规则
            rule-type: flow
            ### 读取配置文件的 data-id
            data-id: mayikt-order-sentienl
            ###  读取培训文件类型为json
            data-type: json
  1. 启动项目在sentinel可视化平台查看

    image-20220310222158646

注意:

我们服务在使用nacos配置中心的时候,本地代码禁止启动类上添加规则

否者就会配置文件就会失效,获取不到。

image-20220310222311408

熔断降级

降级和熔断是配合使用的,现有熔断才有降级的。当接口熔断后才会进行降级走本地fallback方法

熔断

类似于保险丝,如果超出了我们的阈值的情况下,在一定的时间内不会执行我们的业务逻辑直接执行我们的服务降级的方法。

服务降级:走本地fallback方法,返回一个有好的提示给客户端,不会真实的执行我们的业务逻辑

服务降级策略

  1. rt(平均响应时间)
  2. 异常比例
  3. 异常次数(当我们的接口一直报异常,那么就会走我们的服务降级)

RT平均响应时间 单位为秒

假如我当前方法执行需要300毫秒,但是我配置了降级策略,配置当前接口在1秒内访问5次平均执行时间超过10毫秒,那么就会触发熔断机制走降级的本地rallback方法,持续60秒

image-20220311100956073

异常比例,单位为秒

假如当我服务接口1秒内访问5次出现异常比例大于我们配置的比例那么就会触发熔断和执行降级策略走本地的fallbock方法并且在规定的时间内无法访问

异常比例 单位为分钟,可以配合异常比例使用

假如我们当前接口一分钟内出现5次异常,那么就执行触发熔断和执行降级策略走本地的fallbock方法并且在规定的时间内无法访问

跨域问题

package com.xcwl.config;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CrossOriginFilter implements GlobalFilter {
    //解决跨域问题
    /**
     *最重要的就是添加响应头为*全部接口
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        HttpHeaders headers = exchange.getResponse().getHeaders();
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true");
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,"*");
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,"POST,GET,PUT,DELETE");
        headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,"*");
        headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,"*");
        return chain.filter(exchange);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值