Redis中是如何实现分布式锁的?

分布式锁常见的三种实现方式:

  1. 数据库乐观锁;

  2. 基于Redis的分布式锁;

  3. 基于ZooKeeper的分布式锁。

本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的。

要点

Redis要实现分布式锁,以下条件应该得到满足

互斥性

  • 在任意时刻,只有一个客户端能持有锁。

不能死锁

  • 客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

容错性

  • 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

 

实现

可以直接通过 set key value px milliseconds nx 命令实现加锁, 通过Lua脚本实现解锁。

//获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX  30000

//释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

代码解释

  • set 命令要用 set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证了原子性,

  • value 要具有唯一性,可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;

  • 释放锁时要验证 value 值,防止误解锁;

  • 通过 Lua 脚本来避免 Check And Set 模型的并发问题,因为在释放锁的时候因为涉及到多个Redis操作 (利用了eval命令执行Lua脚本的原子性);

加锁代码分析

首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,用来标识这把锁是属于哪个请求加的,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

解锁代码分析

将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,如果相等则解锁(删除key)。

存在的风险

如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,导致出现多个客户端持有锁的情况,这样就不能实现资源的独享了。

  1. 客户端A从master获取到锁

  2. 在master将锁同步到slave之前,master宕掉了(Redis的主从同步通常是异步的)。
    主从切换,slave节点被晋级为master节点

  3. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的情况。

 

redlock算法出现

这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒;

  2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;

  3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1;

  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;

  5. 要是锁建立失败了,那么就依次之前建立过的锁删除;

  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

å¾ç

 

Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:

https://redis.io/topics/distlock 。

Redisson实现

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。

Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Redisson 分布式重入锁用法

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:

// 1.构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
// 2.构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 3.获取锁对象实例(无法保证是按线程的顺序获取到)
RLock rLock = redissonClient.getLock(lockKey);
try {
    /**
     * 4.尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
     */
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
}finally{
    //无论如何, 最后都要解锁
    rLock.unlock();
}

加锁流程图

å¾ç

 

解锁流程图

å¾ç

 

我们可以看到,RedissonLock是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。

需要特别注意的是,RedissonLock 同样没有解决 节点挂掉的时候,存在丢失锁的风险的问题。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。

所以,如果业务场景可以容忍这种小概率的错误,则推荐使用 RedissonLock, 如果无法容忍,则推荐使用 RedissonRedLock。

 

这里的话我也将我之前在学习过程中用到的一些学习资料整理成了文档,以及我自身之前的一个面试文案和知识点补充,有需要的朋友可以加我的QQ学习交流裙   735721532 ,这里也祝愿大家最终都能够学有所成,早日找到满意的工作!

<think>首先,用户的问题是关于Redis实现分布式锁的步骤和方法。我需要基于系统级指令来构建我的回答。 系统级指令关键点: - 所有行内数学表达式用$...$格式(但这里可能不涉及数学,所以暂时忽略)。 - 独立公式用$$...$$并单独成段(同样,可能不适用)。 - LaTeX语法正确(确保任何数学部分正确)。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考的引用: - [^1]: Redis 主从一致性问题,使用 Redlock 或 Redis 集群模式。 - [^2]: Redis 提供简单分布式锁,但 Redlock 或 Redisson 提高健壮性。 - [^3]: Redisson 使用看门狗机制延长锁生存时间。 - [^4]: 释放锁时使用 Lua 脚本确保原子性。 用户问题:"我想在Redis实现分布式锁的方法 请问Redis 实现分布式锁的步骤和方法" 我的回答需要: 1. 解释分布式锁的基本概念。 2. 详细步骤和方法,包括如何获取锁、释放锁、处理过期时间等。 3. 参考引用中的关键点,如原子性、主从问题、Redisson等。 4. 结构清晰,逐步解释。 5. 最后生成相关问题。 大纲回答: - 引言:简要介绍分布式锁Redis中的作用。 - 步骤: 1. 获取锁:使用SET命令,设置键值对,确保原子性(如使用SETNX或SET with NX PX)。 2. 设置过期时间:避免死锁。 3. 释放锁:使用Lua脚本确保只释放自己的锁。 4. 处理潜在问题:如主从一致性,建议使用Redlock或Redisson。 - 示例代码:基于引用[^4]提供Java代码片段。 - 最佳实践:建议使用Redisson或高级方案。 - 生成相关问题。 确保语言中文。 生成相关问题: - 基于主题,如:如何优化Redis分布式锁?如何处理锁续期?Redisson的实现细节?分布式锁的应用场景? 引用标识: - 在相关段落末尾添加 [^number],例如,提到主从问题时添加 [^1]。 完整回答结构: 1. 介绍分布式锁。 2. 步骤分解。 3. 代码示例。 4. 潜在问题及解决方案。 5. 总结。 6. 相关问题。</think>### Redis 实现分布式锁的步骤和方法 在分布式系统中,分布式锁是确保多个进程或服务对共享资源进行互斥访问的关键机制。Redis 凭借其高性能和原子操作,成为实现分布式锁的常用选择。下面我将逐步解释 Redis 实现分布式锁的核心步骤和方法,确保过程清晰、可靠。实现分布式锁的核心目标是保证原子性、安全性和可靠性,避免死锁或锁冲突[^1]。 #### 步骤 1: 获取锁 获取锁的目的是确保只有一个客户端能成功持有锁。在 Redis 中,推荐使用 `SET` 命令结合 `NX`(Not eXists)和 `PX`(毫秒级过期时间)参数来实现原子操作: - **命令格式**: `SET lock_key unique_value NX PX expire_time` - `lock_key`: 锁的键名,例如 `resource_lock`。 - `unique_value`: 唯一标识符(如 UUID 或客户端 ID),用于区分不同客户端的锁,防止误删[^4]。 - `expire_time`: 锁的过期时间(毫秒),例如 `30000`(30 秒),避免客户端崩溃导致死锁。 - **原子性保证**: `NX` 确保只有键不存在时才能设置成功,`PX` 自动设置过期时间,避免两步操作(如先 SETNX 再 EXPIRE)的非原子性问题。 - **返回值**: 如果设置成功,Redis 返回 `OK`;否则返回 `nil`,表示锁已被占用。 例如,在命令行中: ```bash SET resource_lock "client_123" NX PX 30000 ``` 如果返回 `OK`,表示锁获取成功;否则需重试或等待。 #### 步骤 2: 设置锁的过期时间 锁必须设置过期时间,以防止客户端故障时锁永久持有(死锁)。在步骤 1 中,`PX` 参数已集成过期时间设置,无需额外命令。建议: - **过期时间选择**: 根据业务逻辑调整,通常 10-30 秒。如果业务执行时间较长,需结合锁续期机制(如 Redisson 的看门狗)[^3]。 - **注意事项**: 避免在获取锁后单独调用 `EXPIRE`,因为这可能因网络问题导致非原子操作。 #### 步骤 3: 释放锁 释放锁时,必须确保只释放自己持有的锁,防止其他客户端的锁被误删。使用 Lua 脚本实现原子操作: - **Lua 脚本逻辑**: 先检查锁值是否匹配 `unique_value`,再删除键。 - **原子性保证**: Lua 脚本在 Redis 中单线程执行,确保检查和删除的原子性[^4]。 示例代码(Java 中使用 Spring Boot): ```java import org.springframework.data.redis.core.script.DefaultRedisScript; public boolean unlock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); redisScript.setResultType(Long.class); return redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId) == 1; } ``` 此代码中,`requestId` 是步骤 1 中的 `unique_value`。如果匹配,删除锁并返回 1(成功);否则返回 0(失败)[^4]。 #### 步骤 4: 处理潜在问题 Redis 分布式锁在简单场景下有效,但在高可用环境中需考虑以下问题: - **主从一致性问题**: Redis 主从复制是异步的,如果主节点故障,从节点可能未同步锁数据,导致锁丢失。解决方案: - 使用 **Redlock 算法**(Redis 官方推荐),通过多个独立 Redis 节点投票获取锁,提高健壮性[^1][^2]。 - 切换到 **Redis 集群模式**,但需注意集群的复杂性。 - **锁续期问题**: 如果业务执行时间超过锁过期时间,需自动延长锁。推荐使用 **Redisson** 库,它内置看门狗机制(后台线程每 10 秒检查并续期锁)[^3]。 - **网络延迟和重试**: 在锁获取失败时,实现指数退避重试机制,避免频繁请求。 #### 最佳实践和工具推荐 - **简单场景**: 直接使用 Redis `SET` 命令 + Lua 脚本,适用于单节点或低并发环境。 - **高可用场景**: 优先使用 **Redisson**(Java 客户端),它封装了 Redlock、看门狗和锁续期,简化实现[^3][^2]。或采用 **ZooKeeper** 等替代方案。 - **性能调优**: 监控锁竞争和过期时间,避免锁等待导致性能瓶颈。 - **可靠性**: 在关键业务中,结合日志和监控,确保锁操作可追溯。 通过以上步骤,Redis 分布式锁能有效协调分布式资源。但需根据业务需求选择方案:简单锁用原生 Redis,复杂场景用 Redisson 或 Redlock[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值