实现分布式锁的方式非常多,zookeeper、redis、数据库等均可。
zookeeper的四种节点类型
1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。
4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说,没创建一个节点都会加上节点对应的序号,先创建成功,序号越小。
监视器(watcher)
当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。
1.共享锁和排他锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。分为排他锁和共享锁。
排他锁
排他锁(Exclusive Locks, 简称 X 锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。
共享锁
共享锁(Shared Locks,简称S锁),又称为读锁。允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
1.1 实现排他锁原理
简单地说就是多个客户端同时去竞争创建同一个临时子节点,Zookeeper能够保证只有一个客户端创建成功,那么这个创建成功的客户端就获得排他锁。正常情况下,这个客户端执行完业务逻辑会删除这个节点,也就是释放了锁。如果该客户端宕机了,那么这个临时节点会被自动删除,锁也会被释放。
1.2 共享锁原理
基本原理:
创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可
基本思路如下:
1、在你指定的节点下创建一个锁目录lock(持久化节点);
2、线程X进来获取锁在lock目录下,并创建临时有序节点;
3、线程A获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;
4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);
5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。
1.3 羊群效应
羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊也会不假思索地一哄而上,全然不顾前面可能有狼或者不远处有更好的草。因此,“羊群效应”就是比喻人都有一种从众心理,从众心理很容易导致盲从,而盲从往往会陷入骗局或遭到失败。
Zookeeper分布式锁场景中的羊群效应指的是所有的客户端都尝试对一个临时节点去加锁,当一个锁被占有的时候,其他的客户端都会监听这个临时节点。一旦锁被释放,Zookeeper反向通知添加监听的客户端,然后大量的客户端都尝试去对同一个临时节点创建锁,最后也只有一个客户端能获得锁,但是大量的请求造成了很大的网络开销,加重了网络的负载,影响Zookeeper的性能。
解决方案可以参考curator框架创建Zookeeper分布式锁的机制。原理图如下:
curator分布式锁原理
步骤为:
所有客户端都尝试去创建临时有序节点以获取锁
序号最小的临时有序节点获得锁
未获取到锁的客户端给自己的上一个临时有序节点添加监听
获得锁的客户端进行自己的操作,操作完成之后删除自己的临时有序节点
当监听到自己的上一个临时有序节点释放了锁,尝试自己去加锁
操作完成之后释放锁
之后剩下的客户端重复加锁和解锁的操作
其中最核心的思路就是获取锁时创建一个临时顺序节点,顺序最小的那个才能获取到锁,之后尝试加锁的客户端就监听自己的上一个顺序节点,当上一个顺序节点释放锁之后,自己尝试加锁,其余的客户端都对上一个临时顺序节点监听,不会一窝蜂的去尝试给同一个节点加锁导致羊群效应。
下面分别通过Curator和使用zookeeper原生API来实现
使用zookeeper原生方式来实现
1.DistributeLock
package com.alen.distributed.problem.lock.zk.javaapi;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 锁,提供获取锁和解锁的方法
* <p>
* 基本思路如下:
* 1、在你指定的节点下创建一个锁目录lock;
* 2、线程X进来获取锁在lock目录下,并创建临时有序节点;
* 3、线程A获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;
* 4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);
* 5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。
*
* @author alen
* @create 2018-11-15 15:58
**/
public class DistributeLock {
private static final Logger log = LoggerFactory.getLogger(DistributeLock.class);
private static final String ROOT_LOCKS = "/LOCKS";//根节点
private final static byte[] data = {1, 2}; //节点的数据
private ZooKeeper zooKeeper;
private int sessionTimeout; //会话超时时间
private String lockID; //记录锁节点id
private CountDownLatch countDownLatch = new CountDownLatch(1);
static {
try {
ZKClient.getInstance().create(ROOT_LOCKS, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public DistributeLock() throws IOException, InterruptedException {
this.zooKeeper = ZKClient.getInstance();
this.sessionTimeout = ZKClient.getSessionTimeout();
}
//获取锁的方法
public boolean lock() {
try {
//1.通步创建/LOCKS节点下开放权限的临时顺序节点
lockID = zooKeeper.create(ROOT_LOCKS + "/", data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
log.info(Thread.currentThread().getName() + "->成功创建了lock节点[" + lockID + "], 开始去竞争锁");
//获取根节点下的所有子节点
List<String> childrenNodes = zooKeeper.getChildren(ROOT_LOCKS, true);
//排序,从小到大
SortedSet<String> sortedSet = new TreeSet<String>();
for (String children : childrenNodes) {
sortedSet.add(ROOT_LOCKS + "/" + children);
}
//2.拿到最小的节点
String first = sortedSet.first();
if (lockID.equals(first)) {
//表示当前就是最小的节点
log.info(Thread.currentThread().getName() + "->成功获得锁,lock节点为:[" + lockID + "]");
return true;
}
SortedSet<String> lessThanLockId = sortedSet.headSet(lockID);//headSet(E e)//e之前的元素,不包括e
//3.注册它上一个节点的监听
if (!lessThanLockId.isEmpty()) {
//拿到比当前LOCKID这个几点更小的上一个节点
String prevLockID = lessThanLockId.last();
//exists()方法用于监控节点变化,仅仅监控对应节点的一次数据变化,无论是数据修改还是删除!
zooKeeper.exists(prevLockID, new LockWatcher(countDownLatch));
countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
//上面这段代码意味着如果会话超时或者节点被删除(释放)了
log.info(Thread.currentThread().getName() + " 成功获取锁:[" + lockID + "]");
}
return true;
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 解锁,删除节点
* @return
*/
public boolean unlock() {
log.info(Thread.currentThread().getName() + "->开始释放锁:[" + lockID + "]");
try {
zooKeeper.delete(lockID, -1);
log.info("节点[" + lockID + "]成功被删除");
return true;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
return false;
}
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(10);
Random random = new Random();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
DistributeLock lock = null;
try {
lock = new DistributeLock();
latch.countDown();
latch.await();
lock.lock();
Thread.sleep(random.nextInt(500));
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock != null) {
lock.unlock();
}
}
}).start();
}
}
}
2.zk客户端
package com.alen.distributed.problem.lock.zk.javaapi;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* zk客户端
*
* @author alen
* @create 2018-11-15 16:04
**/
public class ZKClient {
private final static String CONNECTSTRING="127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
private static int sessionTimeout=5000;
//获取连接
public static ZooKeeper getInstance() {
final CountDownLatch conectStatus=new CountDownLatch(1);
ZooKeeper zooKeeper= null;
try {
zooKeeper = new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
if(event.getState()== Event.KeeperState.SyncConnected){
conectStatus.countDown();
}
}
});
conectStatus.await();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return zooKeeper;
}
public static int getSessionTimeout() {
return sessionTimeout;
}
}
3.watcher
package com.alen.distributed.problem.lock.zk.javaapi;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;
/**
* 当节点发生变化时,通过watcher机制,可以让客户端得到通知,
* watcher需要实现org.apache.ZooKeeper.Watcher接口。
* @author alen
* @create 2018-11-15 16:07
**/
public class LockWatcher implements Watcher {
private CountDownLatch latch;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType()== Event.EventType.NodeDeleted){
latch.countDown();
}
}
}
基于Apache的开源客户端Curator来实现分布式锁。
1.Curator实现不可重入锁
package com.alen.distributed.problem.lock.zk.cutator.reentrant;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 不可重入锁
* @author alen
* @create 2018-11-15 22:14
**/
public class ZKCuratorNOReentrantLock {
private static final Logger log = LoggerFactory.getLogger(ZKCuratorNOReentrantLock.class);
private InterProcessSemaphoreMutex lock;//不可重入锁
private static String lockPAth = "/noreentrantlock/shareLock";
private final FakeLimitedResource resource;
private static CuratorFramework curatorFramework;
private String clientName;
//zookeeper集群地址
public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
private static final int QTY = 5;
private static final int REPETITIONS = QTY * 10;
static {
CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
client.start();
curatorFramework = client;
}
public ZKCuratorNOReentrantLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
this.resource = resource;
this.clientName = clientName;
this.lock = new InterProcessSemaphoreMutex(client, lockPath);
}
//不可重入锁只能获得一次
public void doWork(long time, TimeUnit unit) throws Exception {
if (!lock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到互斥锁");
}
log.info(clientName + " 已获取到互斥锁");
if (!lock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到互斥锁");
}
log.info(clientName + " 再次获取到互斥锁");
try {
resource.use(); // 使用资源
Thread.sleep(1000 * 1);
} finally {
log.info(clientName + " 释放互斥锁");
lock.release(); // 总是在finally中释放
lock.release(); // 获取锁几次 释放锁也要几次
}
}
public static void main(String[] args) throws Exception {
final FakeLimitedResource resource = new FakeLimitedResource();
ExecutorService service = Executors.newFixedThreadPool(QTY);
try {
for (int i = 0; i < QTY; ++i) {
final int index = i;
Callable<Void> task = new Callable<Void>() {
@Override
public Void call() throws Exception {
try {
final ZKCuratorNOReentrantLock example = new ZKCuratorNOReentrantLock(curatorFramework, lockPAth, resource, "Client " + index);
for (int j = 0; j < REPETITIONS; ++j) {
example.doWork(10, TimeUnit.SECONDS);
}
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
} catch (Exception e) {
throw e;
}
}
}
2.模拟竞争资源
package com.alen.distributed.problem.lock.zk.cutator.reentrant;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author alen
* @create 2018-11-15 18:07
**/
public class FakeLimitedResource {
private final AtomicBoolean inUse = new AtomicBoolean(false);
public void use() throws InterruptedException {
// 真实环境中我们会在这里访问/维护一个共享的资源
// 这个例子在使用锁的情况下不会非法并发异常IllegalStateException /
// /但是在无锁的情况由于sleep了一段时间,很容易抛出异常
if (!inUse.compareAndSet(false, true)) {
throw new IllegalStateException("Needs to be used by one client at a time");
}
try {
Thread.sleep((long) (3 * Math.random()));
} finally {
inUse.set(false);
}
}
}
2.Curator实现可重入锁
Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,之前说过,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"羊群效应",zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。
package com.alen.distributed.problem.lock.zk.cutator.reentrant;
import com.alen.distributed.problem.lock.zk.javaapi.DistributeLock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 基于zk用cutator客户端实现的可重入锁
* @author alen
* @create 2018-11-15 17:39
**/
public class ZKCuratorReentrantLock {
private static final Logger log = LoggerFactory.getLogger(DistributeLock.class);
private InterProcessMutex lock;//可重入锁实现类
private static final String lockPAth = "/zk-curator-reentrantlock";
private final FakeLimitedResource resource;
private String clientName;
private static CuratorFramework curatorFramework;
private static final int QTY = 5;
private static final int REPETITIONS = QTY * 10;
//zookeeper集群地址
public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
static {
CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
client.start();
curatorFramework = client;
}
public ZKCuratorReentrantLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
this.resource = resource;
this.clientName = clientName;
this.lock = new InterProcessMutex(client, lockPath);
}
//可重入锁可多次获得
public void doWork(long time, TimeUnit unit)
throws Exception {
//通过acquire()获得锁,并提供超时机制:
if (!lock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到可重入锁");
}
log.info(clientName + " 已获取到可重入锁");
if (!lock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到可重入锁");
}
log.info(clientName + " 再次获取到可重入锁");
try {
resource.use(); // 使用资源
Thread.sleep(1000 * 1);
} finally {
log.info(clientName + " 释放可重入锁");
lock.release(); // 总是在finally中释放
lock.release(); // 获取锁几次 释放锁也要几次
}
}
public static void main(String[] args) throws Exception {
final FakeLimitedResource resource = new FakeLimitedResource();
ExecutorService service = Executors.newFixedThreadPool(QTY);
try {
for (int i = 0; i < QTY; ++i) {
final int index = i;
Callable<Void> task = new Callable<Void>() {
@Override
public Void call() throws Exception {
try {
final ZKCuratorReentrantLock example = new ZKCuratorReentrantLock(curatorFramework, lockPAth, resource, "Client " + index);
for (int j = 0; j < REPETITIONS; ++j) {
example.doWork(10, TimeUnit.SECONDS);
}
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
} catch (Exception e) {
throw e;
}
}
}
3.可重入读写锁InterProcessReadWriteLock
类似JDK的ReentrantReadWriteLock。一个读写锁管理一对相关的锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞)。此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁, 比如请求写锁 --->读锁 ---->释放写锁。从读锁升级成写锁是不行的。
可重入读写锁相关类介绍
可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex。使用时首先创建一个InterProcessReadWriteLock实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是InterProcessMutex。
实现
package com.alen.distributed.problem.lock.zk.cutator.reentrant;
import com.alen.distributed.problem.lock.zk.javaapi.DistributeLock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 基于zk用curator实现可重入读写锁
*
* @author alen
* @create 2018-11-15 23:41
**/
public class ZKSharedReentrantReadWriteLock {
private static final Logger log = LoggerFactory.getLogger(ZKSharedReentrantReadWriteLock.class);
private final InterProcessReadWriteLock lock;
private final InterProcessMutex readLock;
private final InterProcessMutex writeLock;
private static final String lockPAth = "/zk-SharedReentrantReadWrite-lock";
private final FakeLimitedResource resource;
private String clientName;
private static CuratorFramework curatorFramework;
private static final int QTY = 5;
private static final int REPETITIONS = QTY * 10;
//zookeeper集群地址
public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
static {
CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
client.start();
curatorFramework = client;
}
public ZKSharedReentrantReadWriteLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
this.resource = resource;
this.clientName = clientName;
lock = new InterProcessReadWriteLock(client, lockPath);
readLock = lock.readLock();
writeLock = lock.writeLock();
}
public void doWork(long time, TimeUnit unit) throws Exception {
// 注意只能先得到写锁再得到读锁,不能反过来!!!
if (!writeLock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到写锁");
}
System.out.println(clientName + " 已得到写锁");
if (!readLock.acquire(time, unit)) {
throw new IllegalStateException(clientName + " 不能得到读锁");
}
System.out.println(clientName + " 已得到读锁");
try {
resource.use(); // 使用资源
Thread.sleep(1000 * 1);
} finally {
System.out.println(clientName + " 释放读写锁");
readLock.release();
writeLock.release();
}
}
public static void main(String[] args) throws Exception {
final FakeLimitedResource resource = new FakeLimitedResource();
ExecutorService service = Executors.newFixedThreadPool(QTY);
try {
for (int i = 0; i < QTY; ++i) {
final int index = i;
Callable<Void> task = new Callable<Void>() {
@Override
public Void call() throws Exception {
try {
final ZKSharedReentrantReadWriteLock example = new ZKSharedReentrantReadWriteLock(curatorFramework, lockPAth, resource, "Client " + index);
for (int j = 0; j < REPETITIONS; ++j) {
example.doWork(10, TimeUnit.SECONDS);
}
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
} catch (Exception e) {
throw e;
}
}
}
参考:
https://blog.youkuaiyun.com/u010889616/article/details/80209629
https://blog.youkuaiyun.com/qq_16681279/article/details/78061526