使用 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号