Repeatable Read는 트랜잭션 내 읽기 일관성을 보장하는 Database의 Isolation Level이다.
예를 들어서 Transaction에서 최초 User 테이블의 name 컬럼 데이터를 읽었다면 이는 가장 최근에 커밋된 언두로그 또는 커밋된 메인 데이터의 데이터를 읽고 있는 것이며, 해당 Transaction이 끝날 때까지 이 Trasanction은 그 데이터를 기반으로 동작한다.
즉, 1번 트랜잭션이 User id 1번을 "홍길동"으로 읽었다고 치자. 그러면 그 데이터는 해당 트랜잭션이 끝날 때까지 다른 트랜잭션의 변경에 관계 없이 계속 "홍길동"인 것이다. (1번 트랜잭션에서 변경한 경우는 제외)
보통 자주 쓰이는 데이터 베이스를 기준으로
의 Isolation Level을 기본으로 갖는다.
자 위의 설명을 토대로 아래 시나리오를 생각해보자.
현재 user_test의 id = 1은 "홍길동"인 상황.
update user_test set name = '강길동' where id = 1;
로 name 변경 후 커밋 select user_test.name from user_test where id = 1;
이렇게 동작했을 때 어떤 데이터가 나올까?
나는 당연히 홍길동이 나올줄 알았다.
하지만 실제로 해보면 데이터는 강길동이 나온다.
MySQL 공식 문서에 보면 이 이유가 나온다.
결론적으로 얘기를 하자면 일관적 읽기(Consistent Read)는 첫 번째 읽기부터 적용된다고 써져있다.
consistent read
A read operation that uses snapshot information to present query results based on a point in time, regardless of changes performed by other transactions running at the same time. If queried data has been changed by another transaction, the original data is reconstructed based on the contents of the undo log. This technique avoids some of the locking issues that can reduce concurrency by forcing transactions to wait for other transactions to finish.With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation.
Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED and REPEATABLE READ isolation levels. Because a consistent read does not set any locks on the tables it accesses, other sessions are free to modify those tables while a consistent read is being performed on the table.
For technical details about the applicable isolation levels, see Section 17.7.2.3, “Consistent Nonlocking Reads”.
See Also concurrency, isolation level, locking, READ COMMITTED, REPEATABLE READ, snapshot, transaction, undo log.
이전에 썻던 Post에서 동시에 시작된 트랜잭션에서 Lock이 끝나고 최신 데이터가 들어오는 경우가 있었다.
문제 코드는 아래와 같았는데, commentConverter.requestToEntity
에서 사실 처음으로 select문을 하기 때문에 Transaction 1, 2가 동시에 시작된다면 이 때를 기준으로 commentCount를 둘 다 0으로 들고 있고 이후 fidnByIdForUpdate
에서 락 이후 데이터를 받았을 때 Repeatable Read이기 때문에 트랜잭션 2도 결국 0에서 시작할줄 알았다.
하지만 결과는 두 트랜잭션이 동시에 수행이되면 commentCount는 2가 된다.
@Transactional
public Comment save(CommentSaveDto request, Member member) {
validatePostNotReplied(request);
Comment comment = commentConverter.requestToEntity(member, request);
em.clear();
Post post = postRepository.findByIdForUpdate(request.postId())
.orElseThrow(() -> new NotFoundException(PostStatus.POST_NOT_FOUND));
log.info("[CUSTOM LOG] PESSIMISTIC_WRITE 락 획득: commentCount = {}", post.getCommentCount());
long count = post.getCommentCount();
count++;
post.setCommentCount(count);
postRepository.save(post);
log.info("[CUSTOM LOG] post 저장 : commentCount = {}", post.getCommentCount());
return commentRepository.save(comment);
}
이 이유를 또한 MySQL 공식문서에서 다음과 같은 문장이 나온다.
A SELECT ... FOR UPDATE reads the latest available data, setting exclusive locks on each row it reads. Thus, it sets the same locks a searched SQL UPDATE would set on the rows.
FOR UPDATE 옵션을 써서 Exclusive Lock을 걸었을 경우(Java의 경우 @Lock(LockModeType.PESSIMISTIC_WRITE)
에 해당) Isolation Level의 동작과 관련 없이 가장 최신의 데이터를 가져온다.
현재 test user id = 1은 홍길동인 상황
시나리오
테스트 조건과 시나리오는 위와 동일하지만 단 transaction 1의 2번째 셀렉트 쿼리는 for update를 적용
시나리오