SpringBoot +Redis +RabbitMQ 实现高并发限时秒杀
提示:以下是本篇文章正文内容,下面案例可供参考
docker安装:docker安装RabbitMQ_liangshitian的博客-CSDN博客
windows安装:windows安装rabbitmq安装详细步骤_青蛙与大鹅的博客-CSDN博客_window安装rabbitmq
docker安装:https://blog.csdn.net/qq_33612228/article/details/10360918
windows安装:Windows 64位下安装Redis 以及 可视化工具Redis Desktop Manager的安装和使用_零碎de記憶的博客-CSDN博客_redis可视化工具下载
springboot整合redis:SpringBoot整合Redis_liangshitian的博客-CSDN博客
windows安装:Jmeter安装教程_liuyanh2006的博客-CSDN博客
4.安装工具包:我的资源; rabbitmq+Erlang工具+压力测试jmeter-Java文档类资源-CSDN下载otp_win64_24.3.3.exe+rabbitmq-server-3.9.15.exe+ap更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/jlonghou/85195659
1.商品库存表:stock表
CREATE TABLE `stock` (
`id` varchar(64) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`stock` varchar(255) DEFAULT NULL,
`remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) NOT NULL DEFAULT '',
`create_by` varchar(64) NOT NULL DEFAULT '',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='商品库存表';
2.秒杀订单表:t_order表
CREATE TABLE `t_order` (
`id` varchar(64) NOT NULL,
`order_name` varchar(255) DEFAULT NULL,
`order_user` varchar(255) DEFAULT NULL,
`remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`update_date` datetime DEFAULT NULL COMMENT '最后更新时间',
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) NOT NULL DEFAULT '',
`create_by` varchar(64) NOT NULL DEFAULT '',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='秒杀订单表';
1.pom引入
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-amqp
2.配置application.yml
server:
port: 8090
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/rabbitmq?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: root
# 使用Druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
data:
redis:
repositories:
enabled: false
redis:
database: 0 # redis数据库索引(默认为0),我们使用索引为其他(0-15)的数据库,避免和其他数据库冲突
host: 127.0.0.1
port: 6379
password:
# 4369 -- erlang发现口
#
# 5672 --client端通信口
#
# 15672 -- 管理界面ui端口 http://localhost:15672/
#
# 25672 -- server间内部通信口
rabbitmq: #mq配置
host: 127.0.0.1
port: 5672
username: guest
password: guest
logging:
config: classpath:logback-spring.xml
3.实体类 Order.java+Stock 商品库存表
/**
* @Description: 商品库存表
*/
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField("order_name")
private String orderName;
@TableField("order_user")
private String orderUser;
@TableField("create_by")
private String createBy;
@TableField("update_by")
private String updateBy;
@TableField("create_date")
private Date createDate;
@TableField("update_date")
private Date updateDate;
@TableField("del_flag")
private String delFlag;
}
/**
* @Description: 商品库存表
*/
@TableName("stock")
@Data
public class Stock {
@TableId(type = IdType.AUTO )
private Integer id;
/**
* 产品名称
*/
@TableField("name")
private String name;
/**
* 存货
*/
@TableField("stock")
private Integer stock;
}
4.服务层
StockService存货服务层+OrderService订单服务层
import com.orange.entity.Stock;
import java.util.List;
/**
* 存货服务层
*/
public interface StockService {
/**
* 秒杀商品后-减少库存
* @param name 商品名称
*/
int decrByStock(String name);
/**
* 秒杀商品列表
* @return List
*/
List selectList();
}
import com.orange.entity.Order;
import org.springframework.stereotype.Service;
/**
* 订单服务层
*/
@Service
public interface OrderService {
/**
* 订单保存
* @param order 实体
*/
int saveOrder(Order order);
}
OrderServiceImpl订单实现层+StockServiceImpl存货实现层
package com.example.rabbit.service.imp;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.rabbit.entity.Stock;
import com.example.rabbit.exception.CustomException;
import com.example.rabbit.mapper.StockMapper;
import com.example.rabbit.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Author HOUJL
* @Date 2022/4/21
* @Description:
*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
@Override
public int decrByStock(String name) {
Stock stock = stockMapper.selectOne(new QueryWrapper().lambda().eq(Stock::getName,name));
stock.setStock(stock.getStock()-1);
int i = stockMapper.updateById(stock);
if( i<= 0){
throw new CustomException("减少库存失败");
}
return i;
}
@Override
public List selectList() {
return stockMapper.selectList(new QueryWrapper<>());
}
}
package com.example.rabbit.service.imp;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.rabbit.entity.Order;
import com.example.rabbit.exception.CustomException;
import com.example.rabbit.mapper.OrderMapper;
import com.example.rabbit.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Author HOUJL
* @Date 2022/4/21
* @Description:
*/
@Service
public class OrderServiceImpl extends ServiceImpl implements OrderService {
@Autowired
OrderMapper orderMapper;
@Override
public int saveOrder(Order order) {
int i = orderMapper.insert(order);
if (i <= 0) {
throw new CustomException("保存订单失败");
}
return i;
}
}
5.配置rabbitmq的实现方式以及redis的实现方式
1)在 service包下面直接新建 MQOrderServiceImpl.java,这个类属于订单的消费队列。
package com.example.rabbit.service.imp;
import com.example.rabbit.config.RabbitMqConfig;
import com.example.rabbit.entity.Order;
import com.example.rabbit.service.OrderService;
import com.example.rabbit.service.StockService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.Date;
/**
* @Author HOUJL
* @Date 2022/4/21
* @Description: 消费消息
*/
@Service
@Slf4j
public class MqOrderServiceImpl {
private final OrderService orderService;
private final StockService stockService;
public MqOrderServiceImpl(OrderService orderService, StockService stockService) {
this.orderService = orderService;
this.stockService = stockService;
}
/**
* MQ监听订单消息队列,并消费
* @param order
*/
@RabbitListener(queues = RabbitMqConfig.ORDER_QUEUE, containerFactory = "rabbitListenerContainerFactory")
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Message message, Order order, Channel channel) throws IOException {
log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
/**
* 调用数据库orderService创建订单信息
*/
order.setCreateBy(order.getOrderUser());
order.setCreateDate(new Date());
order.setUpdateBy(order.getOrderUser());
order.setUpdateDate(new Date());
order.setDelFlag("0");
int i = orderService.saveOrder(order);
int j = stockService.decrByStock(order.getOrderName());
if (i>0 && j>0){
//消费成功
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
log.info("消费订单成功,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
}else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
log.info("消费订单失败,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName());
}
}
}
2)MQStockServiceImpl.java这个属于库存得消费队列。
package com.example.rabbit.service.imp;
import com.example.rabbit.config.RabbitMqConfig;
import com.example.rabbit.entity.Order;
import com.example.rabbit.utils.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @Author HOUJL
* @Date 2022/4/21
* @Description: 生产消息
*/
@Service
@Slf4j
public class MqStockServiceImpl {
private final RedisCache redisCache;
private final RabbitTemplate rabbitTemplate;
public MqStockServiceImpl(RedisCache redisCache, RabbitTemplate rabbitTemplate) {
this.redisCache = redisCache;
this.rabbitTemplate = rabbitTemplate;
}
/**
* 使用redis+消息队列进行秒杀实现
*
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
public String secKill(String userName, String stockName) {
log.info("参加秒杀的用户是:{},秒杀的商品是:{}", userName, stockName);
String message = "";
//redis中key对应的value减一
Long decrByResult = redisCache.decrBy(stockName);
if (Objects.nonNull(decrByResult) && decrByResult >= 0) {
/**
* 说明该商品的库存量有剩余,可以进行下订单操作
*/
log.info("用户:{}, 秒杀该商品:{},库存余量{},可以进行下订单操作", userName, stockName, decrByResult);
//1.发消息给订单消息队列,创建订单 2.发消息给库存消息队列,将库存数据减一 3.将订单保存到redis 实现限购功能
Order order = new Order();
order.setOrderUser(userName);
order.setOrderName(stockName);
rabbitTemplate.convertAndSend(RabbitMqConfig.ORDER_EXCHANGE,RabbitMqConfig.ORDER_ROUTING_KEY,order);
message = "用户" + userName + "秒杀" + stockName + "成功";
limitNumber(userName,stockName);
} else {
/**
* 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
*/
log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", userName);
message = "用户:" + userName + "商品的库存量没有剩余,秒杀结束";
}
return message;
}
private void limitNumber(String userName, String stockName) {
String key = userName + ":" + stockName + ":number";
redisCache.incrBy(key);
}
}
6.RabbitMqConfig 和redisUtil工具类
1.RabbitMqConfig.java
package com.example.rabbit.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
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 org.springframework.stereotype.Component;
/**
* @Author HOUJL
* @Date 2022/4/21
* @Description: RabbitMQConfig插件配置
*/
@Configuration
public class RabbitMqConfig {
/**
* 库存交换机
*/
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";
/**
*
* @param connectionFactory
* @return SimpleRabbitListenerContainerFactory
*/
@Bean(name = "rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
//手动确认消息
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
//消费数量
factory.setPrefetchCount(50);
return factory;
}
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 创建库存交换机
* @return
*/
@Bean
public Exchange getStoryExchange() {
return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
}
/**
* 创建库存队列
* @return
*/
@Bean
public Queue getStoryQueue() {
return new Queue(STORY_QUEUE,true);
}
/**
* 库存交换机和库存队列绑定
* @return
*/
@Bean
public Binding bindStory() {
return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
}
/**
* 创建订单队列
* @return
*/
@Bean
public Queue getOrderQueue() {
return new Queue(ORDER_QUEUE);
}
/**
* 创建订单交换机
* @return
*/
@Bean
public Exchange getOrderExchange() {
return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}
/**
* 订单队列与订单交换机进行绑定
* @return
*/
@Bean
public Binding bindOrder() {
return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
}
}
2.RedisCacheConfig.java
package com.example.rabbit.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
3.RedisUtil.java部分代码
/**
* 对指定key的键值减一
* @param key 键
* @return Long
*/
public Long decrBy(String key) {
return redisTemplate.opsForValue().decrement(key);
}
7.controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象。
import com.orange.annotation.AccessLimit;
import com.orange.annotation.LimitNumber;
import com.orange.service.impl.MqStockServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;
@RestController
@Slf4j
@Api(value = "SecKillController", tags = "秒杀控制层")
@RequestMapping("/seck")
public class SecKillController {
@Autowired
private MqStockServiceImpl mqStockService;
/**
* 使用redis+消息队列进行秒杀实现
* @param userName 用户名称
* @param stockName 商品名称
* @return String
*/
@PostMapping(value = "/secKill")
@ApiOperation(value = "redis+消息队列进行秒杀实现", notes = "redis+消息队列进行秒杀实现")
@LimitNumber(value = 2)
@AccessLimit(seconds = 1,maxCount = 800)
public String secKill(@RequestParam(value = "userName") String userName, @RequestParam(value = "stockName") String stockName) {
return mqStockService.secKill(userName, stockName);
}
}
8.需要在springboot得启动类中进行对redis得初始化,简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息。
package com.example.rabbit;
import com.example.rabbit.entity.Stock;
import com.example.rabbit.service.StockService;
import com.example.rabbit.utils.RedisCache;
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;
import java.util.List;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class RabbitApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(RabbitApplication.class, args);
}
@Autowired
private RedisCache redisCache;
@Autowired
private StockService stockService;
@Override
public void run(ApplicationArguments args) throws Exception {
List stockList = stockService.selectList();
for (Stock stockItem : stockList) {
redisCache.setCacheObject(stockItem.getName(),stockItem.getStock(),3600, TimeUnit.SECONDS);
}
}
}
1.项目启动时,redis里的watch会初始化10。
2.打开我们得JMeter工具运行测试(具体使用Jmeter可自行百度)
1)选择中文
2)完成中文之后,我们在测试计划右键,添加一个线程组。
3)给这个线程组的数量为40,这个线程组的作用就是模拟40个用户发送请求,去秒杀;然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求的组件了
4)这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数
点击上方得白色小书本,选择random,1-99得随机数。
5)然后我们把这个函数字符串复制到http得参数上面去。
最后点击运行按钮运行。
5)查看控制台日志,可以看到运行结果已经打印到控制台了,用户名为我们生成的随机数。
再来看下数据库订单表t_order,就保存了10条数据(秒杀成功的),我们初始化的时候给watch库存得数量为10,而我们使用JMeter模拟了40个人发请求,所以这10条数据,也就是40个用户中抢到商品的10个人,也就是线程,谁抢到就是谁得。
6)再来查看下我们得结果树
源码:
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。