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
相关产品推荐
相关产品推荐

