1-初窥Eurek源码

前提

阅读和分析源码的意义再哪儿?

  1. 技术功底
  2. hold全场
  3. 架构设计能力
  4. 职场竞争力

源码实际上是码农技术水平的分水岭


1. 准备调试环境

  • IDEA
  • GRADLE
  • Spring Cloud Edgware.SR3 对应 Netflix Eureka源码:1.7.2

基于Netflix Eureka的Eureka源码调试 git clone https://github.com/Netflix/eureka.git

Spring Cloud Netlix Eureka 只不过对netflix的eureka进行了封装,加了一些注解,对spring boot进行支持

https://github.com/spring-cloud/spring-cloud-netflix
https://github.com/Netflix/eureka

2.Netflix Eureka 项目结构

  • eureka-client:eureka 的客户端,注册到eureka server上去的一个服务,就是一个客户端,无论是注册、还是发现服务,无论是提供者还是服务消费者,都是一个eureka 客户端
  • eureka-client-archaius2
  • eureka-client-jersey2:jersey框架类比于spring web mvc框架,支持mvc模式,支持restful http请求
  • eureka-core:这个指eureka 客户端,注册中心
  • eureka-core-jersey2
  • eureka-examples:eureka 提供的例子,单元测试
  • eureka-resources:基于jsp开发的eureka 控制台,web页面,可以看到服务的注册信息
  • eureka-server:这里把eureka-client、eureka-server、eureka-resources 打包成一个war包,eureka server本身也是一个eureka client 同时也是注册中心,同时也是eureka控制台,真正的使用注册中心
  • eureka-server-governator
  • eureka-test-utils:单元测试工具类

3.eureka server 初探

3.1 eureka server的gradle分析

  • eureka运行的核心的流程,eureka client往eureka server注册的过程,服务注册;服务发现,eureka client从eureka server获取注册表的过程;服务心跳,eureka client定时往eureka server发送续约通知(心跳);服务实例摘除;通信,限流,自我保护,server集群

  • eureka server是依赖eureka client的,eureka server也是一个eureka client,因为server集群模式的时候,eureka server也要扮演eureka client的角色,往其他的eureka server上去注册

  • eureka core,扮演了核心的注册中心的角色,接收别人的服务注册请求,提供服务发现的功能,保持心跳(续约请求),摘除故障服务实例。eureka server依赖eureka core的,基于eureka core的功能对外暴露接口,提供注册中心的功能

  • jersey框架,eureka server依赖jersey框架,可以认为jersey框架类比于spring web mvc框架,支持mvc模式,支持restful http请求。jersey在国内几乎没什么公司使用,很少很少,在国外有一些公司用,netflix就会用,jersey去开发eureka。eureka里面,服务通信,都是基于http请求的,restful接口来通信的。

  • eureka client和eureka server之间进行通信,都是基于jersey框架实现http restful接口请求和调用的。eureka-client-jersey2,eureka-core-jersey2,其实这两个工程,就是eureka为了方便自己,对jersey框架的一个封装,提供更多的功能,方便自己使用。

      ![](https://img-blog.csdnimg.cn/img_convert/87641780eaec0b36c0a75c8ebaf6a5bf.png)
    

eureka server 本质就是一个web应用,工程里并没有什么源码
依赖 eureka-client  本身也可以作为客户端进行服务注册
依赖 eureka-core  eureka-server的全部功能都在eureka-core工程中
依赖 jersey 通过jersey 封装http请求 restful接口通信
打包会将eureka-resorces工程下的resources 给搞到这个war包里

3.2 eureka-server web工程的web.xml分析

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- listener 是在web应用启动的时候就会执行的,负责对这个web应用进行初始化的事-->
<!--  在eureka-core里   负责eureka-server的初始化-->
  <listener>
    <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
  </listener>

<!--  负责状态相关的处理逻辑-->
  <filter>
    <filter-name>statusFilter</filter-name>
    <filter-class>com.netflix.eureka.StatusFilter</filter-class>
  </filter>
<!--  对请求进行认证处理-->
  <filter>
    <filter-name>requestAuthFilter</filter-name>
    <filter-class>com.netflix.eureka.ServerRequestAuthFilter</filter-class>
  </filter>
<!--  负责限流相关的逻辑 -->
  <filter>
    <filter-name>rateLimitingFilter</filter-name>
    <filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
  </filter>
<!--  压缩相关的,encoding编码相关-->
  <filter>
    <filter-name>gzipEncodingEnforcingFilter</filter-name>
    <filter-class>com.netflix.eureka.GzipEncodingEnforcingFilter</filter-class>
  </filter>

<!--  类似于mvc框架-->
  <filter>
    <filter-name>jersey</filter-name>
    <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name>
      <param-value>/(flex|images|js|css|jsp)/.*</param-value>
    </init-param>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>com.sun.jersey;com.netflix</param-value>
    </init-param>

    <!-- GZIP content encoding/decoding -->
    <init-param>
      <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
      <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
    </init-param>
    <init-param>
      <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
      <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
    </init-param>
  </filter>


<!--  statusFilter  requestAuthFilter通用逻辑,对所用请求生效-->
  <filter-mapping>
    <filter-name>statusFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>requestAuthFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>


<!--  默认是不开启的,如果要打开,需要把注释打开 让filter生效-->
  <!-- Uncomment this to enable rate limiter filter.
  <filter-mapping>
    <filter-name>rateLimitingFilter</filter-name>
    <url-pattern>/v2/apps</url-pattern>
    <url-pattern>/v2/apps/*</url-pattern>
  </filter-mapping>
  -->

<!--  拦截 /v2/apps相关的请求-->
  <filter-mapping>
    <filter-name>gzipEncodingEnforcingFilter</filter-name>
    <url-pattern>/v2/apps</url-pattern>
    <url-pattern>/v2/apps/*</url-pattern>
  </filter-mapping>

<!--  拦截所有请求-->
  <filter-mapping>
    <filter-name>jersey</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

<!--  欢迎页面-->
  <welcome-file-list>
    <welcome-file>jsp/status.jsp</welcome-file>
  </welcome-file-list>
</web-app>

  • listener:监听web应用启动的时候就会执行,负责对这个web应用进行初始化的事儿,最重要的就是listener-com.netflix.eureka.EurekaBootStrap
  • filter:
    • StatusFilter:负责状态相关的处理逻辑
    • ServerRequestAuthFilter:对请求进行授权认证的处理的
    • RateLimitingFilter:负责限流相关的逻辑的
    • GzipEncodingEnforcingFilter:gzip,压缩相关的;encoding,编码相关
    • jersey:是拦截所有的请求的
  • welcome-file-list:配置了status.jsp是欢迎页面,首页,eureka-server的控制台页面,展示注册服务的信息
eureka-server -> build.gradle中的依赖和构建的配置
eureka-server -> web应用 -> war包 -> tomcat就可以启动
web.xml -> listener -> 4个filter -> jersy filter -> filter mapping -> welcome file

3.3 eureka server启动之环境初始化

当eureka-server打成war包 放在容器中被启动,监听器会监听到web应用被启动
EurekaBootStrap 实现了 ServletContextListener 重写contextInitialized方法
web应用启动会触发contextInitialized方法执行,也就是eureka server启动初始化的入口

3.3.1 初始化Eureka-server环境-initEurekaEnvironment

protected void initEurekaEnvironment() throws Exception {
    logger.info("Setting the eureka configuration..");

    //ConfigurationManager  管理Eureka自己的所有配置 读取配置文件中配置到内存中,
    //供后续的eureka-server运行来使用  单例
    //获取数据中心
    String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
    // EUREKA_DATACENTER 如果没有配置  就默认default
    if (dataCenter == null) {
        //用默认的
        logger.info("Eureka data center value eureka.datacenter is not set, defaulting to default");
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
    } else {
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
    }
    //EUREKA_ENVIRONMENT = eureka.environment  拿环境
    String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
    if (environment == null) {
        //如果没配默认就是test环境
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
        logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
    }
}


  public static AbstractConfiguration getConfigInstance() {
        if (instance == null) {
            synchronized (ConfigurationManager.class) {
                if (instance == null) {
                    instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
                }
            }
        }
        return instance;
    }
加载eureka-server.properties文件中的配置
  • ConfigurationManager.getConfigInstance():初始化配置管理器管理所有的配置(ConfigurationManager是属于netfilx config开源项目的),读取配置文件中配置到内存中,供后续的eureka-server运行来使用
  • getConfigInstance double check + volatile 单例 实现线程安全
  • 初始化eurueka运行的环境,如果你没有配置的话,默认就给你设置为test环境,初始化完成

3.3.2 初始化Eureka-Server上下文-initEurekaServerContext

1.加载eureka-server.properties文件中的配置

EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig()

  • 创建DefaultEurekaServerConfig对象,面向接口编程,eurekaServerConfig 是一个接口,会执行一个init()方法,DefaultEurekaServerConfig会将eureka-server.properties文件中的配置加载出来 都放到 ConfigurationManager 中去

  • DefaultEurekaServerConfig 提供各个获取配置项的各个方法,都是通过硬编码的配置项的名称,从DynamicPropertyFactory中获取配置项的值

  • 在获取配置项的时候,如果没有配置,那么就会有默认的值,全部属性都有默认值

2.构建ApplicationInfoManager和eurekaClient


        ApplicationInfoManager applicationInfoManager = null;
        if (eurekaClient == null) {
            EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
                    ? new CloudInstanceConfig()
                    : new MyDataCenterInstanceConfig();
     
            applicationInfoManager = new ApplicationInfoManager(
                    instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
         

            EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();

            eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
        } else {
            applicationInfoManager = eurekaClient.getApplicationInfoManager();
        }

EurekaInstanceConfig:基于EurekaInstanceConfig对外暴露接口来获取这个eureka-client.properties文件中的一些配置的读取,判断是否云环境,如果不是从子类MyDataCenterInstanceConfig 创建配置类,面向接口编程

CommonConstants.CONFIG_FILE_NAME=eureka-client

初始化applicationInfoManager:服务实例管理器 如果你是集群模式,eureka server扮演的角色其实就是一个服务,作为一个服务会向其他eureka Server注册 eureka Client->Application ,applicationInfoManager管理当前Server作为Client的InstanceInfo 实例信息

4

EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig() 包含了eureka client相关的一些配置项,去读eureka-client.properties里的一些配置

这里跟上面的EurekaInstanceConfig关注点是不一样的 EurekaInstanceConfig代表一些服务实例InstanceInfo的配置项,

EurekaClientConfig 关联的是eureka client的一些配置项,基于EurekaClientConfig 接口对外暴露 获取eureka client的一些配置项

DiscoveryClient:基于applicationInfoManager(包含了服务实例的信息、配置,作为服务实例管理的一个组件)和eurekaClientConfig(eureka client 相关的配置) 构建一个eureka client

  • eureka client实例信息读取存储
  • 是否从其他eureka服务器获取注册表获取抓取信息(eureka server集群状态)
  • 支持调度的线程池
  • 支持心跳的线程池
  • 支持缓存刷新的线程池
  • 支持eureka client和 eureka server 进行网络通信的组件
  • 注册表的抓取
  • 初始化调度任务
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
        if (args != null) {
            this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
            this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
            this.eventListeners.addAll(args.getEventListeners());
            this.preRegistrationHandler = args.preRegistrationHandler;
        } else {
            this.healthCheckCallbackProvider = null;
            this.healthCheckHandlerProvider = null;
            this.preRegistrationHandler = null;
        }
        
        this.applicationInfoManager = applicationInfoManager;
        InstanceInfo myInfo = applicationInfoManager.getInfo();

        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        instanceInfo = myInfo;
        if (myInfo != null) {
            //appName代表一个服务名称,可能呢服务部署了多台机器,每台机器上部署就是一个服务实例
            appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
        } else {
            logger.warn("Setting instanceInfo to a passed in null value");
        }

        this.backupRegistryProvider = backupRegistryProvider;

        this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
        localRegionApps.set(new Applications());

        //抓取注册表相关的东西
        fetchRegistryGeneration = new AtomicLong(0);

        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));

        /**
         * 指示此客户端是否应从eureka服务器获取eureka注册表信息
         * 如果是eureka server的话 会将这个fetchRegistry给手动设置为false  默认是true
         * 如果是eureka server集群的话就要保持为true
         */
        if (config.shouldFetchRegistry()) {
            this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }
        /**
         * 指示此实例是否应向eureka服务器注册其信息以供其他人发现
         * 如果是单个eureka server 就设置成false  否则反之
         */
        if (config.shouldRegisterWithEureka()) {
            this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }

        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());

        //单机节点  不要注册也不要抓取
        if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            logger.info("Client configured to neither register nor query for data.");
            scheduler = null;
            heartbeatExecutor = null;
            cacheRefreshExecutor = null;
            eurekaTransport = null;
            instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());

            // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
            // to work with DI'd DiscoveryClient
            DiscoveryManager.getInstance().setDiscoveryClient(this);
            DiscoveryManager.getInstance().setEurekaClientConfig(config);

            initTimestampMs = System.currentTimeMillis();
            logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                    initTimestampMs, this.getApplications().size());

            return;  // no need to setup up an network tasks and we are done
        }

        try {
            // default size of 2 - 1 each for heartbeat and cacheRefresh
            //支持调度的线程池
            scheduler = Executors.newScheduledThreadPool(2,
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-%d")
                            .setDaemon(true)
                            .build());

            //支持心跳线程池
            heartbeatExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

            //支持缓存刷新的线程池
            cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

            //支持底层eureka client 和eureka server 进行网络通信的组件
            eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);

            AzToRegionMapper azToRegionMapper;
            if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
                azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
            } else {
                azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
            }
            if (null != remoteRegionsToFetch.get()) {
                azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
            }
            instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
        } catch (Throwable e) {
            throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
        }

        //抓取注册表
        //如果你配置 应该要抓取注册表的信息,那么就会在启动的时候来一次全量的注册表的抓取
        if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            //如果抓取失败 从备份哪里拿
            fetchRegistryFromBackup();
        }

        // call and execute the pre registration handler before all background tasks (inc registration) is started
        if (this.preRegistrationHandler != null) {
            this.preRegistrationHandler.beforeRegistration();
        }
        //初始化调度任务
        initScheduledTasks();

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }

        // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
        // to work with DI'd DiscoveryClient
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);

        initTimestampMs = System.currentTimeMillis();
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                initTimestampMs, this.getApplications().size());
    }

PeerAwareInstanceRegistry:处理注册相关的事情

PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
    registry = new AwsInstanceRegistry(
            eurekaServerConfig,
            eurekaClient.getEurekaClientConfig(),
            serverCodecs,
            eurekaClient
    );
    awsBinder = new AwsBinderDelegate(eurekaServerConfig, eurekaClient.getEurekaClientConfig(), registry, applicationInfoManager);
    awsBinder.start();
} else {
    //创建一个注册表
    registry = new PeerAwareInstanceRegistryImpl(
            eurekaServerConfig,
            eurekaClient.getEurekaClientConfig(),
            serverCodecs,
            eurekaClient
    );
}

	/**
     * Create a new, empty instance registry.
     */
 protected AbstractInstanceRegistry(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, ServerCodecs serverCodecs) {
        this.serverConfig = serverConfig;
        this.clientConfig = clientConfig;
        this.serverCodecs = serverCodecs;
        this.recentCanceledQueue = new CircularQueue<Pair<Long, String>>(1000);
        this.recentRegisteredQueue = new CircularQueue<Pair<Long, String>>(1000);

        this.renewsLastMin = new MeasuredRate(1000 * 60 * 1);

        //增量的任务调度  //默认30秒一次
        //默认30秒一次 ,看一下服务实例的变更记录,是否在队列里停留超过180秒 如果超过3分钟,
        //就会从队列里将这个服务实例给移除掉,这个recentlyChangedQueue 只保留最近三分钟的服务实例变更
        this.deltaRetentionTimer.schedule(getDeltaRetentionTask(),
                serverConfig.getDeltaRetentionTimerIntervalInMs(),
                serverConfig.getDeltaRetentionTimerIntervalInMs());
   }
   
   /**
     * 默认30秒一次 ,看一下服务实例的变更记录,是否在队列里停留超过180秒 如果超过3分钟,
     * 就会从队列里将这个服务实例给移除掉,这个recentlyChangedQueue 只保留最近三分钟的服务实例变更
     * @return
     */
    private TimerTask getDeltaRetentionTask() {
        return new TimerTask() {

            @Override
            public void run() {
                Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
                while (it.hasNext()) {
                    //说明这个记录进入队列的时间超过3分钟
                    if (it.next().getLastUpdateTime() <
                            System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
                        //就把这条记录从队列移除
                        it.remove();
                    } else {
                        break;
                    }
                }
            }

        };
    }

PeerEurekaNodes

//处理PeerEurekaNodes节点相关的事情
//代表一个eureka server集群
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
        registry,
        eurekaServerConfig,
        eurekaClient.getEurekaClientConfig(),
        serverCodecs,
        applicationInfoManager
);

eureka server上下文(context)的构建及初始化

完成eureka server上下文(context)的构建及初始化,EurekaServerContext代表了当前这个eureka server的一个服务器上下文,包含了服务器需要的所有的东西

serverContext = new DefaultEurekaServerContext(
        eurekaServerConfig,
        serverCodecs,
        registry,
        peerEurekaNodes,
        applicationInfoManager
);

 //将serverContext 放入EurekaServerContextHolder  以后谁要是要用这个上下文,直接从holder里面去
 EurekaServerContextHolder.initialize(serverContext);
 
 // 启动eureka server
  serverContext.initialize();
  logger.info("Initialized server context");

从相邻的eureka节点拷贝注册信息

 int registryCount = registry.syncUp();
  //自动检查服务服务实例是否宕机
 registry.openForTraffic(applicationInfoManager, registryCount);

eureka监控相关的事情

EurekaMonitors.registerAllStats();

4.eureka Server启动流程梳理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值