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分钟(我们设置的过期时间)后自动释放,其他节点就可以获取锁并执行任务。
相关推荐
倔强的石头_11 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
jiayou642 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤3 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者4 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区4 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1774 天前
《从零搭建NestJS项目》
数据库·typescript
加号34 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏4 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐4 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再4 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip