使用 Redis 实现分布式锁来控制分布式定时任务,只有其中一个节点执行
使用 Redis 实现分布式锁来控制定时任务的唯一执行,需要借助 Redis 的原子性和键过期机制,手动实现分布式锁。Redis 本身提供了 SETNX(SET if Not eXists)命令来实现分布式锁的核心原理,同时结合过期时间和自动释放机制,确保锁能够按预期释放,防止死锁。
以下是具体实现方案的步骤和代码示例:
1. 使用 Redis 实现分布式锁的核心思路
- 获取锁:每个任务执行前,通过向 Redis 写入一个键值对来表示锁状态。使用 
SET命令,确保只有一个实例可以成功写入(锁键不存在时才写入),并设置锁的过期时间。 - 锁续约:根据任务的预估执行时间,设置合适的锁过期时间,以防止任务执行超时导致锁意外释放。
 - 任务完成后释放锁:任务完成后,显式删除 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),确保释放锁时只删除当前实例持有的锁。
 - isLocked:
tryLock返回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 原子操作和键过期机制的结合,既保证了锁的唯一性,又避免了分布式任务中的资源冲突问题。
                    
                
                
            
        
浙公网安备 33010602011771号