分散式環境下利用快取解決重複性問題

擁抱心中的夢想發表於2018-06-11

今天討論一個關於分散式叢集中如何防止重複操作的問題,重複操作在單機應用就顯得很重要了,何況是在分散式系統中。小編舉個例子,我們首先模擬這麼一個場景,假如我們有一個只有2臺機器的小叢集,每臺機器上面部署了同一個應用服務系統,每個系統中定義了1個相同的定時任務(我們假設它是-----在每天23點執行對同一個資料庫某個操作),因為是在叢集環境中,我們怎麼保證這兩個完全相同的定時任務只執行其中一個呢?

我們想象最壞的一種情況,那就是兩個定時任務都執行了,結果必然影響相應的業務,下面我們假設是兩個定時任務:

task

@Component
@EnableScheduling
public class Task implements SchedulingConfigurer {
    private static String cron = "0 0/1 * * * ?";
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
        // 此處不加防重複操作,兩臺機器必然執行兩次該核心業務
        payRecordSvc.handleProcessRecord();
        } catch (Exception e) {
            LOGGER.error("orderRecordExpiredJob error!", e);
        }
        }, (triggerContext) -> {
            // 任務觸發,可修改任務的執行週期  
            if (StringUtils.isNotBlank(lifecSysConfig.getPayRecordJobQueryCron())) {
                cron = Config.getPayRecordJobQueryCron();
            }
            CronTrigger trigger = new CronTrigger(cron);
            Date nextExec = trigger.nextExecutionTime(triggerContext);
            return nextExec;
        });
    }
}
複製程式碼

既然是分散式系統,我們可以使用某種共享資源來實現併發控制,於是乎codis出現了,codis是redis的分散式版本,當然下面講的使用它來防止重複操作,採用redis也是可行的。當第一臺機器執行時,我們在redis中插入某個鍵值,然後執行定時任務,當第二臺機器準備執行定時任務時,我們可以判斷redis中是否已經存在該鍵,如果存在,則不執行定時任務。

看下面改進程式碼:

@Component
@EnableScheduling
public class PayRecordTradeQueryTask implements SchedulingConfigurer {

    private static String cron = "0 0/1 * * * ?";

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
            try {
                // 防止多臺伺服器重複處理
                boolean isRepeated = RedisAvoidRepeatUtils.repeatOpreat(LifecCommonCst.ORDER_PAY_QUERY_JOB_REPEATED_KEY, "1", jedisPool, 10);
                if (!isRepeated) {
                    payRecordSvc.handleProcessRecord();
                }
            } catch (Exception e) {
                LOGGER.error("orderRecordExpiredJob error!", e);
            }
        }, (triggerContext) -> {
            // 任務觸發,可修改任務的執行週期  
            if (StringUtils.isNotBlank(lifecSysConfig.getPayRecordJobQueryCron())) {
                cron = lifecSysConfig.getPayRecordJobQueryCron();
            }
            CronTrigger trigger = new CronTrigger(cron);
            Date nextExec = trigger.nextExecutionTime(triggerContext);
            return nextExec;
        });
    }
}
複製程式碼

我們進入repeatOpreat()方法看看怎麼實現:

/**
 * 是否重複操作
 * true  :重複操作
 * false : 沒有重複操作
 */
public static boolean repeatOpreat(String key, String value, JedisResourcePool jedisPool, int time) {
    Jedis jedis = null;
    try {
        jedis = getJedis(jedisPool);
        // 這句話的意思是當key存在時,則返回null,否則插入該鍵值,並返回OK
        String status = jedis.set(key, value, "NX", "EX", time);
        LOG.info("key = " + key + "; value = " + value +"; jedis status ->" +  status);
        if ("OK".equalsIgnoreCase(status)) {
            return false;
        }
        return true;
    } catch (Exception e) {
        LOG.error("repeatOpreat faild", e);
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return false;
}
複製程式碼

注意jedis提供了很多個版本的set方法,我們應該使用注入上面的方法,其中引數值NX指示redis,當key存在時,則返回null,否則插入該鍵值,並返回OK,否則當你重複插入相同鍵值時,都會返回OK。這裡的原理是使用到了redis的分散式鎖,同理我們也可以使用setNX命令。

最後,其實利用快取實現防止重複操作的用途非常廣泛,不一定只適用於分散式定時任務的防重,具體問題具體分析!

相關文章