分布式锁
一、什么是分布式锁?为什么需要分布式锁
锁,是用来保证线程或进程同步的工具,用于控制对共享资源的访问。
分布式锁也是锁的一种。普通的锁(例如Java中的Synchronized和ReentrantLock)无法用在多个进程中,此时就需要分布式锁来控制分布式系统对共享资源的访问。
在Java开发的分布式系统中,实现分布式锁的方式通常有这三种:基于MySQL数据库、基于Redis、基于Zookeeper。
这三种方式的共同点都是需要依靠第三方组件来保证进程之间的互斥。
1.1、分布式锁的特性
(1)互斥性:在任意时刻,只能有一个进程持有锁;
(2)无死锁:持有锁的客户端宕机了,也能保证释放锁;
(3)容错:实现分布式锁的组件要有良好的容错性和高可用性。
二、基于MySQL数据库实现分布式锁
各个进程通过读写表中的记录来进行加锁、释放锁。
2.1、实现方式
(1)使用行锁
MySQL的InnoDB引擎支持行锁和事务,行锁可用来锁住表中的某一条记录,从而判断是否可加锁。
行锁语法例如select * from table where id = 值 for update
(2)使用唯一索引
唯一索引是一种约束,若表的一个字段使用了唯一索引,则表示该字段仅有唯一的值。因此,可通过写数据的成功与否来判断是否加锁成功。
(3)状态机
一行数据中,有一个字段作为版本号,类似于Java中的CAS。
每个进程都使用update version+=1 where version = 1进行加锁,但只会有一个进程更新成功。
2.2、弊端
(1)相对于基于内存的组件,MySQL的并发量太低、操作代价太大。
(2)难以满足分布式锁的“无死锁”特性。
得设置锁的过期时间,以免进程持有锁期间宕机,可利用MySQL的事件调度机制;
为了防止进程操作未完成,锁就过期了,得使用额外的线程去进行锁的加时,这无疑又加大了访问数据库的开销。
三、基于Redis方式实现分布式锁
使用set key value NX ex 10 命令实现,表示如果key不存在,则设置该key的值为value,并且过期时间为10。
在Java中,可使用Redisson工具来简化加锁和释放锁的过程。Redisson底层使用了lua脚本来保证加锁和加锁的原子性。
3.1、看门狗机制
为了保证分布式锁的“无死锁”特性,防止某进程在持有锁的过程中宕机导致锁无法释放,需要对锁加上过期时间(即redis中key的过期时间)。
但是,过期时间可能会导致进程对资源的操作还未完成,锁就过期了。所以,当前程序需要定时地对锁进行加时,即看门狗机制。
3.2、主从架构下的分布式锁
随着并发量的提高,单台redis已经响应不过来了,从而构建redis主从架构。
通常,一台redis服务器为master,多台redis服务器为slave节点。
master节点主要用于写操作,即加锁时仅操作master节点,然后master节点将数据同步至slave节点。
slave节点用于读操作,当某个进程判断是否已加锁时,仅需要读取slave节点的数据。
3.2.1、弊端
当master节点宕机时,需要在多个slave中重新选出master节点。
从而导致某个进行加锁,将锁写入master节点时,master节点还未来得及同步数据至slave节点,master就宕机了,导致加锁失败。
3.3、RedLock算法
使用多台独立的redis服务器来保证加锁成功。
顺序向多台redis节点进行加锁,则分布式锁加锁成功的条件有两个:1)需要在超过半数的redis节点中加锁成功;2)需要在规定的时间内完成加锁。
用重试次数和加锁失败后暂停来解决多个进程争抢加锁的情况。
在极端情况中,多台独立的redis服务器也是使用主从架构,加锁后所有的master都同时宕机了,需要等待TTL时间(锁过期时间)后再重启,缺点是 在TTL时间内服务相当于暂停状态。
四、基于Zookeeper实现分布式锁
在Zookeeper中新建一个节点表示已加锁,其它的进程可以监听这一节点是否删除从而避免不断的尝试加锁。
同时,使用Zookeeper的临时节点能够保证持有锁的进程宕机后自动删除锁;ZAB协议能够保证锁的信息成功同步到各个Zookeeper节点,从而避免加锁信息丢失。
4.1、ZAB协议
ZAB全称Zookeeper Atomic Broadcast(Zookeeper原⼦消息⼴播协议),用来保证主从架构中数据一致性的算法。
通常,进程会在Zookeeper主节点中创建节点表示加锁,然后同步到各个从节点中。
同步过程:
(1)主节点收到创建节点命令后,先将该数据写到磁盘数据文件中(即内存中实际的Zookeeper是还没有创建该节点的);
(2)然后将该数据同步到各个从节点,从节点后也将该收到的数据写入各自的数据文件中,并返回ACK给主节点;
(3)当主节点收到半数以上从节点的ACK后,向从节点发送commit,表示可以将文件中的数据加载进内存中,此时才实际地在Zookeeper中创建节点。
4.2、节点的监听
Zookeeper的Watch机制可以监听节点的create、delete、setDate。当某个节点发生以上事件时,使用了Watch的进程会收到异步通知。
4.3、惊群效应
惊群效应是指当多个进程都监听了一个节点,当该节点触发了某个事件后,Zookeeper需要对所有的客户端进程发送通知,这将消耗Zookeeper服务器大量的资源。
为了解决这一问题,可以使用Zookeeper中的临时顺序节点。每个等待加锁的进程在代表锁的节点下新建一个子节点,子节点的序号递增,新的子节点仅需要监听前一个子节点即可。
即每个进程欲获取锁,先创建子节点,并获取所有子节点列表,判断当前创建的子节点是否为最小子节点,如果是最小节点占用锁执行后续代码,执行完毕,释放锁。如果不是最小节点则监听比自己前一个子节点,等待锁。
4.4、弊端
在Zookeeper主从架构中,加锁和释放锁的信息需要同步到各个从节点,并需要半数以上的从节点响应才能进行实际加锁,这需要频繁的网络通信,从而导致性能不高。