ZKMall-B2B2C Redission延时队列

如果你正在寻找一个功能完整、架构先进、开箱即用的电商解决方案,那么 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);
    }
}

✅ 封装了 offerremove,支持动态添加/取消任务。


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
  • 高性能:内存操作,毫秒级延迟
  • 高可用:分布式部署,任务不重复
  • 易维护:标准化接口,开箱即用
  • 活动状态全自动流转,无需人工干预
相关推荐
q***96581 小时前
深入解析Spring Boot中的@ConfigurationProperties注解
java·spring boot·后端
java1234_小锋1 小时前
讲讲Mybatis的一级、二级缓存?
java·开发语言·mybatis
e***87701 小时前
记录 idea 启动 tomcat 控制台输出乱码问题解决
java·tomcat·intellij-idea
发现你走远了1 小时前
2025 idea 指定配置环境运行springboot 设置active和env启动端口,多端口启动 (保姆级图文)
java·spring boot·intellij-idea
sanggou1 小时前
Java秒杀系统设计与实现
java
情怀姑娘2 小时前
面试题---------------场景+算法
java·算法·mybatis
客梦2 小时前
Java 学生管理系统
java·笔记
e***0962 小时前
SpringBoot下获取resources目录下文件的常用方法
java·spring boot·后端
q***14642 小时前
JavaWeb项目打包、部署至Tomcat并启动的全程指南(图文详解)
java·tomcat