좋아요_수, 댓글_수 증감 진행
@Transactional
public void comment(Long userId, Long feedId, CommentRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));
Feed feed = feedRepository.findById(feedId)
.orElseThrow(() -> new GlobalException(ErrorCode.FEED_NOT_FOUND));
commentRepository.save(
Comment.builder()
.user(user)
.feed(feed)
.content(request.getContent())
.build()
);
feed.addComment();
}
public void addComment() {
this.commentCount += 1;
}
@Test
@Transactional
void commentCurrencyTest() throws InterruptedException {
CommentRequest commentRequest = new CommentRequest("하이");
Long feedId = 7L;
int count = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 1; i <= count; i++) {
Long userId = (i % 2 == 0) ? 2L : 3L;
executorService.execute(() -> {
try {
commentService.comment(userId, feedId,commentRequest);
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
countDownLatch.countDown();
});
}
countDownLatch.await();
Feed feed = feedRepository.findById(feedId).orElseThrow(RuntimeException::new);
Assertions.assertThat(feed.getCommentCount()).isEqualTo(100);
}


락 이란?
데이터베이스에서 특정 자원(예: 테이블, 행, 페이지 등)에 대한 접근을 제어하기 위해 사용되는 메커니즘 → 즉, 쉽게 생각하면 특정 자원(DB row 등)에 접근할 수 있는 권한
SHOW ENGINE INNODB STATUS\g 확인------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-01-04 22:52:03 0x16c097000
*** (1) TRANSACTION:
TRANSACTION 573770, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 3 row lock(s), undo log entries 1
MySQL thread id 332, OS thread handle 6136180736, query id 152592 localhost 127.0.0.1 root updating
update feed set comment_count=18,content='안녕하세요',last_modified_at='2025-01-04 22:52:03.477048',like_count=0,user_id=2 where feed_id=7
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 460 page no 4 n bits 80 index PRIMARY of table `devhub`.`feed` trx id 573770 lock mode S locks rec but not gap
Record lock, heap no 9 PHYSICAL RECORD: n_fields 9; compact format; info bits 64
0: len 8; hex 8000000000000007; asc ;;
1: len 6; hex 00000008c145; asc E;;
2: len 7; hex 010000018c0276; asc v;;
3: len 8; hex 99b5396de607882f; asc 9m /;;
4: len 8; hex 99b5896d0307224b; asc m "K;;
5: len 15; hex ec9588eb8595ed9598ec84b8ec9a94; asc ;;
6: len 8; hex 8000000000000002; asc ;;
7: len 4; hex 80000012; asc ;;
8: len 4; hex 80000000; asc ;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 460 page no 4 n bits 80 index PRIMARY of table `devhub`.`feed` trx id 573770 lock_mode X locks rec but not gap waiting
Record lock, heap no 9 PHYSICAL RECORD: n_fields 9; compact format; info bits 64
0: len 8; hex 8000000000000007; asc ;;
1: len 6; hex 00000008c145; asc E;;
2: len 7; hex 010000018c0276; asc v;;
3: len 8; hex 99b5396de607882f; asc 9m /;;
4: len 8; hex 99b5896d0307224b; asc m "K;;
5: len 15; hex ec9588eb8595ed9598ec84b8ec9a94; asc ;;
6: len 8; hex 8000000000000002; asc ;;
7: len 4; hex 80000012; asc ;;
8: len 4; hex 80000000; asc ;;
*** (2) TRANSACTION:
TRANSACTION 573768, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 3 row lock(s), undo log entries 1
MySQL thread id 331, OS thread handle 6135066624, query id 152594 localhost 127.0.0.1 root updating
update feed set comment_count=18,content='안녕하세요',last_modified_at='2025-01-04 22:52:03.478154',like_count=0,user_id=2 where feed_id=7
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 460 page no 4 n bits 80 index PRIMARY of table `devhub`.`feed` trx id 573768 lock mode S locks rec but not gap
Record lock, heap no 9 PHYSICAL RECORD: n_fields 9; compact format; info bits 64
0: len 8; hex 8000000000000007; asc ;;
1: len 6; hex 00000008c145; asc E;;
2: len 7; hex 010000018c0276; asc v;;
3: len 8; hex 99b5396de607882f; asc 9m /;;
4: len 8; hex 99b5896d0307224b; asc m "K;;
5: len 15; hex ec9588eb8595ed9598ec84b8ec9a94; asc ;;
6: len 8; hex 8000000000000002; asc ;;
7: len 4; hex 80000012; asc ;;
8: len 4; hex 80000000; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 460 page no 4 n bits 80 index PRIMARY of table `devhub`.`feed` trx id 573768 lock_mode X locks rec but not gap waiting
Record lock, heap no 9 PHYSICAL RECORD: n_fields 9; compact format; info bits 64
0: len 8; hex 8000000000000007; asc ;;
1: len 6; hex 00000008c145; asc E;;
2: len 7; hex 010000018c0276; asc v;;
3: len 8; hex 99b5396de607882f; asc 9m /;;
4: len 8; hex 99b5896d0307224b; asc m "K;;
5: len 15; hex ec9588eb8595ed9598ec84b8ec9a94; asc ;;
6: len 8; hex 8000000000000002; asc ;;
7: len 4; hex 80000012; asc ;;
8: len 4; hex 80000000; asc ;;
*** WE ROLL BACK TRANSACTION (2)
HOLDS THE LOCK(S) : s-Lock(Shared Lock) 획득WAITING FOR THIS LOCK TO BE GRANTED : Lock 획득을 대기Locking
MySQL extends metadata locks, as necessary, to tables that are related by a foreign key constraint. Extending metadata locks prevents conflicting DML and DDL operations from executing concurrently on related tables. This feature also enables updates to foreign key metadata when a parent table is modified. In earlier MySQL releases, foreign key metadata, which is owned by the child table, could not be updated safely.If a table is locked explicitly with LOCK TABLES, any tables related by a foreign key constraint are opened and locked implicitly. For foreign key checks, a shared read-only lock (LOCK TABLES READ) is taken on related tables. For cascading updates, a shared-nothing write lock (LOCK TABLES WRITE) is taken on related tables that are involved in the operation.
출처 https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html
공유, 읽기 잠금 등으로 데이터를 읽을 때 사용되는 Lock
다른 s-lock과 한 리소스에 두개 이상의 Lock을 동시에 설정할 수 있으나, x-Lock은 불가능
여러 트랜잭션에서 동시에 하나의 데이터를 읽을 수 있다. 그러나 변경중인 리소스를 동시에 읽을 수는 없다.
- FK가 있는 테이블에서 FK를 포함한 데이터를 insert, update, delete 쿼리는 제약 조건 확인을 위해 s-Lock 설정
→ comment 테이블에 데이터 insert 동작중 fk인 Feed에 s-lock
데이터 변경시 사용
다른 Lock 들과 호환되지 않기 때문에, 한 리소스에 하나의 x-Lock만 설정 가능
동시에 여러 트랜잭션이 한 리소스에 엑세스할 수 없게 된다.(읽기도 불가)
→ 오직 하나의 트랜잭션만 해당 리소스 점유 가능
- 레코드에 update 쿼리 동작시 사용되는 모든 레코드에 x-Lock 설정
→ Feed에 commentCount를 update 하면서 x-lock 설정
→ 서로 다른 트랜잭션이 같은 자원에 대해 Lock를 가져 서로 Lock를 해제할때까지 대기하면서 데드락 발생
데드락에 대해 조사하다 보니 A트랜잭션에서 이미 Lock를 점유한 상태에서 B트랜잭션이 동일한 리소스에 대한 Lock 요구하는 상황이라면 이런 상황을 처음부터 막는다면 데드락이 발생하지 않을 거라는 생각이 듬.
이런 방법을 비관적 락이라고 한다.
@Version 애노테이이션 사용@Lock 애노테이션 사용@Entity
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Feed extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long feedId;
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@Version
private Long version;
}
---------------------------
public interface FeedRepository extends JpaRepository<Feed, Long> {
@Lock(LockModeType.OPTIMISTIC)
Optional<Feed> findByFeedId(Long feedId);
}
예를 들어 선착순 100명에게 발급해주는 쿠폰 이벤트에 동시에 100명이 요청했다면 100명 모두 동시에 받는게 아닌 최초의 커밋 1명만 받고 나머지 99명은 다시 이벤트에 신청해야 한다. → 이에 따른 후속 조치를 해줘야 한다.
(Retry 등…)
@Lock 애노테이션 사용PESSIMISTIC_WRITE와 유사하지만 @Version이 지정된 Entity와 협력하기 위해 도입되어 PESSIMISTIC_FORCE_INCREMENT 락을 획득할 시 버전이 업데이트@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select f from Feed f where f.feedId = :feedId")
Optional<Feed> findByFeedId(Long feedId);
예를 들어
→ 댓글과 좋아요라는 기능의 경우 충돌이 일어날수 밖에 없는 기능이라 생각되어 비관적 락을 선택
@Test
void commentCurrencyTest() throws InterruptedException {
CommentRequest commentRequest = new CommentRequest("하이");
Long feedId = 10L;
int count = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 1; i <= count; i++) {
Long userId = (i % 2 == 0) ? 2L : 3L;
executorService.execute(() -> {
try {
commentService.comment(userId, feedId,commentRequest);
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
countDownLatch.countDown();
});
}
countDownLatch.await();
Feed feed = feedRepository.findById(feedId).orElseThrow(RuntimeException::new);
System.out.println(feed.getCommentCount());
Assertions.assertThat(feed.getCommentCount()).isEqualTo(100);
}
----------------------------------------------------------------
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select f from Feed f where f.feedId = :feedId")
Optional<Feed> findByFeedId(Long feedId);
----------------------------------------------------------------
@Transactional
public void comment(Long userId, Long feedId, CommentRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));
Feed feed = feedRepository.findByFeedId(feedId)
.orElseThrow(() -> new GlobalException(ErrorCode.FEED_NOT_FOUND));
commentRepository.save(
Comment.builder()
.user(user)
.feed(feed)
.content(request.getContent())
.build()
);
feed.addComment();
}
