틀린 부분이 있다면 언제든 알려주세요
debate Entity
- 토론방
- pk: deb_id
@Entity
@Getter
@Table(name = "debate")
@NoArgsConstructor
public class Debate {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "deb_id")
private Long debateId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "b_id")
private Book book;
@Column(name = "deb_topic")
private String debateTopic;
...
@OneToMany(mappedBy = "debate", fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "debate", fetch = FetchType.LAZY)
private List<DebateMember> debateMembers = new ArrayList<>();
}
debate_member Entity
- 각 토론방에 참가한 유저 목록
- pk: (deb_id, u_id)
- deb_id, u_id 모두 fk(각각 debate, user 참고)이다.
@Entity
@Getter
@Table(name = "debate_member")
@NoArgsConstructor
public class DebateMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "deb_mem_id")
private Long debMemId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deb_id")
private Debate debate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "u_id")
private User user;
public DebateMember(Debate debate, User user) {
this.debate = debate;
this.user = user;
}
}
각 토론방마다 인원제한이 있다.
지금 테스트할 토론방은 10명 제한이 있다.
설계
클라이언트에서 <토론방에 참여하기> 버튼을 누르면
서버로 요청이 와서 10명 미만인 경우에만 접근을 허용해야 한다.
[*] 실제 서비스 코드라면 해당 (deb_id, u_id) 쌍이 이미 있는지부터 검사해야 한다.
Controller
@GetMapping("/debate/test/{u-id}")
public void joinDebate (@PathVariable("u-id") Long userId) {
try{
debateService.joinDebate(userId);
}catch (Exception e){
e.printStackTrace();
}
}
Service
@Transactional
public void testMethod(Long userId){
Long debateId = 104L; // 테스트 용이기 때문에 임의로 fix해놓았다.
Debate debate = debateRepository.findDebatesTest(debateId);
List<DebateMember> members = debate.getDebateMembers();
log.info("현재 인원:{}",members.size());
if(members.size()<10){
User user = userRepository.getReferenceById(userId);
Debate newDebate = debateRepository.getReferenceById(debateId);
DebateMember debateMember = new DebateMember(newDebate, user);
debateMemberRepository.save(debateMember);
}
}
Repository
@Repository
public interface DebateRepository extends JpaRepository<Debate, Long> {
@Query("select deb from Debate deb left join fetch deb.debateMembers where deb.debateId= :DebateId")
Debate findDebatesTest(@Param("DebateId") Long DebateId);
}
Test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BstApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testConcurrent() throws InterruptedException{
int numThreads= 10;
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
Long[] userList = {4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L};
for(int i =0;i<numThreads;i++){
int finalI = i;
String url = "http://localhost:8080/debate/test/"+userList[finalI];
executorService.submit(()->{
ResponseEntity<String> result = restTemplate.getForEntity(url, null);
System.out.println(result.getStatusCode());
});
}
executorService.shutdown();
}
}
1. 락 없이 실행 -> 그냥 실패
락 없이 실행해보면 10명보다 더 많은 인원이 들어가있는 것을 확인할 수 있다.
일반적으로 select(단순 조회)는 락을 걸지 않는다. 따라서 모두 if문을 만족하여 내부 로직을 수행했고, 정해진 인원수보다 많이 입장하게 된 것으로 보인다.
아래부터 s-lock, x-lock이 자주 등장하는데, 다음과 같은 특성을 알아두면 이해하는데 도움이 될 것이다.
- s-lock은 기본적으로 읽기 작업 동안에만 걸리게 된다. 즉, select 문에 의해서 데이터를 반환받으면 락은 바로 해제된다.
- x-lock은 트랜잭션 전체에 대해 락을 갖고 있고, 커밋이나 롤백으로 해당 트랜잭션을 종료하게 되면 락은 해제된다.
2. 비관적 락 - PESSIMISTIC_READ -> 데드락
비관적 락은 기본적으로 DB의 s-lock, x-lock을 이용하여 구현하는 것이다.
PESSIMISTIC_READ는 s-lock을 사용하는 방법으로, 만약 DB에서 s-lock을 지원하지 않으면 x-lock(for update)으로 대체된다.
@Repository
public interface DebateRepository extends JpaRepository<Debate, Long> {
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("select deb from Debate deb left join fetch deb.debateMembers where deb.debateId= :DebateId")
Debate findDebatesTest(@Param("DebateId") Long DebateId);
}
- tx1) s-lock을 획득하여 findDebatesTest 쿼리 실행
- tx2) s-lock을 획득하여 findDebatesTest 쿼리 실행
- tx1) insert를 위해 x-lock을 획득해야 하지만, tx2에 s-lock이 걸려있어 끝날 때까지 대기 상태가 된다.
- tx2) insert를 위해 x-lock을 획득해야 하지만, tx2에 s-lock이 걸려있어 끝날 때까지 대기 상태가 된다.
- DB는 데드락을 감지하여 예외를 발생시키고 하나를 제외하고 모두 롤백시킨다.
- 디비를 보면 하나의 값만 들어와 있는 것을 확인할 수 있다.
즉, 보통 조회에는 락을 걸지 않고 조회하는데, PESSIMISTIC_READ 때문에 '단순 조회'에 s-lock을 걸어서 생긴 데드락이다.
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-08-17 12:50:32 22960165349120
*** (1) TRANSACTION:
TRANSACTION 40760, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 9 row lock(s), undo log entries 1
MySQL thread id 23883, OS thread handle 22959890827008, query id 2525350 210.179.37.10 admin update
insert into debate_member (deb_id,u_id) values (104,7)
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 88 page no 5 n bits 80 index fk1_idx of table `bstdb`.`debate_member` trx id 40760 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 88 page no 5 n bits 80 index fk1_idx of table `bstdb`.`debate_member` trx id 40760 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
*** (2) TRANSACTION:
TRANSACTION 40761, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 9 row lock(s), undo log entries 1
MySQL thread id 23884, OS thread handle 22960685917952, query id 2525351 210.179.37.10 admin update
insert into debate_member (deb_id,u_id) values (104,8)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 88 page no 5 n bits 80 index fk1_idx of table `bstdb`.`debate_member` trx id 40761 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 88 page no 5 n bits 80 index fk1_idx of table `bstdb`.`debate_member` trx id 40761 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
*** WE ROLL BACK TRANSACTION (2)
4. 비관적 락 - PESSIMISTIC_WRITE
DB의 x-lock을 사용하는 방법이다. 성공적으로 작동한다.
x-lock은 여러 트랜잭션이 동시에 가질 수 없기 때문에 데드락이 발생하지 않는다.
- tx1이 x-lock을 획득한다.
- tx2가 x-lock을 획득하려고 하지만 tx1이 lock을 갖고 있어 대기 상태가 된다.
- tx1이 종료된 이후에야 tx2가 lock을 획득하고 처리를 시작한다.
@Repository
public interface DebateRepository extends JpaRepository<Debate, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select deb from Debate deb left join fetch deb.debateMembers where deb.debateId= :DebateId")
Debate findDebatesTest(@Param("DebateId") Long DebateId);
}
2023-08-17T10:15:22.803+09:00 INFO 45888 --- [nio-8080-exec-8] c.b.b.d.debates.service.DebateService : userId:11 - 명수:3
2023-08-17T10:15:22.844+09:00 INFO 45888 --- [nio-8080-exec-6] c.b.b.d.debates.service.DebateService : userId:4 - 명수:4
2023-08-17T10:15:22.872+09:00 INFO 45888 --- [nio-8080-exec-5] c.b.b.d.debates.service.DebateService : userId:9 - 명수:5
2023-08-17T10:15:22.893+09:00 INFO 45888 --- [nio-8080-exec-3] c.b.b.d.debates.service.DebateService : userId:5 - 명수:6
2023-08-17T10:15:22.915+09:00 INFO 45888 --- [nio-8080-exec-9] c.b.b.d.debates.service.DebateService : userId:13 - 명수:7
2023-08-17T10:15:22.935+09:00 INFO 45888 --- [nio-8080-exec-7] c.b.b.d.debates.service.DebateService : userId:8 - 명수:8
2023-08-17T10:15:22.954+09:00 INFO 45888 --- [io-8080-exec-10] c.b.b.d.debates.service.DebateService : userId:12 - 명수:9
2023-08-17T10:15:22.975+09:00 INFO 45888 --- [nio-8080-exec-1] c.b.b.d.debates.service.DebateService : userId:7 - 명수:10
2023-08-17T10:15:22.986+09:00 INFO 45888 --- [nio-8080-exec-2] c.b.b.d.debates.service.DebateService : userId:6 - 명수:10
2023-08-17T10:15:22.996+09:00 INFO 45888 --- [nio-8080-exec-4] c.b.b.d.debates.service.DebateService : userId:10 - 명수:10
https://willbfine.tistory.com/576
<JPA> 낙관적락(Optimistic Lock), 비관적락(Pessimistic Lock)
가. Lock을 알아보자 a. Lock Lock ~ - lock은 동시성 제어를 위한 상호배제 기법 중 하나이다. - 기본적으로 어플리케이션에서의 공용자원 관리, DB에서의 행 데이터 관리를 위해 사용한다. - DB의 row 단
willbfine.tistory.com
MySQL 낙관적 락과 데드락(dead lock) With JPA Hibernate
프로젝트에서 모임 가입 기능을 구현하면서, 동시성 문제와 데드락까지 경험한 내용 그리고 어떻게 해결하였는지 고민과정과 해결방법을 정리하려고 합니다. 프로젝트 버전 SpringBoot 2.7.8 MySQL 8.
0soo.tistory.com
'개발 > Spring' 카테고리의 다른 글
[리팩토링] Spring-data-JPA와 Querydsl을 함께 사용하며 (0) | 2023.10.01 |
---|---|
MySQL 외래키와 데드락 (1) | 2023.08.17 |
[N+1 문제] 일반조인, fetch조인으로 OSIV, LAZY/EAGER, N+1문제 확실히 이해하기 (0) | 2023.08.16 |
JPA 프록시 개념/존재이유와 find(), getReferenceById() (0) | 2023.08.08 |
[N+1 문제] 게시글 댓글 조회 기능 querydsl로 해결하기 (0) | 2023.08.06 |
댓글