woowacourse-teams / 2022-mo-rak

🥳 모락: 모임을 즐겁게, 편하게!
https://mo-rak.com
51 stars 6 forks source link

fix: 동시에 요청할 경우 발생할 수 있는 데드락 문제를 방지한다. #611

Closed leo0842 closed 1 year ago

leo0842 commented 1 year ago

Close #610 #553

배경 지식

공유락, 배타락

※ Inno DB 스토리지 엔진이라면 배타락인 경우에도 락을 획득하지 않는 단순 읽기는 가능하다.

비관락

약속잡기 조회 시 비관적 락을 걸어주게 되면 해당 레코드에 대해 배타락을 획득한다. 이로 인해 트랜잭션이 끝날 때 까지 다른 트랜잭션들은 배타락 획득도, 공유락 획득도 불가능하다. 한번에 한 트랜잭션만 락을 얻을 수 있으므로 정합성 문제도, 데드락 문제도 해결이 가능하다.

낙관락

트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법이다. 실제 DB 단에서 락을 거는 것이 아니고, 버전 관리 기능을 통해 동시성 문제를 해결한다. 엔티티에 버전 관리용 필드를 추가하여 낙관적락을 구현할 수 있다.

낙관적 락은 version 컬럼이 존재하고, update 시 기존에 조회했던 version 과 일치하는지 확인 후 update 한다. 레코드에 배타락을 걸지 않기에 성능상 크게 저하되지 않고 동시성 문제를 해결할 수 있다.

상세 내용

  1. 비관락
@Query("select a from Appointment a where a.menu.code.code = :appointmentCode")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Appointment> findByCodeForUpdate(@Param("appointmentCode") String appointmentCode);

조회 시 for update 가 추가되어 배타락을 획득한다.

image

비관락은 동시성 문제를 해결할 수 있지만 로직을 진행하는 동안 다른 쓰레드에서는 로직을 실행하지 못하고 대기 상태에 빠진다. 이로 인해 동시에 접근하는 트랜잭션이 많아지면 많아질수록 API 콜의 대기 시간은 늘어나게 되어 비효율적이기 때문에 보류하였다.

foreign key 제거

데드락을 유발시키는 공유락을 획득하지 못하도록 외래키 제약조건 제거하였다. 이와 함께 기존에는 cascade 로 삭제했기때문에 약속잡기 삭제시 선택 가능 시간도 제거하는 로직을 추가하였다.

  1. 낙관락
@Getter
@Entity
@NoArgsConstructor
public class Appointment extends BaseRootEntity<Appointment> {

    @Version
    private Integer version;
}
@Query("select a from Appointment a where a.menu.code.code = :code")
@Lock(LockModeType.OPTIMISTIC)
Optional<Appointment> findByCode(@Param("code") String code);

버전 필드를 추가하고 Lock Mode 로 OPTIMISTIC 을 추가하였다.

image

낙관적 락은 정합성 문제를 해결할 수 있지만, 데이터 유실이 발생한다.

image

데이터 유실을 해결하기 위해서는 비즈니스 로직에서 rollback 이 발생했는지 확인하고 다시 동일한 과정을 수행하는 while 반복문이 필요하다. 그래서 비즈니스 로직에 데이터 유실 방지 로직이 필요해서 비즈니스 로직을 희생해야 하기때문에 낙관적 락도 보류하였다.

  1. 비즈니스 로직
@Modifying(clearAutomatically =true, flushAutomatically =true)
@Query("update Appointment a "
        + "set a.availableTimes.selectedCount = a.availableTimes.selectedCount + 1 "
        + "where a.menu.code.code = :appointmentCode")
void updateSelectedCount(@Param("appointmentCode") String appointmentCode);

image

수정하는 쿼리는 수정하는 시점의 값에서 +1 하는 로직이기때문에 정합성을 맞출 수 있다. 그리고 이 경우 다른 로직으로 인한 공유락을 획득하지 않기에 데드락 문제도 피할 수 있고, 다른 예외 이외에는 롤백이 되지 않기에 데이터 유실 문제도 해결할 수 있다.

leo0842 commented 1 year ago

고생하셨습니다~ 그런데 제가 로직을 잘 이해하지 못한건가 싶긴한데, 어떤 사람이 처음으로 투표를 진행하려는데, 동시에 두 개의 요청을 보내면 둘 다 Transient로 설정된 first값을 읽은 결과가 true여서 selected count가 0에서 2로 증가할 것 같은데요. 요 부분 다시 한번 확인해주시면 좋을 것 같습니다~

동시에 두 개의 요청 에 대해서는 두 가지로 생각할 수 있습니다.

  1. 한 사람이 동시에 두 개의 요청을 보낼 때
  2. 두 사람이 동시에 각각 요청을 보낼 때

현재 로직은 2번을 고려하여 해결한 로직입니다. 이 때 1번으로 고려한다면 비관락 또는 낙관락 또는 synchronized 로 풀어야 하는데, 비관락과 낙관락은 앞서 언급드린 문제가 그대로 발생하고 synchronized 는 분산 환경에서 문제가 발생합니다.

결론적으로 1번, 2번 모두 해결하기 위해서는 현재의 자원보다는 네임드락이나 redis 를 활용한 분산락으로 해결할 수 있을 것 같습니다. 이에 대해서는 다음 이슈로 남기겠습니다!

cjlee38 commented 1 year ago

고생하셨습니다~ 그런데 제가 로직을 잘 이해하지 못한건가 싶긴한데, 어떤 사람이 처음으로 투표를 진행하려는데, 동시에 두 개의 요청을 보내면 둘 다 Transient로 설정된 first값을 읽은 결과가 true여서 selected count가 0에서 2로 증가할 것 같은데요. 요 부분 다시 한번 확인해주시면 좋을 것 같습니다~

동시에 두 개의 요청 에 대해서는 두 가지로 생각할 수 있습니다.

  1. 한 사람이 동시에 두 개의 요청을 보낼 때
  2. 두 사람이 동시에 각각 요청을 보낼 때

현재 로직은 2번을 고려하여 해결한 로직입니다. 이 때 1번으로 고려한다면 비관락 또는 낙관락 또는 synchronized 로 풀어야 하는데, 비관락과 낙관락은 앞서 언급드린 문제가 그대로 발생하고 synchronized 는 분산 환경에서 문제가 발생합니다.

결론적으로 1번, 2번 모두 해결하기 위해서는 현재의 자원보다는 네임드락이나 redis 를 활용한 분산락으로 해결할 수 있을 것 같습니다. 이에 대해서는 다음 이슈로 남기겠습니다!

2번 케이스만 고려하면 해당 로직으로 방어가 가능하겠네요. 상세한 설명 감사합니다~~