여러 사람이 동시에 한 게시글의 좋아요를 누른다면 어떻게 되는가?
만약 한 사람이 같은 아이디로 동시접속 창을 두개 띄워놓고 동시에 좋아요를 눌렀을 때 어떻게 되는가?
동시성을 해결하기 위해서는 크게 2가지 방법이 있을 것이다.
curl <내용> & curl <내용>
같은 JWT 키(같은 아이디)로, 같은 게시글에 좋아요를 요청하면 하나의 요청은 성공하고 다른 하나의 요청은 실패하면서 데드락이 발생한다.
Hibernate: select prfpost0_.id as id1_3_0_, prfpost0_.created_at as created_2_3_0_, prfpost0_.modified_at as modified3_3_0_, prfpost0_.category as category4_3_0_, prfpost0_.content as content5_3_0_, prfpost0_.image_key as image_ke6_3_0_, prfpost0_.like_count as like_cou7_3_0_, prfpost0_.member_id as member_10_3_0_, prfpost0_.tags as tags8_3_0_, prfpost0_.title as title9_3_0_ from prf_post prfpost0_ where prfpost0_.id=?
Hibernate: select prfpost0_.id as id1_3_0_, prfpost0_.created_at as created_2_3_0_, prfpost0_.modified_at as modified3_3_0_, prfpost0_.category as category4_3_0_, prfpost0_.content as content5_3_0_, prfpost0_.image_key as image_ke6_3_0_, prfpost0_.like_count as like_cou7_3_0_, prfpost0_.member_id as member_10_3_0_, prfpost0_.tags as tags8_3_0_, prfpost0_.title as title9_3_0_ from prf_post prfpost0_ where prfpost0_.id=?
Hibernate: select prfpostlik0_.id as id1_5_, prfpostlik0_.created_at as created_2_5_, prfpostlik0_.modified_at as modified3_5_, prfpostlik0_.member_id as member_i4_5_, prfpostlik0_.prf_post_id as prf_post5_5_ from prf_post_like prfpostlik0_ where prfpostlik0_.member_id=? and prfpostlik0_.prf_post_id=?
Hibernate: select prfpostlik0_.id as id1_5_, prfpostlik0_.created_at as created_2_5_, prfpostlik0_.modified_at as modified3_5_, prfpostlik0_.member_id as member_i4_5_, prfpostlik0_.prf_post_id as prf_post5_5_ from prf_post_like prfpostlik0_ where prfpostlik0_.member_id=? and prfpostlik0_.prf_post_id=?
Hibernate: insert into prf_post_like (created_at, modified_at, member_id, prf_post_id) values (?, ?, ?, ?)
Hibernate: insert into prf_post_like (created_at, modified_at, member_id, prf_post_id) values (?, ?, ?, ?)
Hibernate: update prf_post set modified_at=?, like_count=? where id=?
Hibernate: update prf_post set modified_at=?, like_count=? where id=?
<WARN & ERROR>
2023-03-24 16:39:48.913 WARN 1780 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1213, SQLState: 40001
2023-03-24 16:39:48.913 ERROR 1780 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : Deadlock found when trying to get lock; try restarting transaction
Deadlock found when trying to get lock; try restarting transaction
org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
member_id가 7, 8인 사용자가 동일한 게시글에 좋아요를 누르면 id가 8인 사용자만 처리가 되고 7인 사용자는 처리되지 않으며 DeadLock이 발생한다.
❓ 원래는 Deadlock이 발생하면 안 되고, 데이터의 정합성이 깨질지언정 둘 다 처리가 되어야한다.
하지만 왜 Deadlock이 발생하는가?!
우선 왜 DeadLock이 발생하는가?에 대한 이해를 하려면 Lock에 대한 이해가 필요할 것 같다.
SELECT ~ LOCK IN SHARE MODE 를 사용하면 된다.[참고]
Exclusive lock과 Shared lock
Lock의 종류
Select 쿼리는 S락이 아니다.
두 개 이상의 트랜잭션들이 동시에 진행될때 각각 자신의 데이터에 대하여 락을 획득하고 상대방 데이터에 대하여 락을 요청하면 무한 대기 상태에 빠질 수 있는 현상이다.
예를 들어 T1에서 A에 대해 락을 걸고 T2에서 B에 대해 락을 걸었다고 하자. 그리고 나서 T1에서 B에 대해 락을 걸고 T2가 A에 대해 락을 건다면 T1과 T2는 서로 A, B에 대한 락을 유지하며 무한루프에 빠지게 됩니다.
일반적으로 DBMS에서는 데드락 탐지(Deadlock detection) 기능을 제공하기 때문에 데드락이 발생하면 DBMS가 T1 혹은 T2 중 하나를 강제로 중지시켜, 한 트랜잭션은 정상적으로 실행되며 중지된 트랜잭션에서 변경한 데이터는 원래 상태로 되돌려 놓는다. (실제 데드락 상황이 아닐지라도 락에 대한 대기시간이 설정된 시간을 초과하면 이것도 데드락으로 처리된다)
또한, 동시에 같은 Row를 update 하려 하면 Deadlock이 발생한다. 둘 이상의 쓰레드가 서로의 작업이 끝나기를 계속 기다리는 것이다.
<Query A>
INSERT INTO my_table (pk, name) VALUES(1, 'a'), (2,'b') ON DUPLICATE KEY UPDATE name=VALUES(name);
<Query B>
INSERT INTO my_table (pk, name) VALUES(2, 'a'), (1,'b') ON DUPLICATE KEY UPDATE name=VALUES(name);
@Transactional은 데이터베이스의 동일한 행을 동시에 수정하지 못하게 Lock을 걸어준다.@Transactional만 설정해두면, 두 개의 쓰레드가 있을 때 각각의 쓰레드는 2개의 트랜잭션을 만들고 두 트랜잭션은 독립적으로 실행된다. 이때 2개의 트랜잭션이 동일한 DB의 동일한 행에 접근하게 된다면 하나의 트랜잭션은 성공하고 다른 하나의 트랜잭션은 실패하게 될 것이다.위에서 좋아요 동시 요청을 보내면, 데드락이 발생하는데 그 이유는 동시에 같은 Row를 update 하기 때문이다.
교착상태(Deadlock)가 발생한다. 즉 둘 이상의 프로세스가 서로 작업이 끝나길 기다린다는 것이다. 멀티프로그래밍이 가능한 시스템에서 일어날 수 있는 현상이다. 뫼부우스의 띠 처럼 계속 한다는 것.
✔️ synchronized와 @Transactional가 모두 없는 상태
T1: |---add 좋아요--->
T2: |---add 좋아요--->
=> Deadlock 발생
✔️ synchronized만 있는 경우
T1: |---add 좋아요---> T2: |---add 좋아요--->
✔️ @Transactional만 있는 경우
원래는 다음과 같이, 여러 트랜잭션이 독립적으로 실행된다.
T1: begin Transaction ---> method ---> commit Transaction
T2: begin Transaction ---> method ---> commit Transaction
하지만 동시에 두 사용자(아이디가 다름)가 같은 게시글에 좋아요 요청을 하는 경우에는 문제가 발생한다.
// LikeController
@PostMapping("/prf-posts/{prf-post-id}")
public ResponseEntity postPrfPostLike(@AuthenticationPrincipal Member member, @PathVariable("prf-post-id") long prfPostId) {
PrfPost prfPost = prfPostService.findverifiedPrfPost(prfPostId);
PrfPostLike prfPostLike = likeService.addPrfPostLike(member, prfPost);
return new ResponseEntity(new PrfPostLikeDto.Response(prfPost.getLikeCount()), HttpStatus.CREATED);
}
// LikeService
@Transactional
public PrfPostLike addPrfPostLike(Member member, PrfPost prfPost){
checkExistPrfPostLike(member, prfPost); // 이미 좋아요한 경우
PrfPostLike prfPostLike = new PrfPostLike(member, prfPost);
prfPost.likeCountUp();
return prfPostLikeRepository.save(prfPostLike);
}
Hibernate: select prfpost0_.id as id1_3_0_, prfpost0_.created_at as created_2_3_0_, prfpost0_.modified_at as modified3_3_0_, prfpost0_.category as category4_3_0_, prfpost0_.content as content5_3_0_, prfpost0_.image_key as image_ke6_3_0_, prfpost0_.like_count as like_cou7_3_0_, prfpost0_.member_id as member_10_3_0_, prfpost0_.tags as tags8_3_0_, prfpost0_.title as title9_3_0_ from prf_post prfpost0_ where prfpost0_.id=?
Hibernate: select prfpost0_.id as id1_3_0_, prfpost0_.created_at as created_2_3_0_, prfpost0_.modified_at as modified3_3_0_, prfpost0_.category as category4_3_0_, prfpost0_.content as content5_3_0_, prfpost0_.image_key as image_ke6_3_0_, prfpost0_.like_count as like_cou7_3_0_, prfpost0_.member_id as member_10_3_0_, prfpost0_.tags as tags8_3_0_, prfpost0_.title as title9_3_0_ from prf_post prfpost0_ where prfpost0_.id=?
Hibernate: select prfpostlik0_.id as id1_5_, prfpostlik0_.created_at as created_2_5_, prfpostlik0_.modified_at as modified3_5_, prfpostlik0_.member_id as member_i4_5_, prfpostlik0_.prf_post_id as prf_post5_5_ from prf_post_like prfpostlik0_ where prfpostlik0_.member_id=? and prfpostlik0_.prf_post_id=?
Hibernate: select prfpostlik0_.id as id1_5_, prfpostlik0_.created_at as created_2_5_, prfpostlik0_.modified_at as modified3_5_, prfpostlik0_.member_id as member_i4_5_, prfpostlik0_.prf_post_id as prf_post5_5_ from prf_post_like prfpostlik0_ where prfpostlik0_.member_id=? and prfpostlik0_.prf_post_id=?
Hibernate: insert into prf_post_like (created_at, modified_at, member_id, prf_post_id) values (?, ?, ?, ?)
Hibernate: insert into prf_post_like (created_at, modified_at, member_id, prf_post_id) values (?, ?, ?, ?)
Hibernate: update prf_post set modified_at=?, like_count=? where id=?
Hibernate: update prf_post set modified_at=?, like_count=? where id=?
<WARN & ERROR>
2023-03-24 16:39:48.913 WARN 1780 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1213, SQLState: 40001
2023-03-24 16:39:48.913 ERROR 1780 --- [nio-8080-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : Deadlock found when trying to get lock; try restarting transaction
=> Deadlock 발생
=> T1은 성공, T2는 실패
이때 두 트랜잭션이 레코드를 수정하려고 하면, 서로의 락에 의해 데드락이 발생한다. 먼저 UPDATE를 시도한 트랜잭션이 타임아웃되어서 실패하고, 다른 트랜잭션이 성공한다. 결과적으로 하나의 수정만 성공한다.
https://karbachinsky.medium.com/deadlock-found-when-trying-to-get-lock-try-restarting-transaction-54a3ed118068
=> 참고해서 다시 정리하기
1-1. synchronized 추가


1-2. @Transactional 추가

🚨 주의) synchronized와 @Transactional를 같이 사용하면, 동시성 이슈는 해결되지 않는다.
synchronized이 @Transactional보다 먼저 선행되어야 한다.
stackoverflow
stackoverflow를 풀어서 잘 설명해주신 블로그
문제는 해결됐지만 애플리케이션 단에 Lock을 건 것이 좋은 방법은 아닌 것 같다.
지금처럼 요청이 동시에 2 개가 들어온 것이 아니라 동시에 요청이 100만건이 들어오게 된다면 어떻게 될까?
애플리케이션 단에 말고, 데이터베이스 단에 Lock을 걸어보고 성능 차이를 확인해보자.
[읽어볼 자료들]
Transaction Dead Lock
@Transactional은 만능이 아닙니다.
https://tourspace.tistory.com/54
https://backtony.github.io/java/2021-12-24-java-41/
https://zzang9ha.tistory.com/443
https://idea-sketch.tistory.com/45
https://velog.io/@jkijki12/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%B4%EB%B3%B4%EA%B8%B0
https://mangkyu.tistory.com/30
https://sigridjin.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66
https://sabarada.tistory.com/187
https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html
https://velog.io/@rnjsrntkd95/Mysql%EC%9D%98-%EC%9E%A0%EA%B8%88Lock%EA%B3%BC-%EB%8D%B0%EB%93%9C%EB%9D%BDDeadLock-%EB%B0%9C%EC%83%9D