Java 任务调度程序的灵感来自于对比 Quartz 更简单的集群java.util.concurrent.ScheduledExecutorService
的需求。
因此,也受到用户的赞赏(cbarbosa2、rafaelhofmann、BukhariH):
你的自由棒极了!我很高兴我摆脱了石英并用你的取代了它,这更容易处理!
巴博萨2
另请参阅为什么不使用 Quartz?
< dependency >
< groupId >com.github.kagkarlsson</ groupId >
< artifactId >db-scheduler</ artifactId >
< version >15.0.0</ version >
</ dependency >
在数据库模式中创建scheduled_tasks
表。请参阅 postgresql、oracle、mssql 或 mysql 的表定义。
实例化并启动调度程序,然后调度程序将启动任何定义的重复任务。
RecurringTask < Void > hourlyTask = Tasks . recurring ( "my-hourly-task" , FixedDelay . ofHours ( 1 ))
. execute (( inst , ctx ) -> {
System . out . println ( "Executed!" );
});
final Scheduler scheduler = Scheduler
. create ( dataSource )
. startTasks ( hourlyTask )
. threads ( 5 )
. build ();
// hourlyTask is automatically scheduled on startup if not already started (i.e. exists in the db)
scheduler . start ();
有关更多示例,请继续阅读。有关内部工作原理的详细信息,请参阅工作原理。如果您有 Spring Boot 应用程序,请查看 Spring Boot 用法。
已知在生产中运行 db-scheduler 的组织列表:
公司 | 描述 |
---|---|
数码邮政 | 挪威数字邮箱提供商 |
维集团 | 北欧国家最大的运输集团之一。 |
明智的 | 一种廉价、快捷的海外汇款方式。 |
贝克尔职业教育 | |
莫尼托里亚 | 网站监控服务。 |
装载机 | Web 应用程序的负载测试。 |
州政府 | 挪威公共道路管理局 |
光年 | 一种简单易行的全球投资方式。 |
资产净值 | 挪威劳工和福利管理局 |
现代循环 | 使用 ModernLoop 提高面试安排、沟通和协调的效率,从而满足公司的招聘需求。 |
迪菲亚 | 挪威电子医疗公司 |
天鹅 | Swan 帮助开发人员将银行服务轻松嵌入到他们的产品中。 |
陶朗 | TOMRA 是一家挪威跨国公司,设计和制造用于回收的反向自动售货机。 |
请随意打开 PR 将您的组织添加到列表中。
另请参阅可运行的示例。
使用startTasks
构建器方法定义一个重复任务并安排该任务在启动时首次执行。完成后,任务将根据定义的时间表重新安排(请参阅预定义的时间表类型)。
RecurringTask < Void > hourlyTask = Tasks . recurring ( "my-hourly-task" , FixedDelay . ofHours ( 1 ))
. execute (( inst , ctx ) -> {
System . out . println ( "Executed!" );
});
final Scheduler scheduler = Scheduler
. create ( dataSource )
. startTasks ( hourlyTask )
. registerShutdownHook ()
. build ();
// hourlyTask is automatically scheduled on startup if not already started (i.e. exists in the db)
scheduler . start ();
对于具有多个实例和计划的重复任务,请参阅示例 RecurringTaskWithPersistentScheduleMain.java。
一次性任务的实例在将来的某个时间(即非重复性)具有单个执行时间。实例 ID 在此任务中必须是唯一的,并且可用于对某些元数据(例如 ID)进行编码。对于更复杂的状态,支持自定义可序列化 java 对象(如示例中所使用的)。
定义一个一次性任务并启动调度程序:
TaskDescriptor < MyTaskData > MY_TASK =
TaskDescriptor . of ( "my-onetime-task" , MyTaskData . class );
OneTimeTask < MyTaskData > myTaskImplementation =
Tasks . oneTime ( MY_TASK )
. execute (( inst , ctx ) -> {
System . out . println ( "Executed! Custom data, Id: " + inst . getData (). id );
});
final Scheduler scheduler = Scheduler
. create ( dataSource , myTaskImplementation )
. registerShutdownHook ()
. build ();
scheduler . start ();
...然后在某个时刻(在运行时),使用SchedulerClient
安排执行:
// Schedule the task for execution a certain time in the future and optionally provide custom data for the execution
scheduler . schedule (
MY_TASK
. instanceWithId ( "1045" )
. data ( new MyTaskData ( 1001L ))
. scheduledTo ( Instant . now (). plusSeconds ( 5 )));
例子 | 描述 |
---|---|
启用立即执行Main.java | 当调度执行到now() 或更早运行时,本地Scheduler 将得到提示,并“唤醒”以比正常情况更早地检查新执行(由pollingInterval 配置)。 |
MaxRetriesMain.java | 如何设置执行重试次数的限制。 |
ExponentialBackoffMain.java | 如何使用指数退避作为重试策略,而不是默认的固定延迟。 |
ExponentialBackoffWithMaxRetriesMain.java | 如何使用指数退避作为重试策略以及对最大重试次数的硬性限制。 |
TrackingProgressRecurringTaskMain.java | 重复作业可以存储task_data 作为跨执行持久状态的一种方式。这个例子展示了如何操作。 |
SpawningOtherTasksMain.java | 使用executionContext.getSchedulerClient() 演示另一个任务调度实例。 |
SchedulerClientMain.java | 演示SchedulerClient 的一些功能。调度、获取计划执行等。 |
RecurringTaskWithPersistentScheduleMain.java | 多实例重复作业,其中Schedule 作为task_data 的一部分存储。例如,适用于多租户应用程序,其中每个租户都应该有一个重复任务。 |
StatefulRecurringTaskWithPersistentScheduleMain.java | |
JsonSerializerMain.java | 将task_data 的序列化从 Java 序列化(默认)覆盖为 JSON。 |
JobChainingUsingTaskDataMain.java | 作业链,即“当该实例执行完毕后,安排另一个任务。 |
JobChainingUsingSeparateTasksMain.java | 作业链,如上所述。 |
拦截器Main.java | 使用ExecutionInterceptor 在所有ExecutionHandler 执行之前和之后注入逻辑。 |
例子 | 描述 |
---|---|
基本示例 | 基本的一次性任务和重复任务 |
事务性暂存作业 | 以事务方式暂存作业的示例,即确保当事务提交(以及其他数据库修改)时后台作业运行。 |
长时间运行作业 | 长时间运行的作业需要在应用程序重新启动后继续存在,并避免从头开始重新启动。此示例演示了如何在关闭时保持进度,以及限制作业每晚运行的技术。 |
循环状态跟踪 | 一个循环任务,其状态可以在每次运行后修改。 |
并行作业生成器 | 演示如何使用重复作业生成一次性作业,例如用于并行化。 |
作业链 | 具有多个步骤的一次性工作。下一步安排在上一步完成后。 |
多实例循环 | 演示如何实现相同类型但可能不同的计划和数据的多个重复作业。 |
调度程序是使用Scheduler.create(...)
构建器创建的。构建器具有合理的默认值,但以下选项是可配置的。
.threads(int)
线程数。默认10
。
.pollingInterval(Duration)
调度程序检查数据库是否正常执行的频率。默认10s
。
.alwaysPersistTimestampInUTC()
调度程序假设用于持久时间戳的列持久存在Instant
s,而不是LocalDateTime
s,即以某种方式将时间戳绑定到区域。但是,某些数据库对此类类型(没有区域信息)或其他怪癖的支持有限,这使得“始终以 UTC 存储”成为更好的选择。对于这种情况,请使用此设置始终以 UTC 格式存储即时信息。 PostgreSQL 和 Oracle 模式经过测试以保留区域信息。 MySQL和MariaDB -schemas不并且应该使用此设置。注意:为了向后兼容,“未知”数据库的默认行为是假设数据库保留时区。对于“已知”数据库,请参阅AutodetectJdbcCustomization
类。
.enableImmediateExecution()
如果启用此功能,调度程序将尝试向本地Scheduler
提示在计划运行now()
或过去的某个时间后有要执行的执行。注意:如果在事务内调用schedule(..)
/ reschedule(..)
,调度程序可能会尝试在更新可见之前运行它(事务尚未提交)。但它仍然保留,因此即使未命中,它也会在下一个polling-interval
之前运行。您还可以使用调度程序方法scheduler.triggerCheckForDueExecutions()
) 以编程方式触发提前检查到期执行。默认false
。
.registerShutdownHook()
注册一个关闭钩子,它将在关闭时调用Scheduler.stop()
。应始终调用 Stop 来正常关闭并避免死执行。
.shutdownMaxWait(Duration)
调度程序在中断执行程序服务线程之前将等待多长时间。如果您发现自己使用此功能,请考虑是否可以定期检查 ExecutionHandler 中的executionContext.getSchedulerState().isShuttingDown()
并中止长时间运行的任务。默认30min
。
.enablePriority()
可以定义执行的优先级,确定从数据库中获取到期执行的顺序。具有较高优先级值的执行将在具有较低优先级值的执行之前运行(从技术上讲,排序将按order by priority desc, execution_time asc
)。考虑使用 0-32000 范围内的优先级,因为该字段定义为SMALLINT
。如果需要更大的值,请修改架构。目前,此功能是可选的,只有选择通过此配置设置启用优先级的用户才需要列priority
。
使用TaskInstance.Builder
设置每个实例的优先级:
scheduler . schedule (
MY_TASK
. instance ( "1" )
. priority ( 100 )
. scheduledTo ( Instant . now ()));
笔记:
(execution_time asc, priority desc)
上添加索引(替换旧的execution_time asc
)可能会有所帮助。null
值可能会根据数据库(低或高)进行不同的解释。 如果您每秒运行 >1000 次执行,您可能需要使用lock-and-fetch
轮询策略来降低开销和提高吞吐量(了解更多)。如果没有,默认的fetch-and-lock-on-execute
就可以了。
.pollUsingFetchAndLockOnExecute(double, double)
使用默认轮询策略fetch-and-lock-on-execute
。
如果上次从数据库获取的是完整批次 ( executionsPerBatchFractionOfThreads
),则当剩余执行数小于或等于lowerLimitFractionOfThreads * nr-of-threads
时,将触发新的获取。获取的执行不会被锁定/选取,因此调度程序在执行时将与其他实例竞争锁。所有数据库都支持。
默认值: 0,5, 3.0
.pollUsingLockAndFetch(double, double)
使用轮询策略lock-and-fetch
该策略使用select for update .. skip locked
以减少开销。
如果上次从数据库获取的是完整批次,则当剩余执行数小于或等于lowerLimitFractionOfThreads * nr-of-threads
时,将触发新的获取。每次获取的执行数量等于(upperLimitFractionOfThreads * nr-of-threads) - nr-executions-left
。已为此调度程序实例锁定/选取获取的执行,从而节省一个UPDATE
语句。
对于正常使用,设置为例如0.5, 1.0
。
对于高吞吐量(即保持线程繁忙),设置为例如1.0, 4.0
。目前,心跳不会针对队列中选取的执行进行更新(如果upperLimitFractionOfThreads > 1.0
则适用)。如果它们停留在那里超过4 * heartbeat-interval
(默认20m
),而不开始执行,它们将被检测为死亡并可能再次解锁(由DeadExecutionHandler
确定)。目前由postgres支持。 sql-server也支持这一点,但测试表明这很容易出现死锁,因此在理解/解决之前不建议这样做。
.heartbeatInterval(Duration)
更新运行执行的心跳时间戳的频率。默认5m
。
.missedHeartbeatsLimit(int)
在执行被认为死亡之前可能会错过多少次心跳。默认6
。
.addExecutionInterceptor(ExecutionInterceptor)
添加一个ExecutionInterceptor
,它可以围绕执行注入逻辑。对于 Spring Boot,只需注册一个ExecutionInterceptor
类型的 Bean 即可。
.addSchedulerListener(SchedulerListener)
添加一个SchedulerListener
,它将接收与 Scheduler 和 Execution 相关的事件。对于 Spring Boot,只需注册一个SchedulerListener
类型的 Bean 即可。
.schedulerName(SchedulerName)
此调度程序实例的名称。当调度程序选择执行时,该名称将存储在数据库中。默认<hostname>
。
.tableName(String)
用于跟踪任务执行的表的名称。创建表时相应地更改表定义中的名称。默认的scheduled_tasks
。
.serializer(Serializer)
序列化任务数据时使用的序列化器实现。默认使用标准 Java 序列化,但 db-scheduler 还捆绑了GsonSerializer
和JacksonSerializer
。请参阅 KotlinSerializer 的示例。另请参阅序列化器下的其他文档。
.executorService(ExecutorService)
如果指定,则使用此外部管理的执行程序服务来运行执行。理想情况下,仍应提供它将使用的线程数(用于调度程序轮询优化)。默认为null
。
.deleteUnresolvedAfter(Duration)
执行未知任务后自动删除的时间。这些通常可能是不再使用的旧的重复任务。这是非零的,以防止由于配置错误(丢失已知任务)和滚动升级期间的问题而意外删除任务。默认14d
。
.jdbcCustomization(JdbcCustomization)
db-scheduler 尝试自动检测用于查看是否有任何 jdbc 交互需要自定义的数据库。此方法是一个逃生口,允许显式设置JdbcCustomizations
。默认自动检测。
.commitWhenAutocommitDisabled(boolean)
默认情况下,不会对数据源连接发出提交。如果禁用自动提交,则假定事务由外部事务管理器处理。将此属性设置为true
可覆盖此行为并使调度程序始终发出提交。默认false
。
.failureLogging(Level, boolean)
配置如何记录任务失败,即从任务执行处理程序抛出的Throwable
。使用日志级别OFF
可以完全禁用这种日志记录。默认WARN, true
。
任务是使用Tasks
中的构建器类之一创建的。构建器具有合理的默认值,但可以覆盖以下选项。
选项 | 默认 | 描述 |
---|---|---|
.onFailure(FailureHandler) | 参见说明。 | 当ExecutionHandler 抛出异常时该怎么办。默认情况下,重复任务会根据其Schedule 重新安排,一次性任务会在 5m 后再次重试。 |
.onDeadExecution(DeadExecutionHandler) | ReviveDeadExecution | 当检测到死执行(即心跳时间戳过时的执行)时该怎么办。默认情况下,死执行被重新安排到now() 。 |
.initialData(T initialData) | null | 第一次安排重复任务时要使用的数据。 |
该库包含许多用于重复任务的计划实现。参见课程Schedules
。
日程 | 描述 |
---|---|
.daily(LocalTime ...) | 每天在指定时间运行。可以选择指定时区。 |
.fixedDelay(Duration) | 下一次执行时间是上次完成执行后的Duration 。注意:当在startTasks(...) 中使用时,此Schedule 将初始执行安排到Instant.now() |
.cron(String) | Spring 风格的 cron 表达式 (v5.3+)。该模式- 被解释为禁用的时间表。 |
配置计划的另一个选项是使用Schedules.parse(String)
读取字符串模式。
目前可用的模式有:
图案 | 描述 |
---|---|
FIXED_DELAY|Ns | 与.fixedDelay(Duration) 相同,持续时间设置为 N 秒。 |
DAILY|12:30,15:30...(|time_zone) | 与.daily(LocalTime) 相同,带有可选时区(例如欧洲/罗马、UTC) |
- | 残疾人时间表 |
有关时区格式的更多详细信息,请参阅此处。
可以将Schedule
标记为禁用。调度程序不会为禁用计划的任务安排初始执行,并且会删除该任务的任何现有执行。
任务实例可能在字段task_data
中有一些关联的数据。调度程序使用Serializer
来读取此数据并将其写入数据库。默认情况下,使用标准 Java 序列化,但提供了许多选项:
GsonSerializer
JacksonSerializer
对于 Java 序列化,建议指定一个serialVersionUID
以便能够发展表示数据的类。如果未指定,并且类发生更改,反序列化可能会失败并出现InvalidClassException
。如果发生这种情况,请显式查找并设置当前自动生成的serialVersionUID
。然后就可以对类进行不间断的更改。
如果您需要从 Java 序列化迁移到GsonSerializer
,请将调度程序配置为使用SerializerWithFallbackDeserializers
:
. serializer ( new SerializerWithFallbackDeserializers ( new GsonSerializer (), new JavaSerializer ()))
对于 Spring Boot 应用程序,有一个启动器db-scheduler-spring-boot-starter
使调度程序接线非常简单。 (参见完整的示例项目)。
DataSource
。 (在示例中使用 HSQLDB 并自动应用架构。)< dependency >
< groupId >com.github.kagkarlsson</ groupId >
< artifactId >db-scheduler-spring-boot-starter</ artifactId >
< version >15.0.0</ version >
</ dependency >
Task
公开为 Spring beans。如果它们重复出现,它们将自动被拾取并启动。Scheduler
状态公开到执行器运行状况信息中,则需要启用db-scheduler
运行状况指示器。春季健康信息。配置主要通过application.properties
完成。调度程序名称、序列化程序和执行程序服务的配置是通过将DbSchedulerCustomizer
类型的 bean 添加到 Spring 上下文来完成的。
# application.properties example showing default values
db-scheduler.enabled=true
db-scheduler.heartbeat-interval=5m
db-scheduler.polling-interval=10s
db-scheduler.polling-limit=
db-scheduler.table-name=scheduled_tasks
db-scheduler.immediate-execution-enabled=false
db-scheduler.scheduler-name=
db-scheduler.threads=10
db-scheduler.priority-enabled=false
# Ignored if a custom DbSchedulerStarter bean is defined
db-scheduler.delay-startup-until-context-ready=false
db-scheduler.polling-strategy=fetch
db-scheduler.polling-strategy-lower-limit-fraction-of-threads=0.5
db-scheduler.polling-strategy-upper-limit-fraction-of-threads=3.0
db-scheduler.shutdown-max-wait=30m
可以使用Scheduler
与持久的未来执行进行交互。对于不需要完整Scheduler
实例的情况,可以使用其构建器创建更简单的 SchedulerClient:
SchedulerClient . Builder . create ( dataSource , taskDefinitions ). build ()
它将允许执行以下操作:
单个数据库表用于跟踪未来的任务执行。当任务执行到期时,db-scheduler 会选择它并执行它。执行完成后,将咨询Task
以了解应该做什么。例如, RecurringTask
通常会根据其Schedule
在未来重新安排。
调度程序使用乐观锁定或选择更新(取决于轮询策略)来保证只有一个调度程序实例可以选择并运行任务执行。
术语“重复任务”用于应根据某个计划定期运行的任务。
当重复性任务的执行完成时,将参考Schedule
来确定下一次执行的时间,并为该时间创建未来的任务执行(即重新安排)。选择的时间将是根据Schedule
最近的时间,但仍然是将来的时间。
有两种类型的重复任务,常规静态重复任务,其中Schedule
在代码中静态定义,以及动态重复任务,其中Schedule
在运行时定义并保存在数据库中(仍然只需要一个表) 。
静态重复任务是最常见的任务,适用于常规后台作业,因为如果任务实例不存在,调度程序会自动调度该实例,并且如果更新Schedule
,也会更新下一个执行时间。
为了创建静态重复任务的初始执行,调度程序有一个方法startTasks(...)
,该方法获取一个任务列表,如果这些任务还没有现有的执行,则应“启动”这些任务。初始执行时间由Schedule
决定。如果任务已经有未来的执行(即之前至少启动过一次),但更新的Schedule
现在指示另一个执行时间,则现有的执行将被重新安排到新的执行时间(非确定性任务除外)诸如FixedDelay
之类的调度,其中新的执行时间在更远的将来)。
使用Tasks.recurring(..)
创建。
动态重复任务是后来添加到 db-scheduler 中的,添加它是为了支持需要具有不同调度的相同类型任务(即相同实现)的多个实例的用例。 Schedule
与任何常规数据一起保存在task_data
中。与静态重复任务不同,动态任务不会自动安排任务实例。用户可以根据需要创建实例并更新现有实例的计划(使用SchedulerClient
接口)。有关更多详细信息,请参阅示例 RecurringTaskWithPersistentScheduleMain.java。
使用Tasks.recurringWithPersistentSchedule(..)
创建。
术语“一次性任务”用于具有单一执行时间的任务。除了将数据编码到任务执行的instanceId
中之外,还可以将任意二进制数据存储在单独的字段中以供执行时使用。默认情况下,Java 序列化用于封送/解封数据。
使用Tasks.oneTime(..)
创建。
对于不符合上述类别的任务,可以使用Tasks.custom(..)
完全自定义任务的行为。
用例可能是:
在执行期间,调度程序定期更新任务执行的心跳时间。如果一个执行被标记为正在执行,但没有接收到心跳时间的更新,则在时间 X 之后它将被视为死执行。例如,如果运行调度程序的 JVM 突然退出,则可能会发生这种情况。
当发现死执行时,将咨询Task
以了解应该做什么。失效的RecurringTask
通常会重新安排到now()
。
虽然 db-scheduler 最初针对中低吞吐量用例,但它可以很好地处理高吞吐量用例(1000+ 执行/秒),因为它的数据模型非常简单,包括单个执行表。要了解它将如何执行,考虑它在每批执行中运行的 SQL 语句很有用。
原始的默认轮询策略fetch-and-lock-on-execute
将执行以下操作:
select
一批到期执行update
为picked=true
。可能会由于调度程序竞争而错过。update
或delete
记录。每批次总计:1 次选择,2 * 批次大小更新(不包括未命中)
在 v10 中,添加了新的轮询策略( lock-and-fetch
)。它利用了这样一个事实:大多数数据库现在都支持SELECT FOR UPDATE
语句中的SKIP LOCKED
(请参阅第二象限博客)。使用这样的策略,可以获取预锁定的执行,从而减少一条语句:
select for update .. skip locked
一批到期执行。这些将已经由调度程序实例选择。update
或delete
记录。每批次总计:1 次选择和更新,1 * 批次大小更新(无遗漏)
要了解 db-scheduler 的预期结果,请参阅下面在 GCP 中运行的测试的结果。测试使用了几种不同的配置来运行,但每个配置都使用在不同虚拟机上运行的 4 个竞争调度程序实例。 TPS 是大约。 GCP 中显示的每秒事务数。
吞吐量获取(ex/s) | TPS 获取(估计) | 锁定和获取吞吐量(ex/s) | TPS 锁定和获取(估计) | |
---|---|---|---|---|
Postgres 4 核 25GB 内存,4xVM(2 核) | ||||
20 个螺纹,下部 4.0,上部 20.0 | 2000年 | 9000 | 10600 | 11500 |
100个线程,下2.0,上6.0 | 2560 | 11000 | 11200 | 11200 |
Postgres 8 核 50GB 内存,4xVM(4 核) | ||||
50 个螺纹,下部:0.5,上部:4.0 | 4000 | 22000 | 11840 | 10300 |
这些测试的观察结果:
fetch-and-lock-on-execute
lock-and-fetch
目前,轮询策略lock-and-fetch
仅针对Postgres实现。欢迎贡献更多数据库支持。
有许多用户正在使用 db-scheduler 来实现高吞吐量用例。参见示例:
不保证RecurringTask
计划中的所有时刻都将被执行。上一次任务执行完成后,会查阅Schedule
,选择未来最接近的时间作为下一次执行的时间。将来可能会添加新类型的任务来提供此类功能。
SchedulerClient
上的方法( schedule
、 cancel
、 reschedule
)将使用提供的DataSource
中的新Connection
运行。要使操作成为事务的一部分,必须由提供的DataSource
处理它,例如使用 Spring 的TransactionAwareDataSourceProxy
之类的东西。
目前,db-scheduler 的精度取决于pollingInterval
(默认 10 秒),它指定在表中查找到期执行的频率。如果您知道自己在做什么,则可能会在运行时通过scheduler.triggerCheckForDueExecutions()
指示调度程序“尽早查看”。 (另请参阅Builder
上的enableImmediateExecution()
)
请参阅发行版以获取发行说明。
升级到 15.x
priority
和索引priority_execution_time_idx
必须添加到数据库模式中。请参阅 postgresql、oracle 或 mysql 的表定义。在某些时候,此栏将成为必填栏。这将在未来的版本/升级说明中明确说明。升级到 8.x
boolean isDeterministic()
方法来指示它们是否总是产生相同的时刻。升级到 4.x
consecutive_failures
添加到数据库架构中。请参阅 postgresql、oracle 或 mysql 的表定义。 null
被视为 0,因此无需更新现有记录。升级到 3.x
Tasks
类中的构建器完成升级到 2.x
task_data
添加到数据库架构中。请参阅 postgresql、oracle 或 mysql 的表定义。 先决条件
请按照下列步骤操作:
克隆存储库。
git clone https://github.com/kagkarlsson/db-scheduler
cd db-scheduler
使用 Maven 构建(通过添加-DskipTests=true
跳过测试)
mvn package
推荐规格
一些用户在单核虚拟机上运行时遇到过间歇性测试失败的情况。因此,建议至少使用:
Quartz
为什么还要db-scheduler
呢? db-scheduler
的目标是非侵入性且易于使用,但仍然解决持久性问题和集群协调问题。它最初是针对具有适度数据库模式的应用程序,添加 11 个表会让人感觉有点大材小用。更新:此外,截至目前(2024 年),Quartz 似乎也没有得到积极维护。
吻。这是最常见的共享状态应用程序类型。
请创建一个有关功能请求的问题,我们可以在那里进行讨论。如果您不耐烦(或想贡献),欢迎拉取请求:)
是的。目前已在多家企业生产使用,运行顺利。