Backend

[SpringBoot] UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only 해결후기

연_우리 2022. 12. 6. 00:17
반응형

목차

     

     

     

    상황

    1. 이벤트에 당첨된 경우 휴대폰번호를 입력하고 [쿠폰받기] 버튼을 누르면 쿠폰이 즉시 발송된다.

     

    2. 근데 가끔씩 [쿠폰받기] 버튼이 눌린채로 아무런 반응이 없는 경우가 있다.

        (브라우저 자체가 멈춘다던가... 아무런 액션 없이 그냥 대기만 한다던가...)

     

    3. 기다려도 응답이 없으니 사람들은 [쿠폰 안받기] 버튼을 누른다.

     

    4. [쿠폰 안받기] 버튼을 누르면 당첨내역을 초기화시킨다. (다른사람에게 할당되어야하므로)

     

     

    서버에 찍힌 로그는 아래와 같다.

    2022-12-05 10:45:24 쿠폰받기          시작
    
    2022-12-05 10:45:25 쿠폰안받기        시작
    
    2022-12-05 10:45:25 쿠폰받기-쿠폰발송 성공
    
    2022-12-05 10:45:26 쿠폰안받기        종료 
    
    2022-12-05 10:45:27 쿠폰받기          종료 << Error!!!
    
    UnexpectedRollbackException: Transaction silently rolled back 
    because it has been marked as rollback-only....

     

     

    일단 쿠폰이 발송되었고

    DB에 휴대폰번호와 당첨내역을 보니 롤백되어 데이터가 날아간 상황이였다

    ㅎㄷㄷ...... 아예 이벤트 미참여한 고객으로 나옴....

     

     

    더 고려해야할 점은 위에처럼 [쿠폰안받기] 종료 이후 [쿠폰받기] 종료로 순서가 고정되어있지 않고

    어떤게 먼저 수행될지 모른다는 점...

     

     

     

     

    시도한 것 

    Jpa & Querydsl Lock 설정 

    //jpa
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    
    //querydsl
    query.setLockMode(LockModeType.PESSIMISTIC_WRITE);

    [쿠폰받기]와 [쿠폰안받기] 에서 id(15번)로 조회할 때

    비관적 락을 적용하여 다른 트랜잭션에서 읽기도 쓰지도 못하게 하였다.

     

    select문 뒤에 for update가 붙는 것을 확인하였는데... 조회가 된다..!

    PESSIMISTIC_WRITE는 분명 읽기도 쓰지도 못한다그랬는데... 조회가된다......

     

    내가 테스트를 잘못한것일수도 있지만.. 다른분들도 몇번 테스트 해봤지만 조회가 되었고,

    update문이 나갈때 똑같이 에러가 발생하며 롤백되어서 이 방법은 실패

     

     

     

     

     

    나의 해결방법

    update ~~ where ~~ 조건 설정

    서버에서 데이터를 체크해서 제어하기엔 좀 어렵다고 생각되어서 Query로 해결하기로 했다!

    어차피 DB에서 update문은 1번에 1개만 수행될 것이다.

     

     

    [쿠폰 받기], [쿠폰 안받기] 에서 조회할 땐 아래 객체를 똑같이 조회하게된다

    당첨까지 됨. 휴대폰번호 입력하면 됨.

     

     

    CASE 1 ) 

    [쿠폰 안받기] 먼저 수행

    update ~ 
    set participate = 'N', win = 'N' 
    where phone is null

    참여, 당첨내역 초기화 후

     

    [쿠폰 받기] 먼저 수행

    update ~ 
    set participate = 'Y', win = 'Y', phone = '010-0000-0000' 
    where phone is null

    participate와 win은 select 시 가져왔던 값으로 똑같이 덮어주어서 당첨 이력이 남도록 한다.

    쿠폰받기 적용 후

     


     

     

    CASE 2 )

    [쿠폰 받기] 먼저 수행

    update ~ 
    set participate = 'Y', win = 'Y', phone = '010-0000-0000' 
    where phone is null

    쿠폰받기 적용 후

     

    [쿠폰 안받기] 나중 수행

    update ~ 
    set participate = 'N', win = 'N' 
    where phone is null

    phone is null 이 아니니 해당 update문은 아예 무시됨!! 

     

    쿠폰안받기는 무시됨

     


     

     

     

     

     

    Transactional Propagation 셋팅

    update문이 나가도 다른로직에서 에러가 발생해서 롤백되면 안되니까..

    rollbackFor을 쓰기엔 좀.. 부담되어서 propgation을 설정해주었다

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Response SendCoupone(~~){
        
        //참여정보 저장
        saveParticipate();
    
        //당첨정보 저장
        saveWinner();
        
        //쿠폰 발송
        sendCoupon();
    
        return response;
    }

    해당 설정을 해주면 매번 새로운 트랜잭션을 시작한다. 

    참여정보 저장 따로, 당첨정보 저장 따로, 쿠폰발송 따로따로 각개전투하며 커밋된다

     

     

     

     


     

    앞서 말한 [쿠폰받기], [쿠폰안받기]가 동시에 들어오는 빈도수가 생각보다 좀 있어서 

    계속 저 에러가 발생되는 상황이였다 (차라리 죽여...)

     

     

    일단은 update문과 propagation 2가지 설정하는 것으로

    롤백되어서 날아가는 데이터도 잡아냈고, 에러 발생하는 빈도수도 엄청 낮추었다!

     

    내 방법이 정답은 아니지만 이렇게 해결했다는 기록용 후기~~~ 끝!

     

     

     

     

     

     

     

     

     

    ++ 댓글 추가 

    더보기

    좋은 의견을 남겨주셔서 나중에 다시 살펴보려한다!

     

     

     

     

     

     

     

     

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기