关于C#版本 ZookeeperNetEx.Recipes的内容太少了,想要了解怎么使用就需要去了解源码。
以下按照我的学习思路分享:
1.阅读源码
我看的java版本的,不过实现原理和过程基本都是一样的。通过看源码知道使用它时大致需要做什么。
https://github.com/shayhatsor/zookeeper/tree/trunk/src/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()循环重试(我也不确定有没有更好的方法)。