Transaction
- transaction : 최소 행동 단위
- exception 발생시 commit하지않고 rollback을 하기 위함.
Spring Transaction
- 트랜잭션 동기화
- 트랜잭션을 위한 connection을 스레드마다 할당받아 멀티스레드에서 사용할 수 있다.
- db에 종속되지않게 트랜잭션이 추상화되어있다.
- PlatformTransactionManager
- AOP 사용으로 비즈니스로직과 분리되어있다.
@Transactional
- 트랜잭션 전파 Propagation
- A 트랜잭션 내부에서 다른 B 트랜잭션이 생성될때 어떻게 처리할것인지
- PROPAGATION_REQUIRED (default)
- 트랜잭션 참여, A와 B 가 같은 트랜잭션을 사용.
- B <-> A 결과 전파가 가능하다.
- PROPAGATION_SUPPORTS
- 기존 트랜잭션(A)과 커넥션,세션 등을 공유하고, 기존 트랜잭션(A)이 없으면 트랜잭션이 없는것과 같다.
- sync 문제 해결용으로 사용가능할듯.
- PROPAGATION_MANDATORY
- 기존 트랜잭션이 있으면 참여하고, 기존 트랜잭션(A)가 없으면 exception을 발생시킨다
- 단독으로 실행되면 안되는 로직이 있을때 사용가능.
- PROPAGATION_REQUIRES_NEW
- 트랜잭션 분리, B트랜잭션이 새로 생성되고, A의 트랜잭션과 무관하다.
- PROPAGATION_NOT_SUPPORTED
- 기존 트랜잭션(A)이 있으면 중단시킨다.
- 어떤 상황에서필요할까
- PROPAGATION_NEVER
- 기존 트랜잭션(A)가 있으면 Exception을 발생시킨다.
- PROPAGATION_NESTED
- required와 같지만, B의 성공실패가 A에 영향을 주지않는다. A->B로의 롤백전파는 가능.
- 격리수준
- DEFAULT
- datasource의 기본 설정에 따른다.
- mysql은 REPEATABLE_READ
- READ_UNCOMMITTED (dirty read)
- 다른 트랜잭션에서 커밋되지않은 변경사항까지 읽어온다. 만약 다른 트랜잭션에서 롤백되었다면, invalid한 데이터를 읽게된다.
- 가장 빠르기 때문에, 일관성대신 속도를 택할경우 의도적으로 사용될 수 있다.
- READ_COMMITTED
- 실제 커밋된 변경사항만 읽어온다.
- 다른 트랜잭션에서 수정하는것을 허용하기때문에, 다시 읽을때 변경된 값을 읽어올 수 있다.
- REPEATABLE_READ
- 실제 커밋된 변경사항만 읽어오고, 다시읽어오는게 가능하다.
- A 트랜잭션에서 읽고, B에서 수정후 A에서 다시 읽을때 변경된 값을 읽어올 수 없다. ()
- A 트랜잭션에서 읽고, B에서 insert 후 다시 읽을때 insert된 row가(phantom row) 읽힌다.
- 수정은 막아주지만, insert는 허용된다.
- SERIALIZABLE
- REPEATABLE_READ와 동일한데, phantom read가 일어나지 않는다.
- 읽고-> insert 후 -> 다시읽을때, insert된 row가(phantom) 읽히지 않는다.
- 여러 트랜잭션이 같은 테이블에 접근할수없어 성능이 떨어질수있고, 가장 강력한 격리수준이다.
- DEFAULT
- 제한시간
- 설정하지 않으면, 기본 타임아웃이 설정된다.
- org.springframework.transaction.TransactionDefinition#TIMEOUT_DEFAULT = -1 (제한 없음?)
- 읽기전용
- 성능 최적화, 의도되지않은 부작용(상태변경) 방지
JPA에서의 Transaction
no entitymanager with actual transaction available for current thread
jpa는 transaction을 기반으로 동작하므로, transaction 이 있어야 영속성 컨텍스트를 사용할 수 있다.
- save 의 동작 LINK
delete 와 Transaction (write-behind)
- delete는 영속성 컨텍스트의 쓰기지연 저장소에 저장되었다가, 트랜잭션의 커밋시점에 실행된다.
SimpleJPARepository
의 delete 에는 @Transactional(propagation = REQUIRED) 가 걸려있고, 상위 트랜잭션과 공유된다.- 따라서 delete 는 호출부가 아닌 transaction이 제일 먼저 걸린 (application layer 일 수 있음) 에서 호출되어 exception 발생위치도 delete 메서드 호출시점에서는 발생되지 않는다.
- 테스트할때도 마찬가지이며, 테스트코드에 transactional이 걸리면 메서드 종료시점에서 수행될 수 있다.
- jpa에서 delete는 select -> (update) -> remove 의 순서로 진행된다. (save는 왜 없어도 되는걸까?)
- transaction이 없으면 entitymanager를 얻을수 없으므로, exception이 발생한다.
- 참고
write-behind
- 영속성컨텍스트가 변경사항을 모아두었다가, 커밋시점 (flush)시에 한번에 실행하는데,
- insert -> 변경없음 -> insert -> flush = 1번 insert 발생
- insert -> 변경 -> insert -> flush = 1번 insert, 1번 update
- 참고
Lock
@Lock(LockMode.**)
- 낙관적 lock / 비관적 lock
/ | 낙관적 | 비관적 |
---|---|---|
방식 | 버전 방식, 업데이트대상 row 버전이 낮을경우 업데이트하지 않음. | 트랜잭션으로 shared lock을 잡음 |
롤백 | 트랜잭션으로 커밋,롤백 하지않으므로 수동 롤백 필요. | 트랜잭션 롤백 가능 |
컨트롤 | application | db |
- LockMode
- READ == OPTIMISTIC
- 되도록이면 명시적으로 Optimistic..을 사용하자.
- WRITE == OPTIMISTIC_FORCE_INCREMENT
- OPTIMISTIC (낙관적)
- OPTIMISTIC_FORCE_INCREMENT
- 버전정보를 활용하는 낙관적 락
- JPA
@Version
컬럼 필요
- PESSIMISTIC_READ (비관적)
- 트랜잭션으로 읽기까지 금지하는 락
- PESSIMISTIC_WRITE
- 트랜잭션으로 쓰기만 금지하는 락
- PESSIMISTIC_FORCE_INCREMENT
- 트랜잭션으로 쓰기를 금지시키고, 버전정보를 사용.
- NONE : no lock
- READ == OPTIMISTIC
문제상황 : 1개의 column(PK) 와 1개의 row를 가지는 데이터의 lock
- delete -> save -> select
- PK가 곧 유일한 데이터이기때문에, delete 를 할경우 다른 transaction에서 lock을 잡을 row가 없는것으로 판단된다. > 의도대로 lock이 걸리지 않음
- select -> update
- select가 먼저 일어날 경우 read라 그런지(?) 먼저 lock을 잡지 않는다. -> 의도대로 lock이 걸리지 않음.
- update -> select
- 의도대로 동작.
- 참고 ) mysql에서 CUD가 일어날경우 lock을 잡는다.
@Override @Retryable(maxAttempts = 2, backoff = @Backoff(delay = 1000L)) @Lock(LockModeType.PESSIMISTIC_READ) @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED) public int getSequence() { jpaRepository.updateSequence(); // custom query return jpaRepository.findFirst() .orElseGet(() -> jpaRepository.save(SomeEntity.init())) .getSequence(); }
ㅌ 동작 확인
@Test
@DisplayName("여러 스레드가 동시에 업데이트 요청시 중복이 없어야한다.")
void multiThread_lock() throws Exception {
Set<Integer> sequences = new HashSet<>();
int numberOfThreads = 20;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
service.execute(() -> {
sequences.add(service.getSequence());
latch.countDown();
});
}
latch.await();
assertThat(sequences).as("중복이 발생하지 않아야한다.").hasSize(numberOfThreads);
}
문제상황 : Deadlock found when trying to get lock; try restarting transaction
어플리케이션 로그에 db lock 관련 deadlock 로그가 찍혔고,
$ mysql> show engine innodb status;
------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:...
*** (1) HOLDS THE LOCK(S): ...
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: ...
*** (2) TRANSACTION: ...
*** (2) HOLDS THE LOCK(S): ...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED: ...
*** WE ROLL BACK TRANSACTION (1)
db로그를 확인하니 1과 2의 쿼리가 동일했다.
최근 변경사항중 클래스단에 @Transactional
어노테이션을 추가했는데 여기서 중복으로 트랜잭션이 발생한것같다.
@Transactional
public class SomeCommandService{
@Transactional
public void updateSomething(){...}
}
둘중하나 어노테이션 떼어주니 정상동작.
참고
- https://mangkyu.tistory.com/154
- https://mangkyu.tistory.com/169
- https://velog.io/@giantim/5
- https://sas-study.tistory.com/348
- 비관적/낙관적 락