java
1. 添加依赖
首先确保项目中包含了Redisson依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.27.0</version>
</dependency>
2. Redisson配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host:localhost}")
private String redisHost;
@Value("${spring.redis.port:6379}")
private String redisPort;
@Value("${spring.redis.password:}")
private String redisPassword;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点配置(根据实际情况选择集群、哨兵等模式)
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(redisPassword.isEmpty() ? null : redisPassword)
.setDatabase(0)
.setConnectionPoolSize(64) // 连接池大小
.setConnectionMinimumIdleSize(10) // 最小空闲连接数
.setIdleConnectionTimeout(10000) // 连接空闲超时
.setConnectTimeout(10000) // 连接超时
.setTimeout(3000); // 命令等待超时
return Redisson.create(config);
}
}
更优雅的实现方式(推荐)
使用Redisson的tryLock方法,可以更简洁地实现:
@Service
@Slf4j
public class ElegantScheduledService {
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY = "SCHEDULED_TASK_LOCK";
@Scheduled(cron = "0 */2 * * * *") // 每2分钟执行
public void elegantDistributedTask() {
// 使用try-with-resources风格,自动管理锁的释放
RLock lock = redissonClient.getLock(LOCK_KEY);
try {
// 尝试获取锁,不等待,锁租期2分钟
if (lock.tryLock(0, 2, TimeUnit.MINUTES)) {
try {
executeBusinessLogic();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("任务执行被中断");
} catch (Exception e) {
log.error("定时任务执行异常", e);
}
}
private void executeBusinessLogic() {
log.info("正在执行分布式定时任务...");
// 你的业务逻辑
}
}
使用redis分布式锁, 节点1抢到了锁 一直执行, 如果节点1程序挂了, 持有的锁会自动释放吧, 节点2就可以获取到锁了吧
这是一个非常核心的分布式锁问题。答案是:不一定,这完全取决于节点1设置的锁是否具有"过期时间"。
下面我们分两种情况详细解释:
情况一:锁设置了过期时间(最佳实践)------ 会自动释放
这是正确使用Redis分布式锁的方式。
-
• 流程:
-
- 节点1 使用类似
SET lock_key unique_value NX PX 30000的命令获取锁。这个命令的含义是:如果键lock_key不存在(NX),就设置它,值为一个唯一标识(如UUID),并设置一个30秒的过期时间(PX 30000)。
- 节点1 使用类似
-
- 节点1 获取锁成功,开始执行业务逻辑。
-
- 在执行业务逻辑过程中,节点1 的服务器突然宕机。
-
- 由于锁有30秒的过期时间,在30秒后,这个锁会被Redis自动删除(释放)。
-
- 节点2 在尝试获取锁时,如果锁已自动释放,它就能成功获取到锁,并继续执行任务。
-
-
• 结论 :在这种场景下,即使持有锁的节点挂了,锁也会在过期时间后自动释放,避免了死锁,保证了系统的高可用性。这是你必须使用的方案。
情况二:锁没有设置过期时间 ------ 不会自动释放,导致死锁
这是一种非常错误且危险的使用方式。
-
• 流程:
-
- 节点1 使用一个简单的
SETNX lock_key命令获取锁,但没有设置过期时间。
- 节点1 使用一个简单的
-
- 节点1 获取锁成功,开始执行一个非常耗时的任务。
-
- 任务执行中,节点1 宕机。
-
- 因为这个锁没有过期时间,它会永久存在于Redis中。
-
- 节点2 再来尝试获取锁时,会发现锁一直被节点1 占用着,永远无法成功。这就是 死锁。整个系统会因为这把锁而瘫痪。
-
-
• 结论 :绝对不要使用不设置过期时间的分布式锁。
进阶话题:锁的续期(Watch Dog)
你可能会想到一个问题:如果业务逻辑的执行时间不确定,可能超过设置的过期时间怎么办?比如锁的过期时间是30秒,但业务逻辑执行了40秒。
-
• 问题:节点1还在正常运行,但锁在30秒时过期了。节点2在31秒时成功获取了锁。此时系统中就有两个节点同时持有同一把锁,导致数据错乱。
-
• 解决方案:锁续期(Watch Dog Mechanism)
成熟的分布式锁客户端(如Redisson)都实现了这个功能。其原理是:
-
- 节点1获取锁时,会设置一个过期时间(例如30秒)。
-
- 同时,客户端会启动一个后台线程(看门狗),这个线程会定期(比如在锁过期时间的1/3时,即10秒后)去检查节点1是否还持有锁。
-
- 如果节点1的业务逻辑还在执行(即客户端还活着),看门狗就会通过Redis命令重置锁的过期时间(重新设置为30秒),这就是 "续期"。
-
- 如果节点1挂了,看门狗线程也会随之停止,自然也就无法续期。锁最终还是会因为过期而被自动释放。
-
所以,在节点1挂掉的情况下:
-
• 如果使用了看门狗,看门狗线程停止,锁会在设定的过期时间后自动释放。
-
• 如果没有使用看门狗,但只要设置了过期时间,锁同样会在过期时间后自动释放。
总结
| 场景 | 锁是否设置过期时间 | 节点1挂掉后,锁是否自动释放 | 节点2能否获取锁 | 评价 |
|---|---|---|---|---|
| 正确用法 | 是 | 是(在过期时间后) | 是 | 推荐的最佳实践 |
| 错误用法 | 否 | 否(永久死锁) | 否 | 绝对要避免 |
| **高级用法(Redisson)** | **是(且支持自动续期)** | 是(在续期失败后) | 是 | 用于解决长事务问题,生产环境推荐 |
核心要点:
为了确保在持有锁的客户端崩溃时锁能被释放,你必须为锁设置一个过期时间。 对于Java开发者,强烈建议直接使用成熟的客户端如 Redisson,它已经帮你妥善地处理了这些复杂问题。
java
方案1:使用分布式锁控制单节点运行(推荐)
@Service
public class ZabbixDataCollectServiceImpl implements ZabbixDataCollectService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CDC_LOCK_KEY = "flink:cdc:zabbix:lock";
private volatile boolean isRunning = false;
/**
* 使用分布式锁确保只有一个节点运行
*/
@Override
@Async
public void start() {
// 尝试获取分布式锁
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(CDC_LOCK_KEY, "locked", Duration.ofMinutes(5));
if (lockAcquired == null || !lockAcquired) {
log.info("CDC任务已在其他节点运行,本节点跳过执行");
return;
}
try {
// 设置锁续期
startLockRenewal();
// 执行CDC任务
executeCdcJob();
} finally {
// 任务停止时释放锁
redisTemplate.delete(CDC_LOCK_KEY);
}
}
/**
* 锁续期线程
*/
private void startLockRenewal() {
Thread renewalThread = new Thread(() -> {
while (isRunning) {
try {
Thread.sleep(4 * 60 * 1000); // 每4分钟续期一次
redisTemplate.expire(CDC_LOCK_KEY, Duration.ofMinutes(5));
log.debug("CDC锁续期成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
renewalThread.setDaemon(true);
renewalThread.start();
}
private void executeCdcJob() {
isRunning = true;
try {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 原有的CDC配置代码...
MySqlSource<String> mySqlSource = MySqlSource.<String>builder()
.hostname(sourceDbUrl)
.port(sourceDbPort)
// ... 其他配置
.build();
DataStreamSource<String> source = env.fromSource(mySqlSource,
WatermarkStrategy.noWatermarks(), name);
source.sinkTo(sink);
env.execute();
} catch (Exception e) {
log.error("CDC任务执行异常", e);
} finally {
isRunning = false;
}
}
@PreDestroy
public void stop() {
isRunning = false;
redisTemplate.delete(CDC_LOCK_KEY);
}
}
在获取锁后,我们启动了一个续期线程,这个线程会定期延长锁的过期时间。这样在任务执行期间,锁不会因为过期而被释放,从而避免多个节点同时运行。
当节点停止时,我们在@PreDestroy方法中释放锁,并停止续期线程(通过设置isRunning为false,续期线程会退出循环)。
如果节点异常退出,没有执行@PreDestroy方法,那么锁会在5分钟(我们设置的过期时间)后自动释放,其他节点就可以获取锁并执行任务。