SpringBoot+Mysql+Redis+RabbitMQ隊列+多線程模擬併發-實現請求併發下的商城秒殺系統

一、安裝

首先,在使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章