Dubbo笔记衍生篇⑥:本地Mock 和服务降级

本文详细解析了Dubbo本地Mock的配置、服务降级策略以及MockClusterInvoker的工作原理,包括Mock属性校验、MockInvoker的选择与调用过程,以及针对Mock功能的疑问和多分组消费者行为的影响。
摘要由CSDN通过智能技术生成

一、前言

本系列为个人Dubbo学习笔记衍生篇,是正文篇之外的衍生内容,内容来源于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章。仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


1. 本地Mock

服务消费端本地服务mock主要用来做本地测试用,当服务提供端服务不可用时,使用本地mock服务可以模拟服务提供端来让服务消费方测试自己的功能,而不需要发起远程调用。

要实现Mock功能,首先需要消费端先实现服务端的 mock 实现类,需要注意的是Mock实现类必须要符合 接口包名.类名Mock格式。需要注意的是,在执行Mock服务实现类 mock() 方法前,会先发起远程调用,当远程服务调用失败时,才会降级执行mock功能。

开启mock 功能需要设置 :

    // 设置启动时候不检查服务是否可用
    referenceConfig.setCheck(false);
    // 设置启用Mock
    referenceConfig.setMock(true);

2. 服务降级

基于本地Mock,Dubbo提供了一些服务降级措施,当服务提供端某一个非关键的服务出错时,可以手动对消费端的调用进行降级,这样服务消费端就避免了再去调用出错的服务,以避免加重服务提供端的负担。服务降级的本质也是通过mock配置。

Spring 中可以通过 @Reference注解的 mock属性设置

    @Reference(version = "1.0.0", mock = "true")
    private NewDemoService newDemoService;

这里的 mock 属性的取值可以为 :

  • true || default || fail || force : 这四种取值都会按照默认方式去执行, 即Dubbo 默认会先远程调用提供者,如果提供者调用失败,则按照调用接口的路径寻找mock实现类,Mock实现类 遵循接口包名.类名Mock格式 的规则,如调用 com.kingfish.DemoService,则需要创建类 com.kingfish.DemoServiceMock 实现DemoService 接口。如果消费者调用 提供者 DemoService 实现类失败,则会调用 DemoServiceMock 方法并返回。

  • return xxx :当调用失败时,会返回return 指定的mock值,其中xxx 可以不填,默认返回null。其中return的合法字符串可以是:

    	empty: 代表空,基本类型的默认值,或者集合类的空值
    	null: null
    	true: true
    	false: false
    	JSON 格式: 反序列化 JSON 所得到的对象
    
  • throw XxxException :当调用失败时,使用 throw 来返回一个 Exception 对象,作为 Mock 的返回值。其中 XxxException可以不填,默认为 RPCException

  • 指定mock类路径 :即会按照默认路径寻找mock类,并执行mock方法。

  • fail: :该种方式会先去尝试调用服务提供者,若调用失败,再返回mock值。fail: 可以和 return xxxthrow XxxException指定mock类路径 搭配使用。如 fail: return xxx

  • force: :该种方式不会再调用服务提供者,而是直接返回客户端mock值。force:return xxx 这里返回的mock值为return 指定的 xxx,force: 可以和 return xxxthrow XxxException指定mock类路径 搭配使用。如 force: return xxx

更多用法参考: https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/


3. MockClusterInvoker 的加载

Dubbo 在加载 Cluster 接口时会使用其包装类 包装,而其中便提供了 MockClusterWrapper 来对 Invoke 进行包装。如下:
在这里插入图片描述

MockClusterWrapper 实现如下,借由此将 MockClusterInvoker 加入到了调用链路中。

public class MockClusterWrapper implements Cluster {

    private Cluster cluster;

    public MockClusterWrapper(Cluster cluster) {
        this.cluster = cluster;
    }

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new MockClusterInvoker<T>(directory,
                this.cluster.join(directory));
    }

}

二、Mock 属性校验

消费者在创建服务提供者代理类时,第一步就是参数校验,其中就有对 mock参数的校验。其方法AbstractInterfaceConfig#checkMock 的实现如下:

    void checkMock(Class<?> interfaceClass) {
    	// 如果 mock 没有配置,则直接跳过校验
        if (ConfigUtils.isEmpty(mock)) {
            return;
        }
		// 标准化 mock参数
        String normalizedMock = MockInvoker.normalizeMock(mock);
        // 对标准化后的参数校验
        // 如果 是以 return 开头,则校验return 的返回值是否合法
        if (normalizedMock.startsWith(Constants.RETURN_PREFIX)) {
            normalizedMock = normalizedMock.substring(Constants.RETURN_PREFIX.length()).trim();
            try {
                //Check whether the mock value is legal, if it is illegal, throw exception
                // 检测模拟返回值是否合法,不合法抛出异常
                MockInvoker.parseMockValue(normalizedMock);
            } catch (Exception e) {
                throw new IllegalStateException("Illegal mock return in <dubbo:service/reference ... " +
                        "mock=\"" + mock + "\" />");
            }
            // 如果以 throw 开头,则校验 mock 的异常是否合法
        } else if (normalizedMock.startsWith(Constants.THROW_PREFIX)) {
            normalizedMock = normalizedMock.substring(Constants.THROW_PREFIX.length()).trim();
            if (ConfigUtils.isNotEmpty(normalizedMock)) {
                try {
                    //Check whether the mock value is legal
                    MockInvoker.getThrowable(normalizedMock);
                } catch (Exception e) {
                    throw new IllegalStateException("Illegal mock throw in <dubbo:service/reference ... " +
                            "mock=\"" + mock + "\" />");
                }
            }
        } else {
            //Check whether the mock class is a implementation of the interfaceClass, and if it has a default constructor
         
            // 检查模拟类是否是interfaceClass的实现,以及是否具有默认构造函数
            // mock 为 DEFAULT 则按照 接口路径寻找 mock实现类,否则按照指定路径去寻找
            MockInvoker.getMockObject(normalizedMock, interfaceClass);
        }
    }

我们下面主要看MockInvoker#normalizeMock 标准化参数的过程,其实现MockInvoker#normalizeMock 如下:

    public static String normalizeMock(String mock) {
        if (mock == null) {
            return mock;
        }

        mock = mock.trim();

        if (mock.length() == 0) {
            return mock;
        }
		// mock = return,标准化为 return null
        if (Constants.RETURN_KEY.equalsIgnoreCase(mock)) {
            return Constants.RETURN_PREFIX + "null";
        }
		// mock = true || default || fail || force 标准化为 default
        if (ConfigUtils.isDefault(mock) || "fail".equalsIgnoreCase(mock) || "force".equalsIgnoreCase(mock)) {
            return "default";
        }
		// mock 以 fail: 开头,截取后面的数据
        if (mock.startsWith(Constants.FAIL_PREFIX)) {
            mock = mock.substring(Constants.FAIL_PREFIX.length()).trim();
        }
		// mock 以 force: 开头,截取后面的数据
        if (mock.startsWith(Constants.FORCE_PREFIX)) {
            mock = mock.substring(Constants.FORCE_PREFIX.length()).trim();
        }
		// mock 以 return 或 throw 开头,替换单引号为双引号
        if (mock.startsWith(Constants.RETURN_PREFIX) || mock.startsWith(Constants.THROW_PREFIX)) {
            mock = mock.replace('`', '"');
        }

        return mock;
    }

需要注意的是,对于 fail: xxx 或 force: xxx 格式的mock配置,其返回值是 截取了 fail: 和 force: 之后的值。如 force: return 123, 返回值为 return 123


可以看到,AbstractInterfaceConfig#checkMock 对Mock 参数进行了校验,不合格的参数在消费者启动时就会抛出异常。

三、MockClusterInvoker

Dubbo笔记⑨ : 消费者启动流程 - RegistryProtocol#refer 2 cluster.join(directory); 一文中,我们介绍过Dubbo在生成 Invoker 时 MockClusterWrapper 会将 FailoverClusterInvoker 实例包装成了MockClusterInvoker 实例。

这里简单介绍一下:


消费者端获取到的服务提供者实例本身是一个服务代理,其结构如下:
在这里插入图片描述

PefProxy 中存在 Invoker 实例,Invoker实例代表服务的可执行体,其中保存了当前可提供服务的服务列表(即 RegistryDirectory 中的 Invoker 列表),当消费者进行调用时,会调用其内部 Invoker#invoke 方法 。当消费者进行调用时,其调用顺序可以简化如下:

RefProxy#sayHello -> MockClusterInvoker#invoke -> FailoverClusterInvoker#invoke-> FailoverClusterInvoker#list -> RegistryDirectory#doList -> ...

其中

  • MockClusterInvoker : 本文介绍的 MockClusterInvoker ,完成了 Dubbo 本地mock功能
  • FailoverClusterInvoker : 完成了Dubbo集群容错功能,这里可以指定为别的ClusterInvoker。
  • RegistryDirectory :服务目录,里面保存了可以提供服务的服务提供者列表,在调用时会经过负载均衡算法后挑选一个合适的 Invoker 进行服务调用。

本文关注点在 MockClusterInvoker#invoke 方法,其实现如下:

 	@Override
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;
		// 判断是否开启mock 功能,通过设置 mock属性可以表明当前方法是否开启mock功能
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {
            //no mock
            // 没有开启则直接透传下一层
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
            // 如果指定的是 force:return 策略
            result = doMockInvoke(invocation, null);
        } else {
            // 否则按照指定的是 fail-mock 策略
            try {
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                }
                // 执行mock调用
                result = doMockInvoke(invocation, e);
            }
        }
        return result;
    }

上面的逻辑比较简单我们这里可以分为三种情况:

  1. 没有开启mock : 没有开启mock,则直接调用下一层的 invoker方法。
  2. 开启 force 策略 :直接调用 mock 方法
  3. 开启 fail 策略 :先尝试调用服务,服务调用出了异常,再调用mock方法。

这里可以看到,force 和 fail 都是通过 doMockInvoke 方法完成的mock调用,其MockClusterInvoker#doMockInvoke 实现如下:


    @SuppressWarnings({"unchecked", "rawtypes"})
    private Result doMockInvoke(Invocation invocation, RpcException e) {
        Result result = null;
        Invoker<T> minvoker;
		// 1. 从注册中心 获取 Mock  Invoker
        List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
        if (mockInvokers == null || mockInvokers.isEmpty()) {
        	// 2. 创建 MockInvoker 
            minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
        } else {
            minvoker = mockInvokers.get(0);
        }
        try {
        	// 调用 invoke 方法
            result = minvoker.invoke(invocation);
        } catch (RpcException me) {
            if (me.isBiz()) {
                result = new RpcResult(me.getCause());
            } else {
                throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
            }
        } catch (Throwable me) {
            throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
        }
        return result;
    }

这里我们看到 MockClusterInvoker#doMockInvoke 的逻辑很简单。

  1. selectMockInvoker(invocation) 从注册中心筛选出合适的 Invoker 集合 mockInvokers。
  2. 如果mockInvokers 不为空,则取第一个直接调用。否则创建一个 MockInvoker 进行调用。

下面我们来具体看一看:

1. MockClusterInvoker#selectMockInvoker

MockClusterInvoker#selectMockInvoker 从注册中心获取到协议为 mock 的服务列表。

	// org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#selectMockInvoker
    private List<Invoker<T>> selectMockInvoker(Invocation invocation) {
        List<Invoker<T>> invokers = null;
        //TODO generic invoker?
        // 如果是 RPC 调用
        if (invocation instanceof RpcInvocation) {
            //Note the implicit contract (although the description is added to the interface declaration, but extensibility is a problem. The practice placed in the attachment needs to be improved)
            // 记录当前请求是 mock 请求 (invocation.need.mock = true),这个参数在后续会使用到
            ((RpcInvocation) invocation).setAttachment(Constants.INVOCATION_NEED_MOCK, Boolean.TRUE.toString());
            //directory will return a list of normal invokers if Constants.INVOCATION_NEED_MOCK is present in invocation, otherwise, a list of mock invokers will return.
            try {
            	// 如果在调用中存在Constants.INVOCATION_NEED_MOCK 且为 true,则目录将返回模拟调用者的列表,否则,将返回常规调用者的列表。
                invokers = directory.list(invocation);
            } catch (RpcException e) {
              	// .... 日志打印
            }
        }
        return invokers;
    }

其中关键逻辑在于

invokers = directory.list(invocation);

在单注册中心情况下,这里的 directory 实现为 RegistryDirectory,所以这里为 RegistryDirectory#doList。关于 RegistryDirectory#doList 我们在 Dubbo笔记⑭ :Dubbo集群组件 之 Directory。这里简单提一下:

Directory#list 的调用顺序如下:

Directory#list -> RegistryDirectory#doList -> RouterChain#route 

Dubbo笔记⑨ : 消费者启动流程 - RegistryProtocol#refer1. RegistryDirectory#buildRouterChain 章节中我们讲过,RegistryDirectory#RouterChain 中的 routers 实际对象为:

// 调用顺序也如下
MockInvokersSelectorTagRouterAppRouterServiceRouter

RouterChain#route 的方法实现如下:

    public List<Invoker<T>> route(URL url, Invocation invocation) {
        List<Invoker<T>> finalInvokers = invokers;
        for (Router router : routers) {
            finalInvokers = router.route(finalInvokers, url, invocation);
        }
        return finalInvokers;
    }

所以这首先会调用 MockInvokersSelector#route 方法,下面我们来看 MockInvokersSelector#router 的具体实现

1.1 MockInvokersSelector#router

MockInvokersSelector#router 方法会根据自身路由规则获取到 Invoker 集合。需要注意,如果附件 为 null (invocation.getAttachments() == null)此时方法返回的集合是正常服务的Invoker 集合。

public class MockInvokersSelector extends AbstractRouter {

    public static final String NAME = "MOCK_ROUTER";

    @Override
    public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
                                      URL url, final Invocation invocation) throws RpcException {
        // 为空直接返回
        if (CollectionUtils.isEmpty(invokers)) {
            return invokers;
        }
		// 如果 附件为空就走正常的调用逻辑,因为 MockInvokersSelector  无法确定上游调用是mock还是正常调用
        if (invocation.getAttachments() == null) {
            return getNormalInvokers(invokers);
        } else {
        	// 如果INVOCATION_NEED_MOCK 为 true 则执行mock流程。在 selectMockInvoker 方法中将 INVOCATION_NEED_MOCK  置为了true
            String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
            if (value == null) {
                return getNormalInvokers(invokers);
            } else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
                return getMockedInvokers(invokers);
            }
        }
        return invokers;
    }
	// 从当前注册中心的服务列表中 筛选出 mock服务并返回
    private <T> List<Invoker<T>> getMockedInvokers(final List<Invoker<T>> invokers) {
        if (!hasMockProviders(invokers)) {
            return null;
        }
        List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(1);
        for (Invoker<T> invoker : invokers) {
            if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
                sInvokers.add(invoker);
            }
        }
        return sInvokers;
    }
	// 从当前注册中心的服务列表中 筛选出 非mock服务并返回
    private <T> List<Invoker<T>> getNormalInvokers(final List<Invoker<T>> invokers) {
        if (!hasMockProviders(invokers)) {
            return invokers;
        } else {
            List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(invokers.size());
            for (Invoker<T> invoker : invokers) {
                if (!invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
                    sInvokers.add(invoker);
                }
            }
            return sInvokers;
        }
    }


}

上游在调用 MockInvokersSelector 时可能是 Mock调用也可能是正常调用,MockInvokersSelector 针对这两个场景做了不同的处理。这里可以看到,MockInvokersSelector#router 方法的作用是 如果附件为空,则从注册中心获取所有非 Mock 协议服务,否则根据附件中的 INVOCATION_NEED_MOCK 参数值决定是否获取 Mock 协议服务,如果为 true 则获取 mock 服务,否则获取正常服务。

这里可以看到如果开启了mock功能,会从注册中心获取到 mock协议的服务,并作为mock方法调用。但是在mock协议的服务却无法发布,因为 MockProtocol 类在export 方法中直接抛出了异常,并且定义了该类为 final,也就代表着我们无法重写MockProtocol 类

2. MockInvoker#invoke

在 MockInvokersSelector#router 后,如果有 mock 协议方法则远程调用 mock方法,这里不需要多说。如果没有 mock协议服务,则MockInvokersSelector#router返回的为 null。此时MockClusterInvoker#invoke 会创建 MockInvoker 作为本地mock调用。

MockInvoker#invoke 实现如下

    @Override
    public Result invoke(Invocation invocation) throws RpcException {
    	// 尝试获取 methodName.mock 的配置信息
        String mock = getUrl().getParameter(invocation.getMethodName() + "." + Constants.MOCK_KEY);
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(this);
        }
        if (StringUtils.isBlank(mock)) {
        	// 获取 mock 属性的信息,即我们设置 mock值
            mock = getUrl().getParameter(Constants.MOCK_KEY);
        }
		// 为空抛出异常
        if (StringUtils.isBlank(mock)) {
            throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
        }
       // mock 参数标准化 这里返回值会把  force: 和  fail: 截取
       //   因为 关于  fail: 和  force: 策略的判断在 MockClusterInvoker#invoke 中已经完成,在这里这两个策略已经没有意义。
        mock = normalizeMock(URL.decode(mock));
        //处理 return  mock 场景
        if (mock.startsWith(Constants.RETURN_PREFIX)) {
        	// 截取 return 指定返回值并返回,默认为 null
            mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
            try {
                Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
                Object value = parseMockValue(mock, returnTypes);
                return new RpcResult(value);
            } catch (Exception ew) {
                throw new RpcException("mock return invoke error. method :" + invocation.getMethodName()
                        + ", mock:" + mock + ", url: " + url, ew);
            }
            // 处理 throw 场景
        } else if (mock.startsWith(Constants.THROW_PREFIX)) {
        	// 抛出 throw  指定异常,默认 RpcException
            mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
            if (StringUtils.isBlank(mock)) {
                throw new RpcException("mocked exception for service degradation.");
            } else { // user customized class
                Throwable t = getThrowable(mock);
                throw new RpcException(RpcException.BIZ_EXCEPTION, t);
            }
        } else { //impl mock
            try {
            	// 否则则认为是指定了 mock实现类,则进行调用
                Invoker<T> invoker = getInvoker(mock);
                return invoker.invoke(invocation);
            } catch (Throwable t) {
                throw new RpcException("Failed to create mock implementation class " + mock, t);
            }
        }
    }

    private Invoker<T> getInvoker(String mockService) {
    	// 从缓存中获取
        Invoker<T> invoker = (Invoker<T>) mocks.get(mockService);
        if (invoker != null) {
            return invoker;
        }
		// 缓存未命中则通过反射创建
        Class<T> serviceType = (Class<T>) ReflectUtils.forName(url.getServiceInterface());
        T mockObject = (T) getMockObject(mockService, serviceType);
        invoker = proxyFactory.getInvoker(mockObject, serviceType, url);
        // 这里可以看到, Dubbo限制了每个接口 mock 类最大 10000 个
        if (mocks.size() < 10000) {
            mocks.put(mockService, invoker);
        }
        return invoker;
    }

四、总结

1. 流程图

综上,整个过程的流程图大致如下:
在这里插入图片描述

2. 一些疑问

下面是本篇中的一些疑问以及自身理解。才疏学浅,难免有误,感谢指正。

2.1. 远程mock

在上面代码中, MockInvokersSelector#router 中筛选了注册中心上 mock协议的服务。这意味着其实可以调用远程mock 服务。(MockInvokersSelector#router 返回的 Invoker 如果不为空会被用于mock 调用)。但是在服务提供者端缺无法发布 mock协议的服务。原因在于 MockProtocol#export 方法会直接抛出异常,并且MockProtocol 被定义为了 final,这意味着我们没办法重写该方法。

final public class MockProtocol extends AbstractProtocol {

    @Override
    public int getDefaultPort() {
        return 0;
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        throw new UnsupportedOperationException();
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return new MockInvoker<T>(url);
    }
}

2.2. 消费者多分组情况

如果消费者多分组调用时,并且存在至少一个服务提供者的情况下。force 策略不起作用。即还是会进行远程调用。确切的说,应该是在这种情况下,服务路由不起作用
原因在于 RegistryDirectory#doList 中针对多分组情况直接返回了注册中心所有 Invokers。


首先需要明确下面的调用链路

MockClusterInvoker#doMockInvoke =MockClusterInvoker#selectMockInvoker =RegistryDirectory#doList => RouterChain#route(在此方法中进行路由)

RegistryDirectory#doList 方法简化如下:

    @Override
    public List<Invoker<T>> doList(Invocation invocation) {
        if (multiGroup) {
            return this.invokers == null ? Collections.emptyList() : this.invokers;
        }
        List<Invoker<T>> invokers = null;
        invokers = routerChain.route(getConsumerUrl(), invocation);
        return invokers == null ? Collections.emptyList() : invokers;
    }

正常情况下(单分组情况):在 RegistryDirectory#doList 中我们会通过 invokers = routerChain.route(getConsumerUrl(), invocation); 来调用路由链,从而调用MockInvokersSelector#router方法将注册中心所有mock协议的服务筛选出来,但是由于mock协议的服务无法注册,所以这里从注册中心无法获取到 invoker,所以导致 MockClusterInvoker#selectMockInvoker 方法返回的 Invoker集合为空。而MockClusterInvoker#doMockInvoke 判断 MockClusterInvoker#selectMockInvoker 方法返回为空后会创建 MockInvoker 来进行服务调用,完成本地mock功能。也就是说,是否本地调用取决于 MockClusterInvoker#selectMockInvoker 返回的 Invoker 集合是否为空。
在这里插入图片描述

而多分组情况下: RegistryDirectory#doList 直接判断 multiGroup = true,所以直接将注册中心所有的 Invoker 集合返回。此时 MockClusterInvoker#selectMockInvoker 返回一个的Invoker集合不为空。而在MockClusterInvoker#doMockInvoke 中,如果 MockClusterInvoker#selectMockInvoker 返回不为空,则会直接挑选第一个 invoker 进行服务调用。由于没有使用 MockInvoker调用,所以无法完成本地 mock。


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值