在Dropwizard / JPA / Hibernate

时间:2016-10-09 22:07:04

标签: java hibernate jpa transactions dropwizard

我目前正在使用Dropwizard框架和dropwizard-hibernate分别使用JPA / Hibernate(使用PostgreSQL数据库)实现REST API Web服务。 我在资源中有一个方法,我用@UnitOfWork注释,以获得整个请求的一个事务。 资源方法调用我的DAO之一的方法,该方法扩展AbstractDAO<MyEntity>并用于与数据库通信检索或修改我的实体(类型MyEntity)。

DAO方法执行以下操作:首先,它选择实体实例,因此选择数据库中的一行。然后,检查实体实例,并根据其属性,可以更改其某些属性。在这种情况下,应更新数据库中的行。 我没有在任何地方指定任何关于缓存,锁定或事务的东西,所以我假设默认是由Hibernate强制执行的某种乐观锁定机制。 因此(我认为),当在当前实例中从数据库中选择实体实例后删除实体实例时,在尝试提交事务时会抛出StaleStateException因为应该更新的实体实例已被删除之前由另一个线程。

使用@UnitOfWork注释时,我的理解是我无法在DAO方法和资源方法中捕获此异常。 我现在可以为Jersey实现ExceptionMapper<StaleStateException>以向客户端发送带有Retry-After header之类的HTTP 503响应,以告诉它重试其请求。 但我宁愿首先尝试在服务器上重试请求/事务(这里因为@UnitOfWork注释而基本相同)。

使用Dropwizard时,是否存在服务器端事务重试机制的示例实现?像重试可配置的次数(例如3)然后失败并发生异常/ HTTP 503响应。 你会如何实现这个?我想到的第一件事就是像@Retry(exception = StaleStateException.class, count = 3)这样的另一个注释,我可以添加到我的资源中。 有什么建议吗? 或者考虑到不同的锁定/交易相关事项,我的问题是否存在替代解决方案?

2 个答案:

答案 0 :(得分:2)

替代方法是使用注入框架 - 在我的情况下为guice - 并为此使用方法拦截器。这是一个更通用的解决方案。

DW通过https://github.com/xvik/dropwizard-guicey

非常顺利地整合了guice

我有一个可以重试任何异常的通用实现。它与您的注释一样有效,如下所示:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {

}

拦截器随后(使用docs):

 /**
 * Abstract interceptor to catch exceptions and retry the method automatically.
 * Things to note:
 * 
 * 1. Method must be idempotent (you can invoke it x times without alterint the result) 
 * 2. Method MUST re-open a connection to the DB if that is what is retried. Connections are in an undefined state after a rollback/deadlock. 
 *    You can try and reuse them, however the result will likely not be what you expected 
 * 3. Implement the retry logic inteligently. You may need to unpack the exception to get to the original.
 * 
 * @author artur
 *
 */
public abstract class RetryInterceptor implements MethodInterceptor {

    private static final Logger log = Logger.getLogger(RetryInterceptor.class);

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        if(invocation.getMethod().isAnnotationPresent(Retry.class)) {
            int retryCount = 0;
            boolean retry = true;
            while(retry && retryCount < maxRetries()) {
                try {
                    return invocation.proceed();
                } catch(Exception e) {
                    log.warn("Exception occured while trying to executed method", e);
                    if(!retry(e)) {
                        retry = false;
                    } {
                        retryCount++;
                    }
                }
            }
        }
        throw new IllegalStateException("All retries if invocation failed");
    }

    protected boolean retry(Exception e) {
        return false;
    }

    protected int maxRetries() {
        return 0;
    }

}

有关此方法的一些注意事项。

  • 重试方法必须设计为多次调用而不改变任何结果(例如,如果方法以增量形式存储临时结果,则执行两次可能会增加两次)

  • 数据库异常通常不会保存以进行重试。他们必须打开一个新的连接(特别是在重试死锁时,这是我的情况)

除此之外,此基本实现只捕获任何内容,然后将重试计数和检测委托给实现类。例如,我的特定死锁重试拦截器:

public class DeadlockRetryInterceptor extends RetryInterceptor {

    private static final Logger log = Logger.getLogger(MsRetryInterceptor.class);

    @Override
    protected int maxRetries() {
        return 6;
    }

    @Override
    protected boolean retry(Exception e) {
        SQLException ex = unpack(e);
        if(ex == null) {
            return false;
        }
        int errorCode = ex.getErrorCode();
        log.info("Found exception: " + ex.getClass().getSimpleName() + " With error code: " + errorCode, ex);
        return errorCode == 1205;
    }

    private SQLException unpack(final Throwable t) {
        if(t == null) {
            return null;
        }

        if(t instanceof SQLException) {
            return (SQLException) t;
        }

        return unpack(t.getCause());
    }
}

最后,我可以通过以下方式将其绑定到guice:

bindInterceptor(Matchers.any(), Matchers.annotatedWith(Retry.class), new MsRetryInterceptor());

检查任何类以及使用重试注释的任何方法。

重试的示例方法是:

    @Override
    @Retry
    public List<MyObject> getSomething(int count, String property) {
        try(Connection con = datasource.getConnection();
                Context c = metrics.timer(TIMER_NAME).time()) 
        {
            // do some work
            // return some stuff
        } catch (SQLException e) {
            // catches exception and throws it out
            throw new RuntimeException("Some more specific thing",e);
        }
    }

我需要解压缩的原因是旧的遗留案例,比如这个DAO impl,已经捕获了它们自己的异常。

还要注意方法(get)在从我的数据源池调用两次时如何检索新连接,以及如何在其中进行修改(因此:重试安全)

我希望有所帮助。

你可以通过实现ApplicationListeners或RequestFilters或类似方法来做类似的事情,但我认为这是一种更通用的方法,可以在任何guice绑定的方法上重试任何类型的失败。

另请注意,guice只能在构造类时插入方法(注入带注释的构造函数等)。

希望有所帮助,

Artur

答案 1 :(得分:0)

我在Dropwizard存储库中找到了帮助我的a pull request。它基本上允许在资源方法之外使用@UnitOfWork注释。

使用这个,我能够通过将@UnitOfWork注释从资源方法移动到负责数据操作的DAO方法,从资源方法中分离会话开启/关闭和事务创建/提交生命周期这会导致StaleStateException。 然后我能够围绕这个DAO方法构建一个重试机制。

示例说明:

// class MyEntityDAO extends AbstractDAO<MyEntity>
@UnitOfWork
void tryManipulateData() {
    // Due to optimistic locking, this operations cause a StaleStateException when
    // committed "by the @UnitOfWork annotation" after returning from this method.
}

// Retry mechanism, implemented wheresoever.
void manipulateData() {
    while (true) {
        try {
            retryManipulateData();
        } catch (StaleStateException e) {
            continue; // Retry.
        }
        return;
    }
}

// class MyEntityResource
@POST
// ...
// @UnitOfWork can also be used here if nested transactions are desired.
public Response someResourceMethod() {
    // Call manipulateData() somehow.
}

当然,也可以将@UnitOfWork注释附加到服务类中的方法上,该方法使用DAO而不是直接将其应用于DAO方法。在使用注释的任何类中,请记住使用UnitOfWorkAwareProxyFactory创建实例的代理,如拉取请求中所述。