Redisson延遲隊(duì)列實(shí)現(xiàn)訂單關(guān)閉的操作方法
Redisson的RDelayedQueue是實(shí)現(xiàn)訂單到期關(guān)閉的利器,它比定時(shí)任務(wù)更精準(zhǔn)、比Redis過期監(jiān)聽更可靠。其核心原理是利用Redis的**有序集合(Sorted Set)和發(fā)布/訂閱(Pub/Sub)**功能,在分布式環(huán)境下可靠地執(zhí)行延遲任務(wù)。
接下來,我們結(jié)合Java代碼,一步步來看如何實(shí)現(xiàn)。
?? 生產(chǎn)端:訂單創(chuàng)建時(shí),埋下“定時(shí)炸彈”
在用戶下單成功后,我們需要做的不是立即啟動(dòng)一個(gè)計(jì)時(shí)器,而是將這筆訂單的ID作為一條“延遲消息”發(fā)送出去。
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class OrderService {
@Resource
private RedissonClient redissonClient;
// 隊(duì)列的名稱,可以理解為用于存放待關(guān)閉訂單的“信箱”
private static final String ORDER_QUEUE_KEY = "order-close-queue";
public void createOrder(Order order) {
// 1. 保存訂單到數(shù)據(jù)庫(kù) (省略具體邏輯)
saveToDB(order);
log.info("訂單 [{}] 創(chuàng)建成功,等待支付...", order.getId());
// 2. 將訂單ID放入延遲隊(duì)列,設(shè)置30分鐘后“爆炸”
try {
// 獲取一個(gè)阻塞隊(duì)列,這是消費(fèi)者最終要監(jiān)聽的目標(biāo)隊(duì)列
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(ORDER_QUEUE_KEY);
// 基于阻塞隊(duì)列,創(chuàng)建一個(gè)延遲隊(duì)列
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
// 將訂單ID放入延遲隊(duì)列,延遲30分鐘
delayedQueue.offer(order.getId().toString(), 30, TimeUnit.MINUTES);
log.info("訂單 [{}] 已放入延遲隊(duì)列,30分鐘后將自動(dòng)關(guān)閉", order.getId());
} catch (Exception e) {
log.error("放入延遲隊(duì)列失敗", e);
// 這里可以考慮補(bǔ)償機(jī)制,比如記錄日志后由定時(shí)任務(wù)兜底
}
}
}核心邏輯:生產(chǎn)者調(diào)用delayedQueue.offer(),將任務(wù)(訂單ID)和延遲時(shí)間(30分鐘)告訴Redisson。Redisson客戶端會(huì)把這個(gè)任務(wù)連同計(jì)算好的執(zhí)行時(shí)間戳,一起存到Redis的一個(gè)**有序集合(ZSet)**中,并以時(shí)間戳作為排序的分?jǐn)?shù)(score)。
?? 消費(fèi)端:時(shí)刻待命,準(zhǔn)時(shí)“拆彈”
我們需要一個(gè)后臺(tái)任務(wù)一直監(jiān)聽,一旦有訂單到期,立刻執(zhí)行關(guān)閉操作。
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class OrderCloseListener implements CommandLineRunner {
@Resource
private RedissonClient redissonClient;
@Resource
private OrderService orderService; // 注入你的訂單Service
private static final String ORDER_QUEUE_KEY = "order-close-queue";
@Override
public void run(String... args) throws Exception {
// 程序啟動(dòng)后,在一個(gè)獨(dú)立的線程中監(jiān)聽隊(duì)列
new Thread(() -> {
log.info("訂單關(guān)閉監(jiān)聽線程已啟動(dòng),等待到期訂單...");
// 獲取與生產(chǎn)者相同的阻塞隊(duì)列
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(ORDER_QUEUE_KEY);
// 注意:這里必須調(diào)用一次getDelayedQueue,目的是在消費(fèi)端也初始化相關(guān)的監(jiān)聽器
// 雖然不調(diào)用也能工作,但官方推薦調(diào)用以保證可靠性
redissonClient.getDelayedQueue(blockingQueue);
while (true) {
try {
// 阻塞等待,直到有訂單到期。take()方法會(huì)一直阻塞直到拿到數(shù)據(jù)
String orderId = blockingQueue.take();
log.info("收到到期訂單ID:{},開始執(zhí)行關(guān)閉操作", orderId);
// 執(zhí)行真正的關(guān)單業(yè)務(wù)邏輯
orderService.closeExpiredOrder(Long.parseLong(orderId));
} catch (InterruptedException e) {
log.error("監(jiān)聽線程被中斷", e);
Thread.currentThread().interrupt();
break; // 線程中斷時(shí)退出循環(huán)
} catch (Exception e) {
log.error("處理到期訂單時(shí)發(fā)生錯(cuò)誤", e);
// 防止單個(gè)消息處理失敗導(dǎo)致循環(huán)中斷,繼續(xù)監(jiān)聽下一個(gè)
}
}
}, "OrderClose-Listener").start();
}
}核心邏輯:消費(fèi)者通過blockingQueue.take()阻塞地從Redis的**目標(biāo)隊(duì)列(List)**中獲取消息。當(dāng)一個(gè)任務(wù)在ZSet中的時(shí)間戳小于當(dāng)前時(shí)間,Redisson的后臺(tái)線程就會(huì)自動(dòng)把它從ZSet移動(dòng)到目標(biāo)List中。這時(shí),take()方法就會(huì)立即返回,拿到訂單ID,執(zhí)行關(guān)閉邏輯。
?? 探秘Redisson的“內(nèi)部時(shí)鐘”機(jī)制
你可能會(huì)好奇,Redisson是如何精準(zhǔn)地將到期任務(wù)從ZSet移到List的?這背后有一個(gè)巧妙的設(shè)計(jì):
Redisson為每個(gè)延遲隊(duì)列啟動(dòng)了一個(gè)后臺(tái)輪詢線程(基于Netty的時(shí)間輪實(shí)現(xiàn))。這個(gè)線程會(huì):
- 定期查詢ZSet中分?jǐn)?shù)最?。醋钤绲狡冢┑娜蝿?wù)。
- 如果該任務(wù)的到期時(shí)間戳小于當(dāng)前時(shí)間,就把這個(gè)任務(wù)以及所有其他到期的任務(wù),從ZSet和另一個(gè)輔助的List中移除,并推入到消費(fèi)者正在監(jiān)聽的目標(biāo)List中。
- 為了提高效率,當(dāng)有新的、更早到期的任務(wù)加入時(shí),Redisson會(huì)通過**發(fā)布/訂閱(Pub/Sub)**功能發(fā)送一個(gè)通知,喚醒輪詢線程立即工作,而不是等到下一個(gè)輪詢周期。
所以,整個(gè)過程就像有一個(gè)精準(zhǔn)的“鬧鐘”在幫你管理這些任務(wù)。
?? 實(shí)戰(zhàn)要點(diǎn)與進(jìn)階思考
- 消息可靠性:所有任務(wù)數(shù)據(jù)都存儲(chǔ)在Redis中,即使你的應(yīng)用服務(wù)重啟,已經(jīng)提交但未到期的任務(wù)也不會(huì)丟失。Redis的持久化(RDB/AOF)機(jī)制為數(shù)據(jù)提供了最終保障。
- 分布式支持:生產(chǎn)者和消費(fèi)者可以是完全不同的應(yīng)用實(shí)例,只要它們連接同一個(gè)Redis、并使用相同的隊(duì)列名稱(如
order-close-queue)即可。多個(gè)消費(fèi)者實(shí)例可以同時(shí)take同一個(gè)隊(duì)列,實(shí)現(xiàn)任務(wù)的負(fù)載均衡。 - 時(shí)間精度:Redisson的默認(rèn)輪詢間隔約為5秒,這意味著任務(wù)的觸發(fā)時(shí)間可能會(huì)有最多5秒的誤差。如果業(yè)務(wù)對(duì)時(shí)間精度要求極高,可以通過配置
config.setScanInterval(2000)來縮短輪詢間隔(單位為毫秒),但會(huì)略微增加Redis的壓力。 - 業(yè)務(wù)兜底:作為一種最佳實(shí)踐,即使有了延遲隊(duì)列,也建議配合一個(gè)低頻的定時(shí)任務(wù)(如每小時(shí)執(zhí)行一次)作為最后的檢查,去掃描那些極少數(shù)可能因?yàn)楦鞣N意外(如Redis故障)而未被關(guān)閉的訂單,確保業(yè)務(wù)邏輯的最終一致性。
到此這篇關(guān)于Redisson延遲隊(duì)列實(shí)現(xiàn)訂單關(guān)閉的操作方法的文章就介紹到這了,更多相關(guān)redisson延遲隊(duì)列訂單關(guān)閉內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea下如何設(shè)置項(xiàng)目啟動(dòng)的JVM運(yùn)行內(nèi)存大小
這篇文章主要介紹了idea下如何設(shè)置項(xiàng)目啟動(dòng)的JVM運(yùn)行內(nèi)存大小問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
使用Java實(shí)現(xiàn)自動(dòng)調(diào)整Excel行高和列寬
在處理 Excel 文件時(shí),自動(dòng)調(diào)整行高和列寬是一項(xiàng)常見的需求,本文將介紹如何使用 Java 來自動(dòng)調(diào)整 Excel 文件中的行高和列寬,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2026-01-01
IDEA通過git回滾到某個(gè)提交節(jié)點(diǎn)或某個(gè)版本的操作方法
這篇文章主要介紹了IDEA通過git回滾到某個(gè)提交節(jié)點(diǎn)或某個(gè)版本的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
Mybatis?Plus?中的LambdaQueryWrapper示例詳解
這篇文章主要介紹了Mybatis?Plus?中的LambdaQueryWrapper,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
使用mybatisPlus的queryWrapper做左聯(lián)接,內(nèi)聯(lián)接方式
本文介紹了如何使用Mybatis-Plus的QueryWrapper進(jìn)行SQL查詢,包括左連接、內(nèi)連接等操作,通過示例代碼展示了如何構(gòu)建復(fù)雜的SQL查詢,并將結(jié)果存儲(chǔ)在List對(duì)象中返回,希望給讀者提供參考2025-03-03

