使用 Redis 实现分布式锁来控制分布式定时任务,只有其中一个节点执行

使用 Redis 实现分布式锁来控制定时任务的唯一执行,需要借助 Redis 的原子性和键过期机制,手动实现分布式锁。Redis 本身提供了 SETNX(SET if Not eXists)命令来实现分布式锁的核心原理,同时结合过期时间和自动释放机制,确保锁能够按预期释放,防止死锁。

以下是具体实现方案的步骤和代码示例:


1. 使用 Redis 实现分布式锁的核心思路

  1. 获取锁:每个任务执行前,通过向 Redis 写入一个键值对来表示锁状态。使用 SET 命令,确保只有一个实例可以成功写入(锁键不存在时才写入),并设置锁的过期时间。
  2. 锁续约:根据任务的预估执行时间,设置合适的锁过期时间,以防止任务执行超时导致锁意外释放。
  3. 任务完成后释放锁:任务完成后,显式删除 Redis 锁键,允许其他实例获取锁。

Redis 可以确保 SET 命令的原子性操作,结合过期时间,能够实现分布式环境下的高效锁管理。

2. 环境准备

引入 Maven 依赖

确保项目中包含 Redis 的 Spring Boot 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 Redis 连接

application.properties 中添加 Redis 的连接信息:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=your_password # 如果没有密码可以省略

3. Redis 分布式锁工具类

创建一个 RedisLockUtil 工具类来实现分布式锁的核心逻辑。使用 SETNX 命令设置锁,并设定锁的过期时间,确保锁在任务执行结束时能自动释放。

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

import java.util.concurrent.TimeUnit;

@Component
public class RedisLockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 尝试获取锁
    public boolean tryLock(String lockKey, String value, long expireTime) {
        // 只有在 key 不存在时才设置 key,并设定过期时间
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success); // 返回 true 表示获取锁成功
    }

    // 释放锁
    public void releaseLock(String lockKey, String value) {
        // 释放锁时要确保操作的安全性,通过 Lua 脚本验证锁的持有者是否是自己
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                           "return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute((connection, keySerializer, valueSerializer) ->
            connection.eval(luaScript.getBytes(),
                            ReturnType.INTEGER,
                            1,
                            keySerializer.serialize(lockKey),
                            valueSerializer.serialize(value)));
    }
}
  • tryLock:尝试获取锁。setIfAbsent 实现原子性操作,即只有当 lockKey 不存在时才能写入并设置过期时间。expireTime 是锁的自动释放时间,防止因任务异常退出而导致锁被长期持有。
  • releaseLock:释放锁。使用 Lua 脚本来确保锁的安全释放,避免其他任务或实例误删锁。

4. 定时任务类

在定时任务执行时,通过 tryLock 方法尝试获取锁,并在任务执行完成后调用 releaseLock 方法释放锁。

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

import java.util.UUID;

@Component
public class ScheduledTask {

    private static final String LOCK_KEY = "unique_task_lock";
    private static final long LOCK_EXPIRE_TIME = 300; // 锁过期时间,单位:秒 (例如 5 分钟)

    @Autowired
    private RedisLockUtil redisLockUtil;

    @Scheduled(cron = "0 0 * * * *") // 每小时执行一次
    public void executeTask() {
        // 唯一的锁标识,使用 UUID 防止多个节点混淆
        String lockValue = UUID.randomUUID().toString();

        // 尝试获取锁
        boolean isLocked = redisLockUtil.tryLock(LOCK_KEY, lockValue, LOCK_EXPIRE_TIME);
        if (!isLocked) {
            System.out.println("Another instance is executing the task.");
            return; // 获取锁失败,另一个实例正在执行任务
        }

        try {
            // 执行任务逻辑
            System.out.println("Executing scheduled task with Redis lock");
            // 任务逻辑代码...

        } finally {
            // 任务执行完成后释放锁
            redisLockUtil.releaseLock(LOCK_KEY, lockValue);
        }
    }
}
  • LOCK_KEY:指定锁的 Redis 键,确保该键在集群中唯一。可以根据任务名称设置。
  • LOCK_EXPIRE_TIME:锁的过期时间,确保锁的有效性和防止死锁。
  • lockValue:每次尝试获取锁时生成唯一的标识符(使用 UUID),确保释放锁时只删除当前实例持有的锁。
  • isLockedtryLock 返回 true 表示当前实例成功获取锁,可以执行任务;返回 false 则表示其他实例已持有锁,当前任务不执行。

5. 启用 Spring 定时任务

确保定时任务功能已启用,可以在应用主类或配置类中添加 @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);
    }
}

6. 分布式锁实现的安全性和稳定性

Redis 分布式锁的实现依赖于过期时间的设置,但在某些场景下,锁过期时间可能需要动态调整,以保证长时间执行的任务不会因锁过期而中断。

  • 安全释放锁:通过 Lua 脚本检查 lockKey 的值是否等于 lockValue,确保锁由当前任务释放,避免其他实例误删锁。
  • 锁过期时间LOCK_EXPIRE_TIME 应该设置为任务执行时间的上限,防止任务因超时而释放锁。若任务可能超时,可以考虑实现锁续期机制,定时刷新锁的过期时间(需要额外的线程支持)。

总结

使用 Redis 的分布式锁可以轻松实现跨节点的任务同步控制。Redis 原子操作和键过期机制的结合,既保证了锁的唯一性,又避免了分布式任务中的资源冲突问题。

posted @ 2024-11-04 23:53  gongchengship  阅读(127)  评论(0)    收藏  举报