springboot+redis+rabbitmq 实现模拟秒杀系统 (附带 docker 安装 mysql,rabbitmq,redis 教程)
前言
在项目开发中,难免会遇到高并发问题,本文借助秒杀系统的模拟场景,旨在解决高并发问题。
原理
秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。
对于系统并发量变大问题,这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了 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=root
spring.datasource.password=root
server.port=8990
#rabbitmq
spring.rabbitmq.host=192.168.121.130
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#redis配置
spring.redis.host=192.168.121.130
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
#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;
@SpringBootApplication
public 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;
@Data
public 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;
@Data
public 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;
@Mapper
public 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;
@Mapper
public 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;
@Service
public 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
@Slf4j
public 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;
@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();
}
}
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;
@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;
}
}
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;
@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);
}
}
重点: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
@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);
}
}
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
@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);
}
}
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
@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) {
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基础入门实践详解
启动
apache-jmeter-5.2.1
设置语言
创建线程组
添加 http 请求和监听原件
jmeter 可以定义随机参数
点击绿色箭头运行
版权声明: 本文为 InfoQ 作者【yk】的原创文章。
原文链接:【http://xie.infoq.cn/article/013d51870a6c5840f937c70a4】。文章转载请联系作者。
yk
还未添加个人签名 2021.03.31 加入
还未添加个人简介
评论 (1 条评论)