实现基于SpringBoot+Maven+Mysql+Redis+RabbitMQ 的高并发秒杀系统(限时秒杀)

您所在的位置:网站首页 rabbitmq秒杀 实现基于SpringBoot+Maven+Mysql+Redis+RabbitMQ 的高并发秒杀系统(限时秒杀)

实现基于SpringBoot+Maven+Mysql+Redis+RabbitMQ 的高并发秒杀系统(限时秒杀)

2024-06-28 05:25| 来源: 网络整理| 查看: 265

原理:

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

对于网络流量瞬间变大问题,最常用的办法就是将页面静态化,也就是我们常说的前后端分离。把静态页面直接缓存到用户的浏览器中,当用户需要获取数据时,就从服务端接口动态获取。这样会大大节省网络的流量,如果再加上CDN优化,一般都不会有大问题。

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

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

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

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

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

当然,秒杀系统还有很多要处理的事情,比如限流防刷、分布式Session等等。

1.搭建项目环境

下方提供了安装工具的博客

1.1 安装RabbitMQ

https://blog.csdn.net/m0_37034294/article/details/82839494

2.2 安装Redis+RedisDesktopManager

https://blog.csdn.net/qq_39135287/article/details/82686837

2.3 Jmeter压力测试工具

https://blog.csdn.net/liuyanh2006/article/details/82494548

2.4 创建数据库表结构

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

注意:请将主键设为自动增长

2.项目代码

1.创建一个springboot项目,启动器可先不选择,下方直接放pom.xml文件,cv即可。

org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.projectlombok lombok true org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-devtools runtime true mysql mysql-connector-java runtime org.apache.commons commons-lang3 3.8.1 org.springframework.boot spring-boot-starter-amqp io.jsonwebtoken jjwt 0.7.0 org.springframework.boot spring-boot-starter-data-redis org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.0 org.springframework spring-tx tk.mybatis mapper-spring-boot-starter 2.0.3-beta1 tk.mybatis mapper 4.0.0

2.配置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/myredis?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 #JPA Configuration: spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect ##配置连接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 spring.redis.password=123456

新建pojo包,添加实体类

本次数据库操作方面使用了tkmybatis框架,所以实体类我们需要用到JPA的注解,来实现映射关系。

import java.io.Serializable; import lombok.Data; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Data @Table(name = "t_order") public class Order implements Serializable { private static final long serialVersionUID = -8867272732777764701L; @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; } 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 { private static final long serialVersionUID = 2451194410162873075L; @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; @Column(name = "stock") private Long stock; }

配置tkmybatis的接口

​ 4.1 需要通过继承它来实现数据库操作

2新建名为base的包,在base下面新建service的接口

3新建GenericMapper接口

import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; public interface GenericMapper extends Mapper, MySqlMapper { }

在这里插入图片描述

新建mapper层

OrderMapper.java

import com.demo.base.service.GenericMapper; import com.demo.pojo.Order; public interface OrderMapper extends GenericMapper { void insertOrder(Order order); }

StockMapper.java

import com.demo.base.service.GenericMapper; import com.demo.pojo.Stock; public interface StockMapper extends GenericMapper { }

在这里插入图片描述

编写RabbitMQ和redis的配置类 新建config包,新建redis和RabbitMQ的类 package com.demo.config; 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.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @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(); } } package com.demo.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; @Configuration public class RedisConfig { // 配置redis得配置详解 @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }

在这里插入图片描述

编写service层 新建service包以及impl包,这里只提供实现类 import com.demo.mapper.OrderMapper; import com.demo.pojo.Order; import com.demo.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); } } package com.demo.service.impl; import com.demo.mapper.StockMapper; import com.demo.pojo.Stock; import com.demo.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 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 stocks = stockMapper.selectByExample(example); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get(0).getStock().intValue(); } return 0; } }

继续在 service包下面新建

MQOrderService.java

package com.demo.service; import com.demo.config.MyRabbitMQConfig; import com.demo.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.java

package com.demo.service; import com.demo.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.java

**主要用来实现对redis得key和value初始化以及对value得操作** package com.demo.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; @Service public class RedisService { @Autowired private RedisTemplate 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 get(String key, Class 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); } }

​ 下面为service包得完整目录

在这里插入图片描述 10. 新建controller层,添加SecController.java,包含两种方法,参考下面的代码

```java import com.demo.config.MyRabbitMQConfig; import com.demo.pojo.Order; import com.demo.service.OrderService; import com.demo.service.RedisService; import com.demo.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; } } ``` 11. 编写springboot启动类,对redis进行初始化,**简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息** ```java import com.demo.service.RedisService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import tk.mybatis.spring.annotation.MapperScan; @MapperScan("com.demo.mapper") @SpringBootApplication public class DemoApplication implements ApplicationRunner{ public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Autowired private RedisService redisService; /** * redis初始化商品的库存量和信息 * @param args * @throws Exception */ @Override public void run(ApplicationArguments args) throws Exception { redisService.put("watch", 10, 20); } } ``` 12. 至此,代码已全部写完,下图为项目目录,下面进入测试步骤,请检查以上步骤是否出错! [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5smGH7i-1577175164825)(C:\Users\WuLian\Pictures\1\QQ图片20191224144222.png)] 3.测试项目

启动springboot项目,并且打开 redis和rabbitmq的服务

打开Redis Desktop Manager工具,查看是否新建了一个redis :watch

在这里插入图片描述

打开我们得JMeter工具

3.1 选择中文

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

点击上方得白色小书本,选择random,1-99得随机数

在这里插入图片描述

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

在这里插入图片描述

3.6 最后我们在测试计划建一个结果树,查看我们发送请求返回得消息数据 在这里插入图片描述 3.7 点击运行 在这里插入图片描述

4.测试结果

直接在run控制台查看运行结果,以及数据库表的变化。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3