java - 在 Dropwizard/JPA/Hibernate 中自动重试事务/请求

标签 java hibernate jpa transactions dropwizard

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

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

使用 @UnitOfWork 时注释,我的理解是我无法在 DAO 中捕获此异常方法也不在资源方法中。 我现在可以实现 ExceptionMapper<StaleStateException> 为 Jersey 提供带有 Retry-After header 的 HTTP 503 响应或类似的东西告诉客户重试其请求。 但我宁愿先在服务器上重试请求/事务(由于 @UnitOfWork 注释,这里基本上是相同的)。

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

最佳答案

另一种方法是使用注入(inject)框架 - 在我的例子中是 guice - 并为此使用方法拦截器。这是一个更通用的解决方案。

DW通过https://github.com/xvik/dropwizard-guicey非常顺利的和guice集成了

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

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

}

然后拦截器执行(使用文档):

 /**
 * 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;
    }

}

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

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

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

除此之外,这个基本实现只是简单地捕获任何内容,然后将重试计数和检测委托(delegate)给实现类。比如我具体的死锁重试拦截器:

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

最后,我可以通过以下方式将其绑定(bind)到 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 只能在构造类时拦截方法(注入(inject)带注释的构造函数等)

希望对你有帮助,

阿图尔

关于java - 在 Dropwizard/JPA/Hibernate 中自动重试事务/请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39949017/

相关文章:

java - 使用@EmbeddedId 进行映射时出现 Eclipse 错误

JPA Criteria 选择组中具有最大值的所有实例

Eclipse上Java编译错误

java - Spring Data jpa 如何知道属性发生了变化?

java - 套接字序列化速度变慢

java - 是否可以在不迭代的情况下加载具有所有惰性属性的对象?

java - MySQl 和 autoReconnect=true 仍然给我异常

java - 查找迭代器中内容之间的持续时间(以月为单位)

java - 自动提交和@Transactional 以及与 spring、jpa 和 hibernate 的级联

java - 获取结果和计数/过滤列表的最佳策略