服務器采用了負載均衡,有兩台服務器,部署的代碼一樣,所以里面的定時任務在某一時間會被同時執行,這就導致了很多其他意外的發生,想要解決的問題基本就三個:單點執行,故障轉移,服務狀態。這里對比一下網上找的幾種方案:
方案一:固定執行定時任務的機器
方法:在多台機器中選擇一台執行定時任務,每次執行的時候回判斷當前機器和指定的機器是否一致或者啟動時就指定好執行機器。
優缺點:這種方法是可以有效避免多次執行的情況,但是最明顯的缺點就是單點故障問題,如果你指定的機器出現了宕機,任務就不會執行了,業務邏輯就會奔潰。
有下面常見 2 種處理方案:
1、方式一:只在一台服務器上部署該定時任務代碼。
2、方式二:在定時任務代碼上加上某個特定的ip限制,僅某個ip的服務器能運行該定時任務
優點:解決方法容易理解,部署簡單,不需要多套代碼。 缺點:同上,只能規定一台服務器運行,發送故障時就沒辦法了。
常見面試題:SpringCloud架構中如何保證定時任務只有一個服務在執行?
有時候我們在開發過程中,很容易犯這樣一個錯誤,就是在服務中寫一個定時任務,然后也沒做其它的處理就上線了。然后微服務架構為了保證高可用,一般都會部署多個服務,這個時候問題就來了,時間一到定時任務一啟動,發現你部署多少個服務,就會跑多少個定時任務。如果服務器性能一般,定時任務占用內存又多,服務器跑死都有可能。
- 第一步先獲取當前服務ip
- 第二步獲取springcloud集群ip信息
- 最后將當前ip和集群的ip進行對比,如果當前ip是集群中最小的ip則執行定時任務業務,如果不是則return掉。
具體代碼邏輯可以看這篇文章:https://blog.csdn.net/linzhiqiang0316/article/details/88047138
方案二:利用數據庫的共享鎖事務管理機制來運行定時任務
原理:由於MySQL存在表鎖和行鎖,每次執行定時任務的時候從數據庫表中讀取記錄,只有讀取到的記錄標識當前任務狀態為未執行時,當前機器才會去觸發任務,並且更新數據庫狀態(先更新,再執行),由於存在表鎖和行鎖,因此同一時刻只能有一個事務操作,可以保證只執行一次。
方法:在數據庫中新建一張表 - 定時任務表,存儲了上次執行定時任務的 ip地址(ip),任務名稱(task_name),是否正在執行(execute)。集群中的所有服務器都是走以下流程:
1、第一步:查找數據庫的定時任務表。
2、第二步:檢查是否有機器在運行定時任務。
檢查方法:update定時任務表的excute字段為1(1為執行中,0為未執行)、ip為自己的ip,如果update失敗,則證明有機器在執行該定時任務,該機器的定時任務就不執行了,成功則進行第三步。
3、第三步:執行定時任務的具體內容。
4、第四步:還原excute字段為0。
以上是該方案的流程,利用了 mysql 的共享鎖機制判斷,通過是否更新成功來判斷是否有機器正在執行定時任務,這種方案可以保證任務只執行一次,且只要集群中有一台服務器是好的,就會執行任務。方案挺好,暫時想不到有啥缺點,可能增加了數據庫的負擔算一個吧。
方案三:利用 redis 數據庫
原理:和第三種差不多,只是通過 redis 的 key-value 來存儲 任務名-執行ip。
執行定時任務前先查詢 redis 是否有改任務的值,沒有就自己 執行,並插入新的 key-value。有的話就查看ip是否是自己,是的話就執行,不是的話就證明有其他機器在執行,自己就不執行啦。
過期時間可以自己設置,方便有機器出故障時候可以轉移機器執行任務。
優點:利用了redis的自動過期機制實現了轉移故障機器的問題,比較簡單,而且redis的訪問速度也很快。
缺點:這里沒有事務管理機制,訪問redis的時候,一定會出現高並發的情況,所以得自己實現redis的共享鎖機制。
1、通過redis實現任務調度思路
實現功能之前,回顧下之前遇到的三個問題:單點執行,故障轉移,服務狀態。結合着redis的一些接口特性,解決思路如下:
(1)使用redis作為任務調度中心,采用了redis的自動過期與分布式鎖特性
(2)每個服務的ip加項目名作為每台服務的唯一別名
(3)通過redis中對應key值中的value來判定執行的是哪台服務: 如redis中key為 schedular_root:projectA, value為192.168.1.187. 意為項目projectA當前執行任務的節點為192.168.1.187這台機器上的服務
(4)每次執行任務之前判定下redis中schedular_root:projectA是否為空,如果為空,則設置當前ip進去,設置一定時間的有效期,並執行定時任務;如果不為空,判斷是否與本機ip相同,相同則執行定時任務,否則跳過
(5)設置有效期是為了某台機器發生故障時能進行故障轉移
2、核心流程代碼
此解決方案非常簡單,核心代碼也十分容易集成,為了減少耦合度,我們采用了spring的aop進行實現。
// 核心 AOP 實現
@Aspect @Component @Log4j public class QuartzAop { public boolean checkStatus(){ String key = "schedular_root:projectA"; try { // 這個接口必然是並發的,所以加分布式鎖
while (true) { // 一秒的超時時間
boolean lock = RedisUtil.checkLock(key,1); if (lock) { // 獲取到鎖,才能跳出
break; } } String ip = InetAddress.getLocalHost().getHostAddress(); // 獲取服務器上的工作ip
String currentIp = RedisUtil.get(key); // 如果為空的時候,設置進去
if(currentIp == null){ RedisUtil.setex(key, ip, 10); return true; } // 就是當前機器,則返回true
if(currentIp.equals(ip)){ return true; }else{ return false; } } catch (Exception e) { log.error(e); return false; } finally { RedisUtil.unLock(key); } } @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)") public void around(ProceedingJoinPoint jp) throws Throwable{ if(checkStatus()){ String ip = InetAddress.getLocalHost().getHostAddress(); log.info("現在正在執行"+jp.getSignature()+":"+ip); jp.proceed(); } } } // RedisUtil中的鎖代碼
public static boolean checkLock(String key,int second) { String lockKey = "lock:" + key; try { // 1表示之前不存在,設置成功
if (setnx(lockKey, "lock") == 1) { // 設置有限期
setExpiredTime(lockKey, second); return true; } else { // 50毫秒的延遲,避免過多請求
try { Thread.sleep(50L); } catch (InterruptedException e) { log.error(e); } return false; } } catch (RedisException e) { log.error(e); return true; } }
方案四、利用分布式框架
1、ShedLock解決多節點集群定時任務並發
ShedLock適用場景:保證一個定時任務在多個服務實例之間最多只執行一次。配置相對來說最簡單,對TaskName加鎖的方式來實現,只需要數據庫中新建一張表記錄相關鎖信息。詳細可看這篇文章:https://blog.csdn.net/qq_43530309/article/details/109588488
2、Quartz的集群應用方式
3、Elastic Job:當當網開源的一個分布式調度解決方案,在業界比較通用。