2025. 2. 24. 22:45ㆍDatabase
고민
JPA의 낙관적 락(Optimistic Lock)은 트랜잭션 커밋 시점에 데이터베이스의 version 값을 확인하여,
최초 조회 시점의 version 값과 다를 경우 롤백하는 방식으로 동작한다.
이때 다음과 같은 의문점이 들었다.
commit 시점에 version 을 확인하여 최초 조회 시점과 달라진 경우 예외를 발생시키도록 동작한다면,
version 을 확인하기 위해서는 기존 트랜잭션이 아닌 새로운 트랜잭션을 만든 후,
새롭게 만들어진 트랜잭션을 통해 version 값을 확인해오는 식으로 동작하는건가?
이와 같이 생각한 이유는 다음과 같다.
- MYSQL 의 기본 isolation level 은 REPEATABLE READ 로써, 이는 한 트랜잭션에서 동일한 값을 여러번 읽었을 때, 그 결과가 동일함을 보장해준다.
- 낙관적 락 사용 시, 트랜잭션 내에서 특정 데이터를 읽어온 경우, commit 직전에 같은 트랜잭션에서 읽더라도 version 값은 최초 읽은 값과 동일하게 읽혀질 것인데, 이렇게 되면 낙관적 락이 동작하지 않게 된다.
- 따라서 version을 확인하기 위해 새로운 트랜잭션을 만든 후, 해당 트랜잭션을 통해 version 값을 읽어와 비교하는 식으로 동작할 것이라고 판단했다.
결론
결론적으로는 그렇지 않고, UPDATE 시 WHERE 절에 'version = 최초읽은시점의 version 값' 조건을 주어, 영향을 받은 row 가 0 이면 예외를 발생시키는 식으로 동작한다.
이때 UPDATE 절의 WHERE 조건은 MVCC 로 인해 만들어진 undo 로그를 보는 것이 아닌, 실제 데이터를 보는 식으로 동작한다.
(참고 - update 쿼리 시에는 x-lock 이 걸리는데, mysql 의 Undo log 에는 lock 이 걸리지 않는다. 실제 데이터에 락이 걸림)
검증 - MySQL 에서 UPDATE의 동작
환경
- DB: MySQL 8.0 버전
- isolation level: REPEATABLE READ
먼저 MySQL 을 직접 사용하여, update 의 동작을 확인하자.
CREATE TABLE member
(
id bigint not null primary key auto_increment,
name varchar(255) unique not null,
age int not null,
version bigint not null default 0
);
INSERT member (name, age) values ('A', 10);
INSERT member (name, age) values ('B', 20);
INSERT member (name, age) values ('C', 30);
TX 1
set autocommit = 0;
# 1
SELECT * FROM member WHERE name = 'A';
# 5
SELECT * FROM member WHERE name = 'A';
# 6
UPDATE member SET age = 11, version = 1 WHERE name='A' AND version = 0;
SELECT * FROM member WHERE name = 'A';
# 7
commit ;
TX 2
set autocommit = 0;
# 2
SELECT * FROM member WHERE name = 'A';
# 3
UPDATE member SET age = 2, version = 1 WHERE name='A' AND version = 0;
SELECT * FROM member WHERE name = 'A';
# 4
commit;
- # 1의 결과
- # 2의 결과
- # 3의 결과
- # 4의 결과 -> commit
- # 5의 결과
- # 6의 update 쿼리 실행 결과
update 실행 결과 affected row 가 없는 것을 확인할 수 있으며, SELECT 의 결과를 통해 확인하더라도 값이 바뀌지 않음을 확인할 수 있음.
만약 TX 1 의 UPDATE 쿼리의 WHERE 조건이 version=1 로 변경된다면?
set autocommit = 0;
# 1
SELECT * FROM member WHERE name = 'A';
# 5
SELECT * FROM member WHERE name = 'A';
# 6
UPDATE member SET age = 11, version = 1 WHERE name='A' AND version = 1;
SELECT * FROM member WHERE name = 'A';
# 7
commit ;
- # 5 까지는 동일
- # 6의 Update 실행 시
- 6의 select 실행 시
이를 통해 알 수 있듯, update 시에는 undo log 의 값을 보지 않고, 실제 데이터를 확인함!
(참고 - MySQL 에서는 Undo log 에 lock 을 걸지 않는다. 즉 UPDATE 쿼리 시 x-lock 은 undo log 가 아닌 실제 데이터를 잠금)
검증 - JPA 의 낙관적 락 동작
환경
- Java: 21
- Spring Boot : 3.4.3
- (... 생략)
- JdbcResourceLocalTransactionCoordinatorImpl.commit()
- JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback()
- TransactionCoordinatorOwner.beforeTransactionCompletion()
- 구현체: JdbcCoordinatorImpl
- (JdbcSessionOwner)owner.beforeTransactionCompletion();
- 구현체: SessionImpl
- SessionImpl.flushBeforeTransactionCompletion()
- SessionImpl.managedFlush()
- SessionImpl.doFlush()
- fastSessionServices.eventListenerGroup_FLUSH .fireEventOnEachListener( event, FlushEventListener::onFlush );
- DefaultFlushEventListener.onFlush()
- DefaultFlushEventListener.performExecutions()
- ActionQueue.executeActions();
- EntityUpdateAction.executeActions();
- EntityUpdateAction.execute();
- persister.getUpdateCoordinator().update();
- UpdateCoordinatorStandard.update()
- UpdateCoordinatorStandard.performUpdate()
- UpdateCoordinatorStandard.doStaticUpdate()
- mutationExecutor.execute()
- mutationExecutor : MutationExecutorSingleNonBatched
- MutationExecutorSingleNonBatched.performNonBatchedOperations()
- AbstractMutationExecutor.performNonBatchedMutation()
- ModelMutationHelper.checkResults();
- UpdateCoordinatorStandard.checkResult()
- ModelMutationHelper.identifiedResultsCheck()
- statementDetails.getExpectation().verifyOutcome() 에서 체크!
'Database' 카테고리의 다른 글
InnoDB 알아보기 (On-Disk Structures - DoubleWriteBuffer) (0) | 2024.02.03 |
---|---|
InnoDB 알아보기 (On-Disk Structures - Tablespace) (0) | 2024.02.02 |
InnoDB 알아보기 (In-Memory Structures - Log Buffer) (0) | 2024.01.31 |
InnoDB 알아보기 (In-Memory Structures - Adaptive Hash Index) (1) | 2024.01.31 |
InnoDB 알아보기 (In-Memory Structures - Change Buffer) (0) | 2024.01.29 |