现在你已经看到了所有构成部分是如何工作的,我们可以把它们放在一起做一些有用的事情。
业务服务的执行有时会因为并发问题而失败(例如,死锁失败者)。如果操作被重试,那么它很可能在下一次尝试中成功。对于在这种情况下适合重试的业务服务(不需要回到用户那里解决冲突的 idempotent 操作),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显跨越服务层中多个服务的需求,因此,非常适合通过一个切面来实现。
因为我们想重试操作,所以我们需要使用 环绕通知,以便我们可以多次调用 Proceed。下面的列表显示了基本切面的实现:
@Aspectpublic 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@Componentpublic 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*/@Componentpublic class DemoService {@Idempotentpublic 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")@EnableAspectJAutoProxypublic 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重试次数:2Exception in thread "main" 重试次数:3cn.mrcode.study.springdocsread.aspect.PessimisticLockingFailureExceptionat 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)
想要看到被重试后,能成功的情况,就需要稍微修改下测试逻辑了
@Idempotentpublic void test() throws InterruptedException {// 随机休眠几毫秒TimeUnit.MICROSECONDS.sleep(new Random().nextInt(10));if (System.currentTimeMillis() % 2 == 0) {throw new PessimisticLockingFailureException();}}
经过几次尝试运行后,你会看到有一次重试,这次重试后,方法就执行成功了
重试次数:1
