管理非对称资源使用的最佳设计模式

Posted

技术标签:

【中文标题】管理非对称资源使用的最佳设计模式【英文标题】:Best design pattern for managing asymmetrical resource use 【发布时间】:2017-10-21 15:25:59 【问题描述】:

我想就使用托管资源的最佳设计模式发表一些意见,其中涉及两种不同的资源,但您需要以与获取它们相反的顺序释放它们。

首先,让我设置场景。我们正在使用两种类型的对象文档和文档集合。文档集合实际上包含对文档的引用和每个文档的一些元数据。

最初我们有一个对称的图案,像这样流动:

    锁集合 用 Collection 做有用的事情 锁定文档 使用 Collection 和 Document 做有用的事情 解锁文档 解锁收藏

在代码中表示为:

Collection col = null;
try 
    col = getCollection("col1 name", LockMode.WRITE_LOCK);

    // Here we do any operations that only require the Collection

    Document doc = null;
    try 
        doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);

        // Here we do some operations on the document (of the Collection)

     finally 
        if (doc != null) 
            doc.close();
        
    

 finally 
    if (col != null) 
        col.close();
    

现在我们从 Java 7 开始有了 try-with-resources,我们对此进行了改进,以便 Java 代码描述自动释放资源:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) 

    // Here we do any operations that only require the Collection

    try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) 

        // Here we do some operations on the document (of the Collection)

    


我们遇到的问题是在对文档执行操作时保持集合锁定是低效的,因为其他线程必须等待,而且对文档的操作通常不需要修改集合。

因此,我们希望采用不对称模式,以便我们尽快发布该系列。流程应该是这样的:

    锁集合 用 Collection 做有用的事情 锁定文档 做任何需要收集和记录的事情(很少见) 解锁收藏 用 Document 做有用的事情 解锁文档

我想知道在代码中实现这种不对称方法的最佳模式。这显然可以通过 try/finally 等来完成,如下所示:

Collection col = null;
Document doc = null;
try 
    col = getCollection("col1 name", LockMode.WRITE_LOCK);

    // Here we do any operations that only require the Collection
    try 
        doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);

        // Here we do any operations that require both the Collection and Document (rare).

     finally 
        if (col != null) 
        col.close();
    

    // Here we do some operations on the document (of the Collection)

 finally 
    if (doc != null) 
            doc.close();
        
    

我也可以想到一个try-with-resources 方案,我们交换资源释放顺序,但我想知道这是否会使阅读代码变得更难理解。例如:

try (final ManagedRelease<Collection> mcol =
        new ManagedRelease<>(getCollection("col1 name", LockMode.WRITE_LOCK))) 

    // Here we do any operations that only require the Collection

    try (final ManagedRelease<Document> mdoc =
            mcol.withAsymetrical(mcol.resource.getDocument("doc1 name", LockMode.WRITE_LOCK))) 

        // Here we do any operations that require both the Collection and Document (rare).

      // NOTE: Collection is released here

    // Here we do some operations on the document (of the Collection)

  // NOTE: Document is released here

ManagedRelease 类:

private static class ManagedRelease<T extends AutoCloseable> implements AutoCloseable 
    final T resource;
    private Supplier<Optional<Exception>> closer;

    public ManagedRelease(final T resource) 
        this.resource = resource;
        this.closer = asCloserFn(resource);
    

    private ManagedRelease(final T resource, final Supplier<Optional<Exception>> closer) 
        this.resource = resource;
        this.closer = closer;
    

    public <U extends AutoCloseable> ManagedRelease<U> withAsymetrical(final U otherResource) 
        // switch the closers of ManagedRelease<T> and ManagedRelease<U>
        final ManagedRelease<U> asymManagedResource = new ManagedRelease<>(otherResource, closer);
        this.closer = asCloserFn(otherResource);
        return asymManagedResource;
    

    @Override
    public void close() throws Exception 
        final Optional<Exception> maybeEx = closer.get();
        if(maybeEx.isPresent()) 
            throw maybeEx.get();
        
    

    private static Supplier<Optional<Exception>> asCloserFn(final AutoCloseable autoCloseable) 
        return () -> 
            try 
                autoCloseable.close();
                return Optional.empty();
             catch (final Exception e) 
                return Optional.of(e);
            
        ;
    

我欢迎就try-with-resources 不对称资源管理方法是否明智提出意见,以及任何其他可能更合适的模式的指针。

【问题讨论】:

你的问题陈述看起来像线程同步类型。所以,我建议使用 Semaphore 来设计它。 @adamretter 请原谅我的无知,但是您使用的是什么库,因此close 类有一个close 方法。 Document 类也属于哪个库?这些信息与回答问题无关吗? @Cking 他们来自 eXist-db,但是我认为您根本不需要担心 Document 或 Collection 类的细节。对其中任何一个调用 close 只会释放关联的资源(在本例中为锁)。 @gul-md-ershad 我认为您需要解释为什么以及如何使用信号量会导致更好的设计模式。 看起来很像锁升级/降级成语。因此,您首先在 Collection 上获取写入(独占)锁以进行写入,然后在仅访问 Collection 以进行读取时降级为读取(共享)锁。可能多次从写入切换到读取并返回。但是,只有在存在适当的锁定支持时才可行。 【参考方案1】:

第一个问题似乎是未指定的预期行为。特别是,如果Collection.close 抛出Exception,会发生什么? Document 处理是否应该继续?是否应该回滚在两个锁下完成的 Document 的部分处理?

如果答案是 Collection.close 实际上从不抛出任何异常(或者你不在乎它会发生什么),恕我直言,最简单的解决方案是让你的 Collection.close 幂等,然后在中间显式调用它try-with-resources 块在适当的地方。此外,如果在封闭的Collection 上调用,使“通常的”Collection 方法引发类似IllegalStateException 的东西也很有意义。然后你的第二个例子会变成这样:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) 
    // Here we do any operations that only require the Collection

    try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) 

        // Here we do any operations that require both the Collection and Document (rare).


        // NOTE: usually Collection is released here
        col.close();
        // optionally make `col` not final and explicitly set it to `null`
        // here so IDE would notify you about any usage after this point

        // Here we do some operations on the document (of the Collection)

      
  

如果您无法更改Collection.close 代码,您可以更改您的ReleaseManager 以使close 具有幂等性。或者,您也可以将其重命名为 ResourceManager。在那里添加一个 getter 并始终仅通过该 getter 访问资源。如果在 close 之后调用,getter 将抛出 IllegalStateException

如果Collection.close 可能真的会抛出一些异常并且您确实关心这种情况,那么在不知道预期行为是什么的情况下很难提供解决方案。

【讨论】:

【参考方案2】:

我会给你一个像这样的通用、完整和链接的解决方案:

   public static void sample() 
    Resource resourceA = new Resource("A");
    Resource resourceB = new Resource("B");
    LockVisitor.create(resourceA)
        .lock()// lock A
        .doOnValue(Main::doSomething)// do for A
        .with(resourceB)// join with B
        .lock()// lock A & B (A has been locked)
        .doOnBoth(Main::doSomething)// do for A and B
        .toRight()// only need B (unlock A)
        .doOnValue(Main::doSomething)// do for B
        .close();// unlock B
  

  private static void doSomething(Resource... rs) 
    System.out.println("do with: " + Arrays.toString(rs));
  

sample 会输出你所期望的:

lock: Resource(A)
do with: [Resource(A)]
lock: Resource(B)
do with: [Resource(A), Resource(B)]
unlock: Resource(A)
do with: [Resource(B)]
unlock: Resource(B)

首先,我们应该定义可锁定资源。如何锁定以及如何解锁。

public interface Lockable extends AutoCloseable 

  void lock() throws Exception;

  void unlock() throws Exception;

  boolean isLocked();

  @Override
  default void close() throws Exception 
    unlock();
  

你可以让你的类实现这个接口,调用更清晰。

然后我们可以构建我们的LockVisitor(为了减少这个答案的长度,我删除了方法实现。You can find the complete code on github.)

import io.reactivex.functions.Consumer;

public class LockVisitor<T extends Lockable> implements AutoCloseable 
  public static <T extends Lockable> LockVisitor<T> create(T lockable) 
    return new LockVisitor<>(lockable);
  

  T value;
  Exception error;

  public LockVisitor(T value);

  public LockVisitor<T> lock();

  public LockVisitor<T> unlock();

  public LockVisitor<T> doOnValue(Consumer<T> func);

  public LockVisitor<T> doOnError(Consumer<Exception> func);

  public <B extends Lockable> TwoLockVisitor<T, B> with(LockVisitor<B> other);

  public <B extends Lockable> TwoLockVisitor<T, B> with(B other);

和我们的TwoLockVisitor一起访问两个资源:

import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;

public class TwoLockVisitor<A extends Lockable, B extends Lockable> 
  public static <A extends Lockable, B extends Lockable> TwoLockVisitor<A, B> create(A a, B b) 
    return new TwoLockVisitor<>(LockVisitor.create(a), LockVisitor.create(b));
  

  LockVisitor<A> left;
  LockVisitor<B> right;

  public TwoLockVisitor(LockVisitor<A> left, LockVisitor<B> right);

  public TwoLockVisitor<A, B> lock();

  public TwoLockVisitor<A, B> unlock();

  public TwoLockVisitor<A, B> doOnLeft(Consumer<A> func);

  public TwoLockVisitor<A, B> doOnRight(Consumer<B> func);

  public TwoLockVisitor<A, B> doOnBoth(BiConsumer<A, B> func);

  public LockVisitor<A> toLeft();

  public LockVisitor<B> toRight();

现在,您可以使用这些类以任意顺序管理您的资源。

【讨论】:

我真的很喜欢这种模式。我可以看到,通过对方法的命名进行一些更改,我可以有一个非常简洁的方法。不幸的是,在这种情况下使用 Lambda 有点困难,因为我必须与异常繁重的现有代码集成。【参考方案3】:

您的ManagedRelease 方案确实使代码难以理解。使用语言功能最直接明确地写下您的意图是这样的:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) 

    // Here we do any operations that only require the Collection


try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK;
    final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) 

    // Here we do any operations that require both the Collection and Document (rare).


try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) 

    // Here we do some operations on the document (of the Collection)


问题在于每个锁的额外释放和重新获取,而且col 超出了最后一次getDocument 调用的范围,因此它不会完全按原样编译。

我建议用不同的 ManagedRelease 概念来解决这个问题,提升一个级别。我为此设想的使用模式将像这样工作:

// The lambdas here are Supplier
try (final ReleaseManager<Collection> colManager = new ReleaseManager<>(() -> getCollection("col1 name", LockMode.WRITE_LOCK);
    final ReleaseManager<Document> docManager = new ReleaseManager<>(() -> colManager.getResource().get().getDocument("doc1 name", LockMode.WRITE_LOCK)) 

    try (final Managed<Collection> colManaged = colManager.getResource()) 

        // Here we do any operations that only require the Collection

     // Here the resource close does nothing

    try (final Managed<Collection> colManaged = colManager.getResourceForLastUse();
        final Managed<Document> docManaged = docManager.getResource()) 

        // Here we do any operations that require both the Collection and Document (rare).

     // Here the close of colManaged actually closes it, while docManaged.close() is a no-op

    try (final Managed<Document> docManaged = docManager.getResourceForLastUse()) 

        // Here we do some operations on the document (of the Collection)

     // Here the document gets closed
 // Here the managers get closed, which would close their resources if needed

这对每个块中使用哪些资源具有相同的清晰性,使用 try-with-resources 语言功能,在最后一次使用后立即释放每个资源,并且每个锁只获取一次。

对于ReleaseManager的规范:

ReleaseManager 这里是一个泛型类,它采用Supplier 作为资源,在第一次getResource() 调用时懒惰地调用它,并记住结果以备将来调用。 getResource() 返回一个在关闭时什么都不做的包装器,getResourceForLastUse() 返回一个在包装器关闭时确实关闭资源的包装器;我把它们写成同一个类,但你可以把它们改成不同的类,我不确定它是否真的让任何事情更清楚。

ReleaseManager 本身也实现了AutoCloseable,它的close() 实现是一个故障保护,如果资源已被获取但未关闭,则将其关闭。我会考虑让它也以某种方式记录警告,以引起注意,以防资源的最后一次使用未被正确声明为最后一次使用。最后一个考虑是,如果资源已经关闭,两种资源检索方法都应该抛出。

如果你喜欢这个解决方案,我将把 ReleaseManager 的实现留给你作为练习。

【讨论】:

在我看来这不是一个好的解决方案。特别是,拥有锁的全部意义在于我们连续执行一段代码,这可能会破坏其间的一些不变量,但最终会修复它们。所以这个解决方案的主要问题不是双重获取释放的性能。事实上,其他一些线程可以在两次获取之间为 Collection 获取锁是你的线程,它可能会看到不一致的状态。 @SergGr 这是最初的“这表达了你的意图”块的问题,而不是我的实际解决方案。没有其他线程可以在两次获取之间获取锁,因为只有一次获取。 ReleaseManager 在第一次调用资源时获取锁,直到最后一次使用资源完成后才释放它。 我不喜欢这种设计,因为如果不深入研究实现细节,很难从这段代码中猜出getResourceForLastUse 是一个如此特殊的功能。此外,如果需要两个锁的逻辑是有条件的,那么提前释放Collection 锁的代码会看起来很奇怪(或者将依赖于iftry-with-resource 的顺序,这也很丑陋)。在这种情况下,docManager 的正确工作也高度依赖于实现。此外,绝对不清楚为什么colManager.getResource() 调用被包装成try-with-resource 我的意思是你的 getResource 在代码中被调用了 3 次,并且三个调用中只有一个包含在 try-with-resource 中。为什么?如果Managed 在其close 中什么都不做,为什么它会返回Managed?鉴于这样的签名,我认为很难猜到getResourceclose 中什么都不做,但getResourceForLastUse 实际上关闭了。这正是我在发表第一条评论时犯的错误。附言这是对似乎已被删除的评论的回答,但我留下了我的评论,因为它澄清了我的疑虑 嘿@Douglas,这是一种非常有趣的方法,我真的很喜欢它在技术上的工作方式。我认为主要的混淆来自getResourcegetResourceForLastUse 的命名,正如您与@SergGr 的讨论所证明的那样;这可能使用户难以理解锁定的范围。我确实认为它有一些魅力,这不是我会想到的方法。感谢您的建议,它确实让我深思:-)

以上是关于管理非对称资源使用的最佳设计模式的主要内容,如果未能解决你的问题,请参考以下文章

软考 系统架构设计师系统安全分析与设计① 安全基础技术

手机客户端数据传输加密设计

互联网API开放平台安全设计-接口安全加密传输对称加密与非对称加密

记一次接口数据签名设计(RSA非对称加密)

存储的战争第六集——对称加密与非对称加密

C#-C#中的Dispose模式