SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀

场景:

商城对某一商品进行秒杀活动,该项目实例中,商品为watch,库存为10,使用jemter测试工具来模拟高并发场景

代码实例:

mysql表结构:

库存表:                                                                          订单表:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第1张图片        SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第2张图片

application.yml文件

server:
  port: 8089

spring:
   rabbitmq:
     virtual-host: /
     host: localhost
     username: guest
     password: guest
   application:
     name: concurrency-project
   redis:
      host: localhost
      port: 6379
      jedis:
        pool:
          max-active: 1024
          max-wait: -1s
          max-idle: 200
      password: 123456
   datasource:
         name: db_concurrency
         type: com.alibaba.druid.pool.DruidDataSource
         druid:
           driver-class-name: com.mysql.jdbc.Driver
           url: jdbc:mysql://localhost:3306/db_concurrency
           username: root
           password:
           initial-size: 1
           min-idle: 1
           max-active: 20
                     #获取连接等待超时时间
           max-wait: 60000
            #间隔多久进行一次检测,检测需要关闭的空闲连接
           time-between-eviction-runs-millis: 60000
   #一个连接在池中最小生存的时间
           min-evictable-idle-time-millis: 300000
           validation-query: SELECT 'x'
           test-while-idle: true
           test-on-borrow: false
           test-on-return: false
   #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
           pool-prepared-statements: false
           max-pool-prepared-statement-per-connection-size: 20


mybatis:
    mapper-locations: mapper/*.xml
    type-aliases-package: com.concurrency.concurrencyproject.model

pagehelper:
  helper-dialect: mysql

mapper:
  mappers: com.concurrency.concurrencyproject.base.service.GenericMapper
  not-empty: false
  identity: MYSQL


 

项目启动类:

@SpringBootApplication
@MapperScan("com.concurrency.concurrencyproject.mapper")
public class ConcurrencyProjectApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(ConcurrencyProjectApplication.class, args);
    }

    @Autowired
    private RedisService redisService;

    /**
     * redis初始化各商品的库存量
     *
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        redisService.put("watch", 10, 20);
    }
}

启动之后redis初始化为:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第3张图片

RabbitMQ配置类:

@Configuration
public class MyRabbitMQConfig {

    //库存交换机
    public static final String STORY_EXCHANGE = "STORY_EXCHANGE";

    //订单交换机
    public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";

    //库存队列
    public static final String STORY_QUEUE = "STORY_QUEUE";

    //订单队列
    public static final String ORDER_QUEUE = "ORDER_QUEUE";

    //库存路由键
    public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";

    //订单路由键
    public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }


    //创建库存交换机
    @Bean
    public Exchange getStoryExchange() {
        return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
    }

    //创建库存队列
    @Bean
    public Queue getStoryQueue() {
        return new Queue(STORY_QUEUE);
    }

    //库存交换机和库存队列绑定
    @Bean
    public Binding bindStory() {
        return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
    }

    //创建订单队列
    @Bean
    public Queue getOrderQueue() {
        return new Queue(ORDER_QUEUE);
    }

    //创建订单交换机
    @Bean
    public Exchange getOrderExchange() {
        return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
    }

    //订单队列与订单交换机进行绑定
    @Bean
    public Binding bindOrder() {
        return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
    }
}

Redis 配置类

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

 

Redis service

@Service
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 设置String键值对
     *
     * @param key
     * @param value
     * @param millis
     */
    public void put(String key, Object value, long millis) {
        redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES);
    }

    public void putForHash(String objectKey, String hkey, String value) {
        redisTemplate.opsForHash().put(objectKey, hkey, value);
    }

    public  T get(String key, Class type) {
        return (T) redisTemplate.boundValueOps(key).get();
    }

    public void remove(String key) {
        redisTemplate.delete(key);
    }

    public boolean expire(String key, long millis) {
        return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS);
    }

    public boolean persist(String key) {
        return redisTemplate.hasKey(key);
    }

    public String getString(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    public Integer getInteger(String key) {
        return (Integer) redisTemplate.opsForValue().get(key);
    }

    public Long getLong(String key) {
        return (Long) redisTemplate.opsForValue().get(key);
    }

    public Date getDate(String key) {
        return (Date) redisTemplate.opsForValue().get(key);
    }

    /**
     * 对指定key的键值减一
     *
     * @param key
     * @return
     */
    public Long decrBy(String key) {
        return redisTemplate.opsForValue().decrement(key);
    }

}

Controller

@Controller
public class SecController {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisService redisService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private StockService stockService;

    /**
     * 使用redis+消息队列进行秒杀实现
     *
     * @param username
     * @param stockName
     * @return
     */
    @RequestMapping("/sec")
    @ResponseBody
    public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
        LOGGER.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
        String message = null;
        //调用redis给相应商品库存量减一
        Long decrByResult = redisService.decrBy(stockName);
        if (decrByResult >= 0) {
            /**
             * 说明该商品的库存量有剩余,可以进行下订单操作
             */
            LOGGER.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName);
            //发消息给库存消息队列,将库存数据减一
            rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName);

            //发消息给订单消息队列,创建订单
            Order order = new Order();
            order.setOrder_name(stockName);
            order.setOrder_user(username);
            rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order);
            message = "用户" + username + "秒杀" + stockName + "成功";
        } else {
            /**
             * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
             */
            LOGGER.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", username);
            message = username + "商品的库存量没有剩余,秒杀结束";
        }
        return message;
    }

    /**
     * 实现纯数据库操作实现秒杀操作
     *
     * @param username
     * @param stockName
     * @return
     */
    @RequestMapping("/secDataBase")
    @ResponseBody
    public String secDataBase(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
        LOGGER.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
        String message = null;
        //查找该商品库存
        Integer stockCount = stockService.selectByExample(stockName);
        LOGGER.info("用户:{}参加秒杀,当前商品库存量是:{}", username, stockCount);
        if (stockCount > 0) {
            /**
             * 还有库存,可以进行继续秒杀,库存减一,下订单
             */
            //1、库存减一
            stockService.decrByStock(stockName);

            //2、下订单
            Order order = new Order();
            order.setOrder_user(username);
            order.setOrder_name(stockName);
            orderService.createOrder(order);
            LOGGER.info("用户:{}.参加秒杀结果是:成功", username);
            message = username + "参加秒杀结果是:成功";
        } else {
            LOGGER.info("用户:{}.参加秒杀结果是:秒杀已经结束", username);
            message = username + "参加秒杀活动结果是:秒杀已经结束";
        }
        return message;
    }
}

RabbitMQ Service

  • MQStockService
@Service
public class MQStockService {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private StockService stockService;

    /**
     * 监听库存消息队列,并消费
     *
     * @param stockName
     */
    @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
    public void decrByStock(String stockName) {
        LOGGER.info("库存消息队列收到的消息商品信息是:{}", stockName);
        /**
         * 调用数据库service给数据库对应商品库存减一
         */
        stockService.decrByStock(stockName);
    }
}
  • MQOrderService
@Service
public class MQOrderService {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private OrderService orderService;

    /**
     * 监听订单消息队列,并消费
     *
     * @param order
     */
    @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
    public void createOrder(Order order) {
        LOGGER.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrder_user(), order.getOrder_name());
        /**
         * 调用数据库orderService创建订单信息
         */
        orderService.createOrder(order);
    }
}

数据库Service

  • StockService
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private StockMapper stockMapper;

    @Override
    public void decrByStock(String stockName) {
        Example example = new Example(Stock.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("name", stockName);
        List stocks = stockMapper.selectByExample(example);
        if (!CollectionUtils.isEmpty(stocks)) {
            Stock stock = stocks.get(0);
            stock.setStock(stock.getStock() - 1);
            stockMapper.updateByPrimaryKey(stock);
        }
    }

    @Override
    public Integer selectByExample(String stockName) {
        Example example = new Example(Stock.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("name", stockName);
        List stocks = stockMapper.selectByExample(example);
        if (!CollectionUtils.isEmpty(stocks)) {
            return stocks.get(0).getStock().intValue();
        }
        return 0;
    }
}
  • OrderService
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

测试:使用jemter测试,40个线程并发测试(也可以更多)

线程组:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第4张图片

测试参数文件:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第5张图片

先测试使用redis+rabbitmq实现高并发秒杀的代码:

http请求:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第6张图片

点击启动,项目日志打印:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第7张图片

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第8张图片

redis数据:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第9张图片

数据库数据:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第10张图片SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第11张图片

jemeter 察看结果树:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第12张图片SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第13张图片

jemeter 聚合报告:

可以看出,使用redis+rabbitmq可以实现高并发场景下的秒杀任务

接下来用纯数据库操作实现高并发场景下的秒杀任务:

将http请求中的地址换成:/secDataBase , 其他地方不改变;同时清空数据库订单表的数据并将商品的库存量初始化为10

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第14张图片

 启动测试,日志打印如下:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第15张图片

可以发现,由于并发的操作,导致多个请求进来的时候读取的商品库存量都是一样的

数据库的数据:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第16张图片

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第17张图片

可以看出,由于并发的情况,导致订单超卖,所有用户都已经下单成功了,并且数据库中的库存也并未归零(随着并发量的增多,数据库中的库存会为0 ,但是会有更多的用户下单成功,也会有用户下单失败,秒杀结束)

jemeter的察看结果树中http返回都是用户下单成功:

SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀_第18张图片

jemeter的聚合报告如下:

至此,这里使用了redis+rabbitmq实现了高并发秒杀场景,并有效的防止了超卖现象,同时听过使用纯数据库操作产生了超卖的现象。

 

 

你可能感兴趣的:(高并发,redis,rabbitmq,springboot,秒杀,redis,springboot,消息队列,MQ,数据库,高并发)