一、分布式锁介绍
分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。
二、架构介绍
在介绍使用Zookeeper实现分布式锁之前,首先看当前的系统架构图
解释: 左边的整个区域表示一个Zookeeper集群,locker是Zookeeper的一个持久节点,node_1、node_2、node_3是locker这个持久节点下面的临时顺序节点。client_1、client_2、client_n表示多个客户端,Service表示需要互斥访问的共享资源。
三、分布式锁获取思路
1.获取分布式锁的总体思路
在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用createNode方法在locker下创建临时顺序节点,
然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。客户端获取到所有的子节点path之后,如果发现自己在之
前创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,
此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。之后,让这个被关注的节点删除,则客户端的Watcher会
收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如皋是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个
节点并注册监听。当前这个过程中还需要许多的逻辑判断。
2.获取分布式锁的核心算法流程
下面同个一个流程图来分析获取分布式锁的完整算法,如下:
解释:客户端A要获取分布式锁的时候首先到locker下创建一个临时顺序节点(node_n),然后立即获取locker下的所有(一级)子节点。
此时因为会有多个客户端同一时间争取锁,因此locker下的子节点数量就会大于1。对于顺序节点,特点是节点名称后面自动有一个数字编号,
先创建的节点数字编号小于后创建的,因此可以将子节点按照节点名称后缀的数字顺序从小到大排序,这样排在第一位的就是最先创建的顺序节点,
此时它就代表了最先争取到锁的客户端!此时判断最小的这个节点是否为客户端A之前创建出来的node_n,如果是则表示客户端A获取到了锁,
如果不是则表示锁已经被其它客户端获取,因此客户端A要等待它释放锁,也就是等待获取到锁的那个客户端B把自己创建的那个节点删除。
此时就通过监听比node_n次小的那个顺序节点的删除事件来知道客户端B是否已经释放了锁,如果是,此时客户端A再次获取locker下的所有子节点,
再次与自己创建的node_n节点对比,直到自己创建的node_n是locker的所有子节点中顺序号最小的,此时表示客户端A获取到了锁!
四、分布式锁使用
1.可重入锁Shared Reentrant Lock
2.不可重入锁Shared Lock
3.可重入读写锁Shared Reentrant Read Write Lock
4.信号量Shared Semaphore
5.多锁对象 Multi Shared Lock
1.可重入锁相关类介绍
// 构造方法
public InterProcessMutex(CuratorFramework client, String path)
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
// 通过acquire获得锁,并提供超时机制:
public void acquire() throws Exception
public boolean acquire(long time, TimeUnit unit) throws Exception
// 撤销锁
public void makeRevocable(RevocationListener<InterProcessMutex> listener)
public void makeRevocable(final RevocationListener<InterProcessMutex> listener, Executor executor)
错误处理:还是强烈推荐你使用ConnectionStateListener处理连接状态的改变。当连接LOST时你不再拥有锁。
public class FakeLimitedResource
{
private final AtomicBoolean inUse = new AtomicBoolean(false);
// 模拟只能单线程操作的资源
public void use() throws InterruptedException
{
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);
}
}
}
然后创建一个ExampleClientThatLocks类,它负责请求锁,使用资源,释放锁这样一个完整的访问过程。
public class ExampleClientThatLocks
{
private final InterProcessMutex lock;
private final FakeLimitedResource resource;
private final String clientName;
public ExampleClientThatLocks(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName)
{
this.resource = resource;
this.clientName = clientName;
lock = new InterProcessMutex(client, lockPath);
}
public void doWork(long time, TimeUnit unit) throws Exception
{
if (!lock.acquire(time, unit))
{
throw new IllegalStateException(clientName + " 不能得到互斥锁");
}
try
{
System.out.println(clientName + " 已获取到互斥锁");
resource.use(); // 使用资源
Thread.sleep(1000 * 1);
}
finally
{
System.out.println(clientName + " 释放互斥锁");
lock.release(); // 总是在finally中释放
}
}
}
最后创建主程序来测试:
public class InterProcessMutexExample
{
private static final int QTY = 5;
private static final int REPETITIONS = QTY * 10;
private static final String PATH = "/examples/locks";
public static void main(String[] args) throws Exception
{
final FakeLimitedResource resource = new FakeLimitedResource();
final List<CuratorFramework> clientList = new ArrayList<CuratorFramework>();
for (int i = 0; i < QTY; i++)
{
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
clientList.add(client);
}
System.out.println("连接初始化完成!");
ExecutorService service = Executors.newFixedThreadPool(QTY);
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 ExampleClientThatLocks example = new ExampleClientThatLocks(clientList.get(index), PATH, resource, "Client " + index);
for (int j = 0; j < REPETITIONS; ++j)
{
example.doWork(10, TimeUnit.SECONDS);
}
}
catch (Throwable e)
{
e.printStackTrace();
}
finally
{
CloseableUtils.closeQuietly(clientList.get(index));
}
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
System.out.println("OK!");
}
}
连接初始化完成!
Client 4 已获取到互斥锁
Client 4 释放互斥锁
Client 3 已获取到互斥锁
Client 3 释放互斥锁
......
Client 2 已获取到互斥锁
Client 2 释放互斥锁
OK!
运行时查看Zookeeper节点信息如下:
2.不可重入锁Shared Lock
这个锁和上面的相比,就是少了Reentrant的功能,也就意味着它不能在同一个线程中重入。这个类是InterProcessSemaphoreMutex使用方法和上面的类类似
首先我们将上面的例子修改一下,测试一下它的重入。修改ExampleClientThatLocks.doWork,连续两次acquire:
public void doWork(long time, TimeUnit unit) throws Exception
{
if (!lock.acquire(time, unit))
{
throw new IllegalStateException(clientName + " 不能得到互斥锁");
}
System.out.println(clientName + " 已获取到互斥锁");
if (!lock.acquire(time, unit))
{
throw new IllegalStateException(clientName + " 不能得到互斥锁");
}
System.out.println(clientName + " 再次获取到互斥锁");
try
{
resource.use(); // 使用资源
Thread.sleep(1000 * 1);
}
finally
{
System.out.println(clientName + " 释放互斥锁");
lock.release(); // 总是在finally中释放
lock.release(); // 获取锁几次 释放锁也要几次
}
}
3.可重入读写锁Shared Reentrant Read Write Lock
public class ExampleClientReadWriteLocks
{
private final InterProcessReadWriteLock lock;
private final InterProcessMutex readLock;
private final InterProcessMutex writeLock;
private final FakeLimitedResource resource;
private final String clientName;
public ExampleClientReadWriteLocks(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();
}
}
}
连接初始化完成!
Client 1 已得到写锁
Client 1 已得到读锁
Client 1 释放读写锁
......
Client 3 已得到写锁
Client 3 已得到读锁
Client 3 释放读写锁
OK!
此时查看Zookeeper数据节点如下:
4.信号量Shared Semaphore
- InterProcessSemaphoreV2 - 信号量实现类
- Lease - 租约(单个信号)
- SharedCountReader - 计数器,用于计算最大租约数量
public void returnLease(Lease lease)
public void returnAll(Collection<Lease> leases)
注意一次你可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。同时还提供了超时的重载方法。
public Lease acquire() throws Exception
public Collection<Lease> acquire(int qty) throws Exception
public Lease acquire(long time, TimeUnit unit) throws Exception
public Collection<Lease> acquire(int qty, long time, TimeUnit unit) throws Exception
2.编写示例程序
public class InterProcessSemaphoreExample
{
private static final int MAX_LEASE = 10;
private static final String PATH = "/examples/locks";
public static void main(String[] args) throws Exception
{
FakeLimitedResource resource = new FakeLimitedResource();
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, PATH, MAX_LEASE);
Collection<Lease> leases = semaphore.acquire(5);
System.out.println("获取租约数量:" + leases.size());
Lease lease = semaphore.acquire();
System.out.println("获取单个租约");
resource.use();
Collection<Lease> leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);
System.out.println("获取租约,如果为空则超时: " + leases2);
System.out.println("释放租约");
semaphore.returnLease(lease);
System.out.println("释放集合中的所有租约");
semaphore.returnAll(leases);
client.close();
System.out.println("OK!");
}
}
获取租约数量:5
获取单个租约
获取租约,如果为空则超时: null
释放租约
释放集合中的所有租约
OK!
此时查看Zookeeper数据节点如下:
注意:上面所讲的4种锁都是公平锁(fair)。从ZooKeeper的角度看,每个客户端都按照请求的顺序获得锁。相当公平。