9.jobManager基础知识

JobManager基础知识

一、Flink 架构核心组件

JobManager(作业管理器)

  • 负责协调 Flink 作业的整个生命周期:
    • 接收用户提交的 JobGraph
    • 调度任务(生成 ExecutionGraph)
    • 分配 slot(资源)
    • 管理 checkpoint 与 failover
    • 与 TaskManager 通信并发送命令(如启动、取消任务等)

TaskManager(任务执行器)

  • 实际执行 JobManager 分发的子任务(Task)
  • 提供 slot 给 JobManager 使用
  • 运行 Task 线程,汇报运行状态、执行结果等

二、JobManager 中的关键技术栈

1. Akka 通信机制

Flink 早期版本主要使用 Akka actor 作为进程间通信机制:

2. BlobServer 和 Socket 通信

  • 大型 job(如含 jar 包或大型资源)通过 BlobServer 模块进行传输:

3. 定时任务和线程池

  • JobManager 中大量使用线程池(如 ScheduledExecutor)处理:
线程池种类:
  • ScheduledExecutorService
  • ExecutorService
  • MainThreadExecutor(保证事件在 JobManager 主线程中处理)

三、建议的知识预热

在阅读 JobManager 源码之前,建议掌握以下前置知识:

技术点说明
Akka Actor 模型actor 的创建、消息传递、生命周期等基础
Java NIO & Socket了解 ServerSocket, SocketChannel 用法
Java 线程池Executors, ScheduledThreadPoolExecutor 用法

四、Akka Actor模型

Akka 是一组用于设计 分布式、可扩展、高并发、弹性系统 的开源工具库,基于 Actor 模型 实现,主要用于 Java 和 Scala 语言。

Actor API

Actor API是低级别的api代码,通常用来初始化一系列actor动作。拥有自己的:

  • 状态(不可被外部线程修改)

  • 行为(通过 receive 定义)

import akka.actor.Terminated;
static class WatchActor extends AbstractActor {
  private final ActorRef child = getContext().actorOf(Props.empty(), "target");
  private ActorRef lastSender = system.deadLetters();

  public WatchActor() {
    getContext().watch(child); // <-- this is the only call needed for registration
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .matchEquals(
            "kill",
            s -> {
              getContext().stop(child);
              lastSender = getSender();
            })
        .match(
            Terminated.class,
            t -> t.actor().equals(child),
            t -> {
              lastSender.tell("finished", getSelf());
            })
        .build();
  }
}

通过代码可以看到该接口最主要的方法是 createReceive。定义了如何处理接收的信息。

ActorSystem

ActorSystem则是一个低级别的容器,是系统中所有 Actor 的“运行时环境”。

ActorSystem system = ActorSystem.create("testSystem");
ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myActor");

ActorRef

ActorRefActor 的引用(地址),代表一个可以发送消息的目标。

配置 Akka 支持远程通信
akka {
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
  }

  remote {
    enabled-transports = ["akka.remote.netty.tcp"]
    netty.tcp.hostname = "127.0.0.1"
    netty.tcp.port = 2552
  }
}
启动远程 ActorSystem(服务端)
ActorSystem system = ActorSystem.create("RemoteSystem", config);
ActorRef serverActor = system.actorOf(Props.create(ServerActor.class), "serverActor");

serverActor 的地址将是:akka.tcp://RemoteSystem@127.0.0.1:2552/user/serverActor

客户端连接并发送消息
ActorSystem clientSystem = ActorSystem.create("ClientSystem", clientConfig);

// 远程 actor 的路径
String path = "akka.tcp://RemoteSystem@127.0.0.1:2552/user/serverActor";

// 获取远程 actor 的引用(异步查找)
ActorSelection selection = clientSystem.actorSelection(path);

// 发送消息
selection.tell("hello remote actor", ActorRef.noSender());
ActorSelection

ActorSelection 是 Akka 提供的一种 “路径级引用”,允许你通过字符串路径(像 URL 一样)来定位一个或多个 actor,即使你还没拿到它的 ActorRef

Props

Props 是 Akka 中用来 描述如何创建一个 Actor 实例 的配置对象。

// 定义 Actor 类
public class MyActor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(String.class, msg -> System.out.println("Got: " + msg))
            .build();
    }
}

// 创建 Props
Props props = Props.create(MyActor.class);

// 创建 ActorRef(实际部署该 actor)
ActorRef ref = system.actorOf(props, "myActor");

总结

Akka 框架通过 ActorSystem 初始化一个运行时环境,作为所有 actor 的容器和调度中心。在该环境中通过props创建各类 actor,并通过 ActorRef(精确引用)或 ActorSelection(路径匹配引用)与本地或远程的其他 ActorSystem 中的 actor 进行异步通信和交互。

关键词含义
ActorSystemAkka 的运行时容器,负责管理 actor 的生命周期和通信调度
Actor独立的并发单元,封装状态和行为,通过消息驱动运行
ActorRef指向具体 actor 的引用,用于精确发送消息
ActorSelection通过路径匹配方式选中 actor,可延迟绑定
远程通信Akka 支持跨网络进程间 actor 通信,构建分布式系统

Actor 生命周期

在这里插入图片描述

这张图很好的讲解了acotr的生命周期。

方法作用何时触发
preStart()初始化资源,比如启动子 actor、定时器等Actor 被创建时
receive()正常处理消息的地方Actor 运行中
postStop()清理资源,比如关闭连接、取消定时器等Actor 被正常或异常停止时
preRestart(Throwable reason, Option<Object> msg)在重启前调用,可以清理状态Actor 出现异常,准备重启
postRestart(Throwable reason)重启后调用(默认重新调用 preStart()Actor 重启时

五、Socket通信

Socket

  1. Socket–套接字,负责启动该程序内部和外部之间的通信

  2. java.net.Socket
    Socket(String host,int port) //构建一个套接字,用来连接给定的主机和端口
    InputStream getInputStream() 
    OutPutStream getOutputStream() //获取可以从套接字中读取数据的流,以及可以向套接字写出数据的流
    
  3. 套接字超时

    java.net.Socket
    Socker() //创建一个还未被连接的套接字
    void connect(SocketAddress address) //将该套接字连接到给定的地址
    void connect(SocketAddress address,int timeoutInMilliseconds) //将套接字连接到给定的地址。如果在给定的时间内没有响应,则返回
    void setSoTimeout(int timeoutInMilliseconds) //设置该套接字上读请求的阻塞时间
    boolean isConnected() //如果该套接字已被连接,则返回true
    boolean isClosed() //如果套接字已经被关闭,则返回true
    

InetAddress

  1. 一个主机地址由4个字节组成(在IPv6中是16个字节)

  2. java.net.InetAddress
    static InetAddress getByName(String host)
    static InetAddress getAllByName(String host) //为给定的主机名创建一个InetAddress对象,或者一个包含了该主机名所对应的所有因特网地址的数组
    static InetAddress getLocalHost() //为本地主机创建一个InetAddress对象
    byte[] getAddress() //返回一个包含数字型地址的字节数组
    String getHostAddress() //返回一个由十进制数组成的字符串
    String getHostName() //返回主机名
    

ServerSocket

  1. 服务端Socket,一旦启动了服务器程序,便会等待某个客户端连接到它的端口。

  2. java.net.ServerSocket
    ServerSocker(int port) //创建一个监听端口的服务器套接字
    Socket accept() //等待连接
    void close() //关闭服务器套接字
    
  3. 半关闭:套接字连接的一端可以终止其输出,同时仍旧可以接受来自另一端的数据

  4. java.net.Scoket
    void shutdownOutput() //将输出流设为 流结束
    void shutdownInput() //将输入流设为 流结束
    boolean isOutputShutdown() //如果输出已被关闭,则返回true
    boolean isInputShutdown() //如果输入已被关闭,则返回true
    
  5. 可中断套接字:为了中断套接字操作,读取或写出数据时,线程不在阻塞

  6. java.net.InetSocketAddress
    InetSocketAddress(String hostname,int port) //用给定的主机和端口参数创建一个地址对象
    boolean isUnresolved() //如果不能解析该地址对象,则返回true
    
    java.nio.channels.SocketChannel
    static SocketChannel open(SocketAddress address) //打开一个套接字通道,并将其连接到远程地址
    
    java.nio.channels.Channels
    static InputStream newInputStream(ReadableByteChannel channel) //创建一个输入流,用以从指定的通道读取数据
    static OutputStream newOutputStream(WritableByteChannel channl) //创建一个输出流,用以向指定的通道写入数据
    

六、任务和线程池

1.Callable接口

  • 类似于Runnable,但有返回值,且可以抛异常。
java.util.concurrent.Callable<V> {
    V call() throws Exception; // 执行任务并返回结果
}

2.Future接口

用来保存异步计算结果,可以查询任务状态、获取结果、取消任务等。

   java.util.concurrent.Future<V> {
       V get() throws InterruptedException, ExecutionException;
       V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
       boolean cancel(boolean mayInterruptIfRunning);
       boolean isCancelled();
       boolean isDone();
   }

get()方法会阻塞,直到任务完成或超时。

3.FutureTask

  • 同时实现了FutureRunnable接口。
  • 可以作为Runnable提交给线程池执行,同时又能获取执行结果。
public class FutureTask<V> implements RunnableFuture<V> {
    public FutureTask(Callable<V> callable);
    public FutureTask(Runnable runnable, V result);
}

4.线程池的创建 — Executors工厂方法

方法描述
newCachedThreadPool必要时创建新线程,空闲线程会保留60秒
newFiexdThreadPool池中包含固定数目的线程;空闲线程会一直保留
newWorkStealingPool一种适合"fork-join"任务的线程池
newSingleThreadExecutor只有一个线程的"池",会顺序地执行所提交的任务
newScheduledThreadPool用于调度执行的固定线程池
newSingleThreadScheduledExecutor用于调度执行的单线程"池"

5. 使用线程池的一般流程

  1. 使用 Executors 工厂方法创建线程池,如:
ExecutorService executor = Executors.newFixedThreadPool(4);
  1. 提交任务(RunnableCallable):
Future<String> future = executor.submit(() -> {
    // 任务逻辑
    return "结果";
});
  1. 使用 Future 获取结果或取消任务:
String result = future.get();  // 阻塞等待结果
boolean cancelled = future.cancel(true); // 尝试取消
  1. 线程池用完后关闭:
executor.shutdown();  // 关闭线程池,不再接受新任务
  1. 补充说明
  • 线程池的优势:避免频繁创建销毁线程,提升系统性能和资源利用。
  • 取消任务cancel(true) 会中断正在执行的线程(如果任务支持响应中断)。
  • 定时任务一般用 ScheduledExecutorService,支持定时和周期执行。

七.JAVA的反射

反射的定义和作用
  1. 反射库提供了一个丰富且精巧的工具集,用来编写能够动态操作Java代码的程序
  2. 能够分析类能力的程序称为反射
  3. 作用:
    1. 运行时分析类的能力
    2. 运行时检查对象,例如,编写一个适用于所有类的toString方法
    3. 实现泛型数组操作代码。
    4. 利用Method对象
Class类
  1. 程序运行时,Java为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类。
  2. 虚拟机为每个类型管理一个唯一的Class对象。因此,可以利用==运算符实现两个类对象的比较
if (e.getClass()  == Employee.class)
  1. 三种方法获得Class对象

    //第一种
    Employee e;
    Class cl = e.getClass();
    
    //第二种
    var generator = new Random();
    Class cl = generator.getClass();
    String name = cl.getName();
    
    String className = "java.util.Random";
    Class cl = Class.forName(className);
    
    //第三种 class关键字
    Class cl1 = Random.class
    
  2. 调用getConstructor方法生成一个Constructor类型对象

String className = "java.util.Random";
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();
  1. 常用函数
java.lang.Class
static Class forName(String className) //返回一个Class对象,表示名为className的类

Constructor getConstructor(Class...paramterTypes) //生成一个对象,描述有指定参数类型的构造器
    
java.lang.reflect.Constructor
Object newInstance(Object...params) //将params传递到构造器,来构造这个构造器声明类的一个新实例
  1. 加载资源
java.lang.Class
    URL getResource(String name) 
    InputStream getResourceAsStream(String name)
    //找到与类同一位置的资源,返回一个可以用来加载资源的URL或者输入流。如果没有找到资源,则返回null,所以不会抛出异常或者I/O错误。
利用反射分析类的能力
  1. java.lang.reflect包中有三个类Field,Method和Constructorf分别用于描述类的字段、方法和构造器。
  2. getName方法,返回字段、方法或构造器的名称
  3. 代码示例
package reflection;

import java.lang.reflect.*;
import java.security.PublicKey;
import java.util.Scanner;

public class ReflectionTest {

    public static void main(String[] args) throws ClassNotFoundException {
        String name;
        if (args.length > 0) {
            name = args[0];
        } else {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter class name (e.g. java.util.Date)");
            name = in.next();
        }

        Class<?> cl = Class.forName(name);
        Class<?> superclass = cl.getSuperclass();
        String modifiers = Modifier.toString(cl.getModifiers());
        if (modifiers.length() > 0) System.out.println(modifiers + " ");
        System.out.println("class " + name);
        if (superclass != null && superclass != Object.class) System.out.println(" extends " + superclass.getName());

        System.out.print("\n{\n");
        printConstructors(cl);
        System.out.println();
        printMethods(cl);
        System.out.println();
        printFields(cl);
        System.out.println("}");


    }

    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Constructor c : constructors) {
            String name = c.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(c.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(name + "(");
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();

        for (Method m : methods) {
            Class<?> retType = m.getReturnType();
            String name = m.getName();

            System.out.print("  ");
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.print(retType.getName() + " " + name + "(");

            Class<?>[] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }


    }

    public static void printFields(Class cl) {
        Field[] fields = cl.getDeclaredFields();

        for (Field f : fields) {
            Class<?> type = f.getType();
            String name = f.getName();
            System.out.print("  ");
            String modifiers = Modifier.toString(f.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}

  1. 常用方法
java.lang.Class
Field[] getFields()
Field[] getDeclaredFields()
//getFields方法将返回一个包含Field对象的数组,这些对象对应这个类或其超类的公共字段。getDeclaredFields返回去这个类的全部字段
Method[] getMethods()
Method[] getDeclaredMethods()
//返回包含Method对象的数组,getMethods返回所有的公共方法,包括从超类继承的。getDeclaredMethods返回这个类或接口的全部方法,但不包括由超类继承的方法
Constructor[] getConstructors()
Constructor[] getDeclardConstructors()
//返回包含Constructor对象的数组,前者公共构造器,后者全部构造器
String getPackageName()
//返回这个类的包名,如果这个类是一个数组类型,返回元素类型所属的包,是基本类型,则返回"java.lang"
java.lang.reflect.Field
java.lang.reflect.Method
java.lang.reflect.Constructor
Class getDeclaringClass()
//返回Class对象,表示定义了这个构造器、方法或字段的类
Class[] getExceptionTypes()
//返回一个Class对象数组,其中各个对象表示这个方法所抛出异常的类型
int getModifiers()
//返回一个整数,描述这个构造器,方法或字段的修饰符。使用Modifier类中的方法来分析这个返回值
String getName()
//返回一个表示构造器名、方法名或字段名的字符串
Class[] getParamterTypes()
//返回一个Class对象数组,其中各个对下给你表示参数的类型
Class getReturnType()
//返回一个用于表示返回类型的Class对象

java.lang.reflect.Modifier
static String toString(int modifiers)
static boolean isAbstract(int modifiers)
static boolean isFinal(int modifiers)
static boolean isInterface(int modifiers)
static boolean isNative(int modifiers)
static boolean isPrivate(int modifiers)
static boolean isProtected(int modifiers)
static boolean isPublic(int modifiers)
static boolean isStatic(int modifiers)
static boolean isStrict(int modifiers)
static boolean isStrict(int modifiers)
static boolean isSynchronized(int modifiers)
static boolean isVolatile(int modifiers)
//这些方法检测modifiers值与参数对应的二进制位
访问反射的对象
  1. 代码
package reflection;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

public class Objectanalyzer {

    private ArrayList<Object> visited = new ArrayList<>();

    public String toString(Object obj) throws IllegalAccessException {
        if(obj ==null)return "null";
        if(visited.contains(obj)) return "...";
        visited.add(obj);
        Class<?> cl = obj.getClass();
        if(cl == String.class) return (String) obj;
        if(cl.isArray()){
            String r = cl.getComponentType() + "[]{";
            for(int i = 0; i < Array.getLength(obj);i++){
                if(i >0) r += ",";
                Object val = Array.get(obj,i);
                if(cl.getComponentType().isPrimitive()) r +=val;
                else r+=toString(val);
            }
            return r +"}";
        }

        String r = cl.getName();

        do{
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields,true);
            for(Field f : fields){
                if(!Modifier.isStatic(f.getModifiers())){
                    if(!r.endsWith("[")) r+=",";
                    r += f.getName() +"=";
                    Class t = f.getType();
                    Object val = f.get(obj);
                    if(t.isPrimitive()) r += val;
                    else  r += toString(val);
                }
            }
            r += "]";
            cl = cl.getSuperclass();
        }while (cl != null);

        return r;
    }
}

package reflection;

import java.util.ArrayList;

public class ObjectAnalyzerTest {

    public static void main(String[] args) throws IllegalAccessException {
        ArrayList<Integer> square = new ArrayList<>();
        for(int i = 1;i <= 5;i++){
            square.add(i *i);
        }
        System.out.println(new Objectanalyzer().toString(square));
    }
}
  1. 常用方法
java.lang.reflect.AccessibleObject
void setAccessible(boolean flag)
//设置或取消这个可访问对象的可访问标志
void setAccessible(boolean flag)
boolean trySetAccessible()
//为这个可访问的对象设置可访问标志,如果拒绝访问则返回false
boolean isAccessible()]
//得到这个可访问对象的可访问标志值
static void setAccessible(AccessibleObject[] array,boolean flag)
//这是一个便利方法,用于设置一个对象数组的可访问标志

java.lang.reflect.Field
Object get(Object obj)
//返回obj对象中用这个Field对象描述的字段的值
void set(Object obj,Object newValue)
//将obj对象中这个Field对象描述的字段设置为一个新值
生成泛型数组代码
  1. java.lang.reflect包中的Array类允许动态创建数组。

  2. 代码示例

public class CopyOfTest {

    public static void main(String[] args) {
        int[] a = {1,2,3};
        a = (int[]) goodCopyOf(a,10);
        System.out.println(Arrays.toString(a));

        Object[] b ={1,2,3};
        System.out.println(Arrays.toString(b));

    }

    public static Object goodCopyOf(Object a,int newLength){
        Class<?> cl = a.getClass();
        if(!cl.isArray())return null;
        Class<?> componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a,0,newArray,0,Math.min(length,newLength));
        return newArray;
    }
}
  1. 常用代码
java.lang.reflect.Array
static Object get(Object array,int index)
static xxx getXxx(Object array,int index)
//将返回存储在给定数组中给定索引位置上的值
static void set(Object array,int index,Object newValue)
static SetXxx(Obectt array,int index,xxx newValue)
//将一个心智存储到给定数组中的给定位置
static int getLength(Object array)
//返回给定数组的长度
static Object newInstance(Class componectType,int length)
static Object newInstance(Class componectType,int[] lengths)
//返回一个有给定类型、给定大小的新数组
调用任意方法和构造器
  1. 代码示例
public class MethodTableTest {


    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException,
            IllegalAccessException {
        Method square = MethodTableTest.class.getMethod("square", double.class);
        Method sqrt = Math.class.getMethod("sqrt", double.class);

        printTable(1,10,10,square);
        printTable(1,10,10,sqrt);
    }

    public static double square(double x) {
        return x * x;
    }

    public static void printTable(double from, double to, int n, Method f) throws InvocationTargetException,
            IllegalAccessException {
        System.out.println(f);

        double dx = (to - from) / (n - 1);

        for (double x = from; x <= to; x += dx) {
            double y = (Double) f.invoke(null, x);
            System.out.printf("%10.4f | %10.4f%n", x, y);
        }
    }
}
  1. 常用方法
java.lang.reflect.Method
public Object invoke(Object implicitParameter,Object[] explicitParameters)
//调用这个对象描述的方法,传入给定参数,并返回方法的返回值。对于静态方法,传入null作为隐式参数

八.类加载

1.定义

类加载是指将 .class 字节码文件加载到 JVM 内存中,并完成类的验证、准备、解析和初始化等过程。

Java 虚拟机使用**类加载器(ClassLoader)**来加载类,有默认加载器,也允许开发者自定义类加载器。

  • 引导类加载器
  • 平台类加载器
  • 系统类加载器

2.Java 程序中的三大核心类加载器

加载器名称加载内容所属层级
引导类加载器(Bootstrap ClassLoader)核心类库(如 java.lang.*java.util.*最顶层,由 JVM 实现
平台类加载器(Platform ClassLoader)jrt:/modules/ 中的非核心标准库(JDK 9+)引导类的子类加载器
系统类加载器(Application ClassLoader)应用程序 classpath 下的类默认使用者自定义类

3.类加载器命名空间

Java 使用 双亲委派模型
  1. 每个类加载器(除了引导类)都有一个父类加载器。
  2. 加载类时,优先交给父加载器尝试加载。
  3. 如果父加载器无法加载,再由当前加载器尝试。
目的:
  • 避免重复加载
  • 防止用户类覆盖核心类(如 java.lang.String

4.类加载器的命名空间(Namespace)

JVM 中的类是由:类的全限定名(包名+类名) + 加载它的类加载器 唯一决定的。

也就是说:同一个类名,如果是由不同类加载器加载,在 JVM 看来就是两个不同的类,互不兼容。

5.Flink 中的类加载应用:用户代码隔离

Flink 使用 用户隔离类加载器(UserCodeClassLoader) 来支持如下场景:

例子:

  • 用户 A 提交了使用 kafka 1.1 的 jar 包
  • 用户 B 提交了使用 kafka 1.2 的 jar 包

两个作业都包含了类名为 org.apache.kafka.clients.Producer 的类,但 Flink 为每个作业构建独立的类加载器:

ClassLoader classLoader = new FlinkUserCodeClassLoader(userJarUrls, parent);

这样每个作业:

  • 在自己的类加载器空间中运行
  • 相互之间的类不会冲突
  • 即使类名相同,类加载器不同,JVM 认为是不同的类

6.小结

  • 类加载器决定类的唯一性:类名 + 加载器
  • 双亲委派模型防止核心类被篡改
  • Flink 通过自定义类加载器实现用户作业之间的类隔离
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值