🎓博主介绍:Java、Python、js全栈开发 “多面手”,精通多种编程语言和技术,痴迷于人工智能领域。秉持着对技术的热爱与执着,持续探索创新,愿在此分享交流和学习,与大家共进步。
📖DeepSeek-行业融合之万象视界(附实战案例详解100+)
📖全栈开发环境搭建运行攻略:多语言一站式指南(环境搭建+运行+调试+发布+保姆级详解)
👉感兴趣的可以先收藏起来,希望帮助更多的人
在这里插入图片描述

SpringBoot定时任务全解析:@Scheduled与分布式锁的深度结合

一、引言

在现代的软件开发中,定时任务是一种常见的需求。例如,每天凌晨对数据进行备份、每小时统计系统的业务指标等。Spring Boot 为我们提供了方便的定时任务解决方案,即 @Scheduled 注解。然而,在分布式系统中,多个节点同时执行定时任务可能会导致数据不一致等问题,这时就需要引入分布式锁来保证任务的正确执行。本文将深入解析 Spring Boot 中 @Scheduled 注解的使用,并详细介绍如何将其与分布式锁深度结合。

二、Spring Boot 定时任务基础:@Scheduled 注解

2.1 @Scheduled 注解简介

@Scheduled 是 Spring 框架提供的一个注解,用于标记一个方法为定时任务方法。该方法会按照指定的时间间隔或固定时间点执行。@Scheduled 注解可以用于 Spring 管理的 Bean 中的方法上,通常结合 @EnableScheduling 注解来开启 Spring 的定时任务功能。

2.2 @Scheduled 注解的常用属性

  • fixedRate:指定任务执行的固定时间间隔,单位为毫秒。例如,@Scheduled(fixedRate = 5000) 表示任务每隔 5 秒执行一次。
  • fixedDelay:指定任务执行完成后,下一次执行的延迟时间,单位为毫秒。例如,@Scheduled(fixedDelay = 3000) 表示任务执行完成后,等待 3 秒再执行下一次。
  • initialDelay:指定任务首次执行的延迟时间,单位为毫秒。通常与 fixedRatefixedDelay 一起使用。例如,@Scheduled(initialDelay = 2000, fixedRate = 5000) 表示任务在应用启动后延迟 2 秒开始执行,之后每隔 5 秒执行一次。
  • cron:使用 Cron 表达式来指定任务的执行时间。Cron 表达式是一个字符串,由 6 或 7 个字段组成,分别表示秒、分、时、日、月、周和年(可选)。例如,@Scheduled(cron = "0 0 2 * * ?") 表示任务每天凌晨 2 点执行。

2.3 @Scheduled 注解的使用示例

以下是一个简单的 Spring Boot 项目,演示了 @Scheduled 注解的使用:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledTasks {

    @Scheduled(fixedRate = 5000)
    public void fixedRateTask() {
        System.out.println("Fixed rate task executed at: " + System.currentTimeMillis());
    }

    @Scheduled(fixedDelay = 3000)
    public void fixedDelayTask() {
        System.out.println("Fixed delay task executed at: " + System.currentTimeMillis());
    }

    @Scheduled(initialDelay = 2000, fixedRate = 5000)
    public void initialDelayTask() {
        System.out.println("Initial delay task executed at: " + System.currentTimeMillis());
    }

    @Scheduled(cron = "0/10 * * * * ?")
    public void cronTask() {
        System.out.println("Cron task executed at: " + System.currentTimeMillis());
    }
}

在主应用类上添加 @EnableScheduling 注解来开启定时任务功能:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

三、分布式系统中定时任务的问题

在分布式系统中,多个节点可能会同时运行相同的定时任务,这会导致以下问题:

  • 数据不一致:多个节点同时对同一数据进行操作,可能会导致数据冲突和不一致。例如,多个节点同时对库存数据进行更新,可能会导致库存数量不准确。
  • 资源浪费:多个节点同时执行相同的任务,会浪费系统资源,增加系统的负载。

为了解决这些问题,我们需要引入分布式锁来保证在同一时间只有一个节点能够执行定时任务。

四、分布式锁概述

分布式锁是一种在分布式系统中实现互斥访问的机制,用于保证在同一时间只有一个节点能够访问共享资源。常见的分布式锁实现方式有以下几种:

  • 基于数据库:通过数据库的唯一索引或行锁来实现分布式锁。例如,在数据库中创建一个表,使用唯一索引来保证同一时间只有一个记录能够插入成功。
  • 基于 Redis:利用 Redis 的原子操作(如 SETNX)来实现分布式锁。Redis 是一个高性能的键值存储系统,适合用于实现分布式锁。
  • 基于 ZooKeeper:ZooKeeper 是一个分布式协调服务,通过其临时有序节点来实现分布式锁。ZooKeeper 可以保证分布式锁的可靠性和高可用性。

五、@Scheduled 与 Redis 分布式锁的结合

5.1 Redis 分布式锁的实现原理

Redis 分布式锁的基本原理是利用 Redis 的原子操作 SETNX(SET if Not eXists)来实现。SETNX 命令用于设置一个键值对,如果键不存在,则设置成功并返回 1;如果键已经存在,则设置失败并返回 0。通过 SETNX 命令可以保证在同一时间只有一个客户端能够获取到锁。

为了避免死锁,还需要给锁设置一个过期时间,当锁过期后会自动释放。可以使用 Redis 的 EXPIRE 命令来设置锁的过期时间。

5.2 Redis 分布式锁的代码实现

以下是一个使用 Redis 实现分布式锁的工具类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisLockUtil {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 获取分布式锁
     * @param key 锁的键
     * @param value 锁的值
     * @param expireTime 锁的过期时间
     * @param timeUnit 时间单位
     * @return 是否获取到锁
     */
    public boolean tryLock(String key, String value, long expireTime, TimeUnit timeUnit) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, timeUnit);
        return result != null && result;
    }

    /**
     * 释放分布式锁
     * @param key 锁的键
     */
    public void unlock(String key) {
        redisTemplate.delete(key);
    }
}

5.3 @Scheduled 与 Redis 分布式锁的结合示例

以下是一个将 @Scheduled 注解与 Redis 分布式锁结合的示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class DistributedScheduledTasks {

    @Autowired
    private RedisLockUtil redisLockUtil;

    private static final String LOCK_KEY = "distributed_task_lock";
    private static final String LOCK_VALUE = "lock_value";
    private static final long EXPIRE_TIME = 10;
    private static final java.util.concurrent.TimeUnit TIME_UNIT = java.util.concurrent.TimeUnit.SECONDS;

    @Scheduled(fixedRate = 5000)
    public void distributedTask() {
        if (redisLockUtil.tryLock(LOCK_KEY, LOCK_VALUE, EXPIRE_TIME, TIME_UNIT)) {
            try {
                // 执行定时任务的业务逻辑
                System.out.println("Distributed task executed at: " + System.currentTimeMillis());
            } finally {
                // 释放锁
                redisLockUtil.unlock(LOCK_KEY);
            }
        }
    }
}

六、@Scheduled 与 ZooKeeper 分布式锁的结合

6.1 ZooKeeper 分布式锁的实现原理

ZooKeeper 分布式锁的实现基于其临时有序节点的特性。当一个客户端需要获取锁时,会在 ZooKeeper 上创建一个临时有序节点。然后,客户端会检查自己创建的节点是否是所有节点中序号最小的节点,如果是,则表示获取到了锁;否则,客户端会监听比自己序号小的前一个节点的删除事件,当该节点被删除时,客户端会再次检查自己是否是最小节点,以此类推。

6.2 ZooKeeper 分布式锁的代码实现

以下是一个使用 ZooKeeper 实现分布式锁的工具类:

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

@Component
public class ZooKeeperLockUtil {

    private static final String ZOOKEEPER_CONNECTION_STRING = "localhost:2181";
    private static final int SESSION_TIMEOUT = 5000;
    private static final String LOCK_PATH = "/distributed_lock";
    private ZooKeeper zookeeper;
    private CountDownLatch connectedLatch = new CountDownLatch(1);

    public ZooKeeperLockUtil() {
        try {
            zookeeper = new ZooKeeper(ZOOKEEPER_CONNECTION_STRING, SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        connectedLatch.countDown();
                    }
                }
            });
            connectedLatch.await();
            // 创建父节点
            Stat stat = zookeeper.exists(LOCK_PATH, false);
            if (stat == null) {
                zookeeper.create(LOCK_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (IOException | InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取分布式锁
     * @return 锁的节点路径
     */
    public String tryLock() {
        try {
            // 创建临时有序节点
            String lockNode = zookeeper.create(LOCK_PATH + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            List<String> children = zookeeper.getChildren(LOCK_PATH, false);
            Collections.sort(children);
            String firstNode = children.get(0);
            if (lockNode.endsWith(firstNode)) {
                return lockNode;
            } else {
                String prevNode = LOCK_PATH + "/" + children.get(children.indexOf(lockNode.substring(LOCK_PATH.length() + 1)) - 1);
                CountDownLatch latch = new CountDownLatch(1);
                zookeeper.exists(prevNode, new Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        if (event.getType() == Event.EventType.NodeDeleted) {
                            latch.countDown();
                        }
                    }
                });
                latch.await();
                return lockNode;
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 释放分布式锁
     * @param lockNode 锁的节点路径
     */
    public void unlock(String lockNode) {
        try {
            zookeeper.delete(lockNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

6.3 @Scheduled 与 ZooKeeper 分布式锁的结合示例

以下是一个将 @Scheduled 注解与 ZooKeeper 分布式锁结合的示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ZooKeeperDistributedScheduledTasks {

    @Autowired
    private ZooKeeperLockUtil zooKeeperLockUtil;

    @Scheduled(fixedRate = 5000)
    public void distributedTask() {
        String lockNode = zooKeeperLockUtil.tryLock();
        if (lockNode != null) {
            try {
                // 执行定时任务的业务逻辑
                System.out.println("ZooKeeper distributed task executed at: " + System.currentTimeMillis());
            } finally {
                // 释放锁
                zooKeeperLockUtil.unlock(lockNode);
            }
        }
    }
}

七、总结

本文深入解析了 Spring Boot 中 @Scheduled 注解的使用,介绍了分布式系统中定时任务可能遇到的问题,并详细阐述了分布式锁的概念和常见实现方式。通过示例代码,展示了如何将 @Scheduled 注解与 Redis 和 ZooKeeper 分布式锁深度结合,以保证在分布式系统中定时任务的正确执行。

在实际开发中,需要根据具体的业务场景和系统需求选择合适的分布式锁实现方式。Redis 分布式锁适合对性能要求较高的场景,而 ZooKeeper 分布式锁适合对可靠性和一致性要求较高的场景。

Logo

更多推荐