查券返利机器人的异步任务调度: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 研发团队,转载请注明出处!