SpringBoot文章定时发布的技术方案详解(三种延迟任务方案)

 更新时间:2025年12月17日 10:00:27   作者:IT 刘工  
本文介绍了三种实现文章定时发布在SpringBoot项目中的技术方案:JDKDelayQueue、基于RabbitMQ延迟队列以及基于Redis有序集合,每种方案都有其特点和适用场景,感兴趣的朋友跟随小编一起看看吧

在现代内容管理系统中,文章定时发布是一个常见需求。

它允许作者在特定时间自动发布文章,而不需要手动操作。

在SpringBoot项目中,实现这种定时功能有多种技术方案,每种都有其适用场景和特点。

本文将详细介绍三种实现文章定时发布的技术方案:

基于JDK DelayQueue、基于RabbitMQ延迟队列,以及基于Redis有序集合。

我会为每种方案提供核心代码实现,并深入分析它们的优劣,帮助你在实际项目中做出合适的技术选型。

方案一:JDK DelayQueue 实现

实现原理

DelayQueue是JDK提供的一个无界阻塞队列,其中的元素只有在指定的延迟时间到达后才能被取出。这个特性使其天然适合实现延迟任务。

核心代码实现

1. 定义延迟任务对象

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class ArticleTask implements Delayed {
    private final long executeTime; // 执行时间戳
    private final Article article; // 文章内容
    public ArticleTask(Article article, long delay) {
        this.article = article;
        this.executeTime = System.currentTimeMillis() + delay;
    }
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.executeTime, ((ArticleTask) o).executeTime);
    }
    // Getter方法
    public Article getArticle() {
        return article;
    }
}

2. 文章实体类

public class Article {
    private Long id;
    private String title;
    private String content;
    private Integer status; // 状态:0-草稿,1-已发布
    private Date publishTime; // 预定发布时间
    // 省略getter/setter
}

3. 延迟队列服务

@Component
public class DelayQueueService {
    private final DelayQueue<ArticleTask> queue = new DelayQueue<>();
    @PostConstruct
    public void init() {
        // 启动消费线程
        new Thread(this::consume).start();
    }
    /**
     * 添加定时发布任务
     * @param article 文章
     * @param delay 延迟时间(毫秒)
     */
    public void addTask(Article article, long delay) {
        queue.put(new ArticleTask(article, delay));
    }
    /**
     * 消费延迟队列中的任务
     */
    private void consume() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                ArticleTask task = queue.take(); // 阻塞直到有任务到期
                publishArticle(task.getArticle());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    /**
     * 发布文章
     */
    private void publishArticle(Article article) {
        // 更新文章状态为已发布
        article.setStatus(1);
        article.setPublishTime(new Date());
        System.out.println("发布时间:" + new Date() + ",发布文章: " + article.getTitle());
        // 这里可以加入实际的数据库更新逻辑
        // articleService.updateById(article);
    }
}

4. 控制器示例

@RestController
@RequestMapping("/article")
public class ArticleController {
    @Autowired
    private DelayQueueService delayQueueService;
    @PostMapping("/schedule")
    public String schedulePublish(@RequestBody Article article, 
                                 @RequestParam long delayMillis) {
        delayQueueService.addTask(article, delayMillis);
        return "文章已安排定时发布";
    }
}

优劣分析

优点:

  • 实现简单,不依赖外部组件
  • 延迟精度高,任务到期立即执行
  • 性能较好,纯内存操作

缺点:

  • 任务存储在内存,应用重启会丢失
  • 不支持分布式集群部署
  • 任务过多时容易导致内存溢出
  • 缺乏持久化机制和重试机制

方案二:RabbitMQ 延迟队列

实现原理

RabbitMQ可以通过死信队列(Dead Letter Exchange)实现延迟任务。当消息在队列中存活时间超过设定的TTL(Time To Live)后,会被转发到死信交换机,进而路由到死信队列供消费者处理。

核心代码实现

1. RabbitMQ配置类

@Configuration
public class RabbitMQConfig {
    // 定义死信交换机和队列
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange("dead.letter.exchange");
    }
    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable("dead.letter.queue").build();
    }
    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(deadLetterExchange())
                .with("dead.letter");
    }
    // 定义实际业务队列,并绑定死信交换机
    @Bean
    public Queue articleQueue() {
        return QueueBuilder.durable("article.delay.queue")
                .withArgument("x-dead-letter-exchange", "dead.letter.exchange")
                .withArgument("x-dead-letter-routing-key", "dead.letter")
                .build();
    }
}

2. 消息发送服务

@Service
public class ArticleMQService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 安排文章定时发布
     * @param article 文章
     * @param delayMillis 延迟时间(毫秒)
     */
    public void scheduleArticle(Article article, long delayMillis) {
        rabbitTemplate.convertAndSend("", "article.delay.queue", article, message -> {
            // 设置消息的过期时间
            message.getMessageProperties().setExpiration(String.valueOf(delayMillis));
            return message;
        });
        System.out.println("消息已发送,将在 " + delayMillis + " 毫秒后过期");
    }
}

3. 消息消费者

@Component
public class ArticleDelayListener {
    @RabbitListener(queues = "dead.letter.queue")
    public void processDelayedArticle(Article article) {
        // 此时文章已到预定发布时间,执行发布逻辑
        article.setStatus(1);
        article.setPublishTime(new Date());
        System.out.println("发布时间:" + new Date() + ",发布文章: " + article.getTitle());
        // 实际业务中,这里应该更新数据库
        // articleService.updateById(article);
    }
}

4. 控制器示例

@RestController
@RequestMapping("/article")
public class ArticleController {
    @Autowired
    private ArticleMQService articleMQService;
    @PostMapping("/schedule")
    public String schedulePublish(@RequestBody Article article, 
                                 @RequestParam long delayMillis) {
        articleMQService.scheduleArticle(article, delayMillis);
        return "文章已安排定时发布";
    }
}

优劣分析

优点:

  • 任务持久化,服务重启不会丢失
  • 支持分布式集群部署
  • 提供完善的ACK机制和重试机制
  • 具备良好的可靠性和可用性

缺点:

  • 需要额外维护RabbitMQ中间件
  • 基于死信队列的方式存在"队头阻塞"问题
  • 配置相对复杂
  • TTL设置在消息级别时,后到期的消息可能阻塞先到期的消息

提示:RabbitMQ 3.8+版本提供了官方的延迟消息插件(rabbitmq_delayed_message_exchange),可以避免死信队列的队头阻塞问题。

方案三:Redis 延迟任务

实现原理

Redis可以通过有序集合(ZSet)实现延迟任务。将任务执行时间作为score,定期扫描已到期的任务进行处理。

核心代码实现

1. Redis服务类

@Service
public class RedisDelayService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String DELAY_KEY = "article:delay:tasks";
    // 使用Jackson进行序列化
    private final ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 添加延迟任务
     */
    public void addArticleTask(Article article, long delaySeconds) {
        long executeTime = System.currentTimeMillis() + (delaySeconds * 1000);
        try {
            String articleJson = objectMapper.writeValueAsString(article);
            redisTemplate.opsForZSet().add(DELAY_KEY, articleJson, executeTime);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("文章序列化失败", e);
        }
    }
    /**
     * 获取到期的任务
     */
    public Set<String> getExpiredTasks(long maxScore) {
        return redisTemplate.opsForZSet().rangeByScore(DELAY_KEY, 0, maxScore);
    }
    /**
     * 移除已处理的任务
     */
    public void removeTask(String task) {
        redisTemplate.opsForZSet().remove(DELAY_KEY, task);
    }
}

2. 任务扫描器

@Component
public class RedisTaskScanner {
    @Autowired
    private RedisDelayService redisDelayService;
    private static final String LOCK_KEY = "article:delay:lock";
    private static final long LOCK_EXPIRE = 30; // 锁过期时间30秒
    /**
     * 每秒扫描一次到期任务
     */
    @Scheduled(fixedRate = 1000)
    public void scanExpiredTasks() {
        // 使用Redis分布式锁,防止集群环境下重复消费
        boolean locked = tryGetDistributedLock();
        if (!locked) {
            return;
        }
        try {
            long maxScore = System.currentTimeMillis();
            Set<String> expiredTasks = redisDelayService.getExpiredTasks(maxScore);
            if (expiredTasks != null && !expiredTasks.isEmpty()) {
                for (String taskJson : expiredTasks) {
                    processTask(taskJson);
                    // 从集合中移除已处理的任务
                    redisDelayService.removeTask(taskJson);
                }
            }
        } finally {
            // 释放锁
            releaseDistributedLock();
        }
    }
    private void processTask(String taskJson) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            Article article = objectMapper.readValue(taskJson, Article.class);
            // 执行发布逻辑
            article.setStatus(1);
            article.setPublishTime(new Date());
            System.out.println("发布时间:" + new Date() + ",发布文章: " + article.getTitle());
            // 实际业务中更新数据库
            // articleService.updateById(article);
        } catch (Exception e) {
            System.err.println("处理任务失败: " + e.getMessage());
            // 可以加入重试机制或死信队列
        }
    }
    private boolean tryGetDistributedLock() {
        // 简化的分布式锁实现,生产环境建议使用Redisson等成熟方案
        // 这里只是示意,实际实现需要考虑原子性等问题
        return true;
    }
    private void releaseDistributedLock() {
        // 释放锁的实现
    }
}

3. 控制器示例

@RestController
@RequestMapping("/article")
public class ArticleController {
    @Autowired
    private RedisDelayService redisDelayService;
    @PostMapping("/schedule")
    public String schedulePublish(@RequestBody Article article, 
                                 @RequestParam long delaySeconds) {
        redisDelayService.addArticleTask(article, delaySeconds);
        return "文章已安排定时发布";
    }
}

4. 启动类配置

@SpringBootApplication
@EnableScheduling // 开启定时任务支持
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

优劣分析

优点:

  • 性能优秀,支持高并发场景
  • 支持分布式集群部署
  • 数据可持久化,重启不会丢失
  • 灵活性高,可以方便地调整扫描频率

缺点:

  • 存在时间误差,取决于轮询间隔
  • 需要自行处理并发消费问题
  • CPU资源消耗相对较高
  • 需要维护Redis中间件

方案对比与选型建议

综合对比

特性维度

JDK DelayQueue

RabbitMQ

Redis

准时性

高 ⭐⭐⭐⭐⭐

高 ⭐⭐⭐⭐⭐

中高 ⭐⭐⭐⭐

可靠性

低 ⭐⭐

高 ⭐⭐⭐⭐⭐

中高 ⭐⭐⭐⭐

集群支持

不支持 ⭐

原生支持 ⭐⭐⭐⭐⭐

原生支持 ⭐⭐⭐⭐⭐

实现复杂度

低 ⭐⭐⭐⭐⭐

中 ⭐⭐⭐

中 ⭐⭐⭐

资源开销

内存消耗大

中间件维护

CPU/网络开销

持久化

不支持

支持

支持

扩展性

选型建议

1. 单体轻量级应用
如果您的应用是单体架构,任务量不大,且可以接受应用重启时任务丢失,推荐使用 JDK DelayQueue。它的实现最简单,不依赖外部组件。

2. 分布式高可靠场景
如果您的应用是微服务架构,需要高可靠性,并且已经使用了RabbitMQ,推荐使用 RabbitMQ 延迟队列。特别是对于电商、金融等对可靠性要求高的场景。

3. 高性能已有Redis
如果您的项目已经使用Redis,且追求高性能和高灵活性,推荐使用 Redis 有序集合方案。特别适合任务量大、并发高的场景。

4. 简单稳定的备选方案
除了上述三种方案,对于文章定时发布这种允许少量延迟的场景,还可以考虑使用 Spring Schedule + 数据库查询的方案:

@Component
public class DatabaseScheduler {
    @Autowired
    private ArticleService articleService;
    // 每分钟执行一次
    @Scheduled(fixedRate = 60000)
    public void publishScheduledArticles() {
        List<Article> articles = articleService.findScheduledArticles(new Date());
        for (Article article : articles) {
            article.setStatus(1);
            articleService.update(article);
            System.out.println("发布文章: " + article.getTitle());
        }
    }
}

这种方案的优点是实现简单、稳定可靠,缺点是实时性较差且有数据库压力。

生产环境注意事项

  1. 监控与告警:无论选择哪种方案,都需要建立完善的监控体系,确保延迟任务正常执行
  2. 任务去重:在分布式环境下要确保任务不会被重复消费
  3. 失败重试:实现合理的重试机制,处理任务执行失败的情况
  4. 数据备份:定期备份任务数据,防止数据丢失
  5. 性能测试:在生产环境上线前进行充分的压力测试

总结

文章定时发布功能虽然看似简单,但在技术选型时需要综合考虑业务需求、系统架构和运维成本。JDK DelayQueue适合简单场景,RabbitMQ适合高可靠性要求,Redis则在性能和灵活性上表现优异。希望本文的分析和代码示例能够帮助你在实际项目中做出合适的技术决策。

选择合适的技术方案,既要满足当前需求,也要为未来的扩展留出空间。在实际项目中,建议根据具体的业务规模、团队技术栈和运维能力来做出最终决定。

到此这篇关于SpringBoot文章定时发布的技术方案详解(三种延迟任务方案)的文章就介绍到这了,更多相关springboot 定时发布内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring中@Value使用详解及SPEL表达式

    Spring中@Value使用详解及SPEL表达式

    这篇文章主要介绍了Spring中@Value使用详解及SPEL表达式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • Java使用 try-with-resources 实现自动关闭资源的方法

    Java使用 try-with-resources 实现自动关闭资源的方法

    这篇文章主要介绍了Java使用 try-with-resources 实现自动关闭资源的方法,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • Spring的@Lazy懒加载注解用法详细解析

    Spring的@Lazy懒加载注解用法详细解析

    这篇文章主要介绍了Spring的@Lazy懒加载注解用法详细解析,SpringIoC容器会在启动的时候实例化所有单实例 bean ,如果我们想要实现 Spring 在启动的时候延迟加载 bean,即在首次调用bean的时候再去执行初始化,就可以使用 @Lazy 注解来解决这个问题,需要的朋友可以参考下
    2023-11-11
  • Java中的BigDecimal原理详解

    Java中的BigDecimal原理详解

    这篇文章主要介绍了Java中的BigDecimal原理详解,对于日常开发过程中出现小数的问题,通常都是使用float或者double类型来处理,在java中float占用四个字节, double类型占用8个字节,需要的朋友可以参考下
    2023-09-09
  • springboot使用DynamicDataSource动态切换数据源的实现过程

    springboot使用DynamicDataSource动态切换数据源的实现过程

    这篇文章主要给大家介绍了关于springboot使用DynamicDataSource动态切换数据源的实现过程,Spring Boot应用中可以配置多个数据源,并根据注解灵活指定当前使用的数据源,需要的朋友可以参考下
    2023-08-08
  • Hadoop的安装与环境搭建教程图解

    Hadoop的安装与环境搭建教程图解

    这篇文章主要介绍了Hadoop的安装与环境搭建教程图解,本文图文并茂给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-06-06
  • Spring MVC获取HTTP请求头的两种方式小结

    Spring MVC获取HTTP请求头的两种方式小结

    这篇文章主要介绍了Spring MVC获取HTTP请求头的两种方式小结,帮助大家更好的理解和使用Spring MVC,感兴趣的朋友可以了解下
    2021-01-01
  • java对ArrayList中元素进行排序的几种方式总结

    java对ArrayList中元素进行排序的几种方式总结

    在Java中,ArrayList类提供了多种排序方法,可以根据不同的需求选择适合的排序方法,下面这篇文章主要给大家介绍了关于java对ArrayList中元素进行排序的几种方式,需要的朋友可以参考下
    2024-08-08
  • Java Mybatis框架由浅入深全解析中篇

    Java Mybatis框架由浅入深全解析中篇

    MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码本文将为大家深入的介绍一下MyBatis的使用
    2022-07-07
  • Springboot实现接口传输加解密的步骤详解

    Springboot实现接口传输加解密的步骤详解

    这篇文章主要给大家详细介绍了Springboot实现接口传输加解密的操作步骤,文中有详细的图文解释和代码示例供大家参考,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2023-09-09

最新评论