在 Dropwizard/JPA/Hibernate 中自动重试 transactions/requests
Automatic retry of transactions/requests in Dropwizard/JPA/Hibernate
我目前正在分别使用 Dropwizard 框架和 dropwizard-hibernate JPA/Hibernate(使用 PostgreSQL 数据库)实现 REST API Web 服务。
我在资源中有一个方法,我用 @UnitOfWork
注释来为整个请求获取一个事务。
资源方法调用我的 DAOs which extends AbstractDAO<MyEntity>
之一的方法,用于将我的实体(MyEntity
类型)的检索或修改与数据库通信。
此 DAO 方法执行以下操作:首先它选择一个实体实例,因此从数据库中选择一行。之后,检查实体实例并根据其属性更改其某些属性。在这种情况下,应该更新数据库中的行。
我没有在任何地方指定关于缓存、锁定或事务的任何其他内容,因此我假设默认设置是某种由 Hibernate 强制执行的乐观锁定机制。
因此(我认为),当从当前线程的数据库中选择实体实例后在另一个线程中删除它时,在尝试提交事务时抛出 StaleStateException
因为应该更新的实体实例已被删除之前由另一个线程。
当使用 @UnitOfWork
注释时,我的理解是我无法捕获此异常,无论是在 DAO 方法中还是在资源方法中。
我现在可以向客户端实现一个 ExceptionMapper<StaleStateException>
for Jersey to deliver a HTTP 503 response with a Retry-After
header 或类似的东西,告诉它重试它的请求。
但我宁愿先在服务器上重试 request/transaction(由于 @UnitOfWork
注释,这里基本相同)。
是否有使用 Dropwizard 时服务器端事务重试机制的示例实现?就像重试可配置的次数(例如 3 次)然后失败并返回 exception/HTTP 503 响应。
你将如何实施?我首先想到的是另一个注释,例如 @Retry(exception = StaleStateException.class, count = 3)
,我可以将其添加到我的资源中。
对此有什么建议吗?
或者考虑到不同的 locking/transaction-related 事情,我的问题是否有替代解决方案?
我发现 a pull request in the Dropwizard repository that helped me. It basically enables the possibility of using the @UnitOfWork
注释不是资源方法。
使用它,我能够通过移动 @UnitOfWork
annotation from the resource method to the DAO method which is responsible for the data manipulation which causes the StaleStateException
从资源方法中分离会话 opening/closing 和事务 creation/committing 生命周期。
然后我能够围绕这个 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
annotation rather on a method inside a service class which makes use of the DAOs instead of directly applying it to a DAO method. In whatever class the annotation is used, remember to create a proxy of the instances with the UnitOfWorkAwareProxyFactory
。
另一种方法是使用注入框架——在我的例子中是 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;
}
}
关于此方法的一些注意事项。
重试的方法必须设计成可以多次调用而不改变任何结果(例如,如果方法以增量的形式存储临时结果,那么执行两次可能会增加两次)
数据库异常一般不会保存重试。他们必须打开一个新连接(特别是在我的情况下重试死锁时)
除此之外,此基本实现只是简单地捕获任何内容,然后将重试计数和检测委托给实现 class。比如我具体的死锁重试拦截器:
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());
它检查任何 class,以及任何用重试注释的方法。
重试的示例方法是:
@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,已经捕获了它们自己的异常。
另请注意方法 (a get) 在从我的数据源池中调用两次时如何检索新连接,以及如何在其中不进行任何修改(因此:可以安全重试)
希望对您有所帮助。
您可以通过实施 ApplicationListeners 或 RequestFilters 或类似的方法来做类似的事情,但我认为这是一种更通用的方法,可以在任何受 guice 限制的方法上重试任何类型的失败。
另请注意,guice 只能在构造 class 时拦截方法(注入带注释的构造函数等)
希望对您有所帮助,
阿图尔
我目前正在分别使用 Dropwizard 框架和 dropwizard-hibernate JPA/Hibernate(使用 PostgreSQL 数据库)实现 REST API Web 服务。
我在资源中有一个方法,我用 @UnitOfWork
注释来为整个请求获取一个事务。
资源方法调用我的 DAOs which extends AbstractDAO<MyEntity>
之一的方法,用于将我的实体(MyEntity
类型)的检索或修改与数据库通信。
此 DAO 方法执行以下操作:首先它选择一个实体实例,因此从数据库中选择一行。之后,检查实体实例并根据其属性更改其某些属性。在这种情况下,应该更新数据库中的行。
我没有在任何地方指定关于缓存、锁定或事务的任何其他内容,因此我假设默认设置是某种由 Hibernate 强制执行的乐观锁定机制。
因此(我认为),当从当前线程的数据库中选择实体实例后在另一个线程中删除它时,在尝试提交事务时抛出 StaleStateException
因为应该更新的实体实例已被删除之前由另一个线程。
当使用 @UnitOfWork
注释时,我的理解是我无法捕获此异常,无论是在 DAO 方法中还是在资源方法中。
我现在可以向客户端实现一个 ExceptionMapper<StaleStateException>
for Jersey to deliver a HTTP 503 response with a Retry-After
header 或类似的东西,告诉它重试它的请求。
但我宁愿先在服务器上重试 request/transaction(由于 @UnitOfWork
注释,这里基本相同)。
是否有使用 Dropwizard 时服务器端事务重试机制的示例实现?就像重试可配置的次数(例如 3 次)然后失败并返回 exception/HTTP 503 响应。
你将如何实施?我首先想到的是另一个注释,例如 @Retry(exception = StaleStateException.class, count = 3)
,我可以将其添加到我的资源中。
对此有什么建议吗?
或者考虑到不同的 locking/transaction-related 事情,我的问题是否有替代解决方案?
我发现 a pull request in the Dropwizard repository that helped me. It basically enables the possibility of using the @UnitOfWork
注释不是资源方法。
使用它,我能够通过移动 @UnitOfWork
annotation from the resource method to the DAO method which is responsible for the data manipulation which causes the StaleStateException
从资源方法中分离会话 opening/closing 和事务 creation/committing 生命周期。
然后我能够围绕这个 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
annotation rather on a method inside a service class which makes use of the DAOs instead of directly applying it to a DAO method. In whatever class the annotation is used, remember to create a proxy of the instances with the UnitOfWorkAwareProxyFactory
。
另一种方法是使用注入框架——在我的例子中是 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;
}
}
关于此方法的一些注意事项。
重试的方法必须设计成可以多次调用而不改变任何结果(例如,如果方法以增量的形式存储临时结果,那么执行两次可能会增加两次)
数据库异常一般不会保存重试。他们必须打开一个新连接(特别是在我的情况下重试死锁时)
除此之外,此基本实现只是简单地捕获任何内容,然后将重试计数和检测委托给实现 class。比如我具体的死锁重试拦截器:
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());
它检查任何 class,以及任何用重试注释的方法。
重试的示例方法是:
@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,已经捕获了它们自己的异常。
另请注意方法 (a get) 在从我的数据源池中调用两次时如何检索新连接,以及如何在其中不进行任何修改(因此:可以安全重试)
希望对您有所帮助。
您可以通过实施 ApplicationListeners 或 RequestFilters 或类似的方法来做类似的事情,但我认为这是一种更通用的方法,可以在任何受 guice 限制的方法上重试任何类型的失败。
另请注意,guice 只能在构造 class 时拦截方法(注入带注释的构造函数等)
希望对您有所帮助,
阿图尔