分布式定时任务-mysql实现
代码结构参考 xxl-job实现,使用两个线程
线程A:
- 获取全局锁
- 定时从库里扫描近5秒内将要执行的任务
- 如果任务执行时间小于当前时间,则立即执行
- 不然加将任务加入到时间轮中
- 刷新任务下次更新时间
线程B:
- 时间轮实现
- 不停向时间轮取近2秒内任务
- 如果不为空,则执行任务
SQL脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for job_info
-- ----------------------------
DROP TABLE IF EXISTS `job_info`;
CREATE TABLE `job_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`job_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`cron_tab` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`status` int NULL DEFAULT NULL COMMENT '状态: 0,1',
`trigger_time` datetime NULL DEFAULT NULL COMMENT '一次触发时间',
`trigger_timestamp` bigint NULL DEFAULT NULL COMMENT '时间戳',
`update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of job_info
-- ----------------------------
INSERT INTO `job_info` VALUES (1, 'test', '0/10 * * * * *', 1, '2024-01-29 11:13:40', 1706498020000, '2024-01-29 19:13:24');
-- ----------------------------
-- Table structure for job_lock
-- ----------------------------
DROP TABLE IF EXISTS `job_lock`;
CREATE TABLE `job_lock` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`lock_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of job_lock
-- ----------------------------
INSERT INTO `job_lock` VALUES (1, 'lock');
SET FOREIGN_KEY_CHECKS = 1;
实现
JobManager
@Slf4j
public class JobManager {
private static final Integer PRE_READ = 5 * 1000;
//1-list
//2-list
private Map<Integer, List<Integer>> ringMap = new HashMap<>();
private Thread jobScheduler;
private Thread timeRing;
private SqlSessionFactory sqlSessionFactory;
@Autowired
private JobInfoService jobInfoService;
private volatile boolean shutdown;
public JobManager(SqlSessionFactory sessionFactory) {
this.sqlSessionFactory = sessionFactory;
jobScheduler = new Thread(new JobScheduler());
jobScheduler.setDaemon(true);
jobScheduler.start();
timeRing = new Thread(new TimeRing());
timeRing.setDaemon(true);
timeRing.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> JobManager.this.shutdown()));
}
class JobScheduler implements Runnable {
@Override
public void run() {
log.info("{} scheduler started...", Thread.currentThread().getName());
int fetchCount = 200;
while (!shutdown) {
sleepForOneSecond();
//获取全局锁
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = SqlSessionUtils.getSqlSession(sqlSessionFactory).getConnection();
connection.setAutoCommit(false);
String sql = "select * from job_lock where lock_name='lock' for update";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.execute();
//查询待执行job, 读取5S后要执行的job
long now = System.currentTimeMillis();
List<JobInfo> list = jobInfoService.findList(now + PRE_READ, fetchCount);
if (CollectionUtils.isEmpty(list)) {
continue;
}
for (JobInfo jobInfo : list) {
//push time ring if necessary
if (now > jobInfo.getTriggerTimestamp()) {
//立即执行
log.info("{} execute job {} immediately", Thread.currentThread().getName(), jobInfo.getId());
refreshTriggerTime(jobInfo, LocalDateTime.now());
} else {
//未到执行时间
putIntoTimeRing(jobInfo);
refreshTriggerTime(jobInfo, jobInfo.getTriggerTime());
}
//更新 jobInfo trigger next time
}
//刷新job时间
jobInfoService.refreshTime(list);
} catch (SQLException e) {
log.info("{} scheduler job error", Thread.currentThread().getName(), e);
} finally {
try {
if (preparedStatement != null) {
preparedStatement.close();
}
} catch (SQLException e) {
log.info("{} close pstmt error", Thread.currentThread().getName(), e);
}
try {
if (connection != null) {
connection.commit();
connection.close();
}
} catch (SQLException e) {
log.info("{} close conn error", Thread.currentThread().getName(), e);
}
}
}
log.info("{} shutdown now", Thread.currentThread().getName());
}
private void putIntoTimeRing(JobInfo jobInfo) {
int ringSlot = (int) ((jobInfo.getTriggerTimestamp() / 1000) % 60);
log.info("{} put int ring slot at {}-{}", Thread.currentThread().getName(), ringSlot, jobInfo.getTriggerTime());
List<Integer> jobs = ringMap.get(ringSlot);
if (CollectionUtils.isEmpty(jobs)) {
jobs = new ArrayList<>();
jobs.add(jobInfo.getId());
ringMap.put(ringSlot, jobs);
} else {
jobs.add(jobInfo.getId());
}
}
private void refreshTriggerTime(JobInfo jobInfo, LocalDateTime current) {
LocalDateTime next = CronExpression.parse(jobInfo.getCronTab()).next(current);
jobInfo.setTriggerTime(next);
jobInfo.setTriggerTimestamp(next.toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
}
class TimeRing implements Runnable {
@Override
public void run() {
sleepForOneSecond();
log.info("{} start execute job", Thread.currentThread().getName());
while (!shutdown) {
List<Integer> temp = new ArrayList<>();
int sec = LocalDateTime.now().getSecond();
log.info("{} ring slot at {}", Thread.currentThread().getName(), Calendar.getInstance().getTimeInMillis());
for (int i = 0; i < 2; i++) {
int slot = (sec + 60 - i) % 60;
log.info("{} ring slot at {}", Thread.currentThread().getName(), slot);
List<Integer> remove = ringMap.remove(slot);
if (!CollectionUtils.isEmpty(remove)) {
temp.addAll(remove);
}
}
//execute trigger job
if (!CollectionUtils.isEmpty(temp)) {
for (Integer id : temp) {
log.info("{} execute job {}", Thread.currentThread().getName(), id);
}
temp.clear();
}
//next second
sleepForOneSecond();
}
log.info("{} shutdown now", Thread.currentThread().getName());
}
}
private void sleepForOneSecond() {
try {
//1000 - System.currentTimeMillis()%1000 还剩多少毫秒到1s
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
log.error("sleepForOneSecond error ", e);
}
}
public void shutdown() {
this.shutdown = true;
}
}
JobInfoService
public interface JobInfoService extends IService<JobInfo> {
/**
* 查询可执行job列表
* @param now
* @param fetchCount
* @return
*/
List<JobInfo> findList(long now, int fetchCount);
/**
* 刷新job
* @param list
*/
void refreshTime(List<JobInfo> list);
}
@Service
public class JobInfoServiceImpl extends ServiceImpl<JobInfoMapper, JobInfo>
implements JobInfoService {
@Override
public List<JobInfo> findList(long now, int fetchCount) {
return baseMapper.findList(now,fetchCount);
}
@Override
@Transactional
public void refreshTime(List<JobInfo> list) {
for (JobInfo jobInfo : list) {
JobInfo info = new JobInfo();
info.setId(jobInfo.getId());
info.setTriggerTime(jobInfo.getTriggerTime());
info.setTriggerTimestamp(jobInfo.getTriggerTimestamp());
baseMapper.updateById(info);
}
}
}
JobInfoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.boot.excel.mapper.JobInfoMapper">
<resultMap id="BaseResultMap" type="com.example.boot.excel.model.JobInfo">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="jobName" column="job_name" jdbcType="VARCHAR"/>
<result property="cronTab" column="cron_tab" jdbcType="VARCHAR"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="triggerTime" column="trigger_time" jdbcType="TIMESTAMP"/>
<result property="triggerTimestamp" column="trigger_timestamp" jdbcType="BIGINT"/>
</resultMap>
<sql id="Base_Column_List">
id,job_name,cron_tab,
status,trigger_time,trigger_timestamp
</sql>
<select id="findList" resultType="com.example.boot.excel.model.JobInfo">
select * from job_info where status=1 and trigger_timestamp <![CDATA[<=]]>#{now}
order by trigger_timestamp limit
#{fetchCount}
</select>
</mapper>
启动类
@Bean
public JobManager jobManager(SqlSessionFactory sqlSessionFactory){
return new JobManager(sqlSessionFactory);
}
测试
启动两个进程,8081和8082两个端口
# 8081
2024-01-29T11:10:50.002+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:00.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:10.000+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:20.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:20.002+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:40.000+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:50.000+08:00 JobManager : Thread-2 execute job 1
# 8082
2024-01-29T11:10:40.000+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:30.002+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:40.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:11:50.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:00.000+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:10.000+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:12:30.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:13:00.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:13:10.001+08:00 JobManager : Thread-2 execute job 1
2024-01-29T11:13:20.000+08:00 JobManager : Thread-2 execute job 1
可以发现 两个进程最终只能有一个获取到锁并执行任务