redis分布式锁 多节点部署项目 Redisson 来做分布式锁

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. 节点1 使用类似 SET lock_key unique_value NX PX 30000的命令获取锁。这个命令的含义是:如果键 lock_key不存在(NX),就设置它,值为一个唯一标识(如UUID),并设置一个30秒的过期时间(PX 30000)。
      1. 节点1 获取锁成功,开始执行业务逻辑。
      1. 在执行业务逻辑过程中,节点1 的服务器突然宕机。
      1. 由于锁有30秒的过期时间,在30秒后,这个锁会被Redis自动删除(释放)。
      1. 节点2 在尝试获取锁时,如果锁已自动释放,它就能成功获取到锁,并继续执行任务。
  • 结论 :在这种场景下,即使持有锁的节点挂了,锁也会在过期时间后自动释放,避免了死锁,保证了系统的高可用性。这是你必须使用的方案。


情况二:锁没有设置过期时间 ------ 不会自动释放,导致死锁

这是一种非常错误且危险的使用方式。

  • 流程

      1. 节点1 使用一个简单的 SETNX lock_key命令获取锁,但没有设置过期时间。
      1. 节点1 获取锁成功,开始执行一个非常耗时的任务。
      1. 任务执行中,节点1 宕机。
      1. 因为这个锁没有过期时间,它会永久存在于Redis中。
      1. 节点2 再来尝试获取锁时,会发现锁一直被节点1 占用着,永远无法成功。这就是 死锁。整个系统会因为这把锁而瘫痪。
  • 结论绝对不要使用不设置过期时间的分布式锁。


进阶话题:锁的续期(Watch Dog)

你可能会想到一个问题:如果业务逻辑的执行时间不确定,可能超过设置的过期时间怎么办?比如锁的过期时间是30秒,但业务逻辑执行了40秒。

  • 问题:节点1还在正常运行,但锁在30秒时过期了。节点2在31秒时成功获取了锁。此时系统中就有两个节点同时持有同一把锁,导致数据错乱。

  • 解决方案:锁续期(Watch Dog Mechanism)

    成熟的分布式锁客户端(如Redisson)都实现了这个功能。其原理是:

      1. 节点1获取锁时,会设置一个过期时间(例如30秒)。
      1. 同时,客户端会启动一个后台线程(看门狗),这个线程会定期(比如在锁过期时间的1/3时,即10秒后)去检查节点1是否还持有锁。
      1. 如果节点1的业务逻辑还在执行(即客户端还活着),看门狗就会通过Redis命令重置锁的过期时间(重新设置为30秒),这就是 "续期"
      1. 如果节点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分钟(我们设置的过期时间)后自动释放,其他节点就可以获取锁并执行任务。
相关推荐
k***92161 小时前
redis连接服务
数据库·redis·bootstrap
T-BARBARIANS1 小时前
mariadb galera集群在Openstack中的应用
数据库·负载均衡
跟着珅聪学java1 小时前
Redis 缓存击穿与雪崩的核心区别
redis
java1234_小锋2 小时前
Redis线上操作最佳实践有哪些?
java·数据库·redis
普通网友2 小时前
Python函数定义与调用:编写可重用代码的基石
jvm·数据库·python
C++chaofan2 小时前
项目中基于redis实现缓存
java·数据库·spring boot·redis·spring·缓存
普通网友2 小时前
使用Python进行PDF文件的处理与操作
jvm·数据库·python
q***T5832 小时前
后端分布式缓存预热,提高缓存命中率
分布式·缓存
没有bug.的程序员2 小时前
Spring 全家桶在大型项目的最佳实践总结
java·开发语言·spring boot·分布式·后端·spring