java分布式定時(shí)任務(wù)實(shí)現(xiàn)細(xì)節(jié)
一、分布式鎖的底層實(shí)現(xiàn)細(xì)節(jié)(以 Redis 為例)
分布式鎖是解決任務(wù)重復(fù)執(zhí)行的核心,需保證原子性、超時(shí)釋放和可重入性。以下是生產(chǎn)級(jí) Redis 鎖實(shí)現(xiàn):
public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue; // 用于標(biāo)識(shí)鎖持有者(支持可重入)
private final long expireMillis; // 鎖過期時(shí)間(避免死鎖)
// 構(gòu)造函數(shù):初始化鎖參數(shù)
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate,
String lockKey, String requestId, long expireMillis) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockValue = requestId; // 建議使用UUID+線程ID
this.expireMillis = expireMillis;
}
// 嘗試獲取鎖(原子操作)
public boolean tryLock() {
// 使用Redis的SET命令實(shí)現(xiàn):NX(不存在則設(shè)置)+ PX(毫秒過期)
return redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
}
// 釋放鎖(需校驗(yàn)持有者,避免誤釋放)
public boolean unlock() {
// 使用Lua腳本保證刪除操作的原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
return result != null && result > 0;
}
// 帶超時(shí)等待的獲取鎖(輪詢重試)
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
long timeout = unit.toMillis(waitTime);
long start = System.currentTimeMillis();
while (true) {
if (tryLock()) {
return true;
}
// 等待重試(避免自旋過于頻繁)
long remaining = timeout - (System.currentTimeMillis() - start);
if (remaining <= 0) {
return false; // 超時(shí)未獲取到鎖
}
Thread.sleep(Math.min(remaining, 100)); // 最多等待100ms重試
}
}
}關(guān)鍵設(shè)計(jì)點(diǎn):
- 鎖標(biāo)識(shí)(lockValue):用 UUID + 線程 ID 區(qū)分持有者,避免釋放其他節(jié)點(diǎn)的鎖。
- 過期時(shí)間:需大于任務(wù)執(zhí)行時(shí)間(如任務(wù)耗時(shí) 5s,鎖過期設(shè) 10s),防止節(jié)點(diǎn)宕機(jī)導(dǎo)致鎖永久持有。
- 續(xù)約機(jī)制:若任務(wù)執(zhí)行時(shí)間可能超過鎖過期時(shí)間,需啟動(dòng)后臺(tái)線程定期續(xù)約(如每 3s 續(xù)期 10s)。
二、任務(wù)調(diào)度核心原理(以 XXL-Job 為例)
1. 調(diào)度中心與執(zhí)行器通信流程
- 執(zhí)行器注冊(cè):執(zhí)行器啟動(dòng)時(shí)通過 HTTP 請(qǐng)求向調(diào)度中心注冊(cè)(攜帶 appname、IP、端口)。
- 任務(wù)觸發(fā):調(diào)度中心根據(jù) CRON 表達(dá)式計(jì)算下次執(zhí)行時(shí)間,到達(dá)時(shí)間后通過線程池觸發(fā)任務(wù),向執(zhí)行器發(fā)送 HTTP 請(qǐng)求(POST 方式)。
- 執(zhí)行反饋:執(zhí)行器執(zhí)行完任務(wù)后,將結(jié)果(成功 / 失敗、日志)同步回調(diào)度中心。
2. 路由策略與負(fù)載均衡
XXL-Job 支持多種路由策略,解決任務(wù)在集群節(jié)點(diǎn)的分配問題:
- 第一個(gè)節(jié)點(diǎn):固定選擇集群中第一個(gè)在線節(jié)點(diǎn)(適合單節(jié)點(diǎn)執(zhí)行的任務(wù))。
- 輪詢:按順序依次分配給在線節(jié)點(diǎn)(均衡負(fù)載)。
- 分片廣播:所有在線節(jié)點(diǎn)同時(shí)執(zhí)行,每個(gè)節(jié)點(diǎn)處理不同分片(適合大規(guī)模任務(wù))。
分片示例:100 萬(wàn)條數(shù)據(jù)需批量處理,分為 5 個(gè)分片,集群 3 個(gè)節(jié)點(diǎn):
@XxlJob("shardingTask")
public ReturnT<String> shardingHandler(String param) {
// 獲取分片參數(shù)(由調(diào)度中心分配)
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
int shardIndex = shardingVO.getIndex(); // 當(dāng)前分片索引(0-4)
int shardTotal = shardingVO.getTotal(); // 總分片數(shù)(5)
// 按分片處理數(shù)據(jù)(如按ID取模:id % shardTotal == shardIndex)
List<Data> dataList = dataService.queryBySharding(shardIndex, shardTotal);
for (Data data : dataList) {
processData(data);
}
return ReturnT.SUCCESS;
}三、高可用設(shè)計(jì)(避免單點(diǎn)故障)
1. 調(diào)度中心集群化
- 部署方式:多實(shí)例部署(如 2 個(gè)節(jié)點(diǎn)),通過 Nginx 負(fù)載均衡對(duì)外提供服務(wù)。
- 數(shù)據(jù)一致性:依賴 MySQL 主從同步(調(diào)度中心數(shù)據(jù)存儲(chǔ)在 MySQL),確保多實(shí)例數(shù)據(jù)一致。
2. 執(zhí)行器故障轉(zhuǎn)移
- 心跳檢測(cè):執(zhí)行器定期向調(diào)度中心發(fā)送心跳(默認(rèn) 30s 一次),超過 90s 未心跳則標(biāo)記為離線。
- 任務(wù)轉(zhuǎn)移:若執(zhí)行器離線,調(diào)度中心會(huì)將其負(fù)責(zé)的任務(wù)分配給其他在線節(jié)點(diǎn)(需任務(wù)支持重執(zhí)行)。
四、監(jiān)控與告警體系
1. 核心監(jiān)控指標(biāo)
- 任務(wù)維度:執(zhí)行次數(shù)、成功率、平均耗時(shí)、最大耗時(shí)。
- 節(jié)點(diǎn)維度:CPU 使用率、內(nèi)存占用、任務(wù)并發(fā)數(shù)。
2. 集成 Prometheus 監(jiān)控
// 自定義任務(wù)執(zhí)行指標(biāo)(使用Micrometer)
@Component
public class TaskMetrics {
private final MeterRegistry meterRegistry;
public TaskMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
// 記錄任務(wù)執(zhí)行耗時(shí)
public void recordTaskDuration(String taskName, long durationMs) {
Timer.builder("task.execution.duration")
.tag("task", taskName)
.register(meterRegistry)
.record(durationMs, TimeUnit.MILLISECONDS);
}
// 記錄任務(wù)失敗次數(shù)
public void incrementFailCount(String taskName) {
Counter.builder("task.execution.fail")
.tag("task", taskName)
.register(meterRegistry)
.increment();
}
}在任務(wù)執(zhí)行中埋點(diǎn):
@XxlJob("orderTimeoutTask")
public ReturnT<String> orderTimeoutHandler(String param) {
long start = System.currentTimeMillis();
try {
// 任務(wù)邏輯...
metrics.recordTaskDuration("orderTimeoutTask", System.currentTimeMillis() - start);
return ReturnT.SUCCESS;
} catch (Exception e) {
metrics.incrementFailCount("orderTimeoutTask");
return ReturnT.FAIL;
}
}3. 告警配置
通過 Grafana 設(shè)置告警規(guī)則(如任務(wù)失敗率 > 5% 時(shí)觸發(fā)告警),并集成釘釘 / 企業(yè)微信機(jī)器人:
// 釘釘告警示例
public class DingTalkAlarm {
private final String webhook;
public void sendAlarm(String message) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("msgtype", "text");
body.put("text", Map.of("content", "定時(shí)任務(wù)告警:" + message));
new RestTemplate().postForObject(webhook, new HttpEntity<>(body, headers), String.class);
}
}五、自定義輕量級(jí)方案(無(wú)框架依賴)
若場(chǎng)景簡(jiǎn)單(如無(wú)動(dòng)態(tài)配置需求),可基于 Redis + 線程池實(shí)現(xiàn)極簡(jiǎn)方案:
@Component
public class RedisScheduledTask {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private TaskService taskService;
// 初始化定時(shí)任務(wù)(每分鐘執(zhí)行一次)
@PostConstruct
public void init() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(this::executeTask, 0, 1, TimeUnit.MINUTES);
}
// 執(zhí)行任務(wù)(加分布式鎖)
private void executeTask() {
String lockKey = "task:order:timeout";
String requestId = UUID.randomUUID().toString();
RedisDistributedLock lock = new RedisDistributedLock(redisTemplate, lockKey, requestId, 60000);
try {
if (lock.tryLock()) {
// 執(zhí)行核心邏輯
taskService.processTimeoutOrders();
} else {
log.info("任務(wù)被其他節(jié)點(diǎn)執(zhí)行,當(dāng)前節(jié)點(diǎn)跳過");
}
} finally {
lock.unlock(); // 釋放鎖
}
}
}六、避坑指南
- 鎖過期時(shí)間設(shè)置:需大于任務(wù)最大執(zhí)行時(shí)間(可通過壓測(cè)評(píng)估),避免任務(wù)未執(zhí)行完鎖已釋放。
- 任務(wù)冪等性:即使加了鎖,仍需保證任務(wù)可重復(fù)執(zhí)行(如使用
UPDATE orders SET status=1 WHERE id=? AND status=0)。 - 線程池隔離:核心任務(wù)與非核心任務(wù)使用獨(dú)立線程池(如
Executors.newScheduledThreadPool(5)),避免相互阻塞。 - 日志追蹤:任務(wù)執(zhí)行日志需包含唯一 ID(如訂單號(hào)),便于問題排查。
通過以上細(xì)節(jié)設(shè)計(jì),可構(gòu)建既高效又可靠的分布式定時(shí)任務(wù)系統(tǒng),兼顧性能、可用性和可運(yùn)維性。實(shí)際項(xiàng)目中,建議優(yōu)先選用 XXL-Job 等成熟框架,減少重復(fù)開發(fā);特殊場(chǎng)景下再考慮自定義方案。
到此這篇關(guān)于java分布式定時(shí)任務(wù)實(shí)現(xiàn)細(xì)節(jié)的文章就介紹到這了,更多相關(guān)java分布式定時(shí)任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java+Selenium調(diào)用JavaScript的方法詳解
這篇文章主要為大家講解了java在利用Selenium操作瀏覽器網(wǎng)站時(shí)候,有時(shí)會(huì)需要用的JavaScript的地方,代碼該如何實(shí)現(xiàn)呢?快跟隨小編一起學(xué)習(xí)一下吧2023-01-01
J2SE基礎(chǔ)之命令行中編寫第一個(gè) Hello World
“Hello World”程序指的是只在計(jì)算機(jī)屏幕上輸出“Hello, World!”(意為“世界,你好!”)這行字符串的計(jì)算機(jī)程序。hello world作為所有編程語(yǔ)言的起始階段,占據(jù)著無(wú)法改變的地位,所有的編程第一步就在于此了!經(jīng)典之中的經(jīng)典!hello world!2016-05-05
java 運(yùn)行報(bào)錯(cuò)has been compiled by a more recent version of the J
java 運(yùn)行報(bào)錯(cuò)has been compiled by a more recent version of the Java Runtime (class file version 54.0)2021-04-04
編寫android撥打電話apk應(yīng)用實(shí)例代碼
這篇文章主要介紹了編寫android撥打電話apk應(yīng)用實(shí)例代碼,十分的實(shí)用,這里分享給大家,有需要的小伙伴可以參考下2015-04-04
Java注解與反射實(shí)戰(zhàn)之實(shí)現(xiàn)自定義日志與參數(shù)校驗(yàn)注解
在Java開發(fā)中,注解(Annotation)是一種強(qiáng)大的元數(shù)據(jù)形式,它能夠?yàn)槌绦蛟兀ㄈ珙?、方法、變量等)提供附加信?這篇文章主要介紹了Java注解與反射實(shí)戰(zhàn)之實(shí)現(xiàn)自定義日志與參數(shù)校驗(yàn)注解的相關(guān)資料,需要的朋友可以參考下2026-01-01
解決@RequestMapping和@FeignClient放在同一個(gè)接口上遇到的坑
這篇文章主要介紹了解決@RequestMapping和@FeignClient放在同一個(gè)接口上遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
Eclipse?2022?設(shè)置中文漢化的超詳細(xì)圖文教程
這篇文章主要介紹了Eclipse?2022?設(shè)置中文漢化的超詳細(xì)圖文教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
SpringBoot結(jié)合Redis實(shí)現(xiàn)接口冪等性的示例代碼
本文主要介紹了SpringBoot結(jié)合Redis實(shí)現(xiàn)接口冪等性的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06

