2017-02-21 8 views
0

모임 일정을 잡고 확인 전자 메일을 보내는 서비스가 있으며 제출 단추를 여러 번 클릭하면 여러 개의 전자 메일이 전송됩니다.스프링 트랜잭션 롤백 예외가 발생하면 코드를 계속 실행 함

서비스는 이것이다 :

@Service 
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = { Exception.class }) 
public class MeetingService { 
    public void scheduleAndInvite(int meetingId) { 
     try { 
      Meeting meeting = meetingDao.loadById(meetingId); 

      // Validations. 
      if (meeting.getMeetingStatus() != MeetingStatus.Draft) { 
       throw new FmcUserException("not draft"); 
      } 

      // Persist entity 
      meeting.setMeetingStatus(MeetingStatus.Scheduled); 

      meetingDao.persistMyEntity(meeting); 

      // This eventually calls JavaMailSender. Uses the Meeting hibernate entity 
      sendInvitations(meeting); 
     } catch (Exception ex) { 
      logger.error(ex.getMessage(), ex); 
      throw new FmcSystemException(ex); // This class extends RuntimeException. 
     } 
    } 

내가 첫 번째 테스트를 기다리고 있었다 (브라우저에서) 제출에 여러 번 클릭 할 때 (상태 = 초안!)이 회의가 이미 예약되어 있음을 평가하기에 충분하기 . 이 경우 예외가 throw되고 catch 블록에서 catch되어 sendInvitations() 호출을 건너 뜁니다.

12:45:01,117 ERROR [my.framework.mvc.BaseController] (default task-55) could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement: org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement 
    at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:232) 
    at org.springframework.orm.hibernate5.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:755) 

Caused by: org.hibernate.exception.LockAcquisitionException: could not execute statement 
    at org.hibernate.dialect.MySQLDialect$3.convert(MySQLDialect.java:522) 
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42) 

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction 
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) 
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) 

지구에이 여러 이메일을 보내는 방법 나는 아마도 이해할 수 없다 :

그것은 제대로 로그에서 예외의 톤을 생성합니다. 여기서 스프링은 메소드가 끝날 때까지 실행을 계속하지만 왜 그런지?! 예외가 throw 된 후 왜 throw() 문을 넘어 계속 실행해야합니까?

나는이 문제에 sendEmail 호출을 래핑에 의해 해결 될 수 있다는 사실을 알고 :

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { 
     @Override 
     public void afterCommit() { 
      sendInvitations(meeting); 
     } 
    }); 

하지만 다시. 왜 ?

감사합니다.

+3

예외 이후에 Spring은 확실히 실행을 계속하지 않습니다. 여러 이메일을 보내는 이유는 경쟁 조건이 있기 때문입니다. submit을 여러 번 누르면 같은 요청이 병렬로 실행되므로,'meetingDao.loadById'는 업데이트가 끝난 코드의 한 지점에 도달하기 전에 여러 번 실행될 수 있습니다. – Leon

+0

나는 본다. 그것은 커밋이 트랜잭션의 끝에서만 수행되기 때문입니까? 그렇다면 업데이트 이후에 imeddiately 명령을 내릴 수있는 방법이 있습니까? 또는 TransactionSynchronizationManager를 사용하는 것이 더 나은 방법입니까? – tggm

+1

커밋을 강제 할 수 있지만 트랜잭션이 커밋되기 전에 여러 참가자가 읽기 섹션에 도달 할 수 있기 때문에 여전히 경쟁 상태가됩니다. "간단한"해결책은 요청이 제출되면 제출 단추를 사용 불가능하게하는 것입니다. 이것은 누군가가 종점을 직접 호출하는 것을 막지는 못합니다. 이와 같은 시스템 문제로 인해 일반적으로 요청에 대한 idem-potency를 제안합니다. 그러나, 그것은 당신이하고있는 것에 대한 과잉 공격일지도 모릅니다. – Leon

답변

0

예외가 발생 된 후 스프링이 코드를 실행하지 않습니다. 가능한 방법은 없습니다. 이것은 단순히 Java가 작동하는 방식입니다.

제출을 여러 번 푸시하면 여러 번 서버에 요청이 전송되어 병렬로 실행되기 때문입니다. 어떤 시점에서 서로 다른 스레드가 서로의 방식으로 연결될 것입니다.

이 문제를 올바르게 해결하려면이 코드를 동기화하거나 데이터베이스 잠금을 사용하여 다른 트랜잭션이 통과하지 못하도록해야합니다. (SELECT .... FOR UPDATE 또는 다른 수단을 사용)

관련 문제