Redis作为分布式锁的使用详解

 更新时间:2025年05月13日 09:17:26   作者:找不到、了  
这篇文章主要介绍了Redis作为分布式锁的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现。如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此之间的干扰。

实现分布式锁的方式有很多,可以通过各种中间件来进行分布式锁的设计,包括Redis、Zookeeper等。

如下图所示:

1、实现锁的方法

如下图所示锁的流程:

1.1. setnx命令

属于最简单的锁,不推荐生产使用。

SETNX toilet_1 "occupied"  # 尝试锁门
  • 如果返回1:成功
  • 如果返回0:失败

问题:如果客户端崩,永远被占着(死锁)。

1.2. 带过期时间的锁

属于对setnx命令的改进版:

SETNX toilet_1 "occupied"
EXPIRE toilet_1 30  # 30秒后自动解锁

仍然有问题:两条命令不是原子的,可能在SETNX和EXPIRE之间崩溃。

1.3. 原子命令(推荐)

该命令可使用于生产级方案:

SET toilet_1 "user_123" NX EX 30  # 原子操作:锁门+设置30秒自动开锁

1.4. RedLock算法详解

当需要更高可靠性时,Redis作者Antirez提出的分布式锁算法:

1.实现原理

获取当前毫秒级时间戳T1

依次向N个独立的Redis实例申请锁

计算获取锁总耗时 = 当前时间T2 - T1

  • 必须小于锁有效时间
  • 必须获得多数(N/2+1)节点认可

锁实际有效时间 = 初始设置时间 - 获取锁总耗时。

代码示例:

// RedissonRedLock.tryLock()的核心逻辑
while (waitTimeRemaining > 0) {
    long start = System.nanoTime();
    
    // 尝试从多数节点获取锁
    int acquiredLocks = tryAcquireMultipleLocks();
    
    if (acquiredLocks >= majority) {
        // 计算实际有效时间
        long elapsed = System.nanoTime() - start;
        long lockTime = leaseTime - TimeUnit.NANOSECONDS.toMillis(elapsed);
        
        if (lockTime > 0) {
            // 对所有成功节点设置统一过期时间
            scheduleLockExpiration(lockTime);
            return true;
        }
        // 超时则释放已获得的锁
        releaseAcquiredLocks();
    }
    
    // 等待随机时间后重试
    waitTimeRemaining -= calculateWaitTime();
}

2.设计目的

  • 当单个Redis节点宕机时,系统仍能正常工作
  • 防止主从切换时的锁失效(原主节点崩溃,从节点晋升但未同步锁信息)

3.关键保障机制

  • 时钟同步:所有Redis节点必须时间同步(NTP)
  • 过期时间补偿:扣除锁获取耗时
  • 多数派原则:至少(N/2 + 1)个节点确认

4.局限性

1. 仍然存在的竞争问题

2. 网络分区问题

当发生网络分区时:

  • 客户端可能无法感知部分节点状态
  • 可能出现多个客户端同时认为自己持有锁

3. 性能开销

获取多个锁的延迟显著高于单节点:

  • 通常比单节点慢3-5倍
  • 不适合高频短时锁场景

而对于RedLock的本质作用确实主要是为了解决单点故障问题,而不是提升并发性能,并未彻底解决一致性,如果要解决一致性问题,需要结合防护令牌或分布式事务。

1.5. 防护令牌(Fencing Token)模式

当发生锁竞争的时候,假设5节点RedLock:

  • 客户端A获得节点1、2、3的锁
  • 客户端B获得节点3、4、5的锁

此时:

  • 两个客户端都认为自己获得了锁(都获得3个节点)
  • 实际发生了冲突(节点3被双方认为属于自己)

代码示例:

// 获取锁时返回单调递增的token
LockResult result = redLock.tryLockWithToken();
long token = result.getToken();

// 操作资源时验证token
if (resource.getCurrentToken() < token) {
    resource.write(data, token);
} else {
    throw new ConcurrentModificationException();
}
  • 实际实现中会加入** fencing token(防护令牌)机制
  • 每次锁获取附带单调递增的token
  • 资源操作时需要验证token顺序。

1.6. 看门狗机制

在上述的章节了解到,防护令牌可以解决锁竞争一致性的问题,那么如果在锁执行过程中,过期时间到期,而业务还没执行完,那么该怎么办呢?

看门狗(Watchdog)机制是Redis分布式锁中确保业务执行期间锁不会意外释放的关键设计,尤其在Redisson等客户端中广泛使用。

当业务执行时间超过锁的初始过期时间时,防止其他客户端提前获取锁导致数据竞争。

流程:

// 获取锁(默认30秒看门狗时间)
RLock lock = redisson.getLock("order_lock");
lock.lock(); // 内部启动看门狗线程

try {
    // 执行业务逻辑(可能超过30秒)
    processOrder();
} finally {
    lock.unlock(); // 释放时会停止看门狗
}

锁获取时:

public void lock() {
    try {
        lockInterruptibly();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

public void lockInterruptibly() throws InterruptedException {
    // 尝试获取锁,默认leaseTime=30秒
    tryAcquireAsync(leaseTime, TimeUnit.MILLISECONDS).sync();
    
    // 启动看门狗线程
    scheduleExpirationRenewal();
}

看门狗线程:

protected void scheduleExpirationRenewal() {
    Thread renewalThread = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            // 每10秒(leaseTime/3)续期一次
            try {
                Thread.sleep(leaseTime / 3);
                
                // 通过Lua脚本续期
                String script = 
                    "if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
                    "return redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "else return 0; end";
                
                redis.eval(script, 
                    Collections.singletonList(getName()),
                    internalLockLeaseTime, getLockName(Thread.currentThread().getId()));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });
    renewalThread.start();
}

参数和配置方式如下:

jpg jpg

2、使用场景

用一个电影院抢座的例子,通过Java代码展示Redis分布式锁的实际应用。这个场景非常贴近生活,容易理解分布式锁的必要性。

假设有一个热门电影场次,多个用户同时在线选座,我们需要保证:

  • 一个座位只能被一个用户选中
  • 用户有10分钟支付时间
  • 超时未支付自动释放座位

1、基础配置

首先添加Redis和Redisson(Redis Java客户端)依赖:

<!-- pom.xml -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.8</version>
</dependency>

初始化Redis连接:

public class RedisLockDemo {
    private static RedissonClient redisson;
    
    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redisson = Redisson.create(config);
    }
}

简单实现:选座锁

1. 获取座位锁

public boolean lockSeat(String seatNumber, String userId) {
    // 获取分布式锁对象
    RLock lock = redisson.getLock("seat:" + seatNumber);
    
    try {
        // 尝试加锁,waitTime=0表示不等待,leaseTime=10分钟自动解锁
        return lock.tryLock(0, 10, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}

2. 释放座位锁

public void unlockSeat(String seatNumber, String userId) {
    RLock lock = redisson.getLock("seat:" + seatNumber);
    
    // 检查是否还被当前线程持有
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

3. 完整选座流程

public boolean selectSeat(String seatNumber, String userId) {
    if (!lockSeat(seatNumber, userId)) {
        System.out.println(userId + " 抢座失败,座位已被锁定");
        return false;
    }
    
    try {
        System.out.println(userId + " 成功锁定座位 " + seatNumber);
        // 模拟用户支付流程
        boolean paid = mockPaymentProcess(userId);
        
        if (paid) {
            System.out.println(userId + " 支付成功,座位确认");
            return true;
        } else {
            System.out.println(userId + " 支付超时,座位释放");
            return false;
        }
    } finally {
        unlockSeat(seatNumber, userId);
    }
}

private boolean mockPaymentProcess(String userId) {
    // 模拟50%概率支付成功
    try {
        Thread.sleep(2000); // 模拟支付思考时间
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return new Random().nextBoolean();
}

3、高级特性:锁续期

当用户支付时间可能超过10分钟时,需要自动续期:

public boolean lockSeatWithRenewal(String seatNumber, String userId) {
    RLock lock = redisson.getLock("seat:" + seatNumber);
    
    try {
        // 获取锁,并设置看门狗自动续期(默认30秒)
        lock.lock();
        
        // 启动一个线程定期续期
        new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(5000); // 每5秒续期一次
                    lock.expire(10, TimeUnit.MINUTES); // 续期10分钟
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
        
        return true;
    } catch (Exception e) {
        return false;
    }
}

4、测试用例

public static void main(String[] args) {
    RedisLockDemo demo = new RedisLockDemo();
    
    // 模拟3个用户同时抢5号座位
    new Thread(() -> demo.selectSeat("A05", "用户1")).start();
    new Thread(() -> demo.selectSeat("A05", "用户2")).start();
    new Thread(() -> demo.selectSeat("A05", "用户3")).start();
}

输出可能结果:

用户1 成功锁定座位 A05用户2 抢座失败,座位已被锁定用户3 抢座失败,座位已被锁定用户1 支付成功,座位确认

5、关键点解析

  • 锁的Key设计:seat:A05 明确表示对A05座位的锁
  • 唯一标识:虽然没有直接使用userId作为value,但Redisson内部会维护线程与锁的关系
  • 自动释放:即使程序崩溃,10分钟后锁也会自动释放
  • 可重入性:同一个线程可以多次获取同一把锁(Redisson特性)

6、对比生活场景

技术概念电影院例子
Redis服务器电影院售票系统
分布式锁座位锁定状态
锁的Key座位号(如A05)
锁的Value售票员记录的本子(谁锁的)
锁过期时间"保留座位10分钟"的告示牌
获取锁失败看到座位已经被标记"已预订"
锁续期顾客请求延长保留时间

这个例子展示了:

  • 如何用Redis解决现实中的资源竞争问题
  • Java中实际使用Redis分布式锁的代码写法
  • 处理锁超时、续期等常见场景的方法

通过电影院选座这种熟悉的场景,应该能更直观地理解Redis分布式锁的工作机制了。实际开发中,使用Redisson等成熟客户端可以避免很多边界条件的处理。

3、Redis分布式锁的局限性

时钟漂移问题:

  • 依赖系统时钟,多节点时钟不同步可能影响RedLock

持久化延迟:

  • 异步复制可能导致主节点崩溃后从节点丢失锁信息

长时间阻塞:

  • 获取不到锁的客户端需要合理处理等待/超时

4、对比

总结

Redis分布式锁凭借其优异的性能和足够的可靠性,已成为互联网公司的首选方案。理解其实现原理和限制条件,能够帮助我们在不同业务场景中做出合理的技术选型。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • redis中如何使用lua脚本让你的灵活性提高5个逼格详解

    redis中如何使用lua脚本让你的灵活性提高5个逼格详解

    这篇文章主要给大家介绍了关于redis中如何使用lua脚本让你的灵活性提高5个逼格的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-10-10
  • Redis 的内存淘汰策略和过期删除策略的区别

    Redis 的内存淘汰策略和过期删除策略的区别

    这篇文章主要介绍了Redis 的内存淘汰策略和过期删除策略的区别,Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略
    2022-07-07
  • redis集合类型_动力节点Java学院整理

    redis集合类型_动力节点Java学院整理

    这篇文章给大家介绍了redis集合类型的常用方法,感兴趣的朋友参考下吧
    2017-08-08
  • redis监听key过期事件的详细步骤

    redis监听key过期事件的详细步骤

    本文主要介绍了redis监听key过期事件的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Redis使用SETNX命令实现分布式锁

    Redis使用SETNX命令实现分布式锁

    分布式锁是一种用于在分布式系统中控制多个节点对共享资源进行访问的机制,本文主要为大家详细介绍了Redis如何使用SETNX命令实现分布式锁,需要的可以参考下
    2025-01-01
  • 一文详解Redis的主从同步原理

    一文详解Redis的主从同步原理

    Redis为了保证服务高可用,其中一种实现就是主从模式,本篇文章将对主从模式中为了保证主节点和从节点数据一致而实现的主从同步机制进行学习,感兴趣的同学可以参考阅读下
    2023-07-07
  • Redis中有序集合的内部实现方式的详细介绍

    Redis中有序集合的内部实现方式的详细介绍

    本文主要介绍了Redis中有序集合的内部实现方式的详细介绍,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • redis protocol通信协议及使用详解

    redis protocol通信协议及使用详解

    这篇文章主要为大家介绍了redis protocol通信协议及使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • redis-cli命令行工具的使用小结

    redis-cli命令行工具的使用小结

    redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的朋友跟随小编一起看看吧
    2025-01-01
  • Redis解决缓存一致性问题

    Redis解决缓存一致性问题

    本文主要介绍了Redis 解决缓存一致性问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-10-10

最新评论