Spring Boot中使用Redis和Lua腳本實(shí)現(xiàn)延時(shí)隊(duì)列的方案
延時(shí)隊(duì)列是一種常見(jiàn)的需求。延時(shí)隊(duì)列允許我們延遲處理某些任務(wù),這在處理需要等待一段時(shí)間后才能執(zhí)行的操作時(shí)特別有用,如發(fā)送提醒、定時(shí)任務(wù)等。文中,將介紹如何在Spring Boot環(huán)境下使用Redis和Lua腳本來(lái)實(shí)現(xiàn)一個(gè)延時(shí)隊(duì)列。
一、延遲隊(duì)列的四大使用場(chǎng)景
訂單超時(shí)自動(dòng)處理
在電商領(lǐng)域,延遲隊(duì)列對(duì)于處理訂單超時(shí)問(wèn)題至關(guān)重要。一旦用戶下單,訂單信息便進(jìn)入延遲隊(duì)列,并預(yù)設(shè)超時(shí)時(shí)長(zhǎng)。若用戶在此時(shí)間內(nèi)未完成支付,訂單信息將由消費(fèi)者從隊(duì)列中提取,并執(zhí)行如取消訂單、庫(kù)存釋放等后續(xù)操作,高效且自動(dòng)化。
優(yōu)惠券到期溫馨提醒
借助延遲隊(duì)列,我們可以實(shí)現(xiàn)優(yōu)惠券到期前的溫馨提醒服務(wù)。將臨近過(guò)期的優(yōu)惠券信息入隊(duì),并設(shè)定精確延遲時(shí)間。時(shí)間一到,系統(tǒng)自動(dòng)提醒用戶優(yōu)惠券的到期日,引導(dǎo)他們及時(shí)享用優(yōu)惠,提升用戶體驗(yàn)。
智能消息重試策略
在處理網(wǎng)絡(luò)請(qǐng)求失敗、數(shù)據(jù)庫(kù)異常等情況時(shí),延遲隊(duì)列提供了智能的消息重試機(jī)制。當(dāng)消息初次處理失敗,它會(huì)被置入隊(duì)列并設(shè)定重試延時(shí)。延時(shí)結(jié)束后,系統(tǒng)會(huì)再次嘗試處理,確保消息的可靠傳遞與處理。
異步通知與定時(shí)提醒
延遲隊(duì)列還能用于實(shí)現(xiàn)異步通知和定時(shí)提醒功能。用戶完成操作后,系統(tǒng)將相關(guān)通知信息加入隊(duì)列,并設(shè)定發(fā)送延時(shí),確保在最佳時(shí)機(jī)向用戶推送通知,既不打擾用戶,又能保持信息的時(shí)效性。
二、如何利用ZSet實(shí)現(xiàn)延遲隊(duì)列
Redis的ZSet(有序集合)是一個(gè)根據(jù)分?jǐn)?shù)對(duì)唯一字符串成員進(jìn)行排序的數(shù)據(jù)結(jié)構(gòu)。在多個(gè)成員分?jǐn)?shù)相同時(shí),它們會(huì)按照字典順序進(jìn)行排列。ZSet不僅常用于排行榜和限速器等場(chǎng)景,還可巧妙用于實(shí)現(xiàn)延遲隊(duì)列。

基于ZSet的延遲隊(duì)列實(shí)現(xiàn)原理,主要利用了其有序性和按分?jǐn)?shù)排序的特點(diǎn)。以下是具體實(shí)現(xiàn)步驟的簡(jiǎn)要介紹:
定義延遲消息:在ZSet中,我們將延遲消息作為成員,而其對(duì)應(yīng)的延遲時(shí)間則作為該成員的分?jǐn)?shù)。這里的延遲時(shí)間通常是一個(gè)未來(lái)的時(shí)間戳,它指明了消息應(yīng)當(dāng)被處理的確切時(shí)刻。
消息入隊(duì):使用ZADD命令,我們可以輕松地將消息添加到ZSet中,并為其指定相應(yīng)的延遲時(shí)間作為分?jǐn)?shù)。
定期檢查:通過(guò)定期輪詢ZSet,我們可以利用ZRANGEBYSCORE命令來(lái)檢索那些分?jǐn)?shù)(即延遲時(shí)間)小于或等于當(dāng)前時(shí)間戳的消息,這些消息即為到期的、需要被處理的消息。
消息處理與出隊(duì):一旦找到到期的消息,我們可以使用ZPOPMIN命令將它們從ZSet中移除,并進(jìn)行相應(yīng)的處理。在處理過(guò)程中,需要考慮并發(fā)性和數(shù)據(jù)一致性問(wèn)題,確保每條消息都能被正確處理且不會(huì)被重復(fù)處理。
后續(xù)操作與通知:為了提高系統(tǒng)的性能和可靠性,我們可以結(jié)合Redis的Pub/Sub機(jī)制。在處理完消息后,發(fā)布一個(gè)事件來(lái)通知其他服務(wù)或訂閱者進(jìn)行后續(xù)的操作或處理。
通過(guò)這種方式,ZSet能夠有效地按照消息的延遲時(shí)間順序,逐個(gè)取出并處理到期的消息,從而實(shí)現(xiàn)了一個(gè)高效且可靠的延遲隊(duì)列系統(tǒng)。
三、實(shí)現(xiàn)步驟

在Spring Boot環(huán)境下,實(shí)現(xiàn)一個(gè)基于Redis和Lua腳本的延時(shí)隊(duì)列,需要以下幾個(gè)步驟:
環(huán)境準(zhǔn)備
- 安裝并啟動(dòng)Redis服務(wù)器。
- 在Spring Boot項(xiàng)目中添加
spring-boot-starter-data-redis依賴。
Redis數(shù)據(jù)結(jié)構(gòu)選擇
- 使用Redis的
zset(有序集合)數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)延時(shí)任務(wù)。zset中的元素是唯一的,但分?jǐn)?shù)(score)可以相同,可以用作任務(wù)的延遲時(shí)間戳。
Lua腳本編寫(xiě)
- 編寫(xiě)一個(gè)Lua腳本來(lái)處理隊(duì)列的出隊(duì)和入隊(duì)操作,以確保操作的原子性。
Spring Boot應(yīng)用配置
- 配置Redis連接工廠和Redis模板。
實(shí)現(xiàn)延時(shí)隊(duì)列服務(wù)
- 提供一個(gè)服務(wù)來(lái)管理延時(shí)隊(duì)列,包括入隊(duì)、出隊(duì)、檢查并處理到期的任務(wù)等。
定時(shí)任務(wù)調(diào)度
- 使用Spring的
@Scheduled注解或者Redis的鍵空間通知來(lái)定期檢查并處理到期的任務(wù)。
四、實(shí)現(xiàn)代碼
下面是一個(gè)簡(jiǎn)化版本的實(shí)現(xiàn):
1. 添加Maven依賴
在pom.xml中添加spring-boot-starter-data-redis依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2. 配置Redis
在application.yml或application.properties中配置Redis連接信息:
spring:
redis:
host: localhost
port: 63793. Lua腳本
定義一個(gè)Lua腳本原子性地執(zhí)行出隊(duì)操作。腳本使用Redis的有序集合命令來(lái)查找并移除到期的任務(wù):
-- KEYS[1] 延時(shí)隊(duì)列的key
-- ARGV[1] 當(dāng)前時(shí)間戳
-- 返回值:任務(wù)ID(如果存在)或nil
local key = KEYS[1]
local currentTime = tonumber(ARGV[1])
local task = redis.call('zrangebyscore', key, 0, currentTime, 'LIMIT', 0, 1)
if #task > 0 then
redis.call('zremrangebyscore', key, 0, currentTime)
return task[1]
else
return nil
end可以稍微優(yōu)化一下上面的Lua腳本,以減少不必要的操作和提高效率:
-- KEYS[1] 延時(shí)隊(duì)列的key
-- ARGV[1] 當(dāng)前時(shí)間戳
-- 返回值:任務(wù)ID(如果存在)或nil
local key = KEYS[1]
local currentTime = tonumber(ARGV[1])
-- 使用zrangebyscore和zrem的組合命令zpopmin,它原子性地返回并移除分?jǐn)?shù)最低的元素
-- zpopmin命令(5.0及以上版本)
local task = redis.call('zpopmin', key, 1, 'BLOCK', 0, 'SCORES')
-- zpopmin返回的是一個(gè)包含兩個(gè)元素的數(shù)組,第一個(gè)元素是分?jǐn)?shù),第二個(gè)是成員
if task and #task > 0 and task[2] and tonumber(task[1]) <= currentTime then
return task[2] -- 返回任務(wù)ID
else
return nil
end注意:
zpopmin命令是一個(gè)原子性的操作,它返回并刪除分?jǐn)?shù)最低的元素。避免了先查詢后刪除可能帶來(lái)的并發(fā)問(wèn)題。zpopmin`命令在Redis 5.0及以上版本中可用。
zpopmin命令可以設(shè)置阻塞時(shí)間,這里設(shè)置為0,表示不阻塞。如果希望在沒(méi)有可用元素時(shí)阻塞等待一段時(shí)間,可以調(diào)整這個(gè)值。
腳本檢查了返回的分?jǐn)?shù)是否小于等于當(dāng)前時(shí)間戳,以確保只處理到期的任務(wù)。
如果Redis版本低于5.0zpopmin將不可用,可以使用zrangebyscore和zrem的組合,但需要注意并發(fā)問(wèn)題。
4. 實(shí)現(xiàn)延時(shí)隊(duì)列服務(wù)
@Service
public class DelayQueueService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String DELAY_QUEUE_KEY = "delay_queue";
// 入隊(duì)操作
public void enqueue(String taskId, long delayInSeconds) {
long score = System.currentTimeMillis() / 1000 + delayInSeconds;
stringRedisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskId, score);
}
// 出隊(duì)操作,使用Lua腳本確保原子性
public String dequeue() {
String luaScript = "..."; // 上面定義的Lua腳本內(nèi)容
RedisScript<String> script = RedisScript.of(luaScript, String.class);
long currentTime = System.currentTimeMillis() / 1000;
return stringRedisTemplate.execute(script, Collections.singletonList(DELAY_QUEUE_KEY), String.valueOf(currentTime));
}
}5. 定時(shí)任務(wù)調(diào)度
@Component
public class DelayQueueScheduler {
@Autowired
private DelayQueueService delayQueueService;
private static final long POLLING_INTERVAL = 1000; // 檢查間隔1秒
@Scheduled(fixedRate = POLLING_INTERVAL)
public void pollAndProcess() {
String taskId = delayQueueService.dequeue();
if (taskId != null) {
// 處理任務(wù)邏輯,例如調(diào)用某個(gè)服務(wù)或者方法等。
System.out.println("Processing task: " + taskId);
}
}
}五、使用ZSet實(shí)現(xiàn)延遲隊(duì)列的缺陷
雖然Redis的ZSet能滿足一些簡(jiǎn)單場(chǎng)景的延遲隊(duì)列需求,但也存在一些明顯的缺陷。
資源空轉(zhuǎn)問(wèn)題:
延遲任務(wù)的時(shí)間分布往往是不均勻的。在某些時(shí)段,可能會(huì)有大量的任務(wù)需要處理,而在其他時(shí)段則可能幾乎沒(méi)有任務(wù)。這種情況下,如果系統(tǒng)持續(xù)檢查ZSet以尋找到期任務(wù),那么在任務(wù)稀少或無(wú)任務(wù)的時(shí)段,系統(tǒng)會(huì)處于空轉(zhuǎn)狀態(tài),這無(wú)疑是對(duì)計(jì)算資源的浪費(fèi)。
性能瓶頸:
當(dāng)延遲消息數(shù)量眾多時(shí),不斷地輪詢整個(gè)ZSet以查找到期消息會(huì)對(duì)性能產(chǎn)生顯著影響。特別是當(dāng)任務(wù)數(shù)量龐大且到期時(shí)間分散時(shí),范圍查詢的開(kāi)銷(xiāo)會(huì)變得尤為突出。此外,如果多個(gè)任務(wù)同時(shí)到期且回調(diào)函數(shù)執(zhí)行效率低下,還可能導(dǎo)致延遲處理中心的性能下降,進(jìn)而引發(fā)連鎖反應(yīng),影響到后續(xù)任務(wù)的及時(shí)處理。
時(shí)間精度問(wèn)題:
ZSet使用浮點(diǎn)數(shù)作為分?jǐn)?shù)來(lái)排序元素,這在某些需要高精度時(shí)間控制的場(chǎng)景中可能不夠用。同時(shí),Redis實(shí)例的故障、重啟或時(shí)鐘回?fù)艿葐?wèn)題都可能影響到延遲事件處理的準(zhǔn)確性。
六、替代實(shí)現(xiàn)方案
狀態(tài)即時(shí)校驗(yàn):
在某些業(yè)務(wù)流程中,可以通過(guò)即時(shí)校驗(yàn)當(dāng)前狀態(tài)與應(yīng)有狀態(tài)的方式來(lái)替代延遲隊(duì)列。但這種方法更適用于工單等可以持續(xù)校驗(yàn)的業(yè)務(wù)場(chǎng)景,對(duì)于一次性的延遲通知任務(wù)則不太適用。
利用消息中間件的延遲消息功能:
像RocketMQ和RabbitMQ這樣的消息中間件提供了延遲消息的功能。例如,RocketMQ在商業(yè)版本中支持自定義時(shí)長(zhǎng)的延遲消息。
數(shù)據(jù)庫(kù)輪詢:
通過(guò)定期輪詢數(shù)據(jù)庫(kù)中的業(yè)務(wù)單據(jù)表或?qū)iT(mén)的延遲事件表來(lái)處理過(guò)期任務(wù)。但這種方法可能會(huì)對(duì)業(yè)務(wù)數(shù)據(jù)庫(kù)和服務(wù)造成性能負(fù)擔(dān),且輪詢的時(shí)間間隔難以精確把控。
時(shí)間輪算法:
時(shí)間輪算法是一種有效的處理定時(shí)任務(wù)的方法。但為了實(shí)現(xiàn)持久化和避免任務(wù)丟失,需要結(jié)合Redis或關(guān)系數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)延遲任務(wù)。在服務(wù)啟動(dòng)時(shí),需要將存儲(chǔ)的延遲任務(wù)加載到時(shí)間輪中,并在任務(wù)過(guò)期后更新任務(wù)狀態(tài),以防止重復(fù)執(zhí)行或加載。
結(jié)語(yǔ)
通過(guò)使用Redis和Lua腳本,可以在Spring Boot環(huán)境中實(shí)現(xiàn)一個(gè)高效且可靠的延時(shí)隊(duì)列系統(tǒng)。這種方法利用了Redis的有序集合數(shù)據(jù)結(jié)構(gòu)和Lua腳本的原子性操作來(lái)確保任務(wù)的正確性和一致性。通過(guò)定期調(diào)度任務(wù)來(lái)處理到期的任務(wù),可以實(shí)現(xiàn)各種需要延遲執(zhí)行的操作,如發(fā)送提醒、執(zhí)行定時(shí)任務(wù)等。
到此這篇關(guān)于Spring Boot中使用Redis和Lua腳本實(shí)現(xiàn)延時(shí)隊(duì)列的文章就介紹到這了,更多相關(guān)Spring Boot延時(shí)隊(duì)列內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java利用HttpClient模擬POST表單操作應(yīng)用及注意事項(xiàng)
本文主要介紹JAVA中利用HttpClient模擬POST表單操作,希望對(duì)大家有所幫助。2016-04-04
java復(fù)制文件和java移動(dòng)文件的示例分享
本文主要介紹了java將文件夾下面的所有的jar文件拷貝到指定的文件夾下面的方法,需要的朋友可以參考下2014-02-02
Spring MVC之mvc:resources如何處理靜態(tài)資源
這篇文章主要介紹了Spring MVC之mvc:resources如何處理靜態(tài)資源問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
SpringBoot利用自定義注解實(shí)現(xiàn)多數(shù)據(jù)源
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何利用自定義注解實(shí)現(xiàn)多數(shù)據(jù)源效果,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-10-10
IDEA修改idea64.exe.vmoptions文件以及解決coding卡頓問(wèn)題
IDEA修改idea64.exe.vmoptions文件以及解決coding卡頓問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
SpringBoot設(shè)置靜態(tài)資源訪問(wèn)控制和封裝集成方案
這篇文章主要介紹了SpringBoot靜態(tài)資源訪問(wèn)控制和封裝集成方案,關(guān)于springboot靜態(tài)資源訪問(wèn)的問(wèn)題,小編是通過(guò)自定義webconfig實(shí)現(xiàn)WebMvcConfigurer,重寫(xiě)addResourceHandlers方法,具體完整代碼跟隨小編一起看看吧2021-08-08
MyBatis?ofType和javaType的區(qū)別說(shuō)明
這篇文章主要介紹了MyBatis?ofType和javaType的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

