今天討論一個關於分散式叢集中如何防止重複操作
的問題,重複操作在單機應用就顯得很重要了,何況是在分散式系統中。小編舉個例子,我們首先模擬這麼一個場景,假如我們有一個只有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
命令。
最後,其實利用快取實現防止重複操作的用途非常廣泛,不一定只適用於分散式定時任務的防重,具體問題具體分析!