SpringBoot整合Redis實(shí)現(xiàn)訂單超時(shí)自動(dòng)刪除功能
引言
在電商、外賣等O2O場(chǎng)景中,訂單超時(shí)未支付是常見業(yè)務(wù)場(chǎng)景。例如:用戶下單后30分鐘內(nèi)未支付,系統(tǒng)需自動(dòng)取消訂單并釋放庫(kù)存。傳統(tǒng)方案通過定時(shí)任務(wù)輪詢數(shù)據(jù)庫(kù)(如每5分鐘掃描一次超時(shí)訂單),但存在??延遲高(最長(zhǎng)延遲5分鐘)??、??數(shù)據(jù)庫(kù)壓力大(全表掃描)??等問題。
Redis的??過期鍵自動(dòng)刪除機(jī)制??+??鍵空間通知??功能,可完美解決這一痛點(diǎn):訂單創(chuàng)建時(shí)存入Redis并設(shè)置過期時(shí)間(如30分鐘),過期后Redis自動(dòng)觸發(fā)刪除事件,系統(tǒng)監(jiān)聽該事件并執(zhí)行訂單取消邏輯。此方案延遲低(通常毫秒級(jí))、性能高(Redis內(nèi)存操作),是互聯(lián)網(wǎng)高并發(fā)場(chǎng)景的首選。
一、Redis過期機(jī)制核心原理
1.1 Redis的鍵過期策略
Redis支持為鍵設(shè)置過期時(shí)間(EXPIRE/PEXPIRE命令),過期后鍵會(huì)被自動(dòng)刪除。其刪除策略包含三種機(jī)制:
| 策略類型 | 觸發(fā)條件 | 特點(diǎn) |
|---|---|---|
| ??惰性刪除?? | 訪問鍵時(shí)檢查是否過期 | 內(nèi)存友好(不主動(dòng)掃描),但可能導(dǎo)致過期鍵長(zhǎng)期殘留(未被訪問時(shí)) |
| ??定期刪除?? | Redis后臺(tái)線程周期性掃描 | 主動(dòng)清理過期鍵(默認(rèn)每100ms掃描1%數(shù)據(jù)庫(kù)),平衡內(nèi)存與CPU開銷 |
| ??永久有效?? | 未設(shè)置過期時(shí)間 | 鍵會(huì)一直存在,直到顯式刪除或Redis重啟 |
??注意??:生產(chǎn)環(huán)境需確保redis.conf中maxmemory-policy設(shè)置為volatile-ttl(優(yōu)先刪除即將過期的鍵),避免內(nèi)存溢出。
1.2 鍵空間通知(Keyspace Notifications)
Redis支持通過??發(fā)布-訂閱模式??通知客戶端鍵的過期事件。需在redis.conf中啟用相關(guān)配置:
notify-keyspace-events Ex # E表示啟用鍵事件通知,x表示過期事件
啟用后,當(dāng)鍵過期時(shí),Redis會(huì)向__keyevent@<db>__:expired頻道發(fā)送消息(<db>為數(shù)據(jù)庫(kù)編號(hào),默認(rèn)0)。
二、Spring Boot整合Redis環(huán)境準(zhǔn)備
2.1 依賴配置
在pom.xml中添加Spring Data Redis依賴:
<dependencies>
<!-- Spring Boot Redis Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce連接池(默認(rèn)使用Lettuce,比Jedis更輕量) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Lombok簡(jiǎn)化代碼 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 Redis配置
在application.yml中配置Redis連接信息及序列化方式:
spring:
redis:
host: localhost # Redis服務(wù)器地址
port: 6379 # 端口號(hào)(默認(rèn)6379)
password: 123456 # 密碼(無(wú)密碼則忽略)
database: 0 # 使用數(shù)據(jù)庫(kù)0(默認(rèn))
lettuce: # Lettuce連接池配置
pool:
max-active: 8 # 最大連接數(shù)
max-idle: 8 # 最大空閑連接
min-idle: 0 # 最小空閑連接
max-wait: 10000ms # 連接池最大等待時(shí)間
# 序列化配置(默認(rèn)JDK序列化,推薦JSON)
redis:
serializer:
key: org.springframework.data.redis.serializer.StringRedisSerializer
value: org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
2.3 自動(dòng)配置驗(yàn)證
編寫測(cè)試類驗(yàn)證Redis連接:
@SpringBootTest
@Slf4j
public class RedisConfigTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testRedisConnection() {
String key = "test_key";
String value = "test_value";
// 寫入Redis
redisTemplate.opsForValue().set(key, value);
// 讀取Redis
String result = (String) redisTemplate.opsForValue().get(key);
log.info("Redis測(cè)試結(jié)果:{}", result); // 應(yīng)輸出"test_value"
}
}
三、訂單超時(shí)自動(dòng)刪除核心實(shí)現(xiàn)
3.1 訂單實(shí)體類設(shè)計(jì)
定義訂單實(shí)體(需包含唯一標(biāo)識(shí)、過期時(shí)間等業(yè)務(wù)字段):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id; // 訂單ID
private String userId; // 用戶ID
private BigDecimal amount; // 訂單金額
private LocalDateTime createTime; // 創(chuàng)建時(shí)間
private LocalDateTime expireTime; // 過期時(shí)間(用于展示)
private String status; // 訂單狀態(tài)(待支付/已支付/已取消)
}
3.2 訂單Redis存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)
選擇Hash結(jié)構(gòu)存儲(chǔ)訂單詳情(支持部分字段更新),鍵格式為order:{orderId},字段包括:
id:訂單ID(與鍵重復(fù),冗余存儲(chǔ)便于查詢)userId:用戶IDamount:訂單金額status:訂單狀態(tài)createTime:創(chuàng)建時(shí)間
??示例鍵??:order:10001(對(duì)應(yīng)訂單ID為10001的訂單)
3.3 訂單創(chuàng)建與Redis存儲(chǔ)邏輯
在訂單服務(wù)中,創(chuàng)建訂單后需同步存入Redis并設(shè)置過期時(shí)間(如30分鐘):
@Service
@Slf4j
public class OrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderRepository orderRepository; // 數(shù)據(jù)庫(kù)操作(假設(shè)使用JPA)
// 訂單有效時(shí)長(zhǎng)(30分鐘,單位:秒)
private static final long ORDER_EXPIRE_SECONDS = 30 * 60;
/**
* 創(chuàng)建訂單(同步數(shù)據(jù)庫(kù)與Redis)
*/
@Transactional
public Order createOrder(Order order) {
// 1. 保存訂單到數(shù)據(jù)庫(kù)
order.setStatus("待支付");
order.setCreateTime(LocalDateTime.now());
order.setExpireTime(order.getCreateTime().plusMinutes(30));
Order savedOrder = orderRepository.save(order);
// 2. 存儲(chǔ)訂單到Redis并設(shè)置過期時(shí)間
String redisKey = "order:" + savedOrder.getId();
redisTemplate.opsForHash().putAll(redisKey, new HashMap<String, Object>() {{
put("id", savedOrder.getId());
put("userId", savedOrder.getUserId());
put("amount", savedOrder.getAmount());
put("status", savedOrder.getStatus());
put("createTime", savedOrder.getCreateTime().toString());
}});
// 設(shè)置鍵的過期時(shí)間(30分鐘)
redisTemplate.expire(redisKey, ORDER_EXPIRE_SECONDS, TimeUnit.SECONDS);
return savedOrder;
}
/**
* 支付成功后刪除Redis訂單(避免觸發(fā)過期事件)
*/
@Transactional
public void payOrder(Long orderId) {
// 1. 更新數(shù)據(jù)庫(kù)訂單狀態(tài)為已支付
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("訂單不存在"));
order.setStatus("已支付");
orderRepository.save(order);
// 2. 從Redis刪除該訂單(避免過期事件觸發(fā)取消邏輯)
String redisKey = "order:" + orderId;
redisTemplate.delete(redisKey);
}
}
3.4 監(jiān)聽Redis過期事件(關(guān)鍵邏輯)
通過監(jiān)聽Redis的expired事件,觸發(fā)訂單取消和庫(kù)存釋放操作。步驟如下:
3.4.1 定義事件監(jiān)聽器
@Component
@Slf4j
public class RedisOrderExpiredListener {
@Autowired
private OrderService orderService; // 假設(shè)包含取消訂單和釋放庫(kù)存的方法
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@PostConstruct
public void init() {
// 創(chuàng)建Redis消息監(jiān)聽容器
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
// 訂閱過期事件(頻道格式:__keyevent@0__:expired)
container.addMessageListener(this::handleOrderExpired,
new PatternTopic("__keyevent@0__:expired"));
}
/**
* 處理訂單過期事件
*/
private void handleOrderExpired(Message message, byte[] pattern) {
// 1. 解析過期的鍵名(格式:order:10001)
String expiredKey = new String(message.getBody(), StandardCharsets.UTF_8);
if (!expiredKey.startsWith("order:")) {
log.warn("非訂單鍵過期,跳過處理:{}", expiredKey);
return;
}
// 2. 提取訂單ID(去除前綴"order:")
String orderIdStr = expiredKey.substring("order:".length());
Long orderId;
try {
orderId = Long.parseLong(orderIdStr);
} catch (NumberFormatException e) {
log.error("訂單ID格式錯(cuò)誤,鍵:{}", expiredKey, e);
return;
}
// 3. 查詢數(shù)據(jù)庫(kù)確認(rèn)訂單狀態(tài)(避免Redis數(shù)據(jù)與數(shù)據(jù)庫(kù)不一致)
Order order = orderService.getOrderById(orderId);
if (order == null || !"待支付".equals(order.getStatus())) {
log.info("訂單已處理或不存在,無(wú)需取消:{}", orderId);
return;
}
// 4. 執(zhí)行訂單取消邏輯(冪等性設(shè)計(jì),避免重復(fù)處理)
try {
orderService.cancelOrder(orderId);
log.info("訂單超時(shí)自動(dòng)取消成功,orderId={}", orderId);
} catch (Exception e) {
log.error("訂單取消失敗,orderId={}", orderId, e);
// 可重試或人工介入
}
}
}
3.4.2 訂單取消邏輯實(shí)現(xiàn)
在OrderService中添加取消訂單方法(需保證冪等性):
@Service
@Slf4j
public class OrderService {
// ...(其他方法)
/**
* 取消訂單(釋放庫(kù)存、更新狀態(tài))
*/
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("訂單不存在"));
// 冪等性校驗(yàn)(避免重復(fù)取消)
if (!"待支付".equals(order.getStatus())) {
log.info("訂單已取消或已支付,無(wú)需重復(fù)操作:{}", orderId);
return;
}
// 1. 更新訂單狀態(tài)為已取消
order.setStatus("已取消");
orderRepository.save(order);
// 2. 釋放庫(kù)存(調(diào)用庫(kù)存服務(wù))
stockService.releaseStock(order.getUserId(), order.getAmount());
}
}
3.5 庫(kù)存服務(wù)接口(示例)
@Service
@Slf4j
public class StockService {
/**
* 釋放庫(kù)存(示例方法)
*/
public void releaseStock(String userId, BigDecimal amount) {
log.info("釋放用戶{}的庫(kù)存,金額:{}", userId, amount);
// 實(shí)際邏輯:調(diào)用庫(kù)存微服務(wù)API或操作庫(kù)存數(shù)據(jù)庫(kù)
}
}
四、關(guān)鍵技術(shù)細(xì)節(jié)與優(yōu)化
4.1 避免Redis與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致
由于Redis是緩存層,可能存在??主從復(fù)制延遲??或??緩存擊穿??導(dǎo)致的數(shù)據(jù)不一致。解決方案:
- ??雙寫校驗(yàn)??:在取消訂單時(shí),先更新數(shù)據(jù)庫(kù)狀態(tài),再刪除Redis(而非僅依賴Redis過期)。如
payOrder方法中,先更新數(shù)據(jù)庫(kù)再刪Redis。 - ??延遲監(jiān)聽??:監(jiān)聽過期事件后,再次查詢數(shù)據(jù)庫(kù)確認(rèn)訂單狀態(tài)(如示例中的
getOrderById),避免因網(wǎng)絡(luò)延遲或主從同步導(dǎo)致的臟數(shù)據(jù)。
4.2 過期時(shí)間的精準(zhǔn)控制
Redis的過期時(shí)間是??近似精確??的(誤差通常在1秒內(nèi)),對(duì)于高精度場(chǎng)景(如金融交易),可結(jié)合數(shù)據(jù)庫(kù)的expire_time字段,在查詢訂單時(shí)校驗(yàn)是否超時(shí):
/**
* 查詢訂單(同時(shí)校驗(yàn)是否超時(shí))
*/
public Order getOrderById(Long orderId) {
Order order = orderRepository.findById(orderId).orElse(null);
if (order != null && "待支付".equals(order.getStatus())) {
// 校驗(yàn)是否超時(shí)(數(shù)據(jù)庫(kù)時(shí)間與當(dāng)前時(shí)間比較)
LocalDateTime now = LocalDateTime.now();
if (now.isAfter(order.getExpireTime())) {
// 觸發(fā)取消邏輯(避免Redis未及時(shí)刪除)
cancelOrder(orderId);
return null; // 返回null表示訂單已取消
}
}
return order;
}
4.3 高并發(fā)場(chǎng)景下的性能優(yōu)化
??批量監(jiān)聽??:使用RedisMessageListenerContainer的線程池配置,提升事件處理能力:
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 配置線程池(核心線程數(shù)、最大線程數(shù))
container.setTaskExecutor(Executors.newFixedThreadPool(10));
return container;
}
??異步處理??:訂單取消邏輯(如釋放庫(kù)存)使用@Async注解異步執(zhí)行,避免阻塞監(jiān)聽線程:
@Service
@Slf4j
public class OrderService {
@Autowired
private StockService stockService;
@Async("asyncTaskExecutor") // 使用自定義線程池
public void releaseStock(Long userId, BigDecimal amount) {
stockService.releaseStock(userId, amount);
}
}
配置自定義線程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncTaskExecutor")
public Executor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心線程數(shù)
executor.setMaxPoolSize(20); // 最大線程數(shù)
executor.setQueueCapacity(100); // 隊(duì)列容量
executor.setKeepAliveSeconds(30); // 空閑線程存活時(shí)間
executor.setThreadNamePrefix("order-async-");
executor.initialize();
return executor;
}
}
4.4 監(jiān)控與報(bào)警
- ??Redis監(jiān)控??:通過
INFO stats命令查看expired_keys指標(biāo)(每秒過期鍵數(shù)量),監(jiān)控異常過期情況。 - ??日志報(bào)警??:在
RedisOrderExpiredListener中添加異常報(bào)警(如連續(xù)10次處理失敗觸發(fā)郵件/釘釘通知)。 - ??訂單超時(shí)率統(tǒng)計(jì)??:通過Prometheus+Grafana統(tǒng)計(jì)超時(shí)訂單占比,優(yōu)化業(yè)務(wù)邏輯(如延長(zhǎng)熱門商品訂單的有效期)。
五、方案對(duì)比與適用場(chǎng)景
5.1 Redis方案 vs 定時(shí)任務(wù)方案
| 維度 | Redis方案 | 定時(shí)任務(wù)方案 |
|---|---|---|
| ??延遲?? | 毫秒級(jí)(Redis事件觸發(fā)) | 最長(zhǎng)延遲(任務(wù)間隔,如5分鐘) |
| ??數(shù)據(jù)庫(kù)壓力?? | 無(wú)(僅事件觸發(fā)時(shí)查詢) | 高(全表掃描) |
| ??資源消耗?? | 低(Redis內(nèi)存操作) | 高(任務(wù)線程資源) |
| ??適用場(chǎng)景?? | 高并發(fā)、低延遲超時(shí)場(chǎng)景(如電商訂單) | 低并發(fā)、允許延遲的場(chǎng)景(如日志清理) |
5.2 擴(kuò)展方案:Redisson延遲隊(duì)列
若需要更復(fù)雜的延遲任務(wù)管理(如動(dòng)態(tài)調(diào)整延遲時(shí)間、任務(wù)優(yōu)先級(jí)),可使用Redisson的RDelayedQueue:
// Redisson配置
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}
// 使用延遲隊(duì)列
@Service
@Slf4j
public class RedissonDelayedQueueService {
@Autowired
private RedissonClient redissonClient;
private RDelayedQueue<Order> delayedQueue;
private RBlockingQueue<Order> blockingQueue;
@PostConstruct
public void init() {
blockingQueue = redissonClient.getBlockingQueue("orderDelayedQueue");
delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
}
/**
* 添加延遲訂單(30分鐘后觸發(fā))
*/
public void addDelayedOrder(Order order) {
delayedQueue.offer(order, 30, TimeUnit.MINUTES);
}
/**
* 處理延遲訂單(阻塞獲?。?
*/
public void processDelayedOrders() {
while (true) {
try {
Order order = blockingQueue.take(); // 阻塞直到有訂單到期
log.info("處理延遲訂單:{}", order.getId());
// 執(zhí)行取消邏輯...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
??適用場(chǎng)景??:需要?jiǎng)討B(tài)調(diào)整延遲時(shí)間、批量管理延遲任務(wù)的復(fù)雜場(chǎng)景(如網(wǎng) 約車派單超時(shí))。
六、總結(jié)
本文詳細(xì)講解了Spring Boot整合Redis實(shí)現(xiàn)訂單超時(shí)自動(dòng)刪除的全流程,核心步驟包括:
- ??Redis過期機(jī)制??:利用
EXPIRE命令設(shè)置鍵的過期時(shí)間,結(jié)合鍵空間通知監(jiān)聽過期事件。 - ??訂單存儲(chǔ)設(shè)計(jì)??:使用
Hash結(jié)構(gòu)存儲(chǔ)訂單詳情,鍵格式為order:{orderId},設(shè)置30分鐘過期時(shí)間。 - ??事件監(jiān)聽邏輯??:通過
RedisMessageListenerContainer監(jiān)聽__keyevent@0__:expired頻道,解析過期鍵并觸發(fā)訂單取消。 - ??數(shù)據(jù)一致性保障??:監(jiān)聽事件后查詢數(shù)據(jù)庫(kù)確認(rèn)訂單狀態(tài),避免Redis與數(shù)據(jù)庫(kù)數(shù)據(jù)不一致。
- ??性能優(yōu)化??:異步處理取消邏輯、線程池調(diào)優(yōu)、雙寫校驗(yàn)等措施提升系統(tǒng)穩(wěn)定性。
Redis方案憑借其??低延遲、高吞吐量??的特性,成為互聯(lián)網(wǎng)高并發(fā)場(chǎng)景下訂單超時(shí)處理的首選方案。實(shí)際開發(fā)中需結(jié)合業(yè)務(wù)需求,選擇Redis原生方案或Redisson等擴(kuò)展工具,確保系統(tǒng)的可靠性和可維護(hù)性。
以上就是SpringBoot整合Redis實(shí)現(xiàn)訂單超時(shí)自動(dòng)刪除功能的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Redis訂單超時(shí)自動(dòng)刪除的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringMVC表單標(biāo)簽知識(shí)點(diǎn)詳解
這篇文章主要為大家詳細(xì)介紹了SpringMVC表單標(biāo)簽知識(shí)點(diǎn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
BeanFactory和FactoryBean的區(qū)別示例詳解
這篇文章主要為大家介紹了BeanFactory和FactoryBean的區(qū)別示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
如何用java做一個(gè)word轉(zhuǎn)圖片的功能詳解
這篇文章主要給大家介紹了關(guān)于如何用java做一個(gè)word轉(zhuǎn)圖片的功能,通過實(shí)現(xiàn)Java Word轉(zhuǎn)圖片功能,避免PDF中間轉(zhuǎn)換損耗,涵蓋分頁(yè)處理、字體設(shè)置、性能優(yōu)化及替代方案對(duì)比,需要的朋友可以參考下2025-05-05
java使用PageInfo的list通用分頁(yè)處理demo
這篇文章主要為大家介紹了java使用PageInfo的list通用分頁(yè)處理demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2023-12-12
Java實(shí)現(xiàn)Twitter的分布式自增ID算法snowflake
這篇文章主要介紹了Java實(shí)現(xiàn)Twitter的分布式自增ID算法snowflake,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08

