实现基于SpringBoot+Maven+Mysql+Redis+RabbitMQ 的高并发秒杀系统(限时秒杀)
原理:
秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。
对于网络流量瞬间变大问题,最常用的办法就是将页面静态化,也就是我们常说的前后端分离。把静态页面直接缓存到用户的浏览器中,当用户需要获取数据时,就从服务端接口动态获取。这样会大大节省网络的流量,如果再加上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即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</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</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.3-beta1</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.0.0</version>
</dependency>
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<T> extends Mapper<T>, MySqlMapper<T> {
}
- 新建mapper层
OrderMapper.java
import com.demo.base.service.GenericMapper;
import com.demo.pojo.Order;
public interface OrderMapper extends GenericMapper<Order> {
void insertOrder(Order order);
}
StockMapper.java
import com.demo.base.service.GenericMapper;
import com.demo.pojo.Stock;
public interface StockMapper extends GenericMapper<Stock> {
}
- 编写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<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;
}
}
- 编写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<Stock> 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<Stock> 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<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);
}
public <T> T get(String key, Class<T> 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控制台查看运行结果,以及数据库表的变化。
上一篇: 设计模式--简单工厂-工厂方法
下一篇: 自定义事务注解
推荐阅读
-
实现基于SpringBoot+Maven+Mysql+Redis+RabbitMQ 的高并发秒杀系统(限时秒杀)
-
高并发下的商城秒杀设计php+mysql+redis的实现
-
php结合redis实现高并发下的抢购、秒杀功能
-
php结合redis实现高并发下的抢购、秒杀功能(代码实例)
-
php结合redis实现高并发下的抢购、秒杀功能的实例
-
PHP秒杀系统 高并发高性能的极致挑战(完整版)
-
高并发下的商城秒杀设计php+mysql+redis的实现
-
php结合redis实现高并发下的抢购、秒杀功能的实例
-
php结合redis实现高并发下的抢购、秒杀功能的实例
-
php和redis实现高并发下的抢购以及秒杀功能示例详解