前言
本文首先从面试的角度介绍了分布式锁,最后在第五章节总结了高频面试考点。
在分布式系统中,保证共享资源的互斥访问是一项关键的挑战。随着互联网应用的不断发展,分布式环境下对数据的高效管理和一致性控制变得愈发重要。在这种背景下,Redis分布式锁成为了一种常见的解决方案之一。Redis作为一款高性能的内存数据库,提供了丰富的原子操作和分布式特性,使得其成为实现分布式锁的理想选择。
一、Redis分布式锁是什么?
Redis分布式锁是一种利用Redis服务作为中间件,实现在分布式系统环境下多个独立节点间进行资源同步访问的一种锁机制,用于在分布式系统中协调多个节点对共享资源的访问,确保在同一时刻只有一个节点能够对数据库进行操作,以防止竞态条件和数据不一致性的问题。它的核心思想是利用Redis的原子操作,结合分布式特性,实现对锁的获取、释放和维护。
(1)原子性操作:
Redis提供了诸如setnx(set if not exists)、setex (set with expiration)等原子性操作,这意味着设置锁和设置过期时间可以在单个命令中完成。这些操作的原子性保证了在多个客户端同时尝试获取锁时,只有一个客户端能够成功地获取到锁,从而确保了资源的互斥访问。
(2)唯一标识:
为了确保锁的释放安全性,通常会在设置锁时附加一个唯一标识,例如客户端ID或者UUID。当客户端释放锁时,需要验证该标识是否与当前持有锁的客户端匹配,避免误释放其他客户端持有的锁。
(3)超时处理:
为了防止锁被持有的客户端异常退出而导致资源长时间无法释放,Redis分布式锁通常会设置一个过期时间(TTL),即锁的自动释放时间。这样即使锁未被主动释放,一段时间后也会自动释放,避免资源被长时间占用。
(4)重入性处理:
在某些场景下,同一个客户端可能需要多次获取同一个锁。为了防止死锁,需要考虑锁的重入性,即同一个客户端可以多次获取同一个锁而不会被阻塞。
(5)竞争条件处理:
在高并发情况下,可能会出现多个客户端同时尝试获取锁的情况。为了避免竞争条件,通常会设置一定的重试机制,或者采用公平锁的实现方式,以确保所有客户端都有机会获取锁。
二、Redis分布式锁使用场景
集群情况下的定时任务、抢单、幂等性场景。下面以抢卷场景下使用synchronized本地锁和redis分布式锁为例。
-
synchronized本地锁
在同一个jvm下可以使用synchronized本地锁进行加锁操作:
-
Redis分布式锁
由于现在都是使用nginx反向代理技术,即同一份代码部署在多个tomcat中,所以需要使用redis分布式锁
集群情况的下redis分布式锁不紧能锁住本地jvm下的其它线程,还能锁住其它服务器得线程,这就是分布式锁和本地锁的区别。
-
List item
三、如何实现redis分布式锁?
Redis实现分布式锁主要利用Redis的setnx命令。
- redisson实现的分布式锁,执行流程图如下所示:
(1)加锁机制
线程去获取锁,获取成功: 执行 lua脚本,保存数据到 redis数据库。
线程去获取锁,获取失败: 一直通过 while循环尝试获取锁,获取成功后,执行 lua脚本,保存数据到 redis数据库。
(2)看门狗(watch dog)自动延期机制
问题:redis实现分布式锁如何合理控制锁的有效时长呢?
①根据业务执行时间估计。②给锁续期(看门狗机制)
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间(releaseTime / 3,releaseTime是锁的有效期)就检查当前业务是否还持有锁,如果当前业务还在进行但是锁过期了,那么就给锁进行续期操作,当业务执行完成之后,其它线程需要使用锁,那么就释放锁就可以了。
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自动不断尝试获取锁(通过while循环),如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
(3)为啥要用 lua脚本呢?
Redis 分布式锁采用 Lua 脚本的主要原因在于它能够提供更好的原子性和一致性保证,尤其是在处理复杂逻辑时,Lua 脚本的优势尤为明显。
(4)可重入加锁机制
这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
利用hash结构记录线程id和重入次数:
代码展示:
四、Redis分布式锁的缺陷(主从一致性问题)
Redis分布式锁会有个缺陷,就是在 Redis哨兵模式下:
客户端1 对某个master节点写入了 redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。这时客户端2 来尝试加锁的时候,在新的 master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。缺陷在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。
主节点宕机,从节点变成主节点,新的客户端来尝试加锁的时候,在新的主节点上也能加锁,导致各种脏数据的产生。
解决方案:
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上加锁。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁。
五、面试题总结
从面试的角度对redis分布式锁进行总结如下:
(1)Redis分布式锁如何实现 ?
在redis中提供了一个命令setnx(SET if not exists)由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的。
(2)如何控制Redis实现分布式锁有效时长呢?
redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了,还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
(3)redisson实现的分布式锁是可重入的吗?
是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
(4)redisson实现的分布式锁能解决主从一致性的问题吗?
这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
(5)如果业务非要保证数据的强一致性,这个该怎么解决呢?
redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。