【Java】SpringCloud架構系統中如何保證集羣環境下定時任務同時只有一個實例運行工作?

問題

首先說下情況,我們平常開發SpringCloud微服務的時候,若要確保高可用,同一服務都會部署多臺實例,然後註冊到Eureka上。

一般我們會把所有定時任務寫到一個服務裏,那平常單實例的時候,都可以正常執行。如果該定時任務服務部署多個實例,如何確保只在一個服務實例裏執行任務呢?

個人總結了下,可以有以下解決思路。

解決

1. 如果原有的task代碼同時執行一次或多次的結果都是正確的,那麼可以就不做任何處理,只不過會造成資源浪費,當然這種方式不推薦。

比如圖裏的taskA是每隔五分鐘更新下已完成的工單的記錄爲歸檔,那其實本質就是執行一個update的sql,即使是同時執行mysql數據庫也有鎖機制,所以數據不會出錯。那多實例下也可以不做處理,不過再說一遍,雖然結果不會出錯,但是不推薦這樣做,還是要處理一下的。

2. 使用分佈式鎖

藉助分佈式鎖,確保多個實例裏的task只有競爭到鎖的實例任務才執行。比如,redis的分不式鎖。這種方式不好的地方是需要修改邏輯代碼,增加了對redis的依賴。

3. 使用任務調度框架的集羣功能

之前我使用的是Quartz,Quartz是一個完全由Java編寫的開源作業調度框架,Quartz的集羣功能通過故障轉移和負載平衡功能爲您的調度程序帶來高可用性和可擴展性。這種方式也有不好的地方,那就是要實現Quartz的集羣功能,需要修改Quartz的配置,而且是要額外增加Quartz集羣需要的數據庫表,如果一開始開發沒有考慮集羣,後面再加入改動會有點大。

4. 最小ip執行

這應該算是一個思路,我也是在網上看到的,具體實現是。

①先在代碼裏獲取獲取到當前實例的ip,通過一定算法規則轉成一個Long型。
②從Eureka里根據實例名,圖裏的Taks-Server獲取到對應的集羣ip集合,也就是192.168.2.10、192.168.2.11、192.168.2.12,也分別把它們轉成對應的Long型。
③拿第一步裏的Long和第二步獲取的Long的List做對比,如果判斷到它是集合裏最小的,那就在該實例裏執行task,否則就retur掉。

這樣就能確保永遠只有一個實例執行定時任務了。

5. elastic-job

elastic-job 是由噹噹網基於quartz 二次開發之後的分佈式調度解決方案,所以本質和方法3一樣。

總結

如果你的系統架構剛開始,那就可以把任務的分佈式集羣情況考慮進去,使用集羣框架實現,如果你的系統已經完成或趨於穩定,則不建議大改,可以考慮方法2的分佈式鎖或者方法3。

最小ip算法:

@Slf4j
@Component
public class IpService {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * Eureka 客戶端
     */
    private EurekaClient eurekaClient;


    /**
     * 比對當前ip及eureka上的其他ip
     * 判斷當前ip是否是最小ip
     *
     * @param serviceName
     */
    public Boolean canRun(String serviceName) {
        //獲取 eureka 客戶端
        this.eurekaClient = this.applicationContext.getBean(DiscoveryClient.class);

        //獲取當前服務註冊的 eureka 上的全部實例
        Applications applications = this.eurekaClient.getApplications();
        if (applications == null || applications.size() <= 0) {
            log.info("未能從註冊中心找到其它服務");
            return false;
        }

        //若實例不爲空則獲取全部實例的集合
        List<Application> applicationList = applications.getRegisteredApplications();

        if (CollUtil.isNotEmpty(applicationList)) {
            List<String> urlList = new ArrayList<>();
            for (Application application : applicationList) {
                if (application.getName().startsWith(serviceName)) {
                    List<InstanceInfo> instances = application.getInstances();
                    instances.forEach(instanceInfo -> urlList.add(instanceInfo.getIPAddr()));
                }
            }
            return ipCompare(urlList);
        } else {
            return false;
        }
    }

    /**
     * 對比方法
     *
     * @param serviceUrl
     * @return
     */
    private static boolean ipCompare(List<String> serviceUrl) {
        try {
            String localIpStr = getIpAddress();
            assert localIpStr != null;
            long localIpLong = ipToLong(localIpStr);
            int size = serviceUrl.size();
            if (size == 0) {
                return false;
            }

            Long[] longHost = new Long[size];
            for (int i = 0; i < serviceUrl.size(); i++) {
                longHost[i] = ipToLong(serviceUrl.get(i));
            }
            Arrays.sort(longHost);
            if (localIpLong == longHost[0]) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 獲取當前機器的IP
     *
     * @return
     */
    private static String getIpAddress() {
        try {
            for (Enumeration<NetworkInterface> enumNic = NetworkInterface.getNetworkInterfaces();
                 enumNic.hasMoreElements(); ) {
                NetworkInterface ifc = enumNic.nextElement();
                if (ifc.isUp()) {
                    for (Enumeration<InetAddress> enumAddr = ifc.getInetAddresses();
                         enumAddr.hasMoreElements(); ) {
                        InetAddress address = enumAddr.nextElement();
                        if (address instanceof Inet4Address && !address.isLoopbackAddress()) {
                            return address.getHostAddress();
                        }
                    }
                }
            }
            return InetAddress.getLocalHost().getHostAddress();
        } catch (IOException e) {
            //log.warn("Unable to find non-loopback address", e);
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @param ipAddress
     * @return
     */
    private static long ipToLong(String ipAddress) {
        long result = 0;
        String[] ipAddressInArray = ipAddress.split("\\.");
        for (int i = 3; i >= 0; i--) {
            long ip = Long.parseLong(ipAddressInArray[3 - i]);
            // left shifting 24,16,8,0 and bitwise OR
            // 1. 192 << 24
            // 1. 168 << 16
            // 1. 1 << 8
            // 1. 2 << 0
            result |= ip << (i * 8);
        }
        return result;
    }

}

 

 

參考:https://www.cnblogs.com/shamo89/p/12269338.html

      https://blog.csdn.net/linzhiqiang0316/article/details/88047138

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章