rabbitmq 高并发mysql_SpringBoot+Mysql+Redis+RabbitMQ+Jmeter模拟实现高并发秒杀

本文通过SpringBoot、Mysql、Redis和RabbitMQ模拟高并发秒杀场景,防止超卖。介绍了如何使用tkmybatis框架配置实体类和数据库操作,设置RabbitMQ的交换机、队列和绑定,以及Redis配置。通过Redis减少库存,RabbitMQ消息队列处理订单创建,避免数据库直接操作导致的超卖问题。同时对比了纯数据库操作下的秒杀超卖情况,强调了中间件在高并发场景中的重要性。
摘要由CSDN通过智能技术生成

文章前言

众所周知,当遇到比较多数据不一致的问题时,大多数都是因为并发请求时,没及时处理的原因,提一个电商平台比较经常出现得高并发场景限时秒杀活动,他们是怎么来防止超卖呢?如何实现高并发秒杀呢?。

本文模拟了高并发秒杀,并且防止了超卖,也模拟了纯数据库秒杀超卖得场景,本次模拟demo得框架技术为:SpringBoot+Mysql+Redis+RabbitMQ+tkmybatis

数据库表结构:

3fc06dcf235f62645c8504b8709f369d.png

2e8a7462971ad4a47e631d01315d19ab.png

0219ad05cd1f0108af765310990e9319.png

f95707f63bd9296c20b9f41bad8bd543.png

一个为库存表,一个为订单表,本人使用得是mysql8.0。

完整得项目工具展示

Jmeter :

5aef3dd1b4e2a88cce9038b433ed5f4a.png

redisManager :

5f9efa656211aa08581a70e03dc72286.png

RabbitMQ :

4208a62574c5ca710248e8dcc370ab37.png

编写代码

1.首先新建Springboot项目

0aa015f7cbd8016cf150663025d92835.png

2.可以先不勾选需要得jar包,项目初始化好之后,使用maven导入项目需要得jar包

pom.xml :

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

3.配置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/ktoa?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

spring.redis.password=123456

这时可以启动一下springboot项目是否能够正常启动,如没问题可以继续往下编写!!

4.新建pojo包,添加实体类

Order.java:

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 {

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;

}

Stock.java:

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框架,所以实体类我们需要用到JPA的注解,来实现映射关系!!

f743f3e93d44d09d5ed8fe6ca00065dd.png

5.配置tkmybatis得接口

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

48984e4d25d8562e06efafe3f2eb8c75.png

GenericMapper.interface:

import tk.mybatis.mapper.common.Mapper;

import tk.mybatis.mapper.common.MySqlMapper;

public interface GenericMapper extends Mapper, MySqlMapper {

}

关于这个接口得作用你需要了解太多,你只要知道我们得mapper层需要通过继承它来实现数据库操作,如果你接触过jpa或者mybatis-plus,tkmybatis方式跟它们相似。

6.新建mapper层

新建名为mapper得包,在这个包下面新建

1971e61cfa4987ad9548fe782a12a84b.png

OrderMapper.interface:

import com.spbtrediskill.secondskill.base.service.GenericMapper;

import com.spbtrediskill.secondskill.pojo.Order;

import org.apache.ibatis.annotations.Mapper;

@Mapper

public interface OrderMapper extends GenericMapper {

void insertOrder(Order order);

}

StockMapper.interface:

import com.spbtrediskill.secondskill.base.service.GenericMapper;

import com.spbtrediskill.secondskill.pojo.Stock;

import org.apache.ibatis.annotations.Mapper;

@Mapper

public interface StockMapper extends GenericMapper {

}

7.编写RabbitMQ和redis得配置类

新建config包,新建redis和RabbitMQ得类9462f6933425fa829173ecfacd7d083e.png

MyRabbitMQConfig.java:

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

}

}

8.编写service层

新建service包以及impl包,这里只提供实现类,接口可以自行编写

OrderServiceImpl .java:

import com.spbtrediskill.secondskill.mapper.OrderMapper;

import com.spbtrediskill.secondskill.pojo.Order;

import com.spbtrediskill.secondskill.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);

}

}

StockServiceImpl.java:

import com.spbtrediskill.secondskill.mapper.StockMapper;

import com.spbtrediskill.secondskill.pojo.Stock;

import com.spbtrediskill.secondskill.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;

}

}

9.配置rabbitmq得实现方式以及redis得实现方式

在 service包下面新建MQOrderService.java

这个类属于订单得消费队列

import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;

import com.spbtrediskill.secondskill.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:

这个属于库存得消费队列

import com.spbtrediskill.secondskill.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得操作

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包得完整目录:

637d4f1b2e1e4430b50f750b156eee8a.png

10.编写controller层

在新建得controller包下面新建类SecController.java

该controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象

import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;

import com.spbtrediskill.secondskill.pojo.Order;

import com.spbtrediskill.secondskill.service.OrderService;

import com.spbtrediskill.secondskill.service.RedisService;

import com.spbtrediskill.secondskill.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启动类

最后一步我们需要在springboot得启动类中进行对redis得初始化,简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息

import com.spbtrediskill.secondskill.service.RedisService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication

@MapperScan("com.spbtrediskill.secondskill.mapper")

public class SecondskillApplication implements ApplicationRunner{

public static void main(String[] args) {

SpringApplication.run(SecondskillApplication.class, args);

}

@Autowired

private RedisService redisService;

/**

* redis初始化商品的库存量和信息

* @param args

* @throws Exception

*/

@Override

public void run(ApplicationArguments args) throws Exception {

redisService.put("watch", 10, 20);

}

}

项目得整个目录:

9820d16290684af89c922e41f9d2721c.png

至此我们得项目代码就编写完成了,记得仔细检查是否有遗漏,下面准备进入最重要得测试环节!!

测试前提

上面代码编写完整之后我们可以启动springboot,启动之前记得打开redis和rabbitmq得服务,检查是否出错:

0298256218e341d2e46e93571c3074bb.png

启动成功之后打开Redis Desktop Manager工具,查看是否新建了一个redis :watch、

f36097c14879209777cc9390ff915c94.png

ok,如果好了,现在打开我们得JMeter工具,可能有些人对这个工具很陌生,下面我教大家如何使用JMeter,大佬忽略!!

首先选择中文

8f27d5667b2419601438448047cb4767.png

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

0c8e47fb7eef9492b2c7bcf51c222eda.png

bed3b8ec3263b7f02e9fceafe8e87204.png

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

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

8bd490cc2d5166293e205abadeb61227.png

c5e5ef7facb8c2c40bffe4225b674ff0.png

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

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

7b1fec3b933759e051953b4ad23f815e.png

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

95add4c60e62e022843627785154bc23.png

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

320109d7ccc1bff5b3996cd40ed42efc.png

这些完成之后我们就可以开始发送请求了

dde74a8776b982577c7d1d3bfdba1e95.png运行run

测试结果–redis+rabbitmq

运行之后查看我们得控制台:

220c87c6809e7d58a2fa7359de8b40f0.png

可以看到日志已经打印到控制台了,用户名为我们生成得随机数。

再来看下数据库订单表order:

27265d3b171a9867b09bad4ad8729113.png

图中有10条秒杀到商品得用户信息和商品名,我再帮大家理一理,我们初始化得时候给watch库存得数量为10,而我们使用JMeter模拟了40个人发请求,所以这10条数据,也就是40个用户中抢到商品得10个人,也就是线程,谁抢到就是谁得。

再来查看下我们得结果树:

521c382dc41b0cc0e460b418a5d444be.png

7c1783d3a9a0578e3fb67bb3b53532ad.png

结果树上面有40条请求信息,通过其中我们可以看的每条请求得详细数据以及返回得值。

现在我们再打开redismanager,其中我们初始化为10,现在是-30,可以知道有40个线程去获取了它,现在为-30,每次前测试记得,手动清空缓存!!一定要记得

16c5acad019b811639128452d91a9c77.png

233ac845d1f5a1a09cedaca057d037f9.png

纯数据库方式秒杀结果

上面我们实现了redis+rabbitmq得秒杀,现在我们看看纯数据库方式得秒杀,看看有什么区别:

1.首先网stock库存表新增一条数据,类似于redis得初始化

d5c78a9345c98f19a6c20b1cfd202a56.png

2.在jmeter中修改原来得http请求信息,其中小米对应数据库得商品名

432855ed942fcd216a97d292652ade31.png

清空一下结果树,我们开始运行

03699b939d1cd45f3ef98531ae69e482.png

3.run

控制台:

696ded00c30927c1bd081aca64763892.png

重要得是查看数据库得信息:

602061da646691024767532a9b27c2ba.png

库存已经清空,再看order表

f609fe28e12d7d75083909829f231436.png

这样我们可以看到,明明只有10个库存得商品,抢到得人却不止10个,这样明细超卖了,请求树也可以看的超卖信息

61c73badf40cc793e3e91584e2a5e184.png

总结

从这二个方式实现得秒杀就可以知道二者得区别,以及大概得了解这个过程是怎么实现得,写这篇文章得主要初衷是方便那些刚接触这方面得小白,没有人刚来什么都会。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值