You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot多实例场景下如何实现数据库操作同步避免定时任务重复更新

分布式场景下Spring Boot定时任务重复执行解决方案

问题核心原因

你当前使用的synchronized是JVM级别的锁,只能在单个实例内生效,多实例部署属于独立进程,锁不共享,加上你原有校验逻辑是「先查询再判断」的非原子操作,两个实例同时触发定时任务时会同时拿到「未更新」的查询结果,最终导致重复执行。

最优实现方案(无需额外引入中间件,依托现有JPA+数据库)

方案1:使用ShedLock组件(开箱即用,无需手写锁逻辑)

ShedLock是专门用于解决Spring定时任务多实例重复执行的组件,直接适配你当前的JPA环境。

步骤1:引入依赖

<!-- Maven依赖 -->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>5.10.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jpa</artifactId>
    <version>5.10.0</version>
</dependency>

步骤2:添加锁表

在数据库中创建ShedLock所需的锁表:

CREATE TABLE shedlock(
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP(3) NULL,
    locked_at TIMESTAMP(3) NULL,
    locked_by VARCHAR(255) NULL,
    PRIMARY KEY (name)
);

步骤3:添加配置

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m") // 默认锁最长持有10分钟,避免服务挂死导致锁永久不释放
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(EntityManagerFactory entityManagerFactory) {
        return new JpaLockProvider(entityManagerFactory);
    }
}

步骤4:修改定时任务代码

@Scheduled(cron = "0 0 * * * ?", zone = "Europe/Paris")
@SchedulerLock(name = "updateServersTask", lockAtLeastFor = "5m") // 任务名全局唯一,锁至少持有5分钟,避免任务执行完后其他实例重复执行
public void updateServers() {
    applicationServerRepository.findAll().forEach(this::updateServerInfo);
}

配置完成后只有一个实例能拿到锁执行任务,其他实例会直接跳过当前周期的任务执行,符合你的需求。

方案2:手写数据库悲观锁实现

如果不想引入第三方组件,可以自己基于JPA悲观锁实现分布式锁:

步骤1:创建分布式锁实体

@Entity
@Table(name = "distributed_lock")
public class DistributedLock {
    @Id
    private String lockKey; // 锁唯一标识,这里填"update_servers_task"
    private String lockedBy;
    private LocalDateTime lockExpireTime;
    //  补充getter、setter方法
}

步骤2:添加锁查询方法

public interface DistributedLockRepository extends JpaRepository<DistributedLock, String> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT l FROM DistributedLock l WHERE l.lockKey = :key")
    Optional<DistributedLock> findByLockKeyForWrite(String key);
}

步骤3:修改定时任务逻辑

@Scheduled(cron = "0 0 * * * ?", zone = "Europe/Paris")
@Transactional
public void updateServers() {
    // 尝试获取锁,悲观锁会阻塞其他实例的查询,直到当前事务提交释放锁
    Optional<DistributedLock> lockOpt = distributedLockRepository.findByLockKeyForWrite("update_servers_task");
    if (lockOpt.isEmpty()) {
        // 首次使用先初始化锁记录即可
        return;
    }
    DistributedLock lock = lockOpt.get();
    // 判断锁是否过期
    if (lock.getLockExpireTime().isAfter(LocalDateTime.now())) {
        // 锁已被其他实例持有且未过期,直接跳过
        return;
    }
    // 更新锁信息,设置过期时间为当前时间+10分钟
    lock.setLockedBy(UUID.randomUUID().toString());
    lock.setLockExpireTime(LocalDateTime.now().plusMinutes(10));
    distributedLockRepository.save(lock);
    // 执行业务逻辑
    applicationServerRepository.findAll().forEach(this::updateServerInfo);
    // 任务执行完提前释放锁
    lock.setLockExpireTime(LocalDateTime.now());
    distributedLockRepository.save(lock);
}

额外优化点

如果你的原有更新逻辑还是先查询再判断再更新,依然可能存在原子性问题,可以给ApplicationServer实体类添加乐观锁字段避免单条记录重复更新:

@Entity
public class ApplicationServer {
    // 其他原有字段
    @Version
    private Integer version;
}

添加后JPA执行更新时会自动带上version条件,只有版本匹配才会更新成功,就算极端情况出现重复执行的情况也不会导致数据异常。


内容的提问来源于stack exchange,提问作者korallo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 07:54:01