大家好,我是小悟。
听说你要用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就是这个场景下的“时间管理大师”!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海