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 方式.
来源: http://www.bubuko.com/infodetail-2463091.html