查券返利机器人的异步任务调度:Java XXL-Job+Redis实现海量查券请求的分布式任务分发

查券返利机器人的异步任务调度:Java XXL-Job+Redis实现海量查券请求的分布式任务分发

大家好,我是 微赚淘客系统3.0 的研发者省赚客!

在高并发场景下,用户通过查券返利机器人发起的优惠券查询请求可能瞬时达到数十万量级。为避免直接冲击核心接口并保障系统稳定性,我们采用"请求入队 + 异步消费"模式,基于 XXL-JOB 作为分布式调度中心,结合 Redis Stream 实现任务的可靠分发与削峰填谷。

整体架构设计

用户请求首先写入 Redis Stream,由多个消费者实例监听流数据;XXL-JOB 定时触发任务拉取器,动态调整消费速率,并支持失败重试、积压告警与人工干预。该方案解耦了请求入口与处理逻辑,提升系统弹性。

Redis Stream 任务队列定义

每个查券请求封装为一个结构化消息,写入名为 coupon:query:stream 的 Stream:

java 复制代码
package juwatech.cn.task.model;

public class CouponQueryTask {
    private String taskId;      // 全局唯一ID
    private String userId;      // 用户ID
    private String itemId;      // 商品ID
    private long timestamp;     // 请求时间戳
    private int retryCount;     // 重试次数

    // getters and setters
    public String getTaskId() { return taskId; }
    public void setTaskId(String taskId) { this.taskId = taskId; }
    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
    public String getItemId() { return itemId; }
    public void setItemId(String itemId) { this.itemId = itemId; }
    public long getTimestamp() { return timestamp; }
    public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
    public int getRetryCount() { return retryCount; }
    public void setRetryCount(int retryCount) { this.retryCount = retryCount; }
}

生产者将任务推入 Redis:

java 复制代码
package juwatech.cn.task.producer;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class CouponTaskProducer {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public CouponTaskProducer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void submitTask(juwatech.cn.task.model.CouponQueryTask task) throws Exception {
        Map<String, String> payload = new HashMap<>();
        payload.put("task", objectMapper.writeValueAsString(task));

        redisTemplate.opsForStream()
            .add(StreamRecords.newRecord()
                .ofObject(payload)
                .withStreamKey("coupon:query:stream"));
    }
}

XXL-JOB 调度任务配置

在 XXL-JOB 控制台注册执行器 juwatech-coupon-job,并创建任务 coupon-query-consumer,Cron 表达式设为 0/5 * * * * ?(每5秒触发一次)。

对应的 JobHandler 实现如下:

java 复制代码
package juwatech.cn.job;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;

@Component
public class CouponQueryConsumerJob {

    private final juwatech.cn.task.consumer.CouponTaskConsumer consumer;

    public CouponQueryConsumerJob(juwatech.cn.task.consumer.CouponTaskConsumer consumer) {
        this.consumer = consumer;
    }

    @XxlJob("couponQueryConsumer")
    public void execute() {
        try {
            int consumed = consumer.consumeBatch(100); // 每次最多消费100条
            XxlJobHelper.handleSuccess("Consumed " + consumed + " tasks");
        } catch (Exception e) {
            XxlJobHelper.handleFail(e.getMessage());
        }
    }
}

Redis Stream 消费逻辑

消费者从 Stream 读取待处理任务,并调用查券服务:

java 复制代码
package juwatech.cn.task.consumer;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Service
public class CouponTaskConsumer {

    private final RedisTemplate<String, Object> redisTemplate;
    private final juwatech.cn.service.CouponQueryService couponQueryService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public CouponTaskConsumer(RedisTemplate<String, Object> redisTemplate,
                              juwatech.cn.service.CouponQueryService couponQueryService) {
        this.redisTemplate = redisTemplate;
        this.couponQueryService = couponQueryService;
    }

    public int consumeBatch(int batchSize) throws Exception {
        StreamReadOptions options = StreamReadOptions.empty()
                .count(batchSize)
                .block(Duration.ofSeconds(2)); // 阻塞等待新消息

        List<MapRecord<String, Object, Object>> records = redisTemplate.opsForStream()
                .read(options, StreamOffset.create("coupon:query:stream", ReadOffset.lastConsumed()));

        if (records == null || records.isEmpty()) {
            return 0;
        }

        for (MapRecord<String, Object, Object> record : records) {
            try {
                Map<Object, Object> value = record.getValue();
                String taskJson = (String) value.get("task");
                juwatech.cn.task.model.CouponQueryTask task = objectMapper.readValue(taskJson, juwatech.cn.task.model.CouponQueryTask.class);

                // 执行查券
                couponQueryService.queryAndNotify(task.getUserId(), task.getItemId());

                // 确认消费(删除消息)
                redisTemplate.opsForStream().acknowledge("coupon:query:stream", "coupon-group", record.getId());
            } catch (Exception e) {
                handleConsumeFailure(record, e);
            }
        }
        return records.size();
    }

    private void handleConsumeFailure(MapRecord<String, Object, Object> record, Exception e) {
        // 可选:将失败任务写入重试队列或 DLQ
        juwatech.cn.util.AsyncLogger.logAsync("Failed to process task " + record.getId() + ": " + e.getMessage());
    }
}

消费者组与消息可靠性

初始化消费者组以支持多实例并行消费:

bash 复制代码
XGROUP CREATE coupon:query:stream coupon-group $ MKSTREAM

在应用启动时自动创建(可选):

java 复制代码
package juwatech.cn.config;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class RedisStreamInit {

    private final RedisTemplate<String, String> redisTemplate;

    public RedisStreamInit(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @PostConstruct
    public void initConsumerGroup() {
        try {
            redisTemplate.execute((connection) -> {
                connection.streamCommands().xGroupCreate(
                    "coupon:query:stream".getBytes(),
                    "coupon-group".getBytes(),
                    org.springframework.data.redis.connection.stream.ReadOffset.from("0-0").getOffset().getBytes(),
                    true
                );
                return null;
            });
        } catch (Exception ignored) {
            // Group already exists
        }
    }
}

积压监控与弹性扩缩容

通过 Redis 命令 XINFO GROUPS coupon:query:stream 获取 pending 消息数,在 XXL-JOB 中上报指标,结合 Prometheus + Alertmanager 实现积压告警。当 pending > 10000 时,自动扩容消费者实例。

本文著作权归 微赚淘客系统3.0 研发团队,转载请注明出处!

相关推荐
Mr_Xuhhh2 小时前
C语言字符串与内存操作函数模拟实现详解
java·linux·算法
瑞雪兆丰年兮2 小时前
[从0开始学Java|第十一天]ArrayList
java·开发语言
夜郎king2 小时前
基于 Java 实现数九天精准计算:从节气算法到工程化落地
java·开发语言
新缸中之脑2 小时前
Nanobot:轻量级OpenClaw
java·运维·网络
悟能不能悟2 小时前
java.sql.SQLSyntaxErrorException: ORA-01031: insufficient privileges
java·开发语言
马猴烧酒.2 小时前
【DDD重构|第十三天】DDD 领域驱动设计详解+实战
java·jvm·ide·重构·tomcat·maven·团队开发
烧烧的酒0.o2 小时前
Java——JavaSE完整教程
java·开发语言·学习
鹏哥哥啊Aaaa2 小时前
15.idea启动报错
java·ide·intellij-idea
super_lzb2 小时前
VUE 请求代理地址localhost报错[HPM] Error occurred while trying to proxy request
java·spring·vue·springboot·vue报错