spring-retry重试与熔断详解

本文介绍 Spring Retry 项目的重试和熔断功能,包括 RetryOperations 接口、RetryTemplate 模板实现、重试策略及退避策略等。并探讨了有状态与无状态重试的区别及熔断器策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转至:http://www.broadview.com.cn/article/233


本文是《亿级流量》第6章 超时与重试机制补充内容。

spring-retry项目实现了重试和熔断功能,目前已用于SpringBatch、Spring Integration等项目。

RetryOperations定义了重试的API,RetryTemplate提供了模板实现,线程安全的,同于Spring 一贯的API风格,RetryTemplate将重试、熔断功能封装到模板中,提供健壮和不易出错的API供大家使用。

首先,RetryOperations接口API:

public interface RetryOperations {
   <T, E extends Throwable>T execute(RetryCallback<T, E>retryCallback) throws E;
   <T, E extends Throwable>T execute(RetryCallback<T, E>retryCallback, RecoveryCallback<T> recoveryCallback) throws E;
   <T, E extends Throwable>T execute(RetryCallback<T, E>retryCallback, RetryState retryState) throws E, ExhaustedRetryException;
   <T, E extends Throwable>T execute(RetryCallback<T, E>retryCallback, RecoveryCallback<T> recoveryCallback, RetryStateretryState)
         throws E;
}

通过RetryCallback定义需重试的业务服务,当重试超过最大重试时间或最大重试次数后可以调用RecoveryCallback进行恢复,比如返回假数据或托底数据。

那什么时候需重试?spring-retry是当抛出相关异常后执行重试策略,定义重试策略时需要定义需重试的异常(如因远程调用失败的可以重试、而因入参校对失败不应该重试)。只读操作可以重试,幂等写操作可以重试,但是非幂等写操作不能重试,重试可能导致脏写,或产生重复数据。

重试策略有哪些呢?spring-retry提供了如下重试策略。

RetryPolicy提供了如下策略实现:

  • NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试;
  • AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环;
  • SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略;
  • TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试;
  • CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate,稍后详细介绍该策略;
  • CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行。

重试时的退避策略是什么?是立即重试还是等待一段时间后重试,比如是网络错误,立即重试将导致立即失败,最好等待一小段时间后重试,还要防止很多服务同时重试导致DDos。

BackOffPolicy 提供了如下策略实现:

  • NoBackOffPolicy:无退避算法策略,即当重试时是立即重试;
  • FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒;
  • UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒;
  • ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier;
  • ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。

到此基本的概念就讲完了。接下来先看下RetryTemplate主要流程实现:

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
      RecoveryCallback<T> recoveryCallback, RetryState state)
      throws E, ExhaustedRetryException {
   //重试策略
   RetryPolicy retryPolicy = this.retryPolicy;
   //退避策略
   BackOffPolicy backOffPolicy = this.backOffPolicy;
   //重试上下文,当前重试次数等都记录在上下文中
   RetryContext context = open(retryPolicy, state);
   try {
      //拦截器模式,执行RetryListener#open
      boolean running = doOpenInterceptors(retryCallback, context);
      //判断是否可以重试执行
      while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
         try {//执行RetryCallback回调
            return retryCallback.doWithRetry(context);
         } catch (Throwable e) {//异常时,要进行下一次重试准备
            //遇到异常后,注册该异常的失败次数
            registerThrowable(retryPolicy, state, context, e);
            //执行RetryListener#onError
            doOnErrorInterceptors(retryCallback, context, e);
            //如果可以重试,执行退避算法,比如休眠一小段时间后再重试
            if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
               backOffPolicy.backOff(backOffContext);
            }
            //state != null && state.rollbackFor(context.getLastThrowable())
            //在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常
            if (shouldRethrow(retryPolicy, context, state)) {
               throw RetryTemplate.<E>wrapIfNecessary(e);
            }
         }
         //如果是有状态重试,且有GLOBAL_STATE属性,则立即跳出重试终止;当抛出的异常是非需要执行回滚操作的异常时,才会执行到此处,CircuitBreakerRetryPolicy会在此跳出循环;
         if (state != null && context.hasAttribute(GLOBAL_STATE)) {
            break;
         }
      }
      //重试失败后,如果有RecoveryCallback,则执行此回调,否则抛出异常
      return handleRetryExhausted(recoveryCallback, context, state);
   } catch (Throwable e) {
      throw RetryTemplate.<E>wrapIfNecessary(e);
   } finally {
      //清理环境
      close(retryPolicy, context, state, lastException == null || exhausted);
      //执行RetryListener#close,比如统计重试信息
      doCloseInterceptors(retryCallback, context, lastException);
   }
}

有状态or无状态

无状态重试,是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中,在一次调用中进行完整的重试策略判断。

非常简单的情况,如远程调用某个查询方法时是最常见的无状态重试。

RetryTemplate template = new RetryTemplate();
//重试策略:次数重试策略
RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
template.setRetryPolicy(retryPolicy);
//退避策略:指数退避策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(100);
backOffPolicy.setMaxInterval(3000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setSleeper(new ThreadWaitSleeper());
template.setBackOffPolicy(backOffPolicy);

//当重试失败后,抛出异常
String result = template.execute(new RetryCallback<String, RuntimeException>() {
    @Override
    public String doWithRetry(RetryContext context) throws RuntimeException {
        throw new RuntimeException("timeout");
    }
});
//当重试失败后,执行RecoveryCallback
String result = template.execute(new RetryCallback<String, RuntimeException>() {
    @Override
    public String doWithRetry(RetryContext context) throws RuntimeException {
        System.out.println("retry count:" + context.getRetryCount());
        throw new RuntimeException("timeout");
    }
}, new RecoveryCallback<String>() {
    @Override
    public String recover(RetryContext context) throws Exception {
        return "default";
    }
});

有状态重试,有两种情况需要使用有状态重试,事务操作需要回滚或者熔断器模式。

事务操作需要回滚场景时,当整个操作中抛出的是数据库异常DataAccessException,则不能进行重试需要回滚,而抛出其他异常则可以进行重试,可以通过RetryState实现:

//当前状态的名称,当把状态放入缓存时,通过该key查询获取
Object key = "mykey";
//是否每次都重新生成上下文还是从缓存中查询,即全局模式(如熔断器策略时从缓存中查询)
boolean isForceRefresh = true;
//对DataAccessException进行回滚
BinaryExceptionClassifier rollbackClassifier =
        new BinaryExceptionClassifier(Collections.<Class<? extends Throwable>>singleton(DataAccessException.class));
RetryState state = new DefaultRetryState(key, isForceRefresh, rollbackClassifier);

String result = template.execute(new RetryCallback<String, RuntimeException>() {
    @Override
    public String doWithRetry(RetryContext context) throws RuntimeException {
        System.out.println("retry count:" + context.getRetryCount());
        throw new TypeMismatchDataAccessException("");
    }
}, new RecoveryCallback<String>() {
    @Override
    public String recover(RetryContext context) throws Exception {
        return "default";
    }
}, state);

RetryTemplate中在有状态重试时,回滚场景时直接抛出异常处理代码:

//state != null && state.rollbackFor(context.getLastThrowable())
//在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常
if (shouldRethrow(retryPolicy,context, state)) {
    throw RetryTemplate.<E>wrapIfNecessary(e);
}

熔断器场景。在有状态重试时,且是全局模式,不在当前循环中处理重试,而是全局重试模式(不是线程上下文),如熔断器策略时测试代码如下所示。

RetryTemplate template = new RetryTemplate();
CircuitBreakerRetryPolicy retryPolicy =
        new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3));
retryPolicy.setOpenTimeout(5000);
retryPolicy.setResetTimeout(20000);
template.setRetryPolicy(retryPolicy);

for (int i = 0; i < 10; i++) {
    try {
        Object key = "circuit";
        boolean isForceRefresh = false;
        RetryState state = new DefaultRetryState(key, isForceRefresh);
        String result = template.execute(new RetryCallback<String, RuntimeException>() {
            @Override
            public String doWithRetry(RetryContext context) throws RuntimeException {
                System.out.println("retry count:" + context.getRetryCount());
                throw new RuntimeException("timeout");
            }
        }, new RecoveryCallback<String>() {
            @Override
            public String recover(RetryContext context) throws Exception {
                return "default";
            }
        }, state);
        System.out.println(result);
    } catch (Exception e) {
        System.out.println(e);
    }
}

为什么说是全局模式呢?我们配置了isForceRefresh为false,则在获取上下文时是根据key “circuit”从缓存中获取,从而拿到同一个上下文。

Object key = "circuit";
boolean isForceRefresh = false;
RetryState state = new DefaultRetryState(key,isForceRefresh);

如下RetryTemplate代码说明在有状态模式下,不会在循环中进行重试。
if (state != null && context.hasAttribute(GLOBAL_STATE)) {
   break;
}

熔断器策略配置代码,CircuitBreakerRetryPolicy需要配置三个参数:

  • delegate:是真正判断是否重试的策略,当重试失败时,则执行熔断策略;
  • openTimeout:openWindow,配置熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路);
    resetTimeout:timeout,配置重置熔断器重新闭合的超时时间。

判断熔断器电路是否打开的代码:

public boolean isOpen() {
   long time = System.currentTimeMillis() - this.start;
   boolean retryable = this.policy.canRetry(this.context);
   if (!retryable) {//重试失败
      //在重置熔断器超时后,熔断器器电路闭合,重置上下文
      if (time > this.timeout) {
         this.context = createDelegateContext(policy, getParent());
         this.start = System.currentTimeMillis();
         retryable = this.policy.canRetry(this.context);
      } else if (time < this.openWindow) {
         //当在熔断器打开状态时,熔断器电路打开,立即熔断
         if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {
            setAttribute(CIRCUIT_OPEN, true);
         }
         this.start = System.currentTimeMillis();
         return true;
      }
   } else {//重试成功
      //在熔断器电路半打开状态时,断路器电路闭合,重置上下文
      if (time > this.openWindow) {
         this.start = System.currentTimeMillis();
         this.context = createDelegateContext(policy, getParent());
      }
   }
   setAttribute(CIRCUIT_OPEN, !retryable);
   return !retryable;
}

从如上代码可看出spring-retry的熔断策略相对简单:

  • 当重试失败,且在熔断器打开时间窗口[0,openWindow) 内,立即熔断;
  • 当重试失败,且在指定超时时间后(>timeout),熔断器电路重新闭合;
  • 在熔断器半打开状态[openWindow, timeout] 时,只要重试成功则重置上下文,断路器闭合。

CircuitBreakerRetryPolicy的delegate应该配置基于次数的SimpleRetryPolicy或者基于超时的TimeoutRetryPolicy策略,且策略都是全局模式,而非局部模式,所以要注意次数或超时的配置合理性。

比如SimpleRetryPolicy配置为3次,openWindow=5s,timeout=20s,我们来看下CircuitBreakerRetryPolicy的极端情况。

特殊时间序列:

  • 1s:retryable=false,重试失败,断路器电路处于打开状态,熔断,重置start时间为当前时间;
  • 2s:retryable=false,重试失败,断路器电路处于打开状态,熔断,重置start时间为当前时间;
  • 7s:retryable=true,表示可以重试,但是time=5s,time > this.openWindow判断为false,CIRCUIT_OPEN=false,不熔断;此时重试次数=3,等于最大重试次数了;
  • 10s:retryable=false,因重试次数>3,time=8s,time < this.openWindow判断为false,熔断,且在timeout超时之前都处于熔断状态,这个时间段要配置好,否则熔断的时间会太长(默认timeout=20s);
  • (7s,20s]之间的所有重试:和10s的情况一样。

如上是当重试次数正好等于最大重试次数,且time=openWindow时的特殊情况,不过实际场景这种情况几乎不可能发生。

spring-retry的重试机制没有像Hystrix根据失败率阀值进行电路打开/关闭的判断。

如果需要局部循环重试机制,需要组合多个RetryTemplate实现。

spring-retry也提供了注解实现:
@EnableRetry@Retryable@Recover@Backoff@CircuitBreaker。具体可以参考官方文档。

统计分析

spring-retry通过RetryListener实现拦截器模式,默认提供了StatisticsListener实现重试操作统计分析数据。

RetryTemplatetemplate = new RetryTemplate();
DefaultStatisticsRepository repository = new DefaultStatisticsRepository();
StatisticsListener listener = new StatisticsListener(repository);
template.setListeners(new RetryListener[]{listener});

for (int i = 0; i < 10; i++){
    String result = template.execute(new RetryCallback<String, RuntimeException>() {
        @Override
       public String doWithRetry(RetryContext context) throws RuntimeException {
           context.setAttribute(RetryContext.NAME,"method.key");
            return "ok";
        }
    });
}
RetryStatistics statistics = repository.findOne("method.key");
System.out.println(statistics);

此处要给操作定义一个name如“method.key”,从而查询该操作的统计分析数据。

到此spring-retry重试与熔断就介绍完了。spring-retry项目地址https://github.com/spring-projects/spring-retry。

另外可以参考《亿级流量网站架构核心技术》的《第5章 降级特技》和《第6章 超时与重试机制》了解和学习更多内容。


<think>我们正在讨论Java中实现重试机制的中间件或解决方案。根据提供的引用,我们可以看到三种不同的重试机制实现方式:1.引用[1]展示了使用SpringRetry注解的方式(@Retryable)在方法级别配置重试。2.引用[2]提到RocketMQ的消息重试机制,这属于消息中间件自带的重试能力。3.引用[3]描述了Dubbo在服务调用时的重试机制(FailoverCluster)。用户要求关于Java实现重试机制的中间件或解决方案的示例。因此,我们可以重点介绍以下几种常见的实现方式:###1.SpringRetrySpringRetry提供了一种声明式的重试支持,通过注解或编程方式实现。示例代码如引用[1]所示。###2.消息中间件(如RocketMQ)自带的重试消息中间件通常会在消息发送或消费失败时进行重试。引用[2]中提到的RocketMQ重试机制是消息队列层面的一种重试方案。###3.RPC框架(如Dubbo)的重试引用[3]中的Dubbo重试机制是RPC调用层面的重试,通过集群容错策略(如Failover)实现。此外,还有其他的重试中间件和解决方案,例如:###4.Resilience4jResilience4j是一个轻量级的容错库,提供了重试、断路器等功能。它比Hystrix更轻量,且支持函数式编程。###5.GuavaRetryingGuavaRetrying是一个基于Guava的重试工具库,提供了更灵活的重试策略配置。下面,我将给出SpringRetry和Resilience4j的两个具体代码示例。###示例1:使用SpringRetry实现重试首先,确保在项目中添加了SpringRetry的依赖(如果是SpringBoot项目,可以使用spring-boot-starter-aop):```xml<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>```然后,在启动类上添加`@EnableRetry`注解启用重试:```java@SpringBootApplication@EnableRetrypublicclassMyApplication{publicstaticvoidmain(String[]args){SpringApplication.run(MyApplication.class,args);}}```接下来,在需要重试的方法上使用`@Retryable`注解:```java@ServicepublicclassMyService{@Retryable(value={RemoteAccessException.class},maxAttempts=3,backoff=@Backoff(delay=1000,multiplier=1.5))publicvoidcallService(){//业务逻辑,可能会抛出RemoteAccessException//...System.out.println("尝试调用服务...");thrownewRemoteAccessException("模拟调用失败");}//当所有重试都失败后,可以定义一个恢复方法(可选)@Recoverpublicvoidrecover(RemoteAccessExceptione){System.out.println("最终重试失败,执行恢复操作:"+e.getMessage());}}```###示例2:使用Resilience4j实现重试添加Resilience4j的依赖:```xml<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-retry</artifactId></dependency>```然后,使用RetryRegistry创建Retry对象,并使用它来装饰方法调用:```javaimportio.github.resilience4j.retry.Retry;importio.github.resilience4j.retry.RetryConfig;importjava.time.Duration;importjava.util.function.Supplier;publicclassResilience4jRetryExample{publicstaticvoidmain(String[]args){//创建Retry配置RetryConfigconfig=RetryConfig.custom().maxAttempts(3).waitDuration(Duration.ofMillis(1000)).retryOnException(e->einstanceofRemoteAccessException).build();//创建RetryRetryretry=Retry.of("backendService",config);//创建模拟业务逻辑Supplier<String>supplier=()->{System.out.println("尝试调用服务...时间:"+java.time.LocalTime.now());thrownewRemoteAccessException("模拟调用失败");};//使用Retry装饰业务逻辑Supplier<String>decorated=Retry.decorateSupplier(retry,supplier);try{decorated.get();}catch(Exceptione){System.out.println("最终重试失败:"+e.getMessage());}}}classRemoteAccessExceptionextendsRuntimeException{publicRemoteAccessException(Stringmessage){super(message);}}```###总结Java实现重试机制的中间件和解决方案有多种,包括:-SpringRetry:基于SpringAOP,注解方式使用简单。-Resilience4j:轻量级容错库,支持函数式编程,功能丰富。-GuavaRetrying:基于Guava,灵活但需要手动集成。-RPC框架(如Dubbo)和消息中间件(如RocketMQ)也提供了内置的重试机制。用户可以根据项目需求选择合适的方案。对于Spring项目,SpringRetry或Resilience4j都是不错的选择。§§相关问题§§1.SpringRetry和Resilience4j在重试机制上有什么主要区别?2.如何在Dubbo中自定义重试策略?3.RocketMQ的消息重试机制是如何工作的?</think> 以下是关于Java实现重试机制的中间件解决方案详解,包含实际代码示例: ### 一、主流重试方案及示例 #### 1. Spring Retry(声明式重试) ```java // 1. 添加依赖 // spring-boot-starter-aop + spring-retry // 2. 启用重试 @SpringBootApplication @EnableRetry // 关键注解 public class App { ... } // 3. 方法级配置 @Service public class PaymentService { @Retryable( value = {TimeoutException.class}, // 触发重试的异常 maxAttempts = 3, // 最大重试次数 backoff = @Backoff(delay = 1000, multiplier = 2) // 退避策略:初始1s, 乘数2 ) public String processPayment() throws TimeoutException { log.info("尝试支付请求..."); throw new TimeoutException("第三方支付超时"); } @Recover // 全部重试失败后执行 public String fallback(TimeoutException e) { return "支付失败,请人工处理"; } } ``` #### 2. Resilience4j(响应式重试) ```java // 1. 添加依赖 implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' // 2. 配置重试策略 RetryConfig config = RetryConfig.custom() .maxAttempts(3) .waitDuration(Duration.ofSeconds(2)) .retryExceptions(IOException.class) .failAfterMaxAttempts(true) // 达到最大次数后抛出异常 .build(); Retry retry = Retry.of("databaseRetry", config); // 3. 使用装饰器 CheckedFunction0<String> decorated = Retry .decorateCheckedSupplier(retry, () -> { // 数据库操作逻辑 if (Math.random() > 0.7) throw new IOException("DB连接失败"); return "查询成功"; }); Try.of(decorated) .onFailure(e -> log.error("最终失败: {}", e.getMessage())); ``` #### 3. 消息队列重试(RocketMQ方案) ```java // 生产者端(自动重试) Message msg = new Message("PAY_TOPIC", "支付数据".getBytes()); // 设置同步发送重试次数 producer.setRetryTimesWhenSendFailed(3); SendResult sendResult = producer.send(msg); // 消费者端(异常重试) consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { try { // 业务处理... return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } catch (Exception e) { // 失败后返回RECONSUME_LATER触发重试 return ConsumeConcurrentlyStatus.RECONSUME_LATER; } }); ``` #### 4. Dubbo服务调用重试 ```xml <!-- 服务消费方配置 --> <dubbo:reference id="userService" interface="com.example.UserService"> <!-- 失败自动切换 + 重试3次 --> <dubbo:method name="queryUser" retries="3" cluster="failover"/> </dubbo:reference> ``` ### 二、关键机制对比 | 方案 | 触发方式 | 退避策略 | 集成复杂度 | 适用场景 | |-------------|------------|-------------|-----------|---------------------| | Spring Retry | 声明式注解 | 指数/固定延迟 | ★★☆ | Spring应用、服务方法调用 | | Resilience4j| 编程式API | 自定义策略 | ★★★ | 复杂流控、函数式编程 | | RocketMQ | 消息队列机制 | 渐进延迟 | ★★★☆ | 异步消息处理 | | Dubbo | RPC框架内置 | 立即重试 | ★☆ | 分布式服务调用 | ### 三、最佳实践建议 1. **重试幂等性** 确保被重试的操作具备幂等性(如数据库唯一索引、状态机检查) 2. **退避策略选择** ```java // Exponential backoff示例(Resilience4j) .waitDuration(Duration.ofMillis(500)) .exponentialBackoff(1.5) // 延迟时间指数增长 ``` 3. **熔断结合** 重试机制需配合熔断使用(如Resilience4j CircuitBreaker),防止雪崩 4. **监控告警** 记录重试日志并设置阈值告警: ```java retry.getEventPublisher() .onRetry(e -> log.warn("重试事件: {}", e)); ``` > 💡 **提示**:Spring生态推荐Spring Retry;需复杂容错选Resilience4j;MQ场景用RocketMQ内置机制;分布式服务调用使用Dubbo重试[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值