1、概念
分布式锁出现的原因:单体应用单机部署环境下,为了解决多线程并发问题,我们会使用ReentrantLcok或synchronized来解决互斥问题;但业务的需求,单机部署演变成分布式系统后,在分布式部署环境下,原单机部署使用的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁特征:一个方法在同一时间只能被一个机器的一个线程执行;阻塞锁(没有获取到锁,进行等待);非阻塞锁(没有获取到锁,返回失败);锁失效;可重入;高性能、高可靠获取与释放锁;
2、三种实现方式:基于数据库、基于Redis、基于Zookeeper
1)基于数据库
数据库中创建一张表,表中包含方法名在内的多个字段;方法名字段创建唯一索引;
获取与释放:想要执行某个方法,就将方法名插入到数据表中,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
优点:
- 借助数据库,实现方式比较简单;
缺点:
- 基于数据库实现,数据库的可用性和性能将直接影响分布式锁的可用性及性能,数据库需要双机部署数据同步、主备切换;
- 不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
- 没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
- 不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
2)基于Redis
在Redis2.6.12版本之前,使用setnx命令设置key-value、使用expire命令设置key的过期时间获取分布式锁,使用del命令释放分布式锁;
如果setnx成功返回1,说明获得锁,当程序执行完成后删除键到达释放锁的目的,如果setnx失败返回0,说明未获得锁,可通过循环等待继续获取;如果程序获得锁后,断开了与Redis的连接,锁未进行释放,则程序发生死锁,但因有超时时间;
多种情形问题:
- setnx命令设置完key-value后,还没来得及使用expire命令设置过期时间,当前线程挂掉了,会导致当前线程设置的key一直有效,后续线程无法正常通过setnx获取锁,造成死锁;解决方法是因为两个命令是分开执行并且不具备原子特性,如果能将这两个命令合二为一就可以解决问题了,在Redis2.6.12版本中实现了这个功能,Redis为set命令增加了一系列选项,可以通过SET resource_name my_random_value NX PX max-lock-time来获取分布式锁,这个命令仅在不存在key(resource_name)的时候才能被执行成功(NX选项),并且这个key有一个max-lock-time秒的自动失效时间(PX属性)。这个key的值是“my_random_value”,它是一个随机值,这个值在所有的机器中必须是唯一的,用于安全释放锁。
- 在分布式环境下,线程A通过这种实现方式获取到了锁,但是在获取到锁之后,执行被阻塞了,导致该锁失效,此时线程B获取到该锁,之后线程A恢复执行,执行完成后释放该锁,直接使用del命令,将会把线程B的锁也释放掉,而此时线程B还没执行完,将会导致不可预知的问题;解决方法是释放锁的时候,只有key存在并且存储的“my_random_value”值和指定的值一样才执行del命令;
- 为了实现高可用,将会选择主从复制机制,但是主从复制机制是异步的,会出现数据不同步的问题,可能导致多个机器的多个线程获取到同一个锁;解决方法是因为采用了主从复制导致的问题,解决方案是不采用主从复制,使用RedLock算法;
RedLock描述如下:在Redis的分布式环境中,假设有5个Redis master,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端应该执行以下操作:
- 获取当前Unix时间,以毫秒为单位;
- 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例;
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果);
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
优点:
- 高性能,借助Redis实现比较方便;
缺点:
- 线程获取锁后,如果处理时间过长会导致锁超时失效,所以,通过锁超时机制不是十分可靠;
3)基于Zookeeper
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
- 创建一个目录mylock;
- 线程A想获取锁就在mylock目录下创建临时顺序节点;
- 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
- 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:
- 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:
- 因为需要频繁的创建和删除节点,性能上不如Redis方式。