
如果你正在寻找一个功能完整、架构先进、开箱即用的电商解决方案,那么 ZKMall-B2B2C项目或许正是你需要的!该项目基于 Spring Boot 3 + Vue3 构建,涵盖:
✅ 前台商城(用户端)
✅ 后台管理(平台 + 商家双后台)
✅ 完整订单流程:商品、购物车、下单、支付、退款
✅ 会员体系、优惠券、积分、权限控制
✅ 多商户(B2B2C)支持
适合平台型电商场景无论是学习架构,还是快速搭建商业级电商系统,它都是极佳的选择!
在业务系统中,我们经常遇到需要"延迟执行某个操作"的需求。例如:
在 ZKMall 的活动系统中,一个典型活动的生命周期包含多个关键时间节点:
创建\] → \[报名待开始\] → \[报名进行中\] → \[活动待开始\] → \[活动进行中\] → \[活动已结束\] → \[自动退款
传统做法依赖 定时任务轮询数据库,不仅效率低、延迟高,还容易因服务重启或分布式部署导致状态错乱。
为解决这一问题,ZKMall 采用 Redisson 分布式延时队列 ,在活动创建时预埋多个延时任务 ,实现状态变更的精准、自动、无轮询驱动。
🔍 为什么选择 Redisson 延时队列?
| 方案 | 缺陷 | Redisson 优势 |
|---|---|---|
| 动态 Quartz 任务 | 服务重启丢失、分布式难协调 | ✅ 任务持久化在 Redis,重启不丢 |
| 数据库轮询 | 高频查询、CPU 浪费 | ✅ 事件驱动,零轮询 |
| 自建时间轮 | 开发复杂、维护成本高 | ✅ 开箱即用,API 简洁 |
| RabbitMQ TTL | 不支持取消、精度低 | ✅ 支持精确延迟 + 任务取消 |
💡 Redisson 利用 Redis 的 ZSet(有序集合) 存储任务到期时间,后台线程自动将到期任务推入消费队列,天然支持分布式、高并发。
🧱 ZKMall 延时队列架构设计
在 zkmall-admin/redis 模块中封装了一套通用延时队列组件,包含三个核心部分:
1️⃣ 延时任务监听接口
public interface RedisDelayedQueueListener<T> {
void invoke(T t);
}
所有延时任务消费者需实现此接口,定义具体业务逻辑。
2️⃣ 延时队列操作工具类
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisDelayedQueue {
private final RedissonClient redissonClient;
public <T> void addQueue(T t, long delay, TimeUnit timeUnit, String queueName) {
log.info("addQueue name {} delay {} timeUnit {}", queueName, delay, timeUnit);
RBlockingQueue<T> blockingQueue = redissonClient.getBlockingQueue(queueName);
RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
delayedQueue.offer(t, delay, timeUnit);
}
// 支持取消任务(如用户提前支付)
public <T> void removeFromQueue(T t, String queueName) {
RBlockingQueue<T> blockingQueue = redissonClient.getBlockingQueue(queueName);
RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
delayedQueue.remove(t);
}
}
✅ 封装了
offer和remove,支持动态添加/取消任务。
3️⃣ 自动初始化监听器
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisDelayedQueueInit implements ApplicationContextAware {
private final RedissonClient redissonClient;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
// 扫描所有 RedisDelayedQueueListener 实现类
Map<String, RedisDelayedQueueListener> listeners =
applicationContext.getBeansOfType(RedisDelayedQueueListener.class);
for (var entry : listeners.entrySet()) {
String queueName = entry.getValue().getClass().getName();
startConsumerThread(queueName, entry.getValue());
}
}
private <T> void startConsumerThread(String queueName, RedisDelayedQueueListener<T> listener) {
RBlockingQueue<T> queue = redissonClient.getBlockingQueue(queueName);
// ⚠️ 必须先获取 DelayedQueue,否则 take() 可能阻塞无数据
redissonClient.getDelayedQueue(queue);
Thread thread = new Thread(() -> {
log.info("启动延时队列监听线程: {}", queueName);
while (!Thread.currentThread().isInterrupted()) {
try {
T task = queue.take(); // 阻塞等待任务
log.info("消费延时任务: {} => {}", queueName, JSON.toJSONString(task));
// 异步执行,避免阻塞队列线程
CompletableFuture.runAsync(() -> listener.invoke(task));
} catch (Exception e) {
log.error("延时队列消费异常", e);
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
}
}
}, "DelayQueue-" + queueName);
thread.setDaemon(false);
thread.start();
}
}
✅ 项目启动时自动扫描所有监听器,为每个 Listener 启动独立消费线程,天然支持多队列隔离。
🎯 实战:实现"活动状态变更"
步骤 1:定义任务 DTO
@Data
@AllArgsConstructor
public class TaskBodyDTO {
private String code;
private String body;
}
步骤 2:实现监听器
// 监听器
@Slf4j
@Component
@RequiredArgsConstructor
public class DelayQueueConsumeListener implements RedisDelayedQueueListener<TaskBodyDTO> {
private final OrderService orderService;
@Override
public void invoke(PayTimeoutTask task) {
log.info("invoke redis Listener {} {}", taskBodyDTO.getCode(), taskBodyDTO.getBody());
String code = taskBodyDTO.getCode();
String body = taskBodyDTO.getBody();
String time = TimeUtils.yyMMddHHmmss();
List<String> bodyList = new ArrayList<>();
if (body.contains("-")) {
bodyList = Stream.of(body.split("-")).toList();
}
try {
if (StringEnum.SHOP_SIGN_IN.getCode().equals(code)) {
//修改当前活动状态为报名进行中
updateActivityState(body, time, IntegerEnum.ACTIVITY_SIGN_ON.getCode());
} else if (StringEnum.SHOP_ACTIVITY_STAY.getCode().equals(code)) {
//修改当前活动状态为活动待开始
updateActivityState(body, time, IntegerEnum.ACTIVITY_STAY_START.getCode());
} else if (StringEnum.SHOP_ACTIVITY_IN.getCode().equals(code)) {
//修改当前活动状态为活动进行中
updateActivityState(body, time, IntegerEnum.ACTIVITY_START.getCode());
} else if (StringEnum.SHOP_ACTIVITY_END.getCode().equals(code)) {
//修改当前活动状态为活动已结束
updateActivityState(body, time, IntegerEnum.ACTIVITY_END.getCode());
} else if (StringEnum.THREE_DAY_REFUND_BOND.getCode().equals(code)) {
//报名失败3天后自动退款
signError(bodyList.get(0), bodyList.get(1));
} else if (StringEnum.ACTIVITY_END_FIFTEAN_REFUND_BOND.getCode().equals(code)) {
//活动结束15天后自动退款至商家微信
signRefund(bodyList.get(0), bodyList.get(1));
}
//删除redis延时任务记录
cereRedisKeyService.deleteByKey(code + "-" + body);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
💡 实际业务中建议幂等处理(防止重复消费导致状态错乱)。
步骤 3:提交延时任务
在用户生成支付二维码后调用:
TaskBodyDTO dto = new TaskBodyDTO();
dto.setCode(code);
dto.setBody(body);
redisDelayedQueue.addQueue(
dto, mills, TimeUnit.MILLISECONDS,DelayQueueConsumeListener.class.getName()
);
cereRedisKeyService.add(code + "-" + body, invokeTime);
✅ 队列名称使用 Listener 全类名,确保唯一性与自动绑定。
步骤 4:用户提前支付?取消任务!
TaskBodyDTO dto = new TaskBodyDTO(code, body);
redisDelayedQueue.removeFromQueue(dto, DelayQueueConsumeListener.class.getName());
cereRedisKeyService.deleteByKey(code + "-" + body);
⚠️ 注意:
remove()依赖对象的equals()和hashCode()。建议 DTO 使用 Lombok 的@EqualsAndHashCode,或确保字段值完全一致。
📊 效果对比
| 方案 | 延迟精度 | 资源消耗 | 分布式支持 | 开发复杂度 |
|---|---|---|---|---|
| 定时轮询(每分钟扫库) | ±60秒 | 高(CPU/DB) | 需加锁防重 | 低 |
| Quartz 动态任务 | ±1秒 | 中 | 复杂(需集群配置) | 高 |
| Redisson 延时队列 | ±10ms | 极低 | 天然支持 | 低 |
✅ 总结
通过 Redisson 延时队列,ZKMall 实现了:
- 低成本:复用现有 Redis,无需引入 MQ
- 高性能:内存操作,毫秒级延迟
- 高可用:分布式部署,任务不重复
- 易维护:标准化接口,开箱即用
- 活动状态全自动流转,无需人工干预