我目前正在使用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)
这样的另一个注释,我可以添加到我的资源中。
有什么建议吗?
或者考虑到不同的锁定/交易相关事项,我的问题是否存在替代解决方案?
答案 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
创建实例的代理,如拉取请求中所述。