SpringBoot提供了 Schedule模块完美支持定时任务的执行
在实际开发中由于项目部署在分布式或集群服务器上 会导致定时任务多次触发
因此,使用redis分布锁机制可以有效避免多次执行定时任务
核心方法是org.springframework.data.redis.core包下的
setIfAbsent() 方法 返回值为布尔类型
方法类似redis的SETNX命令 即”SET if Not Exists”
服务器在执行邮件定时发送任务之前会向redis缓存中写入lock_key即任务锁 表明此服务器正在执行定时任务
另一台服务器在写入锁时 由于锁已经存在就不做任何操作
执行定时任务的服务器在执行完成后需释放任务锁
具体代码实现如下:
定义注解:
/**
* redis锁注解
* @author zhouzhou
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLock {
String lockPrefix() default "";
String lockKey() default "";
long timeOut() default 5;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
定义切面@Aspect, pointCut就是 RedisLock注解
/**
* Description: redis锁拦截器实现
* User: zhouzhou
* Date: 2018-09-05
* Time: 15:30
*/
@Aspect
@Component
public class RedisLockAspect {
private static final Integer MAX_RETRY_COUNT = 3;
private static final String LOCK_PRE_FIX = "lockPreFix";
private static final String LOCK_KEY = "lockKey";
private static final String TIME_OUT = "timeOut";
private static final int PROTECT_TIME = 2 << 11;//4096
private static final Logger log = LoggerFactory.getLogger(RedisLockAspect.class);
@Autowired
private CommonRedisHelper commonRedisHelper;
@Pointcut("@annotation(com.shuige.components.cache.annotation.RedisLock)")
public void redisLockAspect() {
}
@Around("redisLockAspect()")
public void lockAroundAction(ProceedingJoinPoint proceeding) throws Exception {
//获取redis锁
Boolean flag = this.getLock(proceeding, 0, System.currentTimeMillis());
if (flag) {
try {
proceeding.proceed();
Thread.sleep(PROTECT_TIME);
} catch (Throwable throwable) {
throw new RuntimeException("分布式锁执行发生异常" + throwable.getMessage(), throwable);
} finally {
// 删除锁
this.delLock(proceeding);
}
} else {
log.info("其他系统正在执行此项任务");
}
}
/**
* 获取锁
*
* @param proceeding
* @return
*/
private boolean getLock(ProceedingJoinPoint proceeding, int count, long currentTime) {
//获取注解中的参数
Map annotationArgs = this.getAnnotationArgs(proceeding);
String lockPrefix = (String) annotationArgs.get(LOCK_PRE_FIX);
String key = (String) annotationArgs.get(LOCK_KEY);
long expire = (long) annotationArgs.get(TIME_OUT);
//String key = this.getFirstArg(proceeding);
if (StringUtils.isEmpty(lockPrefix) || StringUtils.isEmpty(key)) {
// 此条执行不到
throw new RuntimeException("RedisLock,锁前缀,锁名未设置");
}
if (commonRedisHelper.setNx(lockPrefix, key, expire)) {
return true;
} else {
// 如果当前时间与锁的时间差, 大于保护时间,则强制删除锁(防止锁死)
long createTime = commonRedisHelper.getLockValue(lockPrefix, key);
if ((currentTime - createTime) > (expire * 1000 + PROTECT_TIME)) {
count ++;
if (count > MAX_RETRY_COUNT){
return false;
}
commonRedisHelper.delete(lockPrefix, key);
getLock(proceeding,count,currentTime);
}
return false;
}
}
/**
* 删除锁
*
* @param proceeding
*/
private void delLock(ProceedingJoinPoint proceeding) {
Map annotationArgs = this.getAnnotationArgs(proceeding);
String lockPrefix = (String) annotationArgs.get(LOCK_PRE_FIX);
String key = (String) annotationArgs.get(LOCK_KEY);
commonRedisHelper.delete(lockPrefix, key);
}
/**
* 获取锁参数
*
* @param proceeding
* @return
*/
private Map getAnnotationArgs(ProceedingJoinPoint proceeding) {
Class target = proceeding.getTarget().getClass();
Method[] methods = target.getMethods();
String methodName = proceeding.getSignature().getName();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Map result = new HashMap();
RedisLock redisLock = method.getAnnotation(RedisLock.class);
result.put(LOCK_PRE_FIX, redisLock.lockPrefix());
result.put(LOCK_KEY, redisLock.lockKey());
result.put(TIME_OUT, redisLock.timeUnit().toSeconds(redisLock.timeOut()));
return result;
}
}
return null;
}
/**
* 获取第一个String类型的参数为锁的业务参数
*
* @param proceeding
* @return
*/
@Deprecated
public String getFirstArg(ProceedingJoinPoint proceeding) {
Object[] args = proceeding.getArgs();
if (args != null && args.length > 0) {
for (Object object : args) {
String type = object.getClass().getName();
if ("java.lang.String".equals(type)) {
return (String) object;
}
}
}
return null;
}
}
CommonRedisHelper
/**
* Description:
* User: zhouzhou
* Date: 2018-09-05
* Time: 15:39
*/
@Component
public class CommonRedisHelper {
@Autowired
RedisTemplate redisTemplate;
/**
* 加分布式锁
*
* @param track
* @param sector
* @param timeout
* @return
*/
public boolean setNx(String track, String sector, long timeout) {
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean flag = valueOperations.setIfAbsent(track + sector, System.currentTimeMillis());
// 如果成功设置超时时间, 防止超时
if (flag) {
valueOperations.set(track + sector, getLockValue(track, sector), timeout, TimeUnit.SECONDS);
}
return flag;
}
/**
* 删除锁
*
* @param track
* @param sector
*/
public void delete(String track, String sector) {
redisTemplate.delete(track + sector);
}
/**
* 查询锁
* @return 写锁时间
*/
public long getLockValue(String track, String sector) {
ValueOperations valueOperations = redisTemplate.opsForValue();
long createTime = (long) valueOperations.get(track + sector);
return createTime;
}
}
使用场景:
@Scheduled(cron = "0,30 * * * * ? ")
@RedisLock(lockPrefix = "hello",lockKey = "world")
public void hello(){
System.out.println("每隔30秒定时任务测试,当前时间为:" + new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒").format(new Date()));
}