SpringBoot集成Redis實現(xiàn)訂單超時管理的詳細步驟
聽說你要用Redis來處理超時支付訂單?Redis就像一個住在你內存里的閃電俠,它跑得飛快,但記性有點差(斷電就失憶)。它是個鍵值對存儲的社交恐懼癥患者,就喜歡簡單直接的交流。不過對付訂單超時這種“限時任務”,它可是專業(yè)的“時間管理大師”!
為什么選Redis來做這個?
你開了一家網紅奶茶店,顧客下單后30分鐘不付款,訂單就自動取消。你總不能雇個店員盯著每個訂單看30分鐘吧?Redis的過期鍵和發(fā)布訂閱功能,就是那個不知疲倦的“自動取消專員”!
詳細步驟:讓我們開始組裝這個“訂單取消機器人”
第1步:引入Redis依賴包
<!-- pom.xml 里加入這個“能量飲料” -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>第2步:配置Redis連接
# application.yml
spring:
redis:
# Redis的地址,默認是本地6379端口
host: localhost
port: 6379
# 密碼(如果設置了的話)
password:
# 數(shù)據庫索引,就像給閃電俠安排的第幾個房間
database: 0
lettuce:
pool:
# 連接池配置,別讓閃電俠累著了
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100ms第3步:配置RedisTemplate
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 鍵的序列化 - 字符串序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 值的序列化 - JSON序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.activateDefaultTyping(
mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}第4步:訂單實體類
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class Order {
private String orderId; // 訂單ID
private String userId; // 用戶ID
private Double amount; // 訂單金額
private Integer status; // 訂單狀態(tài):0-待支付,1-已支付,2-已取消
private LocalDateTime createTime;// 創(chuàng)建時間
private LocalDateTime expireTime;// 過期時間
// 判斷是否已過期
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}第5步:Redis服務類
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisOrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 訂單前綴,避免鍵沖突
private static final String ORDER_KEY_PREFIX = "order:pay:";
private static final String ORDER_EXPIRE_CHANNEL = "order.expire";
/**
* 創(chuàng)建訂單并設置30分鐘過期時間
* 就像給閃電俠說:“盯著這個訂單,30分鐘后提醒我”
*/
public void createOrderWithExpire(Order order, int expireMinutes) {
String orderKey = ORDER_KEY_PREFIX + order.getOrderId();
// 保存訂單到Redis,30分鐘后自動刪除
redisTemplate.opsForValue().set(
orderKey,
order,
expireMinutes,
TimeUnit.MINUTES
);
// 同時設置一個簡單的標志,用于監(jiān)聽過期事件
stringRedisTemplate.opsForValue().set(
orderKey + ":flag",
"1",
expireMinutes,
TimeUnit.MINUTES
);
System.out.println("訂單 " + order.getOrderId() + " 已放入Redis,設置" +
expireMinutes + "分鐘后過期");
}
/**
* 用戶支付成功,刪除過期鍵
* 相當于告訴閃電俠:“不用盯了,顧客付錢了!”
*/
public void handlePaymentSuccess(String orderId) {
String orderKey = ORDER_KEY_PREFIX + orderId;
// 手動刪除訂單和標志
redisTemplate.delete(orderKey);
stringRedisTemplate.delete(orderKey + ":flag");
System.out.println("訂單 " + orderId + " 支付成功,已從Redis移除");
}
/**
* 檢查訂單是否還存在(是否已過期)
*/
public boolean isOrderExist(String orderId) {
String orderKey = ORDER_KEY_PREFIX + orderId;
return Boolean.TRUE.equals(redisTemplate.hasKey(orderKey));
}
/**
* 獲取訂單信息
*/
public Order getOrder(String orderId) {
String orderKey = ORDER_KEY_PREFIX + orderId;
return (Order) redisTemplate.opsForValue().get(orderKey);
}
}第6步:Redis過期監(jiān)聽配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Configuration
public class RedisExpireConfig {
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 監(jiān)聽所有key過期事件
container.addMessageListener(listenerAdapter,
new PatternTopic("__keyevent@0__:expired"));
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(RedisKeyExpireListener receiver) {
return new MessageListenerAdapter(receiver, "handleMessage");
}
}第7步:過期事件監(jiān)聽器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
@Component
public class RedisKeyExpireListener extends MessageListenerAdapter {
@Autowired
private OrderService orderService;
/**
* 當Redis鍵過期時,這個方法會被調用
* 閃電俠會喊:“嘿!那個訂單過期了!”
*/
@Override
public void handleMessage(Message message, byte[] pattern) {
String expiredKey = new String(message.getBody(), StandardCharsets.UTF_8);
// 只處理我們的訂單過期鍵
if (expiredKey.startsWith("order:pay:")) {
// 去掉":flag"后綴獲取訂單ID
String orderId = expiredKey
.replace("order:pay:", "")
.replace(":flag", "");
System.out.println("Redis報告:訂單 " + orderId + " 已超時!");
// 處理訂單超時邏輯
orderService.cancelExpiredOrder(orderId);
}
}
}第8步:訂單服務層
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private RedisOrderService redisOrderService;
@Autowired
private OrderRepository orderRepository;
/**
* 創(chuàng)建訂單
*/
@Transactional
public Order createOrder(String userId, Double amount) {
Order order = new Order();
order.setOrderId(generateOrderId());
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(0); // 待支付
order.setCreateTime(LocalDateTime.now());
order.setExpireTime(LocalDateTime.now().plusMinutes(30));
// 保存到數(shù)據庫
orderRepository.save(order);
// 保存到Redis并設置30分鐘過期
redisOrderService.createOrderWithExpire(order, 30);
return order;
}
/**
* 處理支付回調
*/
@Transactional
public void handlePaymentCallback(String orderId) {
// 檢查訂單是否已過期
if (!redisOrderService.isOrderExist(orderId)) {
throw new RuntimeException("訂單已超時,請重新下單");
}
// 更新訂單狀態(tài)為已支付
orderRepository.updateOrderStatus(orderId, 1);
// 從Redis移除過期鍵
redisOrderService.handlePaymentSuccess(orderId);
System.out.println("訂單 " + orderId + " 支付處理完成");
}
/**
* 取消超時訂單
*/
@Transactional
public void cancelExpiredOrder(String orderId) {
// 再次檢查,防止重復處理
Order order = orderRepository.findById(orderId);
if (order != null && order.getStatus() == 0) {
order.setStatus(2); // 已取消
orderRepository.save(order);
// 可以在這里添加其他邏輯,比如釋放庫存、發(fā)送通知等
System.out.println("訂單 " + orderId + " 因超時未支付已被自動取消");
// 發(fā)送取消通知
sendCancelNotification(order);
}
}
/**
* 發(fā)送取消通知(模擬)
*/
private void sendCancelNotification(Order order) {
// 這里可以集成消息隊列、郵件、短信等
System.out.println("發(fā)送通知:親愛的用戶" + order.getUserId() +
",您的訂單" + order.getOrderId() + "因超時未支付已取消");
}
private String generateOrderId() {
return "ORD" + System.currentTimeMillis() +
(int)(Math.random() * 1000);
}
}第9步:控制器層
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private RedisOrderService redisOrderService;
/**
* 創(chuàng)建訂單
*/
@PostMapping("/create")
public ApiResult createOrder(@RequestParam String userId,
@RequestParam Double amount) {
try {
Order order = orderService.createOrder(userId, amount);
return ApiResult.success("訂單創(chuàng)建成功", order);
} catch (Exception e) {
return ApiResult.error("訂單創(chuàng)建失?。? + e.getMessage());
}
}
/**
* 模擬支付
*/
@PostMapping("/pay")
public ApiResult payOrder(@RequestParam String orderId) {
try {
// 模擬支付處理時間
Thread.sleep(1000);
orderService.handlePaymentCallback(orderId);
return ApiResult.success("支付成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return ApiResult.error("支付處理中斷");
} catch (Exception e) {
return ApiResult.error("支付失?。? + e.getMessage());
}
}
/**
* 檢查訂單狀態(tài)
*/
@GetMapping("/status/{orderId}")
public ApiResult checkOrderStatus(@PathVariable String orderId) {
boolean exists = redisOrderService.isOrderExist(orderId);
if (exists) {
return ApiResult.success("訂單有效,請盡快支付");
} else {
return ApiResult.success("訂單已超時或不存在");
}
}
}
// 簡單的返回結果類
class ApiResult {
private boolean success;
private String message;
private Object data;
// 構造方法和getter/setter省略...
public static ApiResult success(String message) {
return new ApiResult(true, message, null);
}
public static ApiResult success(String message, Object data) {
return new ApiResult(true, message, data);
}
public static ApiResult error(String message) {
return new ApiResult(false, message, null);
}
}第10步:別忘了開啟Redis的鍵空間通知(重要?。?/h3>
在Redis配置文件(redis.conf)中或通過Redis命令行開啟:
# 方式1:配置文件 notify-keyspace-events "Ex" # 方式2:命令行(臨時生效) redis-cli config set notify-keyspace-events Ex
或者在你的Spring Boot應用啟動時自動配置:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class RedisConfigRunner implements CommandLineRunner {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void run(String... args) {
// 開啟鍵過期事件通知
stringRedisTemplate.getConnectionFactory()
.getConnection()
.serverCommands()
.configSet("notify-keyspace-events", "Ex");
System.out.println("Redis鍵空間通知已開啟");
}
}完整的工作流程
- 顧客下單:
POST /orders/create→ 訂單存入數(shù)據庫和Redis,開始30分鐘倒計時 - Redis盯梢:閃電俠開始計時,30分鐘寸步不離
- 顧客支付:
- 30分鐘內支付:
POST /orders/pay→ Redis刪除訂單,交易完成 - 超過30分鐘:Redis鍵自動過期 → 觸發(fā)過期事件 → 自動取消訂單
- 30分鐘內支付:
- 系統(tǒng)通知:給顧客發(fā)送“訂單已取消”的貼心小提示
一些高級玩法
方案優(yōu)化:使用Redisson的延遲隊列(更可靠)
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Service
public class RedissonOrderService {
@Autowired
private RedissonClient redissonClient;
private RBlockingDeque<String> orderQueue;
private RDelayedQueue<String> delayedQueue;
@PostConstruct
public void init() {
orderQueue = redissonClient.getBlockingDeque("orderDelayQueue");
delayedQueue = redissonClient.getDelayedQueue(orderQueue);
// 啟動消費者線程
new Thread(this::consumeExpiredOrders).start();
}
/**
* 添加延遲訂單
*/
public void addDelayOrder(String orderId, long delay, TimeUnit unit) {
delayedQueue.offer(orderId, delay, unit);
System.out.println("訂單 " + orderId + " 已加入延遲隊列,"
+ delay + " " + unit + "后過期");
}
/**
* 消費過期訂單
*/
private void consumeExpiredOrders() {
while (true) {
try {
// 阻塞獲取過期訂單
String orderId = orderQueue.take();
System.out.println("延遲隊列報告:訂單 " + orderId + " 已過期");
// 處理訂單取消邏輯...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}注意事項:防重復處理(冪等性)
// 在OrderService中添加防重復處理
@Transactional
public void cancelExpiredOrder(String orderId) {
// 使用Redis分布式鎖,防止多個實例同時處理同一個訂單
String lockKey = "order:cancel:lock:" + orderId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 再次檢查訂單狀態(tài)(雙重校驗)
Order order = orderRepository.findById(orderId);
if (order != null && order.getStatus() == 0) {
// 更新訂單狀態(tài)
order.setStatus(2);
orderRepository.save(order);
System.out.println("訂單 " + orderId + " 已取消");
}
} finally {
// 釋放鎖
redisTemplate.delete(lockKey);
}
}
}總結
- 性能爆表:Redis基于內存操作,處理速度堪比閃電俠跑步
- 精準定時:Redis的過期機制精準可靠,誤差極小
- 解耦神器:業(yè)務邏輯和定時任務分離,代碼清爽不油膩
- 擴展性強:輕松應對高并發(fā),加個Redis集群就能撐起雙11
- 資源友好:不需要額外的定時任務中間件,省心省力
但也要注意這些“坑”
- Redis持久化:記得配置RDB/AOF,不然閃電俠“失憶”就麻煩了
- 網絡波動:Redis掛了怎么辦?要有降級方案
- 事件丟失:Redis的過期事件可能丟失,重要業(yè)務要有補償機制
- 時鐘同步:多服務器時間要同步,別自己人跟自己人“打架”
最后
想象一下:
- 沒有Redis時:你的數(shù)據庫被定時任務掃得氣喘吁吁,每次都要問:“哪些訂單超時了?”
- 有了Redis后:Redis主動報告:“嘿!這幾個訂單超時了,快處理!”
這就好比從“挨家挨戶查水表”變成了“水表自己打電話報警”,效率提升不是一點點!好的架構,就是讓合適的工具做合適的事。Redis就是這個場景下的“時間管理大師”!

到此這篇關于別再用定時任務掃庫了!SpringBoot集成Redis實現(xiàn)訂單超時管理的文章就介紹到這了,更多相關SpringBoot集成Redis內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java基于Spire.XLS實現(xiàn)從Excel到PDF的發(fā)票自動化生成
隨著業(yè)務數(shù)字化進程的不斷推進,許多企業(yè)開始嘗試用自動化手段替代手工開票,本文將基于 Spire.XLS for Java 介紹如何快速實現(xiàn)從 Excel 到 PDF的發(fā)票生成自動化方法,感興趣的小伙伴可以了解下2025-06-06

