@Schedule定时任务在负载均衡下防止任务多次执行

发布于:2025-06-18 ⋅ 阅读:(21) ⋅ 点赞:(0)

使用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等框架


网站公告

今日签到

点亮在社区的每一天
去签到