之前寫了一些輔助工作相關的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]