<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>可靠任务 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%8F%AF%E9%9D%A0%E4%BB%BB%E5%8A%A1/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 04 Aug 2025 17:10:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%8F%AF%E9%9D%A0%E4%BB%BB%E5%8A%A1/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>我开发的KE Reliable Executor：基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架</title>
      <link>https://ftmao.com/blog/posts/ke-reliable-executor/</link>
      <pubDate>Mon, 04 Aug 2025 17:10:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ke-reliable-executor/</guid>
      <description>&lt;h1 id=&#34;我开发的ke-reliable-executor基于-redismysql-的-spring-boot-可靠任务执行框架&#34;&gt;我开发的KE Reliable Executor：基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架&lt;/h1&gt;
&lt;p&gt;在微服务里，异步任务随处可见：支付后通知第三方、订单超时取消、批量对账、报表生成……这些任务有一个共同诉求——&lt;strong&gt;不能丢、不能重复执行、失败能重试、状态要能查&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统做法是直接引入 Kafka、RocketMQ 这类重型消息中间件。但对于很多中小型业务来说，这未免太重：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要额外维护 Broker、Topic、消费组；&lt;/li&gt;
&lt;li&gt;团队要熟悉一套新的运维和监控体系；&lt;/li&gt;
&lt;li&gt;为了“可靠”还要配置死信队列、事务消息等高级特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你的系统里已经有 Redis（或 MySQL），并且任务量级尚未达到 MQ 的吞吐上限，那么完全可以基于 Redis 搭一个&lt;strong&gt;轻量且可靠的异步任务执行框架&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我开发是这样一个框架：&lt;strong&gt;KE Reliable Executor&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/reliableexecutor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要这个工具&#34;&gt;一、为什么需要这个工具？&lt;/h2&gt;
&lt;p&gt;先回顾一下常见异步任务场景里的痛点：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;痛点&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;任务丢失&lt;/td&gt;
					&lt;td&gt;服务重启或执行线程异常退出后，未执行的任务找不到了&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重复执行&lt;/td&gt;
					&lt;td&gt;多实例部署时，同一个任务被多个节点同时消费&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;失败即结束&lt;/td&gt;
					&lt;td&gt;业务异常或网络抖动后，没有自动重试机制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;状态黑盒&lt;/td&gt;
					&lt;td&gt;无法查询任务当前是成功、失败、执行中还是已取消&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接入成本高&lt;/td&gt;
					&lt;td&gt;引入 MQ 需要改代码、加依赖、搭集群&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;KE Reliable Executor 要解决的就是这些问题。它把“可靠任务执行”抽象成一个 Spring Boot Starter：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务方只负责&lt;strong&gt;定义任务处理器&lt;/strong&gt;和&lt;strong&gt;提交任务&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;框架负责&lt;strong&gt;存储、调度、加锁、重试、状态流转、清理&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;所有状态都可通过 API 查询，也可通过 Micrometer 指标监控。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;适合什么场景&#34;&gt;适合什么场景？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;已有 Redis/MySQL，不想额外维护 MQ；&lt;/li&gt;
&lt;li&gt;任务量中等，对可靠性和重试有要求；&lt;/li&gt;
&lt;li&gt;需要任务状态可查询、可人工介入；&lt;/li&gt;
&lt;li&gt;团队希望以最小成本获得“类 MQ”的可靠执行能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;不适合什么场景&#34;&gt;不适合什么场景？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;海量消息、高吞吐（Kafka/RocketMQ 更合适）；&lt;/li&gt;
&lt;li&gt;严格顺序消费（ZSET 按时间排序，不保证业务绝对顺序）；&lt;/li&gt;
&lt;li&gt;需要复杂路由、死信队列、事务消息等高级 MQ 特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二与竞品的对比&#34;&gt;二、与竞品的对比&lt;/h2&gt;
&lt;h3 id=&#34;21-与-kafka--rocketmq-对比&#34;&gt;2.1 与 Kafka / RocketMQ 对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Kafka / RocketMQ&lt;/th&gt;
					&lt;th&gt;KE Reliable Executor&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;基础设施&lt;/td&gt;
					&lt;td&gt;需要独立 Broker 集群&lt;/td&gt;
					&lt;td&gt;复用 Redis 或 MySQL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;运维成本&lt;/td&gt;
					&lt;td&gt;高（Topic、消费组、监控）&lt;/td&gt;
					&lt;td&gt;低（Spring Boot Starter）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接入成本&lt;/td&gt;
					&lt;td&gt;需要学习 SDK 与语义&lt;/td&gt;
					&lt;td&gt;注解 + 注入接口即可&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;吞吐量&lt;/td&gt;
					&lt;td&gt;极高&lt;/td&gt;
					&lt;td&gt;中等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;状态查询&lt;/td&gt;
					&lt;td&gt;需自建审计表&lt;/td&gt;
					&lt;td&gt;内置状态查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重试机制&lt;/td&gt;
					&lt;td&gt;需配置消费重试&lt;/td&gt;
					&lt;td&gt;内置可配置重试&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用阶段&lt;/td&gt;
					&lt;td&gt;大型业务、高并发&lt;/td&gt;
					&lt;td&gt;中小型业务、快速落地&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论：KE Reliable Executor 不是取代 MQ，而是&lt;strong&gt;在“已有 Redis/MySQL、任务量可控”的场景下，提供一个更轻量的选择&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;22-与-spring-scheduled--数据库轮询对比&#34;&gt;2.2 与 Spring &lt;code&gt;@Scheduled&lt;/code&gt; + 数据库轮询对比&lt;/h3&gt;
&lt;p&gt;很多团队最早会用数据库表 + &lt;code&gt;@Scheduled&lt;/code&gt; 轮询来实现异步任务。这种做法的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无分布式锁&lt;/strong&gt;：多实例同时轮询会重复执行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无状态机&lt;/strong&gt;：任务状态全靠字段控制，容易混乱；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无统一重试策略&lt;/strong&gt;：重试间隔、重试次数需要手写；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无优雅关闭&lt;/strong&gt;：应用关闭时可能丢失正在执行的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KE Reliable Executor 把这些都封装好了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Master 选举 + 任务级锁保证不重复；&lt;/li&gt;
&lt;li&gt;明确的状态机（CREATED → RUNNING → PENDING/COMPLETED/FAILED）；&lt;/li&gt;
&lt;li&gt;可配置的重试间隔与次数；&lt;/li&gt;
&lt;li&gt;优雅释放锁与线程池关闭。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-与-xxl-job--powerjob-对比&#34;&gt;2.3 与 XXL-JOB / PowerJob 对比&lt;/h3&gt;
&lt;p&gt;XXL-JOB 和 PowerJob 是优秀的分布式任务调度平台，但它们更适合&lt;strong&gt;定时任务&lt;/strong&gt;和&lt;strong&gt;调度中心&lt;/strong&gt;模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要单独部署调度中心；&lt;/li&gt;
&lt;li&gt;适合 Cron 表达式触发的批处理任务；&lt;/li&gt;
&lt;li&gt;任务由业务主动提交的场景下，接入不够自然。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KE Reliable Executor 更像一个&lt;strong&gt;嵌入式任务队列&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无额外服务，随业务应用启动；&lt;/li&gt;
&lt;li&gt;任务由业务代码主动提交；&lt;/li&gt;
&lt;li&gt;强调“提交即可靠执行”，而非定时触发。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三整体架构&#34;&gt;三、整体架构&lt;/h2&gt;
&lt;h3 id=&#34;31-模块解构图&#34;&gt;3.1 模块解构图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[业务服务] --&amp;gt;|submitTask| B(ReliableExecutor API)
    B --&amp;gt; C[TaskScheduler]
    C --&amp;gt; D{Master 锁}
    D --&amp;gt;|持有锁| E[扫描 ZSET 任务]
    E --&amp;gt; F[任务级锁]
    F --&amp;gt;|获取成功| G[提交线程池]
    G --&amp;gt; H[TaskExecutor]
    H --&amp;gt; I[&amp;#34;任务处理器 @ReliableTaskHandler&amp;#34;]
    I --&amp;gt;|成功| J[移入 FINISHED]
    I --&amp;gt;|失败| K[更新为 PENDING 等待重试]
    C --&amp;gt; L[TaskStorage]
    L --&amp;gt; M[RedisTaskStorage]
    L --&amp;gt; N[JdbcTaskStorage]
    H --&amp;gt; O[ReliableTaskListener]
    C --&amp;gt; P[ReliableExecutorMetrics]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-两层锁模型&#34;&gt;3.2 两层锁模型&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 全局层
        A[Master 锁] --&amp;gt;|仅一个实例| B[负责调度扫描]
    end
    subgraph 任务层
        C[&amp;#34;任务级锁 task-lock:{taskId}&amp;#34;] --&amp;gt;|仅一个线程| D[执行任务]
    end
    B --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Master 锁&lt;/strong&gt;：同一时刻只有一个实例负责扫描任务，避免多节点拉取同一批任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务级锁&lt;/strong&gt;：针对每个 CREATED 任务，提交执行前再抢一次锁，保证同一任务不会被并发执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-队列设计&#34;&gt;3.3 队列设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[提交任务] --&amp;gt;|add| B[Redis ZSET running-task-zset]
    B --&amp;gt;|ZRANGE 按 score| C[调度器]
    C --&amp;gt;|执行成功/失败/超时| D[Lua 原子转移]
    D --&amp;gt;|RPUSH + ZREM| E[Redis List finished-task-list]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;使用 ZSET 而非 List 的核心原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;天然支持时间调度&lt;/strong&gt;：score 编码创建时间或下次重试时间；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务唯一&lt;/strong&gt;：同一个 member 在队列中只有一份；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态分区&lt;/strong&gt;：CREATED 与 PENDING 使用不同 score 基数，一次范围查询即可区分。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Score 编码规则：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;score = BASE + epochMillis * 100 + retryCount
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CREATED 基数：1_000_000_000_000_000
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PENDING 基数：2_000_000_000_000_000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-任务状态机&#34;&gt;3.4 任务状态机&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; CREATED: submitTask
    CREATED --&amp;gt; RUNNING: 调度/立即执行
    RUNNING --&amp;gt; COMPLETED: 执行成功
    RUNNING --&amp;gt; PENDING: 执行失败，未超重试
    PENDING --&amp;gt; RUNNING: 到达重试时间
    RUNNING --&amp;gt; FAILED: 超过重试次数
    RUNNING --&amp;gt; TIMEOUT: 超过最大时长
    CREATED --&amp;gt; CANCELLED: 取消任务
    PENDING --&amp;gt; CANCELLED: 取消任务
    RUNNING --&amp;gt; ERROR: 无处理器/参数错误&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四核心设计要点&#34;&gt;四、核心设计要点&lt;/h2&gt;
&lt;h3 id=&#34;41-lua-原子转移&#34;&gt;4.1 Lua 原子转移&lt;/h3&gt;
&lt;p&gt;任务进入终态时，必须保证“写入 finished list”和“从 ZSET 移除”是原子的，否则可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务已判定成功但还在 ZSET 里，被下一次调度捞出来；&lt;/li&gt;
&lt;li&gt;任务已从 ZSET 移除但 finished list 没写入，查询不到结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;框架使用 Lua 脚本完成这一转移：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; score &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZSCORE&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;RPUSH&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; score &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZREM&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;逻辑：先查 ZSET，无论是否存在都把终态信息写入 finished list，如果存在再删除。整个脚本在 Redis 单线程执行，天然原子。&lt;/p&gt;
&lt;h3 id=&#34;42-double-check&#34;&gt;4.2 Double-Check&lt;/h3&gt;
&lt;p&gt;调度器拿到任务级锁后、提交线程池前，会再次回查 Redis：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务是否仍在 running ZSET 中？&lt;/li&gt;
&lt;li&gt;任务状态是否仍是 CREATED？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任一条件不满足，立即释放锁并跳过。这是经典的双重检查模式，把“检查-执行”串在锁保护的临界区内。&lt;/p&gt;
&lt;h3 id=&#34;43-双存储后端&#34;&gt;4.3 双存储后端&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[TaskStorage 接口] --&amp;gt; B[RedisTaskStorage]
    A --&amp;gt; C[JdbcTaskStorage]
    B --&amp;gt; D[Redisson ZSET + List]
    C --&amp;gt; E[reliable_executor_task 表]
    F[配置 reliable-executor.task-storage] --&amp;gt;|redis/mysql| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis&lt;/code&gt;：默认，适合已有 Redis 的场景；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mysql&lt;/code&gt;：适合不希望引入 Redis 或需要更强持久化的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种后端通过 Spring 条件装配切换，业务代码无感知。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五快速开始&#34;&gt;五、快速开始&lt;/h2&gt;
&lt;h3 id=&#34;51-引入依赖&#34;&gt;5.1 引入依赖&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gradle&#34; data-lang=&#34;gradle&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dependencies &lt;span style=&#34;color:#f92672&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;com.ftmao:ke-reliable-executor:1.0.0-SNAPSHOT&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;org.redisson:redisson-spring-boot-starter:3.27.2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-配置&#34;&gt;5.2 配置&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;application&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;my-app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;redis&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;redisson&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;config&lt;/span&gt;: |&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        singleServerConfig:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;          address: &amp;#34;redis://127.0.0.1:6379&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;reliable-executor&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;task-storage&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;redis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;core-pool-size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;max-pool-size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;queue-capacity&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-定义处理器&#34;&gt;5.3 定义处理器&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OrderTaskHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@ReliableTaskHandler&lt;/span&gt;(name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;process_order&amp;#34;&lt;/span&gt;, paramType &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; OrderParam.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;processOrder&lt;/span&gt;(OrderParam param) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 业务逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@ReliableTaskHandler&lt;/span&gt;(name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;send_notify&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sendNotify&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 无参任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-提交任务&#34;&gt;5.4 提交任务&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; ReliableExecutor reliableExecutor;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submitOrder&lt;/span&gt;(OrderParam param) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        TaskSubmitRequest request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; TaskSubmitRequest.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskId&lt;/span&gt;(UUID.&lt;span style=&#34;color:#a6e22e&#34;&gt;randomUUID&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;toString&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskName&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;process_order&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskParameter&lt;/span&gt;(param)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;retryLimit&lt;/span&gt;(3)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;retryInterval&lt;/span&gt;(2000)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;maxDurationMillis&lt;/span&gt;(60000)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        reliableExecutor.&lt;span style=&#34;color:#a6e22e&#34;&gt;submitTask&lt;/span&gt;(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;55-监听生命周期&#34;&gt;5.5 监听生命周期&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MyTaskListener&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; ReliableTaskListener {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onTaskCompleted&lt;/span&gt;(String taskId, String taskName) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 任务成功&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onTaskFailed&lt;/span&gt;(String taskId, String taskName, String status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 任务最终失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;六使用注意点与避坑指南&#34;&gt;六、使用注意点与避坑指南&lt;/h2&gt;
&lt;h3 id=&#34;61-业务幂等仍然是最后一道防线&#34;&gt;6.1 业务幂等仍然是最后一道防线&lt;/h3&gt;
&lt;p&gt;框架通过两层锁、double-check、Lua 原子转移把重复执行的概率降到极低，但&lt;strong&gt;任何分布式系统都无法 100% 保证幂等&lt;/strong&gt;。业务 Handler 仍然应该保证幂等，尤其是涉及写数据库、调用外部接口的场景。&lt;/p&gt;
&lt;h3 id=&#34;62-taskid-需要业务保证唯一&#34;&gt;6.2 taskId 需要业务保证唯一&lt;/h3&gt;
&lt;p&gt;框架使用业务传入的 &lt;code&gt;taskId&lt;/code&gt; 作为任务唯一标识。如果同一业务单据重复提交相同 &lt;code&gt;taskId&lt;/code&gt;，第二次提交会生成新的 ZSET member（如果业务字段不同）或覆盖（如果完全相同）。&lt;strong&gt;建议用业务主键 + 操作类型组合生成 taskId&lt;/strong&gt;，例如 &lt;code&gt;order_12345_pay&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;63-重试间隔与最大时长的权衡&#34;&gt;6.3 重试间隔与最大时长的权衡&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;reliable-executor&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;retryInterval&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2000&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 失败后 2s 再试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;maxDurationMillis&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;60000&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 任务最长存活 60s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;retryInterval&lt;/code&gt; 过小会导致失败任务快速反复执行，放大下游压力；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxDurationMillis&lt;/code&gt; 是从任务创建时间算起，不是单次执行时间；&lt;/li&gt;
&lt;li&gt;建议失败任务采用指数退避（当前版本为固定间隔，后续可扩展）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;64-mysql-分支需要自行引入-jpa&#34;&gt;6.4 MySQL 分支需要自行引入 JPA&lt;/h3&gt;
&lt;p&gt;框架中 &lt;code&gt;spring-boot-starter-data-jpa&lt;/code&gt; 是 &lt;code&gt;compileOnly&lt;/code&gt;，使用 MySQL 存储时需要在业务应用里显式引入：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gradle&#34; data-lang=&#34;gradle&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;org.springframework.boot:spring-boot-starter-data-jpa&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;runtimeOnly &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;com.mysql:mysql-connector-j&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;并执行建表脚本 &lt;code&gt;db/migration/V1__create_reliable_executor_tables.sql&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;65-redis-集群环境注意-key-前缀&#34;&gt;6.5 Redis 集群环境注意 Key 前缀&lt;/h3&gt;
&lt;p&gt;框架默认使用 &lt;code&gt;spring.application.name&lt;/code&gt; 作为 Redis Key 前缀。如果多个应用共用同一个 Redis 实例，务必设置不同的 &lt;code&gt;spring.application.name&lt;/code&gt;，否则任务会互相串扰。&lt;/p&gt;
&lt;h3 id=&#34;66-线程池队列满载时会跳过调度&#34;&gt;6.6 线程池队列满载时会跳过调度&lt;/h3&gt;
&lt;p&gt;当线程池队列达到 &lt;code&gt;queueCapacity&lt;/code&gt; 时，调度器会跳过本次周期，避免无限制堆积。如果业务出现持续高峰，建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调大 &lt;code&gt;maxPoolSize&lt;/code&gt; 和 &lt;code&gt;queueCapacity&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;监控 &lt;code&gt;reliable_executor_threadpool_queue_size&lt;/code&gt; 指标；&lt;/li&gt;
&lt;li&gt;必要时配合业务限流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;67-不要忽略超时任务&#34;&gt;6.7 不要忽略超时任务&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;maxDurationMillis&lt;/code&gt; 是从创建时间算起的总时长。如果一个任务在队列里排队太久，可能还没执行就触发 TIMEOUT。对于允许较长延迟的任务，要把这个值设大；对于实时性任务，要把重试间隔和排队容量一起考虑。&lt;/p&gt;
&lt;h3 id=&#34;68-本地开发无-redis-怎么办&#34;&gt;6.8 本地开发无 Redis 怎么办？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 MySQL 分支（H2 也可以跑测试）；&lt;/li&gt;
&lt;li&gt;或者本地启动一个 Redis Docker 容器；&lt;/li&gt;
&lt;li&gt;框架的 Redis 集成测试会在无 Redis 时自动跳过，不影响 CI。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七监控指标&#34;&gt;七、监控指标&lt;/h2&gt;
&lt;p&gt;框架内置 Micrometer 指标，可接入 Prometheus/Actuator：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标名&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_is_master&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;当前节点是否为 Master&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_scheduled_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计调度任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_executed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计执行任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_completed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计成功任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_failed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计失败任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_timeout_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计超时任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_retried_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计重试次数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_threadpool_queue_size&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;线程池队列大小&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_task_execution_seconds&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;任务执行耗时&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八开源与示例&#34;&gt;八、开源与示例&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/reliableexecutor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例模块&lt;/strong&gt;：&lt;code&gt;example/&lt;/code&gt; 子项目包含可运行的 Spring Boot 应用和 REST 接口示例，可直接启动体验任务提交与执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;KE Reliable Executor 是一个&lt;strong&gt;轻量、可靠、易接入&lt;/strong&gt;的异步任务执行框架。它用 Redis ZSET + 两层锁 + Lua 原子转移，在“不引入重型 MQ”的前提下，提供了任务提交、调度、重试、状态查询、监控等完整能力；同时通过 MySQL 分支提供了另一种存储选择。&lt;/p&gt;
&lt;p&gt;如果你的业务符合以下特征，不妨试试它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已有 Redis/MySQL；&lt;/li&gt;
&lt;li&gt;任务量中等，重视可靠性；&lt;/li&gt;
&lt;li&gt;希望快速落地，不想为 MQ 付出额外运维成本；&lt;/li&gt;
&lt;li&gt;需要任务状态可观测、可人工介入。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;记住：框架层负责把重复消费的概率降到极低，业务层仍应保证 Handler 幂等，这才是工程上的稳妥做法。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>
