文章目录
- 定时任务扫描方案
- Redis过期监听方案
- [Redis Sorted Set延迟队列](#Redis Sorted Set延迟队列)
- RabbitMQ延迟队列
- RocketMQ延迟消息
- 时间轮算法
- 方案选型对比
定时任务扫描方案
方案原理
通过定时任务周期性扫描数据库,查询过期订单并批量处理。
核心流程
plain
定时任务触发 → 查询过期订单 → 遍历处理 → 更新状态 → 后续操作
↓ ↓ ↓ ↓ ↓
每分钟1次 LIMIT 1000 并发处理 乐观锁 恢复库存
释放优惠券
发送通知
关键技术点
java
// 1. 过期时间字段+索引
ALTER TABLE t_order ADD COLUMN expire_time DATETIME;
CREATE INDEX idx_status_expire ON t_order(status, expire_time);
// 2. 分布式锁保证单实例执行
RLock lock = redisson.getLock("schedule:order-expire");
if (lock.tryLock(0, 50, TimeUnit.SECONDS)) {
// 处理逻辑
}
// 3. 乐观锁防止并发冲突
UPDATE t_order SET status = 'CANCELLED', version = version + 1
WHERE order_id = ? AND status = 'CREATED' AND version = ?;
// 4. 限流控制处理速率
RateLimiter rateLimiter = RateLimiter.create(50.0); // 每秒50单
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐ | Spring @Scheduled或XXL-Job即可 |
| 时效性 | ⭐⭐⭐ | 取决于扫描频率,通常1-2分钟延迟 |
| 准确性 | ⭐⭐⭐⭐⭐ | 基于数据库,数据不会丢失 |
| 性能 | ⭐⭐⭐ | 频繁扫描数据库,有一定压力 |
| 扩展性 | ⭐⭐⭐ | 可通过分片提升,但有上限 |
Redis过期监听方案
方案原理
订单创建时在Redis中设置key,利用Redis的键过期事件(Keyspace Notifications)触发订单取消。
核心流程
plain
创建订单 → Redis SET key → key过期 → 监听到过期事件 → 取消订单
↓ ↓ ↓ ↓ ↓
order_1 expire:30min 自动删除 KeyExpired 查DB+处理
实现步骤
1. Redis配置
properties
# redis.conf
notify-keyspace-events Ex
# E: Keyspace events
# x: 过期事件
2. 订单创建时写入Redis
java
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
// 1. 创建订单
Order order = saveOrder(request);
// 2. 写入Redis,设置过期时间
String key = "order:expire:" + order.getOrderNo();
redisTemplate.opsForValue().set(key, order.getId().toString(), 30, TimeUnit.MINUTES);
return order;
}
}
3. 监听Redis过期事件
java
@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
@Autowired
private OrderCancelService orderCancelService;
public RedisKeyExpiredListener(RedisMessageListenerContainer container) {
super(container);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
// 解析订单号
if (expiredKey.startsWith("order:expire:")) {
String orderNo = expiredKey.replace("order:expire:", "");
// 异步取消订单
orderCancelService.cancelOrderAsync(orderNo);
}
}
}
常见问题
问题1: Redis过期事件不可靠
plain
风险点:
1. Redis主从复制延迟,从库可能收不到过期事件
2. Redis宕机,过期事件丢失
3. 过期删除策略是懒惰删除+定期删除,不保证立即触发
4. 高并发下,过期事件可能延迟
解决方案:
- Redis过期监听作为触发机制
- 定时任务扫描作为兜底方案
- 双重保障,确保订单不漏处理
问题2: 消息消费幂等性
java
// 同一个过期事件可能被多次消费
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
// 使用分布式锁保证只处理一次
RLock lock = redisson.getLock("lock:" + expiredKey);
if (lock.tryLock()) {
try {
processExpiredOrder(expiredKey);
} finally {
lock.unlock();
}
}
}
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐⭐ | 需要理解Redis发布订阅机制 |
| 时效性 | ⭐⭐⭐⭐ | 秒级触发,但不保证精确 |
| 可靠性 | ⭐⭐ | Redis宕机会丢失事件 |
| 性能 | ⭐⭐⭐⭐ | 无需扫描数据库,性能好 |
| 扩展性 | ⭐⭐⭐ | 受Redis性能限制 |
适用场景:
- 对时效性要求较高(秒级)
- 订单量中等(日订单量<50万)
- 需要结合定时任务兜底
Redis Sorted Set延迟队列
方案原理
利用Redis Sorted Set的score特性,将过期时间作为score,通过轮询获取到期任务。
核心流程
plain
创建订单 → ZADD delay:order → 轮询ZRANGEBYSCORE → 获取到期订单 → 处理
↓ ↓ ↓ ↓ ↓
order_1 score=过期时间戳 每秒查询一次 ZREM移除 取消订单
数据结构:
plain
Redis Sorted Set: delay:order:cancel
┌──────────────┬────────────────────┐
│ Member │ Score │
├──────────────┼────────────────────┤
│ ORD001:1 │ 1735084800000 │ ← 过期时间戳
│ ORD002:2 │ 1735084860000 │
│ ORD003:3 │ 1735084920000 │
└──────────────┴────────────────────┘
score = 订单创建时间 + 30分钟
完整实现
1. 订单创建时加入延迟队列
java
@Service
public class DelayQueueService {
private static final String DELAY_QUEUE_KEY = "delay:order:cancel";
@Autowired
private StringRedisTemplate redisTemplate;
public void addDelayTask(String orderNo, Long orderId, int delayMinutes) {
// 计算过期时间戳
long expireTime = System.currentTimeMillis() + delayMinutes * 60 * 1000L;
// 存储格式: orderNo:orderId
String member = orderNo + ":" + orderId;
// 加入Sorted Set
redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, member, expireTime);
}
public void removeDelayTask(String orderNo, Long orderId) {
String member = orderNo + ":" + orderId;
redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, member);
}
}
2. 轮询消费延迟队列
java
@Component
public class DelayQueueConsumer {
private static final String DELAY_QUEUE_KEY = "delay:order:cancel";
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderCancelService orderCancelService;
@Scheduled(fixedDelay = 1000) // 每秒执行一次
public void consumeDelayQueue() {
long now = System.currentTimeMillis();
// 查询已到期的任务
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore(DELAY_QUEUE_KEY, 0, now, 0, 100);
if (tasks == null || tasks.isEmpty()) {
return;
}
for (String task : tasks) {
// 解析订单信息
String[] parts = task.split(":");
String orderNo = parts[0];
Long orderId = Long.parseLong(parts[1]);
// 尝试删除(防止重复消费)
Long removed = redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, task);
if (removed != null && removed > 0) {
// 异步处理订单取消
orderCancelService.cancelOrderAsync(orderId);
}
}
}
}
3. 订单支付后移除延迟任务
java
@Service
public class PaymentService {
@Autowired
private DelayQueueService delayQueueService;
@Transactional
public void payOrder(Long orderId) {
// 1. 更新订单状态为已支付
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus("PAID");
orderRepository.save(order);
// 2. 从延迟队列中移除
delayQueueService.removeDelayTask(order.getOrderNo(), orderId);
}
}
4.Lua脚本优化(原子性保障)
lua
-- 原子性获取并删除到期任务
local key = KEYS[1]
local now = ARGV[1]
local limit = ARGV[2]
-- 获取到期任务
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)
if #tasks > 0 then
-- 删除已获取的任务
redis.call('ZREM', key, unpack(tasks))
end
return tasks
java
public List<String> popExpiredTasks(int limit) {
String script =
"local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, ARGV[2])" +
"if #tasks > 0 then redis.call('ZREM', KEYS[1], unpack(tasks)) end" +
"return tasks";
List<String> result = redisTemplate.execute(
new DefaultRedisScript<>(script, List.class),
Collections.singletonList(DELAY_QUEUE_KEY),
String.valueOf(System.currentTimeMillis()),
String.valueOf(limit)
);
return result;
}
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐⭐ | 需要理解Sorted Set和Lua脚本 |
| 时效性 | ⭐⭐⭐⭐⭐ | 秒级精确触发 |
| 可靠性 | ⭐⭐⭐⭐ | Redis持久化可保证数据不丢失 |
| 性能 | ⭐⭐⭐⭐⭐ | Redis操作高效,支持高并发 |
| 扩展性 | ⭐⭐⭐⭐ | 可多实例消费,水平扩展 |
适用场景:
- 对时效性要求高(秒级)
- 中大规模订单量
- 已有Redis基础设施
- 强烈推荐
RabbitMQ延迟队列
方案原理
利用RabbitMQ的死信队列(DLX)+ TTL机制实现延迟消息。
核心架构
plain
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ 订单服务 │ ──发送─→│ 延迟队列 │ ──过期─→│ 死信队列 │
│ │ 消息 │ (TTL=30min) │ 后 │ │
└──────────────┘ │ 无消费者 │ │ 取消订单 │
└──────────────────┘ └──────────────┘
↑
│
┌──────────────┐
│ 消费者监听 │
└──────────────┘
实现步骤
1. RabbitMQ配置
java
@Configuration
public class RabbitMQConfig {
// 延迟队列
public static final String DELAY_QUEUE = "queue.order.delay";
// 死信交换机
public static final String DLX_EXCHANGE = "exchange.order.dlx";
// 死信队列
public static final String DLX_QUEUE = "queue.order.cancel";
// 死信路由键
public static final String DLX_ROUTING_KEY = "order.cancel";
@Bean
public Queue delayQueue() {
return QueueBuilder.durable(DELAY_QUEUE)
.withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", DLX_ROUTING_KEY)
.build();
}
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE);
}
@Bean
public Queue dlxQueue() {
return new Queue(DLX_QUEUE);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(DLX_ROUTING_KEY);
}
}
2. 发送延迟消息
java
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public Order createOrder(OrderRequest request) {
// 1. 创建订单
Order order = saveOrder(request);
// 2. 发送延迟消息
OrderDelayMessage message = new OrderDelayMessage();
message.setOrderId(order.getId());
message.setOrderNo(order.getOrderNo());
rabbitTemplate.convertAndSend(
RabbitMQConfig.DELAY_QUEUE,
message,
msg -> {
// 设置TTL = 30分钟
msg.getMessageProperties().setExpiration("1800000");
return msg;
}
);
return order;
}
}
3. 消费死信队列
java
@Component
public class OrderCancelConsumer {
@Autowired
private OrderCancelService orderCancelService;
@RabbitListener(queues = RabbitMQConfig.DLX_QUEUE)
public void handleExpiredOrder(OrderDelayMessage message) {
log.info("收到过期订单消息: orderId={}", message.getOrderId());
// 取消订单
orderCancelService.cancelOrder(message.getOrderId());
}
}
4. 订单支付后取消延迟消息(无法实现)
plain
注意: RabbitMQ的TTL消息一旦发送,无法取消!
解决方案:
1. 消费时检查订单状态,如果已支付则忽略
2. 使用RabbitMQ延迟插件(rabbitmq_delayed_message_exchange)
- RabbitMQ延迟插件方案(推荐)
java
@Configuration
public class RabbitMQDelayConfig {
public static final String DELAY_EXCHANGE = "exchange.order.delay";
public static final String DELAY_QUEUE = "queue.order.cancel";
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(
DELAY_EXCHANGE,
"x-delayed-message",
true,
false,
args
);
}
@Bean
public Queue delayQueue() {
return new Queue(DELAY_QUEUE);
}
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue())
.to(delayExchange())
.with("order.cancel")
.noargs();
}
}
// 发送延迟消息
public void sendDelayMessage(OrderDelayMessage message, int delaySeconds) {
rabbitTemplate.convertAndSend(
DELAY_EXCHANGE,
"order.cancel",
message,
msg -> {
msg.getMessageProperties().setHeader("x-delay", delaySeconds * 1000);
return msg;
}
);
}
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐⭐ | 需要理解RabbitMQ机制 |
| 时效性 | ⭐⭐⭐⭐⭐ | 精确到秒级 |
| 可靠性 | ⭐⭐⭐⭐⭐ | 消息持久化,高可靠 |
| 性能 | ⭐⭐⭐⭐⭐ | 高吞吐量 |
| 扩展性 | ⭐⭐⭐⭐⭐ | 集群部署,水平扩展 |
适用场景:
- 大规模订单系统
- 对可靠性要求极高
- 已有RabbitMQ基础设施
RocketMQ延迟消息
方案原理
RocketMQ原生支持延迟消息,提供18个延迟级别。
延迟级别
plain
RocketMQ预设延迟级别:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
对应level:
level 1 = 1s
level 2 = 5s
level 3 = 10s
...
level 15 = 20m
level 16 = 30m ← 订单超时使用这个级别
level 17 = 1h
level 18 = 2h
实现步骤
1. 发送延迟消息
java
@Service
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public Order createOrder(OrderRequest request) {
// 1. 创建订单
Order order = saveOrder(request);
// 2. 发送延迟消息
OrderDelayMessage message = new OrderDelayMessage();
message.setOrderId(order.getId());
message.setOrderNo(order.getOrderNo());
// 发送延迟消息,延迟级别16 = 30分钟
rocketMQTemplate.syncSend(
"order-cancel-topic",
MessageBuilder.withPayload(message).build(),
3000,
16 // 延迟级别
);
return order;
}
}
2. 消费延迟消息
java
@Component
@RocketMQMessageListener(
consumerGroup = "order-cancel-consumer",
topic = "order-cancel-topic"
)
public class OrderCancelConsumer implements RocketMQListener<OrderDelayMessage> {
@Autowired
private OrderCancelService orderCancelService;
@Override
public void onMessage(OrderDelayMessage message) {
log.info("收到延迟消息: orderId={}", message.getOrderId());
try {
// 取消订单(幂等处理)
orderCancelService.cancelOrder(message.getOrderId());
} catch (Exception e) {
log.error("处理订单取消失败", e);
throw e; // 抛出异常,消息会重试
}
}
}
3. 自定义延迟时间(高级特性)
java
// RocketMQ 5.0+ 支持任意时间延迟
Message message = MessageBuilder
.withPayload(orderMessage)
.build();
// 设置延迟投递时间
long deliverTime = System.currentTimeMillis() + 30 * 60 * 1000; // 30分钟后
message.getHeaders().put(MessageConst.PROPERTY_TIMER_DELIVER_MS, deliverTime);
rocketMQTemplate.syncSend("order-cancel-topic", message);
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐ | 原生支持,使用简单 |
| 时效性 | ⭐⭐⭐⭐⭐ | 精确到秒级 |
| 可靠性 | ⭐⭐⭐⭐⭐ | 金融级可靠性 |
| 性能 | ⭐⭐⭐⭐⭐ | 百万级TPS |
| 扩展性 | ⭐⭐⭐⭐⭐ | 天然支持分布式 |
适用场景:
- 超大规模订单系统
- 对性能和可靠性要求极高
- 阿里云/腾讯云环境
时间轮算法
方案原理
基于Netty的HashedWheelTimer实现时间轮,将任务按时间分配到不同的槽位。
时间轮结构
plain
时间轮(TimeWheel)
0 1 2 3 4 5 6 7
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ [] │ [] │ [*]│ [] │ [*]│ [] │ [] │ [*]│
└────┴────┴────┴────┴────┴────┴────┴────┘
↑
指针
说明:
- 时间轮有8个槽位
- 每个槽位代表1秒
- 指针每秒转动一格
- 到达槽位时,执行该槽位的所有任务
- [*] 表示有任务的槽位
实现代码
java
@Component
public class TimeWheelService {
private final HashedWheelTimer timer;
@Autowired
private OrderCancelService orderCancelService;
public TimeWheelService() {
// 创建时间轮
// tickDuration: 每个槽位时间间隔 = 1秒
// ticksPerWheel: 时间轮槽位数 = 3600 (1小时)
this.timer = new HashedWheelTimer(
1, TimeUnit.SECONDS,
3600
);
}
public void addCancelTask(Long orderId, int delayMinutes) {
timer.newTimeout(timeout -> {
// 取消订单
orderCancelService.cancelOrder(orderId);
}, delayMinutes, TimeUnit.MINUTES);
}
@PreDestroy
public void destroy() {
timer.stop();
}
}
优缺点分析
| 维度 | 评价 | 说明 |
|---|---|---|
| 实现难度 | ⭐⭐⭐⭐ | 需要深入理解时间轮算法 |
| 时效性 | ⭐⭐⭐⭐ | 秒级精确 |
| 可靠性 | ⭐⭐ | 内存存储,服务重启丢失 |
| 性能 | ⭐⭐⭐⭐⭐ | O(1)时间复杂度 |
| 扩展性 | ⭐⭐ | 单机方案,难以扩展 |
适用场景:
- 小规模系统
- 对数据丢失容忍度高
- 追求极致性能
方案选型对比
| 方案 | 实现难度 | 时效性 | 可靠性 | 性能 | 扩展性 | 成本 | 推荐指数 |
|---|---|---|---|---|---|---|---|
| 定时任务扫描 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 低 | ⭐⭐⭐⭐ |
| Redis过期监听 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 低 | ⭐⭐⭐ |
| Redis Sorted Set | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 低 | ⭐⭐⭐⭐⭐ |
| RabbitMQ延迟队列 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 中 | ⭐⭐⭐⭐⭐ |
| RocketMQ延迟消息 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 中 | ⭐⭐⭐⭐⭐ |
| 时间轮算法 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | 低 | ⭐⭐⭐ |
小型项目 (< 10万单/天)
- 推荐: 定时任务扫描
- 理由: 实现简单,无需额外组件,数据量小扫描效率高
中型项目 (10-100万单/天)
- 推荐: Redis Sorted Set + 定时任务兜底
- 理由: 时效性好(秒级),Redis普及率高,双重保障可靠性
大型项目 (100-500万单/天)
- 推荐: RabbitMQ/RocketMQ延迟消息
- 理由: 高性能、高可靠,成熟的消息中间件,天然支持分布式
超大型项目 (> 500万单/天)
- 推荐: RocketMQ延迟消息 + Redis缓存
- 理由: 百万级TPS,金融级可靠性,完善的监控体系