使用spring的@Schedule注解实现定时任务时,简单方便且快速,但是,当应用服务部署在多台服务器上做负载均衡时,会出现同一个定时任务多次执行的情况。
考虑到实际项目比较小,可以从两个方面解决该问题:
1、在已知每个服务器的ip地址且ip是非动态的,可以通过ip指定哪台服务器执行。
1)在配置文件中指定ip地址
job.active.address=xx.xx.xx.xx
2)比对本地ip与配置ip
public boolean isActiveAddress() {
String activeAddress = ConfigManager.getInstance().getConfig("job.active.address");
String curAddress = null;
try {
curAddress = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
if(curAddress.equals(activeAddress)) {
return true;
}
return false;
}
2、可以使用shedlock
1)添加Maven坐标
<!-- 定时任务锁 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-core</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.5.0</version>
</dependency>
2)项目启动类中添加注解@EnableSchedulerLock(defaultLockAtMostFor = "120s")
3)注入bean
@Bean
//基于 Jdbc 的方式提供的锁机制
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
4)添加数据库配置
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3006/xx?useSSL=false&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useSSL=false
spring.datasource.username = xx
spring.datasource.password = xx
5)创建数据库
CREATE TABLE `shedlock` (
`NAME` varchar(64) NOT NULL DEFAULT '' COMMENT '任务名',
`lock_until` timestamp(3) NULL DEFAULT NULL COMMENT '释放时间',
`locked_at` timestamp(3) NULL DEFAULT NULL COMMENT '锁定时间',
`locked_by` varchar(255) DEFAULT NULL COMMENT '锁定实例',
PRIMARY KEY (`NAME`)
) ;
6)在定时任务方法上添加注解@SchedulerLock(name = "scheduleName")
其中,如果有多个定时任务,name要唯一。
本身项目比较大,服务器资源比较多的情况下,也可以考虑使用redis或者其他分布式任务调度框架。
1、spring Boot AOP+Redis,使用redis加锁解锁。任务执行完成后,设置过期时间并释放锁。
1)添加Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)配置redis信息
redis:
host: 127.0.0.1
port: 6379
password: xxx
3)RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisSerializer<Object> objectRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}
@Bean
public RedisSerializer<?> redisSerializer() {
return new StringRedisSerializer();
}
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(redisSerializer());
redisTemplate.setValueSerializer(redisSerializer());
redisTemplate.setHashKeySerializer(redisSerializer());
redisTemplate.setDefaultSerializer(redisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
}
4)自定义注解
@Retention(RUNTIME)
@Target(METHOD)
@Documented
public @interface ScheduleLock {
String lockedKey() default "";
long expireTime() default 100;//释放时间(s)
boolean release() default false; //是否在方法中释放锁
}
5)代理类
@Aspect
@Slf4j
@Component
public class ScheduleLockAspect {
@Resource
private RedisUtil redisUtil;
private static final String LOCK_KEY = "SCHEDULE_LOCK_ASPECT_";
@Around("@annotation(com.xkxx.biksh.start.aspect.ScheduleLock)")
public void scheduleLockPoint(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (null == method) {
log.info("为获取到使用方法:{}", point);
return;
}
String lockKey = method.getAnnotation(ScheduleLock.class).lockedKey();
Long timeOut = method.getAnnotation(ScheduleLock.class).expireTime();
boolean release = method.getAnnotation(ScheduleLock.class).release();
if (StringUtils.isBlank(lockKey)) {
log.info("method:{},锁的key值为空!", lockKey);
return;
}
try {
if (redisUtil.setnx(LOCK_KEY + lockKey, lockKey, timeOut)) {
redisUtil.expire(LOCK_KEY + lockKey, timeOut);
log.info("method:{} 获得锁:{},开始运行!", method, LOCK_KEY + lockKey);
point.proceed();
return;
}
log.info("method:{} 未获得锁:{},运行失败!", method, LOCK_KEY + lockKey);
release = false;
} catch (Throwable throwable) {
log.error("method:{},运行错误!", method, LOCK_KEY + lockKey);
} finally {
if (release) {
log.info("method:{} 执行完成释放锁:{}", method, LOCK_KEY + lockKey);
redisUtil.del(LOCK_KEY + lockKey);
}
}
}
}
6)redis工具类
@Configuration
public class RedisUtil {
@Autowired
public RedisTemplate redisTemplate;
/**
* 定时缓存
*
* @param key
* @param val
* @param expireTime
* @return
*/
public Boolean setnx(String key, String val, Long expireTime) {
return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
Boolean bool = connection.setNX(key.getBytes(), val.getBytes());
if (bool) {
return this.expire(key, expireTime);
}
Long expireTime1 = this.getEcpire(key);
if (expireTime1 == -1L) {
//过期时间为-1,删除缓存
this.del(key);
}
return false;
});
}
/**
* 指定缓存失效时间
*
* @param key
* @param time
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key
* @return
*/
public long getEcpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 删除缓存
*
* @param key
*/
public void del(String... key) {
if (null != key && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
}
7)在定时任务方法上添加注解@ScheduleLock(lockedKey = "scheduleName", expireTime = 600)
2、使用Quartz、xxl-job等框架