ConcurrentHashMap原理分析(二)-擴容
先放上github地址: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的使用可以參考本文參考的文章。