hystrix 单独使用_Hystrix服务熔断与降级

1 基本介绍

Hystrix 叫做断路器/熔断器。微服务系统中,整个系统出错的概率非常高,因为在微服务系统中,涉及到的模块太多了,每一个模块出错,都有可能导致整个服务出,当所有模块都稳定运行时,整个服务才算是稳定运行。我们希望当整个系统中,某一个模块无法正常工作时,能够通过我们提前配置的一些东西,来使得整个系统正常运行,即单个模块出问题,不影响整个系统。

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝), 向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常 ,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

1.1 分布式系统面临的问题

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务雪崩:多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的 “扇出” 。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”. 

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。 

1.2 解决方案

(1)熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。

(2)隔离模式:这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。 

(3)限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。 

2 基本用法

2.1 基本功能

1、服务降级

Hystrix服务降级,其实就是线程池中单个线程障处理,防止单个线程请求时间太长,导致资源长期被占有而得不到释放,从而导致线程池被快速占用完,导致服务崩溃。
Hystrix能解决如下问题:
1.请求超时降级,线程资源不足降级,降级之后可以返回自定义数据
2.线程池隔离降级,分布式服务可以针对不同的服务使用不同的线程池,从而互不影响
3.自动触发降级与恢复
4.实现请求缓存和请求合并

2、服务熔断

熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。

3、服务限流

限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。

更多详细功能请参照官网说明:https://github.com/Netflix/Hystrix/wiki/How-To-Use

2.2 基本演示

首先创建一个新的 SpringBoot 模块,然后添加依赖:

95aef70e1991977a746e747450b37bdc.png

项目创建成功后,添加如下配置,将 Hystrix 注册到 Eureka 上:

spring.application.name=hystrix
server.port=5000
eureka.client.service-url.defaultZone="http://localhost:1111/eureka"

在项目主启动类添加开启断路器注解,同时添加Restemplate

@EnableCircuitBreaker  //开启断路器
@SpringBootApplication
//@SpringCloudApplication
public class HystrixApplication {

public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}

启动类上的注解,也可以使用 @SpringCloudApplication 代替:
这样,Hystrix 的配置就算完成了。接下来提供 Hystrix 的接口:

@Service
public class HelloService {


@Autowired
RestTemplate restTemplate;

/**
* 在这个方法中,我们将发起一个远程调用,去调用 provider 中提供的 /hello 接口
* 但是,这个调用可能会失败。
* 我们在这个方法上添加 @HystrixCommand 注解,配置 fallbackMethod 属性,这个属性表示该方法调用失败时的临时替代方法
*
*/

@HystrixCommand(fallbackMethod = "error")
public String hello(){
return restTemplate.getForObject("http://provider/hello",String.class);
}


/**
* 注意,这个方法名字要和 fallbackMethod 一致
* 方法返回值也要和对应的方法一致
*/
public String error(){
return "调用失败,hystrix指向替代方法";
}

}

新建HelloController去调用service中的方法

@RestController
public class HelloController {

@Autowired
HelloService helloService;

@GetMapping("hello")
public String hello(){
return helloService.hello();
}
}

我们先演示不用hystrix的情况
启动eureka-01、provider-01、provider-02、consumer-01、四个微服务
浏览器输入http://localhost:1114/hello5,不断刷新浏览器可以看到有负载均衡功能。此时关闭任意的provider,再刷新可看到关闭的那个服务提示出错:

5cdfa016a7f382db21560c1c4a77f0bc.png
再去刷新后只能看到还能提供服务的provider。这里是因为provier将掉线的消息发送给provider存在一定的延迟,未收到消息之前(很短时间)consumer调用掉线的provider则会出现刚才的错误页面。

此时Hystrix做的事情就是在那个极短时间内不要出现那个页面,作出后备方案。启动hystrix(带有hystrix的consumer)

1c88f229e6fda40e4a37d8c279f19364.png

此时浏览器输入http://localhost:5000/hello,关闭任意一个provider,迅速刷新浏览器。ea0d01810ff17e5851c395c8a079f9a7.png6a4d0ac4940975a6abfaba5746b35108.png

一段时间过后浏览器只显示成功的页面。

3 请求命令

请求命令就是以继承类的方式来替代前面的注解方式。我们来自定义一个 HelloCommand:

public class HelloCommand extends HystrixCommand<String> {


RestTemplate restTemplate;

public HelloCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}

@Override
protected String run() {

return restTemplate.getForObject("http://provider/hello",String.class);
}
}

调用方法


@Autowired
RestTemplate restTemplate;

@GetMapping("/hello2")
public void hello2() throws Exception{
HelloCommand helloCommand = new
HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
//第一种方式执行结果
String execute = helloCommand.execute();//直接执行
System.out.println(execute);

//第二种方式执行结果 两种只能选一个,且hellocommand只能使用一次
Future<String> queue = helloCommand.queue(); //先入队
String s = queue.get();//获取请求结果
System.out.println(s);

}

注意:

  1. 一个实例只能执行一次

  2. 可以直接执行,也可以先入队,后执行

浏览器输入:http://localhost:5000/hello37cf23ac276ae299886b5de44fc09e3d9.png

PS:通过注解实现请求异步调用,首先,定义如下方法,返回 Future :
首先,定义如下方法,返回 Future :


@HystrixCommand(fallbackMethod = "error")
public Future<String> hello2() {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://provider/hello",
String.class);
}
};
}

然后,调用该方法:

   //调用使用注解方式 进行一异步调用
@GetMapping("/hello4")
public String hello4() throws Exception{
Future<String> stringFuture = helloService.hello2();
String s = stringFuture.get();
return s;
}

通过继承的方式使用 Hystrix,如何实现服务容错/降级?重写继承类的 getFallback 方法即可:

public class HelloCommand extends HystrixCommand<String> {


RestTemplate restTemplate;

DiscoveryClient discoveryClient;


public HelloCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}

//Restemplated掉用
@Override
protected String run() {

return restTemplate.getForObject("http://provider/hello", String.class);
}
//使用继承失败的处理
@Override
protected String getFallback() {
return "调用失败的方法";
}

小结:使用Hystrix可以使用注解方式和继承接口进行服务降级

  1. @HystrixCommand(fallbackMethod = "error")

  2. HelloCommand extends HystrixCommand

两者都可以实现降级处理,并且提供出错后的处理方法。

4 异常处理

就是当发起服务调用时,如果不是 provider 的原因导致请求调用失败,而是 consumer 中本身代码有问题导致的请求失败,即 consumer 中抛出了异常,这个时候,也会自动进行服务降级,只不过这个时候降级,我们还需要知道到底是哪里出异常了。如下示例代码,如果 hello 方法中,执行时抛出异常,那么一样也会进行服务降级,进入到 error 方法中,在 error 方法中,我们可以获取到异常的详细信息:

4.1 注解方式

 @HystrixCommand(fallbackMethod = "errorMsg")
public String hello11(){

int s = 1/0;
return restTemplate.getForObject("http://provider/hello", String.class);
}

public String errorMsg(Throwable t) {

return "调用失败,hystrix指向替代方法,出错信息"+t.getMessage();
}

调用接口:

    @GetMapping("/hello11")
public String hello11() throws Exception{
String s = helloService.hello11();
return s;
}

浏览器输入:http://localhost:5000/hello11603b61f8546575dc9f043e8c9e342f57.png

4.2 继承方式

继承HystrixCommand时已经提供了错误信息方法getExecutionException,调用该方法直接可以拿到Throwable对象,进而得到异常信息。

public class HelloCommand extends HystrixCommand<String> {


RestTemplate restTemplate;

DiscoveryClient discoveryClient;


public HelloCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}

public HelloCommand(Setter setter, DiscoveryClient discoveryClient) {
super(setter);
this.discoveryClient = discoveryClient;
}

protected String run() {
int i = 1/0;
return restTemplate.getForObject("http://provider/hello", String.class);
}

//使用继承失败的处理
@Override
protected String getFallback() {
return "调用失败的方法,错误信息:"+getExecutionException().getMessage();
}

我们使用继承方式测试:7da817e37b3779a5afd5c4ed4d78999b.png

4.3 忽略异常

如果是通过继承的方式来做 Hystrix,在getFallback 方法中,我们可以通过getExecutionException方法来获取执行的异常信息。另一种可能性(作为了解)。如果抛异常了,我们希望异常直接抛出,不要服务降级那么只需要配置忽略某一个异常即可:

 @HystrixCommand(fallbackMethod = "errorMsg",ignoreExceptions = ArithmeticException.class)
public String hello11(){

int s = 1/0;
return getDiscoveryClient();
}

这个配置表示当 hello 方法抛出 ArithmeticException 异常时,不要进行服务降级,直接将错误抛出。

5 请求缓存

请求缓存就是在 consumer 中调用同一个接口,如果参数相同,则可以使用之前缓存下来的数据。首先修改 provider 中的 hello2 接口,一会用来检测缓存配置是否生效:

    @GetMapping("/hello2")
public String hello2(String name) {
System.out.println(new Date() + ">>>" + name);
return "hello " + name;
}

5.1 注解方式

在 hystrix 的请求方法中,添加如下注解:

    @HystrixCommand(fallbackMethod = "errorMsg2")
@CacheResult //这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的value就是方法的返回值。
public String hello12(String name){
return restTemplate.getForObject("http://provider/hello2?name={1}", String.class,name);
}

这个配置完成后,缓存并不会生效,一般来说,我们使用缓存,都有一个缓存生命周期这样一个概念。这里也一样,我们需要初始化 HystrixRequestContext,初始化完成后,缓存开始生效,HystrixRequestContext close之后,缓存失效。

    @GetMapping("/hello12")
public void hello12() {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
String javaboy = helloService.hello12("jtp");
javaboy = helloService.hello12("jtp");
ctx.close();
}

在 ctx close 之前,缓存是有效的,close 之后,缓存就失效了。也就是说,访问一次 hello4 接口,provider 只会被调用一次(第二次使用的缓存),如果再次调用 hello4 接口,之前缓存的数据是失效的。

默认情况下,缓存的 key 就是所调用方法的参数,如果参数有多个,就是多个参数组合起来作为缓存的key。
例如如下方法:

@HystrixCommand(fallbackMethod = "error2")
@CacheResult//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,存的 value 就是方法的返回值。
public String hello3(String name,Integer age) {
return restTemplate.getForObject("http://provider/hello2?name={1}",
String.class, name);
}

此时缓存的 key 就是 name+age,但是,如果有多个参数,但是又只想使用其中一个作为缓存的 key,那么我们可以通过 @CacheKey 注解来解决。

@HystrixCommand(fallbackMethod = "error2")
@CacheResult//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参
数,缓存的 value 就是方法的返回值。
public String hello3(@CacheKey String name, Integer age) {
return restTemplate.getForObject("http://provider/hello2?name={1}",
String.class, name);
}

上面这个配置,虽然有两个参数,但是缓存时以 name 为准。也就是说,两次请求中,只要name一样,即使 age 不一样,第二次请求也可以使用第一次请求缓存的结果。另外还有一个注解叫做 @CacheRemove()。在做数据缓存时,如果有一个数据删除的方法,我们一般除了删除数据库中的数据,还希望能够顺带删除缓存中的数据,这个时候 @CacheRemove() 就派上用场了。

@CacheRemove() 在使用时,必须指定 commandKey 属性,commandKey 其实就是缓存方法的名字,指定了 commandKey,@CacheRemove 才能找到数据缓存在哪里了,进而才能成功删除掉数据。例如如下方法定义缓存与删除缓存:

@HystrixCommand(fallbackMethod = "error2")
@CacheResult//这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的 value 就是方法的返回值。
public String hello3(String name) {
return restTemplate.getForObject("http://provider/hello2?name={1}",
String.class, name);
}
@HystrixCommand
@CacheRemove(commandKey = "hello3")
public String deleteUserByName(String name) {
return null; }

再去调用:

@GetMapping("/hello4")
public void hello4() {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
//第一请求完,数据已经缓存下来了
String javaboy = helloService.hello3("javaboy");
//删除数据,同时缓存中的数据也会被删除
helloService.deleteUserByName("javaboy");
//第二次请求时,虽然参数还是 javaboy,但是缓存数据已经没了,所以这一次,provider 还是会
收到请求
javaboy = helloService.hello3("javaboy");
ctx.close();
}

5.2 继承方式

如果是继承的方式使用 Hystrix ,只需要重写 getCacheKey 方法即可:

public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
String name;
public HelloCommand(Setter setter, RestTemplate restTemplate,String name) {
super(setter);
this.name = name;
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://provider/hello2?name={1}",
String.class, name);
}
@Override
protected String getCacheKey() {
return name;
/**
* 这个方法就是请求失败的回调
*
* @return
*/
@Override
protected String getFallback() {
return "error-extends:"+getExecutionException().getMessage();
}
}

调用时候,一定记得初始化 HystrixRequestContext:

  /**
* 继承方式
* @throws Exception
*/
@GetMapping("/hello3")
public void hello13() throws Exception {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
HelloCommand helloCommand = new
HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), discoveryClient);
//第一种方式执行结果
String execute = helloCommand.execute();//直接执行
System.out.println(execute);

//第二种方式执行结果 两种只能选一个,且hellocommand只能使用一次
HelloCommand helloCommand1 = new
HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), discoveryClient);
Future<String> queue = helloCommand1.queue(); //先入队
String s = queue.get();//获取请求结果
System.out.println(s);

ctx.close();

}

6 请求合并

如果consumer中,频繁的调用 provider 中的同一个接口,在调用时,只是参数不一样,那么这样情况下,我们就可以将多个请求合并成一个,这样可以有效提高请求发送的效率。首先我们在 provider 中提供一个请求合并的接口:

 @GetMapping("/hello3/{ids}")
public List<User> hello3(@PathVariable String ids) {
String[] split = ids.split(",");
List<User> users = new ArrayList<>();

for (String id: split) {
User user = new User();
user.setId(Integer.parseInt(id));
users.add(user);
}
return users;

}

这个接口既可以处理合并之后的请求,也可以处理单个请求(单个请求的话,List 集合中就只有一项数据。)然后,在 Hystrix 中,定义 UserService:

接下来定义 UserBatchCommand ,相当于我们之前的 HelloCommand:

public class UserBatchCommand extends HystrixCommand<List<User>> {

private List<Integer> ids;
private UserService userService;

public UserBatchCommand(List<Integer> ids, UserService userService) {

super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(" batchCmd")).andCommandKey(HystrixCommandKey.Factory.asKey("batchKey")));
this.ids = ids;
this.userService = userService;
}

@Override
protected List<User> run() throws Exception {
return userService.getUsersByIds(ids);
}
}

最后,定义最最关键的请求合并方法:

/**
* 请求合并
*/
public class UserCollapseCommand extends HystrixCollapser<List<User>, User, Integer> {


private UserService userService;
private Integer id;

public UserCollapseCommand(UserService userService, Integer id) {

super(HystrixCollapser.Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("UserCollapseCommand"))
.andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(200)));
this.userService = userService;
this.id = id;
}


/**
* 请求参数
*
* @return
*/
@Override
public Integer getRequestArgument() {
return id;
}


/**
* 请求合并
*
* @param collection
* @return
*/
@Override
protected HystrixCommand<List<User>>
createCommand(Collection<CollapsedRequest<User, Integer>> collection) {
List<Integer> ids = new ArrayList<>(collection.size());
for (CollapsedRequest<User, Integer> userIntegerCollapsedRequest : collection) {
ids.add(userIntegerCollapsedRequest.getArgument());
}
return new UserBatchCommand(ids, userService);
}

/**
* 请求结果分发
* @param users
* @param collection
*/
@Override
protected void mapResponseToRequests(List<User> users,
Collection<CollapsedRequest<User, Integer>> collection) {
int count = 0;
for (CollapsedRequest<User, Integer> request : collection) {
request.setResponse(users.get(count++));
}
}

}

最后就是测试调用:

 /*********************请求合并*****************************/
@GetMapping("/hello5")
public void hello5() throws ExecutionException, InterruptedException {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
UserCollapseCommand cmd1 = new UserCollapseCommand(userService, 99);
UserCollapseCommand cmd2 = new UserCollapseCommand(userService, 98);
UserCollapseCommand cmd3 = new UserCollapseCommand(userService, 97);
UserCollapseCommand cmd4 = new UserCollapseCommand(userService, 96);
Future<User> q1 = cmd1.queue();
Future<User> q2 = cmd2.queue();
Future<User> q3 = cmd3.queue();
Future<User> q4 = cmd4.queue();
User u1 = q1.get();
User u2 = q2.get();
User u3 = q3.get();
User u4 = q4.get();
System.out.println(u1);
System.out.println(u2);
System.out.println(u3);
System.out.println(u4);
ctx.close();
}

通过注解实现请求合并


@Service
public class UserService {

@Autowired
RestTemplate restTemplate;

/******注解方式*******/

@HystrixCollapser(batchMethod = "getUsersByIds", collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "200")})
public Future<User> getUserById(Integer id) {
return null;

}
@HystrixCommand
public List<User> getUsersByIds1(List<Integer> ids) {
User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
}

这里的核心是 @HystrixCollapser 注解。在这个注解中,指定批处理的方法即可。测试代码如下:

    @GetMapping("/hello5")
public void hello6() throws ExecutionException, InterruptedException {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
Future<User> q2 = userService.getUserById(98);
Future<User> q3 = userService.getUserById(99);
Future<User> q4 = userService.getUserById(95);
User u2 = q2.get();
User u3 = q3.get();
User u4 = q4.get();

System.out.println(u2);
System.out.println(u3);
System.out.println(u4);
Thread.sleep(2000);

Future<User> q1 = userService.getUserById(91);
User u1 = q1.get();
System.out.println(u1);
ctx.close();
}

谢谢观看哦?‍♀️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值