springboot+redis實現訂單過期(超時取消)功能的方法詳解
在Spring Boot中使用Redis實現訂單過期(超時取消)功能,有多種成熟方案。以下是完整的實現方案:
一、Redis鍵過期回調方案(推薦)
1. 配置Redis監(jiān)聽器
@Configuration
public class RedisKeyExpirationConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
2. 監(jiān)聽鍵過期事件
@Component
@Slf4j
public class OrderExpirationListener {
private static final String ORDER_KEY_PREFIX = "order:";
private static final String ORDER_EXPIRE_KEY_PREFIX = "order:expire:";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderService orderService;
/**
* 監(jiān)聽所有鍵過期事件
*/
@EventListener
public void handleKeyExpiredEvent(KeyExpiredEvent<String> event) {
String expiredKey = new String(event.getSource());
if (expiredKey.startsWith(ORDER_EXPIRE_KEY_PREFIX)) {
String orderId = expiredKey.substring(ORDER_EXPIRE_KEY_PREFIX.length());
handleOrderExpired(orderId);
}
}
private void handleOrderExpired(String orderId) {
log.info("檢測到訂單過期: {}", orderId);
try {
// 異步處理,避免阻塞Redis監(jiān)聽線程
CompletableFuture.runAsync(() -> {
boolean result = orderService.cancelExpiredOrder(orderId);
if (result) {
log.info("訂單 {} 已成功取消", orderId);
} else {
log.warn("訂單 {} 取消失敗或已處理", orderId);
}
});
} catch (Exception e) {
log.error("處理訂單過期異常: {}", orderId, e);
}
}
}
3. Redis配置(開啟鍵空間通知)
在Redis配置文件redis.conf中開啟鍵空間通知:
# 開啟鍵空間通知 notify-keyspace-events Ex # 或者開啟所有事件 # notify-keyspace-events AKE
Spring Boot配置:
spring:
redis:
host: localhost
port: 6379
database: 0
# 監(jiān)聽鍵過期事件
listen-patterns: "__keyevent@*__:expired"
二、延時隊列方案(Redisson實現)
1. 添加Redisson依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
2. 使用Redisson延時隊列
@Service
@Slf4j
public class OrderDelayQueueService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderService orderService;
private static final String DELAY_QUEUE_NAME = "order:delay:queue";
private static final String PROCESSING_SET = "order:delay:processing";
/**
* 添加訂單到延時隊列
* @param orderId 訂單ID
* @param delayTime 延時時間(分鐘)
*/
public void addToDelayQueue(String orderId, long delayTime) {
RBlockingQueue<String> blockingQueue = redissonClient
.getBlockingQueue(DELAY_QUEUE_NAME);
RDelayedQueue<String> delayedQueue = redissonClient
.getDelayedQueue(blockingQueue);
// 添加到延時隊列
delayedQueue.offer(orderId, delayTime, TimeUnit.MINUTES);
log.info("訂單 {} 已添加到延時隊列,將在 {} 分鐘后過期", orderId, delayTime);
}
/**
* 啟動延時隊列消費者
*/
@PostConstruct
public void startDelayQueueConsumer() {
new Thread(this::consumeDelayQueue, "delay-queue-consumer").start();
}
private void consumeDelayQueue() {
RBlockingQueue<String> blockingQueue = redissonClient
.getBlockingQueue(DELAY_QUEUE_NAME);
while (true) {
try {
// 從隊列中取出過期的訂單
String orderId = blockingQueue.take();
// 處理訂單過期
processExpiredOrder(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("延時隊列消費線程被中斷", e);
break;
} catch (Exception e) {
log.error("處理延時隊列消息異常", e);
}
}
}
/**
* 處理過期訂單
*/
private void processExpiredOrder(String orderId) {
RSet<String> processingSet = redissonClient.getSet(PROCESSING_SET);
// 使用set防止重復處理
if (processingSet.add(orderId)) {
try {
boolean result = orderService.cancelExpiredOrder(orderId);
if (result) {
log.info("延時隊列:訂單 {} 已過期取消", orderId);
} else {
log.info("延時隊列:訂單 {} 無需處理", orderId);
}
} finally {
processingSet.remove(orderId);
}
}
}
}
三、ZSet有序集合方案
@Service
@Slf4j
public class OrderExpireZSetService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String ORDER_EXPIRE_ZSET = "order:expire:zset";
private static final String ORDER_PROCESSING_SET = "order:processing:set";
/**
* 添加訂單到過期集合
* @param orderId 訂單ID
* @param expireTime 過期時間戳
*/
public void addOrderToExpireSet(String orderId, long expireTime) {
redisTemplate.opsForZSet().add(ORDER_EXPIRE_ZSET, orderId, expireTime);
log.info("訂單 {} 已添加到過期集合,過期時間: {}", orderId,
new Date(expireTime));
}
/**
* 批量掃描過期訂單
*/
public void scanExpiredOrders() {
long now = System.currentTimeMillis();
// 獲取已過期的訂單
Set<String> expiredOrders = redisTemplate.opsForZSet()
.rangeByScore(ORDER_EXPIRE_ZSET, 0, now);
if (expiredOrders != null && !expiredOrders.isEmpty()) {
for (String orderId : expiredOrders) {
processExpiredOrder(orderId);
}
// 移除已處理的訂單
redisTemplate.opsForZSet().removeRangeByScore(
ORDER_EXPIRE_ZSET, 0, now);
}
}
/**
* 定時掃描任務
*/
@Scheduled(fixedDelay = 30000) // 每30秒執(zhí)行一次
public void scheduledScan() {
log.debug("開始掃描過期訂單");
scanExpiredOrders();
}
private void processExpiredOrder(String orderId) {
// 使用setnx防止重復處理
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(ORDER_PROCESSING_SET + ":" + orderId, "1", 5, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(success)) {
try {
// 處理訂單過期邏輯
handleOrderExpired(orderId);
} finally {
// 清理處理標記
redisTemplate.delete(ORDER_PROCESSING_SET + ":" + orderId);
}
}
}
private void handleOrderExpired(String orderId) {
// TODO: 實現訂單過期處理邏輯
log.info("處理過期訂單: {}", orderId);
}
}
四、完整訂單服務實現
1. 訂單狀態(tài)枚舉
public enum OrderStatus {
PENDING_PAYMENT(0, "待支付"),
PAID(1, "已支付"),
COMPLETED(2, "已完成"),
CANCELLED(3, "已取消"),
EXPIRED(4, "已過期");
private final int code;
private final String description;
OrderStatus(int code, String description) {
this.code = code;
this.description = description;
}
// getter方法省略
}
2. 訂單實體
@Data
@Entity
@Table(name = "t_order")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String orderNo;
private Long userId;
private BigDecimal amount;
@Enumerated(EnumType.ORDINAL)
private OrderStatus status = OrderStatus.PENDING_PAYMENT;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@Column(name = "create_time")
private LocalDateTime createTime = LocalDateTime.now();
@Column(name = "update_time")
private LocalDateTime updateTime = LocalDateTime.now();
/**
* 判斷訂單是否已過期
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime)
&& status == OrderStatus.PENDING_PAYMENT;
}
}
3. 訂單服務實現
@Service
@Slf4j
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderDelayQueueService delayQueueService;
@Autowired
private OrderExpireZSetService zSetService;
@Autowired
private ApplicationEventPublisher eventPublisher;
private static final String ORDER_LOCK_PREFIX = "order:lock:";
private static final String ORDER_EXPIRE_KEY_PREFIX = "order:expire:";
private static final int ORDER_EXPIRE_MINUTES = 30; // 30分鐘未支付過期
/**
* 創(chuàng)建訂單
*/
public Order createOrder(Long userId, BigDecimal amount) {
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(OrderStatus.PENDING_PAYMENT);
order.setExpireTime(LocalDateTime.now().plusMinutes(ORDER_EXPIRE_MINUTES));
order = orderRepository.save(order);
// 設置Redis過期
setOrderExpire(order.getOrderNo());
// 添加到延時隊列
delayQueueService.addToDelayQueue(order.getOrderNo(), ORDER_EXPIRE_MINUTES);
// 添加到ZSet
zSetService.addOrderToExpireSet(
order.getOrderNo(),
order.getExpireTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
);
log.info("創(chuàng)建訂單成功: {}, 過期時間: {}", order.getOrderNo(), order.getExpireTime());
return order;
}
/**
* 設置Redis鍵過期
*/
private void setOrderExpire(String orderNo) {
String key = ORDER_EXPIRE_KEY_PREFIX + orderNo;
String value = String.valueOf(System.currentTimeMillis());
// 設置30分鐘后過期
redisTemplate.opsForValue().set(
key,
value,
ORDER_EXPIRE_MINUTES,
TimeUnit.MINUTES
);
// 同時存儲訂單信息,用于過期時處理
Map<String, String> orderInfo = new HashMap<>();
orderInfo.put("orderNo", orderNo);
orderInfo.put("userId", "1"); // 實際從訂單獲取
orderInfo.put("amount", "100.00");
redisTemplate.opsForHash().putAll(ORDER_KEY_PREFIX + orderNo, orderInfo);
}
/**
* 支付成功處理
*/
public boolean processPayment(String orderNo) {
// 獲取分布式鎖
String lockKey = ORDER_LOCK_PREFIX + orderNo;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new RuntimeException("訂單處理中,請稍后");
}
try {
Order order = orderRepository.findByOrderNo(orderNo)
.orElseThrow(() -> new RuntimeException("訂單不存在"));
// 檢查訂單狀態(tài)
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
throw new RuntimeException("訂單狀態(tài)異常: " + order.getStatus());
}
// 檢查是否過期
if (order.isExpired()) {
order.setStatus(OrderStatus.EXPIRED);
orderRepository.save(order);
throw new RuntimeException("訂單已過期");
}
// 更新訂單狀態(tài)
order.setStatus(OrderStatus.PAID);
order.setUpdateTime(LocalDateTime.now());
orderRepository.save(order);
// 移除過期設置
removeOrderExpire(orderNo);
// 發(fā)布支付成功事件
eventPublisher.publishEvent(new OrderPaidEvent(this, order));
log.info("訂單支付成功: {}", orderNo);
return true;
} finally {
// 釋放鎖
redisTemplate.delete(lockKey);
}
}
/**
* 處理過期訂單
*/
public boolean cancelExpiredOrder(String orderNo) {
String lockKey = ORDER_LOCK_PREFIX + orderNo;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
return false;
}
try {
Order order = orderRepository.findByOrderNo(orderNo)
.orElseThrow(() -> new RuntimeException("訂單不存在"));
// 雙重檢查:訂單是否仍為待支付狀態(tài)
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
log.info("訂單 {} 狀態(tài)已變更為 {},跳過取消", orderNo, order.getStatus());
return false;
}
// 檢查是否真的過期
if (!order.isExpired()) {
log.info("訂單 {} 未過期,跳過取消", orderNo);
return false;
}
// 更新訂單狀態(tài)
order.setStatus(OrderStatus.EXPIRED);
order.setUpdateTime(LocalDateTime.now());
orderRepository.save(order);
// 釋放庫存等業(yè)務邏輯
releaseStock(order);
// 發(fā)送通知
sendExpireNotification(order);
log.info("訂單 {} 已過期取消", orderNo);
return true;
} finally {
redisTemplate.delete(lockKey);
}
}
/**
* 移除訂單過期設置
*/
private void removeOrderExpire(String orderNo) {
// 刪除過期key
redisTemplate.delete(ORDER_EXPIRE_KEY_PREFIX + orderNo);
// 從延時隊列移除
// 注意:Redisson延時隊列不支持直接移除,需要其他方式
// 從ZSet移除
redisTemplate.opsForZSet().remove(ORDER_EXPIRE_ZSET, orderNo);
// 刪除訂單緩存
redisTemplate.delete(ORDER_KEY_PREFIX + orderNo);
}
/**
* 生成訂單號
*/
private String generateOrderNo() {
// 時間戳 + 隨機數
return "ORD" +
System.currentTimeMillis() +
String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
}
private void releaseStock(Order order) {
// 釋放庫存邏輯
log.info("釋放訂單 {} 的庫存", order.getOrderNo());
}
private void sendExpireNotification(Order order) {
// 發(fā)送通知邏輯
log.info("發(fā)送訂單 {} 過期通知", order.getOrderNo());
}
}
4. 訂單支付事件
public class OrderPaidEvent extends ApplicationEvent {
private final Order order;
public OrderPaidEvent(Object source, Order order) {
super(source);
this.order = order;
}
public Order getOrder() {
return order;
}
}
五、多級過期策略(增強版)
@Component
@Slf4j
public class OrderExpireManager {
@Autowired
private OrderService orderService;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String ORDER_EXPIRE_ZSET = "order:expire:zset";
private static final String ORDER_EXPIRE_DELAY_QUEUE = "order:expire:delay:queue";
/**
* 三級過期檢測策略
*/
public void startExpireMonitor() {
// 1. Redis鍵過期事件(實時)
// 2. 定時任務掃描(兜底)
// 3. 延時隊列(精確控制)
new Thread(this::monitorDelayQueue, "order-expire-monitor").start();
new Thread(this::scheduledScan, "order-expire-scanner").start();
}
/**
* 監(jiān)控延時隊列
*/
private void monitorDelayQueue() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 從延時隊列獲取訂單
String orderId = redisTemplate.opsForList()
.rightPop(ORDER_EXPIRE_DELAY_QUEUE, 1, TimeUnit.SECONDS);
if (orderId != null) {
processExpiredOrder(orderId);
}
} catch (Exception e) {
log.error("監(jiān)控延時隊列異常", e);
}
}
}
/**
* 定時掃描
*/
private void scheduledScan() {
while (!Thread.currentThread().isInterrupted()) {
try {
scanExpiredOrders();
Thread.sleep(30000); // 30秒掃描一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("定時掃描異常", e);
}
}
}
/**
* 掃描過期訂單
*/
private void scanExpiredOrders() {
long now = System.currentTimeMillis();
Set<String> expiredOrders = redisTemplate.opsForZSet()
.rangeByScore(ORDER_EXPIRE_ZSET, 0, now);
if (expiredOrders != null) {
for (String orderId : expiredOrders) {
processExpiredOrder(orderId);
}
// 移除已處理的訂單
redisTemplate.opsForZSet().removeRangeByScore(
ORDER_EXPIRE_ZSET, 0, now);
}
}
/**
* 處理過期訂單
*/
private void processExpiredOrder(String orderId) {
// 防重處理
String lockKey = "order:expire:process:" + orderId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(locked)) {
try {
boolean result = orderService.cancelExpiredOrder(orderId);
if (result) {
log.info("成功處理過期訂單: {}", orderId);
}
} finally {
redisTemplate.delete(lockKey);
}
}
}
}
六、配置類
@Configuration
@EnableScheduling
public class OrderExpireConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 設置key和value的序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public OrderExpireManager orderExpireManager() {
return new OrderExpireManager();
}
@PostConstruct
public void init() {
// 啟動過期監(jiān)控
orderExpireManager().startExpireMonitor();
}
}
七、API接口
@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public ApiResponse<Order> createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(
request.getUserId(),
request.getAmount()
);
return ApiResponse.success(order);
}
@PostMapping("/{orderNo}/pay")
public ApiResponse<Void> payOrder(@PathVariable String orderNo) {
boolean success = orderService.processPayment(orderNo);
if (success) {
return ApiResponse.success("支付成功");
} else {
return ApiResponse.error("支付失敗");
}
}
@GetMapping("/{orderNo}/status")
public ApiResponse<OrderStatus> getOrderStatus(@PathVariable String orderNo) {
// 從Redis或數據庫獲取訂單狀態(tài)
return ApiResponse.success(OrderStatus.PENDING_PAYMENT);
}
}
八、測試類
@SpringBootTest
@Slf4j
class OrderExpireTest {
@Autowired
private OrderService orderService;
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void testOrderExpire() throws InterruptedException {
// 創(chuàng)建訂單
Order order = orderService.createOrder(1L, new BigDecimal("100.00"));
// 驗證Redis中已設置過期
String expireKey = "order:expire:" + order.getOrderNo();
String value = redisTemplate.opsForValue().get(expireKey);
assertNotNull(value);
// 驗證訂單狀態(tài)
assertEquals(OrderStatus.PENDING_PAYMENT, order.getStatus());
// 等待訂單過期
Thread.sleep(2000); // 實際應該等30分鐘
// 測試支付
boolean paid = orderService.processPayment(order.getOrderNo());
assertTrue(paid);
}
@Test
void testConcurrentPay() throws InterruptedException {
Order order = orderService.createOrder(2L, new BigDecimal("200.00"));
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(5);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
try {
boolean result = orderService.processPayment(order.getOrderNo());
if (result) {
successCount.incrementAndGet();
}
} catch (Exception e) {
log.error("支付異常", e);
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// 只有一個支付成功
assertEquals(1, successCount.get());
}
}
九、監(jiān)控和告警
@Component
@Slf4j
public class OrderExpireMonitor {
@Autowired
private StringRedisTemplate redisTemplate;
@Scheduled(fixedRate = 60000) // 每分鐘執(zhí)行一次
public void monitorExpireOrders() {
String expireKeyPattern = "order:expire:*";
// 統(tǒng)計待過期訂單數量
Set<String> keys = redisTemplate.keys(expireKeyPattern);
long expireCount = keys != null ? keys.size() : 0;
// 統(tǒng)計即將在5分鐘內過期的訂單
long soonExpireCount = keys.stream()
.map(key -> redisTemplate.getExpire(key, TimeUnit.SECONDS))
.filter(ttl -> ttl != null && ttl > 0 && ttl <= 300)
.count();
// 記錄監(jiān)控日志
log.info("訂單過期監(jiān)控 - 待過期訂單: {}, 即將過期訂單: {}",
expireCount, soonExpireCount);
// 發(fā)送告警
if (soonExpireCount > 100) {
sendAlert("大量訂單即將過期: " + soonExpireCount + " 個");
}
}
private void sendAlert(String message) {
// 發(fā)送告警到監(jiān)控系統(tǒng)
log.warn("訂單過期告警: {}", message);
}
}
總結建議
推薦方案
生產環(huán)境推薦組合方案:
- 主方案:Redisson延時隊列? + Redis鍵過期回調
- 兜底方案:定時任務掃描ZSet
- 防重處理:Redis分布式鎖
方案對比:
- Redis鍵過期回調:實時性最好,但可靠性依賴Redis配置
- Redisson延時隊列:功能強大,支持分布式,推薦使用
- ZSet定時掃描:實現簡單,但實時性較差
- 多級策略:最可靠,但實現復雜
注意事項:
- 一定要配置Redis的
notify-keyspace-events Ex - 考慮網絡分區(qū)和Redis故障的情況
- 實現冪等性處理,防止重復取消
- 添加監(jiān)控和告警
- 考慮持久化,防止重啟后數據丟失
性能優(yōu)化:
- 使用批量處理過期訂單
- 異步處理過期邏輯
- 合理設置掃描頻率
- 使用連接池
這種實現可以確保訂單過期功能的可靠性和實時性,適合電商等高并發(fā)場景。
到此這篇關于springboot+redis實現訂單過期(超時取消)功能的方法詳解的文章就介紹到這了,更多相關springboot訂單超時取消內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
mac下idea啟動web項目報錯java.net.SocketException:socket closed
本文主要介紹了作者在項目啟動時遇到的一個問題——無法打開調試端口,經過一系列排查和嘗試,最終發(fā)現是由于權限問題導致的,作者還分享了如何修改文件權限的方法,并提醒大家不要隨意kill掉占用端口的進程2024-12-12
SpringBoot繼承LogStash實現日志收集的方法示例
這篇文章主要介紹了SpringBoot繼承LogStash實現日志收集的方法示例,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-05-05
基于java servlet過濾器和監(jiān)聽器(詳解)
下面小編就為大家?guī)硪黄趈ava servlet過濾器和監(jiān)聽器(詳解)。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10
解決Maven?build打包,類找不到,找不到引用的那個項目里面的類問題
文章討論了在使用Maven構建項目時遇到類找不到的問題,并給出了解決方法,主要原因是公共項目被打成了可執(zhí)行的jar,導致其他項目無法引用其中的類,解決方法包括在公共項目的pom中將打包類型改為普通jar,以及在主項目中正確引用公共項目2026-02-02
在Java中基于Geotools對PostGIS數據庫的空間查詢實踐教程
本文將深入探討這一實踐,從連接配置到復雜空間查詢操作,包括點查詢、區(qū)域范圍查詢以及空間關系判斷等,全方位展示如何在 Java 環(huán)境下借助 Geotools 駕馭 PostGIS 數據庫,實現高效精準的空間數據檢索,為相關領域開發(fā)者提供實用的技術路徑,助力空間數據應用的創(chuàng)新拓展2025-05-05

