一、安装
首先,在使用rabbitMQ之前需要先在本地安装,因为rabbit是基于Erlang的,所以需要先下载安装Erlang,具体的步骤请点击这个链接:https://www.cnblogs.com/ericli-ericli/p/5902270.html
Redis安装教程:https://www.runoob.com/redis/redis-install.html
RabbitMQ安装教程:https://www.linuxprobe.com/install-rabbitmq-on-centos-7.html
我就是按照这个步骤安装的,所以一步步来肯定没问题,需要注意的是安装成功后记得给账号分配权限,我就是因为没注意权限才导致好长时间项目不能启动,看我下面的截图注意红框中的信息,这是分配后的,如果为分配读写权限会是另一种黄色的背景颜色。
二、代码
1、创建SpringBoot项目并添加依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ljs</groupId>
<artifactId>miaosha_idea</artifactId>
<version>1.0-SNAPSHOT</version>
<name>miaosha_idea</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- FIXME change it to the project's website -->
<!--<url>http://www.example.com</url>-->
<!--<properties>-->
<!--<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>-->
<!--<maven.compiler.source>1.7</maven.compiler.source>-->
<!--<maven.compiler.target>1.7</maven.compiler.target>-->
<!--</properties>-->
<!--<dependencies>-->
<!--<dependency>-->
<!--<groupId>junit</groupId>-->
<!--<artifactId>junit</artifactId>-->
<!--<version>4.11</version>-->
<!--<scope>test</scope>-->
<!--</dependency>-->
<!--</dependencies>-->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency> -->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打war包插件 -->
<!-- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin> -->
<!-- 打jar包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
2、然后是项目的application配置文件加入rabbitMQ的连接信息,在下图,填入自己rabbit安装成功后设置的账号信息就行,主要配置了thymeleaf,redis,RabbitMQ,数据库的一些配置,注意redis,RabbitMQ和数据库的端口,ip和用户名,密码。
#thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
#拼接前缀与后缀,去创建templates目录,里面放置模板文件
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
#mybatis
#是否打印sql语句
#spring.jpa.show-sql= true
mybatis.type-aliases-package=com.ljs.miaosha.domain
#mybatis.type-handlers-package=com.example.typehandler
#下划线转换为驼峰
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
#ms --3000ms--->3s
mybatis.configuration.default-statement-timeout=3000
#mybatis配置文件路径
#mapperLocaitons
mybatis.mapper-locaitons=classpath:com/ljs/miaosha/dao/*.xml
#druid
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seckill?useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=sasa
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.filters=stat
# 初始化大小,最小,最大
spring.datasource.initialSize=100
spring.datasource.minIdle=500
spring.datasource.maxActive=1000
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=30000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#redis 配置服务器等信息
redis.host=127.0.0.1
redis.port=6379
redis.timeout=10
#redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxldle=500
redis.poolMaxWait=500
#static 静态资源配置,设置静态文件路径css,js,图片等等
#spring.mvc.static-path-pattern=/static/** spring.mvc.static-path-pattern=/**
spring.resources.add-mappings=true
spring.resources.cache-period=3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/
#RabbitMQ配置
spring.rabbitmq.host=122.51.85.243
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=StrongPassword
spring.rabbitmq.virtual-host=/
#消费者数量
spring.rabbitmq.listener.simple.concurrency=10
#消费者最大数量
spring.rabbitmq.listener.simple.max-concurrency=10
#消费,每次从队列中取多少个,取多了,可能处理不过来
spring.rabbitmq.listener.simple.prefetch=1
spring.rabbitmq.listener.auto-startup=true
#消费失败的数据重新压入队列
spring.rabbitmq.listener.simple.default-requeue-rejected=true
#发送,队列满的时候,发送不进去,启动重置
spring.rabbitmq.template.retry.enabled=true
#一秒钟之后重试
spring.rabbitmq.template.retry.initial-interval=1000
#
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10s
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
因为rabbitMQ队列的模式有好几个,当前使用的为Direct模式,此模式类似于一对一的关系(放入队列的时候指定队列名称,消费当前队列消息时,使用@RabbitListener注解指定获取某个名称的队列)。
具体的实现主要为三个步骤:
1.定义一个任意名称的队列,注意类及方法上的注解
package com.rabbitmq.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
static final String QUEUE = "product_secondsKill";
/**
* Direct模式
* @return
*/
@Bean
public Queue directQueue() {
// 第一个参数是队列名字, 第二个参数是指是否持久化
return new Queue(QUEUE, true);
}
}
2.消息入队方法,我是在service层调用的
package com.rabbitmq.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Sender {
private final static Logger log = LoggerFactory.getLogger(Sender.class);
@Autowired
AmqpTemplate amqpTemplate;
int i = 0;
/**
* 消息入队-在需要的时候调用即可
* @param msg此为商品的id,根据此id在消费消息时验证Redis中商品的库存是否充足
*/
public void sendDirectQueue(String msg) {
log.info(">>>>>>>>>>>>>>秒杀请求已发送,商品ID为:"+msg);
try {
//第一个参数是指要发送到哪个队列里面, 第二个参数是指要发送的内容
amqpTemplate.convertAndSend(RabbitMQConfig.QUEUE, msg);
//此处为了记录并发请求下,请求的次数及消息传递的次数
log.info("发送请求>>>>>>>>>>>>>"+i++);
} catch (AmqpException e) {
log.error("请求发送异常:"+e.getMessage());
e.printStackTrace();
}
}
}
3.消息出列(消费消息)的方法,相关逻辑再次执行
package com.rabbitmq.config;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.business.dao.product.ProductMapper;
import com.enums.ErrorCodeEnum;
import com.excetion.ServerBizException;
import com.rabbitmq.client.Channel;
import com.util.redis.RedisStringUtils;
@Component
public class Receiver {
private final static Logger log = LoggerFactory.getLogger(Receiver.class);
@Autowired
RedisStringUtils redisStringUtils;
@Autowired
ProductMapper productMapper;
int i = 0;
/**
* 消息出列(消费消息)方法-和消息入列没有直接的调用关系
* 是通过注解中指定的名称进行的关联
* @param msg-传递进来的数据
* @param channel-注意,注意,注意这两个参数
* @param message-注意,注意,注意这两个参数
* @throws IOException
*/
@RabbitListener(queues = RabbitMQConfig.QUEUE)
public void receiverDirectQueue(String msg,Channel channel, Message message) throws IOException{
log.info(">>>>>>>>>>>>>>>>>接收到秒杀请求,商品ID为:"+msg+"检查Redis中库存是否为0");
try {
long num = redisStringUtils.decr(msg);
if(num < 0) {
/**
* 此处不能判断等于0,因为当商品库存为1时,Redis执行递减返回为0
* 如果判断为0商品最后不能卖完也就是当库存为1时此处就抛异常了
*/
throw new ServerBizException(ErrorCodeEnum.PRODUCT_INVENTORY_IS_NULL);
}
log.info("接收时>>>>>>>>>>>"+i++);
Map map = new HashMap<>();
map.put("id", msg);
map.put("Quantity", num);
//根据商品的id和库存同步数据到MySQL
if(productMapper.updateQuantityByPid(map) == 0) {
throw new ServerBizException("同步到商品表异常!");
}
} catch (Exception e) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
e.printStackTrace();
}
}
}
值得注意的是上面代码方法上需要注意的两个参数,笔者就是因为一开始不懂没有做这些逻辑导致此处出现错误,错误为:当此方法出现异常(比如:并发进来两个秒杀请求而库存中只有一件商品时,判断Redis中库存会进入死循环,一直抛出异常信息直到停止项目),解决为捕获异常并且在catch下面加入此代码,此处的作用请看下图
此段代码的作用为:当消费消息出现异常时,我们需要取消确认,这时我们可以使用 Channel 的 basicReject 方法。其中第一个参数为指定 delivery tag,第二个参数说明如何处理这个失败消息:true为将该消息重新放入队列头,false为忽略该消息。
1.我使用了spring的监听器ApplicationListener,当项目初始化完成的时候自动扫描数据库中的商品信息,商品ID为key,库存为value,如果还需要其他限制逻辑(比如:秒杀的开始,结束时间和支持秒杀的某个时间段)自己去实现就行,我这里没做其他逻辑。下面为具体代码
package com.rabbitmq;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Service;
import com.business.dao.product.ProductMapper;
import com.business.model.product.Product;
import com.util.redis.RedisStringUtils;
@Service
@Scope("singleton")
public class ApplicationInitListener implements ApplicationListener<ContextRefreshedEvent>{
private static final Logger logger = LoggerFactory.getLogger(ApplicationInitListener.class);
@Autowired
ProductMapper productMapper;
@Autowired
RedisStringUtils redisStringUtils;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null) {
logger.info(">>>>>>>>>>>>项目初始化完成,执行监听器中逻辑");
//mapper中的sql,返回全部上架(支持秒杀)的商品集合
List<Product> list = productMapper.selectTimerTask();
Iterator<Product> it = list.iterator();
while(it.hasNext()) {
Product p = it.next();
logger.info("商品ID:"+p.getId()+"商品库存:"+p.getpQuantity());
try {
redisStringUtils.set(String.valueOf(p.getId()), String.valueOf(p.getpQuantity()));
} catch (Exception e) {
logger.error("当前商品ID:"+p.getId()+"库存:"+p.getpQuantity()+"放入Redis缓存异常<<<<<<<<<<<<<<<<<<<<");
e.printStackTrace();
}
}
}
}
}
上边代码很简洁也有相关的注释,所以不再废话,下面就贴一下从请求进入到返回的全部逻辑,先来controller的代码,方法中只是调用了service中的方法,不多说
/**
* 秒杀入口
* @param response
* @param Pid-商品id,做检查库存使用
* @param Uid-用户id,做订单和用户关联使用(比如生成成功秒杀商品的用户订单表)
* 我这里没做多余的逻辑,只看了相关情况的返回结果,有需要的可以自己去实现
*/
@RequestMapping(value = "secondsKill", method = RequestMethod.POST)
public void secondsKill(HttpServletResponse response, String Pid, Integer Uid) {
try {
//模拟发送100次请求,库存设置为少于100查看结果,此100次请求为顺序请求(未并发)
//for(int i=0; i<100; i++) {
boolean result = productService.secondsKill(Pid, String.valueOf(Uid));
if(result) {
ResponseUtil.renderSuccessJson(response, "success", result);
}
//}
} catch (ServerBizException e) {
ResponseUtil.renderFailJson(response, e.getErrCode());
e.printStackTrace();
}
}
接下来是service的方法。
/**
* 商品秒杀
* @param Pid
* @param Uid
* @return
* @throws ServerBizException
*/
int i = 0;
public boolean secondsKill(String Pid, String Uid) throws ServerBizException {
boolean result = true;
//根据商品id获取Redis中的库存数量
String num = redisStringUtils.get(Pid).toString();
System.out.println("redis>>>>>>>>>>"+num);
if(new Long(num) <= 0) {
result = false;
throw new ServerBizException(ErrorCodeEnum.PRODUCT_INVENTORY_IS_NULL);
}
//消息入队,调用相关方法
sender.sendDirectQueue(Pid);
//只为验证请求及发送消息次数
System.out.println("service>>>>>>>>>>"+i++);
return result;
}
还有一个模拟多线程请求的方法,在下边
public static void main(String[] args) {
// 运用java工具类中线程池
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) { // 开启五个线程
String url = "http://localhost:8080/product/secondsKill";
Map<String, Object> paramStr = getHttpParamStr();
pool.execute(new ServiceThreadTest(url, paramStr));
}
}
public static Map<String, Object> getHttpParamStr() {
Map<String, Object> param = new HashMap<String, Object>();
param.put("Pid", "1");
param.put("Uid", "10");
return param;
}
下面测试一下,首先在商品表中设置库存,我先设置10个。
然后我们启动项目,看有没有执行监听器中的逻辑,自动把商品库存放入Redis
项目启动完成,我们往上翻看下控制台信息
可以看到,相关的信息已经输出的控制台了。
三、项目资料等
本人由于能力有限,为了不误人子弟,在此提供源码和教学视频来帮助大家学习
1、项目代码
链接:https://pan.baidu.com/s/1qZjAuce1gRRXHHDHZgDLmw
提取码:iroz
2、该秒杀项目的视频,来源慕课网 https://www.imooc.com
链接:https://pan.baidu.com/s/1vjBlJ82iiIjBSkEN9hbEsA
提取码:33qx