java实现异步非阻塞的几种方式-同步阻塞调用

本文通过一个具体的订单查询接口示例,介绍了Java中同步阻塞调用的基本原理及其实现过程。分析了同步调用下线程的运行状态,并指出了其优缺点。

java实现异步非阻塞的几种方式-同步阻塞调用

1. 同步阻塞调用

在讲异步非阻塞之前还是先来说明同步阻塞的调用吧。明白了同步阻塞的调用,才能更好的明白异步非阻塞的调用。以一个示例来说明吧,这是一个非常常见的程序间的调用。

我们的程序对外提供当前的用户的订单详细查询的接口,订单接口先调用用户服务,获取当前的用户信息;再调用商品接口获取商品的详细信息。

就以这样一个示例程序来说明吧。

在这里插入图片描述

假设这个订单服务调用用户服务的时间是2秒,调用商品服务的时间是3秒,订单服务自身的处理时间是1秒,那整个处理流程所需的时间就是2+3+1=6秒。

1.1 样例代码

用户服务:

/**
 * 模拟的用户服务 
 */
@RestController
@RequestMapping("/user")
public class UserService {

  /** 测试预期值 */
  public static final String CHECK_USER_ID = "1001";

  @RequestMapping(
      value = "/getUserInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody UserDTO input) {
    if (null != input && CHECK_USER_ID.equals(input.getUserId())) {
      UserDTO rsp = new UserDTO();
      rsp.setName("bug_null");
      rsp.setAddress("this is shanghai");
      rsp.setUserId(CHECK_USER_ID);

      ThreadUtils.sleep(5);

      return ApiResponse.ok(rsp);
    }
    return ApiResponse.fail();
  }
}

/**
 * 用户的传输实体
 */
public class UserDTO {
  /** 用户的id */
  private String userId;

  /** 用户的名称 */
  private String name;

  /** 地址信息 */
  private String address;

 ......
}


/**
 * 用户服务的springboot的入口
 */
@SpringBootApplication
public class UserApplication {
  public static void main(String[] args) {
    String[] argsNew = new String[] {"-- server.port=9000"};
    SpringApplication.run(UserApplication.class, argsNew);
  }
}

商品服务

/**
 * 用户模拟商品服务
 */
@RestController
@RequestMapping("/goods")
public class GoodsService {

  /** 商品的id */
  public static final String GOODS_ID = "2001";

  @RequestMapping(
      value = "/getGoodsInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody GoodsDTO input) {
    if (null != input && GOODS_ID.equals(input.getDataId())) {
      GoodsDTO goods = new GoodsDTO();
      goods.setDataId(GOODS_ID);
      goods.setGoodsPrice(1024);
      goods.setMessage("这是一个苹果,apple,还被咬了一口");

      ThreadUtils.sleep(10);

      return ApiResponse.ok(goods);
    }
    return ApiResponse.fail();
  }
}

/**
 * 商品信息
 */
public class GoodsDTO {

  /** 商品的id */
  private String dataId;

  /** 商品的价格 */
  private Integer goodsPrice;

  /** 商品的描述 */
  private String message;

  ......
}

@SpringBootApplication
public class GoodsApplication {

  public static void main(String[] args) {
    String[] argsNew = new String[] {"-- server.port=9001"};
    SpringApplication.run(GoodsApplication.class, argsNew);
  }
}

订单服务

@RestController
@RequestMapping("/order")
public class OrderServerFacade {

  private Logger logger = LoggerFactory.getLogger(OrderServerFacade.class);

  private Gson gson = new Gson();

  /** 获取连接处理对象 */
  private RestTemplate restTemplate = this.getRestTemplate();

  @RequestMapping(
      value = "/orderInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody OrderDTO order) {
    logger.info("getUserInfo start {}", order);
    // 用户信息
    ClientUserDTO userRsp = this.getUserInfo(order.getUserId());
    // 获取商品信息
    ClientGoodsDTO clientGoodRsp = this.getGoods(order.getGoodId());
    ThreadUtils.sleep(1);
    OrderDTO orderRsp = this.builderRsp(userRsp, clientGoodRsp);
    logger.info("getUserInfo start {} rsp {} orderRsp {}  ", order, orderRsp);
    // 构建结果的响应
    return ApiResponse.ok(orderRsp);
  }

  /**
   * 构造响应
   *
   * @param userInfo 用户信息
   * @return 当前响应的用户信息
   */
  private OrderDTO builderRsp(ClientUserDTO userInfo, ClientGoodsDTO goodsInfo) {
    OrderDTO order = new OrderDTO();
    order.setUserId(userInfo.getUserId());
    order.setUserInfo(userInfo);
    order.setGoodId(goodsInfo.getDataId());
    order.setGoodsInfo(goodsInfo);
    return order;
  }

  /**
   * 获取用户的信息
   *
   * @param userId 用户的id
   * @return 用户的信息
   */
  private ClientUserDTO getUserInfo(String userId) {

    logger.info("request get user info start {} ", userId);

    ClientUserDTO clientUser = new ClientUserDTO();
    clientUser.setUserId(userId);
    HttpHeaders headers = new HttpHeaders();
    // 将对象装入HttpEntity中
    HttpEntity<ClientUserDTO> request = new HttpEntity<>(clientUser, headers);
    ResponseEntity<String> result =
        restTemplate.postForEntity("http://localhost:9000/user/getUserInfo", request, String.class);
    if (HttpStatus.OK.value() == result.getStatusCodeValue()) {
      ApiResponse<ClientUserDTO> data =
          gson.fromJson(result.getBody(), new TypeToken<ApiResponse<ClientUserDTO>>() {}.getType());
      // 如果操作成功,则返回结果
      if (data.getResult()) {
        ClientUserDTO rsp = data.getData();
        logger.info("request get user info start {} rsp {} ", userId, rsp);
        return rsp;
      }
    }
    return null;
  }

  /**
   * 获取商品信息
   *
   * @param dataId 商品信息
   * @return 当前的用户的信息
   */
  private ClientGoodsDTO getGoods(String dataId) {

    logger.info("request goods start {} ", dataId);

    ClientGoodsDTO clientGoods = new ClientGoodsDTO();
    clientGoods.setDataId(dataId);
    HttpHeaders headers = new HttpHeaders();
    // 将对象装入HttpEntity中
    HttpEntity<ClientGoodsDTO> request = new HttpEntity<>(clientGoods, headers);
    ResponseEntity<String> result =
        restTemplate.postForEntity(
            "http://localhost:9001/goods/getGoodsInfo", request, String.class);
    if (HttpStatus.OK.value() == result.getStatusCodeValue()) {
      ApiResponse<ClientGoodsDTO> data =
          gson.fromJson(
              result.getBody(), new TypeToken<ApiResponse<ClientGoodsDTO>>() {}.getType());
      // 如果操作成功,则返回结果
      if (data.getResult()) {
        ClientGoodsDTO goodsRsp = data.getData();
        logger.info("request goods start {} , response {} ", dataId, goodsRsp);
        return goodsRsp;
      }
    }
    return null;
  }

  /**
   * 获取连接管理对象
   *
   * @return
   */
  private RestTemplate getRestTemplate() {
    return new RestTemplate(getClientHttpRequestFactory());
  }

  /**
   * 获取连接处理的工厂信息
   *
   * @return
   */
  private SimpleClientHttpRequestFactory getClientHttpRequestFactory() {
    SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
    clientHttpRequestFactory.setConnectTimeout(25000);
    clientHttpRequestFactory.setReadTimeout(25000);
    return clientHttpRequestFactory;
  }
}

1.2 测试

执行单元测试:

/**
 * 订单服务查询
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest(
    classes = {OrderApplication.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestOrderServerFacade {

  @Autowired private Gson gson;

  @Autowired protected TestRestTemplate restTemplate;

  @Test
  public void testUser() {
    OrderDTO orderInfo = new OrderDTO();
    orderInfo.setGoodId("2001");
    orderInfo.setUserId("1001");
    // 将对象装入HttpEntity中
    HttpEntity<OrderDTO> request = new HttpEntity<>(orderInfo);
    ResponseEntity<String> result =
        restTemplate.postForEntity("/order/orderInfo", request, String.class);
    Assert.assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue());
    ApiResponse<OrderDTO> data =
        gson.fromJson(result.getBody(), new TypeToken<ApiResponse<OrderDTO>>() {}.getType());
    System.out.println(data);
    Assert.assertEquals(data.getResult(), Boolean.TRUE);
    Assert.assertEquals(data.getCode(), APICodeEnum.SUCCESS.getErrorData().getCode());
    Assert.assertNotNull(data.getData().getGoodsInfo());
    Assert.assertNotNull(data.getData().getUserInfo());
  }
}

单元测试结果

在这里插入图片描述

从结果中耗时是16秒多。也证实了在开始时我的一个预期。同步阻塞式调用。由于只能顺序式调用。所以总时间就是所有调用的总时间相加。

1.3 分析

那当以客户端去调用服务时,客户端都在做什么呢?

这个时候可以借助于java的命令,拿到线程的栈信息和java堆信息

D:\run\dump>jps -l
13568 com.liujun.asynchronous.nonblocking.common.goods.GoodsApplication
1456 com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderApplication
14848 org.jetbrains.jps.cmdline.Launcher
13604 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
3508 com.liujun.asynchronous.nonblocking.common.user.UserApplication
5556 org.jetbrains.idea.maven.server.RemoteMavenServer36
6996
12840 org/netbeans/Main
14840
16092 sun.tools.jps.Jps

    
D:\run\dump>jstack -l 1456 > order.threaddump

查看日志

2021-03-09 10:39:01.591  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : getUserInfo start OrderDTO{userId='1001', userInfo=null, goodId='2001', goodsInfo=null}
2021-03-09 10:39:01.591  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request get user info start 1001 
2021-03-09 10:39:03.596  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request get user info start 1001 rsp UserDTO{userId='1001', name='bug_null', address='this is shanghai'} 
2021-03-09 10:39:03.597  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request goods start 2001 
2021-03-09 10:39:13.603  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request goods start 2001 , response Goods{dataId='2001', goodsPrice=1024, message='这是一个苹果,apple,还被咬了一口'} 
2021-03-09 10:39:14.603  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : getUserInfo start OrderDTO{userId='1001', userInfo=null, goodId='2001', goodsInfo=null} rsp OrderDTO{userId='1001', userInfo=UserDTO{userId='1001', name='bug_null', address='this is shanghai'}, goodId='2001', goodsInfo=Goods{dataId='2001', goodsPrice=1024, message='这是一个苹果,apple,还被咬了一口'}} orderRsp {}   
 

从日志可以看任务是顺序式的调用。所以整个的调用时间就是所有执行的累加。

在本例中,配制的信息仅提取了线程的后15位信息,但已经可以定位到调用的线程了,现在去查看线程的调用栈信息.

"http-nio-9010-exec-6" #29 daemon prio=5 os_prio=0 tid=0x000000002129d000 nid=0x3ab0 runnable [0x000000002338c000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
	at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
	at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
	- locked <0x00000006c5a0a588> (a java.io.BufferedInputStream)
	at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
	at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1593)
	- locked <0x00000006c5a0a638> (a sun.net.www.protocol.http.HttpURLConnection)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
	- locked <0x00000006c5a0a638> (a sun.net.www.protocol.http.HttpURLConnection)
	at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
	at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:55)
	at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:55)
	at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:766)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:736)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670)
	at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:445)
	at com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderServerFacade.getGoods(OrderServerFacade.java:124)
	at com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderServerFacade.getUserInfo(OrderServerFacade.java:50)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	- locked <0x00000006c59c6e10> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- <0x00000006c53ad000> (a java.util.concurrent.ThreadPoolExecutor$Worker)

IO会阻塞在:java.net.SocketInputStream#socketRead0 的native方法上。

通过分析,就可以知道同步调用的一个线程模型:

在这里插入图片描述

1.4 总结

优点:

这应该是编程最简单的模型了。调用链非常的清楚,简单直接。不需要其他复杂的机制来解决异步所带来的问题,等着结果即可,所以在很大一部分业务场景中,采用的都是此调用模型。

劣势:

同步阻塞的最大问题在于在调用过程中线程处于等待状态,线程的资源没有充分的利用,对于服务器端应用来说,会限并发的用户数。

这是最基础的同步调用模型,详细代码可查看我的github:

https://github.com/kkzfl22/demojava8/blob/master/src/main/java/com/liujun/asynchronous/nonblocking/invoke/synchronous/OrderServerFacade.java

Java中,用于实现异步非阻塞调用HTTP服务的类或库主要包括以下几种: ### 使用 `java.net.http.HttpClient`(Java 11+) 从 Java 11 开始,JDK 提供了内置的异步 HTTP 客户端类 `HttpClient`,它支持同步异步请求,并且基于非阻塞 I/O 实现。它能够通过 `CompletableFuture` 来处理异步响应。以下是一个使用 `HttpClient` 的简单示例: ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class AsyncHttpClientExample { public static void main(String[] args) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://example.com")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println) .join(); } } ``` 该实现基于 NIO2.0 提供的异步通道机制,是真正的异步非阻塞模型[^2]。 ### 使用 `AsyncHttpClient`(Netty 提供的库) 除了 JDK 自带的 `HttpClient`,`AsyncHttpClient` 是一个基于 Netty 的第三方库,专门用于执行异步 HTTP 请求。它提供了更丰富的功能,例如支持 WebSocket、HTTP/2 等协议,并且性能优异。以下是使用 `AsyncHttpClient` 的基本示例: ```java import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.DefaultAsyncHttpClient; import org.asynchttpclient.Response; public class AsyncHttpClientExample { public static void main(String[] args) throws Exception { try (AsyncHttpClient client = new DefaultAsyncHttpClient()) { client.prepareGet("https://example.com") .execute() .toCompletableFuture() .thenApply(Response::getResponseBody) .thenAccept(System.out::println) .join(); } } } ``` 该库基于 Netty 框架实现,而 Netty 是一个高性能的网络编程框架,支持非阻塞 I/O 和异步编程模型,适合大规模并发请求的场景[^3]。 ### 使用 `CompletableFuture` 和 `ExecutorService` 进行自定义实现 如果需要更灵活的控制,也可以结合 `HttpURLConnection` 或 `OkHttp` 等同步 HTTP 客户端,并通过 `CompletableFuture` 和线程池来实现异步非阻塞调用。以下是一个简单的示例: ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CustomAsyncHttpClient { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { URL url = new URL("https://example.com"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); return response.toString(); } catch (Exception e) { throw new RuntimeException(e); } }, executor); future.thenAccept(System.out::println).join(); executor.shutdown(); } } ``` 这种实现方式虽然依赖同步 HTTP 客户端,但通过线程池和 `CompletableFuture` 实现异步效果,适用于对异步编程有特定需求的场景。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值