源码篇--Nacos服务--中章(4):Nacos服务端启动--事件发布&订阅


前言

本文对Nacos 中事件的发布和订阅进行介绍。


一、Nacos 事件发布和订阅:

在 Nacos 中,事件的发布和订阅是通过监听器和回调函数实现的。Nacos 提供了一套事件监听机制,通过注册监听器,可以监听到特定事件的发生,并在事件发生时执行相应的回调函数。具体步骤如下:

  1. 创建事件监听器:在应用程序中创建一个事件监听器,实现对特定事件的监听逻辑。可以通过 Nacos 提供的 SDK 或 API 注册监听器。

  2. 注册监听器:在 Nacos 中,将事件监听器注册到相应的服务实例或配置实例上。注册监听器时,可以指定监听的事件类型和对应的回调函数。

  3. 发布事件:当特定事件发生时(比如配置变更、服务状态变更等),Nacos 会触发相应的监听器,并执行注册的回调函数。

  4. 触发回调:注册的回调函数会被执行,进行相应的处理逻辑。可以在回调函数中编写代码来处理事件,并实现特定的业务逻辑。

  5. 取消监听:在不需要监听特定事件时,可以取消相应的监听器,避免不必要的资源消耗。

通过事件的发布和订阅机制,Nacos 实现了服务实例和配置实例的实时通知和数据同步。例如,当服务实例状态发生变化时,客户端可以立即收到通知并做出相应的处理;当配置内容发生变化时,客户端也可以即时更新配置信息。这样可以保证系统的实时性和一致性,提高系统的可靠性和性能。

总的来说,Nacos 中的事件发布和订阅机制通过事件监听器和回调函数实现,可以使得系统各个组件之间实时通信和数据同步,保证系统的稳定性和可靠性。

Nacos 事件发布和订阅 主要通过 NotifyCenter 进行实现;

二、事件发布:

2.1 默认事件发布工厂构建:

static {
   // Internal ArrayBlockingQueue buffer size. For applications with high write throughput,
    // this value needs to be increased appropriately. default value is 16384
    // ringBufferSize  缓冲区大小获取
    String ringBufferSizeProperty = "nacos.core.notify.ring-buffer-size";
    ringBufferSize = Integer.getInteger(ringBufferSizeProperty, 16384);
    
    // The size of the public publisher's message staging queue buffer
    // shareBufferSize 缓冲区大小获取
    String shareBufferSizeProperty = "nacos.core.notify.share-buffer-size";
    shareBufferSize = Integer.getInteger(shareBufferSizeProperty, 1024);
    
    final Collection<EventPublisher> publishers = NacosServiceLoader.load(EventPublisher.class);
    Iterator<EventPublisher> iterator = publishers.iterator();
    
    if (iterator.hasNext()) {
        clazz = iterator.next().getClass();
    } else {
    	 // 初始化方法时 clazz 取默认的事件发布器
        clazz = DefaultPublisher.class;
    }
    // 默认的事件发布工厂 赋值 当调用apply  方法时 会调用到改实现的方法,创建出一个默认的事件监听器
    DEFAULT_PUBLISHER_FACTORY = (cls, buffer) -> {
        try {
        	// 创建 DefaultPublisher 对象
            EventPublisher publisher = clazz.newInstance();
            // 执行事件发布器的 初始化方法 cls 事件类型,buffer 缓冲区大小
            publisher.init(cls, buffer);
            return publisher;
        } catch (Throwable ex) {
            LOGGER.error("Service class newInstance has error : ", ex);
            throw new NacosRuntimeException(SERVER_ERROR, ex);
        }
    };
    
    try {
        
        // Create and init DefaultSharePublisher instance.
        // 创建共享的事件发布器
        INSTANCE.sharePublisher = new DefaultSharePublisher();
        // 共享事件发布器器初始化
        INSTANCE.sharePublisher.init(SlowEvent.class, shareBufferSize);
        
    } catch (Throwable ex) {
        LOGGER.error("Service class newInstance has error : ", ex);
    }
    
    ThreadUtils.addShutdownHook(NotifyCenter::shutdown);
}

2.2 事件的注册:

以 集群成员变更的事件 为例:

 // 注册事件 事件类型 ,队列大小
  NotifyCenter.registerToPublisher(MembersChangeEvent.class,
          EnvUtil.getProperty(MEMBER_CHANGE_EVENT_QUEUE_SIZE_PROPERTY, Integer.class,
                  DEFAULT_MEMBER_CHANGE_EVENT_QUEUE_SIZE));

(1)事件通知中心NotifyCenter 注册事件:

/**
* eventType 事件类型
* queueMaxSize  事件发布器的队列大小
**/
public static EventPublisher registerToPublisher(final Class<? extends Event> eventType, final int queueMaxSize) {
	// 注册事件发布器
    return registerToPublisher(eventType, DEFAULT_PUBLISHER_FACTORY, queueMaxSize);
}

(2)注册事件发布器 registerToPublisher:

public static EventPublisher registerToPublisher(final Class<? extends Event> eventType,
        final EventPublisherFactory factory, final int queueMaxSize) {
      // 是否慢事件
    if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
    	// 慢事件 返回共享事件发布器
        return INSTANCE.sharePublisher;
    }
    //  获取事件类名 包名+类名
    final String topic = ClassUtils.getCanonicalName(eventType);
    // 获取类锁;
    synchronized (NotifyCenter.class) {
        // MapUtils.computeIfAbsent is a unsafe method.
        // 根据事件类型,获取对应的事件发布器;如果没有事件发布器,就通过事件工厂创建出来一个对应的事件发布器;
         // 缓存事件类型对应的事件发布器;
        MapUtil.computeIfAbsent(INSTANCE.publisherMap, topic, factory, eventType, queueMaxSize);
    }
 	// 从缓存中获取到对应的事件发布器
    return INSTANCE.publisherMap.get(topic);
}

(3)事件工厂事件发布器的构建:

public static <K, C, V, T> V computeIfAbsent(Map<K, V> target, K key, BiFunction<C, T, V> mappingFunction, C param1,
         T param2) {
     // 属性值判断
     Objects.requireNonNull(target, "target");
     Objects.requireNonNull(key, "key");
     Objects.requireNonNull(mappingFunction, "mappingFunction");
     Objects.requireNonNull(param1, "param1");
     Objects.requireNonNull(param2, "param2");
     // 调用 DEFAULT_PUBLISHER_FACTORY 的apply 方法
     return target.computeIfAbsent(key, (keyInner) -> mappingFunction.apply(param1, param2));
     
 }

事件发布器构建:

DEFAULT_PUBLISHER_FACTORY = (cls, buffer) -> {
    try {
       	// 创建 DefaultPublisher 对象
        EventPublisher publisher = clazz.newInstance();
        // 执行事件发布器的 初始化方法 cls 事件类型,buffer 缓冲区大小
        publisher.init(cls, buffer);
        return publisher;
    } catch (Throwable ex) {
        LOGGER.error("Service class newInstance has error : ", ex);
        throw new NacosRuntimeException(SERVER_ERROR, ex);
    }
};

(4)事件发布初始化publisher.init:
默认的事件发布器 DefaultPublisher 和共享的事件发布器 DefaultSharePublisher 调用init 方法都会进入到 DefaultPublisher # init

@Override
public void init(Class<? extends Event> type, int bufferSize) {
	// 守护线程标识
     setDaemon(true);
     // 设置事件发布器的名称  "nacos.publisher-" + 事件的类型 
     setName("nacos.publisher-" + type.getName());
     // 赋值事件的 类型
     this.eventType = type;
     this.queueMaxSize = bufferSize;
     if (this.queueMaxSize == -1) {
         this.queueMaxSize = ringBufferSize;
     }
     // 设置事件发布器中队列 的大小
     this.queue = new ArrayBlockingQueue<>(this.queueMaxSize);
     //  DefaultPublisher extends Thread 开启一个新线程 执行DefaultPublisher 的run 方法 
     start();
 }

(5)事件发布器对于事件的接收(事件的消费):
start(); 方法开启线程 消费对应事件类型 队列中的事件;

 @Override
public void run() {
    openEventHandler();
}

void openEventHandler() {
    try {
        
        // This variable is defined to resolve the problem which message overstock in the queue.
        int waitTimes = 60;
        // To ensure that messages are not lost, enable EventHandler when
        // waiting for the first Subscriber to register
        // 如果改事件类型对应的没有订阅者,则最多循环60次;
        while (!shutdown && !hasSubscriber() && waitTimes > 0) {
            ThreadUtils.sleep(1000L);
            waitTimes--;
        }
			
        while (!shutdown) {
        	// BlockingQueue<Event> queue
        	// 从事件发布器的队列中获取 事件
            final Event event = queue.take();
            receiveEvent(event);
            UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
        }
    } catch (Throwable ex) {
        LOGGER.error("Event listener exception : ", ex);
    }
}

2.3 发布事件NotifyCenter.publishEvent:

public static boolean publishEvent(final Event event) {
 try {
 		// 发布事件
        return publishEvent(event.getClass(), event);
    } catch (Throwable ex) {
        LOGGER.error("There was an exception to the message publishing : ", ex);
        return false;
    }
}

2.3.1 发布事件:

private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
     if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
     	// 如果是慢事件 则使用共享事件发布器 发布事件
         return INSTANCE.sharePublisher.publish(event);
     }
     // 获取topic 包名 +类名
     final String topic = ClassUtils.getCanonicalName(eventType);
     // 根据事件类型 获取到对应的事件处理器
     EventPublisher publisher = INSTANCE.publisherMap.get(topic);
     if (publisher != null) {
          // 使用默认的事件处理器发布事件 
         return publisher.publish(event);
     }
     // 事件发布成功返回状态标识
     if (event.isPluginEvent()) {
         return true;
     }
     LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
     return false;
 }

默认事件发布器DefaultPublisher/ DefaultSharePublisher 发布事件:
默认事件发布器DefaultPublisher/ DefaultSharePublisher 最终都调用 DefaultPublisher # publish 发布事件

@Override
public boolean publish(Event event) {
	// 事件处理器状态检查
    checkIsStart();
    // 向事件发布器的队列 放入改事件
    boolean success = this.queue.offer(event);
    if (!success) {
    	// 放入不成功 直接将改事件发送给对应的订阅者
        LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
        receiveEvent(event);
        return true;
    }
    return true;
}

三、事件订阅:

3.1 以 注册本机ip 变更事件为例:

NotifyCenter.registerSubscriber(new Subscriber<InetUtils.IPChangeEvent>() {
	// ip 事件变更后的回调方法
   @Override
   public void onEvent(InetUtils.IPChangeEvent event) {
       String newAddress = event.getNewIP() + ":" + port;
       ServerMemberManager.this.localAddress = newAddress;
       EnvUtil.setLocalAddress(localAddress);
       
       Member self = ServerMemberManager.this.self;
       self.setIp(event.getNewIP());
       
       String oldAddress = event.getOldIP() + ":" + port;
       ServerMemberManager.this.serverList.remove(oldAddress);
       ServerMemberManager.this.serverList.put(newAddress, self);
       
       ServerMemberManager.this.memberAddressInfos.remove(oldAddress);
       ServerMemberManager.this.memberAddressInfos.add(newAddress);
   }

   @Override
   public Class<? extends Event> subscribeType() {
   		// 定义事件的类型  IPChangeEvent extends SlowEvent
       return InetUtils.IPChangeEvent.class;
   }
});

3.2 事件注册步骤1:

public static void registerSubscriber(final Subscriber consumer) {
	// consumer 事件回调方法,DEFAULT_PUBLISHER_FACTORY 使用默认的事件工厂
    registerSubscriber(consumer, DEFAULT_PUBLISHER_FACTORY);
}

3.3 事件注册步骤2:

public static void registerSubscriber(final Subscriber consumer, final EventPublisherFactory factory) {
    // If you want to listen to multiple events, you do it separately,
    // based on subclass's subscribeTypes method return list, it can register to publisher.
    // 是否是灵活的订阅者(可以订阅多个事件),普通订阅者只能订阅一种特定的事件
    if (consumer instanceof SmartSubscriber) {
    	// 遍历所有的订阅者 判断订阅者订阅的事件类型 ,根据是否慢事件 进行不同处理
        for (Class<? extends Event> subscribeType : ((SmartSubscriber) consumer).subscribeTypes()) {
            // For case, producer: defaultSharePublisher -> consumer: smartSubscriber.
            if (ClassUtils.isAssignableFrom(SlowEvent.class, subscribeType)) {
            	// 订阅者订阅的 是慢事件,则注册到 共享事件发布器
                INSTANCE.sharePublisher.addSubscriber(consumer, subscribeType);
            } else {
                // For case, producer: defaultPublisher -> consumer: subscriber.
                // 其他事件 ,注册到默认事件发布器
                addSubscriber(consumer, subscribeType, factory);
            }
        }
        return;
    }
    // 不是灵活的订阅者 处理
    // 获取订阅的事件类型
    final Class<? extends Event> subscribeType = consumer.subscribeType();
    if (ClassUtils.isAssignableFrom(SlowEvent.class, subscribeType)) {
   		 // 订阅者订阅的 是慢事件,则注册到 共享事件发布器
        INSTANCE.sharePublisher.addSubscriber(consumer, subscribeType);
        return;
    }
      // 其他事件 ,注册到默认事件发布器
    addSubscriber(consumer, subscribeType, factory);
}

3.4 注册到默认事件发布器:

private static void addSubscriber(final Subscriber consumer, Class<? extends Event> subscribeType,
            EventPublisherFactory factory) {
    // topic 获取,包名+类名    
   final String topic = ClassUtils.getCanonicalName(subscribeType);
   // 类锁获取
    synchronized (NotifyCenter.class) {
        // MapUtils.computeIfAbsent is a unsafe method.
        // 将 对应的事件 及其 消费者放入到  Map<String, EventPublisher> publisherMap = new ConcurrentHashMap<>(16)
        // key : 包名+类名的topic  ,value :改事件对应的事件发布器 ,subscribeType 为事件类型,ringBufferSize 事件处理器队列大小
        // 如果publisherMap  没有改subscribeType 类型的事件发布器, 这里会调用 DEFAULT_PUBLISHER_FACTORY 的apply 方法 
        // 生成一个默认的事件处理器
        MapUtil.computeIfAbsent(INSTANCE.publisherMap, topic, factory, subscribeType, ringBufferSize);
    }
    // 获取事件发布器
    EventPublisher publisher = INSTANCE.publisherMap.get(topic);
    if (publisher instanceof ShardedEventPublisher) {
    	// 如果是共享事件发布器,则调用ShardedEventPublisher 的 addSubscriber 方法
        ((ShardedEventPublisher) publisher).addSubscriber(consumer, subscribeType);
    } else {
    	// 调用对应事件发布器的注册方法
        publisher.addSubscriber(consumer);
    }
}

事件定义者订阅 publisher.addSubscriber(consumer):

protected final ConcurrentHashSet<Subscriber> subscribers = new ConcurrentHashSet<>();
@Override
public void addSubscriber(Subscriber subscriber) {
	// 将订阅者(消费者) 放入对应事件类型处理器的 订阅者set 集合中
    subscribers.add(subscriber);
}

3.5 注册到共享事件发布器:

DefaultSharePublisher # addSubscriber:

private final Lock lock = new ReentrantLock();
@Override
public void addSubscriber(Subscriber subscriber, Class<? extends Event> subscribeType) {
   // Actually, do a classification based on the slowEvent type.
   // 对应的事件类型
   Class<? extends SlowEvent> subSlowEventType = (Class<? extends SlowEvent>) subscribeType;
   // For stop waiting subscriber, see {@link DefaultPublisher#openEventHandler}.
   // DefaultPublisher 类中   protected final ConcurrentHashSet<Subscriber> subscribers = new ConcurrentHashSet<>();
   // 将订阅者也存入一份到DefaultPublisher  的set 结合中
   subscribers.add(subscriber);
   // 获取锁
   lock.lock();
   try {
   	// 获取改事件类型的 订阅者
       Set<Subscriber> sets = subMappings.get(subSlowEventType);
       if (sets == null) {
       		// 如果集合为空,说明改事件是被消费者第一次订阅
           Set<Subscriber> newSet = new ConcurrentHashSet<>();
           // set 集合添加 订阅者
           newSet.add(subscriber);
           // 放回 事件 以及对应的事件订阅者
           subMappings.put(subSlowEventType, newSet);
           return;
       }
       sets.add(subscriber);
   } finally {
   		// 释放锁
       lock.unlock();
   }
}

3.6 默认事件发布器和共享事件发布器区别:

  • DefaultSharePublisher extends DefaultPublisher 共享事件发布器 继承了默认事件发布器 ,默认事件发布器有的共享事件发布器都有;
  • DefaultSharePublisher 的事件发布器,每种事件都会定义一个自己的DefaultSharePublisher 对象,然后对应的消费者,也只是消费者一种事件;
  • DefaultSharePublisher 的事件发布器,只要是SlowEvent 都会公用一个 DefaultSharePublisher 对象,然后对应的消费者和事件会被放入到 Map<Class<? extends SlowEvent>, Set> subMappings = new ConcurrentHashMap<>() map 集合中,其中key 是对应的慢事件 包名+类名,value 为对应改事件的消费者集合;
  • 共享的事件发布器,可以对多个类型事件进行发布,而默认的一个事件发布器只针对一类事件;
  • 共享的事件发布器缓冲区和队列大小是1024;
  • 默认的事件类型缓冲区和队列大小是,128

四、事件消费:

在NotifyCenter static 的静态代码块中,共享事件发布器,和默认的事件发布器,都会调用到 DefaultPublisher 的 init 方法:
在这里插入图片描述
在 init 方法初始化完成的最后 会调用 start 方法:

@Override
public void init(Class<? extends Event> type, int bufferSize) {
    setDaemon(true);
    setName("nacos.publisher-" + type.getName());
    this.eventType = type;
    this.queueMaxSize = bufferSize;
    if (this.queueMaxSize == -1) {
        this.queueMaxSize = ringBufferSize;
    }
    this.queue = new ArrayBlockingQueue<>(this.queueMaxSize);
    //DefaultPublisher extends Thread  这里会开启线程 消费队列中的事件
    start();
}

在线程的run 方法中会调用接收事件的 receiveEvent 方法:
在这里插入图片描述

这里的 receiveEvent 会有两个实现,一个对应不是 慢事件的 默认事件发布器,一个对应共享事件的 慢事件发布器

4.1 非慢事件消费(对应默认事件发布器):

DefaultPublisher # receiveEvent

void receiveEvent(Event event) {
  final long currentEventSequence = event.sequence();
   // 改事件没有订阅者 直接返回
   if (!hasSubscriber()) {
       LOGGER.warn("[NotifyCenter] the {} is lost, because there is no subscriber.", event);
       return;
   }
   
   // Notification single event listener
   // 遍历事件的订阅者
   for (Subscriber subscriber : subscribers) {
       if (!subscriber.scopeMatches(event)) {
           continue;
       }
       
       // Whether to ignore expiration events
       // 如果是已经消费过的事件 则进行下一次循环
       if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
           LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
                   event.getClass());
           continue;
       }
       
       // Because unifying smartSubscriber and subscriber, so here need to think of compatibility.
       // Remove original judge part of codes.
       // 将事件推送给对应的消费者
       notifySubscriber(subscriber, event);
   }
}

DefaultPublisher # notifySubscriber

@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {
   
   LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);
   // 定义一个线程去处理 事件的回调onEvent 方法
   final Runnable job = () -> subscriber.onEvent(event);
   // 改事件是否有执行器,有则放入到执行器执行,否则直接使用该线程执行
   final Executor executor = subscriber.executor();
   
   if (executor != null) {
       executor.execute(job);
   } else {
       try {
           job.run();
       } catch (Throwable e) {
           LOGGER.error("Event callback exception: ", e);
       }
   }
}

以ip 地址变更为例,最终回调 onEvent 方法,进行对应的业务处理

NotifyCenter.registerSubscriber(new Subscriber<InetUtils.IPChangeEvent>() {
  @Override
    public void onEvent(InetUtils.IPChangeEvent event) {
        String newAddress = event.getNewIP() + ":" + port;
        ServerMemberManager.this.localAddress = newAddress;
        EnvUtil.setLocalAddress(localAddress);
        
        Member self = ServerMemberManager.this.self;
        self.setIp(event.getNewIP());
        
        String oldAddress = event.getOldIP() + ":" + port;
        ServerMemberManager.this.serverList.remove(oldAddress);
        ServerMemberManager.this.serverList.put(newAddress, self);
        
        ServerMemberManager.this.memberAddressInfos.remove(oldAddress);
        ServerMemberManager.this.memberAddressInfos.add(newAddress);
    }

    @Override
    public Class<? extends Event> subscribeType() {
        return InetUtils.IPChangeEvent.class;
    }
});

4.1 慢事件消费(对应共享事件发布器):

DefaultSharePublisher # receiveEvent

@Override
public void receiveEvent(Event event) {
    // 事件的序列号
    final long currentEventSequence = event.sequence();
    // get subscriber set based on the slow EventType.
    final Class<? extends SlowEvent> slowEventType = (Class<? extends SlowEvent>) event.getClass();
    
    // Get for Map, the algorithm is O(1).
    // 根据类型获取慢事件的 订阅者
    Set<Subscriber> subscribers = subMappings.get(slowEventType);
    if (null == subscribers) {
        LOGGER.debug("[NotifyCenter] No subscribers for slow event {}", slowEventType.getName());
        return;
    }
    
    // Notification single event subscriber
    // 遍历订阅者 调用 notifySubscriber 消费事件
    for (Subscriber subscriber : subscribers) {
        // Whether to ignore expiration events
        if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
            LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
                    event.getClass());
            continue;
        }
        
        // Notify single subscriber for slow event.
        notifySubscriber(subscriber, event);
    }
}

DefaultPublisher # notifySubscriber

@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {
   
   LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);
   // 定义一个线程去处理 事件的回调onEvent 方法
   final Runnable job = () -> subscriber.onEvent(event);
   // 改事件是否有执行器,有则放入到执行器执行,否则直接使用该线程执行
   final Executor executor = subscriber.executor();
   
   if (executor != null) {
       executor.execute(job);
   } else {
       try {
           job.run();
       } catch (Throwable e) {
           LOGGER.error("Event callback exception: ", e);
       }
   }
}

总结

Nacos 事件发布&订阅 本身就是一个生产者和消费者的模型,Nacos 将事件分成了两类,常规事件 和 慢事件,常规事件:每种事件类型都对应自己的一个 默认事件发布器对象 队列大小为128,通过set 集合存放消费改事件的消费者 ; 慢事件:都使用共享事件发布器, 队列大小为1024,使用map 集合存放消费改事件的消费者 key: 包名+类名的 topIc, value 为set 集合存放消费改事件的消费者 ;

生产者通过NotifyCenter # publishEvent 方法将事件 放入到事件发布器的ArrayBlockingQueue 队列中;

消费者通过NotifyCenter # registerSubscriber 将消费者放入到 事件发布器的 集合中(慢事件为 Map<Class<? extends SlowEvent>, Set> subMapping ; 非慢事件为 ConcurrentHashSet subscribers )

通过事件发布器中的线程中while 循环 进行 事件的获取,最终回调 onEvent 方法完成事件的消费;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值