Redis的分布式锁

Redis分布式锁详解:原理、RedLock挑战与解决方案
本文探讨了Redis如何实现分布式锁,包括setnx命令的应用、死锁问题的处理、RedLock算法的介绍及其在集群环境中的局限性,以及成本和性能考虑。特别关注了RedLock的适用场景和存在的问题,指出业务兜底的重要性。

Redis的分布式锁

理解的扩展:

在开发的时候,我们经常是一个进程中,当多个线程需要去竞争某一个资源的时候,就需要增加一把锁,来保证只有一个线程可以获得到资源。比如 synchronize

但是,当多个进程,访问共享资源时,是否可以采用相同的思维方式哪?答案是肯定的。

分布式锁:控制分布式系统中,不同的进程访问共享资源的一种锁的实现。

秒杀,抢红包场景下就需要。在项目当中,经常需要控制这个部分,也是采用最多的部分,面试当中也是非常多的问题。

在这里插入图片描述

为什么Redis可以实现分布式锁?

因为redis是一个单独的非业务服务,不会受到其他业务服务的限制,所有的业务服务都可以向redis发送写入命令,且只有一个业务服务可以写入命令成功,那么这个写入命令成功的服务即获得了锁,可以进行后续对资源的操作,其他未写入成功的服务,则进行其他处理。

如何实现??

使用redis当中的String类型就可以实现

锁的获取

setnx命令:表示set if not exists.如果key不存在,就去设置值,否则什么也不做。

两个客户端同时向redis写入try_lock,客户端1写入成功,即获取分布式锁成功。客户端2写入失败,则获取分布式锁失败。

锁的释放

当客户端1操作完后,释放锁资源,即删除try_lock。那么此时客户端2再次尝试获取锁时,则会获取锁成功。

死锁问题

假如客户端1在获取到锁资源后,服务宕机了,那么这个try_lock会一直存在redis中,那么其他服务就永远无法获取到锁了。

如何解决死锁问题

设置键过期时间,超过这个时间即给key删除掉。

但是,这又有新的问题:

假如服务A加锁成功,锁会在10s后自动释放,但由于业务复杂,执行时间过长,10s内还没执行完,此时锁已经被redis自动释放掉了。此时服务B就重新获取到了该锁,服务B开始执行他的业务,服务A在执行到第12s的时候执行完了,那么服务A会去释放锁,则此时释放的却是服务B刚获取到的锁。

虽然可以通过增加删除key时间来处理这个问题,但是并没有从根本上解决。假设设个100s,绝大多数都是1s后就会释放锁,但是由于服务宕机,则会导致100s内其他服务都无法获取到锁,这也是灾难性的。

比较完善的处理方案是:

在锁将要过期的时候,如果服务还没有处理完业务,那么将这个锁再续一段时间。比如设置key在10s后过期,那么再开启一个守护线程,在第8s的时候检测服务是否处理完,如果没有,则将这个key再续10s后过期。

Redission中,已经帮我们实现了这个功能,这个功能叫做:看门狗

如何避免释放其他服务的锁

每个服务在设置value的时候,带上自己服务的唯一标识,如UUID,或者一些业务上的独特标识。这样在删除key的时候,只删除自己服务之前添加的key就可以了。

说明:

如果需要先查看锁是否是自己服务添加的,需要先get取出来判断,然后再进行del。这样的话就无法保证原子性了。但是可以通过lua脚本,将上述两步变成了一步完成,这样就保证原子性了。

RedLock算法

引入的背景

为了防止redis宕机,所以引入了集群。那么在集群环境下,可能会出现新的情况。

假设是主从集群,且主从数据并不是强一致性。当主节点宕机后,主节点的数据还未来得及同步到从节点,进行主从切换后,新的主节点并没有老的主节点的全部数据,这就会导致刚写入到老的主节点的锁在新的主节点并没有,其他服务来获取锁时还是会加锁成功。此时则会有2个服务都可以操作公共资源,此时的分布式锁则是不安全的。

redis的作者也想到这个问题,于是他发明了RedLock。Redlock的核心思想:

搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

什么是Redlock算法?

要实现RedLock,需要至少5个实例(官方推荐),且每个实例都是master,不需要从库和哨兵.

1、客户端先获取当前时间戳T1

2、客户端依次向5个master实例发起加锁命令,且每个请求都会设置超时时间(毫秒级,注意:不是锁的超时时间),如果某一个master实例由于网络等原因导致加锁失败,则立即向下一个master实例申请加锁。

3、当客户端加锁成功的请求大于等于3个时,且再次获取当前时间戳T2,当时间戳T2 - 时间戳T1 < 锁的过期时间。则客户端加锁成功,否则失败。

4、加锁成功,开始操作公共资源,进行后续业务操作。

5、加锁失败,向所有redis节点发送锁释放命令。

即当客户端在大多数redis实例上申请加锁成功后,且加锁总耗时小于锁过期时间,则认为加锁成功。释放锁需要向全部节点发送锁释放命令。

解释:

第3步为啥要计算申请锁前后的总耗时与锁释放时间进行对比呢?

因为如果申请锁的总耗时已经超过了锁释放时间,那么可能前面申请redis的锁已经被释放掉了,保证不了大于等于3个实例都有锁存在了,锁也就没有意义了

Redlock存在的问题

1、得5个redis实例,成本大大增加.

2、可以通过上面的流程感受到,这个RedLock锁太重了.

3、主从切换这种场景绝大多数的时候不会碰到,偶尔碰到的话,保证最终的兜底操作我觉得也没啥问题。

分布式系统的NPC问题

N:Network Delay,网络延迟

P:Process Pause,进程暂停(GC)

C:Clock Drift,时钟漂移

例子

1、客户端 1 请求锁定节点 A、B、C、D、E

2、客户端 1 的拿到锁后,进入 GC(时间比较久)

3、所有 Redis 节点上的锁都过期了

4、客户端 2 获取到了 A、B、C、D、E 上的锁

5、客户端 1 GC 结束,认为成功获取锁

6、客户端 2 也认为获取到了锁,发生【冲突】

在第2步已经成功获取到锁后,由于GC时间超过锁过期时间,导致GC完成后其他客户端也能够获取到锁,此时2个客户端都会持有锁。就会有问题。

这个问题无论是redlock还是zookeeper都会有这种问题。不做业务上的兜底操作就没得解。时钟漂移问题也只能是尽量避免吧。无法做到根本解决。

Redlock其实价值不大:

1、得额外的多台服务器部署redis,每台服务器可都是钱啊,而且部署和运维的成本也增加了。

2、用RedLock感觉太重了,效率会很低,既然用了redis,就是为了提升效率,结果一个锁大大降低了效率

3、如果在集群情况下有锁丢失的情况,我们业务上做好兜底操作就可以了,可以不用上RedLock。

4、毕竟集群情况下主从切换的场景还是极少的,为了极少的情况去浪费大量的性能,感觉划不来

5、就算是上了RedLock,也是避免不了NPC问题的,还不是得业务上做兜底。

### Redis 分布式锁的实现方式、使用方法及最佳实践 #### ### 1. Redis 分布式锁的基本原理 Redis 分布式锁的核心思想是利用 Redis 的原子性操作来确保的唯一性。通过 Redis 的 `SET` 命令,结合参数 `NX` 和 `EX`,可以在多线程环境下实现加和解的功能[^1]。此外,为了提高可用性,还可以采用 RedLock 算法或多实例部署的方式。 #### ### 2. Redis 分布式锁的实现方式 #### #### 2.1 单实例 Redis 实现分布式锁 单实例 Redis 实现分布式锁是最简单的实现方式。通过以下命令完成加和解操作: ```python import time import redis # 初始化 Redis 客户端 client = redis.StrictRedis(host='localhost', port=6379, db=0) lock_key = "distributed_lock" lock_value = "unique_identifier" # 加操作 def acquire_lock(): result = client.set(lock_key, lock_value, nx=True, ex=10) # 设置过期时间为 10 秒 return result is not None # 解操作 def release_lock(): script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ client.eval(script, 1, lock_key, lock_value) ``` 上述代码中,`nx=True` 确保只有当键不存在时才设置键值对,从而实现加功能。`ex=10` 参数为设置了 10 秒的过期时间,防止死的发生[^1]。 #### #### 2.2 多实例 Redis 实现分布式锁(RedLock 算法) 在高可用场景下,可以使用 RedLock 算法实现分布式锁。RedLock 算法通过多个 Redis 实例来确保的可靠性。以下是 RedLock 的伪代码实现: ```python import redis import time class RedLock: def __init__(self, redis_nodes): self.redis_nodes = [redis.StrictRedis(**node) for node in redis_nodes] def acquire_lock(self, lock_key, lock_value, ttl): quorum = len(self.redis_nodes) // 2 + 1 start_time = time.time() success_count = 0 for node in self.redis_nodes: if node.set(lock_key, lock_value, nx=True, px=ttl): success_count += 1 elapsed_time = time.time() - start_time validity_time = ttl - int(elapsed_time * 1000) if success_count >= quorum and validity_time > 0: return True, validity_time else: self.release_lock(lock_key, lock_value) return False, 0 def release_lock(self, lock_key, lock_value): for node in self.redis_nodes: try: script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ node.eval(script, 1, lock_key, lock_value) except Exception: pass ``` RedLock 算法要求在大多数 Redis 实例上成功加,并且整个过程的时间小于的有效期,才能认为加成功[^3]。 #### ### 3. Redis 分布式锁的最佳实践 #### #### 3.1 设置合理的超时时间 为了避免死问题,必须为设置一个合理的超时时间。如果持有者在超时时间内未完成任务,将自动释放[^1]。 #### #### 3.2 使用唯一的标识符 在加时,应为每个分配一个唯一的标识符(如 UUID),以便在解时验证的拥有者身份,防止误删其他线程的[^3]。 #### #### 3.3 防止 GC 停顿导致失效 Java 程序中的垃圾回收(GC)可能导致线程长时间暂停,从而使提前释放。为了解决这一问题,可以使用续租机制,在即将到期时主动延长的有效期。 #### #### 3.4 监控的竞争情况 在高并发场景下,可以通过监控的竞争情况来优化系统性能。例如,记录加失败的次数或等待时间,分析是否存在争用问题[^1]。 #### ### 4. 示例代码:基于 Redisson 的分布式锁实现 Redisson 是一个成熟的 Redis 客户端库,提供了丰富的分布式锁功能。以下是使用 Redisson 实现分布式锁的示例代码: ```java import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonLockExample { public static void main(String[] args) throws InterruptedException { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("myDistributedLock"); lock.lock(); // 加 try { // 执行业务逻辑 System.out.println("Lock acquired, performing task..."); Thread.sleep(1000); // 模拟任务执行 } finally { lock.unlock(); // 解 System.out.println("Lock released."); } redisson.shutdown(); } } ``` Redisson 提供了多种类型,包括公平、可重入和红(RedLock),开发者可以根据实际需求选择合适的类型[^3]。 #### ### 5. 注意事项 - 在高并发场景下,应尽量减少的粒度,避免因竞争导致性能下降。 - 如果 Redis 实例发生故障,可能会导致丢失。因此,在关键业务场景下,建议使用哨兵模式或集群模式来提高 Redis 的可用性[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值