트랜잭션과 락
본 내용은 real MySQL 책을 참조하여 쓰여졌습니다!
1. 트랜잭션
트랜잭션의 기본 개념에 대해서는 다음 블로그를 참조해주세요! 이 글은 트랜잭션에 대해 이미 이해하고 있음을 가정하고 있습니다.
https://mangkyu.tistory.com/50
트랜잭션의 경우 InnoDB 스토리지 엔진은 지원하지만 MyISAM이나 MEMORY는 지원하지 않습니다.
트랜잭션은 꼭 여러 쿼리가 조합되었을 때만 의미 있는 개념이 아니며 하나의 논리적인 작업 셋에 쿼리 개수와 관계 없이 해당 셋이 100% 적용되거나 아무것도 적용되지 않음을 보장해주는 것입니다.
예를 들어 다음 쿼리에서
INSERT INTO tab_myisam (fdpk) VALUES (1), (2), (3)
일 때 3이 이미 들어가 있다면 쿼리가 실패할 것입니다. 이때 트랜잭션을 지원하지 않는 MyISAM에서는 1, 2는 그대로 들어가게 되며 innoDB는 들어가지 않습니다. (AUTO-COMMIT 기준)
이러한 부분 업데이트 현상은 테이블 데이터 정합성을 맞추는데 어려운 문제를 만들어 내며 코드도 복잡해지게 됩니다.
- 주의사항
트랜잭션 또한 DBMS의 커넥션과 동일하게 꼭 필요한 최소의 코드에만 적용하는게 좋습니다. 예를 들어
1) 처리 시작
-> 데이터베이스 커넥션 생성
-> 트랜잭션 시작
2) 사용자 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 여부 확인
4) 첨부로 업로드된 파일 확인 및 저장
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
7) 저장된 내용 또는 기타 정보를 DBMS에서 조회
8) 게시물 등록에 대한 알림 메일을 발송
9) 알림 메일 발송 이력을 DBMS에 저장
<- 트랜잭션 종료
<- 데이터베이스 커넥션 반납
10) 처리 완료
라는 게시판에 게시물을 작성하는 로직이 있다고 해볼까요?
이는 트랜잭션 처리에 좋지 않은 영향을 주는데
- 2, 3, 4는 트랜잭션에 포함될 필요가 없습니다. 실제 데이터 저장은 5번부터 시작되며 이렇게 필요 없는 것까지 트랜잭션으로 처리하면 데이터베이스 커넥션 소유 시간이 길어져 여유 커넥션 개수가 줄어들게 됩니다.
- 8번과 같은 메일 전송 등의 네트워크 I/O는 통신할 수 없는 상황이 오는 등의 트랜잭션에 포함하기 위험한 처리입니다. (장애가 전파될 수 있습니다.)
- 즉 위처럼 여러 트랜잭션으로 분리될 수 있는 로직은 트랜잭션이 분리되는 편이 좋습니다.
개선안
1) 처리 시작
2) 사용자 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 여부 확인
4) 첨부로 업로드된 파일 확인 및 저장
-> 데이터베이스 커넥션 생성
-> 트랜잭션 시작
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
<- 트랜잭션 종료 (COMMIT)
7) 저장된 내용 또는 기타 정보를 DBMS에서 조회
8) 게시물 등록에 대한 알림 메일을 발송
-> 트랜잭션 시작
9) 알림 메일 발송 이력을 DBMS에 저장
<- 트랜잭션 종료(COMMIT)
<- 데이터 베이스 커넥션 종료
10) 처리 완료
강조하자면 데이터베이스 커넥션을 가지고 있는 범위와 트랜잭션이 활성화되어 있는 프로그램 범위를 비즈니스 로직에 따라 최소화해야 된다는 것입니다.
2. MySQL 엔진 락
MySQL에 사용되는 락에는 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나눌 수 있습니다.
MySQL 엔진 레벨
- 글로벌 락
- 테이블 락
- 네임드 락
- 메타데이터 락
스토리지 엔진 레벨: (InnoDB)
- 레코드 락
레코드 자체만을 잠그는 것을 말합니다.
InnoDB에서 중요한 차이점이 잇는데 레코드 자체가 아니라 인덱스의 레코드를 잠근다는 점입니다. 즉 변경해야 할 레코드를 찾기 위해 검색한 인덱스의 레코드를 모두 걸어야 합니다.
예를 들어 employees라는 테이블이 있고 first_name과 last_name column이 있다고 하겠습니다. 그 중 first_name에만 인덱스가 되어 있다고 해볼까요?
또한 first_name = ‘Georgi’은 100건
first_name = ‘Georgi’ AND last_name = ‘Klassen’은 1건만 존재하는 상황이 있다고 해봅시다.
그럴 경우
UPDATE employees SET hire_date = NOW() WHERE first_name = ‘Georgi’ AND last_name = ‘Klassen’;
라는 update 쿼리가 있고 해당 레코드가 1건이라면 어떻게 될까요? 놀랍게도 last_name에는 인덱스가 없기 때문에 first_name = ‘Georgi’에 해당되는 100건의 레코드에 모두 락이 걸리게 됩니다.
즉 Update를 위한 적절한 인덱스가 없다면 동시성이 엄청나게 떨어지게 됩니다.
이는 테이블에 인덱스가 하나도 없다면 테이블을 풀스캔하면서 모든 레코드를 잠그게 될 수 있다는 것으로 MySQL의 InnoDB에서는 인덱스 설계가 매우 매우 중요함을 알 수 있습니다.
- 갭 락
레코드와 레코드와 레코드와 바로 인접한 레코드 사이의 간격만을 잠급니다. 이는 레코드와 레코드 사이에 새로운 레코드가 생성되는 것을 제어합니다. 주로 넥스트 키 락의 일부로 자주 사용됩니다.
- 넥스트 키 락
레코드 락과 갭 락을 합친 형태의 락으로 REPEATABLE_READ 격리 수준에서만 사용할 수 있습니다. 이는 로그에 기록되는 쿼리가 레플리카 서버에서 실행될 때 소스 서버에서 만들어낸 결과와 동일한 결과를 만들어내도록 보장해주는 것이 주목적입니다.
- 자동 증가 락
자동 증가하는 값을 추출하기 위한 AUTO_INCREMENT에서 여러 레코드가 INSERT되는 경우 중복 없이 증가하기 위해 쓰입니다. 따라서 명시적으로 획득하고 해제되지 않습니다.
3. MySQL의 격리 수준
Dirty read | non-repeatable read | phaontom read | |
read_uncommited | O | O | O |
read_commited | X | O | O |
repeatable read | X | X | O (InnoDB는 X) |
serializable | X | X | X |
- READ UNCOMMITED
커밋되지 않은 다른 트랜잭션 변경 내용이 다른 트랜잭션에서 보일 수 있습니다.
-> 사실 커밋이 되지도 않는 데이터가 보인다는 건 애초에 격리인가 의문이 듭니다. 실제로 RDBMS에서는 격리 수준으로 인정되지 않는다고 합니다. 트랜잭션 연쇄 롤백까지도 일으킬 수 있는 치명적인 문제가 있습니다.
- READ COMMITED
오라클 DBMS 기본 격리 수준이며 온라인 서비스에서 가장 많이 선택됩니다.
하나의 트랜잭션에서 똑같은 SELECT 쿼리를 실행했을 때 다른 결과를 가져올 수 있는 문제가 있습니다. 즉 REAPEATABLE_READ 정합성에서 어긋납니다. 이는 하나의 트랜잭션에서 동일 데이터를 여러 번 읽고 변경하는 작업이 금전적인 처리와 연결되면 큰 문제가 됩니다. 예를 들어 입금된 금액의 총합을 조회하는 트랜잭션과 입금, 출금 처리가 계속되는 트랜잭션 이 두 트랜잭션이 동시에 실행되고 있으면 총합 계산 쿼리는 실행될 때마다 계속 다른 결과를 가져올 것입니다.
- REPEATABLE READ
전에 설명한 Undo Log 덕분에 (MVCC) 락 없이 달성하고 있습니다. 모든 InnoDB의 트랜잭션은 고유 트랜잭션 번호(순차 증가)를 가지며 Undo Log에도 기록됩니다. 이는 다른 트랜잭션의 INSERT에 따라 한 트랜잭션의 SELECT 결과가 달라질 수 있는 PHANTOM READ에 자유롭지 못하다는 문제가 남습니다.. 하지만 InnoDB의 경우 갭락과 넥스트 키 락 덕분에 발생하지 않습니다. (엄밀히는 SELECT FOR UPDATE와 FOR SHARE 상황에서는 발생할 수 있습니다.)