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

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