redisson 闭锁(RCountDownLatch)与栅栏(RBarrier)

2 阅读5分钟

Redisson 提供的分布式闭锁(RCountDownLatch)和栅栏(RBarrier)是两种协调多节点任务执行顺序的同步原语,它们分别基于 Redis 实现了 Java 标准库中的 CountDownLatchCyclicBarrier 功能。以下从原理、用法到应用场景详细说明:

一、分布式闭锁(RCountDownLatch)

1. 核心原理

闭锁是一种一次性门闩,初始化时设置计数器值,线程通过 countDown() 方法递减计数器,当计数器变为 0 时,所有等待的线程被释放:

  • 计数器初始化:如 latch.trySetCount(3),表示需要 3 次 countDown() 操作;
  • 等待操作:线程调用 await() 阻塞,直到计数器为 0;
  • 释放操作:每次 countDown() 使计数器减 1,减至 0 时唤醒所有等待线程。

Redisson 的 RCountDownLatch 将计数器存储在 Redis 中,通过发布订阅机制实现跨节点通知。

2. 基本用法

// 创建闭锁,初始计数为3
RCountDownLatch latch = redisson.getCountDownLatch("task:latch");
latch.trySetCount(3); // 设置需要3个节点完成任务

// 工作节点代码(每个节点执行)
// ... 执行任务
latch.countDown(); // 任务完成,计数器减1

// 主线程等待所有工作节点完成
latch.await(); // 阻塞直到计数器为0
System.out.println("所有任务已完成");

3. 带超时的等待

// 等待最多10秒
boolean completed = latch.await(10, TimeUnit.SECONDS);
if (completed) {
    // 所有任务完成
} else {
    // 超时处理
}

二、分布式栅栏(RBarrier)

1. 核心原理

栅栏是一种循环屏障,初始化时设置参与线程数( parties),当所有线程都到达栅栏时,栅栏打开,所有线程继续执行,然后栅栏可以被重复使用:

  • 初始化:如 barrier.trySetParties(5),表示需要 5 个线程到达;
  • 等待操作:线程调用 await() 阻塞,直到所有线程都到达;
  • 循环特性:所有线程通过后,栅栏重置,可再次使用。

Redisson 的 RBarrier 同样基于 Redis 存储状态,通过发布订阅实现跨节点同步。

2. 基本用法

// 创建栅栏,设置需要5个线程参与
RBarrier barrier = redisson.getBarrier("task:barrier");
barrier.trySetParties(5); // 设置参与线程数

// 每个工作节点执行
// ... 执行阶段1任务
barrier.await(); // 等待其他节点完成阶段1

// 所有节点都完成阶段1后,继续执行阶段2
// ... 执行阶段2任务
barrier.await(); // 等待其他节点完成阶段2

// 继续后续任务

3. 动态调整参与线程数

// 动态增加参与线程数
barrier.addParties(2); // 增加2个参与者

// 动态减少参与线程数
barrier.reduceParties(1); // 减少1个参与者

三、闭锁 vs 栅栏

特性闭锁(RCountDownLatch)栅栏(RBarrier)
协调目标等待其他线程完成(一个线程等待多个)所有线程互相等待(多个线程同步)
计数器操作只能 countDown()(递减)可动态调整 addParties/reduceParties
循环特性一次性使用,计数器到 0 后不可重置可重复使用,通过后自动重置
典型场景主线程等待多个子任务完成多阶段任务的同步点(如并行计算)

四、应用场景

1. 闭锁的典型场景

  • 多节点数据同步完成后汇总
    // 主节点初始化闭锁
    RCountDownLatch latch = redisson.getCountDownLatch("data:sync:latch");
    latch.trySetCount(3); // 等待3个从节点同步数据
    
    // 从节点同步数据
    // ... 执行数据同步
    latch.countDown(); // 同步完成,通知主节点
    
    // 主节点等待并汇总
    latch.await();
    aggregateData(); // 汇总所有从节点数据
    
  • 服务启动依赖检查
    // 初始化闭锁,等待3个关键服务启动
    RCountDownLatch serviceLatch = redisson.getCountDownLatch("service:start:latch");
    serviceLatch.trySetCount(3);
    
    // 每个服务启动后调用
    public void startService() {
        // ... 启动服务
        serviceLatch.countDown(); // 服务启动完成
    }
    
    // 主应用等待所有服务启动
    serviceLatch.await();
    System.out.println("所有服务已启动,应用上线");
    

2. 栅栏的典型场景

  • 多阶段并行计算
    // 初始化栅栏,5个计算节点参与
    RBarrier barrier = redisson.getBarrier("compute:barrier");
    barrier.trySetParties(5);
    
    // 每个计算节点执行
    for (int phase = 1; phase <= 3; phase++) {
        // 执行阶段计算
        computePhase(phase);
        
        // 等待所有节点完成当前阶段
        barrier.await();
        
        // 所有节点完成后,继续下一阶段
    }
    
  • 分布式游戏同步
    // 游戏初始化,10个玩家参与
    RBarrier gameBarrier = redisson.getBarrier("game:barrier");
    gameBarrier.trySetParties(10);
    
    // 每个玩家客户端执行
    while (gameRunning) {
        // 处理玩家输入
        processInput();
        
        // 等待所有玩家完成当前帧
        gameBarrier.await();
        
        // 同步所有玩家状态
        syncGameState();
    }
    

五、注意事项

1. 异常处理

  • 闭锁超时:使用 await(timeout, unit) 避免永久阻塞,超时后需手动处理未完成的任务。
  • 栅栏破坏:若某个线程在 await() 时异常退出,可能导致栅栏无法打开,需捕获异常并重置栅栏。

2. Redis 可靠性

  • 若 Redis 节点故障,可能导致同步信息丢失,需结合 Redis 的高可用部署(如哨兵、集群)。
  • 长时间等待时,建议使用带超时的方法,避免 Redis 连接断开导致的永久阻塞。

3. 性能考虑

  • 频繁的 countDown()await() 会产生 Redis 网络开销,建议批量操作或减少同步频率。
  • 对于高并发场景,可考虑使用本地计数器结合定期同步到 Redis 的方式。

六、总结

  • 闭锁(RCountDownLatch) 适合一次性的“等待所有任务完成”场景,如数据汇总、服务启动协调。
  • 栅栏(RBarrier) 适合多阶段的任务同步,如并行计算、分布式游戏,支持循环使用。
  • 两者均通过 Redis 实现跨节点同步,需注意 Redis 的可靠性和性能问题。