1. 程式人生 > 實用技巧 >IDEA SpringBoot+JPA+MySql+Redis秒殺系統

IDEA SpringBoot+JPA+MySql+Redis秒殺系統

先放上github地址:[spike-system](https://github.com/ghdefe/spike-system),可以直接下載完整專案執行測試

SpringBoot+JPA+MySql+Redis秒殺系統

技術棧:SpringBoot, MySql, Redis, RabbitMQ, JPA,(lombok)

Controller

/put : 上架 "watch"商品10個

@RequestMapping("/put")
    	String put(@RequestParam String orderName, @RequestParam Long count)

/sec : 秒殺購買商品

    @RequestMapping("/sec")  
    	String sec(String userName, String orderName)

Guide

專案參考自
入門基礎必備,使用Idea實現SpringBoot+Mysql+Redis+RabbitMQ+Jmeter模擬實現高併發秒殺
簡化了原專案,將tkmybatis替換成JPA。並將一些比較複雜的操作儘可能簡化。

專案結構

		│  MainSystemApplication.java
		│
		├─config
		│      MyRabbitConfig.java
		│      MyRedisConfig.java
		│
		├─controller
		│      Test.java
		│
		├─dao
		│      StockRe.java
		│      TOrderRe.java
		│
		├─domain
		│      Stock.java
		│      TOrder.java
		│
		└─service
				MQOrderService.java
				MQStockService.java
				RedisService.java

說明

stock代表庫存,TORder代表訂單。當一個購買行為發生時,應用會先查詢購買物品的庫存是否足夠,如果足夠,則將庫存減一,並生成一個訂單資料到資料庫儲存,最後返回購買成功的訊息給使用者

START

首先利用idea建立springboot專案時勾選lombok\web\redis\RabbitMQ\MySql\JPA,生成的pom依賴如下

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

首先要建立兩個實體類到domain包下

package com.chunmiao.mainsystem.domain;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable;

/**
 * 庫存實體類
 */
@Data
@Entity
public class Stock implements Serializable {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    //貨品庫存數量
    private Long stock;

}

package com.chunmiao.mainsystem.domain;

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.io.Serializable;

/**
 * 訂單實體類
 */
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TOrder implements Serializable{
    @Id
    @GeneratedValue
    private Long id;

    private String orderName;

    private String orderUser;

}

然後建立jpa的Repository到dao包下。 jpa實現crud的操作只需要繼承JPARepository介面即可自動提供find\sava\delete實現類給使用者使用。

package com.chunmiao.mainsystem.dao;

import com.chunmiao.mainsystem.domain.TOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TOrderRe extends JpaRepository<TOrder,Long> {
}

由於JPA預設只提供findById的操作,如果想通過別的欄位查詢需要自己提供介面,當然,JPA也會為這個介面自動提供實現類

package com.chunmiao.mainsystem.dao;

import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StockRe extends JpaRepository<Stock,Long> {

    Stock findByName(String name);

}

然後建立config包,配置RabbitMQ和Redis. RabbitMQ這裡涉及到交換機、佇列、路由鍵的概念,需要搜尋RabbitMQ基礎教程瞭解一下

package com.chunmiao.mainsystem.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRabbitConfig {

    public final static String TORDER_EXCHANG = "TORDER_EXCHANG";

    public final static String TORDER_QUEUE = "TORDER_QUEUE";

    public final static String TORDER_ROUTING_KEY = "TORDER_ROUTING_KEY";

    public final static String STOCK_EXCHANG = "STOCK_EXCHANG";

    public final static String STOCK_QUEUE = "STOCK_QUEUE";

    public final static String STOCK_ROUTING_KEY = "STOCK_ROUTING_KEY";

    /**
     * 訂單訊息
     * 1.建立交換機
     * 2.建立佇列
     * 3.通過路由鍵繫結交換機和佇列
     */
    @Bean
    public Exchange getTOrderExchang() {
        return ExchangeBuilder.directExchange(TORDER_EXCHANG).build();
    }

    @Bean
    public Queue getTOrderQueue() {
        return QueueBuilder.nonDurable(TORDER_QUEUE).build();
    }

    @Bean
    public Binding bindTOrder() {
        return BindingBuilder.bind(getTOrderQueue()).to(getTOrderExchang()).with(TORDER_ROUTING_KEY).noargs();
    }

    /**
     * 庫存訊息
     * 1.建立交換機
     * 2.建立佇列
     * 3.通過路由鍵繫結交換機和佇列
     */
    @Bean
    public Exchange getStockExchange() {
        return ExchangeBuilder.directExchange(STOCK_EXCHANG).build();
    }

    @Bean
    public Queue getStockQueue() {
        return QueueBuilder.nonDurable(STOCK_QUEUE).build();
    }

    @Bean
    public Binding bindStock() {
        return BindingBuilder.bind(getStockQueue()).to(getStockExchange()).with(STOCK_ROUTING_KEY).noargs();
    }

}

Redis的配置主要是序列化的配置,應該不配置也可以的。但是配置後檢視的時候更美觀,而且效能會更好更簡潔。

package com.chunmiao.mainsystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class MyRedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> re = new RedisTemplate<>();
        re.setConnectionFactory(redisConnectionFactory);
        re.setKeySerializer(new StringRedisSerializer());
        re.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));   // 不能用generic的Serializer,有存Long取Integer的bug
        re.afterPropertiesSet();
        return re;
    }


}

然後到Service層,邏輯是RedisService提供增加庫存、查詢庫存服務,增加庫存時需要呼叫StockRe儲存增加庫存到資料庫。

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.StockRe;
import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

/**
 * stock資訊快取到redis
 */
@Service
public class RedisService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private StockRe stockRe;

    public void put(String key, Long value) {
        BoundValueOperations<String, Object> bp = redisTemplate.boundValueOps(key);
        Long count = (Long) bp.get();
        if ( count!= null){
            count = count >= 0 ? count + value : value;
        } else count = value;
        bp.set(count);

        Stock stock = stockRe.findByName(key);
        if (stock == null) {
            stock = new Stock();
            stock.setName(key);
            stock.setStock(0l);
        }
        long l = stock.getStock() + value;
        stock.setStock(l);
        stockRe.save(stock);
    }
	// 返回當前商品庫存-1的結果,如果庫存小於0時直接返回,這樣呼叫它的類就知道已經沒有庫存了
    public Long decrBy(String key) {
        BoundValueOperations<String, Object> bp = redisTemplate.boundValueOps(key);
        Long count = (Long) bp.get();
        if (count == null) return -1l;
        if (count >= 0) {
            count--;
            bp.set(count);
        }
        return count;
    }
}

MQOrderService用於消費佇列中的訂單訊息,建立新訂單儲存到資料庫。只需要使用@RabbitMQListener即可實現監聽

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.TOrderRe;
import com.chunmiao.mainsystem.domain.TOrder;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import static com.chunmiao.mainsystem.config.MyRabbitConfig.TORDER_QUEUE;
import static com.chunmiao.mainsystem.config.MyRabbitConfig.STOCK_QUEUE;

/**
 * 從MQ中拿訊息,建立一個新訂單到資料庫
 */
@Service
public class MQOrderService {
    @Autowired
    private TOrderRe orderRe;

    @RabbitListener(queues = TORDER_QUEUE)
    public void saveOrder(TOrder order) {
        System.out.println("建立新訂單");
        orderRe.save(order);
    }
}

同理,監聽庫存訊息並修改資料庫值

package com.chunmiao.mainsystem.service;

import com.chunmiao.mainsystem.dao.StockRe;
import com.chunmiao.mainsystem.domain.Stock;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import static com.chunmiao.mainsystem.config.MyRabbitConfig.STOCK_QUEUE;

/**
 * 從MQ拿訊息,使庫存減少一個
 */
@Service
public class MQStockService {
    @Autowired
    private StockRe stockRe;

    @RabbitListener(queues = STOCK_QUEUE)
    public void decrStock(String orderName) {
        System.out.println("減少資料庫的庫存");
        Stock stock = stockRe.findByName(orderName);
        if (stock!= null) {
            stock.setStock(stock.getStock() - 1);
            stockRe.save(stock);
        }
    }
}

最後一個類,controller,提供介面。購買邏輯是直接呼叫redisService提供的方法,實現操作 庫存-1,返回該結果,如果結果>=0說明庫存充足,傳送建立新訂單和庫存-1訊息給RabbitMQ,然後直接返回購買成功的結果給使用者即可;如果庫存不足,直接返回購買失敗即可。使用RabbitMQ的好處是,只需要關心是否有庫存,然後簡單的傳送訊息之後就不用再管了,同時做到了削峰的好處

package com.chunmiao.mainsystem.controller;


import com.chunmiao.mainsystem.domain.TOrder;
import com.chunmiao.mainsystem.service.RedisService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import static com.chunmiao.mainsystem.config.MyRabbitConfig.*;

@RestController
public class Test {
    @Autowired
    private RedisService redisService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/put")
    String put(@RequestParam String orderName, @RequestParam Long count) {
        redisService.put(orderName, count);
        return "上架商品\n" + orderName + ":" + count;
    }

    @RequestMapping("/sec")
    String sec(String userName, String orderName) {
        String msg = "秒殺使用者:" + userName + "\n" + "秒殺商品: " + orderName;
        System.out.println("\n---------------------------------------------");
        System.out.println("秒殺使用者:" + userName + "\n" + "秒殺商品: " + orderName);
        Long count = redisService.decrBy(orderName);
        // 秒殺成功
        System.out.println("當前商品數量為: " + (count + 1));
        if (count >= 0) {
            System.out.println("庫存充足");
            // 建立新訂單
            rabbitTemplate.convertAndSend(TORDER_EXCHANG,TORDER_ROUTING_KEY,
                    TOrder.builder()
                            .orderName(orderName)
                            .orderUser(userName)
                            .build());
            // 建立庫存-1訊息
            rabbitTemplate.convertAndSend(STOCK_EXCHANG,STOCK_ROUTING_KEY,orderName);
            System.out.println("秒殺成功");
            msg +=  "成功";
        } else {
            System.out.println("庫存不足");
            msg += "失敗";
        }
        return msg;
    }
}

最後可以執行起來,先put一個商品進去,再利用Jmeter進行壓力測試,Jmeter的使用可以參考本文參考的文章。