ZookeeperNetEx.Recipes(C#)开源包分布式锁源码走读及使用

关于C#版本 ZookeeperNetEx.Recipes的内容太少了,想要了解怎么使用就需要去了解源码。

以下按照我的学习思路分享:

1.阅读源码

我看的java版本的,不过实现原理和过程基本都是一样的。通过看源码知道使用它时大致需要做什么。
https://github.com/apache/zookeeper/tree/master/zookeeper-recipes/zookeeper-recipes-lock

Test是用分布式锁实现选举的例子。

大致就是这5个类,先简单介绍以下:
在这里插入图片描述

  • LockListener 封装得到锁之后要进行的操作 lockAcquire()lockRelease()
  • ProtocolSupportWriteLock的父类,实现了重试等基础功能;
  • 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)再看一下operationexecute() (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触发后上锁成功的是没有释放锁逻辑的,所以需要我们自己加)。

在这里插入图片描述
如果是最小的,那就执行
在这里插入图片描述
当执行完这句,所以逻辑就结束了,会回到WriteLockLock
在这里插入图片描述
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()

LockListenerlockAcquired()里,当执行完了 countDownEvent.Signal();

这样当执行完后,主线程就解除阻塞了,然后再writelock.unlock()就可以了。

模拟结果:
在这里插入图片描述

4.2 LockAsync同步锁问题

4.1的方案里有个问题,就是其实执行countDownEvent.Signal()是在LockListenerlockAquire()方法里,然后要一步一步returnwritelockLock()里才释放同步锁。但countDownEvent.Signal(),执行完主线程就醒了,可以执行unlock()了。但unlock()需要先获得同步锁,这里如果cpu给不同线程分配的不好的话(非常不幸运才会出现),可能lock线程还没释放锁,主线程就去unlock了,这时因为无法获得锁,unlock()失败。

所以4.1也不能在Lock()方法里调用unlock();

我想到的解决方案就是unlock()循环重试(我也不确定有没有更好的方法)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
什么是ZooKeeperZooKeeper是一个分布式的,开放源码分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能括:配置维护、域名服务、分布式同步、组服务等。 ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 Rabbit ZooKeeper Extensions 该项目使用了 Apache ZooKeeper .NET async Client 组件,除提供了基本的zk操作,还额外封装了常用的功能以便让.net开发者更好的使用zookeeper。 提供的功能 session过期重连 永久watcher 递归删除节点 递归创建节点 跨平台(支持.net core) 使用说明 创建连接 IZookeeperClient client = neZookeeperClient(neZookeeperClientOptions         {             ConnectionString = "172.18.20.132:2181",             BasePath = "/", //default value             ConnectionTimeout = TimeSpan.FromSeconds(10), //default value             SessionTimeout = TimeSpan.FromSeconds(20), //default value             OperatingTimeout = TimeSpan.FromSeconds(60), //default value             ReadOnly = false, //default value             SessionId = 0, //default value             SessionPasswd = null //default value         }); 创建节点 var data = Encoding.UTF8.GetBytes("2016"); //快速创建临时节点 await client.CreateEphemeralAsync("/year", data); await client.CreateEphemeralAsync("/year", data, ZooDefs.Ids.OPEN_ACL_UNSAFE); //快速创建永久节点 await client.CreatePersistentAsync("/year", data); await client.CreatePersistentAsync("/year", data, ZooDefs.Ids.OPEN_ACL_UNSAFE); //完整调用 await client.CreateAsync("/year", data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); //递归创建 await client.CreateRecursiveAsync("/microsoft/netcore/aspnet", data, CreateMode.PERSISTENT); 获取节点数据 IEnumerable data = await client.GetDataAsync("/year"); Encoding.UTF8.GetString(data.ToArray()); 获取子节点 IEnumerable children= await client.GetChildrenAsync("/microsoft"); 判断节点是否存在 bool exists = await client.ExistsAsync("/year"); 删除节点 await client.DeleteAsync("/year"); //递归删除 bool success = await client.DeleteRecursiveAsync("/microsoft"); 更新数据 Stat stat = await client.SetDataAsync("/year", Encoding.UTF8.GetBytes("2017")); 订阅数据变化 await client.Subscrib
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值