1. 트랜잭션의 정의
트랜잭션은 작업의 수와 상관없이 작업 세 자체가 100%반영(커밋) 이 되거나 아무것도 적용되지 않음(롤백)을 보장해주는 것이다.
2. MySQL 엔진 마다 다르게 처리하는 트랜잭션
MySQL 에도 여러 엔진이 있다. 요즘 흔하게 사용되는 InnoDB 스토리지 엔진은 당연히 트랜잭션의 기능을 제공한다.
하지만, MyISAM 이나 MEMORY 엔진은 트랜잭션을 적용하지 않는다.
MyISAM 이나 MEMORY 엔진이 더 빠르다고 생각하고 사용하기에 덜 복잡하다고 생각할 수 있지만 트랜잭션을 지원하지 않아 예외 발생 시 후처리 과정이 포함되어야 하므로 어플리케이션 레벨에서 코드를 짜기는 훨씬 어렵다.
3. 트랜잭션 단위의 축소
하나의 트랜잭션 단위에 많은 작업을 하면 그 시간 동안 DB 커넥션을 물고 있는 것이어서 자원 소모량이 크고 작업이 많으면 예외가 발생할 확률이 올라간다. 특히 작업 가운데 외부 서버와의 통신을 해야 하는 작업이 있다면 외부 서버와의 통신이 트랜잭션 처리 결과에 영향을 주게된다. 그래서 최대한 트랜잭션 단위를 축소하는 것이 좋다.
4. MySQL 엔진의 잠금
먼저, 스토리지 엔진 레벨의 잠금이 아님에 주의해야 한다.
1) 글로벌 락
글로벌 락은 FLUSH TABLES WITH READ LOCK 명령으로 획득할 수 있으며, 전체 MySQL 서버 자체에 대한 락으로 한 세션이 글로벌 락을 걸면 다른 세션에서의 요청은 대기 상태로 남는다.
이 글로벌 락은 MyISAM 이나 Memory 엔진에서 사용하는 글로벌 락이고 InnoDB 가 기본 스토리지 엔진으로 채택된 이후 부터는(Inno DB 는 트랜잭션을 지원하기 때문에 모든 데이터 변경 작업을 멈출 필요가 없다) 가벼운 글로벌 락의 필요성이 생겼다. 백업 락을 도입하여 백업 툴이 안정적으로 백업을 할 수 있게 한다는 구체적인 내용은 시간이 되면 학습을 해야 겠다.(지금 당장 필요한 내용이 아니기 때문입니다.)
2) 테이블 락
개별 테이블 단위로 설정되는 잠금이며, 명시적/묵시적으로 획득할 수 있다.
명시적인 락 설정은 명령어를 통해서 획득하는 방식이고 묵시적인 락은 테이블 스키마를 변경하거나 테이블의 레코드 값을 수정하는 경우 별도의 명령 없이 락을 획득해 쿼리를 수행한 후 락을 해제하는 방식이다.
Inno DB 의 경우 레코드 락을 지원하기 때문에 묵시적 락이 설정되지 않는다. 더 정확히 말하면 테이블 락이 설정되긴 하지만 데이터 변경(DML) 쿼리에서는 무시되고 스키마를 변경하는 쿼리(DDL)의 경우에만 영향을 미친다.
3) 네임드 락
네임드 락은 GET_LOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다. 네임드 락의 목적은 어플리케이션 레벨에서 락을 설정하기 위함이다. 어플리케이션 레벨에서는 언어 차원에서 락을 제공하긴 하지만 어플리케이션 서버가 여러 대인 경우 언어 차원에서 제공하는 락을 사용할 수 없다.(하나의 서버에서 락을 걸고 작업을 수행해도 같은 서버의 다른 쓰레드에서만 해당 작업에 대해 대기할 뿐 다른 서버에서는 해당 작업을 수행할 수 있다). 레디스를 활용한 분산 락을 활용하는 방법이 있지만 이 방법은 비용적인 문제가 발생한다. 이런 상황에서 사용하는 방법은 네임드 락을 활용하는 방법이다.
4) 메타 데이터 락
메타 데이터 락은 데이터베이스 객체의 이름이나 구조를 변경하는 경우 획득하는 락이다. 구체적인 내용은 다음에 학습
5. Inno DB 스토리지 엔진 잠금
Inno DB 스토리지 엔진은 MySQL 엔진에서 제공하는 잠금과 별개로 스토리지 엔진 내부에서 레코드 기반의 잠금 방식을 제공한다.
1) 레코드 락
레코드 자체를 잠그는 방식이다. 다른 DBMS 와의 레코드 락과 유사히지만 다른 점이 있다면 인덱스 레코드에 락을 건다는 것이다. 인덱스가 하나도 없는 테이블이더라도 내부적으로 자동 생성된 클러스터링 인덱스를 이용해 잠금을 설정한다.

인덱스 레코드에 락이 걸리기 때문에 인덱스 적용 시 주의해야 한다.
예를 들어, 특정 컬럼에만 인덱스가 적용된 상황에서 인덱스가 적용된 컬럼과 그렇지 않은 컬럼을 이용하여 변경작업을 하는 경우 여러 인덱스 레코드에 잠금이 걸릴 수 있다.
# member 테이블에는 last_name 컬럼만으로 구성된 인덱스 KEY idx_last_name(last_name)가 존재한다.
# 해당 구성원의 등록일을 오늘로 변경하는 쿼리를 실행해보자.
UPDATE member SET register_date = NOW() WHERE last_name LIKE 'J%' AND first_name = 'MangKyu';

계속 고민했던 내용은 물리적인 락이 걸리는 대상이 인덱스 레코드라는 설명을 듣었을 때(세컨더리 인덱스를 사용하는 경우 물리적인 락은 세컨더리 인덱스 테이블 레코드에 걸림) 세컨더리 인덱스 레코드에 걸리면 해당 인덱스 테이블을 참조하지 않으면 락이 걸리지 모르는 것이 아니어서 동시성 문제가 발생하는 것이 아닌가였다. InnoDB 에서는 락에 대한 정보를 내부적으로 따로 관리하기 떄문에 아무리 물리적인 락이 세컨더리 인덱스 레코드에 걸린다고 해도 실제 데이터 레코드에 락이 걸린지 알 수 있는 것 같다.(제 생각이긴 합니다 )
2) 갭 락
인덱스 레코드에 잠금을 하는 것이 아닌 레코드와 레코드 사이의 인덱스 테이블 공간를 잠금하는 것이다. 갭 락은 넥스트 키 락의 일부로 사용되고 단독으로는 사용되지 않는다. 프라이머리 키나 유니크 인덱스 키의 경우 해당 값이 유일하기 때문에 동등 비교 시 갭 락이 사용되지 않음을 볼 수 있다.
갭 락의 사용은 팬텀 읽기의 문제를 방지할 수 있다.
3) 넥스트 키 락
레코드 락과 갭 락을 합쳐 놓은 형태의 잠금을 넥스트 키 락이라고 한다.

4) 자동 증가 락
하나의 테이블에 여러 개의 Insert문이 동시에 요청이 온 경우 해당 테이블이 auto_increment 라는 컬럼 속성을 가지고 있다면 각 레코드는 해당 속성의 값이 중복되지 않고 연속된 값을 가져야 한다.
이를 보장하기 위해서 InnoDB 에서는 내부적으로 AUTO_INCREMENT 락이라고 하는 테이블 수준의 잠금을 사용한다
홰당 락은 UPDATE 이나 DELETE 쿼리에는 사용되지 않고 오직 INSERT 문에서만 사용된다. InnoDB 의 다른 잠금과는 달리 AUTO_INCREMENT 값을 가져오는 순간만 락이 걸렸다가 즉시 해제된다.
AUTO_INCREMENT 락을 명시적으로 획득하거나 해제하는 방법은 없다.
자동 증가 락의 자동 방식에는 여러 종류가 있지만 나중에 학습하기로 기약한다.
6. MySQL의 트랜잭션 격리 수준
트랜잭션의 격리 수준(isolation level)이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다. 트랜잭션의 격리 수준은 크게 "READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE"의 4가지로 나뉜다. 뒤로 갈수록 격리(고립) 수준은 높아지며, 동시 처리 성능도 떨어지는 것이 일반적이다. 하지만, SERIALIZABLE 격리 수준이 아니라면 크게 성능의 개선이나 저하는 발생하지 않는다.
데이터베이스의 격리 수준을 이야기할 때면 같이 이야기되는 것이 부정합 문제이다. 각 부정합 문제는 각 격리 수준을 설명하면서 같이 설명한다.

1) READ UNCOMMITTED
제일 낮은 격리 수준인 READ UNCOMMITTED 는 커밋되지 않은 데이터가 보이는 문제인 DIRTY READ 부정합이 발생한다.

READ UNCOMITTED 격리 수준는 정합성에 많은 문제를 일으켜 거의 사용되지 않는 격리 수준이다.
2) READ COMMITTED
READ COMMITTED 격리 수준은 오라클 DBMS에서 기본으로 사용하는 격리 수준이며, 온라인 서비스에서 가장 많이 사용되는 격리 수준이다. 이 레벨에서는 커밋된 데이터만 다른 트랜잭션이 조회되기 때문에 DIRTY READ 부정합 문제가 발생하지 않는다.

위의 그림은 Inno DB 엔진에서 DIRTY READ 부정합 문제를 어떻게 해결했는지를 나타내는 그림이다. 기존 데이터를 언두 영역에 백업해 두고 다른 트랜잭션에서 해당 데이터를 조회할 때 백업된 데이터가 조회된다.
READ COMMITTED 격리 수준에서도 NON-REPEATABLE READ 라는 부정합 문제가 발생할 수 있다.

두 개의 트랜잭션이 있을 때 트랜잭셔 A 에는 트랜잭션 B 앞 뒤로 조회 쿼리를 요청할 때 트랜잭션 B의 결과로 인해 NON-REPEATABLE READ(반복할 수 없는 읽기) 부정합 문제가 발생한다. 이 부정합 문제는 웹 프로그램에서는 크게 문제가 되지 않는 것처럼 보일 수 있지만 금전적인 처리와 연결되면 큰 문제가 될 수 있다.
3) REPEATABLE READ
REPEATABLE READ(반복가능한 읽기)는 MySQL 의 InnoDB 스토리지 엔진에서 기본으로 사용되는 격리 수준이다. 이 격리 수준에서는 NON-REPEATABLE READ 부정합 문제가 발생하지 않는다. InnoDB 엔진이 언두 로그 영역이 있고 트랜잭션에 고유한 번호를 부여하기 때문이다. 동작 방식은 아래 그림과 같다.

그림 상에서 trx-id 가 10번인 트랜잭션이 데이터를 조회할 때 자신의 번호보다 작은 트랜잭션 아이디를 가진 트랜잭션의 변경 사항만 보기 때문에 NON-REPEATABLE READ 부정합 문제는 발생하지 않는다.
다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다 안 보였다 하는 현상이 PHANTOM READ(팬텀 리드) 부정합 문제가 해당 격리 수준에서 발견할 수 있다.

하지만, InnoDB 엔진은 갭 락과 넥스트 키 락 덕분에 PHANTOM READ 부정합 문제가 발생하지 않는다. 따라서, SERIALIZABLE 격리 수준을 사용할 필요가 없다.
4) SERIALIZABLE
읽기 작업 시에 공유 잠금을 걸어 다른 트랜잭션에서 해당 레코드를 변경하지 못한다. SERIALIZABLE 격리 수준에서는 PHANTOM READ 문제가 발생하지 않는다.(아직 일반적인 DBMS 에서 SERIALIZABLE 격리 수준에서 팬텀 리드 부정합 문제를 어떻게 해결했는지를 파악하기 힘들다.)
참고자료
- https://mangkyu.tistory.com/298
[MySQL] 스토리지 엔진 수준의 락의 종류(레코드 락, 갭 락, 넥스트 키 락, 자동 증가 락)
이번에는 스토리지 엔진 수준의 락의 종류에 대해 알아보도록 하겠습니다. 아래의 내용은 RealMySQL과 MySQL 공식 문서 등을 참고하여 작성하였으며, 모든 내용은 InnoDB를 기준으로 설명합니다. 1. 스
mangkyu.tistory.com
- Real MySQL