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



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

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

最初我们有一个对称的模式,它像一样流动

  1. 锁定集合
  2. 使用Collection做有用的事情
  3. 锁定文档
  4. 使用集合和文档做有用的事情
  5. 解锁文档
  6. 解锁收藏

并且在代码中表示为:

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();
}
}

既然我们从Java7开始就有了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锁定是低效的,因为其他线程必须等待,而且对文档的操作通常不需要修改Collection。

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

  1. 锁定集合
  2. 使用Collection做有用的事情
  3. 锁定文档
  4. 做任何需要收集和文档的事情(罕见)
  5. 解锁收藏
  6. 使用文档做有用的事情
  7. 解锁文档

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

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不对称资源管理方法是否明智的意见,以及任何可能更合适的其他模式的指针。

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

如果答案是Collection.close实际上从来没有抛出任何异常(或者你不在乎它会发生什么),那么IMHO最简单的解决方案是使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实际上可能会抛出一些异常,并且您确实关心这些场景,那么在不知道预期行为的情况下,很难提供解决方案

我将为您提供一个通用的、完整的、链接的解决方案,如下所示:

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(为了减少这个答案的长度,我删除了方法实现。你可以在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();
}

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

您的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的实现作为一个练习留给您。

最新更新