本文將圍繞高并發場景中的限流和秒殺需求綜合演示Spring Boot整合JPA、Redis快取和RabbitMQ訊息佇列的做法,
本專案將通過整合Springboot和Redis以及Lua腳本來實作限流和秒殺的效果,將通過RabbitMQ訊息佇列來實作異步保存秒殺結果的效果,
一、專案概述
本專案將要實作的秒殺是指商家在某個時間段以非常低的價格銷售商品的一種營銷活動,
由于商品價格非常低,因此單位時間內發起購買商品的請求會非常多,從而會對系統造巨大的壓力,對此,在一些秒殺系統中往往會整合限流的功能,同時會通過訊息佇列異步地保存秒殺結果,
本章將要實作的限流和秒殺功能歸納如下:
(1)通過Spring Boot的控制器類對外接收秒殺請求,
(2)針對請求進行限流操作,比如秒殺商品的數量是10個,就限定在秒殺開始后的20秒內只有100個請求能參加秒殺,該操作是通過Redis來實作的,
(3)通過限流檢驗的這些請求將會同時競爭若干個秒殺商品,該操作將通過基于Redis的Lua腳本來實作,
(4)為了降低資料庫的壓力,秒殺成功的記錄將通過RabbitMQ佇列以異步的方式記錄到資料庫中,
(5)同時,將通過RestTemple物件以多執行緒的方式模擬發送秒殺請求,以此來觀察本秒殺系統的運行效果,
也就是說,本系統會綜合用到Spring Boot、JPA、Redis和RabbitMQ,相關組件之間的關系如圖所示,

二、基于Redis的Lua腳本分析
Lua使用標準C語言開發而成的,它是一種輕量級的腳本語言,可嵌入基于Redis等的應用程式中,Lua腳本可以駐留在記憶體中,所以具有較高的性能,適用于處理高并發的場景,
Lua腳本的特性
Lua腳本語言是由巴西一所大學的Roberto lerusalimschy 、 Waldemar Celes和 LnHenrique de Figuciredo設計而成的,它具有如下兩大特性
(1)輕量性:Lua只具有一些核心和最基本的庫,所以非常輕便,非常適合嵌入由其他語言撰寫的代碼中,
(2)擴展性:Lua語言中預留了擴展介面和相關擴展機制,這樣在Lua語言中就能很方便地引入其他開發語言的功能,
本章給出的秒殺場景中會向Redis服務器發送多條指令,為了降低網路呼叫的開銷,會把相關Redis命令放在Lua腳本里,通過呼叫Lua腳本只需要耗費少量的網路呼叫代價就能執行多條Redis命令,
此外,秒殺相關的Redis陳述句還需要具備原子性,即這些陳述句要么全都執行,要么全都不執行,而Lua腳本是作為一個整體來執行的,所以可以充分地確保相關秒殺陳述句的原子性,
在Redis中引入Lua腳本
在啟動Redis服務器以后,可以通過redis-cli命令運行lua腳本,具體步驟如下:
-
可以在
C:work\redisConf\lua目錄中創建redisCallLua.lua檔案,在其中撰寫Lua腳本,注意,Lua腳本檔案的擴展名一般都是.lua, -
在第一步創建的
redisCallLua.lua檔案中加入一行代碼,在其中通過redis.call命令執行set name Peter的命令,redis.call('set', 'name', 'Peter')通過
rdis.call方法在Redis中呼叫Lua腳本時,第一個引數是Redis命令,比如這里是set,第二個引數以及之后的引數是執行該條Redis命令的引數, -
通過如下的
--eval命令執行第二步定義的Lua腳本,其中C:work\redisConf\lua是這條Lua腳本所在的路徑,而redisCallLua.lua是腳本名,redis-cli --eval C:\work\redisConf\lua\redisCallLua.lua
上述命令運行后,得到的回傳結果是空(nil),原因是該Lua腳本只是通過set命令設定了值,并沒有回傳結果,不過通過get name命令就能看到通過這條Lua腳本快取的name值,具體是Peter,
如果Lua腳本包含的陳述句很少,那么還可以直接用eval命令來執行該腳本,具體做法是,
先通過redis-cli陳述句連接到Redis服務器,隨后再執行如下eval命令:
eval "redis.call('set','BookName','Spring Boot')" 0
從上述陳述句中能看到,在該條eval命令之后通過雙引號引入了待執行的Lua腳本,在該
腳本中依然是通過redis.call陳述句執行Redis的set命令,進行設定快取的操作,
在該eval命令之后還指定了Lua腳本中KEYS型別引數的個數,這里是0,表示該Lua腳本沒有KEYS型別的引數,注意,這里設定的是KEYS型別的引數,而不是ARGV型別的引數,下文將詳細說明這兩種引數的差別,
Lua腳本的回傳值和引數
在Lua腳本中,可以通過retum陳述句回傳執行的結果,這部分對應的語法比較簡單,
同時,Redis在通過eval命令執行Lua腳本時,可以傳入KEYS和ARGV這兩種不同型別的引數,它們的區別是,可以用KEYS引數來傳入Redis命令所需要的引數,可以用ARGV引數來傳入自定義的引數,通過如下兩個eval執行Lua腳本的命令,可以看到這兩種引數的差別,
127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]" 1 keyono argvone argvtwo
1) "keyone"
2) "argvone"
3) "argvtwo"
127.0.0.1:6379> eval "return {KEYS[1].ARGV[1],ARGV[2]}" 2 keyone argvone argvtwo
1) "key1"
2) "argvtwo"
在第1行eval陳述句中,KEYS[1]表示KEYS型別的第一個引數,而ARGV[1]和ARGV[2]對應地表示第一個和第二個ARGV型別的引數,
在第1行eval陳述句中,雙引號之后的1表示KEYS型別的引數個數是1,所以統計引數個數時并不把ARGV自定義型別的引數統計在內,隨后的keyone, argvone和argvtwo分別對應KEYS[1]、ARGV[1]和ARGV[2].
執行第一行對應的Lua腳本時,會看到如第2~4行所示的輸出結果,這里輸出了KEYS[1]、
ARGV[1]和ARGV[2]這3個引數對應的值,
第5行腳本和第1行的差別是,表示KEYS引數個數的值從1變成了2,但這里第2個引數是ARGV型別的,而不是KEYS型別的,所以這條Lua腳本陳述句會拋棄第2個引數,即ARGV[1],通過第6行和第7行的輸出結果能驗證這點,
所以,在通過eval命令執行Lua腳本時,一定要確保引數個數和型別的正確性,同時,這里再次提醒,eval命令之后傳入的引數個數是KEYS型別引數的個數,而不是ARGV型別的,
分支陳述句
在Lua腳本中,可以通過if…else陳述句來控制代碼的執行流程,具體語法如下:
if(布爾運算式) then
布爾運算式是true時執行的陳述句
else
布爾運算式是false時執行的陳述句
end
通過如下的ifDemo.lua范例,讀者可以看到在Lua腳本中使用分支陳述句的做法,
if redis.call('exists','studentID')==1 then
return 'Existed'
else
redis.call('set','StudentID','001');
return 'Not Existed'
end
在第1行中,通過if陳述句判斷redis.call命令執行的exists陳述句是否回傳1,如果是,則表示StudentID鍵存在,就會執行第2行的returm 'Existed’陳述句回傳Existed,否則走第3行的else流程,執行第4行和第5行的陳述句,設定StudentID的值,并通過retum陳述句回傳Not Existed,
由此可以看到在Lua腳本中使用if分支陳述句的做法,該腳本的運行結果是:第一次運行時,由于StudentID鍵不存在,因此會走else流程,從而看到Not Existed的輸出,而在第二次運行時,由于此時該鍵已經存在,因此會直接輸出’Existed’的結果,
三、實作限流和秒殺功能
本節將要創建的QuickBuyDemo專案中,一方面會用到上文提到的Lua腳本實作限流和秒殺的功能,另一方面將通過RabbitMQ訊息佇列實作異步保存秒殺結果的功能,
創建專案并撰寫組態檔
可以在IDEA集成開發環境中創建名為QuickBuyDemo的Maven專案,在該專案的pom.xml檔案中通過如下關鍵代碼引入所需要的依賴包:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.10</version>
</dependency>
</dependencies>
這里通過第2-5行代碼引入了SpringBoot的依賴包,通過第6-9行代碼引入了RabbitMQ訊息佇列相關的依賴包,通過第10-13行代碼引入了Redis相關的依賴包,通過第14-23行代碼引入了HTTP客戶端相關的依賴包,在本專案中將通過HTTP客戶端模擬客戶請求,從而驗證秒殺效果,
在本專案resources目錄的application.properties組態檔中,將通過如下代碼配置訊息佇列和Redis快取:
rabbitmq.host=127.0.0.1
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.password=guest
redis.host=localhost
redis.port=6379
在該組態檔中,通過第1~4行代碼配置了RabbitMQ的連接引數,通過第5行和第6行代碼配置了Redis的連接引數,
撰寫啟動類和控制器類
本專案的啟動類如下,由于和大多數的Spring Boot專案啟動類完全一致,因此不再重復講述,
package prj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SpringBootApp.class, args);
}
}
本專案的控制器類代碼如下,在該Controller控制器類的第11-25行代碼中封裝了實作秒殺服務的quickBuy方法,該方法是以quickBuy/{item}/{person}格式的URL請求對外提供服務的,其中item引數表示商品,而person引數則表示商品的購買人,
package prj.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import prj.receiver.BuyService;
@RestController
public class Controller {
@Autowired
private BuyService buyService;
@RequestMapping("/quickBuy/{item}/{person}")
public String quickBuy(@PathVariable String item, @PathVariable String person){
//20秒里限流100個請求
if(buyService.canVisit(item, 20,100)) {
String result = buyService.buy(item, person);
if (!result.equals("0")) {
return person + " success";
} else {
return person + " fail";
}
}
else{
return person + " fail";
}
}
}
在quickBuy方法中,首先通過第14行的buyService.canVisit方法對請求進行了限流操作,這里在20秒中只允許有100個請求訪問,如果通過限流驗證,那么會繼續通過第15行的buyService.buy方法進行秒殺操作,注意,這里的實作限流和秒殺功能的代碼都封裝在第10行定義的BuyService類中,
訊息佇列的相關配置
在本專案的RabbitMQConfig類中將配置RabbitMQ的訊息佇列和訊息交換機,具體代碼如下:
package prj;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig{
//定義含主題的訊息佇列
@Bean
public Queue objectQueue() {
return new Queue("buyRecordQueue");
}
//定義交換機
@Bean
TopicExchange myExchange() {
return new TopicExchange("myExchange");
}
@Bean
Binding bindingObjectQueue(Queue objectQueue,TopicExchange exchange) {
return BindingBuilder.bind(objectQueue).to(exchange).with("buyRecordQueue");
}
}
其中通過第9行的objectQueue方法創建了名為buyRecordQucue的訊息佇列,該訊息隊同將向用戶傳輸秒殺的結果,通過第14行的myExchange方法創建了名為myExhnge的清息交換機,并通過第18行的bindingObjectQueue方法根據buyRecordQucue主題系結了上述訊息以列和訊息交換機,
實作秒殺功能的Lua腳本
在本專案中,實作秒殺效果的Lua腳本代碼如下:
local item = KEYS[1]
local person = ARGV[1]
local left = tonumber(redis.call('get',item))
if (left>=1) then
redis.call ('decrby',item,1)
redis.call ('rpush", 'personList',person)
return 1
else
在該腳本中,首先通過KEYS[1]引數傳入待秒殺的商品,并賦予item物件,再通過ARGV[1]引數傳入發起秒殺請求的用戶,并賦子person物件,
隨后在第3行中,通過get item命令從Redis快取中獲取該商品還有多少庫存,再通過第4行的if陳述句進行判斷,
如果發現該商品剩余的庫存數量大于等于1,就會執行第5~7行的Lua腳本,先通過decrby命令把庫存數減1,再呼叫rpush命令記錄當前秒殺成功的用戶,并通過第7行的return陳述句回傳1,表示秒殺成功,如果發現庫存數已經小于1,那么會直接通過第9行的陳述句返且0,表示秒殺失敗,
在業務實作類中實作限流和秒殺
在BuyService.java中,將呼叫Redis和Lua腳本實作限流和秒殺的功能,具體代碼如下:
package prj.receiver;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import prj.model.buyrecord;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class BuyService {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private AmqpTemplate amqpTemplate;
public boolean canVisit(String item, int limitTime, int limitNum) {
long curTime = System.currentTimeMillis();
// 在zset里存入請求
redisTemplate.opsForZSet().add(item, curTime, curTime);
// 移除時間范圍外的請求
redisTemplate.opsForZSet().removeRangeByScore(item,0,curTime - limitTime * 1000);
// 統計時間范圍內的請求個數
Long count = redisTemplate.opsForZSet().zCard(item);
// 統一設定所有請求的超時時間
redisTemplate.expire(item, limitTime, TimeUnit.SECONDS);
return limitNum >= count;
}
public String buy(String item, String person){
String luaScript = "local person = ARGV[1]\n" +
"local item = KEYS[1] \n" +
"local left = tonumber(redis.call('get',item)) \n" +
"if (left >= 1) \n" +
"then redis.call('decrby',item,1) \n" +
" redis.call('rpush','personList',person) \n" +
"return 1 \n" +
"else \n" +
"return 0\n" +
"end\n" +
"\n" ;
String key=item;
String args=person;
DefaultRedisScript<String> redisScript = new DefaultRedisScript<String>();
redisScript.setScriptText(luaScript);
//呼叫lua腳本,請注意傳入的引數
Object luaResult = redisTemplate.execute((RedisConnection connection) -> connection.eval(
redisScript.getScriptAsString().getBytes(),
ReturnType.INTEGER,
1,
key.getBytes(),
args.getBytes()));
//如果秒殺成功,向訊息佇列發訊息,異步插入到資料庫
if(!luaResult.equals("0") ){
buyrecord record = new buyrecord();
record.setItem(item);
record.setPerson(person);
amqpTemplate.convertAndSend("myExchange","buyRecordQueue",record);
}
//根據lua腳本的執行情況回傳結果
return luaResult.toString();
}
}
在上述代碼中,首先通過第2-11行的import陳述句引入了本類所要用到的依賴包,隨后在第15行中定義了呼叫Redis會用到的redisTemplate物件,在第17行中定義了向RabbitMQ訊息佇列發送訊息所要用到的amqpTemplate物件,
第18行的canVisit方法實作了限流效果,該方法的item引數表示待限流的商品,limitTime和LimitNum引數分別表示在指定時間內需要限流的請求個數,
在該方法中使用Redis的有序集合實作了限流效果,具體的做法是,在第21行的代碼中,通過zadd方法把表示操作型別的item作為鍵插入有序集合,插入時用表示當前時間的curTime作為值,以保證值的唯一性,同樣再用curTime值作為有序集合中元素的score值,
隨后在第23行中,通過removeRangeByScore命令移除從0到距當前時間limitTime范圍內的資料,比如限流的時間范圍是20秒,那么通過這條命令就能在有序集合中移除score范圍從0到距離當前時間20秒的資料,從而確保有序集合只保存最近20秒內的請求,

在此基礎上,通過第25行代碼用zcard命令統計有序集合內鍵為item的個數,如果通過第28行的布爾陳述句發現當前個數還沒達到限流的上限,該方法就會回傳true,表示該請求能繼續,否則回傳false,表示該請求將會被限流,
同時,需要通過第27行的expire陳述句設定有序集合中資料的超時時間,這樣就能確保在限流以及秒殺動作完成后這些鍵能自動洗掉,
第30行定義的buy方法將會實作秒殺的功能,其中先通過第31~41行代碼定義實作秒殺功能的Lua腳本,該腳本之前分析過,隨后再通過第47一52行代碼使用redisTemplate.execute方法執行這段Lua腳本,
在執行時,會通過第50行代碼指定KEYS型別引數的個數,通過第51行和第52行代碼傳入該腳本執行時所需要用到的KEYS和ARGVS引數,
隨后會通過第54行的f陳述句判斷秒殺腳本的執行結果,如果秒殺成功,那么會通過第55~58行代碼用amqpTemplate物件向buyRecordQueue佇列發送包含秒殺結果的record物件,最后,再通過第61行的陳述句回傳秒殺的結果,
觀察秒殺效果
至此,可以通過如下步驟啟動Redis、RabbitMQ和QuickBuyDemo專案,并觀察秒殺效果,
- 在命令列中通過
rabbitmq-server.bat start命令啟動RabbitMQ, - 通過運行redis-server.exe啟動Redis服務器,并通過運行redis-cli.exe啟動Redis客戶端,隨后在Redis客戶端通過
set Computer 10命令向Redis中快取一條庫存資料,表示有10個Computer可供秒殺, - 在QuickBuyDemo專案中,通過運行SpringBootApp.java啟動類啟動該專案,成功啟動后,在瀏覽器中輸入
http:localhost:8080/quickBuy/Computer/Tom發起秒殺請求,其中Computer引數表示秒殺的商品,而Tom則表示發起秒殺請求的人,
輸入后,能在瀏覽器中看到Tom success的結果,隨后到Redis客戶端視窗運行get Computer命令,能看到Computer的庫存數量會降到9,由此可以確認秒殺成功,同時,可以通過lindex personList 0命令觀察到成功發起秒殺請求的人是Tom,
四、以異步方式保存秒殺結果
如果在上述QuickBuyDemo專案中直接把秒殺結果插入MySQL資料庫,那么當秒殺請求并發量很高時會對資料庫造成很大的壓力,所以在該專案中會通過訊息佇列把秒殺結果傳輸到DBHandlerPrj專案中,用異步的方式保存資料,從而降低資料庫的負載壓力,
創建專案并設計資料庫
首先需要創建名為DBHandlerPrj的Maven專案,在其中實作異步保存秒殺資料的功能,該專案的pom.xml檔案如下,其中通過第2-5行代碼引入了Spring Boot依賴包,通過第6-9行代碼引入了RabbitMO訊息佇列的依賴包,通過第10~18行代碼引入了JPA和MySQL的依賴包,
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
本專案將會用到如表所示的buyrecord表,該表是創建在本地MySQL的QuickBuy資料表(schema)中的,在其中將會保存秒殺結果,
| 欄位名 | 型別 | 說明 |
|---|---|---|
| item | 字串 | 秒殺成功的商品名 |
| person | 字串 | 秒殺成功的用戶 |
而本專案的啟動類SpringBootAppjava和QuickBuyDemo專案中的完全一致,所以不再重復說明,
配置訊息佇列和資料庫引數
在本專案resources目錄的application.yml檔案中,將通過如下代碼配置訊息佇列和資料庫連接引數,
server:
port: 8090
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
spring:
jpa:
show-sql: true
hibernate:
dll-auto: validate
datasource:
url: jdbc:mysql://localhost:3306/QuickBuy?serverTimezone=GMT
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
由于之前的QuickBuyDemo專案已經占用了8080埠,因此本組態檔將通過第1行和第2行代碼設定作業埠為8090,隨后,本組態檔將通過第3~7行代碼設定RabbiMQ訊息佇列的連接引數,具體是連接到本地5672埠,且連接所用的用戶名和密碼都是guest,
由于本專案是通過JPA的方式連接MySQL庫的,因此本組態檔通過第8-12行代碼配置了JPA的引數,通過第13-17行代碼配置了MySQL的連接引數,
此外,和QuickBuyDemo專案一樣,本專案依然是在RabbitMQConfg.java組態檔中設定RabbitMQ訊息佇列和交換機,具體代碼如下,其中配置的訊息佇列名字buyRecordQueue與交換機的名字myExchange需要和QuickBuyDemo專案中的定義保持一致,
package prj;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig{
//定義含主題的訊息佇列
@Bean
public Queue objectQueue() {
return new Queue("buyRecordQueue");
}
//定義交換機
@Bean
TopicExchange myExchange() {
return new TopicExchange("myExchange");
}
@Bean
Binding bindingObjectQueue(Queue objectQueue,TopicExchange exchange) {
return BindingBuilder.bind(objectQueue).to(exchange).with("buyRecordQueue");
}
}
監聽訊息佇列并保存秒殺結果
在本專案的QuickBuySevivce.java檔案中將會監聽buyRecordQueue訊息佇列,并把秒殺結果存入MySOL資料表,具體代碼如下:
package prj.service;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import prj.model.buyrecord;
import prj.repo.BuyRecordRepo;
@Component
@RabbitListener(queues = "buyRecordQueue")
public class QuickBuyService {
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private BuyRecordRepo buyRecordRepo;
@RabbitHandler
public void saveBuyRecord(buyrecord record){
buyRecordRepo.save(record);
}
}
在本類的第10行通過@RabbitListener注解說明將要監聽buyRecordQueue訊息佇列,當該訊息佇列有訊息時,會觸發本類第17行的saveBuyRecord方法,該方法被第16行的@RabbitHandler注解所修飾,在該方法中會呼叫JPA類buyRecordRepo的save方法向資料表中保存秒殺結果,
QuickBuyServce類中用到的模型類buyrecord和QuickBuyDemo專案中的很相似,由于該類需要通過訊息佇列在網路中傳輸,因此需要像第9行那樣實作Serializable介面,
package prj.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="buyrecord")
public class buyrecord implements Serializable {
@Id
@Column(name = "person")
private String person;
@Column(name = "item")
private String item;
public void setItem(String item) {
this.item = item;
}
public void setPerson(String person) {
this.person = person;
}
public String getItem() {
return item;
}
public String getPerson() {
return person;
}
}
全鏈路效果演示
開發好上述兩個專案以后,可以用對如下步驟觀察全鏈路的秒殺效果:
-
啟動RabbitMQ、Redis服務器和客戶端,通過
set Computer 10命令快取秒殺商品的數量,同時通過運行啟動類啟動QuickBuyDemo專案, -
啟動DBHandlerPrj專案
在QuickBuyDemo項日中開發如下的QuickBuyThread.java檔案,在其中用多執行緒的方式模擬多個秒殺情求,代碼如下:
package prj.client;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
class QuickBuyThread extends Thread{
public void run() {
RestTemplate restTemplate = new RestTemplate();
String user = Thread.currentThread().getName();
ResponseEntity<String> entity = restTemplate.
getForEntity("http://localhost:8080/quickBuy/Computer/"+user , String.class);
System.out.println(entity.getBody());
}
}
public class MockQuickBuy {
public static void main(String[] args){
for (int i = 0; i < 15; i++) {
new QuickBuyThread().start();
}
}
}
第4行定義的QuickBuyThread類以繼承Thread類的方式實作了執行緒的效果,在第5行執行緒的run方法中用restTemplate.getForEntity方法模擬發送了秒殺的請求,其中用當前執行緒的名字作為發起秒殺的用戶,
public class MockQuickBuy {
public static void main(String[] args){
for (int i = 0; i < 15; i++) {
new QuickBuyThread().start();
}
}
}
在第12行MockQuickBuy類的main方法中,通過第14行的for回圈啟動了15個執行緒發起秒殺請求,由于之前在Redis快取中設定的Computer商品數量是10個,因此會有10個請求秒殺成功,5個請求不成功,如下輸出陳述句能確認這一結果,
此外,如果再到 MySQL資料庫用select from QuickBuy.buyrecord陳述句觀察秒殺結果,能看到成功秒殺的用戶,這些用戶名和上述輸出結果中的用戶名完全一致,
本文來自于《Spring Boot+Vue.js+分布式組件全堆疊開發訓練營(視頻教學版)》第17章
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/423366.html
標籤:其他
上一篇:2022虎年的期望和新年Flag
