分布式定时任务-mysql实现

分布式定时任务-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

可以发现 两个进程最终只能有一个获取到锁并执行任务

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值