大家好,我是小悟。
听说你要用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键空间通知已开启"); } }完整的工作流程
- 顾客下单:
POST /orders/create→ 订单存入数据库和Redis,开始30分钟倒计时 - Redis盯梢:闪电侠开始计时,30分钟寸步不离
- 顾客支付:
- 30分钟内支付:
POST /orders/pay→ Redis删除订单,交易完成 - 超过30分钟:Redis键自动过期 → 触发过期事件 → 自动取消订单
- 30分钟内支付:
- 系统通知:给顾客发送“订单已取消”的贴心小提示
一些高级玩法
方案优化:使用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); } } }总结
- 性能爆表:Redis基于内存操作,处理速度堪比闪电侠跑步
- 精准定时:Redis的过期机制精准可靠,误差极小
- 解耦神器:业务逻辑和定时任务分离,代码清爽不油腻
- 扩展性强:轻松应对高并发,加个Redis集群就能撑起双11
- 资源友好:不需要额外的定时任务中间件,省心省力
但也要注意这些“坑”
- Redis持久化:记得配置RDB/AOF,不然闪电侠“失忆”就麻烦了
- 网络波动:Redis挂了怎么办?要有降级方案
- 事件丢失:Redis的过期事件可能丢失,重要业务要有补偿机制
- 时钟同步:多服务器时间要同步,别自己人跟自己人“打架”
最后
想象一下:
- 没有Redis时:你的数据库被定时任务扫得气喘吁吁,每次都要问:“哪些订单超时了?”
- 有了Redis后:Redis主动报告:“嘿!这几个订单超时了,快处理!”
这就好比从“挨家挨户查水表”变成了“水表自己打电话报警”,效率提升不是一点点!好的架构,就是让合适的工具做合适的事。Redis就是这个场景下的“时间管理大师”!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海