SpringBoot定時任務(wù)多實例互斥執(zhí)行的完整指南
Spring Boot 的 @Scheduled 寫定時任務(wù)很方便,但多實例部署時有個問題:同一個定時任務(wù)會在每臺機器上都觸發(fā)執(zhí)行。
比如部署了兩臺應(yīng)用服務(wù)器,凌晨 2 點的數(shù)據(jù)統(tǒng)計任務(wù)會同時跑兩遍,數(shù)據(jù)重復(fù)、文件重復(fù)生成。
解決這個問題通常有幾種思路。
常見方案
方案一:單機執(zhí)行
只在一臺指定的機器上跑任務(wù):
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
String hostname = InetAddress.getLocalHost().getHostName();
if (!"app-server-01".equals(hostname)) {
return;
}
// do something
}
問題很明顯:那臺機器掛了,任務(wù)就不執(zhí)行了。
方案二:Redis 分布式鎖
用 Redis 的 SETNX 實現(xiàn)互斥(或者Redission):
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent("task:data-sync", "1", 10, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(locked)) {
return;
}
try {
// do something
} finally {
stringRedisTemplate.delete("task:data-sync");
}
}
能用,但每個任務(wù)都要寫一遍加鎖釋放邏輯,而且需要項目里有 Redis。
ShedLock 方案
ShedLock 是一個專門解決定時任務(wù)重復(fù)執(zhí)行問題的框架,支持多種存儲后端(數(shù)據(jù)庫、Redis、MongoDB 等)。
核心思路:在存儲層記錄每個任務(wù)的鎖狀態(tài),任務(wù)執(zhí)行前先搶鎖,搶到了才執(zhí)行。
集成步驟
1. 添加依賴
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.42.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.42.0</version>
</dependency>
2. 創(chuàng)建鎖表
CREATE TABLE shedlock (
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL,
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
3. 配置 LockProvider
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withDataSource(dataSource)
.withTableName("shedlock")
.build()
);
}
}
4. 給定時任務(wù)加注解
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "dataSyncTask", lockAtMostFor = "5m")
public void syncData() {
// do something
}
完成。多臺服務(wù)器部署時,只有搶到鎖的那臺會執(zhí)行任務(wù)。
注解參數(shù)說明
@SchedulerLock 有幾個關(guān)鍵參數(shù):
name:鎖名稱,相同 name 的任務(wù)會互斥執(zhí)行。建議用任務(wù)名來命名,保持唯一。
lockAtMostFor:鎖的最大持有時間。
這是為了防止任務(wù)執(zhí)行過程中機器宕機,導(dǎo)致鎖永遠不釋放。比如設(shè)置 5m,即使任務(wù)執(zhí)行超時,鎖也會在 5 分鐘后自動過期。
一般按任務(wù)預(yù)期執(zhí)行時間的 2 倍左右設(shè)置,留些余量。
lockAtLeastFor:鎖的最小持有時間。
這是為了防止任務(wù)執(zhí)行太快,鎖立即釋放被其他機器搶到。
比如定時任務(wù)每分鐘執(zhí)行一次,任務(wù) 5 秒就跑完了。如果沒有這個參數(shù),其他機器可能會在同一分鐘內(nèi)再次搶到鎖執(zhí)行。
這種情況下可以設(shè)置 lockAtLeastFor = "1m",確保鎖保持到下一分鐘。
實現(xiàn)原理
ShedLock 的實現(xiàn)邏輯不復(fù)雜。
任務(wù)執(zhí)行前,會向數(shù)據(jù)庫插入或更新鎖記錄:
INSERT INTO shedlock (name, lock_until, locked_at, locked_by)
VALUES ('dataSyncTask', '2025-01-25 02:05:00', NOW(), '192.168.1.10')
ON DUPLICATE KEY UPDATE
lock_until = '2025-01-25 02:05:00',
locked_at = NOW(),
locked_by = '192.168.1.10'
WHERE lock_until < NOW();
關(guān)鍵是最后的 WHERE lock_until < NOW() 條件:
- 如果當(dāng)前鎖已過期,UPDATE 成功,搶到鎖
- 如果當(dāng)前鎖未過期,UPDATE 影響行數(shù)為 0,搶鎖失敗,任務(wù)不執(zhí)行
任務(wù)執(zhí)行完成后不需要主動釋放鎖,等待 lock_until 時間到期即可。
適用場景
ShedLock 的定位很明確:專門為定時任務(wù)設(shè)計的分布式鎖框架。
適合
- 定時任務(wù)需要互斥執(zhí)行,避免重復(fù)
- 希望用注解方式簡化鎖的代碼邏輯
- 需要自動鎖過期機制,防止死鎖
不適合
- 高并發(fā)搶鎖的業(yè)務(wù)場景(比如秒殺、庫存扣減),ShedLock 不是為此設(shè)計
- 需要可重入鎖、讀寫鎖等復(fù)雜特性
- 需要精確控制鎖獲取和釋放時機的業(yè)務(wù)邏輯
ShedLock 和通用分布式鎖是互補關(guān)系,不是替代關(guān)系。如果你的業(yè)務(wù)代碼里需要手動加鎖解鎖,用 Redisson 或手動實現(xiàn) Redis SETNX 更合適。但如果只是為了解決定時任務(wù)重復(fù)執(zhí)行的問題,ShedLock 是更簡潔的方案。
幾個注意事項
1. 存儲后端選擇
本文演示用的是 JDBC 方式(基于數(shù)據(jù)庫表)。ShedLock 還支持 Redis、MongoDB、ZooKeeper、Hazelcast 等多種存儲后端,根據(jù)項目現(xiàn)有技術(shù)棧選擇即可。
2. 表名自定義(數(shù)據(jù)庫存儲時)
如果用 JDBC 作為存儲后端,默認表名是 shedlock,可以按需修改: 3. 機器標(biāo)識
locked_by 字段記錄是哪臺機器拿到的鎖,默認是主機名??梢宰远x成更有意義的標(biāo)識:
.withLockedByValue("app-" + getServerIp())
4. 主從/多數(shù)據(jù)源場景(數(shù)據(jù)庫存儲時)
如果項目有多個數(shù)據(jù)源,確保 ShedLock 用的是主庫,避免主從延遲導(dǎo)致的鎖問題。
總結(jié)
ShedLock 是一個專門為定時任務(wù)設(shè)計的分布式鎖框架:
- 優(yōu)點:注解式使用、集成簡單、自動鎖過期、支持多種存儲后端
- 局限性:只適用于定時任務(wù)場景,不適用于通用業(yè)務(wù)加鎖
和手動寫 Redis 分布式鎖相比,ShedLock 把定時任務(wù)鎖的邏輯抽象出來了,代碼更簡潔。但如果你需要的是通用業(yè)務(wù)鎖,還是用 Redisson 或手寫 SETNX 更合適。
以上就是SpringBoot定時任務(wù)多實例互斥執(zhí)行的完整指南的詳細內(nèi)容,更多關(guān)于SpringBoot定時任務(wù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot集成Redis消息隊列的實現(xiàn)示例
本文主要介紹了SpringBoot集成Redis消息隊列的實現(xiàn)示例,包括配置和消費邏輯,RedisStream提供了高吞吐量、順序消費和消費組機制等優(yōu)勢,具有一定的參考價值,感興趣的可以了解一下2025-05-05
java實現(xiàn)從網(wǎng)絡(luò)下載多個文件
這篇文章主要為大家詳細介紹了java實現(xiàn)從網(wǎng)絡(luò)下載多個文件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07
Java實現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫的方法示例
這篇文章主要介紹了Java實現(xiàn)Excel導(dǎo)入導(dǎo)出數(shù)據(jù)庫的方法,結(jié)合實例形式分析了java針對Excel的讀寫及數(shù)據(jù)庫操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-08-08

