使用场景
- 单机服务端场景(路由器部署,测试支持-Xms32m -Xmx32m配置),一键部署,直接使用
- electron 集成使用,使用electron-egg框架,直接启动时集成,无需依赖mysql
- 单机测试非常方便,admin + executor集成在一起,一键调度,支持shell、powershell等场景。
具体代码:https://github.com/BoomManPro/xxl-job/tree/boommanpro/feat/sqliteDbAdmin
- 额外魔改了前端页面,merge xxl-job-front前端,使用起来比原版好用。
- 支持pushplus推送,重构了alarm模块,可支持其他推送。
可能的TODO
- XXL-JOB的python、shell、powershell脚本市场。
- 调度锁按 database-id 自动选择 MySQL / SQLite 实现(当前分支固定使用 SQLite 实现,见下文“调度锁”)。
魔改链路
- 关于锁部分通过重构代码,判断是否是sqlite模式,采用不同sql执行。
- 关于其他涉及语法差异部分,使用mybatis的databaseId执行不同类型sql数据库。
- spring.datasource.hikari.connection-test-query=PRAGMA journal_mode=WAL; 解决sqlite并发问题
- 通过集成XxlJobExecutorConfig实现admin端绑定执行器一体化。
实现细节(对照分支代码)
分支基于 XXL-JOB 2.4.1-SNAPSHOT(Spring Boot 2.7.9),改动集中在 xxl-job-admin 模块,完整代码见 分支 boommanpro/feat/sqliteDbAdmin。
1. 依赖与配置
引入 SQLite JDBC 驱动(pom.xml):
<!-- sqlite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.34.0</version>
</dependency>数据源与 MyBatis 配置(application.properties,数据库文件路径改成你本机的):
### mybatis
mybatis.configuration.database-id=sqlite
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
#mybatis.type-aliases-package=com.xxl.job.admin.core.model
### xxl-job, datasource
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
#spring.datasource.username=root
#spring.datasource.password=root_pwd
## xxl-job sqlite
spring.datasource.url=jdbc:sqlite:/path/to/xxl-job/doc/sqlite/test.db
spring.datasource.driver-class-name=org.sqlite.JDBC
### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=PRAGMA journal_mode=WAL;
spring.datasource.hikari.validation-timeout=1000mybatis.configuration.database-id=sqlite:既告诉 MyBatis 走 sqlite 方言的 SQL,也是下面数据源配置生效的开关。PRAGMA journal_mode=WAL:WAL 模式一旦设置就写进数据库文件,对之后所有连接持续生效(WAL 是持久化的日志模式),这里借 Hikari 的连接校验语句保证它被启用。WAL 下读和写可以并发,但同一时刻仍然只允许一个写事务。仓库自带一份已建好 8 张表的数据库文件 doc/sqlite/test.db,复制一份改路径即可使用。
2. 数据源
只在 database-id 为 sqlite 时生效。额外把文本日期的解析格式设为 yyyy-MM-dd HH:mm:ss:初始化数据里的时间是“2018-11-03 22:21:31”这样的文本,而 sqlite-jdbc 默认按带毫秒的 yyyy-MM-dd HH:mm:ss.SSS 解析,不指定会读取失败;程序运行后新写入的时间按驱动默认存成毫秒时间戳,库里两种格式并存(DataSourceTypeConfiguration.java):
@Bean
@ConditionalOnProperty(value = "mybatis.configuration.database-id", havingValue = "sqlite")
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
Properties dataSourceProperties = new SQLiteConfig().toProperties();
dataSourceProperties.setProperty(SQLiteConfig.Pragma.DATE_STRING_FORMAT.pragmaName, "yyyy-MM-dd HH:mm:ss");
dataSource.setDataSourceProperties(dataSourceProperties);
log.info("load datasource for sqlite type");
return dataSource;
}3. SQL 方言差异:MyBatis databaseId
注册数据库厂商与 databaseId 的映射(DatabaseConfig.java):
@Bean
public DatabaseIdProvider getDatabaseIdProvider() {
Properties properties = new Properties();
properties.setProperty("Oracle", "oracle");
properties.setProperty("MySQL", "mysql");
properties.setProperty("DB2", "db2");
properties.setProperty("Derby", "derby");
properties.setProperty("H2", "h2");
properties.setProperty("HSQL", "hsql");
properties.setProperty("Informix", "informix");
properties.setProperty("MS-SQL", "ms-sql");
properties.setProperty("PostgreSQL", "postgresql");
properties.setProperty("Sybase", "sybase");
properties.setProperty("Hana", "hana");
properties.setProperty("SQLite", "sqlite");
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}Mapper 里给有方言差异的 SQL 写一份 databaseId="sqlite" 的版本,MyBatis 会优先使用和当前数据库匹配的语句。以注册表清理为例,MySQL 版用 DATE_ADD,SQLite 没有这个函数(XxlJobRegistryMapper.xml):
<select id="findDead" parameterType="java.util.HashMap" resultType="java.lang.Integer" >
SELECT t.id
FROM xxl_job_registry AS t
WHERE t.update_time <![CDATA[ < ]]> DATE_ADD(#{nowTime},INTERVAL -#{timeout} SECOND)
</select>
<select id="findDead" parameterType="java.util.HashMap" resultType="java.lang.Integer" databaseId="sqlite">
SELECT t.id
FROM xxl_job_registry AS t
WHERE t.update_time <![CDATA[ < ]]> #{deadDate}
</select>SQLite 版本的截止时间改成在 Java 里算好再传进来(JobRegistryHelper.java):
Date deadDate = LocalDateTimeUtils.toDate(LocalDateTime.now().minusSeconds(RegistryConfig.DEAD_TIMEOUT));
List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date(),deadDate);4. 调度锁
XXL-JOB 调度线程每轮都先执行一条加锁 SQL,保证多个调度中心不会重复调度。把这条 SQL 抽成接口,分别实现(XxlJobSqlConfigService.java、MySQLDbSqlConfigService.java、SqliteDbSqlConfigService.java):
package com.xxl.job.admin.core.util;
public interface XxlJobSqlConfigService {
String getLockSql();
}
package com.xxl.job.admin.core.util;
import org.springframework.stereotype.Service;
@Service
public class MySQLDbSqlConfigService implements XxlJobSqlConfigService{
@Override
public String getLockSql() {
return "select * from xxl_job_lock where lock_name = 'schedule_lock' for update";
}
}
package com.xxl.job.admin.core.util;
import org.springframework.stereotype.Service;
@Service
public class SqliteDbSqlConfigService implements XxlJobSqlConfigService{
@Override
public String getLockSql() {
return "select * from xxl_job_lock where lock_name = 'schedule_lock'";
}
}调度线程里使用(JobScheduleHelper.java):
connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
preparedStatement = conn.prepareStatement(xxlJobSqlConfigService.getLockSql());
preparedStatement.execute();SQLite 不支持
SELECT ... FOR UPDATE,所以 SQLite 版本去掉了 for update。单机部署(只有一个调度中心)时没有问题,但这条语句在 SQLite 下不再起分布式锁的作用,不能多实例部署 admin。当前分支在 XxlJobAdminConfig.java 里直接返回 SQLite 实现:
public XxlJobSqlConfigService xxlJobSqlConfigService(){
return sqliteDbSqlConfigService;
}如果要在同一份代码里切回 MySQL,需要改成按 database-id 选择对应实现。
5. admin 与执行器合一
通过 xxl.job.executor.enable=true 在 admin 进程里直接启动一个执行器,并注册到本机 admin,一个进程就能完成调度和执行(XxlJobConfig.java):
@Slf4j
@Configuration
@ConditionalOnProperty(value = "xxl.job.executor.enable", havingValue = "true")
public class XxlJobConfig {
@Bean
public SampleXxlJob sampleXxlJob() {
return new SampleXxlJob();
}
@Bean
@ConfigurationProperties(prefix = "xxl.job")
public XxlJobExecutorConfig xxlJobExecutorConfig() {
return new XxlJobExecutorConfig();
}
@Bean
public XxlJobSpringExecutor xxlJobExecutor(XxlJobExecutorConfig xxlJobExecutorConfig) {
log.info(">>>>>>>>>>> xxl-job config init.");
return new XxlJobSpringExecutor(xxlJobExecutorConfig);
}
}xxl.job.accessToken=default_token
xxl.job.executor.enable=true
xxl.job.admin-addresses=http://127.0.0.1:8080/
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.address=
xxl.job.executor.ip=
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=306. PushPlus 告警
告警类型新增 PushPlus(AlarmType.java),按任务配置推送的 token 和群组(PushPlusAlarmConfig.java),发送逻辑见 PushPlushJobAlarm.java:
@Getter
@AllArgsConstructor
public enum AlarmType {
EMAIL("email", "通过邮件发送告警信息"),
PUSH_PLUSH("push_plush", "通过PushPlus发送告警信息"),
;
private final String value;
private final String desc;
}@Data
public class PushPlusAlarmConfig {
private String desc;
private List<Single> configs;
@Data
public static class Single {
private String token;
private String topic;
}
}