SpringCloud续篇SpringCloud Alibaba

SpringCloud续篇SpringCloud Alibaba

SpringCloud Alibaba同SpringCloud一样,SpringCloud Alibaba也是一套微服务解决方案,也有完整的微服务必要的组件,并且阿里开源的组件都经历过了考验,性能强悍,设计合理,现在开源出来大家用成套的产品搭配完善的可视化界面给开发运维带来了极大的便利。

1、SpringCloud Alibaba入门概述

1.1、出现的原因

前面漫长而又枯燥的学习,我们已经几乎学完了SpringCloud所有常用的组件!也明白了SpringCloud版本适配等非常麻烦,项目启动很容易报错!并且部分环境搭建也十分复杂,没有完善的可视化界面,我们需要大量的二次开发和定制SpringCloud的复杂配置,还是难以上手,部分差别难以区分和合理应用。这也是SpringCloud的几大痛点。

除了以上几点原因,更重要的是Netflix相关的部分组件停止维护与更新,让我们开发处于被动,给开发带来了不便!部分组件也没有稳定的趋势!

无论部分组件停更还是阿里商业运营等一系列因素,SpringCloud Alibaba都给我们带来很多的惊喜,只需要少量的注解和少量的配置,就能将SpringCloud应用接入到阿里微服务解决方案!并且SpringCloud官方也很支持SpringCloud Alibaba整合起来也非常方便。

SpringCloud Alibaba的诞生:2018年10月31日,SpringCloud Alibaba正式入驻SpringCloud官方孵化器,并在Maven中央仓库发布了第一个版本。

1.2、主要功能一览

官方博客:https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/README-zh.md
在这里插入图片描述
微服务组件
在这里插入图片描述
如何引入依赖呢?

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

然后在dependencies中添加自己所需使用的依赖即可使用。

2、Nacos注册与发现、配置中心

2.1、Nacos概述

前面我们也说了,Nacos属于重点微服务组件。集服务发现、配置、管理于一体(代替Eureka做服务注册中心,代替Config做服务配置中心),是一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台。可帮助我们快速容易的构建和管理微服务基础设施。英文全称Dynamic Naming Configuration Service

官方文档地址:https://nacos.io/zh-cn/docs/concepts.html

更多概念与架构请参照官方文档,官方文档都有详细介绍。
在这里插入图片描述
几种服务注册与发现组件对比:

服务注册与发现组件CAP模型控制台管理社区活跃
EurekaAP支持低(2.x版本闭源)
ZookeeperCP不支持
ConsulCP支持
NacosAP支持

据说Nacos在阿里巴巴内有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。

2.2、安装并运行Nacos

遵循一切上云的原则,我们选择把Nacos站点通过Docker部署在Linux服务器上。

# 1.下载镜像
docker pull nacos/nacos-server:2.0.2

# 2.运行 nacos 注意防火墙、服务器安全组的 8848 和 9848 端口要开放
docker run --name nacos-server -e MODE=standalone -p 8848:8848 -p 9848:9848 -d nacos/nacos-server:2.0.2

原因如下
在这里插入图片描述
启动成功后,我们访问http://IP地址:8848/nacos/index.html即可登录Nacos站点,注意默认账户与密码都是nacos
在这里插入图片描述
至此Nacos环境我们已经搭建好了。

2.3、Nacos Discovery

前面我们已经搭建好了Nacos服务,接下来就是演示以Nacos作为服务的注册/发现中心。我们还是参照之前的服务模块,分别搭建一个服务提供者与服务消费者模块,完成一次远程服务的调用。

注意以下所有服务都要注册到Nacos

1、使用SpringBoot的初始化向导,搭建cloud_payment_8001服务,作为服务提供者。初始pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.laizhenghua</groupId>
    <artifactId>cloud_payment_8001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud_payment_8001</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-discovery -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

先引入这些依赖,后面使用到什么我们引入什么即可。

2、编写配置文件application.yml

server:
  port: 8001
spring:
  application:
    name: payment-service
  cloud:
    nacos:
      discovery:
        server-addr: 139.198.33.126:8848 # 配置Nacos地址
        username: nacos
        password: nacos

注意:消费者的配置文件,可以加上

# 消费者要去访问的微服务名称(注册成功进去 nacos 的微服务提供者)
service-url:
  nacos-user-service: http://payment-service

3、服务提供者编写测试方法,供服务消费者调用。需要注意的是Nacos自带负载均衡机制,服务提供者可以搭建集群模式进行测试。

/**
 * @description: PaymentController
 * @date: 2022/2/26 12:26
 */
@Slf4j
@RestController
@RequestMapping(value = "/payment")
public class PaymentController {
    @Value(value = "${server.port}")
    private String serverPort;

    @RequestMapping(value = "/getServerPort", method = RequestMethod.GET)
    public R getServerPort() {
        log.info("我是服务提供者");
        return R.ok().put("port", serverPort);
    }
}

4、主程序添加@EnableDiscoveryClient注解,并启动

搭建模式如综上所述,我们的服务消费者cloud_order_8002/order-service模块也是一样,如
在这里插入图片描述
5、消费者编写测试方法!去从注册中心消费生产者提供的服务。

配置类

/**
 * @description: RestTemplateConfig
 * @date: 2022/2/26 13:42
 */
@Configuration
public class RestTemplateConfig {
    @Bean
    @LoadBalanced // 一定不要忘记加这个注解
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

业务方法

/**
 * @description: OrderController
 * @date: 2022/2/26 13:44
 */
@RestController
@RequestMapping(value = "/order")
public class OrderController {
    @Autowired
    private RestTemplate restTemplate;

    @Value(value = "${service-url.nacos-user-service}")
    private String paymentServerUrl;

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public R test() {
        return R.ok().put("data", restTemplate.getForObject(paymentServerUrl + "/payment/getServerPort", R.class).get("port"));
    }
}

6、重启消费者(8002),进行测试
在这里插入图片描述
到这里我们只是完成了简单的一步,也算是刚刚入门!知识点补充:Nacos支持AP与CP模式的切换。

一般来说,如果不需要存储服务级别的信息且服务实例是通过Nacos-client注册,并且能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring CloudDubbo服务,都用于AP模式,AP模式为了服务的可能性而减弱一致性,因此AP模式下只支持注册临时实例。

如果需要在服务级别编辑或者存储配置信息,那么CP是必须的。K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,改模式下注册实例之前必须先注册服务,如果服务不存在,则返回错误。

切换方式

curl -X PUT 'nacos_server:8848/nacos/vi/ns/operator/switches?entry=serverMode&value=CP'

2.4、Nacos Config

依稀记得使用Spring Cloud Config做服务配置中心的时候,需要集成GitHubRabbitMQ、搭建Config服务端、搭建Config客户端等一堆东西,真的是非常麻烦!让我做技术选型绝对不选SpringCloud Config

而到了Nacos这里可以让我们以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。也就是说Nacos消除了配置变更时重新部署应用和服务的需要。让配置管理变得更加高效和敏捷,帮助我们在生产环境中管理配置变更和降低配置变更带来的风险。

还是基于上面实例改造,添加alibaba-nacos-config依赖。

<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-nacos-config -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

1、更改配置文件在每个应用的 src/main/resources/bootstrap.yml 配置文件中配置 Nacos Config 元数据

NacosSpring Cloud Config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。bootstrap.yml配置文件会优先于application.yml加载被读取!因此Nacos Config元数据需要配在bootstrap.yml文件中。

Nacos Config元数据必须要有Nacos server的地址和应用名,例如:

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=example

这是因为spring.application.name是构成 Nacos 配置管理 Data ID 字段(配置列表显示)的一部分。

Nacos Spring Cloud 中,Data ID 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
  • spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot 文档。 注意:当 spring.profiles.active 为空时,对应的连接符-也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-exetension 为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension来配置。目前只支持 propertiesyaml 类型。

综上所述,给出配置示例:

bootstrap.yml

server:
  port: 8001
spring:
  application:
    name: payment-service
  cloud:
    nacos:
      discovery:
        server-addr: 139.198.33.126:8848
        username: nacos
        password: nacos
      config:
        server-addr: 139.198.33.126:8848
        file-extension: yaml # 指定yaml格式的配置

application.yml

spring:
  profiles:
    active: dev

2、去Nacos注册中心,配置列表添加配置

此配置叫数据集,可添加任何配置,可实现动态获取配置(后面讲解),注意bootstrap.ymlfile-extension配置项,我们配的是yaml,Data ID 中也要写yaml,否则启动会报错。
在这里插入图片描述
3、修改有业务方法,测试是否能获取配置中心配置的文件内容

/**
 * @description: PaymentController
 * @date: 2022/2/26 12:26
 */
@Slf4j
@RefreshScope // 动态获取并刷新配置
@RestController
@RequestMapping(value = "/payment")
public class PaymentController {
    @Value(value = "${server.port}")
    private String serverPort;

    @Value(value = "${config.info}")
    private String configInfo;

    @RequestMapping(value = "/getConfigInfo", method = RequestMethod.GET)
    public R getConfigInfo() {
        log.info("我是服务提供者");
        Map<String, Object> data = new HashMap<>();
        data.put("port", serverPort);
        data.put("configInfo", configInfo);
        return R.ok().put("data", data);
    }
}

4、重新启动服务进行测试

注意启动时如果报这个错误:java.lang.IllegalArgumentException: Param 'serviceName' is illegal, serviceN。可能是SpringCloud的版本太高了,不知道是啥原因没有读取到bootstrap.yml文件,此时需要添加一个依赖就能解决:

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-bootstrap -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

访问业务方法
在这里插入图片描述
小结:我们发现可以正常返回我们在配置中心配置的内容,再次修改配置内容,然后重新访问,还是可以读到最新修改的配置。此时就可以实现动态修改配置文件!如果配置中心和当前应用的配置文件中都配置了相同的项,会优先使用配置中心的配置。

2.5、Nacos配置中心使用细节

2.5.1、引子

问题1:

实际开发中,通常一个系统会准备:

  1. dev开发环境
  2. test测试环境
  3. prodd生产环境

如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境等,那么怎么对这些微服务配置进行管理呢?

2.5.2、命名空间

用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。

说白了就是做配置隔离,采用Namespace(默认public) + Group(默认DEFAULT_GROUP) + Data ID的模式进行不同环境的配置区分,类似于Java里面的package + class + generic/泛型。最外层的Namespace是可以用于区分部署环境,而GroupData ID逻辑上区分两个目标对象。

比如说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。

命名空间的第一个应用场景:环境隔离

# 1.默认是public(保留空间),默认新增的所有的配置都在public空间里
# 2.命名空间 --> 可以新建dev/test/prop空间
# 3.利用dev/test/prop空间来做环境的隔离
# 4.在 bootstrap.yml 配置文件里配置需要使用哪个命名空间下的配置
spring:
  cloud:
    nacos:
      config:
        namespace: 6e2f9961-21e7-45f8-ab5b-18d3cc065ec7 # 命名空间ID

如图
在这里插入图片描述
命名空间的第二个应用场景:微服务隔离

# 1.每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名下所有的配置
# 2.命名空间 --> 新建命令空间(payment-service、order-service)
# 3.让微服务启动时,读自己的配置文件
# 4. 修改 bootstrap.yml 配置文件,指定微服务启动时读取的配置文件(payment-service)
spring:
  cloud:
    nacos:
      config:
        namespace: b30efa5c-6321-43c8-8e69-ebe026fecb75 # 命名空间ID

如图
在这里插入图片描述

2.5.3、配置集

所有配置的集合就叫配置集。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。

bootstrap.yml

# 1.配置集ID:类似文件名(Data Id)
# 2.配置分组:默认所有的配置集都属于 DEFAULT_GROUP 比如我们可以为获得进行分组,双十一、618、双十二等

spring:
  cloud:
    nacos:
      config:
        group: dev

总结:在我们这个项目中,我们为每个微服务创建自己的命名空间,利用配置分组区分环境(dev、test、prop),这是最常用的配置方式。例如:

bootstrap.yml

spring:
  application:
    name: order-service # 服务名
  cloud:
    nacos:
      discovery:
        server-addr: 139.198.35.12:8848
        username: nacos
        password: nacos
      config:
        server-addr: 139.198.35.12:8848 # 配置中心地址
        file-extension: yaml # 指定yaml格式的配置
        namespace: 6e2f9961-21e7-45f8-ab5b-18d3cc065ec7 # order-service微服务自己的命名空间
        group: dev # 开发环境

如图
在这里插入图片描述
同时加载多个配置集

在实际开发中,我们不可能所有的配置都写在一个配置文件里,那样的话不便于维护并且比较繁琐。所以我们要拆分配置文件,比如数据库连接信息我们写在datasource.yaml,mybatis配置相关写在mybatis.yaml配置文件里等等。配置中心Nacos也支持拆分操作!!

我们在配置中为order-service新建多个配置文件(datasource.yaml、mybatis.yaml、other.yaml)
在这里插入图片描述
最后在bootstrap.yml中加载多个配置集

spring:
  application:
    name: payment-service
  cloud:
    nacos:
      discovery:
        server-addr: 139.198.35.12:8848
        username: nacos
        password: nacos
      config:
        server-addr: 139.198.35.12:8848
        file-extension: yaml # 指定yaml格式的配置
        namespace: 6e2f9961-21e7-45f8-ab5b-18d3cc065ec7 # 命名空间ID
        group: dev
        extension-configs:
         - data-id: datasource.yaml
           group: dev
           refresh: true
         - data-id: mybatis.yaml
           group: dev
           refresh: true
         - data-id: other.yml
           group: dev
           refresh: true

或者是bootstrap.properties

# 服务名
spring.application.name=order-service
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# coupon微服务自己的命名空间
spring.cloud.nacos.config.namespace=b30efa5c-6321-43c8-8e69-ebe026fecb75
# spring.cloud.nacos.config.group=dev

spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yaml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yaml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true

spring.cloud.nacos.config.extension-configs[2].data-id=other.yaml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true
2.5.4、配置中心Nacos总结
  1. 任何配置文件,都可以放在配置中心中,只需要在bootstrap.properties中说明加载配置中心哪些配置文件即可。
  2. 可以使用@Value@ConfigurationProperties注解获取配置文件的信息。
  3. 需要添加@RefreshScope动态获取并刷新配置,并且配置中心的配置文件,会优先于写在当前应用的配置文件
  4. 更多关于Nacos的使用可查看官方文档:https://nacos.io/zh-cn/docs/what-is-nacos.html

3、Nacos集群与持久化

3.1、Nacos中的集群与持久化理论概述

Nacos集群配置,也是比较重要的一项配置。在实际微服务架构中,我们不可能只有一个Nacos服务站点!加上这台服务挂了,那么所有微服务调用链将崩溃!所以实际生产中至少要有3台服务站点(服务的注册与发现),才能保证整个应用的稳定。

官方集群部署说明:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html

官方也明确说明了,生产环境推进使用Linux系统做集群部署。

官网集群部署架构图:
在这里插入图片描述
数据存储/持久化的一些说明:

我们发现Nacos站点退出后重新登录,我们配置过的配置列表信息还在。这是因为Nacos默认使用嵌入式数据库实现的数据存储,所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。

根据官方文档得知,是Nacos 0.7版本及之后的版本才增加了支持MySQL数据源能力。整合要求

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

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

再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql

3.2、Nacos持久化切换配置

前面我们说了Nacos默认使用嵌入式数据库(derby)实现的数据存储,这也是可以证明的,例如可在GitHub上查看Nacos项目的pom.xml文件。
在这里插入图片描述

也说了0.7版本之后支持MySQL数据源,那么如何进行切换呢?其实前面也已经说过了

  1. 初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
  2. 修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。

由于我们是使用Docker进行部署的,数据库初始化文件在容器上也能找得到,只是不叫nacos-mysql.sql,而是叫schema.sql
在这里插入图片描述
1、这时只需一个命令就能复制容器内的文件,然后把复制出来的文件,去数据库执行一下即可完成第一步。

# 11caa57861de 是容器运行的进程ID
docker cp '11caa57861de':/home/nacos/conf/schema.sql /home/nacos/nacos-mysql.sql

这个sql根本不是MySQL的语法,还是给出真正的nacos-mysql.sql

drop table if exists config_info;
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) DEFAULT NULL,
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info' ROW_FORMAT=DYNAMIC;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_aggr   */
/******************************************/
drop table if exists config_info_aggr;
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段' ROW_FORMAT=DYNAMIC;


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_beta   */
/******************************************/
drop table if exists config_info_beta;
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta' ROW_FORMAT=DYNAMIC;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_tag   */
/******************************************/
drop table if exists config_info_tag;
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag' ROW_FORMAT=DYNAMIC;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_tags_relation   */
/******************************************/
drop table if exists config_tags_relation;
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation' ROW_FORMAT=DYNAMIC;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = group_capacity   */
/******************************************/
drop table if exists group_capacity;
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表' ROW_FORMAT=DYNAMIC;

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = his_config_info   */
/******************************************/
drop table if exists his_config_info;
CREATE TABLE `his_config_info` (
  `id` bigint(64) unsigned NOT NULL,
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00',
  `src_user` text,
  `src_ip` varchar(20) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造' ROW_FORMAT=DYNAMIC;


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = tenant_capacity   */
/******************************************/
drop table if exists tenant_capacity;
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表' ROW_FORMAT=DYNAMIC;

drop table if exists tenant_info;
CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info' ROW_FORMAT=DYNAMIC;

drop table if exists users;
CREATE TABLE users (
	username varchar(50) NOT NULL PRIMARY KEY,
	password varchar(500) NOT NULL,
	enabled boolean NOT NULL
) ROW_FORMAT=DYNAMIC;

drop table if exists roles;
CREATE TABLE roles (
	username varchar(50) NOT NULL,
	role varchar(50) NOT NULL,
	constraint uk_username_role UNIQUE (username,role)
) ROW_FORMAT=DYNAMIC;
drop table if exists permissions;
CREATE TABLE permissions (
    role varchar(50) NOT NULL,
    resource varchar(512) NOT NULL,
    action varchar(8) NOT NULL,
    constraint uk_role_permission UNIQUE (role,resource,action)
) ROW_FORMAT=DYNAMIC;

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

2、修改conf/application.properties文件,追加上以下配置

spring.datasource.platform=mysql

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

重启docker即可完成Nacos的持久化配置。在来测试下是否切换成功!我们在配置列表新建一条配置如下图
在这里插入图片描述
查询数据库

SELECT * FROM nacos.config_info

在这里插入图片描述

3.3、Nacos集群配置(重点)

前面所做的工作,都是为这一小节做铺垫,因为官方也明确说明了生产环境一定要集群部署!
在这里插入图片描述
部署之前我们需要准备:

  1. 1个Nginx
  2. 3个Nacos注册中心(3个或3个以上Nacos节点才能构成集群)
  3. 1个MySQL

才能模拟真正生产环境上的部署。MySQL数据库的配置请参照上一小节。每台都要配上。

1、Nacos的集群配置cluster.conf,这个文件配置IP地址与端口号即可,注意每一台Nacos都要有这个文件。
在这里插入图片描述
cluster.conf

139.198.35.10:8848
139.198.34.44:8848
139.198.35.122:8848

注意这三个IP都要保证部署好了Nacos站点,都能能正常访问。

2、修改Nginx的配置,由它做负载均衡器

注意:Nacos 2.0以后Nginx不能配置http转发,而是要配置tcp转发。
在这里插入图片描述
修改代理
在这里插入图片描述
负载均衡修改
在这里插入图片描述
至此,我们的所有配置也算都完成了。需要注意的点就是3个Nacos都要能稳定运行,并且都做了相同的持久化切换配置。

3、进行访问测试,因为我们的Nginx监听的80端口,因此直接访问http://nginxIP地址/nacos即可
在这里插入图片描述
OK,完美。Nacos集群部署完美搞定!

3.4、微服务注册进Nacos集群

Nacos集群我们已经搞定了,把我们的微服务注册进去集群中,这也是最后一步!其实在bootstrap.yml/.properties文件中修改这个配置项即可:

spring.cloud.nacos.config.server-addr=nginx的ip地址:端口号

bootstrap.yml

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 180.76.238.292:80
        username: nacos
        password: nacos
      config:
        server-addr: 180.76.238.292:80
        file-extension: yaml
        namespace: f8740c3d-a048-42a4-9227-6b75758de048
        group: dev

4、Sentinel熔断与限流

说道Sentinel也是非常重要的一个组件,配合Nacos基本上成为了微服务分布式架构首选!

4.1、Sentinel概述

官网:https://github.com/alibaba/Sentinel/wiki/介绍

是什么?

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Hystrix的痛点

需要我们开发者自己手工搭建监控平台,没有一套web可视化界面可以给我们进行更加细粒度的配置流控、速率控制、服务熔断、服务降级等并且配置方式篇复杂!

Sentinel也是参照了Hystrix的一些特性,它又有那些特性呢?
在这里插入图片描述
Sentinel 的开源生态
在这里插入图片描述
Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

4.2、Sentinel的安装与运行

能部署在服务器上,我们坚决部署在云服务器上,并且还是测试与学习环境,对于安装与运行怎么简单怎么来!

# 1.下载镜像
docker pull bladex/sentinel-dashboard:1.7.0
# 2.创建容器
docker run --name sentinel -d -p 8858:8858  bladex/sentinel-dashboard:1.7.0

启动成功后,访问http://ip:8858/#/login即可访问Sentinel管理页面
在这里插入图片描述
注意用户与密码都是sentinel
在这里插入图片描述
Sentinel使用 / 集成初体验。注意要确保Nacos站点没有任何问题!

1、为工程添加GAV坐标

<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2、修改配置文件bootstrap.yml

spring:
  application:
    name: order-service
  main:
    allow-circular-references: true # 使用循环依赖 SpringBoot 2.6以后不推荐使用了
  profiles:
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: 121.411.57.179:8848
        username: nacos
        password: nacos
        namespace: c6de5330-5d3f-4fc3-9305-4e9879dc99d1
        group: dev
      config:
        server-addr: 121.411.57.179:8848
        file-extension: yaml
        namespace: c6de5330-5d3f-4fc3-9305-4e9879dc99d1
        group: dev
    sentinel: # Sentinel的配置
      transport:
        dashboard: 180.762.238.29:8858 # dashboard / 监控平台地址
        port: 8719 # 服务与sentinel后台通信端口,默认是8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未被占用的端口

3、编写业务测试方法

/**
 * @description: FlowLimitController
 * @date: 2022/3/6 15:40
 */
@Slf4j
@RestController
@RequestMapping(value = "/sentinel")
public class FlowLimitController {

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public R test() {
        log.info("sentinel is working");
        return R.ok();
    }
}

4、测试(注意服务器的安全组8858和8719端口一定要放开)

Sentinel采用懒加载机制,需要提前访问一次,才能看到效果。另外如果实时监控没有出现数据,原因可能有如下两点:

1、IP设置的不对,这个IP如果没有指定是默认生成的
在这里插入图片描述
我们可以配置文件中指明这个IP地址,例如

spring:
  cloud:
    sentinel: # Sentinel的配置
      transport:
        dashboard: 180.762.238.29:8858 # dashboard / 监控平台地址
        port: 8719
        clientIp: 192.168.1.5 # 指明通信IP地址

2、客户端(我们自己搭建的服务)时间与Sentinel应用所在的机器时间不一致,特别是是Docker部署的Sentinel最容易出现这个问题。此时我们需要修改Docker的时区,与客户端保持一致。

# 1.查看日志是否报错
docker logs [CONTAINER ID / Sentinel dashboard 的容器ID]

[root@laizhenghua /]# docker logs a6288e32889b
2022-03-06 10:30:04.074 ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher   : Failed to fetch metric from <http://192.168.1.5:8719/metric?startTime=1646562463000&endTime=1646562469000&refetch=false> (ConnectionException: Connection timed out)

# 好家伙,果然报错了

# 2.修改docker的时区
# 进入容器
[root@laizhenghua /]# docker exec -it a6288e32889b /bin/sh
/bladex/sentinel # cd /
/ # 
/ # cd /etc
/etc # mv localtime localtime.bak
/etc # cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
/etc # date
Sun Mar  6 18:50:54 CST 2022
# 3.重启容器
[root@laizhenghua /]# docker restart a6288e32889b

如果还没有监控到,还是下个jar包,在本地运行吧,docker部署Sentinel坑有点多。
在这里插入图片描述

4.3、流控规则

基本介绍(各种概念官方说的已经很清楚了)

流量控制(flow control),其原理是监控应用流量的 QPS(每秒请求数) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

官网介绍地址:https://github.com/alibaba/Sentinel/wiki/流量控制

如何添加流控规则呢?有两种途径

第一种
在这里插入图片描述
第二种
在这里插入图片描述
例如我们配置一个1秒只允许访问一次的流控规则
在这里插入图片描述
快速访问http://127.0.0.1:8002/sentinel/test即可看到效果。
在这里插入图片描述
这也是三种流控模式之一的直接模式也称快速失败模式,这种模式在API达到限流条件时直接限流。除了这种模式外,Sentinel还提供了关联链路流控模式。

另外触发限流后,直接调用的是默认报错信息,对于我们开发者来说,看起来没啥问题,但是用户看到了这种报错肯定是不行的。是否能自定义报错信息呢?留个坑,后面(流控效果)再详细说明。

关联流控模式:当关联的资源达到阈值时,就限流自己

1、准备两个测试方法

/**
 * @description: FlowLimitController
 * @date: 2022/3/6 15:40
 */
@Slf4j
@RestController
@RequestMapping(value = "/sentinel")
public class FlowLimitController {

    @RequestMapping(value = "/payment", method = RequestMethod.GET)
    public R payment() {
        log.info("支付完成后需要调订单服务生产订单");
        return R.ok();
    }
    @RequestMapping(value = "/order", method = RequestMethod.GET)
    public R order() {
        log.info("订单已生成:" + UUID.randomUUID().toString());
        return R.ok();
    }
}

2、设置效果,当关联资源/sentinel/order的QPS阈值超过1时,就限流/sentinel/payment的REST访问地址,当关联资源到阈值后限制配置好的资源名。
在这里插入图片描述
3、测试效果。这里我们需要postman模拟并发密集访问http://127.0.0.1:8002/sentinel/order才能看到效果。因为payment关联着order,只有order达到阈值才能触发payment的限流。
在这里插入图片描述
postman访问20次http://127.0.0.1:8002/sentinel/order期间,快速访问http://127.0.0.1:8002/sentinel/payment即可看到效果。
在这里插入图片描述
链路流控模式:适用于多个请求调用同一个微服务。只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)

1、现有3个服务地址,假设paymentorder都要访问test

http://127.0.0.1:8002/sentinel/test
http://127.0.0.1:8002/sentinel/payment
http://127.0.0.1:8002/sentinel/order

2、当order访问test达到一定阈值后,我们想对order进行限流,我们就可以这样配置
在这里插入图片描述
关于流控规则就说这么多,真正使用时还需要实际情况去分析。

4.4、流控效果

前面我们也知道了快速失败(Blocked by Sentinel (flow limiting)),是默认流控效果(直接失败,抛出异常)。除了这种流控效果Sentinel还提供了Warm Up和排队等待。

Warm Up(预热):即预热 / 冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

计算公式:阈值除以coldFactor(冷加载因子)(默认值为3)经过预热时长后才会达到阈值
在这里插入图片描述
coldFactor为3,即请求QPS从(threshold / 3)开始,经多少预热时长才逐渐升值设定的QPS阈值?

例如:阈值为10 + 预热时长设置5秒,系统初始化的阈值为 10 / 3 约等于3,即阈值刚开始为3,然后经过了5秒后阈值慢慢升高恢复到10
在这里插入图片描述
频繁访问http://127.0.0.1:8002/sentinel/test即可体验到效果。这种预热流控效果最常用的是秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来。


排队等待:匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
在这里插入图片描述
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

例如:阈值QPS = 1时,1秒通过一个请求,可以这样配置
在这里插入图片描述

4.5、降级规则

官方概念解释:https://github.com/alibaba/Sentinel/wiki/熔断降级
在这里插入图片描述

END

THANK YOU

END

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lambda.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值