java实现高并发秒杀---简单示例 springboot+redis+rabbitmq+mybatis

前言

​ 为什么要写秒杀系统呢,像现在限时秒杀已经成为电商项目中的一种常态,像现在的一些龙头电商企业都具备限时秒杀活动,这样能调动用户的购买兴趣的活动,能够给项目带来更大的活力,所以对于这个秒杀功能不说是一定能写,但是一定要了解他。

原理

秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。

对于系统并发量变大问题,这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此这种做法刚好适用。

那么具体是如何来减少对数据库的访问的呢?

假如,某个商品可秒杀的数量是10,那么在秒杀活动开始之前,把商品的ID和数量加载到Redis缓存。当服务端收到请求时,首先预减Redis中的数量,如果数量减到小于0时,那么随后的访问直接返回秒杀失败的信息。也就是说,最终只有10个请求会去访问数据库。

如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。

这样处理以后,我们的应用是可以很简单的进行分布式横向扩展的,以应对更大的并发。

工具

本次使用的开发工具IDEARedisRabittMQMySql

关于安装Redis工具请参考博客redis安装

关于安装RabittMQ工具请参考博客win10安装rabbitmq

开发环境

使用的开发技术包括

  • SpringBoot 2.2.1: 用来简化Spring应用的创建、运行、调试、部署 的框架
  • MySQL 8.0.15:关系型数据库
  • Mybatis:半自动化的对象关系映射的框架
  • RabittMQ 3.7.14:用于实现队列,想要了解实现延时队列可以参考我的上一篇博客
  • Redis 3.0.5:非关系型数据库,缓存,用于减少数据库压力,防止超卖

准备

  1. 数据库

    sql

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for t_order
    -- ----------------------------
    DROP TABLE IF EXISTS `t_order`;
    CREATE TABLE `t_order`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `order_user` int(255) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of t_order
    -- ----------------------------
    INSERT INTO `t_order` VALUES (1, 'watch', 1);
    INSERT INTO `t_order` VALUES (2, 'watch', 1);
    INSERT INTO `t_order` VALUES (3, 'watch', 1);
    INSERT INTO `t_order` VALUES (4, 'watch', 10);
    INSERT INTO `t_order` VALUES (5, 'watch', 8);
    INSERT INTO `t_order` VALUES (6, 'watch', 93);
    INSERT INTO `t_order` VALUES (7, 'watch', 7);
    INSERT INTO `t_order` VALUES (8, 'watch', 42);
    INSERT INTO `t_order` VALUES (9, 'watch', 46);
    INSERT INTO `t_order` VALUES (10, 'watch', 86);
    INSERT INTO `t_order` VALUES (11, 'watch', 88);
    INSERT INTO `t_order` VALUES (12, 'watch', 91);
    
    -- ----------------------------
    -- Table structure for stock
    -- ----------------------------
    DROP TABLE IF EXISTS `stock`;
    CREATE TABLE `stock`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of stock
    -- ----------------------------
    INSERT INTO `stock` VALUES (1, 'watch', '0');
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
  2. SpringBoot pom文件

    	<!--jar依赖-->
    	<dependency>
        	<groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
    	</dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
    <!-- bulid插件 编译mapper层下的xml -->
    <resources>
        <resource>
            <directory>src/main/java/</directory>
            <includes>
                <include>com/weisen/springbootspk/mapper/**/*.xml</include>
            </includes>
        </resource>
    </resources>
    
  3. application.yml

    server:
      port: 8080	#端口
    
    spring:
      rabbitmq:	#配置rabbitmq
        virtual-host: /
        host: localhost  #ip
        username: guest  #账号
        password: weisen #密码
      application:
        name: concurrency-project
      redis:	#配置redis
        host: localhost	
        port: 6379	#redis端口
        jedis:
          pool:
            max-active: 1024
            max-wait: -1s
            max-idle: 200
      datasource:	#mysql连接
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC
          username: root
          password: root
    
    #配置mybatis
    mybatis:
      mapper-locations: classpath:mapper/*.xml
    

核心

config配置

  1. RabittMQ配置

    MyRabbitMQConfig

    package com.weisen.springbootspk.config;
    
    import org.springframework.amqp.core.*;
    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;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 20:56
     */
    @Configuration
    public class MyRabbitMQConfig {
    
        //库存交换机
        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";
    
        @Bean
        public MessageConverter messageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    
    
        //创建库存交换机
        @Bean
        public Exchange getStoryExchange() {
            return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
        }
    
        //创建库存队列
        @Bean
        public Queue getStoryQueue() {
            return new Queue(STORY_QUEUE);
        }
    
        //库存交换机和库存队列绑定
        @Bean
        public Binding bindStory() {
            return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
        }
    
        //创建订单队列
        @Bean
        public Queue getOrderQueue() {
            return new Queue(ORDER_QUEUE);
        }
    
        //创建订单交换机
        @Bean
        public Exchange getOrderExchange() {
            return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
        }
    
        //订单队列与订单交换机进行绑定
        @Bean
        public Binding bindOrder() {
            return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
        }
    }
    
  2. RedisConfig配置

    RedisConfig

    package com.weisen.springbootspk.config;
    
    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.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 20:57
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(redisConnectionFactory);
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
            template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.afterPropertiesSet();
            return template;
        }
    }
    

pojo 实体

​ 实体中未添加get set方法是因为使用了lombok插件

​ idea如何使用lombok可参考idea中springboot项目使用lombok 插件

  1. Order

    package com.weisen.springbootspk.pojo;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 20:45
     */
    @Data
    public class Order implements Serializable {
        private static final long serialVersionUID = -8271355836132430489L;
        Integer id;
        String orderName;
        String orderUser;
    }
    
    
  2. Stock

    package com.weisen.springbootspk.pojo;
    
    import lombok.Data;
    import org.springframework.data.annotation.Id;
    
    import java.io.Serializable;
    
    /**
     * 作者:weisen
     * 日期:2019/12/24 9:11
     */
    @Data
    public class Stock implements Serializable {
        private static final long serialVersionUID = 6235666939721331057L;
        Integer id;
        String name;
        Integer stock;
    }
    
    

mapper层

  1. order

    package com.weisen.springbootspk.mapper.order;
    
    import com.weisen.springbootspk.pojo.Order;
    import org.apache.ibatis.annotations.Mapper;
    
    /**
     * 作者:weisen
     * 日期:2019/12/24 9:32
     */
    @Mapper
    public interface OrderMapper {
    
        Integer insert(Order order);
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="Order mapper接口的相对路径">
    
        <insert id="insert" parameterType="Order实体的相对路径">
            insert t_order(order_name,order_user) value (#{orderName},#{orderUser})
        </insert>
    
    </mapper>
    
  2. stock

    package com.weisen.springbootspk.mapper.stock;
    
    import com.weisen.springbootspk.pojo.Stock;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    
    import java.util.List;
    
    /**
     * 作者:weisen
     * 日期:2019/12/24 9:30
     */
    @Mapper
    public interface StockMapper {
    
        List<Stock> selectList(@Param("name") String name);
    
        Integer updateByPrimaryKey(Stock stock);
    
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="stock mapper接口的相对路径">
    
        <select id="selectList" resultType="Stock实体的相对路径">
            select * from stock where name = #{name}
        </select>
    
        <update id="updateByPrimaryKey" parameterType="Stock实体的相对路径">
            update stock set stock = #{stock} where id = #{id};
        </update>
    
    </mapper>
    

service层

  1. 订单跟库存的消息和消费的请求

    MQOrderService 监听订单消息队列,并消费

    package com.weisen.springbootspk.service;
    
    import com.weisen.springbootspk.config.MyRabbitMQConfig;
    import com.weisen.springbootspk.pojo.Order;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 21:07
     */
    @Service
    @Slf4j
    public class MQOrderService {
    
        @Autowired
        private OrderService orderService;
    
        /**
         * 监听订单消息队列,并消费
         * @param order
         */
        @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
        public void createOrder(Order order) {
            log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
    
            /**
             * 调用数据库orderService创建订单信息
             */
            orderService.createOrder(order);
        }
    
    }
    
    

    MQStockService 监听库存消息队列,并消费

    package com.weisen.springbootspk.service;
    
    import com.weisen.springbootspk.config.MyRabbitMQConfig;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.logging.log4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 21:05
     */
    @Service
    @Slf4j
    public class MQStockService {
    
        @Autowired
        private StockService stockService;
    
        /**
         * 监听库存消息队列,并消费
         * @param stockName
         */
        @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
        public void decrByStock(String stockName) {
            log.info("库存消息队列收到的消息商品信息是:{}", stockName);
            /**
             * 调用数据库service给数据库对应商品库存减一
             */
            stockService.decrByStock(stockName);
        }
    }
    
    

  2. Redis的处理

    RedisService

    package com.weisen.springbootspk.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 20:58
     */
    @Service
    public class RedisService {
    
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        /**
         * 设置String键值对
         * @param key
         * @param value
         * @param millis
         */
        public void put(String key, Object value, long millis) {
            redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES);
        }
    
        public void putForHash(String objectKey, String hkey, String value) {
            redisTemplate.opsForHash().put(objectKey, hkey, value);
        }
    
        /**
         * 对指定key的键值减一
         * @param key
         * @return
         */
        public Long decrBy(String key) {
            return redisTemplate.opsForValue().decrement(key);
        }
    
    }
    
    
  3. 普通业务的调用(由于是一个简单的逻辑实现,就没有严格按照面向接口的方式实现)

    OrderService

    package com.weisen.springbootspk.service;
    
    import com.weisen.springbootspk.mapper.order.OrderMapper;
    import com.weisen.springbootspk.pojo.Order;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 21:10
     */
    @Service
    public class OrderService{
    
        @Autowired
        private OrderMapper orderMapper;
    
        public void createOrder(Order order) {
            orderMapper.insert(order);
        }
        
    }
    
    

    StockService

    package com.weisen.springbootspk.service;
    
    
    import com.weisen.springbootspk.mapper.stock.StockMapper;
    import com.weisen.springbootspk.pojo.Stock;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    import java.util.List;
    
    /**
     * 作者:weisen
     * 日期:2019/12/23 21:08
     */
    @Service
    @Slf4j
    public class StockService {
    
        @Autowired
        private StockMapper stockMapper;
    
        public void decrByStock(String stockName) {
            List<Stock> stocks = stockMapper.selectList(stockName);
            if (!CollectionUtils.isEmpty(stocks)) {
                Stock stock = stocks.get(0);
                stock.setStock(stock.getStock() - 1);
                stockMapper.updateByPrimaryKey(stock);
            }
        }
    
        public Integer selectByName(String stockName) {
            List<Stock> stocks = stockMapper.selectList(stockName);
            if (!CollectionUtils.isEmpty(stocks)) {
                return stocks.get(0).getStock().intValue();
            }
            return 0;
        }
    
    }
    
    

controller

SecController 写了两种实现方式,可以看一看其中的区别的什么

package com.weisen.springbootspk.controller;

import com.weisen.springbootspk.config.MyRabbitMQConfig;
import com.weisen.springbootspk.pojo.Order;
import com.weisen.springbootspk.service.OrderService;
import com.weisen.springbootspk.service.RedisService;
import com.weisen.springbootspk.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 作者:weisen
 * 日期:2019/12/23 21:03
 */
@Controller
@Slf4j
public class SecController {



    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisService redisService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private StockService stockService;

    /**
     * 使用redis+消息队列进行秒杀实现
     * @param username
     * @param stockName
     * @return
     */
    @RequestMapping("/sec")
    @ResponseBody
    public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
        log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
        String message = null;
        //调用redis给相应商品库存量减一
        Long decrByResult = redisService.decrBy(stockName);
        if (decrByResult >= 0) {
            /**
             * 说明该商品的库存量有剩余,可以进行下订单操作
             */
            log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName);
            //发消息给库存消息队列,将库存数据减一
            rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName);

            //发消息给订单消息队列,创建订单
            Order order = new Order();
            order.setOrderName(stockName);
            order.setOrderUser(username);
            rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order);
            message = "用户" + username + "秒杀" + stockName + "成功";
        } else {
            /**
             * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
             */
            log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", username);
            message = username + "商品的库存量没有剩余,秒杀结束";
        }
        return message;
    }

    /**
     * 实现纯数据库操作实现秒杀操作
     * @param username
     * @param stockName
     * @return
     */
    @RequestMapping("/secDataBase")
    @ResponseBody
    public String secDataBase(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
        log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
        String message = null;
        //查找该商品库存
        Integer stockCount = stockService.selectByExample(stockName);
        log.info("用户:{}参加秒杀,当前商品库存量是:{}", username, stockCount);
        if (stockCount > 0) {

            /**
             * 还有库存,可以进行继续秒杀,库存减一,下订单
             */
            //1、库存减一
            stockService.decrByStock(stockName);

            //2、下订单
            Order order = new Order();
            order.setOrderUser(username);
            order.setOrderName(stockName);
            orderService.createOrder(order);
            log.info("用户:{}.参加秒杀结果是:成功", username);
            message = username + "参加秒杀结果是:成功";
        } else {
            log.info("用户:{}.参加秒杀结果是:秒杀已经结束", username);
            message = username + "参加秒杀活动结果是:秒杀已经结束";
        }
        return message;
    }

}

SpringBoot启动类

@SpringBootApplication
public class SpringbootspkApplication implements ApplicationRunner {

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

    @Autowired
    private RedisService redisService;

    /**
     * redis初始化各商品的库存量
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        redisService.put("watch", 10, 20);
    }
}

测试

使用工具 apache-jmeter-5.2.1

如何使用工具 可参考Apache JMeter5.2基础入门实践详解

  1. 启动apache-jmeter-5.2.1

  2. 设置语言

    在这里插入图片描述

  3. 创建线程组

    在这里插入图片描述

  4. 在线程组上右键添加请求和监听器元件

    在这里插入图片描述

  5. 编辑线程组

    在这里插入图片描述

    jmeter可以定义随机参数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
点击绿色箭头运行在这里插入图片描述

  • 16
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值