私は会議をスケジュールして確認メールを送信するサービスを持っていますが、送信ボタンを何度もクリックすると複数のメールが送信されます。春のロールバック時に例外がスローされた後にコードを実行し続ける
サービスはこれです:(ブラウザで)提出の上に複数回クリックすると
@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.
}
}
私はこの会議がすでにスケジュールされていることを評価するのに十分であるように最初のテスト(!ステータス=案を)期待していました。その場合、例外がスローされ、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()ステートメントを越えて実行を続行する必要がありますか?
私はこの問題は中のsendEmail呼び出しをラップすることによって解決することができることを知っている:
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
sendInvitations(meeting);
}
});
しかし、その後、再び。どうして ?
多くの感謝!
例外の後、春は間違いなく実行されません。複数の電子メールが送信される理由は、競合状態が存在するためです。 submitを複数回押すと、同じリクエストが並行して実行されるため、更新が行われるコードのポイントに達する前に、 'meetingDao.loadById'を何度も実行することができます。 – Leon
私は参照してください。コミットはトランザクションの終了時にのみ実行されるためですか?もしそうなら、更新後すぐに強制的にコミットする方法がありますか?それとも、TransactionSynchronizationManagerを使用する方が良いでしょうか? – tggm
コミットを強制することはできますが、複数の参加者がトランザクションがコミットされる前に読み取りセクションに到達できるため、引き続き競合状態になります。 「シンプルな」修正は、要求が提出された時点で送信ボタンを無効にすることです。これで誰もエンドポイントを直接呼び出すことはできません。このようなシステム上の問題があれば、私は通常、あなたの要求のidem-potencyを提案します。しかし、それはあなたがやっていることに対する完全な過剰なものかもしれません。 – Leon