문제 발견
현재 ‘당장’ 서비스를 운영하고 있는 중인데, 가끔 point_history 테이블에 대해 데드락 문제나 중복키 문제가 발생했다.
데드락 발생 로그
ERROR - Deadlock found when trying to get lock; try restarting transaction ... (생략)
중복키 발생 로그
ERROR - Duplicate entry '더블PKID' for key 'point_history.PRIMARY' ... (생략)
문제 원인
로그를 통해 point_history 테이블에 insert할 때 위의 두 문제가 발생함을 확인할 수 있었다.
발생원인
로그를 통해서 동시에 동일한 접속 요청이(같은 유저, 시간,분,초까지 동일한) 2번 있는 것을 확인했다. 말로만 듣던 따닥 (연속) 요청 문제였다.
우리 서비스는 하루에 1번이상 접속하면, 1일 1회 500 포인트를 적립할 수 있다.
그렇기에 접속시 유저 여부를 확인하고, 500 포인트를 적립한 후, 유저의 최근 로그인 일자를 업데이트하는 과정을 거친다.
접속 요청으로 처리해야하는 과정을 아래에 간단히 정리해봤다.
1. User 테이블에서 사용자 여부 확인 및 최근 접속일(updated_at) 날짜 조회
2. 만약 오늘 접속하지 않은 유저라면 500 포인트 적립 (Point_history 테이블 insert)
3. 유저의 최근 접속일(updated_at)을 업데이트한다. (User 테이블 update)
코드
@Transactional
public void addAccessPoint(String oauthId) {
User user = userSearchService.findUserByOauthId(oauthId);
if (!user.getAccessedAt().equals(LocalDate.now())) {
addPointEvent(EarnPoint.ACCESS.getTitle(), user); // point_history 테이블 insert
defaultOauthLoginService.updateUserAccessedAt(user); // user 테이블 update
}
}
테이블 ERD
여기서는 User 테이블과 Point_history 테이블만 참고하면 된다 !
- User 테이블 : 사용자 정보를 저장
- oauth_id : 사용자 ID - pk
- created_at : 사용자 가입 날짜
- updated_at 컬럼 - 사용자가 최근 접속한 날짜
- ( 글에서는 updated_at == accessed_at 동일한 컬럼이다. ERD 구성에는 updated_at이라고 적어두고,, 실제로 테이블 생성할 때는 accessed_at이라고 만들어버림.. 바보몽총... 참고 부탁드립니다..)
- Point_history 테이블 : 사용자의 포인트 사용, 적립 내역을 저장
- oauth_id : User 테이블의 FK 값으로, 사용자의 ID - pk (복합키)
- created_at : 포인트 사용 및 적립 시간 - pk (복합키)
- product_name : 포인트 사용 및 적립 내용 - pk (복합키)
- chage_point : 사용 및 적립된 포인트
- balance_point : 사용자의 총 포인트
(필요한 부분만 확인할 수 있도록 간략히 표현했다)
연속 요청 문제로 중복키 문제는 이해할 수 있지만, 데드락이 발생되는 것은 이해할 수 없어서 꽤나 고민하고 다른 경우의 문제도 찾아봤다 ㅜ
중복키 발생 원인
연속 요청 문제로 , 동일한 사용자가 똑같은 시간에 접속 요청을 보냈기 때문에, Point_history 테이블의 PK가 중복되어 중복키 문제가 발생
어떻게 데드락이 발생되는 걸까 ? 아래의 과정을 살펴보자 !!
접속 요청 처리 과정 중, point_history 테이블에 insert하고 User 테이블에 update하는 과정 중에 데드락이 발생하는 것이었다.
결론
트랜잭션 A의 Update 쿼리로 인해 User 테이블의 row에 대한 X 락 요청
→ 트랜잭션 B가 User 테이블(row)에 대한 S락을 갖고 있어서 거부 되었음
-> 트랜잭션 B의 S락 해제를 위해선 트랜잭션 A의 Point_history 테이블(row)에 대한 X락 회수 필요
→ 트랜잭션 A는 Update 쿼리가 실행되지 않았음으로 커밋 불가 → X락 회수 불가
데드락 발생
즉 트랜잭션 B의 User 테이블(row)의 S락 때문에, 트랜잭션 A가 User 테이블(row)의 X락을 갖지 못해 데드락이 발생한 것.
여기서 포인트! ✨
기존에 S락 또는 X락을 갖는 트랜잭션이 존재할 경우, 다른 트랜잭션은 X락을 갖지 못한다.
락 호환성
락 상태 조회 (데드락 걸리기 전)
SELECT * FROM performance_schema.data_locks;
문제 해결
데드락을 해결하는데는 아래와 같이 3가지의 방법이 있다.
- 데드락 발생 조건을 고려해서 테이블을 구성하여 예방한다.
- 데드락 발생 가능성을 검사해 데드락을 회피한다. (은행원 알고리즘)
- 데드락 발생 시, 해결하여 회복한다. (프로세스 종료 및 선점 )
이중에 나는 예방하기로 했다.
애초에 User 테이블은 여러 테이블에 외래키로 참조되는 경우가 많은데, 불필요한 Update 쿼리가 있다는 것은 좋지 않다. 그래서 이 기회에 User 테이블과 Point 와 관련된 테이블 구성을 수정하기로 했다.
테이블 수정
기존 ERD
개선된 ERD
- UserAccessLog
- 기존 User 테이블에는 최근 접속한 날짜를 저장하는 updated_at 컬럼이 있었다.
- updated_at 컬럼을 없애고 , UserAccess 테이블을 만들어, 유저의 접속 날짜를 따로 저장한다.
- User 테이블의 updated_at 필드를 매번 수정할 필요가 없어지고 , User의 접속 기록을 관리하여 더 다양한 이벤트를 시도할 수 있다.
- PointHistory
- Balance_point 컬럼과 UserPoint 테이블을 제거했다.
- Balance_point 컬럼과 UserPoint 테이블은 유저의 최종 포인트를 저장했었는데 , 불필요하다고 생각되어 제거하였다.
- 제거 이유 1. 최종 포인트는 [Balance_point 컬럼과 UserPoint 테이블] 없이도, pointHistory 테이블의 change_point 컬럼으로 구할 수 있다.
- 제거 이유2. 정합성의 신뢰도가 낮다. UserPoint 테이블의 point 컬럼의 값과 PointHistory 테이블의 Balance_point 컬럼값이 100% 맞다는 보장이 없다. 동시성 이슈로 불일치할 수 있음
테이블 수정 후 데드락 확인하기
아래와 같이 테스트 해봤을 때, 성공적으로 데드락을 예방할 수 있었다. 다만 중복키 문제가 일어나긴 하지만, 연속 요청 문제가 발생되어도 중복으로 저장되지 않는다.
'개발일지 > 문제 해결하기 ~' 카테고리의 다른 글
서브쿼리를 이용한 조인 쿼리 개선하기(3) - JAVA 병목 원인 찾기 (0) | 2024.08.06 |
---|---|
서브쿼리를 이용한 조인 쿼리 개선하기(2) -쿼리 개선기 (0) | 2024.08.01 |
서브쿼리를 이용한 조인 쿼리 개선하기(1) - 현재 상황 분석 (0) | 2024.07.30 |
꾸준히 기록할 수 있는 사람이 되자 !