使用spring task定时任务的时候,平时本地测试没问题,如果部署到分布式集群环境就会出现一个任务执行多次的结果,轻则影响JVM效率,重则出现数据异常,系统运行结果不符合预期造成经济损失。
本文展示最直观最轻量的解决方案可供参考修改。
pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
任务配置类 :异步多线程
package com.ts.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class TaskConfig {
private int corePoolSize = 5;
private int maxPoolSize = 10;
private int queueCapacity = 5;
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.initialize();
return executor;
}
}
Flag类:给任务一个标记,记录任务的状态和version,相当于乐观锁
package com.ts.entity;
import lombok.Data;
@Data
public class Flag {
//id
private String id;
//任务名字,随意
private String name;
//任务状态 RUNNING,STOPED
private String status;
//乐观锁
private Long version;
}
Dao层
package com.ts.dao;
import com.ts.entity.Flag;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface FlagMapper {
/**
* 获取任务标记
* @param name
* @return
*/
Flag selectByName(@Param("name") String name);
/**
* 根据version进行更新任务状态
* @param flag
* @param version
* @return
*/
int updateByVersion(@Param("flag")Flag flag, @Param("version")Long version);
/**
* 更新任务状态
* @param flag
* @return
*/
int updateStatus(Flag flag);
}
mybatis,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.ts.dao.FlagMapper">
<resultMap id="FlagResultMap" type="com.ts.entity.Flag">
<id column="id" property="id" jdbcType="CHAR"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="status" property="status" jdbcType="VARCHAR"/>
<result column="version" property="version" jdbcType="BIGINT"/>
</resultMap>
<sql id="Flag_Column_List">
id,`name`,status,version
</sql>
<!-- 获取任务标记 -->
<select id="selectByName" parameterType="string" resultMap="FlagResultMap">
SELECT <include refid="Flag_Column_List"/>
FROM flag
WHERE status = 'STOPED' and `name` = #{name};
</select>
<!-- 更新任务标记 -->
<update id="updateByVersion" parameterType="com.ts.entity.Flag">
update flag
<set>
<if test="flag.status != null">
status = #{flag.status, jdbcType=VARCHAR},
</if>
<if test="flag.version != null">
version = #{flag.version, jdbcType = BIGINT},
</if>
</set>
where id = #{flag.id,jdbcType = CHAR} and version = #{version, jdbcType = BIGINT}
</update>
<!-- 更新任务标记 -->
<update id="updateStatus" parameterType="com.ts.entity.Flag">
update flag
<set>
<if test="status != null">
status = #{status, jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType = CHAR}
</update>
</mapper>
application.yml配置文件
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/task?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath*:mybatis/*.xml
启动类
package com.ts;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@MapperScan(value = "com.ts.dao")
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class, args);
}
}
任务类:MyTask任务类用3个除了日志其他完全一样的任务方法模拟分布式集群3个节点,每隔5秒钟执行一次任务,一般情况下3个任务逻辑都会完全执行,也就是3个节点都执行了任务逻辑。使用乐观锁方式获取执行权判断是否能够执行之后,任务逻辑只执行1次。
package com.ts.task;
import com.ts.dao.FlagMapper;
import com.ts.entity.Flag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Async
@Slf4j
public class MyTask {
@Autowired
private FlagMapper flagMapper;
private int count = 0;
@Scheduled(cron = "0/5 * * * * ?")
public void node1() {
Flag flag = flagMapper.selectByName("有量天尊ts");
if (flag != null) {
flag.setStatus("RUNNING");
Long version = flag.getVersion();
flag.setVersion(flag.getVersion() + 1);
int update = flagMapper.updateByVersion(flag,version);
if (update != 0) {
try {
log.info("====节点1执行任务===" + count++ );
} catch (Exception e) {
e.printStackTrace();
} finally {
flag.setStatus("STOPED");
flagMapper.updateStatus(flag);
}
}
}
}
@Scheduled(cron = "0/5 * * * * ?")
public void node2() {
Flag flag = flagMapper.selectByName("有量天尊ts");
if (flag != null) {
flag.setStatus("RUNNING");
Long version = flag.getVersion();
flag.setVersion(flag.getVersion() + 1);
int update = flagMapper.updateByVersion(flag,version);
if (update != 0) {
try {
log.info("====节点2执行任务===" + count++ );
} catch (Exception e) {
e.printStackTrace();
} finally {
flag.setStatus("STOPED");
flagMapper.updateStatus(flag);
}
}
}
}
@Scheduled(cron = "0/5 * * * * ?")
public void node3() {
Flag flag = flagMapper.selectByName("有量天尊ts");
if (flag != null) {
flag.setStatus("RUNNING");
Long version = flag.getVersion();
flag.setVersion(flag.getVersion() + 1);
int update = flagMapper.updateByVersion(flag,version);
if (update != 0) {
try {
log.info("====节点3执行任务===" + count++ );
} catch (Exception e) {
e.printStackTrace();
} finally {
flag.setStatus("STOPED");
flagMapper.updateStatus(flag);
}
}
}
}
}