最近在工作做中,使用到了redis的分布式锁,主要采用了其中一个原子操作getAndSet
,并且利用了redis单线程的特性操作。这里来写另一种分布式的锁,zk实现的分布式锁。
zk的特性
1、数据结构
zk的数据结构,存储的方式为key-value,存储的结构为文件夹的结构。
所以由于是这样的结构,在同级目录下面不存在相同的节点
2、节点的特性
- 有序节点
- 持久化节点
- 临时节点
3、watch的机制
在zk中,可以设置一些监听,来监听节点的一些变化操作,做出响应。
绑定监听:getData、exists、getChildren
触发监听:create、delete、setData
查操作可以绑定监听,增、删、改可以绑定监听,且监听是一次性的,在触发一次会消失,可以循环绑定来达到一直监听的效果。
4、leader的机制
这个机制,在本次的zk分布式锁中,没有作用,但是也是zk一个很重要的点。点击查看详情
方案预想
1、 利用在同级目录下,不能创建相同的节点特性,可以利用多线程去创建一个节点,但是只能有一个线程可以创建成功,所以该线程得到锁。释放锁:释放锁时,通过删除该节点,来触发刚才没有获取到的线程的监听,让他们再次来竞争获取。如图:
结论:可以达到效果,但是如果有1000个线程发起竞争,那么在释放锁时会999个线程触发监听,重新发起创建节点的请求。性能上不够优化。羊群效应,产生过多不必要的开销。
2、利用有序的节点特性,可以让多个线程同时创建多个有序的节点,其中创建的节点最小的线程,可以获取本次的锁,在释放锁以后,删除节点,自动为排序的下一个节点获取锁。如下图:
结论:在之前的文章中,我们分析过独占锁的源码,在独占锁中,用阻塞队列来存放线程,同时在公平锁模式下,会优先用队列的第一个线程来获取锁。这里,就很类似,当001被删除后,002 就会顺势获取锁,依次获取。
代码实现
public class DispathLock implements Lock,Watcher {
private ZooKeeper zooKeeper;
private String ROOTPATH = "/lock";
private String current; //当前节点
private String pre; //前一个节点
public DispathLock() {
try {
//创建zk的链接
zooKeeper = new ZooKeeper("127.0.0.1:2181",500,this);
Stat stat = zooKeeper.exists(ROOTPATH,false);
//判断是否存在 /lock 的节点,用来存放在锁竞争过程中的数据
if(stat == null){
//不存在 /lock 创建 zooKeeper.create(ROOTPATH,"0".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void lock() {
if(tryLock()){
System.out.println(Thread.currentThread().getName() +" 获取锁成功");
return;
}
try {
waitForLock();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//等待pre节点被删除 ==上一个获取锁的线程释放锁
private void waitForLock() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists( pre, true);
if(stat !=null){
System.out.println(Thread.currentThread().getName() + "正在等待");
synchronized (pre){
Long curentTime = System.currentTimeMillis();
pre.wait(10000); //设置超时时间
Long now = System.currentTimeMillis();
if(now - curentTime>= 10000){
System.out.println(Thread.currentThread().getName()+"超过等待时间,退出竞争");
unlock();
return;
}
System.out.println(Thread.currentThread().getName() + "获取锁成功");
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
//创建临时有序的节点
try {
current = zooKeeper.create(ROOTPATH+"/","0".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName() + "获取到的节点为"+current);
List<String> list = zooKeeper.getChildren(ROOTPATH,false); //当前所有的节点
//对节点排序
list = list.stream().map(e -> {
return ROOTPATH+"/"+e;
}).sorted((a,b) -> {
return a.compareTo(b);
}).collect(Collectors.toList());
//当前节点的序号
int i = list.indexOf(current);
if(i == 0){
return true; //拿到锁了
}else{
//记录上一个节点
pre = list.get(i - 1) ;
return false;
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
System.out.println("释放锁");
try {
zooKeeper.delete(current,-1);
current = null;
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public Condition newCondition() {
return null;
}
@Override
public void process(WatchedEvent watchedEvent) {
//由于监听的是上一个的节点,所以,当process被触发时,上一个线程释放了锁,所以本线程可以去获取锁了
synchronized (pre){
pre.notify();
}
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i = 0;i<10;i++){
new Thread(() -> {
try {
countDownLatch.await();
DispathLock dispathLock = new DispathLock();
dispathLock.lock(); //获取锁
}catch (Exception e){
e.printStackTrace();
}
}).start();
countDownLatch.countDown();
}
}
}
主要采用了,方案2中的计划,简单实现了分布式锁。但是其中,没有实现重入锁,并且代码中没有只实现了公平锁,没有实现非公平锁。我们下面来看一下Curator的实现
InterProcessMutex
Curator是ZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex。
在curator中,提供了各种基于zk的工具,locks、atomic、cache等等。本节讲锁,所以看一下InterProcessMutex的简单使用:
public class CuratorDemo {
public static void main(String[] args) {
//1 重试策略:初试时间为1s 重试10次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
//2 通过工厂创建连接
CuratorFramework cf = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.build();
//3 开启连接
cf.start();
InterProcessMutex lock = new InterProcessMutex(cf,"/cur-lock");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "尝试获取锁。。。");
try {
//可重入
lock.acquire();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获得锁");
try {
System.out.println(Thread.currentThread().getName() + "执行中。。。。。。。。。。");
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完,释放锁");
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放成功");
},"线程"+i).start();
}
}
}
InterProcessMutex 分析 ,可以看到在构造时,需要传入一个根节点,在获取锁的时候,会在这个根节点下面做一些操作。直接来看获取锁的方法:
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
//判断缓存中是否有线程,有的话则说明,本次是获取重入操作,基数+1 直接返回true,不用去竞争
if ( lockData != null )
{
//重入锁 基数+1
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);
//把当前获取锁的线程放入缓存中
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
//下面是真正去获取锁的操作,相比于自己的版本,多了重试的限制
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
while ( !isDone )
{
isDone = true;
try
{
//从InterProcessMutex的构造函数可知实际driver为StandardLockInternalsDriver的实例
// 在Zookeeper中创建临时顺序节点
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
//循环等待来激活分布式锁,实现锁的公平性
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// gets thrown by StandardLockInternalsDriver when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
if ( hasTheLock )
{
return ourPath;
}
return null;
}
分为2块代码,下面为创建有序的临时节点
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
if ( lockNodeBytes != null )
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
}
else
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}
以下为公平锁的实现策略
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
boolean haveTheLock = false;
boolean doDelete = false;
try
{
if ( revocable.get() != null )
{
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
{
List<String> children = getSortedChildren();
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() )
{
haveTheLock = true;
}
else
{
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this)
{
try
{
// use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
wait(millisToWait);
}
else
{
wait();
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
结论
如需要使用基于zk的一些工具,尽量使用一些Curator中提供的。希望看完的同学,可以仔细看下面的这篇,了解zk中的leader选举过程
点击查看详情