别再用定时任务扫库了!SpringBoot集成Redis实现订单超时管理

305 阅读8分钟

大家好,我是小悟。

听说你要用Redis来处理超时支付订单?Redis就像一个住在你内存里的闪电侠,它跑得飞快,但记性有点差(断电就失忆)。它是个键值对存储的社交恐惧症患者,就喜欢简单直接的交流。不过对付订单超时这种“限时任务”,它可是专业的“时间管理大师”!

为什么选Redis来做这个?

你开了一家网红奶茶店,顾客下单后30分钟不付款,订单就自动取消。你总不能雇个店员盯着每个订单看30分钟吧?Redis的过期键和发布订阅功能,就是那个不知疲倦的“自动取消专员”!


详细步骤:让我们开始组装这个“订单取消机器人”

第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: 
    # 数据库索引,就像给闪电侠安排的第几个房间
    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;          // 订单状态:0-待支付,1-已支付,2-已取消
    private LocalDateTime createTime;// 创建时间
    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";
    
    /**
     * 创建订单并设置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
        );
        
        // 同时设置一个简单的标志,用于监听过期事件
        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过期监听配置

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);
        
        // 监听所有key过期事件
        container.addMessageListener(listenerAdapter, 
            new PatternTopic("__keyevent@0__:expired"));
        
        return container;
    }
    
    @Bean
    public MessageListenerAdapter listenerAdapter(RedisKeyExpireListener receiver) {
        return new MessageListenerAdapter(receiver, "handleMessage");
    }
}

第7步:过期事件监听器

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;
    
    /**
     * 创建订单
     */
    @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));
        
        // 保存到数据库
        orderRepository.save(order);
        
        // 保存到Redis并设置30分钟过期
        redisOrderService.createOrderWithExpire(order, 30);
        
        return order;
    }
    
    /**
     * 处理支付回调
     */
    @Transactional
    public void handlePaymentCallback(String orderId) {
        // 检查订单是否已过期
        if (!redisOrderService.isOrderExist(orderId)) {
            throw new RuntimeException("订单已超时,请重新下单");
        }
        
        // 更新订单状态为已支付
        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);
            
            // 可以在这里添加其他逻辑,比如释放库存、发送通知等
            System.out.println("订单 " + orderId + " 因超时未支付已被自动取消");
            
            // 发送取消通知
            sendCancelNotification(order);
        }
    }
    
    /**
     * 发送取消通知(模拟)
     */
    private void sendCancelNotification(Order order) {
        // 这里可以集成消息队列、邮件、短信等
        System.out.println("发送通知:亲爱的用户" + 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;
    
    /**
     * 创建订单
     */
    @PostMapping("/create")
    public ApiResult createOrder(@RequestParam String userId, 
                                @RequestParam Double amount) {
        try {
            Order order = orderService.createOrder(userId, amount);
            return ApiResult.success("订单创建成功", order);
        } catch (Exception e) {
            return ApiResult.error("订单创建失败:" + 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());
        }
    }
    
    /**
     * 检查订单状态
     */
    @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的键空间通知(重要!)

在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键空间通知已开启");
    }
}

完整的工作流程

  1. 顾客下单POST /orders/create → 订单存入数据库和Redis,开始30分钟倒计时
  2. Redis盯梢:闪电侠开始计时,30分钟寸步不离
  3. 顾客支付
    • 30分钟内支付:POST /orders/pay → Redis删除订单,交易完成
    • 超过30分钟:Redis键自动过期 → 触发过期事件 → 自动取消订单
  4. 系统通知:给顾客发送“订单已取消”的贴心小提示

一些高级玩法

方案优化:使用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 {
            // 再次检查订单状态(双重校验)
            Order order = orderRepository.findById(orderId);
            if (order != null && order.getStatus() == 0) {
                // 更新订单状态
                order.setStatus(2);
                orderRepository.save(order);
                
                System.out.println("订单 " + orderId + " 已取消");
            }
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    }
}

总结

  1. 性能爆表:Redis基于内存操作,处理速度堪比闪电侠跑步
  2. 精准定时:Redis的过期机制精准可靠,误差极小
  3. 解耦神器:业务逻辑和定时任务分离,代码清爽不油腻
  4. 扩展性强:轻松应对高并发,加个Redis集群就能撑起双11
  5. 资源友好:不需要额外的定时任务中间件,省心省力

但也要注意这些“坑”

  1. Redis持久化:记得配置RDB/AOF,不然闪电侠“失忆”就麻烦了
  2. 网络波动:Redis挂了怎么办?要有降级方案
  3. 事件丢失:Redis的过期事件可能丢失,重要业务要有补偿机制
  4. 时钟同步:多服务器时间要同步,别自己人跟自己人“打架”

最后

想象一下:

  • 没有Redis时:你的数据库被定时任务扫得气喘吁吁,每次都要问:“哪些订单超时了?”
  • 有了Redis后:Redis主动报告:“嘿!这几个订单超时了,快处理!”

这就好比从“挨家挨户查水表”变成了“水表自己打电话报警”,效率提升不是一点点!好的架构,就是让合适的工具做合适的事。Redis就是这个场景下的“时间管理大师”!

别再用定时任务扫库了!SpringBoot集成Redis实现订单超时管理.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海