写点什么

springboot+redis+rabbitmq 实现模拟秒杀系统 (附带 docker 安装 mysql,rabbitmq,redis 教程)

用户头像
yk
关注
发布于: 2021 年 03 月 31 日

前言

在项目开发中,难免会遇到高并发问题,本文借助秒杀系统的模拟场景,旨在解决高并发问题。

原理

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

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

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

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

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

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

安装所需工具

虚拟机:docker 安装 mysql,rabbitmq,redis

虚拟机安装和 docker 安装我就不介绍了,网上都有教程。

1、docker 安装 mysql

[root@yk3 docker]# docker pull mysql
复制代码



mysql 镜像下载完成(因为我之前下载了 mysql 镜像,所以这里显示 already exists)

使用命令:docker images 查看下载的镜像

[root@yk3 docker]# docker images
复制代码



红框里的就是 mysql 镜像。

使用 mysql 镜像制作容器并运行。

[root@yk3 docker]# docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root 14340cbfa999
复制代码


这个 14340cbfa999 是我刚才下载的 mysql 镜像的 id,也就是 IMAGE ID 字段对应的值,读者可以根据自己的 id 自行更换。

注意一定要加上-e MYSQL_ROOT_PASSWORD=数据库密码,不然启动不起来。

启动完成。使用 docker ps -a 命令查看运行中的容器。


使用本地的 navicat 连接虚拟机 docker 中的 mysql



如果连接错误,报 2058 错误,解决方法:

docker 进入 mysql 容器:

[root@yk3 docker]# docker exec -it e9023467ecfb /bin/bash
复制代码


注意:-it 后面跟上 mysql 的容器 id


然后登陆 mysql:


进入 mysql 容器执行命令:

mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
复制代码

执行成功,exit 返回。再用 navicat 连接即可成功。

2、docker 安装 redis

同样是 docker 先下载 redis 镜像

[root@yk3 docker]# docker pull redis
复制代码

制作容器并运行:(注意镜像 id 改为自己的)

[root@yk3 docker]# docker run --name myredis -d -p 6379:6379 a617c1c92774
复制代码

 使用 redis-plus 连接测试:


连接成功!

3、docker 安装 rabbitmq

同样是 docker 先下载 rabbitmq 镜像

[root@yk3 docker]# docker pull rabbitmq
复制代码

制作容器并运行:(注意镜像 id 改为自己的) 

[root@yk3 docker]# docker run -d --name rabbit -p 15672:15672 -p 5672:5672 603fe110af88
复制代码

这里要两个端口号:15672 和 5672,15672 对应的是 http(也就是我们登录 RabbitMQ 后台管理时用的端口),5672 对应的是 RabbitMQ 的通信。

到这一步,rabbitmq 已经启动完毕,但是在本地访问客户端http://192.168.121.130:15672这个端口还需要以下配置,

进入容器:

[root@yk3 docker]# docker exec -it 603fe110af88 /bin/bash
复制代码

执行命令:rabbitmq-plugins enable rabbitmq_management 便可访问


登录:用户名密码都为 guest。

项目代码

 新建一个 springboot 项目,项目结构:


application.properties:

# DB Configuration#指定数据库驱动spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver#数据库jdbc连接url地址,serverTimezone设置数据库时区东八区spring.datasource.url=jdbc:mysql://192.168.121.130:3306/mqtest?serverTimezone=GMT%2B8#数据库账号spring.datasource.username=rootspring.datasource.password=rootserver.port=8990#rabbitmqspring.rabbitmq.host=192.168.121.130spring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guest
#redis配置spring.redis.host=192.168.121.130spring.redis.port=6379spring.redis.jedis.pool.max-active=1024spring.redis.jedis.pool.max-wait=-1sspring.redis.jedis.pool.max-idle=200
#mybatis配置mybatis.mapper-locations=classpath:mapper/*.xml#打印sql语句logging.level.com.rabbitmq.mapper=debug
复制代码

启动类:(启动时将库存加入到 redis 中)

package com.rabbitmq;
import com.rabbitmq.config.RedisService;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;
@SpringBootApplicationpublic class ConsumerApplication implements ApplicationRunner {
public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); }
@Autowired private RedisService redisService;
/** * redis初始化各商品的库存量 * @param args * @throws Exception */ @Override public void run(ApplicationArguments args) throws Exception { redisService.put("watch", 10000, 20); }
}
复制代码

pom.xml:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.4.4</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>    <groupId>com.rabbitmq</groupId>    <artifactId>consumer</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>consumer</name>    <description>Demo project for Spring Boot</description>    <properties>        <java.version>1.8</java.version>    </properties>    <dependencies>        <!--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> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!-- bulid插件 编译mapper层下的xml --> <resources> <resource> <directory>src/main/java/</directory> <includes> <include>com/rabbitmq/mapper/**/*.xml</include> </includes> </resource> </resources> </build>
</project>
复制代码

表文件:

CREATE TABLE `stock` (  `id` int NOT NULL AUTO_INCREMENT,  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,  `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,  PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
CREATE TABLE `t_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `order_user` int DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=69406 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
复制代码


对应的实体类:

stock 实体类:

package com.rabbitmq.bean;
import lombok.Data;
import java.io.Serializable;
@Datapublic class Stock implements Serializable { private static final long serialVersionUID = 6235666939721331057L; Integer id; String name; Integer stock;}
复制代码


order 实体类:

package com.rabbitmq.bean;
import lombok.Data;
import java.io.Serializable;
@Datapublic class Order implements Serializable { private static final long serialVersionUID = -8271355836132430489L; Integer id; String orderName; String orderUser;}
复制代码


mapper 接口文件:

package com.rabbitmq.mapper;
import com.rabbitmq.bean.Order;import org.apache.ibatis.annotations.Mapper;
@Mapperpublic interface OrderMapper {
Integer insert(Order order);}
复制代码


package com.rabbitmq.mapper;
import com.rabbitmq.bean.Stock;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapperpublic interface StockMapper {
List<Stock> selectList(@Param("name") String name);
Integer updateByPrimaryKey(Stock stock);
}
复制代码


mapper 对应的 xml 配置文件

<?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="com.rabbitmq.mapper.OrderMapper">
<insert id="insert" parameterType="com.rabbitmq.bean.Order"> insert t_order(order_name,order_user) value (#{orderName},#{orderUser}) </insert>
</mapper>
复制代码


<?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="com.rabbitmq.mapper.StockMapper">
<select id="selectList" resultType="com.rabbitmq.bean.Stock"> select * from stock where name = #{name} </select>
<update id="updateByPrimaryKey" parameterType="com.rabbitmq.bean.Stock"> update stock set stock = #{stock} where id = #{id}; </update>
</mapper>
复制代码


普通请求的 service(为了方便对比,没有使用 rabbitmq):

package com.rabbitmq.service;
import com.rabbitmq.bean.Order;import com.rabbitmq.mapper.OrderMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
@Servicepublic class OrderService{
@Autowired private OrderMapper orderMapper;
public void createOrder(Order order) { orderMapper.insert(order); } }
复制代码


package com.rabbitmq.service;
import com.rabbitmq.bean.Stock;import com.rabbitmq.mapper.StockMapper;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;
@Service@Slf4jpublic class StockService {
@Autowired private StockMapper stockMapper;
public void decrByStock(String stockName) { synchronized(this) { 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) { synchronized (this){ List<Stock> stocks = stockMapper.selectList(stockName); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get(0).getStock().intValue(); } return 0; } }
}
复制代码


接下来重点来了,使用 rabbitmq 实现:

rabbitmq 配置类:

package com.rabbitmq.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;
@Configurationpublic 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(); }}
复制代码


redis 配置类:

package com.rabbitmq.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;
@Configurationpublic 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; }}
复制代码


redis 的操作类:

package com.rabbitmq.config;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Servicepublic 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); }
}
复制代码


重点:rabbitmq 实现的 service 层:

package com.rabbitmq.mqservice;
import com.rabbitmq.config.MyRabbitMQConfig;import com.rabbitmq.service.OrderService;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@Slf4jpublic 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); }
}
复制代码


package com.rabbitmq.mqservice;
import com.rabbitmq.config.MyRabbitMQConfig;import com.rabbitmq.service.StockService;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@Slf4jpublic 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); }}
复制代码


controller 层:

package com.rabbitmq.controller;
import com.rabbitmq.bean.Order;import com.rabbitmq.config.MyRabbitMQConfig;import com.rabbitmq.config.RedisService;import com.rabbitmq.service.OrderService;import com.rabbitmq.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;
@Controller@Slf4jpublic 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) { synchronized (this){ redisService.decrBy(stockName); log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName); String message = null; //查找该商品库存 Integer stockCount = stockService.selectByName(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; } }
}
复制代码


代码编写完毕,启动运行。

测试

使用工具 apache-jmeter-5.2.1

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

  1. 启动apache-jmeter-5.2.1

  2. 设置语言


  1. 创建线程组


  1. 添加 http 请求和监听原件


jmeter 可以定义随机参数







  1. 点击绿色箭头运行


发布于: 2021 年 03 月 31 日阅读数: 49
用户头像

yk

关注

还未添加个人签名 2021.03.31 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
赞!神速!
22 小时前
回复
没有更多了
springboot+redis+rabbitmq实现模拟秒杀系统(附带docker安装mysql,rabbitmq,redis教程)