先放上github地址:spike-system,可以直接下载完整项目运行测试

SpringBoot+JPA+MySql+Redis+RabbitMQ 秒杀系统

技术栈:SpringBoot, MySql, Redis, RabbitMQ, JPA,(lombok)

Controller

/put : 上架 "watch"商品10个

@RequestMapping("/put")
String put(@RequestParam String orderName, @RequestParam Long count)

/sec : 秒杀购买商品

    @RequestMapping("/sec")
String sec(String userName, String orderName)

Guide

项目参考自

入门基础必备,使用Idea实现SpringBoot+Mysql+Redis+RabbitMQ+Jmeter模拟实现高并发秒杀

简化了原项目,将tkmybatis替换成JPA。并将一些比较复杂的操作尽可能简化。

项目结构

		│  MainSystemApplication.java

├─config
│ MyRabbitConfig.java
│ MyRedisConfig.java

├─controller
│ Test.java

├─dao
│ StockRe.java
│ TOrderRe.java

├─domain
│ Stock.java
│ TOrder.java

└─service
MQOrderService.java
MQStockService.java
RedisService.java

说明

stock代表库存,TORder代表订单。当一个购买行为发生时,应用会先查询购买物品的库存是否足够,如果足够,则将库存减一,并生成一个订单数据到数据库保存,最后返回购买成功的消息给用户

START

首先利用idea创建springboot项目时勾选lombok\web\redis\RabbitMQ\MySql\JPA,生成的pom依赖如下

    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

首先要创建两个实体类到domain包下

package com.chunmiao.mainsystem.domain;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable; /**
* 库存实体类
*/
@Data
@Entity
public class Stock implements Serializable {
@Id
@GeneratedValue
private Long id; private String name; //货品库存数量
private Long stock; }

package com.chunmiao.mainsystem.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor; import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable; /**
* 订单实体类
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TOrder implements Serializable{
@Id
@GeneratedValue
private Long id; private String orderName; private String orderUser; }

然后创建jpa的Repository到dao包下。

jpa实现crud的操作只需要继承JPARepository接口即可自动提供find\sava\delete实现类给用户使用。

package com.chunmiao.mainsystem.dao;

import com.chunmiao.mainsystem.domain.TOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; @Repository
public interface TOrderRe extends JpaRepository<TOrder,Long> {
}

由于JPA默认只提供findById的操作,如果想通过别的字段查询需要自己提供接口,当然,JPA也会为这个接口自动提供实现类

package com.chunmiao.mainsystem.dao;

import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; @Repository
public interface StockRe extends JpaRepository<Stock,Long> { Stock findByName(String name); }

然后创建config包,配置RabbitMQ和Redis.

RabbitMQ这里涉及到交换机、队列、路由键的概念,需要搜索RabbitMQ基础教程了解一下

package com.chunmiao.mainsystem.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class MyRabbitConfig { public final static String TORDER_EXCHANG = "TORDER_EXCHANG"; public final static String TORDER_QUEUE = "TORDER_QUEUE"; public final static String TORDER_ROUTING_KEY = "TORDER_ROUTING_KEY"; public final static String STOCK_EXCHANG = "STOCK_EXCHANG"; public final static String STOCK_QUEUE = "STOCK_QUEUE"; public final static String STOCK_ROUTING_KEY = "STOCK_ROUTING_KEY"; /**
* 订单消息
* 1.创建交换机
* 2.创建队列
* 3.通过路由键绑定交换机和队列
*/
@Bean
public Exchange getTOrderExchang() {
return ExchangeBuilder.directExchange(TORDER_EXCHANG).build();
} @Bean
public Queue getTOrderQueue() {
return QueueBuilder.nonDurable(TORDER_QUEUE).build();
} @Bean
public Binding bindTOrder() {
return BindingBuilder.bind(getTOrderQueue()).to(getTOrderExchang()).with(TORDER_ROUTING_KEY).noargs();
} /**
* 库存消息
* 1.创建交换机
* 2.创建队列
* 3.通过路由键绑定交换机和队列
*/
@Bean
public Exchange getStockExchange() {
return ExchangeBuilder.directExchange(STOCK_EXCHANG).build();
} @Bean
public Queue getStockQueue() {
return QueueBuilder.nonDurable(STOCK_QUEUE).build();
} @Bean
public Binding bindStock() {
return BindingBuilder.bind(getStockQueue()).to(getStockExchange()).with(STOCK_ROUTING_KEY).noargs();
} }

Redis的配置主要是序列化的配置,应该不配置也可以的。但是配置后查看的时候更美观,而且性能会更好更简洁。

package com.chunmiao.mainsystem.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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration
public class MyRedisConfig { @Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> re = new RedisTemplate<>();
re.setConnectionFactory(redisConnectionFactory);
re.setKeySerializer(new StringRedisSerializer());
re.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class)); // 不能用generic的Serializer,有存Long取Integer的bug
re.afterPropertiesSet();
return re;
} }

然后到Service层,逻辑是RedisService提供增加库存、查询库存服务,增加库存时需要调用StockRe保存增加库存到数据库。

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.StockRe;
import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; /**
* stock信息缓存到redis
*/
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StockRe stockRe; public void put(String key, Long value) {
BoundValueOperations<String, Object> bp = redisTemplate.boundValueOps(key);
Long count = (Long) bp.get();
if ( count!= null){
count = count >= 0 ? count + value : value;
} else count = value;
bp.set(count); Stock stock = stockRe.findByName(key);
if (stock == null) {
stock = new Stock();
stock.setName(key);
stock.setStock(0l);
}
long l = stock.getStock() + value;
stock.setStock(l);
stockRe.save(stock);
}
// 返回当前商品库存-1的结果,如果库存小于0时直接返回,这样调用它的类就知道已经没有库存了
public Long decrBy(String key) {
BoundValueOperations<String, Object> bp = redisTemplate.boundValueOps(key);
Long count = (Long) bp.get();
if (count == null) return -1l;
if (count >= 0) {
count--;
bp.set(count);
}
return count;
}
}

MQOrderService用于消费队列中的订单消息,创建新订单保存到数据库。只需要使用@RabbitMQListener即可实现监听

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.TOrderRe;
import com.chunmiao.mainsystem.domain.TOrder;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import static com.chunmiao.mainsystem.config.MyRabbitConfig.TORDER_QUEUE;
import static com.chunmiao.mainsystem.config.MyRabbitConfig.STOCK_QUEUE; /**
* 从MQ中拿消息,创建一个新订单到数据库
*/
@Service
public class MQOrderService {
@Autowired
private TOrderRe orderRe; @RabbitListener(queues = TORDER_QUEUE)
public void saveOrder(TOrder order) {
System.out.println("创建新订单");
orderRe.save(order);
}
}

同理,监听库存消息并修改数据库值

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.StockRe;
import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import static com.chunmiao.mainsystem.config.MyRabbitConfig.STOCK_QUEUE; /**
* 从MQ拿消息,使库存减少一个
*/
@Service
public class MQStockService {
@Autowired
private StockRe stockRe; @RabbitListener(queues = STOCK_QUEUE)
public void decrStock(String orderName) {
System.out.println("减少数据库的库存");
Stock stock = stockRe.findByName(orderName);
if (stock!= null) {
stock.setStock(stock.getStock() - 1);
stockRe.save(stock);
}
}
}

最后一个类,controller,提供接口。购买逻辑是直接调用redisService提供的方法,实现操作 库存-1,返回该结果,如果结果>=0说明库存充足,发送创建新订单和库存-1消息给RabbitMQ,然后直接返回购买成功的结果给用户即可;如果库存不足,直接返回购买失败即可。使用RabbitMQ的好处是,只需要关心是否有库存,然后简单的发送消息之后就不用再管了,同时做到了削峰的好处

package com.chunmiao.mainsystem.controller;

import com.chunmiao.mainsystem.domain.TOrder;
import com.chunmiao.mainsystem.service.RedisService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import static com.chunmiao.mainsystem.config.MyRabbitConfig.*; @RestController
public class Test {
@Autowired
private RedisService redisService; @Autowired
private RabbitTemplate rabbitTemplate; @RequestMapping("/put")
String put(@RequestParam String orderName, @RequestParam Long count) {
redisService.put(orderName, count);
return "上架商品\n" + orderName + ":" + count;
} @RequestMapping("/sec")
String sec(String userName, String orderName) {
String msg = "秒杀用户:" + userName + "\n" + "秒杀商品: " + orderName;
System.out.println("\n---------------------------------------------");
System.out.println("秒杀用户:" + userName + "\n" + "秒杀商品: " + orderName);
Long count = redisService.decrBy(orderName);
// 秒杀成功
System.out.println("当前商品数量为: " + (count + 1));
if (count >= 0) {
System.out.println("库存充足");
// 创建新订单
rabbitTemplate.convertAndSend(TORDER_EXCHANG,TORDER_ROUTING_KEY,
TOrder.builder()
.orderName(orderName)
.orderUser(userName)
.build());
// 创建库存-1消息
rabbitTemplate.convertAndSend(STOCK_EXCHANG,STOCK_ROUTING_KEY,orderName);
System.out.println("秒杀成功");
msg += "成功";
} else {
System.out.println("库存不足");
msg += "失败";
}
return msg;
}
}

最后可以运行起来,先put一个商品进去,再利用Jmeter进行压力测试,Jmeter的使用可以参考本文参考的文章。

IDEA SpringBoot+JPA+MySql+Redis+RabbitMQ 秒杀系统的更多相关文章

  1. springboot+jpa+mysql+redis+swagger整合步骤

    springboot+jpa+MySQL+swagger框架搭建好之上再整合redis: 在电脑上先安装redis: 一.在pom.xml中引入redis 二.在application.yml里配置r ...

  2. 基于springboot+bootstrap+mysql+redis搭建一套完整的权限架构【六】【引入bootstrap前端框架】

    https://blog.csdn.net/linzhefeng89/article/details/78752658 基于springboot+bootstrap+mysql+redis搭建一套完整 ...

  3. springboot+jpa+mysql+swagger整合

    Springboot+jpa+MySQL+swagger整合 创建一个springboot web项目 <dependencies> <dependency>      < ...

  4. spring-boot jpa mysql emoji utfmb4 异常处理

    spring-boot jpa mysql utf8mb4 emoji 写入失败 mysql database,table,column 默认为utf8mb4 Caused by: java.sql. ...

  5. Springboot Jpa: [mysql] java.sql.SQLException: Duplicate entry 'XXX' for key 'PRIMARY'

    前言 1.问题背景 偶尔会出现登录请求出错的情况,一旦失败就会短时间内再也登录不上,更换浏览器或者刷新可能会暂时解决这个问题. 项目运行日志如下: 2022-07-21 09:43:40.946 DE ...

  6. SpringBoot+Jpa+SpringSecurity+Redis+Vue的前后端分离开源系统

    项目简介: eladmin基于 Spring Boot 2.1.0 . Jpa. Spring Security.redis.Vue的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 ...

  7. 从 0 使用 SpringBoot MyBatis MySQL Redis Elasticsearch打造企业级 RESTful API 项目实战

    大家好!这是一门付费视频课程.新课优惠价 699 元,折合每小时 9 元左右,需要朋友的联系爱学啊客服 QQ:3469271680:我们每课程是明码标价的,因为如果售价为现在的 2 倍,然后打 5 折 ...

  8. SpringBoot+Jpa+MySql学习

    上一篇介绍了springboot简单整合mybatis的教程.这一篇是介绍springboot简单整合jpa的教程. 由于jpa的功能强大,后续会继续写关于jpa的介绍已经使用,本文只是简单介绍一下它 ...

  9. docker-compose部署mysql,redis,rabbitmq

    version: '3' services: mysql: image: mysql:5.7.31 container_name: mysql restart: always command: --c ...

随机推荐

  1. 20190923-11Linux crond 系统定时任务 000 019

    crond 服务管理 1.重新启动crond服务 [root@hadoop101 ~]# service crond restart centOS7是 systemctl restart crond ...

  2. JUC使用

    1.什么是JUC 源码 + 官方文档 面试高频问! java.util 工具包.包.分类 业务:普通的线程代码 Thread Runnable 没有返回值.效率相比入 Callable 相对较低! 2 ...

  3. 【漏洞复现篇】CVE-2020-1472-微软NetLogon权限提升-手把手教学-简单域环境搭建与Exp执行

    一.漏洞简介 NetLogon 远程协议是一种在 Windows 域控上使用的 RPC 接口,被用于各种与用户和机器认证相关的任务.最常用于让用户使用 NTLM 协议登录服务器,也用于 NTP 响应认

  4. appcan 文件下载与预览

    用appcan开发的app如何在手机上查看附件和预览附件呢?今天就为大家介绍一下,用APP看附件实大是太方便了. 1.直接上代码吧,首先要初始化插件用到的所有方法.这个方法中 cbIsFileExis ...

  5. Python-变量-字符串

    str 字符串如何表示字符串? 单行 单引号 '' 如果字符串中有单引号就需要双引号表示,反之亦然 双引号 " " 换行表示 \ one_str = "简洁胜于优雅&qu ...

  6. makefile的隐式规则

    target := exe source_code = hello.c OBJS = $(source_code:.c=.o) $(target):$(OBJS) gcc $^ -o $@ clean ...

  7. 《Java从入门到失业》第五章:继承与多态(5.1-5.7):继承

    5.继承与多态 5.1为什么要继承 最近我儿子迷上了一款吃鸡游戏<香肠派对>,无奈给他买了许多玩具枪,我数了下,有一把狙击枪AWM,一把步枪AK47,一把重机枪加特林(Gatling).假 ...

  8. Webstorm破解版安装教程

    Webstorm破解版: 安装包链接见:https://pan.baidu.com/s/1XJqRtM9C4M8AmH50S9dVDQ 提取码: dah3 内附安装教程, 原创文章,转载请先联系作者

  9. python数据结构之图深度优先和广度优先实例详解

    本文实例讲述了python数据结构之图深度优先和广度优先用法.分享给大家供大家参考.具体如下: 首先有一个概念:回溯 回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标.但当探索到 ...

  10. C++重载>>和<<(输入和输出运算符)详解

    转载:http://c.biancheng.net/view/2311.html 在C++中,标准库本身已经对左移运算符<<和右移运算符>>分别进行了重载,使其能够用于不同数据 ...