之前写了一些辅助工作相关的Spring Boot怎么使用AOP。这里继续正题,怎么减少Spring Boot 乐观锁加锁报错的情况(基本可以解决)。
1. 包依赖
-
spring-boot-starter-data-jpa, Spring Boot的JPA starter
-
h2, H2内存数据库
-
spring-boot-starter-test,Spring Boot的Junit测试starter
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-data-jpa</artifactId> 4 <version>1.2.6.RELEASE</version> 5 </dependency> 6 7 <dependency> 8 <groupId>com.h2database</groupId> 9 <artifactId>h2</artifactId> 10 <version>1.4.188</version> 11 <scope>runtime</scope> 12 </dependency> 13 14 <dependency> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-test</artifactId> 17 <version>1.2.6.RELEASE</version> 18 <scope>test</scope> 19 </dependency>
2. 如何在启用乐观锁?
我用的是JPA, 所以很简单,在实体类加一个字段,并注解@Version。
1 @Entity 2 public class Account { 3 4 //primary key, auto generated 5 @Id 6 @GeneratedValue(strategy = GenerationType.AUTO) 7 private int id; 8 9 private String name; 10 11 // enable optimistic locking version control 12 @Version 13 private int version; 14 15 /*omitted getter/setter, but required*/ 16 }
3. 通过AOP实现对RetryOnOptimisticLockingFailureException的恢复
为了减少对代码的侵入,对之前的AOP例子进行少许修改:
- 自定义一个注解,用来标注需要恢复这个错误的接口
1 @Retention(RetentionPolicy.RUNTIME) 2 public @interface RetryOnOptimisticLockingFailure { 3 4 }
- 切入点表达式使用注解,不再使用execution
1 @Pointcut("@annotation(RetryOnOptimisticLockingFailure)") 2 public void retryOnOptFailure() { 3 // pointcut mark 4 } 5 6 @Around("retryOnOptFailure()") 7 public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 8 int numAttempts = 0; 9 do { 10 numAttempts++; 11 try { 12 return pjp.proceed(); 13 } catch (OptimisticLockingFailureException ex) { 14 if (numAttempts > maxRetries) { 15 //log failure information, and throw exception 16 throw ex; 17 }else{ 18 //log failure information for audit/reference 19 //will try recovery 20 } 21 } 22 } while (numAttempts <= this.maxRetries); 23 24 return null; 25 }
- 在需要对错误进行恢复的RESTFul接口加上恢复标签
至于为什么一定是要在RESTFul接口上加,而不是其他地方(例如service层),是因为Spring Boot的事务管理的上下文是从resource层开始建立的,在service层恢复是无效的,因为数据库的操作依然是在之前失败的事务里,之后再细说吧。
1 @RestController 2 @RequestMapping("/account") 3 public class AccountResource { 4 5 @Autowired 6 private AccountService accountService; 7 8 @RequestMapping(value = "/{id}/{name}", method = RequestMethod.PUT) 9 @ResponseBody 10 @RetryOnOptimisticLockingFailure 11 public void updateName(@PathVariable Integer id, @PathVariable String name) { 12 accountService.updateName(id, name); 13 } 14 }
4. 测试用例
@Test public void testUpdate() { new Thread(() -> this.client.put(base + "/1/llt-2", null)).start(); new Thread(() -> this.client.put(base + "/1/llt-3", null)).start(); try { //waiting for execution result of service Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
5. 测试一下效果如何
- 没有在AccountResource的updateName方法加@RetryOnOptimisticLockingFailure:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.leolztang.sb.aop.model.Account] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.leolztang.sb.aop.model.Account#1]] with root cause org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.leolztang.sb.aop.model.Account#1] at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2541) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3285)
- 在AccountResource的updateName方法加@RetryOnOptimisticLockingFailure:
Original:name=[llz-1],version=[0],New:name=[llt-2],version=[1] Original:name=[llt-2],version=[1],New:name=[llt-3],version=[2]