Spring Boot 整合 ShedLock 处理定时任务重复执行的问题

前言

在分布式系统中,定时任务的执行往往需要考虑到多个实例的并发执行问题。假设一个定时任务会在多个节点上并发执行,可能导致重复执行,甚至引发数据异常或系统不一致问题。为了解决这一问题,ShedLock 是一个简单而有效的解决方案,它可以确保在分布式环境中,只有一个节点在某一时刻执行指定的定时任务

什么是 ShedLock?

ShedLock 是一个轻量级的 Java 库,用于解决分布式系统中定时任务的重复执行问题。它的核心思想是在数据库中加锁,确保在分布式环境下,只有一个节点能够在指定时间执行某个任务。ShedLock 可以与 Spring Scheduler、Quartz 等定时任务框架结合使用。

github开源地址:https://github.com/lukas-krecan/ShedLock 目前拥有 3.7K Star,小伙伴们也可以针对文档更系统性的学习如何应用ShedLock。

ShedLock 的工作原理:

  • 定时任务执行时,ShedLock 会尝试在数据库中为该任务获取锁。
  • 如果当前节点成功获取到锁,则执行定时任务。
  • 如果当前节点未能获取到锁(其他节点已获取锁),则该任务跳过执行,等待下次调度。

定时任务重复执行的问题

在分布式应用中,定时任务往往通过多个实例运行,每个实例都会按照自己的调度计划执行任务。例如,假设有一个定时任务负责同步某个外部系统的数据。如果该任务在多个实例上同时执行,就会导致重复的数据同步,进而造成数据不一致、重复处理等问题。

典型场景

我们来模拟一个场景,假设在一个电商系统中,定时任务负责将库存数据同步到第三方库存管理系统。如果该任务在多个节点上同时执行,可能会导致:

1、重复的库存更新请求。

2、数据不同步或数据丢失。

3、系统负载过高,造成性能瓶颈。

使用 ShedLock 解决定时任务重复执行问题

接下来,将通过 Spring Boot 和 ShedLock 来演示如何解决定时任务的重复执行问题。希望能给大家一个参考!

添加依赖

首先确保我们的的 pom.xml 中包含必要的依赖。需要添加 Spring Boot Starter、Spring Scheduling 和 ShedLock 相关的依赖。

java 复制代码
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Boot Scheduler -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-scheduled</artifactId>
    </dependency>
    
    <!-- ShedLock Core -->
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-spring</artifactId>
        <version>6.2.0</version>
    </dependency>

    <!-- ShedLock JDBC -->
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-jdbc-template</artifactId>
        <version>6.2.0</version>
    </dependency>
    
    <!-- MySQL JDBC Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

对应版本兼容性依赖:

配置数据库

ShedLock 需要在数据库中存储锁的信息。这里博主使用 MySQL 来存储锁。首先,创建一个名为 shedlock 的表来存储锁数据。

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

该表的字段意义如下:

● name: 锁的名称,用于区分不同的任务。

● lock_until: 锁的过期时间,任务完成后应释放锁。

● locked_at: 锁的创建时间。

● locked_by: 锁被哪个节点持有。

配置 ShedLock

接下来,我们在 Spring Boot 配置类中进行 ShedLock 的配置。以下是一个基本的配置类:

java 复制代码
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "30m") //默认锁最大过期时间为 30 秒
public class ShedLockConfig {
}

注解说明:

@EnableSchedulerLock: 启用 ShedLock 功能,defaultLockAtMostFor 参数指定锁的最大过期时间。如果任务执行超过此时间,锁将会被释放。

@EnableScheduling: 启用 Spring 的调度功能。

创建定时任务

接下来我们创建一个定时任务,并为其添加 ShedLock 锁,确保在分布式环境中只有一个实例会执行该任务。

java 复制代码
package com.example;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class DataSyncService {

    /**
     * 使用 @SchedulerLock 注解来确保该任务只会在一个节点上执行
     */
    @Scheduled(fixedRate = 5000)  // 每 5 秒执行一次
    @SchedulerLock(name = "syncInventoryTask", lockAtMostFor = "5m", lockAtLeastFor = "5m")
    public void syncInventory() {
        System.out.println("同步库存数据...");
        // 模拟库存同步操作
        try {
            Thread.sleep(3000);  // 模拟执行耗时 3 秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("库存同步完成!");
    }
}

上述代码中,我们在在 syncInventory 方法上,我们使用了 @SchedulerLock 注解,指定锁的名称 syncInventoryTask,并设置锁的最大持续时间为 5 秒。

  • name: 锁的名称,确保每个定时任务有唯一的名称。
  • lockAtMostFor: 锁的最大过期时间,防止任务因为异常而无法释放锁。
  • lockAtLeastFor: 锁的最小持续时间,确保锁至少保持一定时间。

配置数据库连接

最后 application.properties 或 application.yml 配置我们的数据库连接信息

java 复制代码
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
相关推荐
Maynor99610 分钟前
OpenClaw 玩家必备:用 AI 自动追踪社区最新动态
java·服务器·人工智能
堕27415 分钟前
java数据结构当中的《排序》(一 )
java·数据结构·排序算法
亓才孓30 分钟前
[Class的应用]获取类的信息
java·开发语言
开开心心就好37 分钟前
AI人声伴奏分离工具,离线提取伴奏K歌用
java·linux·开发语言·网络·人工智能·电脑·blender
80530单词突击赢1 小时前
JavaWeb进阶:SpringBoot核心与Bean管理
java·spring boot·后端
爬山算法1 小时前
Hibernate(87)如何在安全测试中使用Hibernate?
java·后端·hibernate
云姜.1 小时前
线程和进程的关系
java·linux·jvm
是码龙不是码农1 小时前
支付防重复下单|5 种幂等性设计方案(从初级到架构级)
java·架构·幂等性
曹牧1 小时前
Spring Boot:如何在Java Controller中处理POST请求?
java·开发语言
heartbeat..1 小时前
JVM 性能调优流程实战:从开发规范到生产应急排查
java·运维·jvm·性能优化·设计规范