灰度路由的实现

书接上文,上次介绍基于gateway,nacos,redis实现动态路由。本次介绍灰度路由的实现。
首先介绍gateway中的一个全局过滤器类ReactiveLoadBalancerClientFilter

ReactiveLoadBalancerClientFilter

此类为gateway提供的负载均衡过滤器。在配置的断言命中时触发。其中和新方法filter中的逻辑为:

  • 获取上下文中的请求URI,并判断模式是否是"lb",即多节点负载模式
  • 在上下文的originalRequestUrl中加入当前url
  • 调用choose方法,进行负载均衡,返回应用实例
  • 重新构建可执行示例
  • 构建URI,换成ip和端口模式
  • 过滤器释放,接着执行其他过滤器链

以下是该过滤器核心方法源码,我已添加注释:

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //从上下文中获取请求地址
        URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        //判断请求模式。
        String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
    	//当模式支持负载均衡
        if (url != null && ("lb".equals(url.getScheme()) || "lb".equals(schemePrefix))) { 
            //在上下文中加入原始url
            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
            if (log.isTraceEnabled()) {
                log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
            }
            //负载均衡返回serviceInstance
            return this.choose(exchange).doOnNext((response) -> {
                if (!response.hasServer()) {
                    throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
                } else {
                    URI uri = exchange.getRequest().getURI();
                    String overrideScheme = null;
                    if (schemePrefix != null) {
                        overrideScheme = url.getScheme();
                    }

                    DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme);
                    URI requestUrl = this.reconstructURI(serviceInstance, uri);
                    if (log.isTraceEnabled()) {
                        log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
                    }

                    exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
                }
            }).then(chain.filter(exchange));
        } else {
            return chain.filter(exchange);
        }
    }

由上述代码可见,负载均衡策略发生在choose方法,下面我们看一下choose方法。


    private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
        //获取源url
        URI uri = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        //后获取ServiceInstance 列表
        ReactorLoadBalancer<ServiceInstance> loadBalancer = (ReactorLoadBalancer)this.clientFactory.getInstance(uri.getHost(), ReactorLoadBalancer.class, new Class[]{ServiceInstance.class});
        if (loadBalancer == null) {
            throw new NotFoundException("No loadbalancer available for " + uri.getHost());
        } else {
            //负载均衡策略获取其中一个ServiceInstance
            return loadBalancer.choose(this.createRequest());
        }
    }

以上便是gateway提供的负载均衡过滤器。

灰度路由介绍

  1. 首先,灰度概念,介于对与错之间的,便是灰度。详细概念可自行查阅。而灰度路由的产生,实在强大的负载均衡策略下,选中你想要的那一个路由。若抛开gateway组件不谈,可以理解为,如果在强大的负载均衡策略下,所有请求都到达同一个服务实例。一句话——谈何容易。若真的实现,那就可以实现定向攻击了。
  2. 可是,gateway为我们提供了支持编辑的“元数据”标签。如下图所示:

image_73.png

  1. 这个自定义的元数据标签,就会让我们的负载均衡策略变得更加灵活。可以做到按照标签匹配。
  2. 也有人说,这样不安全,容易受到攻击。但是不要忘记,这个配置界面,只有内部可见,只要保证网络的内外隔离,就保证元数据标签不会被外人可见,那么就会被认为,是安全的。这极大地方便了开发者调试和定位线上问题,且不影响线上使用。

灰度路由方案实现

下面,我将实现灰度路由方案

  1. 首先,基于上述知识,若想实现灰度路由方案,我们只需要按我们需求修改负载均衡逻辑即可。方案有两种,一种是修改ribbon的负载均衡逻辑,一种是我们继承ReactiveLoadBalancerClientFilter类,并重载他的filter逻辑,重载他的choose方法。
  2. 而重新修改ribbon负载均衡逻辑由于涉及到源码,修改起来并不容易。而基于以上了解到的知识,采用方案二,将会变得容易。
  3. 所以,我们将采用方案二,创建新的filter,继承ReactiveLoadBalancerClientFilter类,重写choose方法。
  4. 总结一下,我们需要修改ReactiveLoadBalancerClientFilter 中loadBalance的choose方案,根据请求待的元数据标签和naco所带的元数据标签比较,命中则返回,不命中,则走其他的策略。

本方案实现的核心代码如下:

  @Override
    public ServiceInstance choose(String serviceId, ServerHttpRequest request) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

        //注册中心无实例 抛出异常
        if (CollectionUtils.isEmpty(instances)) {
            System.out.println(("No instance available for {}" + serviceId));
            ;
            throw new NotFoundException("No instance available for " + serviceId);
        }

        // 获取请求version,无则随机返回可用实例
        String reqVersion = request.getHeaders().getFirst("VERSION");
        if (StringUtils.isBlank(reqVersion)) {
            return instances.get(RandomUtils.nextInt(instances.size()));
        }

        // 遍历可以实例元数据,若匹配则返回此实例
        for (ServiceInstance instance : instances) {
            Map<String, String> metadata = instance.getMetadata();
            String targetVersion = metadata.get("VERSION");
            if (reqVersion.equalsIgnoreCase(targetVersion)) {
                return instance;
            }
        }
        return instances.get(RandomUtils.nextInt(instances.size()));
    }

核心代码可见,在不命中时,方便理解,我随机返回了一个。在这里,大家可以按照自己的业务逻辑进行改写,也可以重新调用loadbalabce的choose方法。
至此,整个灰度路由的内容介绍完了。

工程代码

下面我将放置实现的过程源码。

package com.css.dynamic.config;

import com.css.dynamic.filter.GrayReactiveLoadBalancerClientFilter;
import com.css.dynamic.rule.GrayLoadBalancer;
import com.css.dynamic.rule.VersionGrayLoadBalancer;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**
 * Mica ribbon rule auto configuration.
 *
 * @author L.cm
 * @link https://github.com/lets-mica/mica
 */
@Configuration
@EnableConfigurationProperties(LoadBalancerProperties.class)
@ConditionalOnProperty(value = "route.gray.rule.enabled", havingValue = "true")
@AutoConfigureBefore(GatewayReactiveLoadBalancerClientAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class GrayLoadBalancerClientConfiguration {

    @Bean
    public GrayReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(GrayLoadBalancer grayLoadBalancer,
                                                                                LoadBalancerProperties properties) {
        return new GrayReactiveLoadBalancerClientFilter(properties, grayLoadBalancer);
    }

//    @Bean
//    public ReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
//                                                                            LoadBalancerProperties properties) {
//        return new ReactiveLoadBalancerClientFilter(clientFactory,properties);
//    }


    @Bean
    public GrayLoadBalancer grayLoadBalancer(DiscoveryClient discoveryClient) {
        return new VersionGrayLoadBalancer(discoveryClient);
    }

}

package com.css.dynamic.filter;

import com.alibaba.nacos.client.naming.utils.RandomUtils;
import com.css.gatewaynacos.rule.GrayLoadBalancer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

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

/**
 * @author 郭辉
 * @className TODO
 * @description TODO
 * @date 2022/8/20 8:40
 * @company 海康威视
 * @since 1.0.0
 */
@Component
public class MyGrayLoadBalancer extends ReactiveLoadBalancerClientFilter {
    private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
    private LoadBalancerProperties properties;

    private GrayLoadBalancer grayLoadBalancer;

    private DiscoveryClient discoveryClient;

    public MyGrayLoadBalancer(LoadBalancerProperties properties, GrayLoadBalancer grayLoadBalancer,DiscoveryClient discoveryClient) {
        super(null, properties);
        this.properties = properties;
        this.grayLoadBalancer = grayLoadBalancer;
        this.discoveryClient = discoveryClient;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
        if (!"lb".equalsIgnoreCase(schemePrefix) && !url.getScheme().equalsIgnoreCase("lb")){
            return chain.filter(exchange);
        }
        ServiceInstance serviceInstance = getServiceInstance(exchange, url);

        return Mono.just(new DefaultResponse(serviceInstance)).doOnNext((response)->{
            if (!response.hasServer()){
                throw NotFoundException.create(properties.isUse404(),
                        "Unable to find instance for " + url.getHost());
            }

            URI uri = exchange.getRequest().getURI();

            // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
            // if the loadbalancer doesn't provide one.
            String overrideScheme = null;
            if (schemePrefix != null) {
                overrideScheme = url.getScheme();
            }

            DelegatingServiceInstance delegatingServiceInstance = new DelegatingServiceInstance(
                    response.getServer(), overrideScheme);

            URI requestUrl = LoadBalancerUriTools.reconstructURI(delegatingServiceInstance, uri);
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
        }).then(chain.filter(exchange));
    }

    private ServiceInstance getServiceInstance(ServerWebExchange exchange, URI url) {
        List<ServiceInstance> instances = discoveryClient.getInstances(url.getHost());
        String version = exchange.getRequest().getHeaders().getFirst("VERSION");
        if (StringUtils.isEmpty(version)){//随机取一个实例
            return instances.get(RandomUtils.nextInt(instances.size()));
        }
        ServiceInstance serviceInstance = null;
        for (ServiceInstance service : instances) {
            Map<String, String> metadata = service.getMetadata();
            if (metadata != null && metadata.get("VERSION") .equalsIgnoreCase(version)){
                serviceInstance = service;
            }
        }
        return serviceInstance;
    }

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

}

package com.css.dynamic.filter;

import com.css.dynamic.rule.GrayLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;

/**
 * @description: 灰度路由策略,若没有灰度,则随机返回实例
 * @author: guohui13
 * @date: 2022/8/19 17:24
 **/
@Component
public class GrayReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {
    private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 0;
    private LoadBalancerProperties properties;

    private GrayLoadBalancer grayLoadBalancer;

    public GrayReactiveLoadBalancerClientFilter(LoadBalancerProperties properties, GrayLoadBalancer grayLoadBalancer) {
        super(null, properties);
        this.properties = properties;
        this.grayLoadBalancer = grayLoadBalancer;
    }

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
        if (url == null
                || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        // preserve the original url
        ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);

//        if (log.isTraceEnabled()) {
//            log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName()
//                    + " url before: " + url);
//        }
        return choose(exchange).doOnNext(response -> {

            if (!response.hasServer()) {
                throw NotFoundException.create(properties.isUse404(),
                        "Unable to find instance for " + url.getHost());
            }
            URI uri = exchange.getRequest().getURI();
            // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
            // if the loadbalancer doesn't provide one.
            String overrideScheme = null;
            if (schemePrefix != null) {
                overrideScheme = url.getScheme();
            }
            DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(
                    response.getServer(), overrideScheme);

            URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri);

//            if (log.isTraceEnabled()) {
//                log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
//            }
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
        }).then(chain.filter(exchange));
    }

    private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
        URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        ServiceInstance serviceInstance = grayLoadBalancer.choose(uri.getHost(), exchange.getRequest());
        return Mono.just(new DefaultResponse(serviceInstance));
    }
}

package com.css.dynamic.rule;

import com.alibaba.nacos.client.naming.utils.RandomUtils;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;

/**
 * 基于客户端版本号灰度路由
 *
 * @description:
 * @author: guohui13
 * @date: 2022/8/19 17:24
 **/
@AllArgsConstructor
public class VersionGrayLoadBalancer implements GrayLoadBalancer {
    private DiscoveryClient discoveryClient;


    /**
     * 根据serviceId 筛选可用服务
     *
     * @param serviceId 服务ID
     * @param request   当前请求
     * @return
     */
    @Override
    public ServiceInstance choose(String serviceId, ServerHttpRequest request) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

        //注册中心无实例 抛出异常
        if (CollectionUtils.isEmpty(instances)) {
            System.out.println(("No instance available for {}" + serviceId));
            ;
            throw new NotFoundException("No instance available for " + serviceId);
        }

        // 获取请求version,无则随机返回可用实例
        String reqVersion = request.getHeaders().getFirst("VERSION");
        if (StringUtils.isBlank(reqVersion)) {
            return instances.get(RandomUtils.nextInt(instances.size()));
        }

        // 遍历可以实例元数据,若匹配则返回此实例
        for (ServiceInstance instance : instances) {
            Map<String, String> metadata = instance.getMetadata();
//            String targetVersion = MapUtil.getStr(metadata, "VERSION");
            String targetVersion = metadata.get("VERSION");
            if (reqVersion.equalsIgnoreCase(targetVersion)) {
//                log.debug("gray requst match success :{} {}", reqVersion, instance);
                return instance;
            }
        }
//        return instances.get(RandomUtil.randomInt(instances.size()));
        return instances.get(RandomUtils.nextInt(instances.size()));
    }
}

<think>嗯,用户问的是Skyline自定义路由实现。首先,我需要明确Skyline在这里指的是什么。因为“Skyline”可能有不同的上下文,比如前端框架、后端服务,或者是某个特定系统里的组件。通常,在微服务或分布式系统中,自定义路由是一个常见的需求,用于控制请求的流向,比如灰度发布、A/B测试、流量染色等。所以,假设这里的Skyline是指某个服务框架或网关的自定义路由功能。 接下来,我需要思考自定义路由的一般实现方式。通常,路由的核心是根据请求的某些特征(如Header、Path、参数等)匹配规则,然后决定转发到哪个服务实例。自定义路由可能允许用户动态配置这些规则,而无需修改代码。 可能的步骤包括: 1. **规则定义**:用户如何配置路由规则?是通过配置文件、数据库还是管理界面?规则的结构可能包括匹配条件和目标服务。 2. **规则加载与解析**:系统如何加载这些规则,并在运行时解析它们。可能需要监听配置变化,动态更新路由策略。 3. **请求匹配**:当请求到达时,如何根据规则进行匹配。这里可能涉及优先级、匹配顺序等问题。 4. **路由执行**:匹配到规则后,如何将请求转发到目标服务,可能涉及负载均衡、服务发现等机制。 5. **动态生效**:如何保证路由规则变更后实时生效,而无需重启服务。可能使用热加载或推送机制。 此外,需要考虑性能问题,比如规则匹配的效率,是否使用缓存,避免每次请求都遍历所有规则。还有异常处理,比如没有匹配到规则时的默认路由策略。 可能需要参考类似的开源项目,比如Spring Cloud Gateway、Envoy等,它们的路由实现机制。例如,Spring Cloud使用RouteLocator来定义路由规则,通过Predicate和Filter进行处理。 不过,如果Skyline是某个特定公司的内部框架,可能实现细节有所不同。但通用思路应该类似。需要确保回答结构清晰,分步骤说明,并举例可能的实现方式,比如基于配置的路由规则,使用DSL或JSON/YAML格式定义条件。 另外,用户可能关心如何实际应用,比如配置示例、动态更新的方法,或者与服务发现的集成。需要覆盖这些方面,但保持回答不过于技术细节,而是原理性的解释。 最后,确认是否需要提到相关技术,比如API网关、服务网格,或者特定的编程语言实现,但可能用户更关注概念和实现思路,而非具体代码。</think>Skyline自定义路由实现通常指在微服务架构中,通过动态规则控制请求流量的技术方案。以下为分步骤解析: --- ### 一、核心概念 1. **路由定义** 通过预配置规则,将请求特征(如Header、URI、参数等)与目标服务实例关联。例如: $$匹配条件: \text{Header中包含} \ \texttt{"Env=Gray"} \rightarrow \text{转发至灰度集群}$$ 2. **动态优先级** 规则按优先级排序,匹配时遵循"首次命中即执行"原则。 --- ### 二、关键技术实现 1. **规则存储与加载** - 规则以JSON/YAML格式存储在配置中心(如Nacos、Etcd) - 服务启动时拉取规则,并通过监听机制实现热更新 ```yaml routes: - priority: 1 conditions: ["Header.User-Type == VIP"] target: service-vip-cluster ``` 2. **请求匹配引擎** - 解析请求的元数据(Path、Header、Cookie等) - 使用`AST(抽象语法树)`或`有限状态机`高效匹配条件 - 示例匹配逻辑: $$ \text{IF} \ \exists x \in \text{Headers} \ \text{s.t.} \ x.key=\texttt{"Region"} \ \land \ x.value=\texttt{"EU"} \ \text{THEN} \ \text{路由至欧洲节点} $$ 3. **负载均衡集成** 匹配到目标服务后,结合负载均衡算法(如轮询、一致性哈希)选择实例。 --- ### 三、高级特性实现 1. **流量染色** - 通过添加特定Header(如`X-Trace-ID: blue-green`)标记流量 - 实现全链路压测/隔离测试 2. **熔断降级** 当目标服务异常时,自动切换至备用路由: $$ \text{当} \ \text{error\_rate} \geq 0.8 \ \text{持续} \ 10s \ \Rightarrow \ \text{路由至降级服务} $$ 3. **动态脚本支持** 部分框架允许嵌入Lua/JavaScript脚本实现复杂逻辑: ```js function route(request) { if (request.query.version > '2.0') return 'canary-cluster'; } ``` --- ### 四、性能优化要点 1. **规则缓存** 在内存中维护规则解析后的数据结构,避免重复解析配置 2. **快速失败机制** 使用`布隆过滤器`快速排除不匹配的规则 3. **异步更新** 通过双缓冲机制实现配置无锁更新,保证高并发下的稳定性 --- ### 五、典型应用场景 1. **灰度发布** 将5%流量导至新版本服务: $$ \text{WHERE} \ \text{hash(user\_id) \% 100 < 5} \ \Rightarrow \ \text{v2-endpoint} $$ 2. **多区域路由** 根据用户地理位置选择最近数据中心 3. **服务版本隔离** 按设备类型(iOS/Android)分流至不同服务集群 --- ### 六、开源参考 可研究以下实现方案: - **Spring Cloud Gateway**:基于Java的Filter链路由机制 - **Envoy**:基于C++的层级化路由配置(HTTP route table) - **Apache APISIX**:使用etcd存储+动态插件加载 实际实现需根据具体框架调整,核心是**规则抽象+高效匹配+动态更新**三要素的结合。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值