SpringBoot +Redis +RabbitMQ 实现高并发限时秒杀

SpringBoot +Redis +RabbitMQ 实现高并发限时秒杀


提示:以下是本篇文章正文内容,下面案例可供参考

一、软件安装

1.安装RabbitMQ

docker安装:docker安装RabbitMQ_liangshitian的博客-CSDN博客

windows安装:windows安装rabbitmq安装详细步骤_青蛙与大鹅的博客-CSDN博客_window安装rabbitmq

2.安装Redis

docker安装:https://blog.csdn.net/qq_33612228/article/details/10360918

windows安装:Windows 64位下安装Redis 以及 可视化工具Redis Desktop Manager的安装和使用_零碎de記憶的博客-CSDN博客_redis可视化工具下载

springboot整合redis:SpringBoot整合Redis_liangshitian的博客-CSDN博客

3.安装 Jmeter测试工具

windows安装:Jmeter安装教程_liuyanh2006的博客-CSDN博客

4.安装工具包:我的资源; rabbitmq+Erlang工具+压力测试jmeter-Java文档类资源-CSDN下载otp_win64_24.3.3.exe+rabbitmq-server-3.9.15.exe+ap更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/jlonghou/85195659

二、详细

1.思路

  1. 在用户发起秒杀访问时,先访问本地已经初始化好的map,看当前秒杀商品id的库存是否已售罄,若已售罄,直接返回秒杀结束异常,若库存还有,在执行下面的操作。通过内存标记可以减少对后面步骤中的redis访问操作,降低redis的压力,不然每个请求都将访问一次redis
  2. 系统启动时,即将商品和库存数据初始化到redis中,所有的抢购操作都在redis中进行处理,通过Redis预减少库存来减少数据库访问
  3. 通过使用RabbitMQ用异步队列处理下单,实现系统高响应。此处响应客户端后,一般都是抢购成功了,当然不排除例外,此时客户端通过ajax请求轮询访问下单结果接口,直到响应状态成功或者失败

2.数据库设计

         1.商品库存表:stock表      

 CREATE TABLE `stock` (
  `id` varchar(64) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `stock` varchar(255) DEFAULT NULL,
  `remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) NOT NULL DEFAULT '',
  `create_by` varchar(64) NOT NULL DEFAULT '',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='商品库存表';

        2.秒杀订单表:t_order表

 CREATE TABLE `t_order` (
  `id` varchar(64) NOT NULL,
  `order_name` varchar(255) DEFAULT NULL,
  `order_user` varchar(255) DEFAULT NULL,
  `remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
  `create_date` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) NOT NULL DEFAULT '',
  `create_by` varchar(64) NOT NULL DEFAULT '',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='秒杀订单表';

3.代码参考

1.pom引入

      <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

 2.配置application.yml

server:
  port: 8090

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/rabbitmq?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: root
    # 使用Druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      filters: stat
      maxActive: 20
      initialSize: 1
      maxWait: 60000
      minIdle: 1
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxOpenPreparedStatements: 20
  data:
    redis:
      repositories:
        enabled: false
  redis:
    database: 0   # redis数据库索引(默认为0),我们使用索引为其他(0-15)的数据库,避免和其他数据库冲突
    host: 127.0.0.1
    port: 6379
    password:

#  4369 -- erlang发现口
#
#  5672 --client端通信口
#
#  15672 -- 管理界面ui端口 http://localhost:15672/
#
#  25672 -- server间内部通信口

  rabbitmq:  #mq配置
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest

logging:
  config: classpath:logback-spring.xml
 3.实体类 Order.java+Stock 商品库存表
/**
 * @Description: 商品库存表
 */
@Data
@TableName("t_order")
public class Order {

    @TableId(type = IdType.AUTO)
    private Integer id;

    @TableField("order_name")
    private String orderName;


    @TableField("order_user")
    private String orderUser;


    @TableField("create_by")
    private String createBy;


    @TableField("update_by")
    private String updateBy;


    @TableField("create_date")
    private Date createDate;

    @TableField("update_date")
    private Date updateDate;

    @TableField("del_flag")
    private String delFlag;
}
 
/**
 * @Description: 商品库存表

 */
@TableName("stock")
@Data
public class Stock {
    @TableId(type = IdType.AUTO )
    private Integer id;
    /**
     * 产品名称
     */
    @TableField("name")
    private String name;


    /**
     * 存货
     */
    @TableField("stock")
    private Integer stock;
}

4.服务层

StockService存货服务层+OrderService订单服务层

import com.orange.entity.Stock;

import java.util.List;

/**
 * 存货服务层
 */
public interface StockService {
    /**
     * 秒杀商品后-减少库存
     * @param name 商品名称
     */
    int decrByStock(String name);


    /**
     * 秒杀商品列表
     * @return List<Stock>
     */
    List<Stock> selectList();

}

import com.orange.entity.Order;
import org.springframework.stereotype.Service;

/**
 * 订单服务层
 */
@Service
public interface OrderService {
    /**
     * 订单保存
     * @param order 实体
     */
    int saveOrder(Order order);
}
  OrderServiceImpl订单实现层+StockServiceImpl存货实现层 
package com.example.rabbit.service.imp;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.rabbit.entity.Stock;
import com.example.rabbit.exception.CustomException;
import com.example.rabbit.mapper.StockMapper;
import com.example.rabbit.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Author HOUJL
 * @Date 2022/4/21
 * @Description:
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {
    @Autowired
    private StockMapper stockMapper;

    @Override
    public int decrByStock(String name) {
        Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().lambda().eq(Stock::getName,name));
        stock.setStock(stock.getStock()-1);
        int i = stockMapper.updateById(stock);
        if( i<= 0){
            throw new CustomException("减少库存失败");
        }
        return i;
    }

    @Override
    public List<Stock> selectList() {
        return stockMapper.selectList(new QueryWrapper<>());
    }
}


package com.example.rabbit.service.imp;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.rabbit.entity.Order;
import com.example.rabbit.exception.CustomException;
import com.example.rabbit.mapper.OrderMapper;
import com.example.rabbit.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author HOUJL
 * @Date 2022/4/21
 * @Description:
 */
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Autowired
    OrderMapper orderMapper;

    @Override
    public int saveOrder(Order order) {
        int i = orderMapper.insert(order);
        if (i <= 0) {
            throw new CustomException("保存订单失败");
        }
        return i;
    }
}

5.配置rabbitmq的实现方式以及redis的实现方式

  1)在 service包下面直接新建 MQOrderServiceImpl.java,这个类属于订单的消费队列。

        

package com.example.rabbit.service.imp;

import com.example.rabbit.config.RabbitMqConfig;
import com.example.rabbit.entity.Order;
import com.example.rabbit.service.OrderService;
import com.example.rabbit.service.StockService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Date;

/**
 * @Author HOUJL
 * @Date 2022/4/21
 * @Description: 消费消息
 */
@Service
@Slf4j
public class MqOrderServiceImpl {

    private final OrderService orderService;
    private final StockService stockService;

    public MqOrderServiceImpl(OrderService orderService, StockService stockService) {
        this.orderService = orderService;
        this.stockService = stockService;
    }

    /**
     * MQ监听订单消息队列,并消费
     * @param order
     */
    @RabbitListener(queues = RabbitMqConfig.ORDER_QUEUE, containerFactory = "rabbitListenerContainerFactory")
    @Transactional(rollbackFor = Exception.class)
    public void saveOrder(Message message, Order order, Channel channel) throws IOException {
        log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        /**
         * 调用数据库orderService创建订单信息
         */
        order.setCreateBy(order.getOrderUser());
        order.setCreateDate(new Date());
        order.setUpdateBy(order.getOrderUser());
        order.setUpdateDate(new Date());
        order.setDelFlag("0");
        int i = orderService.saveOrder(order);
        int j = stockService.decrByStock(order.getOrderName());
        if (i>0 && j>0){
            //消费成功
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
            log.info("消费订单成功,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        }else {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
            log.info("消费订单失败,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
        }
    }
}

  2)MQStockServiceImpl.java这个属于库存得消费队列。

        

package com.example.rabbit.service.imp;

import com.example.rabbit.config.RabbitMqConfig;
import com.example.rabbit.entity.Order;
import com.example.rabbit.utils.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
 * @Author HOUJL
 * @Date 2022/4/21
 * @Description: 生产消息
 */
@Service
@Slf4j
public class MqStockServiceImpl {
    private final RedisCache redisCache;
    private final RabbitTemplate rabbitTemplate;

    public MqStockServiceImpl(RedisCache redisCache, RabbitTemplate rabbitTemplate) {
        this.redisCache = redisCache;
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 使用redis+消息队列进行秒杀实现
     *
     * @param userName  用户名称
     * @param stockName 商品名称
     * @return String
     */
    public String secKill(String userName, String stockName) {
        log.info("参加秒杀的用户是:{},秒杀的商品是:{}", userName, stockName);
        String message = "";
        //redis中key对应的value减一
        Long decrByResult = redisCache.decrBy(stockName);
        if (Objects.nonNull(decrByResult) && decrByResult >= 0) {
            /**
            * 说明该商品的库存量有剩余,可以进行下订单操作
            */
            log.info("用户:{}, 秒杀该商品:{},库存余量{},可以进行下订单操作", userName, stockName, decrByResult);
            //1.发消息给订单消息队列,创建订单 2.发消息给库存消息队列,将库存数据减一 3.将订单保存到redis 实现限购功能
            Order order = new Order();
            order.setOrderUser(userName);
            order.setOrderName(stockName);
            rabbitTemplate.convertAndSend(RabbitMqConfig.ORDER_EXCHANGE,RabbitMqConfig.ORDER_ROUTING_KEY,order);
            message = "用户" + userName + "秒杀" + stockName + "成功";
            limitNumber(userName,stockName);
        } else {
            /**
             * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
             */
            log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", userName);
            message = "用户:" + userName + "商品的库存量没有剩余,秒杀结束";
        }
        return message;
    }

    private void limitNumber(String userName, String stockName) {
        String key = userName + ":" + stockName + ":number";
        redisCache.incrBy(key);
    }
}

6.RabbitMqConfig 和redisUtil工具类

  1.RabbitMqConfig.java

        

package com.example.rabbit.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @Author HOUJL
 * @Date 2022/4/21
 * @Description: RabbitMQConfig插件配置
 */
@Configuration
public class RabbitMqConfig {

    /**
     * 库存交换机
     */

    public static final String STORY_EXCHANGE = "STORY_EXCHANGE";

    /**
     * 订单交换机
     */
    public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";

    /**
     * 库存队列
     */
    public static final String STORY_QUEUE = "STORY_QUEUE";

    /**
     * 订单队列
     */
    public static final String ORDER_QUEUE = "ORDER_QUEUE";

    /**
     * 库存路由键
     */
    public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";

    /**
     * 订单路由键
     */
    public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";

    /**
     *
     * @param connectionFactory
     * @return SimpleRabbitListenerContainerFactory
     */
    @Bean(name = "rabbitListenerContainerFactory")
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        //手动确认消息
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        //消费数量
        factory.setPrefetchCount(50);
        return factory;
    }

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 创建库存交换机
     * @return
     */
    @Bean
    public Exchange getStoryExchange() {
        return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
    }

    /**
     * 创建库存队列
     * @return
     */
    @Bean
    public Queue getStoryQueue() {
        return new Queue(STORY_QUEUE,true);
    }

    /**
     * 库存交换机和库存队列绑定
     * @return
     */
    @Bean
    public Binding bindStory() {
        return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
    }

    /**
     * 创建订单队列
     * @return
     */
    @Bean
    public Queue getOrderQueue() {
        return new Queue(ORDER_QUEUE);
    }

    /**
     * 创建订单交换机
     * @return
     */
    @Bean
    public Exchange getOrderExchange() {
        return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
    }

    /**
     * 订单队列与订单交换机进行绑定
     * @return
     */
    @Bean
    public Binding bindOrder() {
        return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
    }

}

         2.RedisCacheConfig.java

                

package com.example.rabbit.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

         3.RedisUtil.java部分代码

        

/**
     * 对指定key的键值减一
     * @param key 键
     * @return Long
     */
    public Long decrBy(String key) {
        return redisTemplate.opsForValue().decrement(key);
    }

7.controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象。

        

import com.orange.annotation.AccessLimit;
import com.orange.annotation.LimitNumber;
import com.orange.service.impl.MqStockServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
@Api(value = "SecKillController",  tags = "秒杀控制层")
@RequestMapping("/seck")
public class SecKillController {

    @Autowired
    private MqStockServiceImpl mqStockService;


    /**
     * 使用redis+消息队列进行秒杀实现
     * @param userName 用户名称
     * @param stockName 商品名称
     * @return String
     */
    @PostMapping(value = "/secKill")
    @ApiOperation(value = "redis+消息队列进行秒杀实现", notes = "redis+消息队列进行秒杀实现")
    @LimitNumber(value = 2)
    @AccessLimit(seconds = 1,maxCount = 800)
    public String secKill(@RequestParam(value = "userName") String userName, @RequestParam(value = "stockName") String stockName) {
        return mqStockService.secKill(userName, stockName);
    }

}

8.需要在springboot得启动类中进行对redis得初始化,简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息。

        

package com.example.rabbit;

import com.example.rabbit.entity.Stock;
import com.example.rabbit.service.StockService;
import com.example.rabbit.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.List;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class RabbitApplication implements ApplicationRunner {

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


    @Autowired
    private RedisCache redisCache;
    @Autowired
    private StockService stockService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<Stock> stockList = stockService.selectList();
        for (Stock stockItem : stockList) {
            redisCache.setCacheObject(stockItem.getName(),stockItem.getStock(),3600, TimeUnit.SECONDS);
        }
    }
}

6.redis+RabbitMQ测试

  1.项目启动时,redis里的watch会初始化10。

 2.打开我们得JMeter工具运行测试(具体使用Jmeter可自行百度)

      1)选择中文

       

   2)完成中文之后,我们在测试计划右键,添加一个线程组。

       

  3)给这个线程组的数量为40,这个线程组的作用就是模拟40个用户发送请求,去秒杀;然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求的组件了

  

4)这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数
点击上方得白色小书本,选择random,1-99得随机数。

5)然后我们把这个函数字符串复制到http得参数上面去。

最后点击运行按钮运行。

5)查看控制台日志,可以看到运行结果已经打印到控制台了,用户名为我们生成的随机数。

再来看下数据库订单表t_order,就保存了10条数据(秒杀成功的),我们初始化的时候给watch库存得数量为10,而我们使用JMeter模拟了40个人发请求,所以这10条数据,也就是40个用户中抢到商品的10个人,也就是线程,谁抢到就是谁得。

6)再来查看下我们得结果树

源码:


总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 6
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值