SpringBoot定時(shí)任務(wù)多實(shí)例互斥執(zhí)行的完整指南
Spring Boot 的 @Scheduled 寫(xiě)定時(shí)任務(wù)很方便,但多實(shí)例部署時(shí)有個(gè)問(wèn)題:同一個(gè)定時(shí)任務(wù)會(huì)在每臺(tái)機(jī)器上都觸發(fā)執(zhí)行。
比如部署了兩臺(tái)應(yīng)用服務(wù)器,凌晨 2 點(diǎn)的數(shù)據(jù)統(tǒng)計(jì)任務(wù)會(huì)同時(shí)跑兩遍,數(shù)據(jù)重復(fù)、文件重復(fù)生成。
解決這個(gè)問(wèn)題通常有幾種思路。
常見(jiàn)方案
方案一:?jiǎn)螜C(jī)執(zhí)行
只在一臺(tái)指定的機(jī)器上跑任務(wù):
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledTask() {
String hostname = InetAddress.getLocalHost().getHostName();
if (!"app-server-01".equals(hostname)) {
return;
}
// do something
}
問(wèn)題很明顯:那臺(tái)機(jī)器掛了,任務(wù)就不執(zhí)行了。
方案二:Redis 分布式鎖
用 Redis 的 SETNX 實(shí)現(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");
}
}
能用,但每個(gè)任務(wù)都要寫(xiě)一遍加鎖釋放邏輯,而且需要項(xiàng)目里有 Redis。
ShedLock 方案
ShedLock 是一個(gè)專門(mén)解決定時(shí)任務(wù)重復(fù)執(zhí)行問(wèn)題的框架,支持多種存儲(chǔ)后端(數(shù)據(jù)庫(kù)、Redis、MongoDB 等)。
核心思路:在存儲(chǔ)層記錄每個(gè)任務(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. 給定時(shí)任務(wù)加注解
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "dataSyncTask", lockAtMostFor = "5m")
public void syncData() {
// do something
}
完成。多臺(tái)服務(wù)器部署時(shí),只有搶到鎖的那臺(tái)會(huì)執(zhí)行任務(wù)。
注解參數(shù)說(shuō)明
@SchedulerLock 有幾個(gè)關(guān)鍵參數(shù):
name:鎖名稱,相同 name 的任務(wù)會(huì)互斥執(zhí)行。建議用任務(wù)名來(lái)命名,保持唯一。
lockAtMostFor:鎖的最大持有時(shí)間。
這是為了防止任務(wù)執(zhí)行過(guò)程中機(jī)器宕機(jī),導(dǎo)致鎖永遠(yuǎn)不釋放。比如設(shè)置 5m,即使任務(wù)執(zhí)行超時(shí),鎖也會(huì)在 5 分鐘后自動(dòng)過(guò)期。
一般按任務(wù)預(yù)期執(zhí)行時(shí)間的 2 倍左右設(shè)置,留些余量。
lockAtLeastFor:鎖的最小持有時(shí)間。
這是為了防止任務(wù)執(zhí)行太快,鎖立即釋放被其他機(jī)器搶到。
比如定時(shí)任務(wù)每分鐘執(zhí)行一次,任務(wù) 5 秒就跑完了。如果沒(méi)有這個(gè)參數(shù),其他機(jī)器可能會(huì)在同一分鐘內(nèi)再次搶到鎖執(zhí)行。
這種情況下可以設(shè)置 lockAtLeastFor = "1m",確保鎖保持到下一分鐘。
實(shí)現(xiàn)原理
ShedLock 的實(shí)現(xiàn)邏輯不復(fù)雜。
任務(wù)執(zhí)行前,會(huì)向數(shù)據(jù)庫(kù)插入或更新鎖記錄:
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)前鎖已過(guò)期,UPDATE 成功,搶到鎖
- 如果當(dāng)前鎖未過(guò)期,UPDATE 影響行數(shù)為 0,搶鎖失敗,任務(wù)不執(zhí)行
任務(wù)執(zhí)行完成后不需要主動(dòng)釋放鎖,等待 lock_until 時(shí)間到期即可。
適用場(chǎng)景
ShedLock 的定位很明確:專門(mén)為定時(shí)任務(wù)設(shè)計(jì)的分布式鎖框架。
適合
- 定時(shí)任務(wù)需要互斥執(zhí)行,避免重復(fù)
- 希望用注解方式簡(jiǎn)化鎖的代碼邏輯
- 需要自動(dòng)鎖過(guò)期機(jī)制,防止死鎖
不適合
- 高并發(fā)搶鎖的業(yè)務(wù)場(chǎng)景(比如秒殺、庫(kù)存扣減),ShedLock 不是為此設(shè)計(jì)
- 需要可重入鎖、讀寫(xiě)鎖等復(fù)雜特性
- 需要精確控制鎖獲取和釋放時(shí)機(jī)的業(yè)務(wù)邏輯
ShedLock 和通用分布式鎖是互補(bǔ)關(guān)系,不是替代關(guān)系。如果你的業(yè)務(wù)代碼里需要手動(dòng)加鎖解鎖,用 Redisson 或手動(dòng)實(shí)現(xiàn) Redis SETNX 更合適。但如果只是為了解決定時(shí)任務(wù)重復(fù)執(zhí)行的問(wèn)題,ShedLock 是更簡(jiǎn)潔的方案。
幾個(gè)注意事項(xiàng)
1. 存儲(chǔ)后端選擇
本文演示用的是 JDBC 方式(基于數(shù)據(jù)庫(kù)表)。ShedLock 還支持 Redis、MongoDB、ZooKeeper、Hazelcast 等多種存儲(chǔ)后端,根據(jù)項(xiàng)目現(xiàn)有技術(shù)棧選擇即可。
2. 表名自定義(數(shù)據(jù)庫(kù)存儲(chǔ)時(shí))
如果用 JDBC 作為存儲(chǔ)后端,默認(rèn)表名是 shedlock,可以按需修改: 3. 機(jī)器標(biāo)識(shí)
locked_by 字段記錄是哪臺(tái)機(jī)器拿到的鎖,默認(rèn)是主機(jī)名??梢宰远x成更有意義的標(biāo)識(shí):
.withLockedByValue("app-" + getServerIp())
4. 主從/多數(shù)據(jù)源場(chǎng)景(數(shù)據(jù)庫(kù)存儲(chǔ)時(shí))
如果項(xiàng)目有多個(gè)數(shù)據(jù)源,確保 ShedLock 用的是主庫(kù),避免主從延遲導(dǎo)致的鎖問(wèn)題。
總結(jié)
ShedLock 是一個(gè)專門(mén)為定時(shí)任務(wù)設(shè)計(jì)的分布式鎖框架:
- 優(yōu)點(diǎn):注解式使用、集成簡(jiǎn)單、自動(dòng)鎖過(guò)期、支持多種存儲(chǔ)后端
- 局限性:只適用于定時(shí)任務(wù)場(chǎng)景,不適用于通用業(yè)務(wù)加鎖
和手動(dòng)寫(xiě) Redis 分布式鎖相比,ShedLock 把定時(shí)任務(wù)鎖的邏輯抽象出來(lái)了,代碼更簡(jiǎn)潔。但如果你需要的是通用業(yè)務(wù)鎖,還是用 Redisson 或手寫(xiě) SETNX 更合適。
以上就是SpringBoot定時(shí)任務(wù)多實(shí)例互斥執(zhí)行的完整指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot定時(shí)任務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot定時(shí)任務(wù)實(shí)現(xiàn)數(shù)據(jù)庫(kù)數(shù)據(jù)同步全過(guò)程
- SpringBoot文章定時(shí)發(fā)布的技術(shù)方案詳解(三種延遲任務(wù)方案)
- SpringBoot中使用定時(shí)任務(wù)schedule詳解
- springboot根據(jù)啟動(dòng)文件關(guān)閉定時(shí)任務(wù)的解決方法
- Springboot使用Redis實(shí)現(xiàn)定時(shí)任務(wù)的三種方式
- SpringBoot項(xiàng)目如何使用多線程執(zhí)行定時(shí)任務(wù)
相關(guān)文章
Jedis出現(xiàn)connection timeout問(wèn)題解決方法(JedisPool連接池使用實(shí)例)
這篇文章主要介紹了Jedis出現(xiàn)connection timeout問(wèn)題解決方法,使用Jedis的JedisPool連接池解決了這個(gè)問(wèn)題,需要的朋友可以參考下2014-05-05
springboot集成mybatisPlus+多數(shù)據(jù)源的實(shí)現(xiàn)示例
這篇文章主要介紹了springboot集成mybatisPlus+多數(shù)據(jù)源的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
通過(guò)Java實(shí)現(xiàn)反向代理集群服務(wù)的平滑分配
這篇文章主要介紹了如何通過(guò)Java語(yǔ)言,自己編寫(xiě)的平滑加權(quán)輪詢算法,結(jié)合線程池和Socket?網(wǎng)絡(luò)編程等,并實(shí)現(xiàn)反向代理集群服務(wù)的平滑分配,需要的可以參考一下2022-04-04
Springboot中如何通過(guò)yml為實(shí)體類注入屬性
這篇文章主要介紹了Springboot中如何通過(guò)yml為實(shí)體類注入屬性,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
idea?maven依賴引入失效無(wú)法正常導(dǎo)入依賴問(wèn)題的解決方法
有時(shí)候idea導(dǎo)入一個(gè)新項(xiàng)目,或者pom文件修改(新增)了依賴,pom文件和代碼會(huì)報(bào)紅,提示依賴包不存在,下面這篇文章主要給大家介紹了關(guān)于idea?maven依賴引入失效無(wú)法正常導(dǎo)入依賴問(wèn)題的解決方法,需要的朋友可以參考下2023-04-04

