分布式锁浅析
Posted 小蜻蜓网络
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式锁浅析相关的知识,希望对你有一定的参考价值。
一:为什么要使用分布式锁?
在传统的单机应用中,如果多线程对一个共享变量进行同步访问的时候,我们可以使用很多很多的机制去解决线程安全问题。但是数据量的不断增加,业务逻辑日趋复杂,我们需要搭建集群、分布式才能满足要求。而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种情况下,我们就需要用到分布式锁。
举个例子来讲:
假设12306卖票网站北京--曲阜的高铁票只剩3张,而用户A和用户B每人都需要买两张票,理想状态下,用户A买了2张票后,用户B是无法再继续购买2张的,要给出余票不足的提示。但是现实状况是,用户A和用户B看到页面展示了剩余3张票,用户A先去购买了2张,但是在还未更新库存的时候,用户B也下单去购买,此时B下单后更新库存,票还剩1张呢(当然12306现在不会出现这种情况)------典型的秒杀活动。
从上述例子看出,高并发情况下,如果不做处理将会出现各种不可预知的后果。那么在这种高并发多线程的情况下,解决问题最有效最普遍的方法就是给共享资源或对共享资源的操作加一把锁,来保证对资源的访问互斥。在Java JDK已经为我们提供了这样的锁,利用ReentrantLcok或者synchronized,即可达到资源互斥访问的目的。但是在分布式系统中,由于分布式系统的分布性,即多线程和多进程并且分布在不同机器中,这两种锁将失去原有锁的效果,需要我们自己实现一种锁——分布式锁。
二、分布式锁要满足什么样的条件:
1. 判断是否获得锁必须是原子性的:保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行,否则可能导致多个请求都获取到锁。。
2. 具备锁失效机制:网络中断或宕机无法释放锁时,锁必须被清除,不然会发生死锁。
3. 应该是一把可重入锁:一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁(否则可能会造成死锁的问题)。
4. 高性能获取锁和释放锁。
5.阻塞锁和非阻塞锁:阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。最好是一把阻塞锁(根据业务需求考虑要不要这条)
6.高可用的获取锁与释放锁;
三、分布式锁的几种实现方式:
1.基于 数据库 实现分布式锁:
(1)基于mysql锁表:
该实现方式完全依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。这种方式存在以下几个问题:
(1) 锁没有失效时间,解锁失败会导致死锁,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
(2) 只能是非阻塞锁,insert失败直接就报错了,无法进入队列进行重试
(3) 不可重入,同一线程在没有释放锁之前无法再获取到锁
(2)采用乐观锁增加版本号:
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
(3) 使用排他锁机制:
for update会加上排他锁,commit之后会释放锁。
2.基于 缓存(redis) 实现分布式锁:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
3.基于 ZooKeeper 实现分布式锁:
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)客户端A想获取锁就在mylock目录下创建临时顺序节点,假设为1;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)客户端B获取所有节点,判断自己不是最小节点,如果不是的话,就设置监听比自己小的节点;
(5)客户端A处理完,删除自己的节点,客户端B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这种方式具备高可用、可重入、阻塞锁特性,可以解决失效死锁问题。但是因为需要频繁的创建和删除节点,性能上不如Redis方式。
以上是关于分布式锁浅析的主要内容,如果未能解决你的问题,请参考以下文章