惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
WordPress大学
WordPress大学
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
博客园 - Franky
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
Jina AI
Jina AI
Last Week in AI
Last Week in AI
雷峰网
雷峰网
IT之家
IT之家
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
定时消息任务触发了两次
Aimee · 2026-08-04 · via Aimee's Blog

定时消息任务触发了两次

用户反馈说收到两条一模一样的活动提醒,发送时间相差不到 1 秒。

看日志,这条定时任务的执行记录出现了两次,两次都显示"执行成功"。

不是 MQ 重投的问题,定时任务根本没走 MQ——是调度器直接触发的。


分布式调度器的多节点问题

单机调度器(比如 Spring @Scheduled)不存在这个问题,因为只有一个进程。

但服务部署了 3 个节点,三个节点同时跑 @Scheduled,同一时刻三个节点都会触发同一个任务——没有协调机制,谁都不知道别人也触发了。

这是分布式调度最基础的问题:多节点竞争同一个任务


分布式锁抢任务

最简单的解法:每次触发时先抢分布式锁,抢到了才执行:

@Scheduled(cron = "0 0 10 * * ?")
public void sendActivityReminder() {
    String lockKey = "scheduled:activity-reminder:" + LocalDate.now();
    boolean locked = redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS);
    if (!locked) {
        log.info("Another node is handling this task, skip");
        return;
    }
    try {
        doSendActivityReminder();
    } finally {
        redisLock.unlock(lockKey);
    }
}

锁的 key 里带日期,每天一把新锁,不会因为昨天的锁没过期影响今天。

TTL 设成任务执行时间的 2 倍左右,防止执行节点宕机后锁永远不释放导致后续触发也无法执行。


更可靠的方案:专业分布式调度框架

分布式锁能解决重复触发,但还有其他问题:哪个节点执行了、执行多久了、失败了怎么重试、任务执行历史在哪里查?

专业的分布式调度框架(XXL-Job、ElasticJob、SchedulerX)从调度层面解决这些问题:

  • 任务由调度中心统一下发给一个执行节点,不是所有节点同时触发
  • 执行节点上报执行状态
  • 失败自动重试,重试次数可配置
  • 可视化任务监控和日志

如果系统里已经有这类框架,优先用它,不要自己在 Spring 里加分布式锁。


任务版本号:防止旧任务覆盖新状态

有时候问题不是两次触发,而是:任务 A 触发后执行很慢,同时任务 B(相同类型,新的一次触发)也开始执行。A 执行完更新数据时,B 可能已经把状态推进得更远了——A 的更新反而把状态回退了。

防御方式是乐观锁版本号:

UPDATE scheduled_task
SET status = 'DONE', version = version + 1
WHERE id = ? AND version = ?

如果版本号不符,说明中间有其他任务修改过,本次更新忽略。


exactly-once 的定时任务执行,核心原则:调度层保证只触发一次(分布式锁或调度框架),执行层保证幂等(版本号或状态检查)。两层都做,才能真正避免重复执行的影响。