Eureka源码分析

一.EurekaServer

1.入口分析:

切入点(一):

对于EurekaServer,我们只是在主配置类中添加了@EnableEurekaServer这个注解,所以我们需要以此为入口分析EurekaServer端源码

@EnableEurekaServer这个注解的主要作用是导入EurekaServerMarkerConfiguration配置类
主要作用向spring容器中注入一个标记类 Marker
通过判断spring容器中是否含有Marker这个bean,来判断是否是Euraka注册中心

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented

@Import(EurekaServerMarkerConfiguration.class)
//@EnableEurekaServer这个注解的主要作用是导入EurekaServerMarkerConfiguration配置类
public @interface EnableEurekaServer {

}

//主要作用向spring容器中注入一个标记类  Marker
//通过判断spring容器中是否含有marker这个bean,来判断是否是Euraka注册中心
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {

	@Bean
	public Marker eurekaServerMarkerBean() {
		return new Marker();
	}

	class Marker {

	}

}

切入点(二):

利用springboot自动配置的方式,导入了EnableAutoConfiguration配置类

hbger

自动配置

首先分析下自动配置类上的注解信息:

  • @ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
    判断spring容器中是否有Marker类,如果有则EurekaServerAutoConfiguration会被注入到spring容器中
    结合切入点(一)得出结论,只有使用了@EnableEurekaServer注解,才会激活EurekaServer的自动配置
  • @Import(EurekaServerInitializerConfiguration.class)
    导入了EurekaServerInitializerConfiguration这个Bean,这里先给出结论,后文在具体分析它的源码
    • 初始化 Eureka 配置
    • 初始化 Eureka Context ,包括集群同步注册信息,启动一些定时器(服务剔除,自我保护机制监听…)
    • 初始化自我保护机制的阈值
  • @PropertySource({"classpath:/eureka/server.properties"})
    加载类路径下的:eureka/server.properties属性文件
  • @EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class })把配置文件中eureka.dashboard下的信息绑定到EurekaDashboardProperties类,eureka.instance.registry下的信息绑定到InstanceRegistryProperties类

然后分析下注入的几个重要的Bean:

  • EurekaController:初始化Eureka仪表盘页面访问的接口信息(通过指定地址访问EurekaServer控制页面)
  • EurekaServerConfig:封装了EurekaServer的配置信息(配置文件eureka.server)
  • jerseyApplication:构建一个Jersey过滤器,类似SpringMVC,用于拦截客户端发来的请求
  • PeerAwareInstanceRegistry:原生Eureka提供的接口,用于完成服务注册,服务同步功能
  • EurekaServerBootstrap:构建EurekaServer启动器
@Configuration(proxyBeanMethods = false)
/**  导入EurekaServerInitializerConfiguration bean,这个类作用:
 *   1.初始化 Eureka 配置
 *   2.初始化 Eureka Context ,包括集群同步注册信息,启动一些定时器(服务剔除,自我保护机制监听...)
 *   3.初始化自我保护机制的阈值
 */
@Import(EurekaServerInitializerConfiguration.class)
//判断spring容器中是否有Marker类,如果有则EurekaServerAutoConfiguration会被注入到spring容器中
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")

/**
 * Euraka自动配置类
 * 主要做两件事
 * 1.初始化Eureka上下文
 * 2.把jersy核心过滤器注入到spring web容器中
 */
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
    
   
    //初始化 EurekaServer仪表盘控制页面
    @Bean
	@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled",
			matchIfMissing = true)
	public EurekaController eurekaController() {
		return new EurekaController(this.applicationInfoManager);
	}

     // 加载EurekaServer端的相关配置(yml文件eureka.server下的信息)
    @Configuration(proxyBeanMethods = false)
	protected static class EurekaServerConfigBeanConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
			EurekaServerConfigBean server = new EurekaServerConfigBean();
			if (clientConfig.shouldRegisterWithEureka()) {
				// Set a sensible default if we are supposed to replicate
				server.setRegistrySyncRetries(5);
			}
			return server;
		}

	}
    //PeerAwareInstanceRegistry为原生Eureka提供的接口,用于完成服务注册,服务同步
    @Bean
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		this.eurekaClient.getApplications(); // force initialization
		return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
				serverCodecs, this.eurekaClient,
				this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(),
				this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
	}
    
    //初始化 EurekaServer 启动器,EurekaServerContext为原生Eureka的初始化类
    @Bean
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}

    
    //注册Jersey过滤器。 把Jersey过滤器工作起来,这样就可以拦截请求了
	@Bean
	public FilterRegistrationBean<?> jerseyFilterRegistration(
			javax.ws.rs.core.Application eurekaJerseyApp) {
		FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<Filter>();
		bean.setFilter(new ServletContainer(eurekaJerseyApp));
		bean.setOrder(Ordered.LOWEST_PRECEDENCE);
		bean.setUrlPatterns(
				Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));

		return bean;
	}
    //构建一个Jersey过滤器
	@Bean
	public javax.ws.rs.core.Application jerseyApplication(Environment environment,
			ResourceLoader resourceLoader) {

		ClassPathScanningCandidateComponentProvider provider = 
						new ClassPathScanningCandidateComponentProvider(
																	false, environment);

		// Filter to include only classes that have a particular annotation.
		// 过滤包含@Path @Provider标记的类,类似于springMVC中的注解
		provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));
		provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));

		// Find classes in Eureka packages (or subpackages)
		//
		Set<Class<?>> classes = new HashSet<>();
		for (String basePackage : EUREKA_PACKAGES) {
			Set<BeanDefinition> beans = provider.findCandidateComponents(basePackage);
			for (BeanDefinition bd : beans) {
				Class<?> cls = ClassUtils.resolveClassName(bd.getBeanClassName(),
						resourceLoader.getClassLoader());
				classes.add(cls);
			}
		}

		// Construct the Jersey ResourceConfig
		Map<String, Object> propsAndFeatures = new HashMap<>();
		propsAndFeatures.put(
				// Skip static content used by the webapp
				ServletContainer.PROPERTY_WEB_PAGE_CONTENT_REGEX,
				EurekaConstants.DEFAULT_PREFIX + "/(fonts|images|css|js)/.*");

		DefaultResourceConfig rc = new DefaultResourceConfig(classes);
		rc.setPropertiesAndFeatures(propsAndFeatures);

		return rc;
	}
    
}
初始化

接下来我们看下EurekaServerInitializerConfiguration这个初始化类,省略部分代码
发现它实现了SmartLifecycle接口,触发Spring生命周期回调机制,也就是说这个类注入到Spring容器后会调用start方法
在Start方法中完成了初始化EurekaServer,以及发布了特定事件,我们可以监听这种事件,然后做一些特定的业务需求

@Configuration(proxyBeanMethods = false)
public class EurekaServerInitializerConfiguration
		implements ServletContextAware, SmartLifecycle, Ordered {

...
	@Override
	public void start() {
		new Thread(() -> {
			try {
				//初始化EurekaServer,同时启动Eureka Server
				eurekaServerBootstrap.contextInitialized(
						EurekaServerInitializerConfiguration.this.servletContext);
				log.info("Started Eureka Server");
				//发布服务注册注册事件
				publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
				EurekaServerInitializerConfiguration.this.running = true;
                //发布EurekaServer启动完成事件
				publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
			}
			catch (Exception ex) {
				// Help!
				log.error("Could not initialize Eureka servlet context", ex);
			}
		}).start();
	}
...
}

主要做了两件事儿:初始化运行环境(eureka.environment配置信息)、初始化上下文

    public void contextInitialized(ServletContext context) {
        try {
            //初始化 yml配置信息
            this.initEurekaEnvironment();
            //初始化Eureka上下文
            this.initEurekaServerContext();
            context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
        } catch (Throwable var3) {
            log.error("Cannot bootstrap eureka server :", var3);
            throw new RuntimeException("Cannot bootstrap eureka server :", var3);
        }
    }

我们主要关注主要逻辑,在初始化上下文过程中首先进行了集群同步操作,注意这是在服务端启动时做的操作,在服务端运行过程中同样会进行集群同步操作,另外还设置了服务剔除的定时器,定时清除没有续约的实例,具体内容在后文主线部分再做分析

    protected void initEurekaServerContext() throws Exception {
        JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), 10000);
        XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), 10000);
        if (this.isAws(this.applicationInfoManager.getInfo())) {
            
            this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig, 
                                                   this.eurekaClientConfig,
                                                   this.registry,
                                                   this.applicationInfoManager);
            this.awsBinder.start();
        }

        EurekaServerContextHolder.initialize(this.serverContext);
        log.info("Initialized server context");
        //服务启动时,从集群中的其他节点同步信息
        int registryCount = this.registry.syncUp();
        //初始化自我保护机制阈值,设置服务剔除定时器
        this.registry.openForTraffic(this.applicationInfoManager, registryCount);
        EurekaMonitors.registerAllStats();
    }

2.主线分析

主要从5方面分析Eureka Server源码:服务注册、心跳链接、服务下架、集群同步、自我保护机制

服务注册源码分析

Eureka服务注册实际是利用了Jersey框架核心过滤器,拦截服务注册请求实现的。

入口:ApplicationResource#addInstance

    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                               @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {

        // 做一些信息校验
		//	....
		//核心方法   PeerAwareInstanceRegistry registry;
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  
    }
    @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
       // Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认90s
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }
        //服务注册
        super.register(info, leaseDuration, isReplication);
        //集群同步
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }
   /**
     * Registers a new instance with a given duration.
     * registrant 本次请求传入的注册信息
     * leaseDuration 服务过期时间 默认是60秒
     * isReplication 集群同步需要使用这个属性
     */
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        read.lock();
        try {
            //存储所有服务信息的集合
            //ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            //数据结构,其中最外层String 对应服务称;里面Map<服务ID,租债器>
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap 
                    = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            //判断传入的微服务实例是否存在
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
      
            if (existingLease != null && (existingLease.getHolder() != null)) {
                //解决冲突
                //拿到注册中心中存在的微服务实例最后操作时间戳
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                //拿到传入的微服务实例最后操作时间戳
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
				//如果存在时间戳>传入服务时间戳
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
					//存在的服务实例覆盖传入服务实例
                    registrant = existingLease.getHolder();
                }
            } else {
         
                synchronized (lock) {
                    //expectedNumberOfClientsSendingRenews   
                    //心跳连接预估值  会在服务注册和服务下架的时候更新
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                      
                        this.expectedNumberOfClientsSendingRenews = this.
                            expectedNumberOfClientsSendingRenews + 1;
                        //自我保护阈值更新
                        updateRenewsPerMinThreshold();
                    }
                }
            }
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            //完成服务注册
            gMap.put(registrant.getId(), lease);
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
               
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                   
                    overriddenInstanceStatusMap
                        .put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
          InstanceStatus overriddenStatusFromMap=overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            
            InstanceStatus overriddenInstanceStatus = 
                getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            
            invalidateCache(registrant.getAppName(),
                            registrant.getVIPAddress(), 
                            registrant.getSecureVipAddress());
          
        } finally {
            read.unlock();
        }
    }
心跳续约源码分析

入口InstanceResource#renewLease

    @PUT
    public Response renewLease(
            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
            @QueryParam("overriddenstatus") String overriddenStatus,
            @QueryParam("status") String status,
            @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
        
        boolean isFromReplicaNode = "true".equals(isReplication);
        //心跳续约入口
        boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
        
        if (!isSuccess) {
      
            return Response.status(Status.NOT_FOUND).build();
        }
        // Check if we need to sync based on dirty time stamp, the client
        // instance might have changed some value
        Response response;
        if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
            response = this.validateDirtyTimestamp(
                Long.valueOf(lastDirtyTimestamp), 
                isFromReplicaNode
            );

            if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
                    && (overriddenStatus != null)
                    && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
                    && isFromReplicaNode) {
                registry.storeOverriddenStatusIfRequired(
                    app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
            }
        } else {
            response = Response.ok().build();
        }
        return response;
    }
    public boolean renew(final String appName, final String serverId, boolean isReplication) {
        
        List<Application> applications = this.getSortedApplications();
        Iterator var5 = applications.iterator();
		
        while(var5.hasNext()) {
            Application input = (Application)var5.next();
            if (input.getName().equals(appName)) {
                InstanceInfo instance = null;
                Iterator var8 = input.getInstances().iterator();

                while(var8.hasNext()) {
                    InstanceInfo info = (InstanceInfo)var8.next();
                    if (info.getId().equals(serverId)) {
                        instance = info;
                        break;
                    }
                }
                //事件监听机制
                this.publishEvent(new EurekaInstanceRenewedEvent
                						(this, appName, serverId, instance, isReplication));
                break;
            }
        }

        return super.renew(appName, serverId, isReplication);
    }
    public boolean renew(final String appName, final String id, final boolean isReplication) {
        if (super.renew(appName, id, isReplication)) {
            //集群同步
            replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
            return true;
        }
        return false;
    }
    public boolean renew(String appName, String id, boolean isReplication) {
        
        RENEW.increment(isReplication);
        
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = gMap.get(id);
        }
        if (leaseToRenew == null) {
            RENEW_NOT_FOUND.increment(isReplication); 
            return false;
        } else {
            InstanceInfo instanceInfo = leaseToRenew.getHolder();
            if (instanceInfo != null) {

                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                        instanceInfo, leaseToRenew, isReplication);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                  
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
             
                                    instanceInfo.getOverriddenStatus().name(),
                                    instanceInfo.getId());
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

                }
            }
            renewsLastMin.increment();
            leaseToRenew.renew();
            return true;
        }
    }
集群同步

集群同步操作在PeerAwareInstanceRegistryImpl这个类中完成:

服务注册,服务下架,心跳续约,状态修改等操作均需要进行集群同步

public enum Action {
        Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride;

        private com.netflix.servo.monitor.Timer timer = Monitors.newTimer(this.name());

        public com.netflix.servo.monitor.Timer getTimer() {
            return this.timer;
        }
}

原理:客户端client发送注册请求给服务端server1,之后服务端会发送同样的请求给集群中其他节点server2,server3,区别在于请求中是否携带isReplication属性

/**
 *  action 操作类型,appName 服务名,id服务ID,info 实例信息,isReplication 是否是集群同步操作
 */
private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info ,
                                  InstanceStatus newStatus , boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }

            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // 如果url代表的主机为当前节点点,没必要同步
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }
//根据不同的操作,发送不同的请求    
private void replicateInstanceActionsToPeers(Action action, String appName,
                                                 String id, InstanceInfo info, 
                                                 InstanceStatus newStatus,
                                                 PeerEurekaNode node) {
        try {
            InstanceInfo infoFromRegistry;
            CurrentRequestVersion.set(Version.V2);
            switch (action) {
                case Cancel:
                    node.cancel(appName, id);
                    break;
                case Heartbeat:
                    InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                    break;
                case Register:
                    node.register(info);
                    break;
                case StatusUpdate:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                    break;
                case DeleteStatusOverride:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.deleteStatusOverride(appName, id, infoFromRegistry);
                    break;
            }
        } catch (Throwable t) {
          
        } finally {
            CurrentRequestVersion.remove();
        }
    }
服务剔除、服务下架

入口:PeerAwareInstanceRegistryImpl#openForTraffic
通过上文分析,我们知道EurekaServer在初始化过程中会,设置一个服务剔除的定时器

    @Override
    public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
        // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
        this.expectedNumberOfClientsSendingRenews = count;
        updateRenewsPerMinThreshold();

        this.startupTime = System.currentTimeMillis();
        if (count > 0) {
            this.peerInstancesTransferEmptyOnStartup = false;
        }
        DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
        //aws与亚马逊云有关,忽略
        boolean isAws = Name.Amazon == selfName;
        if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
            logger.info("Priming AWS connections for all replicas..");
            primeAwsReplicas(applicationInfoManager);
        }
        logger.info("Changing status to UP");
        
        applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
        //开启定时任务,默认60秒执行一次,用于清理60秒之内没有续约的实例
        super.postInit();
    }

    protected void postInit() {
        renewsLastMin.start();
        if (evictionTaskRef.get() != null) {
            evictionTaskRef.get().cancel();
        }
        //设置服务剔除定时器,剔除逻辑在EvictionTask线程类中
        evictionTaskRef.set(new EvictionTask());
        
        evictionTimer.schedule(evictionTaskRef.get(),
                serverConfig.getEvictionIntervalTimerInMs(),
                serverConfig.getEvictionIntervalTimerInMs());
    }
  public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");
		//如果触发自我保护机制,则不进行剔除操作
        if (!isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
            return;
        }
		//获取服务列表,删选要剔除的服务
        List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
        for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
            Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
            if (leaseMap != null) {
                for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                    Lease<InstanceInfo> lease = leaseEntry.getValue();
                    if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                        expiredLeases.add(lease);
                    }
                }
            }
        }
		//判断如果此次剔除的数量如果过大的话,为保证可用性,不会全部剔除,会有个保护值默认85%
        //比如100个服务要剔除20个,此次剔除操作超过85%阈值,此次剔除只会剔除15个服务
        int registrySize = (int) getLocalRegistrySize();
        int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
        int evictionLimit = registrySize - registrySizeThreshold;

        int toEvict = Math.min(expiredLeases.size(), evictionLimit);
        if (toEvict > 0) {

            Random random = new Random(System.currentTimeMillis());
            for (int i = 0; i < toEvict; i++) {
                // Pick a random item (Knuth shuffle algorithm)
                int next = i + random.nextInt(expiredLeases.size() - i);
                Collections.swap(expiredLeases, i, next);
                Lease<InstanceInfo> lease = expiredLeases.get(i);

                String appName = lease.getHolder().getAppName();
                String id = lease.getHolder().getId();
                EXPIRED.increment();
                logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
                internalCancel(appName, id, false);
            }
        }
    }
自我保护机制

会在服务初始化,服务注册,服务下架,15分钟定时器自动触发

 //15分钟定时器自动触发  
private void scheduleRenewalThresholdUpdateTask() {
        timer.schedule(new TimerTask() {
                           @Override
                           public void run() {
                               updateRenewalThreshold();
                           }
                       }, serverConfig.getRenewalThresholdUpdateIntervalMs(),
                serverConfig.getRenewalThresholdUpdateIntervalMs());
    }

阈值算法:所有注册的实例 * 60s / 心跳续约时间默认30s * 自我保护机制触发百分比默认85%

 protected void updateRenewsPerMinThreshold() {
        this.numberOfRenewsPerMinThreshold = 
            (int) (this.expectedNumberOfClientsSendingRenews
                * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
                * serverConfig.getRenewalPercentThreshold());
    }
服务发现

入口:ApplicationsResource#getContainers

    @GET
    public Response getContainers(@PathParam("version") String version,
                                  @HeaderParam(HEADER_ACCEPT) String acceptHeader,
                                  @HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
                                  @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
                                  @Context UriInfo uriInfo,
                                  @Nullable @QueryParam("regions") String regionsStr) {

        boolean isRemoteRegionRequested = null != regionsStr && !regionsStr.isEmpty();
        String[] regions = null;
        if (!isRemoteRegionRequested) {
            EurekaMonitors.GET_ALL.increment();
        } else {
            regions = regionsStr.toLowerCase().split(",");
            Arrays.sort(regions); 
            EurekaMonitors.GET_ALL_WITH_REMOTE_REGIONS.increment();
        }

        if (!registry.shouldAllowAccess(isRemoteRegionRequested)) {
            return Response.status(Status.FORBIDDEN).build();
        }
        CurrentRequestVersion.set(Version.toEnum(version));
        KeyType keyType = Key.KeyType.JSON;
        String returnMediaType = MediaType.APPLICATION_JSON;
        if (acceptHeader == null || !acceptHeader.contains(HEADER_JSON_VALUE)) {
            keyType = Key.KeyType.XML;
            returnMediaType = MediaType.APPLICATION_XML;
        }
		//封装缓存key
        Key cacheKey = new Key(Key.EntityType.Application,
                ResponseCacheImpl.ALL_APPS,
                keyType, CurrentRequestVersion.get(), 
               EurekaAccept.fromString(eurekaAccept), regions
        );

        Response response;
        if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
            //从缓存中获取注册表信息
            response = Response.ok(responseCache.getGZIP(cacheKey))
                    .header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
                    .header(HEADER_CONTENT_TYPE, returnMediaType)
                    .build();
        } else {
            response = Response.ok(responseCache.get(cacheKey))
                    .build();
        }
        CurrentRequestVersion.remove();
        return response;
    }

会向缓存中拿注册信息

    public byte[] getGZIP(Key key) {
        //入口
        Value payload = getValue(key, shouldUseReadOnlyResponseCache);
        if (payload == null) {
            return null;
        }
        return payload.getGzipped();
    }
 Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            if (useReadOnlyCache) {
                //只读缓存
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                //读写缓存  过期时间默认180s
                payload = readWriteCacheMap.get(key);
            }
        } catch (Throwable t) {
            logger.error("Cannot get value for key : {}", key, t);
        }
        return payload;
    }
三级缓存

多级缓存设计思想 :

  • 在拉取注册表的时候:

    • 首先从ReadOnlyCacheMap里查缓存的注册表。
    • 若没有,就找ReadWriteCacheMap里缓存的注册表。
    • 如果还没有,就从内存中获取实际的注册表数据。
  • 在注册表发生变更的时候:

    • 会在内存中更新变更的注册表数据,同时过期掉ReadWriteCacheMap
    • 此过程不会影响ReadOnlyCacheMap提供人家查询注册表。
    • 默认每30秒Eureka Server会将ReadWriteCacheMap更新到ReadOnlyCacheMap
    • 默认每180秒Eureka Server会将ReadWriteCacheMap里是数据失效

入口:ResponseCacheImpl

只读缓存:只读缓存的数据只会来源于读写缓存,而且没有主动更新缓存的API,只能通过定时更新,默认每30s执行一次

if (shouldUseReadOnlyResponseCache) {
    timer.schedule(getCacheUpdateTask(),
                   new Date(
                       ((System.currentTimeMillis() /  esponseCacheUpdateIntervalMs)
                        * responseCacheUpdateIntervalMs)
                       + responseCacheUpdateIntervalMs),
                   responseCacheUpdateIntervalMs);
}
    private TimerTask getCacheUpdateTask() {
        return new TimerTask() {
            @Override
            public void run() {
                logger.debug("Updating the client cache from response cache");
                for (Key key : readOnlyCacheMap.keySet()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("...");
                    }
                    try {
                        CurrentRequestVersion.set(key.getVersion());
                        Value cacheValue = readWriteCacheMap.get(key);
                        Value currentCacheValue = readOnlyCacheMap.get(key);
                        //判断只读缓存和读写缓存数据是否一致
                        if (cacheValue != currentCacheValue) {
                            readOnlyCacheMap.put(key, cacheValue);
                        }
                    } catch (Throwable th) {
                        logger.error("...");
                    } finally {
                        CurrentRequestVersion.remove();
                    }
                }
            }
        };
    }

读写缓存:guava

  this.readWriteCacheMap =
                CacheBuilder.newBuilder()
      .initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
      //设置过期时间  默认180s
      .expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
      .removalListener(new RemovalListener<Key, Value>() {
          @Override
          public void onRemoval(RemovalNotification<Key, Value> notification) {
              Key removedKey = notification.getKey();
              if (removedKey.hasRegions()) {
                  Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                  regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
              }
          }
      })
      //设置监听器,如果读写缓存没有数据,则进入这个方法
      .build(new CacheLoader<Key, Value>() {
          @Override
          public Value load(Key key) throws Exception {
              if (key.hasRegions()) {
                  Key cloneWithNoRegions = key.cloneWithoutRegions();
                  regionSpecificKeys.put(cloneWithNoRegions, key);
              }
              //从真实缓存中获取数据,里面涉及全量拉取和增量拉取,
              //与client端相对应,里面会有一个队列统计最近修改的数据
              Value value = generatePayload(key);
              return value;
          }
      });

真实缓存:

private Value generatePayload(Key key) {
        Stopwatch tracer = null;
        try {
            String payload;
            switch (key.getEntityType()) {
                case Application:
                    boolean isRemoteRegionRequested = key.hasRegions();
					//全量获取
                    if (ALL_APPS.equals(key.getName())) {
                        if (isRemoteRegionRequested) {
                            tracer = serializeAllAppsWithRemoteRegionTimer.start();
                            payload = getPayLoad(key,
                                registry.getApplicationsFromMultipleRegions(key.getRegions()));
                        } else {
                            tracer = serializeAllAppsTimer.start();
                            payload = getPayLoad(key, registry.getApplications());
                        }
                    //增量获取
                    } else if (ALL_APPS_DELTA.equals(key.getName())) {
                        if (isRemoteRegionRequested) {
                            tracer = serializeDeltaAppsWithRemoteRegionTimer.start();
                            versionDeltaWithRegions.incrementAndGet();
                            versionDeltaWithRegionsLegacy.incrementAndGet();
                            payload = getPayLoad(key,
                               registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
                        } else {
                            tracer = serializeDeltaAppsTimer.start();
                            versionDelta.incrementAndGet();
                            versionDeltaLegacy.incrementAndGet();
                            payload = getPayLoad(key, registry.getApplicationDeltas());
                        }
                    } else {
                        tracer = serializeOneApptimer.start();
                        payload = getPayLoad(key, registry.getApplication(key.getName()));
                    }
                    break;
                case VIP:
                case SVIP:
                    tracer = serializeViptimer.start();
                    payload = getPayLoad(key, getApplicationsForVip(key, registry));
                    break;
                default:

                    payload = "";
                    break;
            }
            return new Value(payload);
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
    }

二.EurekaClient

1.入口分析

还是从spring.factories文件入手
利用springboot自动配置原理自动导入了EurekaClientAutoConfiguration配置类:

在这里插入图片描述

而EurekaClientAutoConfiguration自动配置类主要做了两件事情:

  • 初始化Eureka客户端的相关配置
  • 初始化Eureka客户端
	//初始化Eureka客户端的相关配置,配置文件eureka.client下的信息
	@Bean
	@ConditionalOnMissingBean(value = EurekaClientConfig.class,
										search = SearchStrategy.CURRENT)
	public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
		EurekaClientConfigBean client = new EurekaClientConfigBean();
		if ("bootstrap".equals(this.env.getProperty("spring.config.name"))) {

			client.setRegisterWithEureka(false);
		}
		return client;
	}

	//初始化Eureka客户端,CloudEurekaClient是SpringCloud提供的客户端
    @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(value = EurekaClient.class,search = SearchStrategy.CURRENT)
    public EurekaClient eurekaClient(ApplicationInfoManager manager,
                                     EurekaClientConfig config) {
        return new CloudEurekaClient(manager, config, this.optionalArgs,
                                     this.context);
    }

并且在CloudEurekaClient实例化过程中,完成了服务注册,以及开启服务发现定时任务、心跳续约定时任务等核心功能

进入CloudEurekaClient实例化链,最终会调用到以下代码

    @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, 
                    EurekaClientConfig config, 
                    AbstractDiscoveryClientOptionalArgs args, 
                    Provider<BackupRegistry> backupRegistryProvider, 
                    EndpointRandomizer endpointRandomizer) {
          ...
          //设置定时调度任务   
          this.scheduler = Executors.newScheduledThreadPool( 2, 
           									  (new ThreadFactoryBuilder())
                                                            .setNameFormat("DiscoveryClient-%d")
                                                            .setDaemon(true)
                                                            .build());
         //心跳续约线程池
          this.heartbeatExecutor = new ThreadPoolExecutor(
                  1, this.clientConfig.getHeartbeatExecutorThreadPoolSize(), 
                  0L,  TimeUnit.SECONDS,new SynchronousQueue(),
                  (new ThreadFactoryBuilder())
                          .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                          .setDaemon(true)
                          .build());
           //缓存刷新线程池
           this.cacheRefreshExecutor = new ThreadPoolExecutor(
                   1, this.clientConfig.getCacheRefreshExecutorThreadPoolSize(),
                   0L, TimeUnit.SECONDS, new SynchronousQueue(), 
                   (new ThreadFactoryBuilder())
                           .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                           .setDaemon(true).build());

             
           ...
           //完成服务发现(全量拉取)
           if (this.clientConfig.shouldFetchRegistry() && !this.fetchRegistry(false)) {
                //如果拉取失败,从备份服务器拉取
                this.fetchRegistryFromBackup();
            }
          // 判断是否需要强制注册,默认false
          if (this.clientConfig.shouldRegisterWithEureka() 
              && this.clientConfig.shouldEnforceRegistrationAtInit()) {
                try {
                    //2-1 注册接口
                    if (!this.register()) {
                        throw new IllegalStateException("...");
                    }
                } catch (Throwable var9) {
                    logger.error("Registration error at startup: {}", var9.getMessage());
                    throw new IllegalStateException(var9);
                }
            }
        //2-2 初始化调度任务
        this.initScheduledTasks();}
    private void initScheduledTasks() {
        //判断是否开启服务发现功能,默认true
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            //定时拉取服务注册列表
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()   //该线程执行更新的具体逻辑
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }
		//判断是否开启服务注册功能
        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
 
            //心跳续约定时任务
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()  //该线程执行续约的具体逻辑
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // 服务注册线程
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

          
			// 2-3 开始服务注册
            instanceInfoReplicator
                .start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

2.主线分析

服务注册

入口代码中的2-1,以及2-3调用了相同的注册接口

底层实现其实就是组装URL,发送了一个http请求到Server端 ApplicationResource#addInstance方法

public EurekaHttpResponse<Void> register(InstanceInfo info) {
    //请求服务端接口的url地址
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;

    EurekaHttpResponse var5;
    try {
        Builder resourceBuilder = this.jerseyClient
            .resource(this.serviceUrl).path(urlPath).getRequestBuilder();
        this.addExtraHeaders(resourceBuilder);
        //发送请求
        response = (ClientResponse)
            ((Builder)((Builder)((Builder)resourceBuilder.header("Accept-Encoding", "gzip"))
                       .type(MediaType.APPLICATION_JSON_TYPE))
                       .accept(new String[]{"application/json"}))
                       .post(ClientResponse.class, info);
        var5 = EurekaHttpResponse
            .anEurekaHttpResponse(response.getStatus())
            .headers(headersOf(response))
            .build();
    } finally {

        if (response != null) {
            response.close();
        }

    }

    return var5;
}
服务发现

从服务发现定时任务的线程进入

/**
*    forceFullRegistryFetch  是否全量拉取   初始化时全量拉取,定时更新增量拉取
*/  
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = this.FETCH_REGISTRY_TIMER.start();

        try {
            boolean var4;
            try {
                //获取本地缓存
                Applications applications = this.getApplications();
                if (!this.clientConfig.shouldDisableDelta() && Strings.isNullOrEmpty(
                    this.clientConfig.getRegistryRefreshSingleVipAddress()) && 
                    !forceFullRegistryFetch && 
                    //本地缓存不为空
                    applications != null &&
                    applications.getRegisteredApplications().size() != 0 && 
                    applications.getVersion() != -1L) {
                    //增量拉取
                    this.getAndUpdateDelta(applications);
                    
                } else {
         			//全量拉取
                    this.getAndStoreFullRegistry();
                }

                applications.setAppsHashCode(applications.getReconcileHashCode());
                this.logTotalInstances();
                break label122;
            } catch (Throwable var8) {
                
                var4 = false;
            } finally {
                if (tracer != null) {
                    tracer.stop();
                }

            }

            return var4;
        }

        this.onCacheRefreshed();
        this.updateInstanceRemoteStatus();
        return true;
    }

全量拉取:发送了一个http请求到Server端 ApplicationsResource#getContainers方法

增量拉取:发送了一个http请求到Server端 ApplicationsResource#getContainerDifferential方法

    private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();
		//发送增量拉取的请求
        Applications delta = null;
        EurekaHttpResponse<Applications> httpResponse = 
					eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            delta = httpResponse.getEntity();
        }
		
        if (delta == null) {
            //如果没有拉取到服务信息,全量拉取
            getAndStoreFullRegistry();
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, 
														currentUpdateGeneration + 1)) {
            String reconcileHashCode = "";
            if (fetchRegistryUpdateLock.tryLock()) {
                try {
                    //把拉取到的服务信息,合并到本地缓存
                    updateDelta(delta);
                    reconcileHashCode = getReconcileHashCode(applications);
                } finally {
                    fetchRegistryUpdateLock.unlock();
                }
            } else {
                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
            }

          ....
        } else {
          ....  
        }
    }
  private void updateDelta(Applications delta) {
        int deltaCount = 0;
        Iterator var3 = delta.getRegisteredApplications().iterator();

        while(var3.hasNext()) {
            Application app = (Application)var3.next();
            Iterator var5 = app.getInstances().iterator();

            while(var5.hasNext()) {
                InstanceInfo instance = (InstanceInfo)var5.next();
                Applications applications = this.getApplications();
                String instanceRegion = this.instanceRegionChecker.getInstanceRegion(instance);
                if (!this.instanceRegionChecker.isLocalRegion(instanceRegion)) {
                    Applications remoteApps
                        = (Applications)this.remoteRegionVsApps.get(instanceRegion);
                    if (null == remoteApps) {
                        remoteApps = new Applications();
                        this.remoteRegionVsApps.put(instanceRegion, remoteApps);
                    }

                    applications = remoteApps;
                }

                ++deltaCount;
                Application existingApp;
                //新增操作(服务注册)
                if (ActionType.ADDED.equals(instance.getActionType())) {
                    existingApp = applications.getRegisteredApplications(instance.getAppName());
                    if (existingApp == null) {
                        applications.addApplication(app);
                    }
                  applications.getRegisteredApplications(instance.getAppName())
              													.addInstance(instance);
                //修改操作(心跳续约)   
                } else if (ActionType.MODIFIED.equals(instance.getActionType())) {
                    existingApp = applications.getRegisteredApplications(instance.getAppName());
                    if (existingApp == null) {
                        applications.addApplication(app);
                    }
                    applications.getRegisteredApplications(instance.getAppName())
                        .addInstance(instance);
                //删除操作(服务下架)    
                } else if (ActionType.DELETED.equals(instance.getActionType())) {
                    existingApp = applications.getRegisteredApplications(instance.getAppName());
                    if (existingApp != null) {
                        logger.debug("Deleted instance {} to the existing apps ", instance.getId());
                        existingApp.removeInstance(instance);
                        if (existingApp.getInstancesAsIsFromEureka().isEmpty()) {
                            applications.removeApplication(existingApp);
                        }
                    }
                }
            }
        }

      	
        this.getApplications().setVersion(delta.getVersion());
        this.getApplications().shuffleInstances(this.clientConfig.shouldFilterOnlyUpInstances());
        var3 = this.remoteRegionVsApps.values().iterator();

        while(var3.hasNext()) {
            Applications applications = (Applications)var3.next();
            applications.setVersion(delta.getVersion());
            applications.shuffleInstances(this.clientConfig.shouldFilterOnlyUpInstances());
        }

    }
心跳续约

从心跳续约定时任务的线程HeartbeatThread进入

boolean renew() {
    try {
        //发送心跳续约请求
        EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient
            .sendHeartBeat(
            this.instanceInfo.getAppName(), 
            this.instanceInfo.getId(), 
            this.instanceInfo, 
            (InstanceStatus)null
        );
		//心跳续约失败
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            this.REREGISTER_COUNTER.increment();
            long timestamp = this.instanceInfo.setIsDirtyWithTime();
            //重新注册
            boolean success = this.register();
            if (success) {
                this.instanceInfo.unsetIsDirty(timestamp);
            }

            return success;
        } else {
            return httpResponse.getStatusCode() == Status.OK.getStatusCode();
        }
    } catch (Throwable var5) {

        return false;
    }
}
boolean renew() {
    try {
        //发送心跳续约请求
        EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient
            .sendHeartBeat(
            this.instanceInfo.getAppName(), 
            this.instanceInfo.getId(), 
            this.instanceInfo, 
            (InstanceStatus)null
        );
		//心跳续约失败
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            this.REREGISTER_COUNTER.increment();
            long timestamp = this.instanceInfo.setIsDirtyWithTime();
            //重新注册
            boolean success = this.register();
            if (success) {
                this.instanceInfo.unsetIsDirty(timestamp);
            }

            return success;
        } else {
            return httpResponse.getStatusCode() == Status.OK.getStatusCode();
        }
    } catch (Throwable var5) {

        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值