现在你已经看到了所有构成部分是如何工作的,我们可以把它们放在一起做一些有用的事情。
业务服务的执行有时会因为并发问题而失败(例如,死锁失败者)。如果操作被重试,那么它很可能在下一次尝试中成功。对于在这种情况下适合重试的业务服务(不需要回到用户那里解决冲突的 idempotent 操作),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显跨越服务层中多个服务的需求,因此,非常适合通过一个切面来实现。
因为我们想重试操作,所以我们需要使用 环绕通知,以便我们可以多次调用 Proceed。下面的列表显示了基本切面的实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意,这个切面实现了 Ordered 接口,这样我们就可以把切面的优先级设置得比事务 advice 高(我们希望每次重试都是一个新的事务)。 maxRetries 和 order 属性都是由 Spring 配置的。主要的动作发生在 advice 周围的 doConcurrentOperation 中。请注意,就目前而言,我们将重试逻辑应用于每个 businessService()
。我们尝试进行,如果失败了,出现 PessimisticLockingFailureException,我们就再试一次,除非我们已经用尽了所有的重试尝试。
对应的 Spring 配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了细化这个切面,使它只重试幂等操作,我们可以定义下面的冥等注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然后我们可以使用注解来注解服务操作的实现。对只重试冥等操作的切面的修改涉及到细化 pointcut 表达式,以便只有 @Idempotent
可以匹配,如下所示:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
下面是这个例子
切面编写
package cn.mrcode.study.springdocsread.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
* @author mrcode
*/
@Aspect
@Component
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
/**
* 最大重试次数
*/
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("execution(* cn.mrcode.study.springdocsread.web.DemoService.*(..))" +
"&& @annotation(cn.mrcode.study.springdocsread.aspect.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// 当前执行次数
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
// 如果执行成功,则会直接返回执行的结果
// 如果执行失败,则会继续走这个循环,直到达到最大重试次数
return pjp.proceed();
} catch (PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
System.out.println("重试次数:" + numAttempts);
} while (numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
异常类
package cn.mrcode.study.springdocsread.aspect;
/**
* @author mrcode
*/
public class PessimisticLockingFailureException extends RuntimeException{
}
冥等注解
package cn.mrcode.study.springdocsread.aspect;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 冥等注解标识
* @author mrcode
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
要切入点的服务类
package cn.mrcode.study.springdocsread.web;
import org.springframework.stereotype.Component;
import cn.mrcode.study.springdocsread.aspect.Idempotent;
import cn.mrcode.study.springdocsread.aspect.PessimisticLockingFailureException;
/**
* @author mrcode
*/
@Component
public class DemoService {
@Idempotent
public void test() {
// 这里对当前时间进行取模,也就是说会概率性的抛出异常
if (System.currentTimeMillis() % 2 == 0) {
throw new PessimisticLockingFailureException();
}
}
public void test(String name) {
}
}
开启 aspect 支持
package cn.mrcode.study.springdocsread.web;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author mrcode
*/
@Configuration
@ComponentScan("cn.mrcode.study.springdocsread")
@EnableAspectJAutoProxy
public class AppConfig {
}
启动类
package cn.mrcode.study.springdocsread;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.mrcode.study.springdocsread.web.AppConfig;
import cn.mrcode.study.springdocsread.web.DemoService;
public class TestDemo {
public static void main(String[] args) {
final AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
final DemoService bean = ctx.getBean(DemoService.class);
bean.test();
}
}
对于那个切入点,在 idea 中有图形化的标志,有下面这个图标的方法,表示被增强了,可以点击它跳转到增强的切面中
运行这个测试方式会看到以下的情况,全部失败,最终抛出异常:
重试次数:1
重试次数:2
Exception in thread "main" 重试次数:3
cn.mrcode.study.springdocsread.aspect.PessimisticLockingFailureException
at cn.mrcode.study.springdocsread.web.DemoService.test(DemoService.java:15)
at cn.mrcode.study.springdocsread.web.DemoService$$FastClassBySpringCGLIB$$77b34426.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
at cn.mrcode.study.springdocsread.aspect.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:45)
...
at cn.mrcode.study.springdocsread.web.DemoService$$EnhancerBySpringCGLIB$$a20b230b.test(<generated>)
at cn.mrcode.study.springdocsread.TestDemo.main(TestDemo.java:17)
想要看到被重试后,能成功的情况,就需要稍微修改下测试逻辑了
@Idempotent
public void test() throws InterruptedException {
// 随机休眠几毫秒
TimeUnit.MICROSECONDS.sleep(new Random().nextInt(10));
if (System.currentTimeMillis() % 2 == 0) {
throw new PessimisticLockingFailureException();
}
}
经过几次尝试运行后,你会看到有一次重试,这次重试后,方法就执行成功了
重试次数:1