分布式锁
1.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实现的分布式锁,它是可以保证强一致性的。
2.zookeeper分布式锁
1.watch监听机制
watch监听机制主要用于监听节点状态变更,用于后续事件触发,假设当B节点监听A节点时,一旦A节点发生修改、删除、子节点列表发生变更等事件,B节点则会收到A节点改变的通知,接着完成其他额外事情。
2.zookeeper工作机制
3.ZooKeeper分布式锁的基本实现原理
-
应用程序通过ZooKeeper客户端创建一个指定的持久节点(例如/locks)作为锁的根节点。
-
当应用程序要获取锁时,它会在根节点下创建一个临时顺序节点(例如/locks/lock-0001)。
-
应用程序通过获取所有子节点,并且根据节点的顺序进行排序。
-
如果当前节点是所有子节点中最小的(即序号最小),则表示应用程序成功获取了锁。
-
如果当前节点不是最小的,则应用程序监听前一个节点,等待前一个节点被删除。
-
当前一个节点被删除后,应用程序回到第3步重新尝试获取锁。
-
当释放锁时,只需将自己的临时有序节点删除即可。
上述步骤中,通过创建临时顺序节点的方式来表示应用程序获取锁的请求。而通过获取所有子节点,并且根据节点的顺序进行排序的方式来判断当前是否可以获取锁。同时,通过监听前一个节点的方式来实现等待和唤醒机制。
4.zookeeper又是如何有序的将锁分配给不同线程
每当添加一个新的临时节点时,其都会基于watcher机制监听着它本身的前一个节点等待前一个节点的通知,当前一个节点删除时,就轮到它来持有锁了。然后依次类推
3.mysql分布式锁
1.悲观锁
悲观锁思想:在一个人占有锁的时候,其他人都处于等待状态,性能太低。
在查询单条数据sql语句后 加上 for update实现悲观锁,多个实例操作的是同一个数据库。这是是一种行锁,每次只锁一条记录。
在innodb引擎下同一张表下的同一条记录只允许同时执行一个事务获取到锁。当一个事务获取到这把锁后,其他事务不能进行任何操作,并处于等待状态,直到这个占有锁的事务提交,释放锁资源,然后其他事务才可以去抢夺锁资源。
2.乐观锁
Java悲观锁 lock syn 乐观锁 原子类 可见性 cas
对于分布式锁的实现,比较常见的一种就是基于MySQL悲观锁方式来完成,这种方式的思想就是利用MySQL的InnoDB引擎的行锁机制来完成。
3.对于乐观锁的实现:根据条件和根据版本号。
4.myisam不支持事务,悲观锁表锁。
4.应用场景
解决主从一致性问题
秒杀库存预减
商品超卖
5.分布式锁的注意事项
-
死锁问题:确保锁具有超时机制,防止因节点崩溃或网络问题导致锁无法释放。
-
锁的粒度:锁的粒度应尽可能小,避免影响系统的并发性能。
-
锁的公平性:在高并发场景下,确保锁的公平性,避免某些节点长时间无法获得锁。
-
锁的可重入性:在某些场景下,可能需要支持可重入锁,即同一个节点可以多次获取同一把锁。
6.总结
通过合理选择和使用分布式锁,可以有效解决分布式系统中的并发控制问题,确保系统的稳定性和数据的一致性。