摘要:
事情是這樣的前兩周在做項目的時候碰到一個需求---要求每天晚上執行一個任務,公司統一使用的是 xxl-job 寫定時任務的,我當時為了方便自己,然后就簡單的使用了Spring的那個@Scheduled來定時,當時寫完覺得這也太方便了吧,以后我就只使用這個方法定時了,方便又快捷,用什么 xxl-job 呢(要什么自行車呢😂),哈哈。
需求:
要求在當天11:58的時候需要生成當天任務的報告,然后在第二天12:10得時候生成第二天得任務,-----本來我使用了兩個@Scheduled注解,想用兩個定時任務來做的,結果生成任務的那一個定時任務居然不起作用。。。然后剛好起作用的這個任務在不起作用的那個之前,所以我就在第一個任務執行時發消息到MQ,使用MQ的延時隊列來做了。
一、第一版代碼
@Scheduled(cron = "0 58 23 */1 * ?")
@Override
public void createReprotTimer() {
// 發送到 MQ 12分鍾后(00:10)--- 生成第二天的任務
String activeProfile = SpringUtil.getActiveProfile();
String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
producerUtil.sendTimeMsg(
"TID_COMMON",
tag,
"生成任務".getBytes(),
"CREATE_TASK",
System.currentTimeMillis() + 720 * 1000
);
// 每天晚上 11:58 生成報告
this.createReprot();
}
是不是賊簡單,哈哈,一切看着沒什么毛病,當然在測試環境測試時也沒有任何問題,但是后面上了生產問題就暴露出來了
到生產發現的問題:測試環境一切正常,在生產環境一直會出現報告和任務重復生成的情況
原因:我們測試環境只有一台服務器,所以這個執行完全沒毛病,但是到了正式環境時每個服務都是以集群的形式部署的,當時我這個服務部署在兩台服務器上,所以每天到了11:58的時候兩台服務器都會檢測到我的定時,所以它們會執行兩次。
二、第二版代碼
方案:基於這個問題我立刻就想到了跨服務器肯定的使用第三方工具了,於是就使用了Redis來解決。
思路:執行這個方法的時候,先判斷Redis中是否存在我存放的指定的業務Key,如果里面已經存在了這個Key,那么就說明已經生成過任務--執行過這個方法了,那么就直接跳過,如果判斷里面沒有Key---說明今天還沒有執行過定時任務,則直接執行,然后再把Key壓到Redis中並且定時一分鍾后過期。
@Scheduled(cron = "0 58 23 */1 * ?")
@Override
public void createReprotTimer() {
if (!redisUtil.hasKey("CREATE_TASK_TIME")) {
// // 發送到 MQ 12分鍾后(00:10)--- 生成第二天的任務
String activeProfile = SpringUtil.getActiveProfile();
String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
producerUtil.sendTimeMsg(
"TID_COMMON",
tag,
"生成任務".getBytes(),
"CREATE_TASK",
System.currentTimeMillis() + 720 * 1000
);
// 每天晚上 11:58 生成報告
this.createReprot();
redisUtil.setEx("CREATE_TASK_TIME", "CREATE_TASK_TIME", 1, TimeUnit.MINUTES);
}
}
第二版代碼出現的問題:不起作用,依然會重復執行🤡
分析:貌似代碼沒問題,思路也沒問題了。。。
原因:看似代碼沒任何問題----當然對於一般業務性質的問題是沒有太大的問題的,但是我們這個場景是定時場景,那就意味着兩台服務器肯定是同一時刻執行到這段代碼的----他們同時判斷Redis中是否有這個業務Key,那么這個它們肯定得到的結果就是Redis中沒有這個Key,它們兩個就會執行這段代碼,導致生成重復的任務;
解決方法:問題原因找到了,那么也就意味着問題已經幾乎解決了,很明顯,這里只需要加一個分布式鎖就可以了,保證在這個時間點上這段代碼是順序執行的就可以了。
三、第三版代碼
分布式鎖我這里使用的是Redisson,用法很簡單,開箱即用--------算了寫一下吧
1、引入依賴:
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
2、業務代碼:
@Scheduled(cron = "0 58 23 */1 * ?")
@Override
public void createReprotTimer() {
// 冪等處理,防止生成重復報告和發送重復MQ消息, 加鎖:防止兩台服務同時執行這段代碼
RLock lock = redissonClient.getLock("CREATE_REPORT");
lock.lock(1, TimeUnit.MINUTES);
try {
if (!redisUtil.hasKey("CREATE_TASK_TIME")) {
// // 發送到 MQ 12分鍾后(00:10)--- 生成第二天的任務
String activeProfile = SpringUtil.getActiveProfile();
String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
producerUtil.sendTimeMsg(
"TID_COMMON",
tag,
"生成任務".getBytes(),
"CREATE_TASK",
System.currentTimeMillis() + 720 * 1000
);
// 每天晚上 11:58 生成報告
this.createReprot();
redisUtil.setEx("CREATE_TASK_TIME", "CREATE_TASK_TIME", 1, TimeUnit.MINUTES);
}
} catch (Exception e) {
throw new ServiceException(OrderExceptionEnum.ORDER_FILE_CANCEL);
} finally {
lock.unlock();
}
}
稍微再解釋一下吧:首先我們將這一段代碼鎖住,並且設置鎖的超時時間為1分鍾,防止出現所無法釋放的情況,然后執行時去Redis中獲取這個唯一的業務Key,如果沒有就直接執行這個代碼,並且執行完成之后將Key壓入Redis中;如果已經有了我們要找的這個唯一的業務Key,那么就直接跳過即可。
完美解決---又是一個愉快的周末