在Spring Boot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案。以下是完整的实现方案:
|
1 2 3 4 5 6 7 8 9 10 11 |
@Configuration public class RedisKeyExpirationConfig {
@Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@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;
/** * 监听所有键过期事件 */ @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监听线程 CompletableFuture.runAsync(() -> { boolean result = orderService.cancelExpiredOrder(orderId); if (result) { log.info("订单 {} 已成功取消", orderId); } else { log.warn("订单 {} 取消失败或已处理", orderId); } }); } catch (Exception e) { log.error("处理订单过期异常: {}", orderId, e); } } } |
在Redis配置文件redis.conf中开启键空间通知:
|
1 2 3 4 5 |
# 开启键空间通知 notify-keyspace-events Ex
# 或者开启所有事件 # notify-keyspace-events AKE |
Spring Boot配置:
|
1 2 3 4 5 6 7 |
spring: redis: host: localhost port: 6379 database: 0 # 监听键过期事件 listen-patterns: "__keyevent@*__:expired" |
|
1 2 3 4 5 |
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.5</version> </dependency> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
@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); } } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
@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秒执行一次 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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方法省略 } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@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; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
@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分钟未支付过期
/** * 创建订单 */ 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("创建订单成功: {}, 过期时间: {}", 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("订单不存在"));
// 检查订单状态 if (order.getStatus() != OrderStatus.PENDING_PAYMENT) { throw new RuntimeException("订单状态异常: " + order.getStatus()); }
// 检查是否过期 if (order.isExpired()) { order.setStatus(OrderStatus.EXPIRED); orderRepository.save(order); throw new RuntimeException("订单已过期"); }
// 更新订单状态 order.setStatus(OrderStatus.PAID); order.setUpdateTime(LocalDateTime.now()); orderRepository.save(order);
// 移除过期设置 removeOrderExpire(orderNo);
// 发布支付成功事件 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("订单不存在"));
// 双重检查:订单是否仍为待支付状态 if (order.getStatus() != OrderStatus.PENDING_PAYMENT) { log.info("订单 {} 状态已变更为 {},跳过取消", orderNo, order.getStatus()); return false; }
// 检查是否真的过期 if (!order.isExpired()) { log.info("订单 {} 未过期,跳过取消", orderNo); return false; }
// 更新订单状态 order.setStatus(OrderStatus.EXPIRED); order.setUpdateTime(LocalDateTime.now()); orderRepository.save(order);
// 释放库存等业务逻辑 releaseStock(order);
// 发送通知 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) { // 发送通知逻辑 log.info("发送订单 {} 过期通知", order.getOrderNo()); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
@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(); }
/** * 监控延时队列 */ 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("监控延时队列异常", 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); } } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@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() { // 启动过期监控 orderExpireManager().startExpireMonitor(); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@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或数据库获取订单状态 return ApiResponse.success(OrderStatus.PENDING_PAYMENT); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
@SpringBootTest @Slf4j class OrderExpireTest {
@Autowired private OrderService orderService;
@Autowired private StringRedisTemplate redisTemplate;
@Test void testOrderExpire() throws InterruptedException { // 创建订单 Order order = orderService.createOrder(1L, new BigDecimal("100.00"));
// 验证Redis中已设置过期 String expireKey = "order:expire:" + order.getOrderNo(); String value = redisTemplate.opsForValue().get(expireKey); assertNotNull(value);
// 验证订单状态 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()); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@Component @Slf4j public class OrderExpireMonitor {
@Autowired private StringRedisTemplate redisTemplate;
@Scheduled(fixedRate = 60000) // 每分钟执行一次 public void monitorExpireOrders() { String expireKeyPattern = "order:expire:*";
// 统计待过期订单数量 Set<String> keys = redisTemplate.keys(expireKeyPattern); long expireCount = keys != null ? keys.size() : 0;
// 统计即将在5分钟内过期的订单 long soonExpireCount = keys.stream() .map(key -> redisTemplate.getExpire(key, TimeUnit.SECONDS)) .filter(ttl -> ttl != null && ttl > 0 && ttl <= 300) .count();
// 记录监控日志 log.info("订单过期监控 - 待过期订单: {}, 即将过期订单: {}", expireCount, soonExpireCount);
// 发送告警 if (soonExpireCount > 100) { sendAlert("大量订单即将过期: " + soonExpireCount + " 个"); } }
private void sendAlert(String message) { // 发送告警到监控系统 log.warn("订单过期告警: {}", message); } } |
推荐方案
生产环境推荐组合方案:
方案对比:
注意事项:
性能优化:
这种实现可以确保订单过期功能的可靠性和实时性,适合电商等高并发场景。