利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀

前言

​ “商品秒杀”功能模块是建立在“商品详情”功能模块的基础之上,对 于这一功能模块而言,其主要的核心流程在于:前端发起抢购请 求,该请求将携带着一些请求数据:待秒杀Id跟当前用户Id等数 据;后端接口在接收到请求之后,将执行一系列的判断与秒杀处 理逻辑,最终将处理结果返回给到前端。

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第1张图片

本文将使用SpringBoot+Mysql+Redis+RabbitMQ+tkmybatis实现限时秒杀功能

安装项目所需工具

​ 为避免博客内容过于冗杂,安装教程都附上前辈们的博客,这里就不过多阐述了

RabbitMQ安装

​ RabbitMQ安装教程

Redis+RedisDesktopManager

​ Redis+RedisDesktopManager安装教程

Jmeter压力测试工具

​ Jmeter安装教程

创建数据库

库存表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 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', '14');

SET FOREIGN_KEY_CHECKS = 1;

订单表

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` 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;

SET FOREIGN_KEY_CHECKS = 1;

创建SpringBoot项目

​ 这里我们先不勾选jar包,直接next,随后再直接导入至pom.xml中

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第2张图片

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.8.1version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.7.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.0version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-txartifactId>
        dependency>
        <dependency>
            <groupId>tk.mybatisgroupId>
            <artifactId>mapper-spring-boot-starterartifactId>
            <version>2.0.3-beta1version>
        dependency>
        <dependency>
            <groupId>tk.mybatisgroupId>
            <artifactId>mapperartifactId>
            <version>4.0.0version>
        dependency>
    dependencies>

application.properties

spring.devtools.restart.enabled=false
##配置数据库连接
spring.datasource.username=root
spring.datasource.password=root
server.port=8443
spring.datasource.url=jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
##配置rabbitmq连接
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
##配置连接redis --都记得打开服务
spring.redis.host=localhost
spring.redis.port=6379 
spring.redis.jedis.pool.max-active=1024
spring.redis.jedis.pool.max-wait=-1s
spring.redis.jedis.pool.max-idle=200
##这里的5201314是我的redis密码,记得改为自己的
spring.redis.password=5201314


新建pojo包,添加实体类

Order.java

package com.hunt.springbootredis.pojo;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:18
 * 

* Hint: */ import lombok.Data; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Data @Table(name = "t_order") public class Order implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "order_name") private String order_name; @Column(name = "order_user") private String order_user; }

Stock.java

package com.hunt.springbootredis.pojo;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:18
 * 

* Hint: */ import lombok.Data; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Table(name = "stock") @Data public class Stock implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; @Column(name = "stock") private Long stock; }

新建base包,在base下面新建service包,随后新建接口GenericMapper.interface

package com.hunt.springbootredis.base.service;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:19
 * 

* Hint: */ import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; public interface GenericMapper<T> extends Mapper<T>, MySqlMapper<T> { }

新建mapper层

OrderMapper.interface

package com.hunt.springbootredis.mapper;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:20
 * 

* Hint: */ import com.hunt.springbootredis.base.service.GenericMapper; import com.hunt.springbootredis.pojo.Order; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface OrderMapper extends GenericMapper<Order> { void insertOrder(Order order); }

StockMapper.interface

package com.hunt.springbootredis.mapper;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:21
 * 

* Hint: */ import com.hunt.springbootredis.base.service.GenericMapper; import com.hunt.springbootredis.pojo.Stock; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface StockMapper extends GenericMapper<Stock> { }

编写RabbitMQ和redis配置类
新建config包

MyRabbitMQConfig.java

package com.hunt.springbootredis.config;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:22
 * 

* Hint: */ import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.ExchangeBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; 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 java.util.HashMap; import java.util.Map; @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(); } }

RedisConfig .java

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;

@Configuration
public class RedisConfig {
    // 配置redis得配置详解
    @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;
    }
}

编写service层

OrderService

package com.hunt.springbootredis.service;

import com.hunt.springbootredis.pojo.Order;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:24
 * 

* Hint: */ public interface OrderService { void createOrder(Order order); }

OrderServiceImpl

package com.hunt.springbootredis.service.impl;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:25
 * 

* Hint: */ import com.hunt.springbootredis.mapper.OrderMapper; import com.hunt.springbootredis.pojo.Order; import com.hunt.springbootredis.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void createOrder(Order order) { orderMapper.insert(order); } }

StockService

package com.hunt.springbootredis.service;

public interface StockService {

    void decrByStock(String stockName);

    Integer selectByExample(String stockName);
}

StockServiceImpl

package com.hunt.springbootredis.service.impl;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:27
 * 

* Hint: */ import com.hunt.springbootredis.mapper.StockMapper; import com.hunt.springbootredis.pojo.Stock; import com.hunt.springbootredis.service.StockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import tk.mybatis.mapper.entity.Example; import java.util.List; @Service public class StockServiceImpl implements StockService { @Autowired private StockMapper stockMapper; // 秒杀商品后减少库存 @Override public void decrByStock(String stockName) { Example example = new Example(Stock.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("name", stockName); List<Stock> stocks = stockMapper.selectByExample(example); if (!CollectionUtils.isEmpty(stocks)) { Stock stock = stocks.get(0); stock.setStock(stock.getStock() - 1); stockMapper.updateByPrimaryKey(stock); } } // 秒杀商品前判断是否有库存 @Override public Integer selectByExample(String stockName) { Example example = new Example(Stock.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("name", stockName); List<Stock> stocks = stockMapper.selectByExample(example); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get(0).getStock().intValue(); } return 0; } }

MQOrderService

package com.hunt.springbootredis.service;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:28
 * 

* Hint:订单得消费队列 */ import com.hunt.springbootredis.config.MyRabbitMQConfig; import com.hunt.springbootredis.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; @Service @Slf4j public class MQOrderService { @Autowired private OrderService orderService; /** * 监听订单消息队列,并消费 * * @param order */ @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE) public void createOrder(Order order) { log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrder_user(), order.getOrder_name()); /** * 调用数据库orderService创建订单信息 */ orderService.createOrder(order); } }

MQStockService

package com.hunt.springbootredis.service;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:29
 * 

* Hint:库存得消费队列 */ import com.hunt.springbootredis.config.MyRabbitMQConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @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); } }

RedisService

package com.hunt.springbootredis.service;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:32
 * 

* Hint:配置类,主要用来实现对redis得key和value初始化以及对value得操作 */ 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; @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); } public <T> T get(String key, Class<T> type) { return (T) redisTemplate.boundValueOps(key).get(); } public void remove(String key) { redisTemplate.delete(key); } public boolean expire(String key, long millis) { return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS); } public boolean persist(String key) { return redisTemplate.hasKey(key); } public String getString(String key) { return (String) redisTemplate.opsForValue().get(key); } public Integer getInteger(String key) { return (Integer) redisTemplate.opsForValue().get(key); } public Long getLong(String key) { return (Long) redisTemplate.opsForValue().get(key); } public Date getDate(String key) { return (Date) redisTemplate.opsForValue().get(key); } /** * 对指定key的键值减一 * * @param key * @return */ public Long decrBy(String key) { return redisTemplate.opsForValue().decrement(key); } }

controller

SecController

package com.hunt.springbootredis.controller;

/**
 * 作者:HuntLee
 * 日期:2019-12-24 14:35
 * 

* Hint:controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象 */ import com.hunt.springbootredis.config.MyRabbitMQConfig; import com.hunt.springbootredis.pojo.Order; import com.hunt.springbootredis.service.OrderService; import com.hunt.springbootredis.service.RedisService; import com.hunt.springbootredis.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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @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 */ @PostMapping(value = "/sec", produces = "application/json;charset=utf-8") @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.setOrder_name(stockName); order.setOrder_user(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.setOrder_user(username); order.setOrder_name(stockName); orderService.createOrder(order); log.info("用户:{}.参加秒杀结果是:成功", username); message = username + "参加秒杀结果是:成功"; } else { log.info("用户:{}.参加秒杀结果是:秒杀已经结束", username); message = username + "参加秒杀活动结果是:秒杀已经结束"; } return message; }*/ }

项目结构图
利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第3张图片

然后打开JMeter工具

选择中文。。。

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第4张图片

添加一个线程组

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第5张图片

给这个线程组得数量为40,这个线程组得作用就是模拟40个用户发送请求,去秒杀

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第6张图片

然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求的组件了

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第7张图片

配置

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第8张图片

这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数**${__Random(1,99,)}**

最后我们在测试计划建一个结果树,查看我们发送请求返回得消息数据

成功

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第9张图片

ize_16,color_FFFFFF,t_70)

然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求的组件了

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第10张图片

配置

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第11张图片

这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数**${__Random(1,99,)}**

最后我们在测试计划建一个结果树,查看我们发送请求返回得消息数据

成功

利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀_第12张图片

你可能感兴趣的:(利用Java-SpringBoot+Redis+Jmeter实现高并发限时秒杀)