在Spring Boot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案。以下是完整的实现方案:
一、Redis键过期回调方案(推荐)
1. 配置Redis监听器
java
@Configuration
public class RedisKeyExpirationConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
2. 监听键过期事件
typescript
@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);
}
}
}
3. Redis配置(开启键空间通知)
在Redis配置文件redis.conf中开启键空间通知:
bash
# 开启键空间通知
notify-keyspace-events Ex
# 或者开启所有事件
# notify-keyspace-events AKE
Spring Boot配置:
yaml
spring:
redis:
host: localhost
port: 6379
database: 0
# 监听键过期事件
listen-patterns: "__keyevent@*__:expired"
二、延时队列方案(Redisson实现)
1. 添加Redisson依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
2. 使用Redisson延时队列
typescript
@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有序集合方案
typescript
@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. 订单状态枚举
arduino
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. 订单实体
less
@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. 订单服务实现
scss
@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());
}
}
4. 订单支付事件
scala
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;
}
}
五、多级过期策略(增强版)
typescript
@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);
}
}
}
}
六、配置类
typescript
@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();
}
}
七、API接口
less
@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);
}
}
八、测试类
ini
@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());
}
}
九、监控和告警
typescript
@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);
}
}
总结建议
推荐方案
-
生产环境推荐组合方案:
- 主方案:Redisson延时队列 + Redis键过期回调
- 兜底方案:定时任务扫描ZSet
- 防重处理:Redis分布式锁
-
方案对比:
- Redis键过期回调:实时性最好,但可靠性依赖Redis配置
- Redisson延时队列:功能强大,支持分布式,推荐使用
- ZSet定时扫描:实现简单,但实时性较差
- 多级策略:最可靠,但实现复杂
-
注意事项:
- 一定要配置Redis的
notify-keyspace-events Ex - 考虑网络分区和Redis故障的情况
- 实现幂等性处理,防止重复取消
- 添加监控和告警
- 考虑持久化,防止重启后数据丢失
- 一定要配置Redis的
-
性能优化:
- 使用批量处理过期订单
- 异步处理过期逻辑
- 合理设置扫描频率
- 使用连接池
这种实现可以确保订单过期功能的可靠性和实时性,适合电商等高并发场景。