SpringBoot+SpringCloud+rabbitmq+redis(秒殺)

前言

無論開發者或用戶都知道一個環節,那就是雙11/12秒殺環節,開發者需要把後臺代碼優化到極致以應對大量的用戶請求,而用戶即需要快速的手速進行搶單環節~

本次實例介紹&流程&版本選型

  • 實例介紹:

由於是演示環境我就一個服務完成當前需求了,同一時間多個請求進入搶購即下單環節,而我們要做的就是限流當前請求,時服務端起到高吞吐量,以達到最高效率完成搶單環節。

  • 實例流程:

用戶下單訪問請求,到業務層簡單處理不耗時業務後即丟放一個標識或訂單號到隊列當中去(此處就用到了rabbitmq消息中間件),之後設立監聽器即消費者,用於實時去消費當前請求,消費者處理時先從redis緩存中獲取當前商品庫存量,第一次請求如果沒有即從數據庫獲取,然後放入緩存當中去,一個請求過來即庫存量自減一位然後緊接創建訂單,直至庫存爲0時不接收任何消息其餘請求直接放入死信隊列,由延時隊列去處理通用的返回結果即(搶單失敗,商品已售空等信息。


  • 實例版本選型:

- SpringBoot:2.0.0 -
-SpringCloud:Finchley.M9

- JDK:1.8
- maven:3.8
- rabbitmq:2.0.2
- redis:2.0.5

數據庫腳本

訂單表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for order_info
-- ----------------------------
DROP TABLE IF EXISTS `order_info`;
CREATE TABLE `order_info`  (
  `id` bigint(20) NOT NULL,
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '地址',
  `order_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '訂單編號',
  `com_modity` tinyint(255) NULL DEFAULT NULL COMMENT '商品類型',
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
庫存表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for stok_order
-- ----------------------------
DROP TABLE IF EXISTS `stok_order`;
CREATE TABLE `stok_order`  (
  `id` bigint(20) NOT NULL,
  `com_modity` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '商品類型',
  `stock_count` tinyint(255) NULL DEFAULT NULL COMMENT '庫存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of stok_order
-- ----------------------------
INSERT INTO `stok_order` VALUES (1, '1', 5);

SET FOREIGN_KEY_CHECKS = 1;

核心maven依賴

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.itxwl</groupId>
    <artifactId>rabbitmq-study</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbitmq-study</name>
    <packaging>jar</packaging>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.M9</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--SpringBoot整合Rabbitmq依賴引入-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

配置文件

spring:
  rabbitmq:
    host: localhost #地址
    port: 5672 #端口
    username: xwl #用戶名
    password: 258000 #密碼
    virtual-host: /xwl #虛擬機地址/權限
    template:
      #exchange: xwl_exchange #交換機
      retry:
        initial-interval: 10000ms #如果沒有接收到消費回執,即每隔10秒訪問一次
        enabled: true #開啓重試機制
        max-interval: 30000ms #最大疊加制不超過30
        max-attempts: 2 #每次訪問一次間隔後都以2倍疊加再次訪問
    listener:
      simple:
        default-requeue-rejected: false #監聽器拋出異常而拒絕的消息是否被重新放回隊列。默認值爲true
        #none無應答確認發送
        #manual意味着監聽者必須通過調用Channel.basicAck()來告知所有的消息。
        #auto意味着容器會自動應答,除非MessageListener拋出異常,這是默認配置方式。
        acknowledge-mode: manual
        prefetch: 1
        concurrency: 5 #消費者監聽 分發5個隊列執行
      type: simple
    publisher-confirms: true
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/mythread?characterEncoding=utf-8
eureka:
  client:
    register-with-eureka: false #單體應用測試-不註冊eureka
    fetch-registry: false #不發送心跳到註冊中心
#配置mybatis-plus打印sql語句於控制檯
mybatis-plus:
  configuration:
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #開啓駝峯命名轉換
    map-underscore-to-camel-case: true

Rabbitmq交換機&隊列配置類

package com.itxwl.rabbitmqstudy.seckill.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
 * @Auther: 
 * @Date: 2020/6/16 16:09
 * @Description:
 */
@Component
@Configuration
@SuppressWarnings("ALL")
public class RabbitMqConfig {
    //交換機名稱
    public static  String ITEM_TOPIC_EXCHANGE="xwl_exchange";
    //隊列名稱
    public static final String ITEM_QUEUE = "xwl_queue";
    //聲明交換機
    @Bean("xwlTopicExchange")
    public Exchange topicExchange(){
        return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
    }
    //聲明主隊列
    @Bean("xwlQueue")
    public Queue itemQueue(){
        Map<String, Object> args = new HashMap<>();
        //聲明死信交換器
        args.put("x-dead-letter-exchange", "deal_exchange");
        //聲明死信路由鍵
        args.put("x-dead-letter-routing-key", "DelayKey");
        //聲明主隊列如果發生堵塞或其它-10秒自動消費消息
        args.put("x-message-ttl",10000);
        return QueueBuilder.durable(ITEM_QUEUE).withArguments(args).build();
    }
    //主隊列綁定交換機以及-路由(此處採用TOPC通配符)
    @Bean
    public Binding itemQueueExchange(@Qualifier("xwlQueue") Queue queue,
                                     @Qualifier("xwlTopicExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
    }
    //聲明死信隊列
    @Bean("dealQueue")
    public Queue dealQueue(){
        return QueueBuilder.durable("deal_queue").build();
    }
    //聲明死信交換機
    @Bean("dealExchange")
    public Exchange dealExchange(){
        return ExchangeBuilder.topicExchange("deal_exchange").durable(true).build();
    }
    //死信隊列綁定交換機以及路由key
    @Bean
    public Binding dealQueueExchange(@Qualifier("dealQueue") Queue queue,
                                     @Qualifier("dealExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("DelayKey").noargs();
    }
}

下單請求入口

package com.itxwl.rabbitmqstudy.seckill.controller;
import com.itxwl.rabbitmqstudy.seckill.service.ISeckKillService;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
/**
 * @Auther: 
 * @Date: 2020/6/23 15:18
 * @Description:
 */
@RestController
@AllArgsConstructor
@RequestMapping("skill")
public class SeckKillController {
    private RedisTemplate redisTemplate;
    private ISeckKillService seckKillService;
    /**
    **模擬用戶組
    **/
    public List<String> getUsers(){
      return   Arrays.asList("張三","李四","王五","趙六","李珏","郭思","呂布","王月英","嘻哈","田豐");
    }
    /**
     * 模擬搶單-入口
     * @param -用戶名
     * @param -商品類型 -此處默認1
     * @return
     */
    @GetMapping("getShopByType")
    public String getShopByType(){
        //爲了演示結果需
        redisTemplate.opsForValue().set("stockCount",null);
        getUsers().stream().forEach(name ->{
            seckKillService.getShopByType(name,1);
        });
        return "已經收到您的搶購申請,請稍後留意信息提示結果";
    }
}

業務處理(請求放致隊列)

聲明接口

package com.itxwl.rabbitmqstudy.seckill.service;

public interface ISeckKillService {
    void getShopByType(String userName, Integer shopType);
}

接口實現類

package com.itxwl.rabbitmqstudy.seckill.service.impl;

import com.itxwl.rabbitmqstudy.seckill.service.ISeckKillService;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;


/**
 * @Auther: 
 * @Date: 2020/6/23 15:25
 * @Description:
 */
@Service
@AllArgsConstructor
public class SeckKillServiceImpl implements ISeckKillService {
    //交換機名稱
    public static final String ITEM_TOPIC_EXCHANGE = "xwl_exchange";
    //下單隊列路由key
    public static final String ITEM_ROUKEY = "item.sendKill";
    //引入消息發送API
    private RabbitTemplate rabbitTemplate;
    @Override
    @SneakyThrows
    public void getShopByType(String userName, Integer shopType) {
        //不做任何操作處理~直接進去隊列-
        rabbitTemplate.convertAndSend(ITEM_TOPIC_EXCHANGE,ITEM_ROUKEY,userName);
    }
}

實體創建

package com.itxwl.rabbitmqstudy.seckill.eneity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Auther: 
 * @Date: 2020/6/23 15:40
 * @Description:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NameTypeDto {
    private String userName;
    private Integer shopType;
}
package com.itxwl.rabbitmqstudy.seckill.eneity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Auther: 
 * @Date: 2020/6/23 15:12
 * @Description:
 */
@TableName("order_info")
@Data
@NoArgsConstructor
public class OrderInfo {

    private Long id;
    private String userName;
    private String address;//地址
    private String orderCode;//訂單編號
    private Integer comModity;//商品類型

    public OrderInfo(String userName,String orderCode,Integer comModity){
        this.userName=userName;
        this.address="河南衛輝唐莊鎮。。。";
        this.orderCode=orderCode;
        this.comModity=comModity;
    }
}

package com.itxwl.rabbitmqstudy.seckill.eneity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * @Auther: 
 * @Date: 2020/6/23 15:16
 * @Description:
 */
@TableName("stok_order")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StokOrder {
    private Long id;
    private String comMondity;//商品類型
    private Integer stockCount;//庫存
}

數據層處理接口

package com.itxwl.rabbitmqstudy.seckill.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itxwl.rabbitmqstudy.seckill.eneity.StokOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
@SuppressWarnings("ALL")
public interface StokOrderMapper  extends BaseMapper<StokOrder> {
    @Select("select stock_count from stok_order where com_modity=#{shopType}")
    Integer findCountByShopType(@Param("shopType") Integer shopType);
}
package com.itxwl.rabbitmqstudy.seckill.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itxwl.rabbitmqstudy.seckill.eneity.OrderInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}

隊列監聽核心業務處理

注意:springboot普通類加載注入IOC對象通常爲空,因爲普通類加載並沒有將屬性加載入spring容器當中去,所以需要使用一個註解@PostConstruct:在當前類加載時初始化一次賦值對象即可使用IOC特性

package com.itxwl.rabbitmqstudy.seckill.config;
import com.itxwl.rabbitmqstudy.seckill.eneity.OrderInfo;
import com.itxwl.rabbitmqstudy.seckill.mapper.OrderInfoMapper;
import com.itxwl.rabbitmqstudy.seckill.mapper.StokOrderMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
 * @Auther: 
 * @Date: 2020/6/23 16:15
 * @Description:
 */
@Component
@Configuration
@SuppressWarnings("ALL")
public class SendKillListener {
    //交換機名稱
    public static final String ITEM_TOPIC_EXCHANGE = "xwl_exchange";
    //隊列名稱
    public static final String ITEM_QUEUE = "xwl_queue";
    private RedisTemplate re;
    private StokOrderMapper stokOrderMapper;
    private OrderInfoMapper orderInfoMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private StokOrderMapper sr;
    @Autowired
    private OrderInfoMapper oo;
    @PostConstruct
    public void init() {
        this.re = redisTemplate;
        this.orderInfoMapper = oo;
        this.stokOrderMapper = sr;
    }
    /**
     * 監聽主隊列~
     *
     * @param message
     * @param map
     * @param channel
     * @throws InterruptedException
     * @throws IOException
     */
    @RabbitListener(queues = "xwl_queue")
    public void sendMiss(Message message, @Headers Map<String, Object> map, Channel channel) throws InterruptedException, IOException {
        String msg = new String(message.getBody(), "UTF-8");
        Integer shopCount=0;
        //第一個請求進來獲取庫存-先去緩存redis找對應key值如果沒有發送一個連接查詢後續無需再次獲取庫存
        if (StringUtils.isEmpty(re.opsForValue().get("stockCount"))) {
            re.opsForValue().set("stockCount", stokOrderMapper.findCountByShopType(1), 60, TimeUnit.MINUTES);
            shopCount = ((Integer) re.opsForValue().get("stockCount"));
        }else {
            //自減緩存內庫存量- 每次減-
             shopCount = ((Integer) re.opsForValue().get("stockCount"))-1;
        }
        //如果庫存量小於等於0即已經搶完
        if (shopCount<= 0) {
            //即放入死信隊列-推送後續隊列內消息即爲搶單失敗-
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            //返回 不做後續處理
            return;
        }
        //如果庫存數不爲0即沒有搶完
        //數據庫存儲訂單
        orderInfoMapper.insert(new OrderInfo(msg, UUID.randomUUID().toString(), 1));
        //設置緩存庫存量key過期時間-redis自行刪除(賦值減值)
        re.opsForValue().set("stockCount", shopCount, 60, TimeUnit.MINUTES);
        //手動設置ACK接收確認當前消息消費完畢
        ;
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println(msg + "搶購成功,恭喜!");
    }
    /**
     * 監聽死信隊列-即推送搶單失敗
     *
     * @param message
     * @param map
     * @param channel
     * @throws InterruptedException
     * @throws IOException
     */
    @RabbitListener(queues = "deal_queue")
    public static void sendMiss2(Message message, @Headers Map<String, Object> map, Channel channel) throws InterruptedException, IOException {
        String msg = new String(message.getBody(), "UTF-8");
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println(msg + "商品已被搶空~下次再來");
    }
}

結果展示

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

源碼地址:

https://github.com/xwlgithub/xuewenliang/tree/master/rabbitmq-study

交流

如果我的這個業務處理有問題的話加我一塊交流交流
Q:2509647976
wx: x331191249

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