使用Redis队列优化内存队列

文章目录

问题描述

项目中遇到一个情况,执行批量扫描任务,并使用内存队列对任务进行排队。接口请求时采用异步,快速响应接口,后台排队执行任务。但是,在部署到服务器后,只要升级服务,没跑完的任务就会丢失。所以需要对内存队列进行优化,想到了两个方案:

  • 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;
  }

}

以上为个人学习分享,如有问题,欢迎指出:)

相关推荐
这个DBA有点耶2 分钟前
从INDEX到JOIN_ORDER:MySQL 8.0 Hint语法体系与五类实战场景
数据库·mysql·架构
数字新视界6 分钟前
机房动环监控助力智能管理
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
梅菲塔亚10 分钟前
博客_Redis入门到实战
数据库·redis·缓存
abcyuu15 分钟前
用友 U8 Cloud 生产制造模块深度解析:从 BOM 建立到成本核算的技术实现
前端·数据库·制造
夜雪一千16 分钟前
MySQL 的ID用什么数据类型更好?主键ID选型实战
数据库·mysql
蓝胖的四次元口袋23 分钟前
Redis 知识梳理(3)
数据库·redis·缓存
一铭1119932 分钟前
海豹办公 1.9.15 跨端办公软件实践 文档兼容 Access 数据库与移动 WebView 排错
android·数据库·electron·开源软件·access
Lynne30944 分钟前
TSN 在变电站自动化通信网络升级中的应用价值与技术挑战
服务器·网络·数据库
弈栈录1 小时前
用户、会话与消息表应该如何设计
数据库·后端
内存漫游1 小时前
Checkpoint 与 Crash Recovery:数据库恢复机制入门
数据库