文章目录
问题描述
项目中遇到一个情况,执行批量扫描任务,并使用内存队列对任务进行排队。接口请求时采用异步,快速响应接口,后台排队执行任务。但是,在部署到服务器后,只要升级服务,没跑完的任务就会丢失。所以需要对内存队列进行优化,想到了两个方案:
- Redis队列
- 消息队列
如何选型
考虑业务的批量任务只是一次发起后就可以排队处理,不会源源不断的产生新的任务,且任务失败大概率是代码逻辑问题,不是网络波动,再次跑大概率还是失败,所以不需要重试机制。且使用消息队列的三个重要特性是:限流削峰、异步、解耦,但是使用需要维护三方:生产、存储、消费。而使用Redis只需要维护一方,且使用内存速度快。综上,该业务场景使用消息队列维护成本相对较高,意义不大。
如何实现
原代码
java
@Slf4j
@Component
public class Scheduler {
private final ConcurrentLinkedQueue<Long> pendingQueue = new ConcurrentLinkedQueue<>();
public void batchTriggerSubtasks(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
log.warn("任务ID列表为空,跳过处理");
return;
}
pendingQueue.addAll(ids);
}
@Scheduled(cron = "0/10 * * * * ?")
public void processPendingSubtasks() {
// 从队列中取出要处理的子任务数量
int tasksToProcess = Math.min(remainingCapacity, pendingQueue.size());
// ...
int processedCount = 0;
while (processedCount < tasksToProcess && !pendingQueue.isEmpty()) {
Long subtaskId = pendingQueue.poll();
if (subtaskId != null) {
try {
//具体业务...
} catch (Exception e) {
log.error("触发任务失败,ID: {}, 错误: {}", subtaskId, e.getMessage(), e);
// 失败的任务重新加入队列末尾,以便稍后重试
pendingQueue.offer(subtaskId);
}
}
}
}
}
改进后
java
@Slf4j
@Component
public class Scheduler {
private static final String PENDING_QUEUE = "queue:pending";
@Resource
private RedisUtils redisUtils;
public void batchTriggerSubtasks(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
log.warn("任务ID列表为空,跳过处理");
return;
}
redisUtils.addAll(PENDING_QUEUE, ids);
}
@Scheduled(cron = "0/10 * * * * ?")
public void processPendingTasks() {
// 从队列中取出要处理的子任务数量
int tasksToProcess = (int) Math.min(remainingCapacity, redisUtils.size(PENDING_QUEUE));
// ...
int processedCount = 0;
while (processedCount < tasksToProcess && redisUtils.size(PENDING_QUEUE) != 0) {
Long id = redisUtils.poll(PENDING_QUEUE);
if (id != null) {
try {
//具体业务...
} catch (Exception e) {
log.error("触发任务失败,ID: {}, 错误: {}", id, e.getMessage(), e);
// 失败的任务重新加入队列末尾,以便稍后重试
redisUtils.offer(PENDING_QUEUE, id);
}
}
}
}
}
java
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* redis工具类.
*/
@Component
public class RedisUtils {
private boolean keyExisted = true;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
public RedisUtils() {
}
public void setVal(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void setValWithExpireTime(String key, Object value, long expireTime, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, expireTime, timeUnit == null
? TimeUnit.SECONDS : timeUnit);
}
public Object getVal(String key) {
return redisTemplate.opsForValue().get(key);
}
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
public boolean tryLock(String key, long expireTime) {
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(key, "", expireTime, TimeUnit.SECONDS));
}
public void delByKey(String key) {
redisTemplate.delete(key);
}
public String getRedisStrValue(String key) {
return (String) this.stringRedisTemplate.opsForValue().get(key);
}
public void setRedisStrSecondValue(String key, String value, long senconds) {
this.stringRedisTemplate.opsForValue().set(key, value, senconds, TimeUnit.SECONDS);
}
public boolean expire(String key, long time) {
try {
if (time > 0L) {
this.redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception var5) {
var5.printStackTrace();
return false;
}
}
/**
* 批量添加任务到指定队列.
*/
public <T> void addAll(String queueKey, Collection<T> items) {
if (items != null && !items.isEmpty()) {
redisTemplate.opsForList().rightPushAll(queueKey, items.toArray());
}
}
/**
* 获取队列大小.
*
* @param queueKey 队列名
* @return 队列大小.
*/
public long size(String queueKey) {
Long size = redisTemplate.opsForList().size(queueKey);
return size != null ? size : 0L;
}
/**
* 从队列头部取出任务.
*/
@SuppressWarnings("unchecked")
public <T> T poll(String queueKey) {
return (T) redisTemplate.opsForList().leftPop(queueKey);
}
/**
* 从队列头部取出任务,带超时时间.
*/
@SuppressWarnings("unchecked")
public <T> T poll(String queueKey, long timeout, TimeUnit timeUnit) {
return (T) redisTemplate.opsForList().leftPop(queueKey, timeout, timeUnit);
}
/**
* 添加单个任务到队列.
*/
public <T> void offer(String queueKey, T item) {
if (item != null) {
redisTemplate.opsForList().rightPush(queueKey, item);
}
}
/**
* 批量取出指定数量的任务.
*/
@SuppressWarnings("unchecked")
public <T> List<T> pollBatch(String queueKey, int batchSize) {
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) {
for (int i = 0; i < batchSize; i++) {
operations.opsForList().leftPop(queueKey);
}
return null;
}
});
return results.stream()
.filter(obj -> obj != null)
.map(obj -> (T) obj)
.collect(Collectors.toList());
}
/**
* 查看队列头部的任务但不移除.
*/
@SuppressWarnings("unchecked")
public <T> T peek(String queueKey) {
return (T) redisTemplate.opsForList().index(queueKey, 0);
}
/**
* 清空指定队列.
*/
public void clear(String queueKey) {
redisTemplate.delete(queueKey);
}
/**
* 检查队列是否存在.
*/
public boolean exists(String queueKey) {
Boolean exists = redisTemplate.hasKey(queueKey);
return exists != null && exists;
}
}
以上为个人学习分享,如有问题,欢迎指出:)