🔥 本文是DelayQueue系列的下篇,聚焦实战应用场景和性能优化。通过多个真实案例,带你掌握DelayQueue在项目中的最佳实践和性能调优技巧。
📚 系列专栏推荐:
一、DelayQueue实战应用
1.1 订单超时自动取消
1.1.1 业务场景分析
在电商系统中,订单创建后通常需要在一定时间内完成支付,否则系统会自动取消订单并释放库存。这是一个典型的延时任务场景:
- 订单创建后,需要设置一个倒计时(通常为15分钟或30分钟)
- 如果在倒计时结束前完成支付,需要取消该延时任务
- 如果倒计时结束时订单仍未支付,则自动取消订单
- 系统需要支持大量并发订单的超时管理
传统实现方式通常采用定时任务扫描数据库,但这种方式存在以下问题:
- 数据库压力大,特别是订单量大的场景
- 实时性不够,可能出现几秒甚至几分钟的延迟
- 资源消耗高,需要频繁扫描数据库
使用DelayQueue可以很好地解决这些问题,实现内存级的订单超时管理。
1.1.2 实现方案设计
基于DelayQueue的订单超时取消方案设计如下:
- 创建一个实现Delayed接口的订单超时任务类
- 维护一个全局的DelayQueue,用于管理所有未支付订单的超时任务
- 订单创建时,向DelayQueue中添加对应的超时任务
- 订单支付成功时,从DelayQueue中移除对应的超时任务
- 启动专门的线程从DelayQueue中获取到期的任务,执行订单取消逻辑
这种设计的优势在于:
- 内存级处理,性能高
- 精确的超时控制,无需频繁扫描数据库
- 支持动态取消超时任务
- 系统重启后可以通过数据库中的订单状态和创建时间重建延时队列
1.1.3 完整代码示例
import java.util.Map;
import java.util.concurrent.*;
/**
* 基于DelayQueue实现的订单超时自动取消功能
*/
public class OrderTimeoutCancelService {
// 订单超时时间,单位毫秒
private final long ORDER_TIMEOUT = 30 * 60 * 1000; // 30分钟
// 延迟队列,用于处理订单超时
private final DelayQueue<OrderDelayTask> delayQueue = new DelayQueue<>();
// 用于存储订单与任务的映射关系,便于取消任务
private final Map<String, OrderDelayTask> taskMap = new ConcurrentHashMap<>();
// 订单服务,实际业务中通过依赖注入获取
private final OrderService orderService;
public OrderTimeoutCancelService(OrderService orderService) {
this.orderService = orderService;
// 启动处理线程
new Thread(this::processTimeoutOrders).start();
}
/**
* 添加订单超时任务
* @param orderId 订单ID
*/
public void addOrderTimeoutTask(String orderId) {
// 创建超时任务
OrderDelayTask task = new OrderDelayTask(orderId, ORDER_TIMEOUT);
// 添加到延迟队列
delayQueue.offer(task);
// 保存映射关系
taskMap.put(orderId, task);
System.out.println("订单[" + orderId + "]加入超时队列,将在"
+ ORDER_TIMEOUT/1000 + "秒后自动取消");
}
/**
* 订单支付成功,取消超时任务
* @param orderId 订单ID
*/
public void orderPaid(String orderId) {
OrderDelayTask task = taskMap.remove(orderId);
if (task != null) {
// 从队列中移除任务(这里利用了equals方法判断)
delayQueue.remove(task);
System.out.println("订单[" + orderId + "]已支付,取消超时任务");
}
}
/**
* 处理超时订单的线程任务
*/
private void processTimeoutOrders() {
System.out.println("订单超时处理线程已启动");
while (true) {
try {
// 获取超时的订单任务
OrderDelayTask task = delayQueue.take();
// 从映射中移除
taskMap.remove(task.getOrderId());
// 执行订单取消逻辑
orderService.cancelOrder(task.getOrderId(), "订单超时未支付");
System.out.println("订单[" + task.getOrderId() + "]超时未支付,已自动取消");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
// 处理异常,实际项目中应该有更完善的异常处理
System.err.println("处理超时订单异常:" + e.getMessage());
}
}
}
/**
* 订单延迟任务
*/
static class OrderDelayTask implements Delayed {
private final String orderId;
private final long expireTime; // 过期时间,单位:毫秒
public OrderDelayTask(String orderId, long delayTime) {
this.orderId = orderId;
this.expireTime = System.currentTimeMillis() + delayTime;
}
public String getOrderId() {
return orderId;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((OrderDelayTask) o).expireTime);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
OrderDelayTask that = (OrderDelayTask) obj;
return orderId.equals(that.orderId);
}
@Override
public int hashCode() {
return orderId.hashCode();
}
}
/**
* 模拟订单服务接口
*/
interface OrderService {
void cancelOrder(String orderId, String reason);
}
/**
* 测试代码
*/
public static void main(String[] args) throws InterruptedException {
// 模拟订单服务实现
OrderService orderService = (orderId, reason) ->
System.out.println("执行订单[" + orderId + "]取消操作,原因:" + reason);
// 创建订单超时服务
OrderTimeoutCancelService service = new OrderTimeoutCancelService(orderService);
// 模拟创建3个订单
service.addOrderTimeoutTask("ORDER_001");
service.addOrderTimeoutTask("ORDER_002");
service.addOrderTimeoutTask("ORDER_003");
// 模拟1秒后支付了订单2
Thread.sleep(1000);
service.orderPaid("ORDER_002");
// 等待所有订单处理完成
Thread.sleep(35000);
}
}
1.1.4 注意事项与优化点
-
任务去重:
- 重写了equals和hashCode方法,确保可以根据订单ID正确移除任务
- 使用ConcurrentHashMap存储订单ID与任务的映射,便于快速查找和取消任务
-
异常处理:
- 处理线程中捕获所有异常,避免因单个任务异常导致整个处理线程终止
- 实际项目中应该添加更完善的日志记录和异常处理机制
-
系统重启恢复:
- 系统重启后,内存中的DelayQueue会丢失所有任务
- 解决方案:系统启动时,从数据库加载所有未支付且未超时的订单,重新加入DelayQueue
// 系统启动时恢复未处理的订单超时任务 public void recoverOrderTasks() { List<Order> pendingOrders = orderService.findPendingPaymentOrders(); for (Order order : pendingOrders) { // 计算剩余超时时间 long createTime = order.getCreateTime().getTime(); long now = System.currentTimeMillis(); long remainTimeout = createTime + ORDER_TIMEOUT - now; // 如果订单还未超时,则加入延迟队列 if (remainTimeout > 0) { OrderDelayTask task = new OrderDelayTask(order.getOrderId(), remainTimeout); delayQueue.offer(task); taskMap.put(order.getOrderId(), task); } else { // 已超时但未处理的订单,直接执行取消逻辑 orderService.cancelOrder(order.getOrderId(), "系统重启,订单超时未支付"); } } System.out.println("成功恢复" + pendingOrders.size() + "个未支付订单的超时任务"); }
-
性能优化:
- 使用线程池替代单个线程处理超时订单,提高并发处理能力
- 批量处理超时订单,减少数据库操作次数
- 考虑使用分布式延迟队列,解决单机容量和可靠性问题
1.2 限时优惠券管理
1.2.1 优惠券过期处理
电商和营销系统中,限时优惠券是常见的营销手段。优惠券通常有固定的有效期,过期后需要自动失效。传统的优惠券过期处理方式有:
- 定时任务扫描:定期扫描数据库,将过期优惠券标记为失效
- 使用时判断:用户使用优惠券时判断是否过期
- 缓存过期:将优惠券信息存入Redis等缓存,设置过期时间
这些方式各有优缺点,但都不够实时或资源消耗较大。使用DelayQueue可以实现内存级的优惠券过期管理,既保证实时性,又减少系统资源消耗。
1.2.2 动态失效时间控制
优惠券的一个特点是失效时间可能是动态的:
- 固定日期失效:如"2023-12-31 23:59:59"
- 相对时间失效:如"领取后7天内有效"
- 活动结束失效:如"双11活动结束后失效"
DelayQueue可以很好地支持这些动态失效时间控制,只需在创建延时任务时计算正确的延迟时间即可。
1.2.3 并发安全处理
优惠券系统面临的并发场景主要有:
- 大量用户同时领取优惠券
- 用户使用优惠券的同时,优惠券可能正好过期
- 系统需要动态调整优惠券的有效期
这些场景需要保证数据一致性和操作的原子性。DelayQueue本身是线程安全的,但与数据库操作的配合需要特别注意事务和锁的使用。
1.2.4 实现代码与优化
import java.util.*;
import java.util.concurrent.*;
/**
* 基于DelayQueue实现的限时优惠券管理系统
*/
public class CouponExpirationManager {
// 延迟队列,用于处理优惠券过期
private final DelayQueue<CouponExpireTask> delayQueue = new DelayQueue<>();
// 用于存储优惠券ID与任务的映射关系
private final Map<String, CouponExpireTask> taskMap = new ConcurrentHashMap<>();
// 优惠券服务,实际业务中通过依赖注入获取
private final CouponService couponService;
// 线程池,用于处理过期优惠券
private final ExecutorService executorService;
public CouponExpirationManager(CouponService couponService) {
this.couponService = couponService;
// 创建线程池
this.executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
r -> {
Thread thread = new Thread(r, "coupon-expiration-thread");
thread.setDaemon(true); // 设置为守护线程
return thread;
}
);
// 启动处理线程
this.executorService.execute(this::processExpiredCoupons);
}
/**
* 添加优惠券过期任务
* @param couponId 优惠券ID
* @param expireTime 过期时间点(时间戳)
*/
public void addCouponExpireTask(String couponId, long expireTime) {
// 计算延迟时间
long delay = expireTime - System.currentTimeMillis();
if (delay <= 0) {
// 已过期,直接处理
couponService.expireCoupon(couponId);
return;
}
// 创建过期任务
CouponExpireTask task = new CouponExpireTask(couponId, delay);
// 添加到延迟队列
delayQueue.offer(task);
// 保存映射关系
taskMap.put(couponId, task);
System.out.println("优惠券[" + couponId + "]加入过期队列,将在"
+ new Date(expireTime) + "过期");
}
/**
* 更新优惠券过期时间
* @param couponId 优惠券ID
* @param newExpireTime 新的过期时间点(时间戳)
*/
public void updateCouponExpireTime(String couponId, long newExpireTime) {
// 移除旧任务
CouponExpireTask oldTask = taskMap.remove(couponId);
if (oldTask != null) {
delayQueue.remove(oldTask);
}
// 添加新任务
addCouponExpireTask(couponId, newExpireTime);
System.out.println("优惠券[" + couponId + "]过期时间已更新为" + new Date(newExpireTime));
}
/**
* 取消优惠券过期任务(优惠券被使用或手动作废)
* @param couponId 优惠券ID
*/
public void cancelExpireTask(String couponId) {
CouponExpireTask task = taskMap.remove(couponId);
if (task != null) {
delayQueue.remove(task);
System.out.println("优惠券[" + couponId + "]过期任务已取消");
}
}
/**
* 处理过期优惠券的线程任务
*/
private void processExpiredCoupons() {
System.out.println("优惠券过期处理线程已启动");
while (!Thread.currentThread().isInterrupted()) {
try {
// 批量处理过期优惠券,提高效率
List<CouponExpireTask> expiredTasks = new ArrayList<>();
CouponExpireTask task = delayQueue.take(); // 获取一个过期任务
expiredTasks.add(task);
// 尝试一次性获取多个过期任务
delayQueue.drainTo(expiredTasks, 100);
// 批量处理过期优惠券
List<String> couponIds = new ArrayList<>(expiredTasks.size());
for (CouponExpireTask expiredTask : expiredTasks) {
String couponId = expiredTask.getCouponId();
taskMap.remove(couponId);
couponIds.add(couponId);
}
// 批量更新数据库
couponService.batchExpireCoupons(couponIds);
System.out.println("已处理" + couponIds.size() + "张过期优惠券");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
System.err.println("处理过期优惠券异常:" + e.getMessage());
}
}
}
/**
* 关闭管理器
*/
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* 优惠券过期任务
*/
static class CouponExpireTask implements Delayed {
private final String couponId;
private final long expireTime; // 过期时间点,单位:毫秒
public CouponExpireTask(String couponId, long delayTime) {
this.couponId = couponId;
this.expireTime = System.currentTimeMillis() + delayTime;
}
public String getCouponId() {
return couponId;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((CouponExpireTask) o).expireTime);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
CouponExpireTask that = (CouponExpireTask) obj;
return couponId.equals(that.couponId);
}
@Override
public int hashCode() {
return couponId.hashCode();
}
}
/**
* 优惠券服务接口
*/
interface CouponService {
void expireCoupon(String couponId);
void batchExpireCoupons(List<String> couponIds);
}
/**
* 测试代码
*/
public static void main(String[] args) throws InterruptedException {
// 模拟优惠券服务实现
CouponService couponService = new CouponService() {
@Override
public void expireCoupon(String couponId) {
System.out.println("优惠券[" + couponId + "]已过期");
}
@Override
public void batchExpireCoupons(List<String> couponIds) {
System.out.println("批量处理过期优惠券:" + couponIds);
}
};
// 创建优惠券过期管理器
CouponExpirationManager manager = new CouponExpirationManager(couponService);
// 模拟添加优惠券过期任务
Calendar calendar = Calendar.getInstance();
// 优惠券1:5秒后过期
calendar.add(Calendar.SECOND, 5);
manager.addCouponExpireTask("COUPON_001", calendar.getTimeInMillis());
// 优惠券2:10秒后过期
calendar.add(Calendar.SECOND, 5);
manager.addCouponExpireTask("COUPON_002", calendar.getTimeInMillis());
// 优惠券3:15秒后过期
calendar.add(Calendar.SECOND, 5);
manager.addCouponExpireTask("COUPON_003", calendar.getTimeInMillis())