使用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.1port: 6379password: xxx
3)RedisConfig
@Configuration
public class RedisConfig {@Beanpublic RedisSerializer<Object> objectRedisSerializer(){return new GenericFastJsonRedisSerializer();}@Beanpublic RedisSerializer<?> redisSerializer() {return new StringRedisSerializer();}@Beanpublic 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;}@Beanpublic 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 {@Resourceprivate 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 {@Autowiredpublic 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等框架