关于C#版本 ZookeeperNetEx.Recipes
的内容太少了,想要了解怎么使用就需要去了解源码。
以下按照我的学习思路分享:
1.阅读源码
我看的java版本的,不过实现原理和过程基本都是一样的。通过看源码知道使用它时大致需要做什么。
https://github.com/apache/zookeeper/tree/master/zookeeper-recipes/zookeeper-recipes-lock
Test是用分布式锁实现选举的例子。
大致就是这5个类,先简单介绍以下:
LockListener
封装得到锁之后要进行的操作lockAcquire()
和lockRelease()
;ProtocolSupport
是WriteLock
的父类,实现了重试等基础功能;WriteLock
是分布式的主体,我们在使用的时候就是new
这个类,里面主要方法有lock()
和unlock()
;ZNodeName
是对zk
节点名的封装,分布式锁需要创建临时顺序节点,这个类就包含order
,节点名比较等;ZookeeperOperation
是对Callback
逻辑的再封装,主要方法execute()
;
2.使用
从上面的简单分析,大致尝试一下使用:
2.1自己实现一个LockListener
主要就是lockAcquired
获得锁时执行的内容,和lockReleased
释放锁时的事情。
CountDownEvent
可以先不看(4.1会说)。
class WriteLockListener : LockListener
{
string name { get; set; }
CountdownEvent count;
public WriteLockListener(string name, CountdownEvent count)
{
this.name = name;
this.count = count;
}
public Task lockAcquired()
{
Console.WriteLine(name + " accquired lock");
Console.WriteLine(name + " do sth");
Thread.Sleep(1000);
count.Signal();
return null;
}
public Task lockReleased()
{
int id = Thread.GetCurrentProcessorId();
Console.WriteLine(name + " released lock");
return null;
}
}
2.2 WriteLock
登场
ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 20000, null, false);
//new 一个WriteLock 需要传入一个zk实例
WriteLock writelock = new WriteLock(zk, "/lock", null);
//CountdownEvent count = new CountdownEvent(1); 可以先忽略
//自己实现的LockListener
WriteLockListener listener = new WriteLockListener(name,count);
//向writelock里set listener,这样writelock就知道该执行什么了
writelock.setLockListener(listener);
2.3执行过程:
//执行
writelock.Lock();
先看下WriteLock
结构:
继承了ProtocolSupport
所以ProtocolSupport
的方法(如重试)可以直接用;
AsyncLock
是用来上锁的,writelock.Lock()
,Unlock()
都需要通过它获得锁,这样就保证在获取锁执行过程中不被unlock()
打断。
callback
就是我们传入的自定义的LockListener
了
zop
也是用来执行操作的。
(1)writelock.Lock()
这里用using
获得锁,using
之后会自动释放锁(成功或异常都会)。
(2)retryOperation()
是ProtocolSupport()
的方法:
这里就是调用zop
中的execute()
.
解释一下重试逻辑:
创建的zk客户端实例自带重连功能,所以ConnectionLossException
时不需要做什么特别的,等待zk去重连;
如果重连了很久连不上,就会出现会话过期,SessionExpiredException
,这时就不行了。
(3)再看一下operation
的execute()
(ZooKeeperOperation
类在WriteLock.cs
中)
这一段就是核心代码了:
public async Task<bool> execute()
{
do
{
if (writeLock.Id == null)
{
long sessionId = writeLock.zookeeper.getSessionId();
string prefix = "x-" + sessionId + "-";
// lets try look up the current ID if we failed
// in the middle of creating the znode
await findPrefixInChildren(prefix, writeLock.zookeeper, writeLock.dir).ConfigureAwait(false);
writeLock.idName = new ZNodeName(writeLock.Id);
}
if (writeLock.Id != null)
{
List<string> names = (await writeLock.zookeeper.getChildrenAsync(writeLock.dir).ConfigureAwait(false)).Children;
if (names.Count == 0)
{
LOG.warn("No children in: " + writeLock.dir + " when we've just " + "created one! Lets recreate it...");
// lets force the recreation of the id
writeLock.Id = null;
}
else
{
// lets sort them explicitly (though they do seem to come back in order ususally :)
SortedSet<ZNodeName> sortedNames = new SortedSet<ZNodeName>();
foreach (string name in names)
{
sortedNames.Add(new ZNodeName(writeLock.dir + "/" + name));
}
writeLock.ownerId = sortedNames.Min.Name;
SortedSet<ZNodeName> lessThanMe = new SortedSet<ZNodeName>();
foreach (ZNodeName name in sortedNames) {
if (writeLock.idName.CompareTo(name) > 0) lessThanMe.Add(name);
else break;
}
if (lessThanMe.Count > 0)
{
ZNodeName lastChildName = lessThanMe.Max;
writeLock.lastChildId = lastChildName.Name;
if (LOG.isDebugEnabled())
{
LOG.debug("watching less than me node: " + writeLock.lastChildId);
}
Stat stat = await writeLock.zookeeper.existsAsync(writeLock.lastChildId, new LockWatcher(writeLock)).ConfigureAwait(false);
if (stat != null)
{
return false;
}
LOG.warn("Could not find the" + " stats for less than me: " + lastChildName.Name);
}
else
{
if (writeLock.Owner)
{
var tempCallback = writeLock.callback.Value;
if (tempCallback != null) {
await tempCallback.lockAcquired().ConfigureAwait(false);
}
return true;
}
}
}
}
} while (writeLock.Id == null);
return false;
}
I.找到父节点(如果没有会创建),创建自己的节点(findPrefixInChildren
)
II.然后获取父节点的子节点,放到SortedSet
中。
III.判断是否获得锁
如果当前节点不是最小的,就找到前一个节点注册一个LockWatcher()
。
(注意这里有个do{ }while(writelock.Id!=null)
,所以如果出现再注册前一节点前,前一节点就下线了的情况,就会循环再次获取所有子节点,再找前一个)。
LockWatcher
的逻辑比较简单,就是传入我们的writelock
,然后触发该watcher
时,process
方法里就是调用writelock.Lock();
执行上锁逻辑。(这里注意如果是被watcher
触发后上锁成功的是没有释放锁逻辑的,所以需要我们自己加)。
如果是最小的,那就执行
当执行完这句,所以逻辑就结束了,会回到WriteLock
的Lock
里
using
释放锁,结束。
(4)再看一眼unlock()
方法
主要逻辑就是删除节点。注意这里也要上锁。
/// <summary>
/// Removes the lock or associated znode if
/// you no longer require the lock. this also
/// removes your request in the queue for locking
/// in case you do not already hold the lock. </summary>
public async Task unlock()
{
using(await lockable.LockAsync().ConfigureAwait(false))
{
if (Id != null)
{
// we don't need to retry this operation in the case of failure
// as ZK will remove ephemeral files and we don't wanna hang
// this process when closing if we cannot reconnect to ZK
try
{
ZooKeeperOperation zopdel = new DeleteNode(this);
await zopdel.execute().ConfigureAwait(false);
}
catch (KeeperException.NoNodeException)
{
// do nothing
}
catch (KeeperException e)
{
LOG.warn("Caught: " + e, e);
throw;
}
finally
{
var tempCallback = callback.Value;
if (tempCallback != null)
{
await tempCallback.lockReleased().ConfigureAwait(false);
}
Id = null;
}
}
}
}
4.一些使用时的问题
我的实现:
LockListener
internal class WriteLockListener : LockListener
{
private string name;
private Action acquireHandler;
private Action releaseHandler;
private CountdownEvent count;
public WriteLockListener(string name, CountdownEvent count)
{
this.name = name;
this.count = count;
}
public Task lockAcquired()
{
if (acquireHandler != null)
{
acquireHandler.Invoke();
}
count.Signal();
return Task.FromResult(0);
}
public Task lockReleased()
{
if (releaseHandler != null)
{
releaseHandler.Invoke();
}
return Task.FromResult(0);
}
public void SetAcquireHandler(Action handler)
{
acquireHandler = handler;
}
public void SetReleaseHandler(Action handler)
{
releaseHandler = handler;
}
}
}
上锁线程:
public async Task<bool> TryLock()
{
try
{
await writelock.Lock().ConfigureAwait(false);
count.Wait();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return false;
}
finally
{
await writelock.unlock().ConfigureAwait(false);
}
return true;
}
4.0 writelock.Lock()
和writelock.unlock()
一定要await
异步方法如果不加await
是捕获不到异常的!
4.1主线程怎么写,它怎么知道lock
操作执行完了,什么时候调用unlock()
释放锁?CountDownEvent
的作用?
如果幸运第一次就获得锁了,那直接Lock()
, 然后unlock()
就行了;但不幸的是如果利用监听,等事件到来时,Recipes
为我们实现的LockWatcher
是没有释放锁的逻辑的。那我们主线程怎么知道事件触发它并获得锁了,又该什么时候去释放呢?
这里可以借用一个CountDownEvent(1)
,或者SemoporeSlim
也行。
主线程writelock.Lock()
后,countDownEvent.wait();
LockWatcher
被触发,调用process()->writelock.Lock()->zop.execute()->locklistener(callback).lockAcquired()
在LockListener
的lockAcquired()
里,当执行完了 countDownEvent.Signal();
这样当执行完后,主线程就解除阻塞了,然后再writelock.unlock()
就可以了。
模拟结果:
4.2 LockAsync
同步锁问题
4.1的方案里有个问题,就是其实执行countDownEvent.Signal()
是在LockListener
的lockAquire()
方法里,然后要一步一步return
到writelock
的Lock()
里才释放同步锁。但countDownEvent.Signal()
,执行完主线程就醒了,可以执行unlock()
了。但unlock()
需要先获得同步锁,这里如果cpu给不同线程分配的不好的话(非常不幸运才会出现),可能lock
线程还没释放锁,主线程就去unlock
了,这时因为无法获得锁,unlock()
失败。
所以4.1也不能在Lock()
方法里调用unlock();
我想到的解决方案就是unlock()
循环重试(我也不确定有没有更好的方法)。