4. Redis实现Lock接口

4. Redis实现Lock接口

基于 Redis 实现 Java Lock 接口的设计与实现文档

一、背景与目标

在分布式系统中,为了保证多个节点对共享资源访问的一致性和互斥性,往往需要引入分布式锁机制。Redis 由于其性能优秀、支持原子操作且具备良好的生态,被广泛应用于实现分布式锁的场景。

本文旨在介绍如何通过 Redis 来实现 Java 的 Lock 接口,并深入探讨不同的实现方式、其原理、优缺点与扩展思路。


二、为什么 Redis 非常适合用于实现分布式锁?

Redis 在以下多个方面天然适配于分布式锁的实现:

1. 原子操作

  • Redis 提供的 SET key value NX PX 命令具备原子性。
  • 同一时间只有一个客户端能设置成功,从而实现互斥。

2. 快速响应(低延迟)

  • Redis 是内存数据库,读写速度极快,适合高频加解锁操作。

3. 丰富的操作语义

  • 除了 SET NX PX,Redis 支持 Lua 脚本,可以原子化处理复杂的检查+释放逻辑,避免 race condition。

4. 自然支持过期机制

  • Redis 可为 key 设置 TTL(Time to Live),防止客户端 crash 导致锁永远无法释放,即“死锁”。

5. 客户端丰富,生态完善

  • Java 中可用 Jedis、Lettuce、Redisson 等多个客户端,并支持与 Spring 框架高度集成。

6. 支持持久化与高可用

  • Redis 支持主从复制、哨兵机制、集群部署等,可以支持更高的可用性和容灾能力。

7. 简单易用

  • 相比 ZooKeeper,Redis 的部署成本更低,学习曲线平滑,在非强一致性场景下性价比更高。

三、目标与实现思路

我们希望实现如下目标:

  1. 使用 Redis 实现 java.util.concurrent.locks.Lock 接口。
  2. 支持基本的 lock()unlock()tryLock() 方法。
  3. 支持阻塞、超时、自动过期等机制。
  4. 可扩展以支持公平锁、可重入锁等功能。

Redis 本身支持如下两种机制来构建分布式锁:

  • 基于 SET key value NX PX 命令实现基本锁
  • 基于 Redisson(推荐)或实现 Redlock 算法

四、Redis 分布式锁核心原理

1. 基本命令方式

使用 Redis 的 SET 命令实现原子加锁:

SET lock_key unique_value NX PX 30000

含义:

  • NX 表示只有当 key 不存在时才设置成功,相当于加锁。
  • PX 表示过期时间,防止死锁。
  • unique_value 一般为 UUID,表示当前线程唯一标识。

释放锁使用 Lua 脚本确保原子性:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

2. Redlock 算法(多 Redis 节点)

由 Redis 作者提出,适用于高可用场景。要求:

  • 向多个 Redis 实例写入锁
  • 获得超过半数成功则视为加锁成功
  • 使用时需注意时钟漂移、网络抖动等问题

推荐使用 Redisson 等成熟组件代替手写实现。


五、RedisLock 实现类的详细实现流程

1. 构造函数

  • 传入 RedisTemplate 与 lockKey。
  • 初始化 ThreadLocal<String> 用于保存本线程生成的唯一锁标识。

2. lock() 实现逻辑

  • 通过 tryLock() 自旋尝试加锁。
  • 若失败,则 Thread.sleep 一段时间后重试,直到成功。

3. tryLock() 实现逻辑

  • 使用 UUID 作为当前线程锁的唯一标识。
  • 通过 Redis 的 SET NX PX 尝试设置 key。
  • 若成功,保存 UUID 到线程变量中。
  • 若失败,返回 false。

4. unlock() 实现逻辑

  • 获取当前线程存储的 UUID。
  • 使用 Lua 脚本判断当前 key 是否由本线程持有。
  • 若是则释放;否则不做处理。

5. 不支持的方法处理

  • lockInterruptiblytryLock(timeout)newCondition 方法暂时抛出 UnsupportedOperationException

6. 锁值隔离说明

  • ThreadLocal 保证多线程环境下 UUID 隔离。
  • 避免线程 A 设置了锁,线程 B 意外解锁的情况。

六、基本实现(自定义 RedisLock)

public class RedisLock implements Lock {
    private final String lockKey;
    private final RedisTemplate<String, String> redisTemplate;
    private final ThreadLocal<String> lockValue = new ThreadLocal<>();

    public RedisLock(String lockKey, RedisTemplate<String, String> redisTemplate) {
        this.lockKey = lockKey;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void lock() {
        while (!tryLock()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public boolean tryLock() {
        String uuid = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, uuid, Duration.ofSeconds(30));
        if (Boolean.TRUE.equals(success)) {
            lockValue.set(uuid);
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        String value = lockValue.get();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            value
        );
        lockValue.remove();
    }

    @Override public void lockInterruptibly() throws InterruptedException { throw new UnsupportedOperationException(); }
    @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException(); }
    @Override public Condition newCondition() { throw new UnsupportedOperationException(); }
}

七、扩展方式与改进思路

1. 可重入锁实现

  • 通过 ThreadLocal 记录当前线程持有锁次数
  • Redis 中的 value 存储 JSON:threadId + count
  • 释放时进行 count–

2. 公平锁

  • 使用 Redis List 模拟等待队列
  • 每次加锁都判断是否轮到当前线程

3. Watch Dog 自动续期

  • 每隔一定时间判断锁是否持有
  • 自动续约过期时间,避免因业务时间长导致锁被抢走

Redisson 实现中使用 ScheduledExecutorService 来实现

4. 集群下支持(Redlock)

  • 建议使用 Redisson,支持 Redlock
  • 若自定义实现,需记录多个节点响应时延和分布式时间偏差

八、开源方案推荐

名称说明
Redisson支持 ReentrantLock、ReadWriteLock、Semaphore、CountDownLatch 等多种同步原语
Lettuce提供较低级的 Redis 操作能力,适用于自定义实现
Jedis经典 Redis 客户端,可支持基础锁实现

九、总结

  • Redis 是实现分布式锁的理想选择,适合场景如定时任务调度、单点任务控制、共享资源互斥访问等。
  • 基于 Redis 的 SET NX PX 命令+Lua 脚本可实现原子性的加解锁。
  • 实现 Lock 接口需关注:唯一标识、过期时间、线程隔离性、可扩展性。
  • 推荐使用 Redisson 等成熟方案,在开发效率、可靠性、扩展性方面优势明显。

附录:参考文献